First commit
This commit is contained in:
commit
c6e2478c40
13918 changed files with 2303184 additions and 0 deletions
9
modules/README.txt
Normal file
9
modules/README.txt
Normal file
|
@ -0,0 +1,9 @@
|
|||
|
||||
This directory is reserved for core module files. Custom or contributed modules
|
||||
should be placed in their own subdirectory of the sites/all/modules directory.
|
||||
For multisite installations, they can also be placed in a subdirectory under
|
||||
/sites/{sitename}/modules/, where {sitename} is the name of your site (e.g.,
|
||||
www.example.com). This will allow you to more easily update Drupal core files.
|
||||
|
||||
For more details, see: http://drupal.org/node/176043
|
||||
|
36
modules/aggregator/aggregator-feed-source.tpl.php
Normal file
36
modules/aggregator/aggregator-feed-source.tpl.php
Normal file
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Default theme implementation to present the source of the feed.
|
||||
*
|
||||
* The contents are rendered above feed listings when browsing source feeds.
|
||||
* For example, "example.com/aggregator/sources/1".
|
||||
*
|
||||
* Available variables:
|
||||
* - $source_icon: Feed icon linked to the source. Rendered through
|
||||
* theme_feed_icon().
|
||||
* - $source_image: Image set by the feed source.
|
||||
* - $source_description: Description set by the feed source.
|
||||
* - $source_url: URL to the feed source.
|
||||
* - $last_checked: How long ago the feed was checked locally.
|
||||
*
|
||||
* @see template_preprocess()
|
||||
* @see template_preprocess_aggregator_feed_source()
|
||||
*
|
||||
* @ingroup themeable
|
||||
*/
|
||||
?>
|
||||
<div class="feed-source">
|
||||
<?php print $source_icon; ?>
|
||||
<?php print $source_image; ?>
|
||||
<div class="feed-description">
|
||||
<?php print $source_description; ?>
|
||||
</div>
|
||||
<div class="feed-url">
|
||||
<em><?php print t('URL:'); ?></em> <a href="<?php print $source_url; ?>"><?php print $source_url; ?></a>
|
||||
</div>
|
||||
<div class="feed-updated">
|
||||
<em><?php print t('Updated:'); ?></em> <?php print $last_checked; ?>
|
||||
</div>
|
||||
</div>
|
47
modules/aggregator/aggregator-item.tpl.php
Normal file
47
modules/aggregator/aggregator-item.tpl.php
Normal file
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Default theme implementation to format an individual feed item for display
|
||||
* on the aggregator page.
|
||||
*
|
||||
* Available variables:
|
||||
* - $feed_url: URL to the originating feed item.
|
||||
* - $feed_title: Title of the feed item.
|
||||
* - $source_url: Link to the local source section.
|
||||
* - $source_title: Title of the remote source.
|
||||
* - $source_date: Date the feed was posted on the remote source.
|
||||
* - $content: Feed item content.
|
||||
* - $categories: Linked categories assigned to the feed.
|
||||
*
|
||||
* @see template_preprocess()
|
||||
* @see template_preprocess_aggregator_item()
|
||||
*
|
||||
* @ingroup themeable
|
||||
*/
|
||||
?>
|
||||
<div class="feed-item">
|
||||
<h3 class="feed-item-title">
|
||||
<a href="<?php print $feed_url; ?>"><?php print $feed_title; ?></a>
|
||||
</h3>
|
||||
|
||||
<div class="feed-item-meta">
|
||||
<?php if ($source_url): ?>
|
||||
<a href="<?php print $source_url; ?>" class="feed-item-source"><?php print $source_title; ?></a> -
|
||||
<?php endif; ?>
|
||||
<span class="feed-item-date"><?php print $source_date; ?></span>
|
||||
</div>
|
||||
|
||||
<?php if ($content): ?>
|
||||
<div class="feed-item-body">
|
||||
<?php print $content; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($categories): ?>
|
||||
<div class="feed-item-categories">
|
||||
<?php print t('Categories'); ?>: <?php print implode(', ', $categories); ?>
|
||||
</div>
|
||||
<?php endif ;?>
|
||||
|
||||
</div>
|
7
modules/aggregator/aggregator-rtl.css
Normal file
7
modules/aggregator/aggregator-rtl.css
Normal file
|
@ -0,0 +1,7 @@
|
|||
/**
|
||||
* Right-to-Left styles for theme in the Aggregator module.
|
||||
*/
|
||||
|
||||
#aggregator .feed-source .feed-icon {
|
||||
float: left;
|
||||
}
|
25
modules/aggregator/aggregator-summary-item.tpl.php
Normal file
25
modules/aggregator/aggregator-summary-item.tpl.php
Normal file
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Default theme implementation to present a linked feed item for summaries.
|
||||
*
|
||||
* Available variables:
|
||||
* - $feed_url: Link to originating feed.
|
||||
* - $feed_title: Title of feed.
|
||||
* - $feed_age: Age of remote feed.
|
||||
* - $source_url: Link to remote source.
|
||||
* - $source_title: Locally set title for the source.
|
||||
*
|
||||
* @see template_preprocess()
|
||||
* @see template_preprocess_aggregator_summary_item()
|
||||
*
|
||||
* @ingroup themeable
|
||||
*/
|
||||
?>
|
||||
<a href="<?php print $feed_url; ?>"><?php print $feed_title; ?></a>
|
||||
<span class="age"><?php print $feed_age; ?></span>
|
||||
|
||||
<?php if ($source_url): ?>,
|
||||
<span class="source"><a href="<?php print $source_url; ?>"><?php print $source_title; ?></a></span>
|
||||
<?php endif; ?>
|
25
modules/aggregator/aggregator-summary-items.tpl.php
Normal file
25
modules/aggregator/aggregator-summary-items.tpl.php
Normal file
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Default theme implementation to present feeds as list items.
|
||||
*
|
||||
* Each iteration generates a single feed source or category.
|
||||
*
|
||||
* Available variables:
|
||||
* - $title: Title of the feed or category.
|
||||
* - $summary_list: Unordered list of linked feed items generated through
|
||||
* theme_item_list().
|
||||
* - $source_url: URL to the local source or category.
|
||||
*
|
||||
* @see template_preprocess()
|
||||
* @see template_preprocess_aggregator_summary_items()
|
||||
*
|
||||
* @ingroup themeable
|
||||
*/
|
||||
?>
|
||||
<h3><?php print $title; ?></h3>
|
||||
<?php print $summary_list; ?>
|
||||
<div class="links">
|
||||
<a href="<?php print $source_url; ?>"><?php print t('More'); ?></a>
|
||||
</div>
|
20
modules/aggregator/aggregator-wrapper.tpl.php
Normal file
20
modules/aggregator/aggregator-wrapper.tpl.php
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Default theme implementation to wrap aggregator content.
|
||||
*
|
||||
* Available variables:
|
||||
* - $content: All aggregator content.
|
||||
* - $page: Pager links rendered through theme_pager().
|
||||
*
|
||||
* @see template_preprocess()
|
||||
* @see template_preprocess_aggregator_wrapper()
|
||||
*
|
||||
* @ingroup themeable
|
||||
*/
|
||||
?>
|
||||
<div id="aggregator">
|
||||
<?php print $content; ?>
|
||||
<?php print $pager; ?>
|
||||
</div>
|
633
modules/aggregator/aggregator.admin.inc
Normal file
633
modules/aggregator/aggregator.admin.inc
Normal file
|
@ -0,0 +1,633 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Administration page callbacks for the Aggregator module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Page callback: Displays the Aggregator module administration page.
|
||||
*/
|
||||
function aggregator_admin_overview() {
|
||||
return aggregator_view();
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the aggregator administration page.
|
||||
*
|
||||
* @return
|
||||
* A HTML-formatted string with administration page content.
|
||||
*/
|
||||
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'), array('data' => t('Operations'), 'colspan' => '3'));
|
||||
$rows = array();
|
||||
foreach ($result as $feed) {
|
||||
$rows[] = array(
|
||||
l($feed->title, "aggregator/sources/$feed->fid"),
|
||||
format_plural($feed->items, '1 item', '@count items'),
|
||||
($feed->checked ? t('@time ago', array('@time' => format_interval(REQUEST_TIME - $feed->checked))) : t('never')),
|
||||
($feed->checked && $feed->refresh ? t('%time left', array('%time' => format_interval($feed->checked + $feed->refresh - REQUEST_TIME))) : t('never')),
|
||||
l(t('edit'), "admin/config/services/aggregator/edit/feed/$feed->fid"),
|
||||
l(t('remove items'), "admin/config/services/aggregator/remove/$feed->fid"),
|
||||
l(t('update items'), "admin/config/services/aggregator/update/$feed->fid", array('query' => array('token' => drupal_get_token("aggregator/update/$feed->fid")))),
|
||||
);
|
||||
}
|
||||
$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) {
|
||||
$rows[] = array(l($category->title, "aggregator/categories/$category->cid"), format_plural($category->items, '1 item', '@count items'), l(t('edit'), "admin/config/services/aggregator/edit/category/$category->cid"));
|
||||
}
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Form constructor for adding and editing feed sources.
|
||||
*
|
||||
* @param $feed
|
||||
* (optional) If editing a feed, the feed to edit as a PHP stdClass value; if
|
||||
* adding a new feed, NULL. Defaults to NULL.
|
||||
*
|
||||
* @ingroup forms
|
||||
* @see aggregator_form_feed_validate()
|
||||
* @see aggregator_form_feed_submit()
|
||||
*/
|
||||
function aggregator_form_feed($form, &$form_state, stdClass $feed = NULL) {
|
||||
$period = drupal_map_assoc(array(900, 1800, 3600, 7200, 10800, 21600, 32400, 43200, 64800, 86400, 172800, 259200, 604800, 1209600, 2419200), 'format_interval');
|
||||
$period[AGGREGATOR_CLEAR_NEVER] = t('Never');
|
||||
|
||||
$form['title'] = array('#type' => 'textfield',
|
||||
'#title' => t('Title'),
|
||||
'#default_value' => isset($feed->title) ? $feed->title : '',
|
||||
'#maxlength' => 255,
|
||||
'#description' => t('The name of the feed (or the name of the website providing the feed).'),
|
||||
'#required' => TRUE,
|
||||
);
|
||||
$form['url'] = array('#type' => 'textfield',
|
||||
'#title' => t('URL'),
|
||||
'#default_value' => isset($feed->url) ? $feed->url : '',
|
||||
'#maxlength' => NULL,
|
||||
'#description' => t('The fully-qualified URL of the feed.'),
|
||||
'#required' => TRUE,
|
||||
);
|
||||
$form['refresh'] = array('#type' => 'select',
|
||||
'#title' => t('Update interval'),
|
||||
'#default_value' => isset($feed->refresh) ? $feed->refresh : 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' => isset($feed->block) ? $feed->block : 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 this 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 this feed's block. If you choose '0' this feed's block will be disabled.", array('@block-admin' => url('admin/structure/block'))),
|
||||
);
|
||||
|
||||
// Handling of categories.
|
||||
$options = array();
|
||||
$values = array();
|
||||
$categories = db_query('SELECT c.cid, c.title, f.fid FROM {aggregator_category} c LEFT JOIN {aggregator_category_feed} f ON c.cid = f.cid AND f.fid = :fid ORDER BY title', array(':fid' => isset($feed->fid) ? $feed->fid : NULL));
|
||||
foreach ($categories as $category) {
|
||||
$options[$category->cid] = check_plain($category->title);
|
||||
if ($category->fid) $values[] = $category->cid;
|
||||
}
|
||||
|
||||
if ($options) {
|
||||
$form['category'] = array(
|
||||
'#type' => 'checkboxes',
|
||||
'#title' => t('Categorize news items'),
|
||||
'#default_value' => $values,
|
||||
'#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('Save'),
|
||||
);
|
||||
if (!empty($feed->fid)) {
|
||||
$form['actions']['delete'] = array(
|
||||
'#type' => 'submit',
|
||||
'#value' => t('Delete'),
|
||||
);
|
||||
$form['fid'] = array(
|
||||
'#type' => 'hidden',
|
||||
'#value' => $feed->fid,
|
||||
);
|
||||
}
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Form validation handler for aggregator_form_feed().
|
||||
*
|
||||
* @see aggregator_form_feed_submit()
|
||||
*/
|
||||
function aggregator_form_feed_validate($form, &$form_state) {
|
||||
if ($form_state['values']['op'] == t('Save')) {
|
||||
// Ensure URL is valid.
|
||||
if (!valid_url($form_state['values']['url'], TRUE)) {
|
||||
form_set_error('url', t('The URL %url is invalid. Enter a fully-qualified URL, such as http://www.example.com/feed.xml.', array('%url' => $form_state['values']['url'])));
|
||||
}
|
||||
// Check for duplicate titles.
|
||||
if (isset($form_state['values']['fid'])) {
|
||||
$result = db_query("SELECT title, url FROM {aggregator_feed} WHERE (title = :title OR url = :url) AND fid <> :fid", array(':title' => $form_state['values']['title'], ':url' => $form_state['values']['url'], ':fid' => $form_state['values']['fid']));
|
||||
}
|
||||
else {
|
||||
$result = db_query("SELECT title, url FROM {aggregator_feed} WHERE title = :title OR url = :url", array(':title' => $form_state['values']['title'], ':url' => $form_state['values']['url']));
|
||||
}
|
||||
foreach ($result as $feed) {
|
||||
if (strcasecmp($feed->title, $form_state['values']['title']) == 0) {
|
||||
form_set_error('title', t('A feed named %feed already exists. Enter a unique title.', array('%feed' => $form_state['values']['title'])));
|
||||
}
|
||||
if (strcasecmp($feed->url, $form_state['values']['url']) == 0) {
|
||||
form_set_error('url', t('A feed with this URL %url already exists. Enter a unique URL.', array('%url' => $form_state['values']['url'])));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Form submission handler for aggregator_form_feed().
|
||||
*
|
||||
* @see aggregator_form_feed_validate()
|
||||
*
|
||||
* @todo Add delete confirmation dialog.
|
||||
*/
|
||||
function aggregator_form_feed_submit($form, &$form_state) {
|
||||
if ($form_state['values']['op'] == t('Delete')) {
|
||||
$title = $form_state['values']['title'];
|
||||
// Unset the title.
|
||||
unset($form_state['values']['title']);
|
||||
}
|
||||
aggregator_save_feed($form_state['values']);
|
||||
if (isset($form_state['values']['fid'])) {
|
||||
if (isset($form_state['values']['title'])) {
|
||||
drupal_set_message(t('The feed %feed has been updated.', array('%feed' => $form_state['values']['title'])));
|
||||
if (arg(0) == 'admin') {
|
||||
$form_state['redirect'] = 'admin/config/services/aggregator/';
|
||||
return;
|
||||
}
|
||||
else {
|
||||
$form_state['redirect'] = 'aggregator/sources/' . $form_state['values']['fid'];
|
||||
return;
|
||||
}
|
||||
}
|
||||
else {
|
||||
watchdog('aggregator', 'Feed %feed deleted.', array('%feed' => $title));
|
||||
drupal_set_message(t('The feed %feed has been deleted.', array('%feed' => $title)));
|
||||
if (arg(0) == 'admin') {
|
||||
$form_state['redirect'] = 'admin/config/services/aggregator/';
|
||||
return;
|
||||
}
|
||||
else {
|
||||
$form_state['redirect'] = 'aggregator/sources/';
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
watchdog('aggregator', 'Feed %feed added.', array('%feed' => $form_state['values']['title']), WATCHDOG_NOTICE, l(t('view'), 'admin/config/services/aggregator'));
|
||||
drupal_set_message(t('The feed %feed has been added.', array('%feed' => $form_state['values']['title'])));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a feed.
|
||||
*
|
||||
* @param $feed
|
||||
* An associative array describing the feed to be cleared.
|
||||
*
|
||||
* @see aggregator_admin_remove_feed_submit()
|
||||
*/
|
||||
function aggregator_admin_remove_feed($form, $form_state, $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->title)),
|
||||
'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_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' => 'textfield',
|
||||
'#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.'));
|
||||
}
|
||||
|
||||
// Validate the URL, if one was entered.
|
||||
if (!empty($form_state['values']['remote']) && !valid_url($form_state['values']['remote'], TRUE)) {
|
||||
form_set_error('remote', t('This URL is not valid.'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
$feeds = _aggregator_parse_opml($data);
|
||||
if (empty($feeds)) {
|
||||
drupal_set_message(t('No new feed has been added.'));
|
||||
return;
|
||||
}
|
||||
|
||||
$form_state['values']['op'] = t('Save');
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
$form_state['values']['title'] = $feed['title'];
|
||||
$form_state['values']['url'] = $feed['url'];
|
||||
drupal_form_submit('aggregator_form_feed', $form_state);
|
||||
}
|
||||
|
||||
$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 $feed
|
||||
* An object describing the feed to be refreshed.
|
||||
*/
|
||||
function aggregator_admin_refresh_feed($feed) {
|
||||
if (!isset($_GET['token']) || !drupal_valid_token($_GET['token'], 'aggregator/update/' . $feed->fid)) {
|
||||
return MENU_ACCESS_DENIED;
|
||||
}
|
||||
aggregator_refresh($feed);
|
||||
drupal_goto('admin/config/services/aggregator');
|
||||
}
|
||||
|
||||
/**
|
||||
* Form constructor for the aggregator system settings.
|
||||
*
|
||||
* @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' => variable_get('aggregator_allowed_html_tags', '<a> <b> <br> <dd> <dl> <dt> <em> <i> <li> <ol> <p> <strong> <u> <ul>'),
|
||||
'#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.
|
||||
$fetchers = module_implements('aggregator_fetch');
|
||||
foreach ($fetchers as $k => $module) {
|
||||
if ($info = module_invoke($module, 'aggregator_fetch_info')) {
|
||||
$label = $info['title'] . ' <span class="description">' . $info['description'] . '</span>';
|
||||
}
|
||||
else {
|
||||
$label = $module;
|
||||
}
|
||||
unset($fetchers[$k]);
|
||||
$fetchers[$module] = $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' => variable_get('aggregator_fetcher', 'aggregator'),
|
||||
);
|
||||
}
|
||||
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' => variable_get('aggregator_parser', 'aggregator'),
|
||||
);
|
||||
}
|
||||
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' => variable_get('aggregator_processors', array('aggregator')),
|
||||
);
|
||||
}
|
||||
if (count($basic_conf)) {
|
||||
$form['basic_conf'] = array(
|
||||
'#type' => 'fieldset',
|
||||
'#title' => t('Basic configuration'),
|
||||
'#description' => t('For most aggregation tasks, the default settings are fine.'),
|
||||
'#collapsible' => TRUE,
|
||||
'#collapsed' => FALSE,
|
||||
);
|
||||
$form['basic_conf'] += $basic_conf;
|
||||
}
|
||||
|
||||
// Implementing modules will expect an array at $form['modules'].
|
||||
$form['modules'] = array();
|
||||
|
||||
$form['actions'] = array('#type' => 'actions');
|
||||
$form['actions']['submit'] = array(
|
||||
'#type' => 'submit',
|
||||
'#value' => t('Save configuration'),
|
||||
);
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Form submission handler for aggregator_admin_form().
|
||||
*/
|
||||
function aggregator_admin_form_submit($form, &$form_state) {
|
||||
if (isset($form_state['values']['aggregator_processors'])) {
|
||||
$form_state['values']['aggregator_processors'] = array_filter($form_state['values']['aggregator_processors']);
|
||||
}
|
||||
system_settings_form_submit($form, $form_state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Form constructor to add/edit/delete aggregator categories.
|
||||
*
|
||||
* @param $edit
|
||||
* An associative array 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_form_category_validate()
|
||||
* @see aggregator_form_category_submit()
|
||||
*/
|
||||
function aggregator_form_category($form, &$form_state, $edit = array('title' => '', 'description' => '', 'cid' => NULL)) {
|
||||
$form['title'] = array('#type' => 'textfield',
|
||||
'#title' => t('Title'),
|
||||
'#default_value' => $edit['title'],
|
||||
'#maxlength' => 64,
|
||||
'#required' => TRUE,
|
||||
);
|
||||
$form['description'] = array('#type' => 'textarea',
|
||||
'#title' => t('Description'),
|
||||
'#default_value' => $edit['description'],
|
||||
);
|
||||
$form['actions'] = array('#type' => 'actions');
|
||||
$form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save'));
|
||||
if ($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) {
|
||||
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'])));
|
||||
}
|
||||
}
|
220
modules/aggregator/aggregator.api.php
Normal file
220
modules/aggregator/aggregator.api.php
Normal file
|
@ -0,0 +1,220 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Documentation for aggregator API.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @addtogroup hooks
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create an alternative fetcher for aggregator.module.
|
||||
*
|
||||
* A fetcher downloads feed data to a Drupal site. The fetcher is called at the
|
||||
* first of the three aggregation stages: first, data is downloaded by the
|
||||
* active fetcher; second, it is converted to a common format by the active
|
||||
* parser; and finally, it is passed to all active processors, which manipulate
|
||||
* or store the data.
|
||||
*
|
||||
* Modules that define this hook can be set as the active fetcher within the
|
||||
* configuration page. Only one fetcher can be active at a time.
|
||||
*
|
||||
* @param $feed
|
||||
* A feed object representing the resource to be downloaded. $feed->url
|
||||
* contains the link to the feed. Download the data at the URL and expose it
|
||||
* to other modules by attaching it to $feed->source_string.
|
||||
*
|
||||
* @return
|
||||
* TRUE if fetching was successful, FALSE otherwise.
|
||||
*
|
||||
* @see hook_aggregator_fetch_info()
|
||||
* @see hook_aggregator_parse()
|
||||
* @see hook_aggregator_process()
|
||||
*
|
||||
* @ingroup aggregator
|
||||
*/
|
||||
function hook_aggregator_fetch($feed) {
|
||||
$feed->source_string = mymodule_fetch($feed->url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the title and short description of your fetcher.
|
||||
*
|
||||
* The title and the description provided are shown within the configuration
|
||||
* page. Use as title the human readable name of the fetcher and as description
|
||||
* a brief (40 to 80 characters) explanation of the fetcher's functionality.
|
||||
*
|
||||
* This hook is only called if your module implements hook_aggregator_fetch().
|
||||
* If this hook is not implemented aggregator will use your module's file name
|
||||
* as title and there will be no description.
|
||||
*
|
||||
* @return
|
||||
* An associative array defining a title and a description string.
|
||||
*
|
||||
* @see hook_aggregator_fetch()
|
||||
*
|
||||
* @ingroup aggregator
|
||||
*/
|
||||
function hook_aggregator_fetch_info() {
|
||||
return array(
|
||||
'title' => t('Default fetcher'),
|
||||
'description' => t('Default fetcher for resources available by URL.'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an alternative parser for aggregator module.
|
||||
*
|
||||
* A parser converts feed item data to a common format. The parser is called
|
||||
* at the second of the three aggregation stages: first, data is downloaded
|
||||
* by the active fetcher; second, it is converted to a common format by the
|
||||
* active parser; and finally, it is passed to all active processors which
|
||||
* manipulate or store the data.
|
||||
*
|
||||
* Modules that define this hook can be set as the active parser within the
|
||||
* configuration page. Only one parser can be active at a time.
|
||||
*
|
||||
* @param $feed
|
||||
* An object describing the resource to be parsed. $feed->source_string
|
||||
* contains the raw feed data. The hook implementation should parse this data
|
||||
* and add the following properties to the $feed object:
|
||||
* - description: The human-readable description of the feed.
|
||||
* - link: A full URL that directly relates to the feed.
|
||||
* - image: An image URL used to display an image of the feed.
|
||||
* - etag: An entity tag from the HTTP header used for cache validation to
|
||||
* determine if the content has been changed.
|
||||
* - modified: The UNIX timestamp when the feed was last modified.
|
||||
* - items: An array of feed items. The common format for a single feed item
|
||||
* is an associative array containing:
|
||||
* - title: The human-readable title of the feed item.
|
||||
* - description: The full body text of the item or a summary.
|
||||
* - timestamp: The UNIX timestamp when the feed item was last published.
|
||||
* - author: The author of the feed item.
|
||||
* - guid: The global unique identifier (GUID) string that uniquely
|
||||
* identifies the item. If not available, the link is used to identify
|
||||
* the item.
|
||||
* - link: A full URL to the individual feed item.
|
||||
*
|
||||
* @return
|
||||
* TRUE if parsing was successful, FALSE otherwise.
|
||||
*
|
||||
* @see hook_aggregator_parse_info()
|
||||
* @see hook_aggregator_fetch()
|
||||
* @see hook_aggregator_process()
|
||||
*
|
||||
* @ingroup aggregator
|
||||
*/
|
||||
function hook_aggregator_parse($feed) {
|
||||
if ($items = mymodule_parse($feed->source_string)) {
|
||||
$feed->items = $items;
|
||||
return TRUE;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the title and short description of your parser.
|
||||
*
|
||||
* The title and the description provided are shown within the configuration
|
||||
* page. Use as title the human readable name of the parser and as description
|
||||
* a brief (40 to 80 characters) explanation of the parser's functionality.
|
||||
*
|
||||
* This hook is only called if your module implements hook_aggregator_parse().
|
||||
* If this hook is not implemented aggregator will use your module's file name
|
||||
* as title and there will be no description.
|
||||
*
|
||||
* @return
|
||||
* An associative array defining a title and a description string.
|
||||
*
|
||||
* @see hook_aggregator_parse()
|
||||
*
|
||||
* @ingroup aggregator
|
||||
*/
|
||||
function hook_aggregator_parse_info() {
|
||||
return array(
|
||||
'title' => t('Default parser'),
|
||||
'description' => t('Default parser for RSS, Atom and RDF feeds.'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a processor for aggregator.module.
|
||||
*
|
||||
* A processor acts on parsed feed data. Active processors are called at the
|
||||
* third and last of the aggregation stages: first, data is downloaded by the
|
||||
* active fetcher; second, it is converted to a common format by the active
|
||||
* parser; and finally, it is passed to all active processors that manipulate or
|
||||
* store the data.
|
||||
*
|
||||
* Modules that define this hook can be activated as a processor within the
|
||||
* configuration page.
|
||||
*
|
||||
* @param $feed
|
||||
* A feed object representing the resource to be processed. $feed->items
|
||||
* contains an array of feed items downloaded and parsed at the parsing stage.
|
||||
* See hook_aggregator_parse() for the basic format of a single item in the
|
||||
* $feed->items array. For the exact format refer to the particular parser in
|
||||
* use.
|
||||
*
|
||||
* @see hook_aggregator_process_info()
|
||||
* @see hook_aggregator_fetch()
|
||||
* @see hook_aggregator_parse()
|
||||
*
|
||||
* @ingroup aggregator
|
||||
*/
|
||||
function hook_aggregator_process($feed) {
|
||||
foreach ($feed->items as $item) {
|
||||
mymodule_save($item);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the title and short description of your processor.
|
||||
*
|
||||
* The title and the description provided are shown within the configuration
|
||||
* page. Use as title the natural name of the processor and as description a
|
||||
* brief (40 to 80 characters) explanation of the functionality.
|
||||
*
|
||||
* This hook is only called if your module implements hook_aggregator_process().
|
||||
* If this hook is not implemented aggregator will use your module's file name
|
||||
* as title and there will be no description.
|
||||
*
|
||||
* @return
|
||||
* An associative array defining a title and a description string.
|
||||
*
|
||||
* @see hook_aggregator_process()
|
||||
*
|
||||
* @ingroup aggregator
|
||||
*/
|
||||
function hook_aggregator_process_info() {
|
||||
return array(
|
||||
'title' => t('Default processor'),
|
||||
'description' => t('Creates lightweight records of feed items.'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove stored feed data.
|
||||
*
|
||||
* Aggregator calls this hook if either a feed is deleted or a user clicks on
|
||||
* "remove items".
|
||||
*
|
||||
* If your module stores feed items for example on hook_aggregator_process() it
|
||||
* is recommended to implement this hook and to remove data related to $feed
|
||||
* when called.
|
||||
*
|
||||
* @param $feed
|
||||
* The $feed object whose items are being removed.
|
||||
*
|
||||
* @ingroup aggregator
|
||||
*/
|
||||
function hook_aggregator_remove($feed) {
|
||||
mymodule_remove_items($feed->fid);
|
||||
}
|
||||
|
||||
/**
|
||||
* @} End of "addtogroup hooks".
|
||||
*/
|
41
modules/aggregator/aggregator.css
Normal file
41
modules/aggregator/aggregator.css
Normal file
|
@ -0,0 +1,41 @@
|
|||
/**
|
||||
* Styles for theme in the Aggregator module.
|
||||
*/
|
||||
|
||||
#aggregator .feed-source .feed-title {
|
||||
margin-top: 0;
|
||||
}
|
||||
#aggregator .feed-source .feed-image img {
|
||||
margin-bottom: 0.75em;
|
||||
}
|
||||
#aggregator .feed-source .feed-icon {
|
||||
float: right; /* LTR */
|
||||
display: block;
|
||||
}
|
||||
#aggregator .feed-item {
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
#aggregator .feed-item-title {
|
||||
margin-bottom: 0;
|
||||
font-size: 1.3em;
|
||||
}
|
||||
#aggregator .feed-item-meta,
|
||||
#aggregator .feed-item-body {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
#aggregator .feed-item-categories {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
#aggregator td {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
#aggregator td.categorize-item {
|
||||
white-space: nowrap;
|
||||
}
|
||||
#aggregator .categorize-item .news-item .body {
|
||||
margin-top: 0;
|
||||
}
|
||||
#aggregator .categorize-item h3 {
|
||||
margin-bottom: 1em;
|
||||
margin-top: 0;
|
||||
}
|
61
modules/aggregator/aggregator.fetcher.inc
Normal file
61
modules/aggregator/aggregator.fetcher.inc
Normal file
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Fetcher functions for the aggregator module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Implements hook_aggregator_fetch_info().
|
||||
*/
|
||||
function aggregator_aggregator_fetch_info() {
|
||||
return array(
|
||||
'title' => t('Default fetcher'),
|
||||
'description' => t('Downloads data from a URL using Drupal\'s HTTP request handler.'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_aggregator_fetch().
|
||||
*/
|
||||
function aggregator_aggregator_fetch($feed) {
|
||||
$feed->source_string = FALSE;
|
||||
|
||||
// Generate conditional GET headers.
|
||||
$headers = array();
|
||||
if ($feed->etag) {
|
||||
$headers['If-None-Match'] = $feed->etag;
|
||||
}
|
||||
if ($feed->modified) {
|
||||
$headers['If-Modified-Since'] = gmdate(DATE_RFC7231, $feed->modified);
|
||||
}
|
||||
|
||||
// Request feed.
|
||||
$result = drupal_http_request($feed->url, array('headers' => $headers));
|
||||
|
||||
// Process HTTP response code.
|
||||
switch ($result->code) {
|
||||
case 304:
|
||||
break;
|
||||
case 301:
|
||||
$feed->url = $result->redirect_url;
|
||||
// Do not break here.
|
||||
case 200:
|
||||
case 302:
|
||||
case 307:
|
||||
if (!isset($result->data)) {
|
||||
$result->data = '';
|
||||
}
|
||||
if (!isset($result->headers)) {
|
||||
$result->headers = array();
|
||||
}
|
||||
$feed->source_string = $result->data;
|
||||
$feed->http_headers = $result->headers;
|
||||
break;
|
||||
default:
|
||||
watchdog('aggregator', 'The feed from %site seems to be broken, due to "%error".', array('%site' => $feed->title, '%error' => $result->code . ' ' . $result->error), WATCHDOG_WARNING);
|
||||
drupal_set_message(t('The feed from %site seems to be broken, because of error "%error".', array('%site' => $feed->title, '%error' => $result->code . ' ' . $result->error)));
|
||||
}
|
||||
|
||||
return $feed->source_string === FALSE ? FALSE : TRUE;
|
||||
}
|
14
modules/aggregator/aggregator.info
Normal file
14
modules/aggregator/aggregator.info
Normal file
|
@ -0,0 +1,14 @@
|
|||
name = Aggregator
|
||||
description = "Aggregates syndicated content (RSS, RDF, and Atom feeds)."
|
||||
package = Core
|
||||
version = VERSION
|
||||
core = 7.x
|
||||
files[] = aggregator.test
|
||||
configure = admin/config/services/aggregator/settings
|
||||
stylesheets[all][] = aggregator.css
|
||||
|
||||
; Information added by Drupal.org packaging script on 2017-06-21
|
||||
version = "7.56"
|
||||
project = "drupal"
|
||||
datestamp = "1498069849"
|
||||
|
340
modules/aggregator/aggregator.install
Normal file
340
modules/aggregator/aggregator.install
Normal file
|
@ -0,0 +1,340 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Install, update and uninstall functions for the aggregator module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Implements hook_uninstall().
|
||||
*/
|
||||
function aggregator_uninstall() {
|
||||
variable_del('aggregator_allowed_html_tags');
|
||||
variable_del('aggregator_summary_items');
|
||||
variable_del('aggregator_clear');
|
||||
variable_del('aggregator_category_selector');
|
||||
variable_del('aggregator_fetcher');
|
||||
variable_del('aggregator_parser');
|
||||
variable_del('aggregator_processors');
|
||||
variable_del('aggregator_teaser_length');
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_schema().
|
||||
*/
|
||||
function aggregator_schema() {
|
||||
$schema['aggregator_category'] = array(
|
||||
'description' => 'Stores categories for aggregator feeds and feed items.',
|
||||
'fields' => array(
|
||||
'cid' => array(
|
||||
'type' => 'serial',
|
||||
'not null' => TRUE,
|
||||
'description' => 'Primary Key: Unique aggregator category ID.',
|
||||
),
|
||||
'title' => array(
|
||||
'type' => 'varchar',
|
||||
'length' => 255,
|
||||
'not null' => TRUE,
|
||||
'default' => '',
|
||||
'description' => 'Title of the category.',
|
||||
),
|
||||
'description' => array(
|
||||
'type' => 'text',
|
||||
'not null' => TRUE,
|
||||
'size' => 'big',
|
||||
'description' => 'Description of the category',
|
||||
),
|
||||
'block' => array(
|
||||
'type' => 'int',
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
'size' => 'tiny',
|
||||
'description' => 'The number of recent items to show within the category block.',
|
||||
)
|
||||
),
|
||||
'primary key' => array('cid'),
|
||||
'unique keys' => array(
|
||||
'title' => array('title'),
|
||||
),
|
||||
);
|
||||
|
||||
$schema['aggregator_category_feed'] = array(
|
||||
'description' => 'Bridge table; maps feeds to categories.',
|
||||
'fields' => array(
|
||||
'fid' => array(
|
||||
'type' => 'int',
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
'description' => "The feed's {aggregator_feed}.fid.",
|
||||
),
|
||||
'cid' => array(
|
||||
'type' => 'int',
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
'description' => 'The {aggregator_category}.cid to which the feed is being assigned.',
|
||||
)
|
||||
),
|
||||
'primary key' => array('cid', 'fid'),
|
||||
'indexes' => array(
|
||||
'fid' => array('fid'),
|
||||
),
|
||||
'foreign keys' => array(
|
||||
'aggregator_category' => array(
|
||||
'table' => 'aggregator_category',
|
||||
'columns' => array('cid' => 'cid'),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
$schema['aggregator_category_item'] = array(
|
||||
'description' => 'Bridge table; maps feed items to categories.',
|
||||
'fields' => array(
|
||||
'iid' => array(
|
||||
'type' => 'int',
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
'description' => "The feed item's {aggregator_item}.iid.",
|
||||
),
|
||||
'cid' => array(
|
||||
'type' => 'int',
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
'description' => 'The {aggregator_category}.cid to which the feed item is being assigned.',
|
||||
)
|
||||
),
|
||||
'primary key' => array('cid', 'iid'),
|
||||
'indexes' => array(
|
||||
'iid' => array('iid'),
|
||||
),
|
||||
'foreign keys' => array(
|
||||
'aggregator_category' => array(
|
||||
'table' => 'aggregator_category',
|
||||
'columns' => array('cid' => 'cid'),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
$schema['aggregator_feed'] = array(
|
||||
'description' => 'Stores feeds to be parsed by the aggregator.',
|
||||
'fields' => array(
|
||||
'fid' => array(
|
||||
'type' => 'serial',
|
||||
'not null' => TRUE,
|
||||
'description' => 'Primary Key: Unique feed ID.',
|
||||
),
|
||||
'title' => array(
|
||||
'type' => 'varchar',
|
||||
'length' => 255,
|
||||
'not null' => TRUE,
|
||||
'default' => '',
|
||||
'description' => 'Title of the feed.',
|
||||
),
|
||||
'url' => array(
|
||||
'type' => 'text',
|
||||
'not null' => TRUE,
|
||||
'description' => 'URL to the feed.',
|
||||
),
|
||||
'refresh' => array(
|
||||
'type' => 'int',
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
'description' => 'How often to check for new feed items, in seconds.',
|
||||
),
|
||||
'checked' => array(
|
||||
'type' => 'int',
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
'description' => 'Last time feed was checked for new items, as Unix timestamp.',
|
||||
),
|
||||
'queued' => array(
|
||||
'type' => 'int',
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
'description' => 'Time when this feed was queued for refresh, 0 if not queued.',
|
||||
),
|
||||
'link' => array(
|
||||
'type' => 'text',
|
||||
'not null' => TRUE,
|
||||
'description' => 'The parent website of the feed; comes from the <link> element in the feed.',
|
||||
),
|
||||
'description' => array(
|
||||
'type' => 'text',
|
||||
'not null' => TRUE,
|
||||
'size' => 'big',
|
||||
'description' => "The parent website's description; comes from the <description> element in the feed.",
|
||||
),
|
||||
'image' => array(
|
||||
'type' => 'text',
|
||||
'not null' => TRUE,
|
||||
'size' => 'big',
|
||||
'description' => 'An image representing the feed.',
|
||||
),
|
||||
'hash' => array(
|
||||
'type' => 'varchar',
|
||||
'length' => 64,
|
||||
'not null' => TRUE,
|
||||
'default' => '',
|
||||
'description' => 'Calculated hash of the feed data, used for validating cache.',
|
||||
),
|
||||
'etag' => array(
|
||||
'type' => 'varchar',
|
||||
'length' => 255,
|
||||
'not null' => TRUE,
|
||||
'default' => '',
|
||||
'description' => 'Entity tag HTTP response header, used for validating cache.',
|
||||
),
|
||||
'modified' => array(
|
||||
'type' => 'int',
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
'description' => 'When the feed was last modified, as a Unix timestamp.',
|
||||
),
|
||||
'block' => array(
|
||||
'type' => 'int',
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
'size' => 'tiny',
|
||||
'description' => "Number of items to display in the feed's block.",
|
||||
)
|
||||
),
|
||||
'primary key' => array('fid'),
|
||||
'indexes' => array(
|
||||
'url' => array(array('url', 255)),
|
||||
'queued' => array('queued'),
|
||||
),
|
||||
'unique keys' => array(
|
||||
'title' => array('title'),
|
||||
),
|
||||
);
|
||||
|
||||
$schema['aggregator_item'] = array(
|
||||
'description' => 'Stores the individual items imported from feeds.',
|
||||
'fields' => array(
|
||||
'iid' => array(
|
||||
'type' => 'serial',
|
||||
'not null' => TRUE,
|
||||
'description' => 'Primary Key: Unique ID for feed item.',
|
||||
),
|
||||
'fid' => array(
|
||||
'type' => 'int',
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
'description' => 'The {aggregator_feed}.fid to which this item belongs.',
|
||||
),
|
||||
'title' => array(
|
||||
'type' => 'varchar',
|
||||
'length' => 255,
|
||||
'not null' => TRUE,
|
||||
'default' => '',
|
||||
'description' => 'Title of the feed item.',
|
||||
),
|
||||
'link' => array(
|
||||
'type' => 'text',
|
||||
'not null' => TRUE,
|
||||
'description' => 'Link to the feed item.',
|
||||
),
|
||||
'author' => array(
|
||||
'type' => 'varchar',
|
||||
'length' => 255,
|
||||
'not null' => TRUE,
|
||||
'default' => '',
|
||||
'description' => 'Author of the feed item.',
|
||||
),
|
||||
'description' => array(
|
||||
'type' => 'text',
|
||||
'not null' => TRUE,
|
||||
'size' => 'big',
|
||||
'description' => 'Body of the feed item.',
|
||||
),
|
||||
'timestamp' => array(
|
||||
'type' => 'int',
|
||||
'not null' => FALSE,
|
||||
'description' => 'Posted date of the feed item, as a Unix timestamp.',
|
||||
),
|
||||
'guid' => array(
|
||||
'type' => 'text',
|
||||
'not null' => TRUE,
|
||||
'description' => 'Unique identifier for the feed item.',
|
||||
)
|
||||
),
|
||||
'primary key' => array('iid'),
|
||||
'indexes' => array(
|
||||
'fid' => array('fid'),
|
||||
'timestamp' => array('timestamp'),
|
||||
),
|
||||
'foreign keys' => array(
|
||||
'aggregator_feed' => array(
|
||||
'table' => 'aggregator_feed',
|
||||
'columns' => array('fid' => 'fid'),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return $schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* @addtogroup updates-6.x-to-7.x
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* Add hash column to aggregator_feed table.
|
||||
*/
|
||||
function aggregator_update_7000() {
|
||||
db_add_field('aggregator_feed', 'hash', array('type' => 'varchar', 'length' => 64, 'not null' => TRUE, 'default' => ''));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add aggregator teaser length to settings from old global default teaser length
|
||||
*/
|
||||
function aggregator_update_7001() {
|
||||
variable_set('aggregator_teaser_length', variable_get('teaser_length'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add queued timestamp.
|
||||
*/
|
||||
function aggregator_update_7002() {
|
||||
db_add_field('aggregator_feed', 'queued', array(
|
||||
'type' => 'int',
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
'description' => 'Time when this feed was queued for refresh, 0 if not queued.',
|
||||
));
|
||||
db_add_index('aggregator_feed', 'queued', array('queued'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @} End of "addtogroup updates-6.x-to-7.x"
|
||||
*/
|
||||
|
||||
/**
|
||||
* @addtogroup updates-7.x-extra
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* Increase the length of {aggregator_feed}.url.
|
||||
*/
|
||||
function aggregator_update_7003() {
|
||||
db_drop_unique_key('aggregator_feed', 'url');
|
||||
db_change_field('aggregator_feed', 'url', 'url', array('type' => 'text', 'not null' => TRUE, 'description' => 'URL to the feed.'));
|
||||
db_change_field('aggregator_feed', 'link', 'link', array('type' => 'text', 'not null' => TRUE, 'description' => 'The parent website of the feed; comes from the <link> element in the feed.'));
|
||||
db_change_field('aggregator_item', 'link', 'link', array('type' => 'text', 'not null' => TRUE, 'description' => 'Link to the feed item.'));
|
||||
db_change_field('aggregator_item', 'guid', 'guid', array('type' => 'text', 'not null' => TRUE, 'description' => 'Unique identifier for the feed item.'));
|
||||
db_add_index('aggregator_feed', 'url', array(array('url', 255)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add index on timestamp.
|
||||
*/
|
||||
function aggregator_update_7004() {
|
||||
if (!db_index_exists('aggregator_item', 'timestamp')) {
|
||||
db_add_index('aggregator_item', 'timestamp', array('timestamp'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @} End of "addtogroup updates-7.x-extra"
|
||||
*/
|
790
modules/aggregator/aggregator.module
Normal file
790
modules/aggregator/aggregator.module
Normal file
|
@ -0,0 +1,790 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Used to aggregate syndicated content (RSS, RDF, and Atom).
|
||||
*/
|
||||
|
||||
/**
|
||||
* Denotes that a feed's items should never expire.
|
||||
*/
|
||||
define('AGGREGATOR_CLEAR_NEVER', 0);
|
||||
|
||||
/**
|
||||
* Implements hook_help().
|
||||
*/
|
||||
function aggregator_help($path, $arg) {
|
||||
switch ($path) {
|
||||
case 'admin/help#aggregator':
|
||||
$output = '';
|
||||
$output .= '<h3>' . t('About') . '</h3>';
|
||||
$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>';
|
||||
$output .= '<h3>' . t('Uses') . '</h3>';
|
||||
$output .= '<dl>';
|
||||
$output .= '<dt>' . t('Viewing feeds') . '</dt>';
|
||||
$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>';
|
||||
$output .= '<dt>' . t('Adding, editing, and deleting feeds') . '</dt>';
|
||||
$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>';
|
||||
$output .= '<dt>' . t('OPML integration') . '</dt>';
|
||||
$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>';
|
||||
$output .= '<dt>' . t('Configuring cron') . '</dt>';
|
||||
$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>';
|
||||
$output .= '</dl>';
|
||||
return $output;
|
||||
case 'admin/config/services/aggregator':
|
||||
$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>';
|
||||
$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>';
|
||||
return $output;
|
||||
case 'admin/config/services/aggregator/add/feed':
|
||||
return '<p>' . t('Add a feed in RSS, RDF or Atom format. A feed may only have one entry.') . '</p>';
|
||||
case 'admin/config/services/aggregator/add/category':
|
||||
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>';
|
||||
case 'admin/config/services/aggregator/add/opml':
|
||||
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>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_theme().
|
||||
*/
|
||||
function aggregator_theme() {
|
||||
return array(
|
||||
'aggregator_wrapper' => array(
|
||||
'variables' => array('content' => NULL),
|
||||
'file' => 'aggregator.pages.inc',
|
||||
'template' => 'aggregator-wrapper',
|
||||
),
|
||||
'aggregator_categorize_items' => array(
|
||||
'render element' => 'form',
|
||||
'file' => 'aggregator.pages.inc',
|
||||
),
|
||||
'aggregator_feed_source' => array(
|
||||
'variables' => array('feed' => NULL),
|
||||
'file' => 'aggregator.pages.inc',
|
||||
'template' => 'aggregator-feed-source',
|
||||
),
|
||||
'aggregator_block_item' => array(
|
||||
'variables' => array('item' => NULL, 'feed' => 0),
|
||||
),
|
||||
'aggregator_summary_items' => array(
|
||||
'variables' => array('summary_items' => NULL, 'source' => NULL),
|
||||
'file' => 'aggregator.pages.inc',
|
||||
'template' => 'aggregator-summary-items',
|
||||
),
|
||||
'aggregator_summary_item' => array(
|
||||
'variables' => array('item' => NULL),
|
||||
'file' => 'aggregator.pages.inc',
|
||||
'template' => 'aggregator-summary-item',
|
||||
),
|
||||
'aggregator_item' => array(
|
||||
'variables' => array('item' => NULL),
|
||||
'file' => 'aggregator.pages.inc',
|
||||
'template' => 'aggregator-item',
|
||||
),
|
||||
'aggregator_page_opml' => array(
|
||||
'variables' => array('feeds' => NULL),
|
||||
'file' => 'aggregator.pages.inc',
|
||||
),
|
||||
'aggregator_page_rss' => array(
|
||||
'variables' => array('feeds' => NULL, 'category' => NULL),
|
||||
'file' => 'aggregator.pages.inc',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_menu().
|
||||
*/
|
||||
function aggregator_menu() {
|
||||
$items['admin/config/services/aggregator'] = array(
|
||||
'title' => 'Feed aggregator',
|
||||
'description' => "Configure which content your site aggregates from other sites, how often it polls them, and how they're categorized.",
|
||||
'page callback' => 'aggregator_admin_overview',
|
||||
'access arguments' => array('administer news feeds'),
|
||||
'weight' => 10,
|
||||
'file' => 'aggregator.admin.inc',
|
||||
);
|
||||
$items['admin/config/services/aggregator/add/feed'] = array(
|
||||
'title' => 'Add feed',
|
||||
'page callback' => 'drupal_get_form',
|
||||
'page arguments' => array('aggregator_form_feed'),
|
||||
'access arguments' => array('administer news feeds'),
|
||||
'type' => MENU_LOCAL_ACTION,
|
||||
'file' => 'aggregator.admin.inc',
|
||||
);
|
||||
$items['admin/config/services/aggregator/add/category'] = array(
|
||||
'title' => 'Add category',
|
||||
'page callback' => 'drupal_get_form',
|
||||
'page arguments' => array('aggregator_form_category'),
|
||||
'access arguments' => array('administer news feeds'),
|
||||
'type' => MENU_LOCAL_ACTION,
|
||||
'file' => 'aggregator.admin.inc',
|
||||
);
|
||||
$items['admin/config/services/aggregator/add/opml'] = array(
|
||||
'title' => 'Import OPML',
|
||||
'page callback' => 'drupal_get_form',
|
||||
'page arguments' => array('aggregator_form_opml'),
|
||||
'access arguments' => array('administer news feeds'),
|
||||
'type' => MENU_LOCAL_ACTION,
|
||||
'file' => 'aggregator.admin.inc',
|
||||
);
|
||||
$items['admin/config/services/aggregator/remove/%aggregator_feed'] = array(
|
||||
'title' => 'Remove items',
|
||||
'page callback' => 'drupal_get_form',
|
||||
'page arguments' => array('aggregator_admin_remove_feed', 5),
|
||||
'access arguments' => array('administer news feeds'),
|
||||
'file' => 'aggregator.admin.inc',
|
||||
);
|
||||
$items['admin/config/services/aggregator/update/%aggregator_feed'] = array(
|
||||
'title' => 'Update items',
|
||||
'page callback' => 'aggregator_admin_refresh_feed',
|
||||
'page arguments' => array(5),
|
||||
'access arguments' => array('administer news feeds'),
|
||||
'file' => 'aggregator.admin.inc',
|
||||
);
|
||||
$items['admin/config/services/aggregator/list'] = array(
|
||||
'title' => 'List',
|
||||
'type' => MENU_DEFAULT_LOCAL_TASK,
|
||||
'weight' => -10,
|
||||
);
|
||||
$items['admin/config/services/aggregator/settings'] = array(
|
||||
'title' => 'Settings',
|
||||
'description' => 'Configure the behavior of the feed aggregator, including when to discard feed items and how to present feed items and categories.',
|
||||
'page callback' => 'drupal_get_form',
|
||||
'page arguments' => array('aggregator_admin_form'),
|
||||
'access arguments' => array('administer news feeds'),
|
||||
'type' => MENU_LOCAL_TASK,
|
||||
'file' => 'aggregator.admin.inc',
|
||||
);
|
||||
$items['aggregator'] = array(
|
||||
'title' => 'Feed aggregator',
|
||||
'page callback' => 'aggregator_page_last',
|
||||
'access arguments' => array('access news feeds'),
|
||||
'weight' => 5,
|
||||
'file' => 'aggregator.pages.inc',
|
||||
);
|
||||
$items['aggregator/sources'] = array(
|
||||
'title' => 'Sources',
|
||||
'page callback' => 'aggregator_page_sources',
|
||||
'access arguments' => array('access news feeds'),
|
||||
'file' => 'aggregator.pages.inc',
|
||||
);
|
||||
$items['aggregator/categories'] = array(
|
||||
'title' => 'Categories',
|
||||
'page callback' => 'aggregator_page_categories',
|
||||
'access callback' => '_aggregator_has_categories',
|
||||
'file' => 'aggregator.pages.inc',
|
||||
);
|
||||
$items['aggregator/rss'] = array(
|
||||
'title' => 'RSS feed',
|
||||
'page callback' => 'aggregator_page_rss',
|
||||
'access arguments' => array('access news feeds'),
|
||||
'type' => MENU_CALLBACK,
|
||||
'file' => 'aggregator.pages.inc',
|
||||
);
|
||||
$items['aggregator/opml'] = array(
|
||||
'title' => 'OPML feed',
|
||||
'page callback' => 'aggregator_page_opml',
|
||||
'access arguments' => array('access news feeds'),
|
||||
'type' => MENU_CALLBACK,
|
||||
'file' => 'aggregator.pages.inc',
|
||||
);
|
||||
$items['aggregator/categories/%aggregator_category'] = array(
|
||||
'title callback' => '_aggregator_category_title',
|
||||
'title arguments' => array(2),
|
||||
'page callback' => 'aggregator_page_category',
|
||||
'page arguments' => array(2),
|
||||
'access arguments' => array('access news feeds'),
|
||||
'file' => 'aggregator.pages.inc',
|
||||
);
|
||||
$items['aggregator/categories/%aggregator_category/view'] = array(
|
||||
'title' => 'View',
|
||||
'type' => MENU_DEFAULT_LOCAL_TASK,
|
||||
'weight' => -10,
|
||||
);
|
||||
$items['aggregator/categories/%aggregator_category/categorize'] = array(
|
||||
'title' => 'Categorize',
|
||||
'page callback' => 'drupal_get_form',
|
||||
'page arguments' => array('aggregator_page_category_form', 2),
|
||||
'access arguments' => array('administer news feeds'),
|
||||
'type' => MENU_LOCAL_TASK,
|
||||
'file' => 'aggregator.pages.inc',
|
||||
);
|
||||
$items['aggregator/categories/%aggregator_category/configure'] = array(
|
||||
'title' => 'Configure',
|
||||
'page callback' => 'drupal_get_form',
|
||||
'page arguments' => array('aggregator_form_category', 2),
|
||||
'access arguments' => array('administer news feeds'),
|
||||
'type' => MENU_LOCAL_TASK,
|
||||
'weight' => 1,
|
||||
'file' => 'aggregator.admin.inc',
|
||||
);
|
||||
$items['aggregator/sources/%aggregator_feed'] = array(
|
||||
'page callback' => 'aggregator_page_source',
|
||||
'page arguments' => array(2),
|
||||
'access arguments' => array('access news feeds'),
|
||||
'file' => 'aggregator.pages.inc',
|
||||
);
|
||||
$items['aggregator/sources/%aggregator_feed/view'] = array(
|
||||
'title' => 'View',
|
||||
'type' => MENU_DEFAULT_LOCAL_TASK,
|
||||
'weight' => -10,
|
||||
);
|
||||
$items['aggregator/sources/%aggregator_feed/categorize'] = array(
|
||||
'title' => 'Categorize',
|
||||
'page callback' => 'drupal_get_form',
|
||||
'page arguments' => array('aggregator_page_source_form', 2),
|
||||
'access arguments' => array('administer news feeds'),
|
||||
'type' => MENU_LOCAL_TASK,
|
||||
'file' => 'aggregator.pages.inc',
|
||||
);
|
||||
$items['aggregator/sources/%aggregator_feed/configure'] = array(
|
||||
'title' => 'Configure',
|
||||
'page callback' => 'drupal_get_form',
|
||||
'page arguments' => array('aggregator_form_feed', 2),
|
||||
'access arguments' => array('administer news feeds'),
|
||||
'type' => MENU_LOCAL_TASK,
|
||||
'weight' => 1,
|
||||
'file' => 'aggregator.admin.inc',
|
||||
);
|
||||
$items['admin/config/services/aggregator/edit/feed/%aggregator_feed'] = array(
|
||||
'title' => 'Edit feed',
|
||||
'page callback' => 'drupal_get_form',
|
||||
'page arguments' => array('aggregator_form_feed', 6),
|
||||
'access arguments' => array('administer news feeds'),
|
||||
'file' => 'aggregator.admin.inc',
|
||||
);
|
||||
$items['admin/config/services/aggregator/edit/category/%aggregator_category'] = array(
|
||||
'title' => 'Edit category',
|
||||
'page callback' => 'drupal_get_form',
|
||||
'page arguments' => array('aggregator_form_category', 6),
|
||||
'access arguments' => array('administer news feeds'),
|
||||
'file' => 'aggregator.admin.inc',
|
||||
);
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Title callback: Returns a title for aggregator category pages.
|
||||
*
|
||||
* @param $category
|
||||
* An aggregator category.
|
||||
*
|
||||
* @return
|
||||
* A string with the aggregator category title.
|
||||
*/
|
||||
function _aggregator_category_title($category) {
|
||||
return $category['title'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether there are any aggregator categories.
|
||||
*
|
||||
* @return
|
||||
* TRUE if there is at least one category and the user has access to them;
|
||||
* FALSE otherwise.
|
||||
*/
|
||||
function _aggregator_has_categories() {
|
||||
return user_access('access news feeds') && (bool) db_query_range('SELECT 1 FROM {aggregator_category}', 0, 1)->fetchField();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_permission().
|
||||
*/
|
||||
function aggregator_permission() {
|
||||
return array(
|
||||
'administer news feeds' => array(
|
||||
'title' => t('Administer news feeds'),
|
||||
),
|
||||
'access news feeds' => array(
|
||||
'title' => t('View news feeds'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_cron().
|
||||
*
|
||||
* Queues news feeds for updates once their refresh interval has elapsed.
|
||||
*/
|
||||
function aggregator_cron() {
|
||||
$result = db_query('SELECT * FROM {aggregator_feed} WHERE queued = 0 AND checked + refresh < :time AND refresh <> :never', array(
|
||||
':time' => REQUEST_TIME,
|
||||
':never' => AGGREGATOR_CLEAR_NEVER
|
||||
));
|
||||
$queue = DrupalQueue::get('aggregator_feeds');
|
||||
foreach ($result as $feed) {
|
||||
if ($queue->createItem($feed)) {
|
||||
// Add timestamp to avoid queueing item more than once.
|
||||
db_update('aggregator_feed')
|
||||
->fields(array('queued' => REQUEST_TIME))
|
||||
->condition('fid', $feed->fid)
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
|
||||
// Remove queued timestamp after 6 hours assuming the update has failed.
|
||||
db_update('aggregator_feed')
|
||||
->fields(array('queued' => 0))
|
||||
->condition('queued', REQUEST_TIME - (3600 * 6), '<')
|
||||
->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_cron_queue_info().
|
||||
*/
|
||||
function aggregator_cron_queue_info() {
|
||||
$queues['aggregator_feeds'] = array(
|
||||
'worker callback' => 'aggregator_refresh',
|
||||
'time' => 60,
|
||||
);
|
||||
return $queues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_block_info().
|
||||
*/
|
||||
function aggregator_block_info() {
|
||||
$blocks = array();
|
||||
$result = db_query('SELECT cid, title FROM {aggregator_category} ORDER BY title');
|
||||
foreach ($result as $category) {
|
||||
$blocks['category-' . $category->cid]['info'] = t('!title category latest items', array('!title' => $category->title));
|
||||
}
|
||||
$result = db_query('SELECT fid, title FROM {aggregator_feed} WHERE block <> 0 ORDER BY fid');
|
||||
foreach ($result as $feed) {
|
||||
$blocks['feed-' . $feed->fid]['info'] = t('!title feed latest items', array('!title' => $feed->title));
|
||||
}
|
||||
return $blocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_block_configure().
|
||||
*/
|
||||
function aggregator_block_configure($delta = '') {
|
||||
list($type, $id) = explode('-', $delta);
|
||||
if ($type == 'category') {
|
||||
$value = db_query('SELECT block FROM {aggregator_category} WHERE cid = :cid', array(':cid' => $id))->fetchField();
|
||||
$form['block'] = array(
|
||||
'#type' => 'select',
|
||||
'#title' => t('Number of news items in block'),
|
||||
'#default_value' => $value,
|
||||
'#options' => drupal_map_assoc(range(2, 20)),
|
||||
);
|
||||
return $form;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_block_save().
|
||||
*/
|
||||
function aggregator_block_save($delta = '', $edit = array()) {
|
||||
list($type, $id) = explode('-', $delta);
|
||||
if ($type == 'category') {
|
||||
db_update('aggregator_category')
|
||||
->fields(array('block' => $edit['block']))
|
||||
->condition('cid', $id)
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_block_view().
|
||||
*
|
||||
* Generates blocks for the latest news items in each category and feed.
|
||||
*/
|
||||
function aggregator_block_view($delta = '') {
|
||||
if (user_access('access news feeds')) {
|
||||
$block = array();
|
||||
list($type, $id) = explode('-', $delta);
|
||||
$result = FALSE;
|
||||
switch ($type) {
|
||||
case 'feed':
|
||||
if ($feed = db_query('SELECT fid, title, block FROM {aggregator_feed} WHERE block <> 0 AND fid = :fid', array(':fid' => $id))->fetchObject()) {
|
||||
$block['subject'] = check_plain($feed->title);
|
||||
$result = db_query_range("SELECT * FROM {aggregator_item} WHERE fid = :fid ORDER BY timestamp DESC, iid DESC", 0, $feed->block, array(':fid' => $id));
|
||||
$read_more = theme('more_link', array('url' => 'aggregator/sources/' . $feed->fid, 'title' => t("View this feed's recent news.")));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'category':
|
||||
if ($category = db_query('SELECT cid, title, block FROM {aggregator_category} WHERE cid = :cid', array(':cid' => $id))->fetchObject()) {
|
||||
$block['subject'] = check_plain($category->title);
|
||||
$result = db_query_range('SELECT i.* FROM {aggregator_category_item} ci LEFT JOIN {aggregator_item} i ON ci.iid = i.iid WHERE ci.cid = :cid ORDER BY i.timestamp DESC, i.iid DESC', 0, $category->block, array(':cid' => $category->cid));
|
||||
$read_more = theme('more_link', array('url' => 'aggregator/categories/' . $category->cid, 'title' => t("View this category's recent news.")));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
$items = array();
|
||||
if (!empty($result)) {
|
||||
foreach ($result as $item) {
|
||||
$items[] = theme('aggregator_block_item', array('item' => $item));
|
||||
}
|
||||
}
|
||||
|
||||
// Only display the block if there are items to show.
|
||||
if (count($items) > 0) {
|
||||
$block['content'] = theme('item_list', array('items' => $items)) . $read_more;
|
||||
}
|
||||
return $block;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds/edits/deletes aggregator categories.
|
||||
*
|
||||
* @param $edit
|
||||
* An associative array describing the category to be added/edited/deleted.
|
||||
*/
|
||||
function aggregator_save_category($edit) {
|
||||
$link_path = 'aggregator/categories/';
|
||||
if (!empty($edit['cid'])) {
|
||||
$link_path .= $edit['cid'];
|
||||
if (!empty($edit['title'])) {
|
||||
db_merge('aggregator_category')
|
||||
->key(array('cid' => $edit['cid']))
|
||||
->fields(array(
|
||||
'title' => $edit['title'],
|
||||
'description' => $edit['description'],
|
||||
))
|
||||
->execute();
|
||||
$op = 'update';
|
||||
}
|
||||
else {
|
||||
db_delete('aggregator_category')
|
||||
->condition('cid', $edit['cid'])
|
||||
->execute();
|
||||
// Remove category from feeds.
|
||||
db_delete('aggregator_category_feed')
|
||||
->condition('cid', $edit['cid'])
|
||||
->execute();
|
||||
// Remove category from feed items.
|
||||
db_delete('aggregator_category_item')
|
||||
->condition('cid', $edit['cid'])
|
||||
->execute();
|
||||
// Make sure there is no active block for this category.
|
||||
if (module_exists('block')) {
|
||||
db_delete('block')
|
||||
->condition('module', 'aggregator')
|
||||
->condition('delta', 'category-' . $edit['cid'])
|
||||
->execute();
|
||||
}
|
||||
$edit['title'] = '';
|
||||
$op = 'delete';
|
||||
}
|
||||
}
|
||||
elseif (!empty($edit['title'])) {
|
||||
// A single unique id for bundles and feeds, to use in blocks.
|
||||
$link_path .= db_insert('aggregator_category')
|
||||
->fields(array(
|
||||
'title' => $edit['title'],
|
||||
'description' => $edit['description'],
|
||||
'block' => 5,
|
||||
))
|
||||
->execute();
|
||||
$op = 'insert';
|
||||
}
|
||||
if (isset($op)) {
|
||||
menu_link_maintain('aggregator', $op, $link_path, $edit['title']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add/edit/delete an aggregator feed.
|
||||
*
|
||||
* @param $edit
|
||||
* An associative array describing the feed to be added/edited/deleted.
|
||||
*/
|
||||
function aggregator_save_feed($edit) {
|
||||
if (!empty($edit['fid'])) {
|
||||
// An existing feed is being modified, delete the category listings.
|
||||
db_delete('aggregator_category_feed')
|
||||
->condition('fid', $edit['fid'])
|
||||
->execute();
|
||||
}
|
||||
if (!empty($edit['fid']) && !empty($edit['title'])) {
|
||||
db_update('aggregator_feed')
|
||||
->condition('fid', $edit['fid'])
|
||||
->fields(array(
|
||||
'title' => $edit['title'],
|
||||
'url' => $edit['url'],
|
||||
'refresh' => $edit['refresh'],
|
||||
'block' => $edit['block'],
|
||||
))
|
||||
->execute();
|
||||
}
|
||||
elseif (!empty($edit['fid'])) {
|
||||
$iids = db_query('SELECT iid FROM {aggregator_item} WHERE fid = :fid', array(':fid' => $edit['fid']))->fetchCol();
|
||||
if ($iids) {
|
||||
db_delete('aggregator_category_item')
|
||||
->condition('iid', $iids, 'IN')
|
||||
->execute();
|
||||
}
|
||||
db_delete('aggregator_feed')->
|
||||
condition('fid', $edit['fid'])
|
||||
->execute();
|
||||
db_delete('aggregator_item')
|
||||
->condition('fid', $edit['fid'])
|
||||
->execute();
|
||||
// Make sure there is no active block for this feed.
|
||||
if (module_exists('block')) {
|
||||
db_delete('block')
|
||||
->condition('module', 'aggregator')
|
||||
->condition('delta', 'feed-' . $edit['fid'])
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
elseif (!empty($edit['title'])) {
|
||||
$edit['fid'] = db_insert('aggregator_feed')
|
||||
->fields(array(
|
||||
'title' => $edit['title'],
|
||||
'url' => $edit['url'],
|
||||
'refresh' => $edit['refresh'],
|
||||
'block' => $edit['block'],
|
||||
'link' => '',
|
||||
'description' => '',
|
||||
'image' => '',
|
||||
))
|
||||
->execute();
|
||||
|
||||
}
|
||||
if (!empty($edit['title'])) {
|
||||
// The feed is being saved, save the categories as well.
|
||||
if (!empty($edit['category'])) {
|
||||
foreach ($edit['category'] as $cid => $value) {
|
||||
if ($value) {
|
||||
db_insert('aggregator_category_feed')
|
||||
->fields(array(
|
||||
'fid' => $edit['fid'],
|
||||
'cid' => $cid,
|
||||
))
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all items from a feed.
|
||||
*
|
||||
* @param $feed
|
||||
* An object describing the feed to be cleared.
|
||||
*/
|
||||
function aggregator_remove($feed) {
|
||||
_aggregator_get_variables();
|
||||
// Call hook_aggregator_remove() on all modules.
|
||||
module_invoke_all('aggregator_remove', $feed);
|
||||
// Reset feed.
|
||||
db_update('aggregator_feed')
|
||||
->condition('fid', $feed->fid)
|
||||
->fields(array(
|
||||
'checked' => 0,
|
||||
'hash' => '',
|
||||
'etag' => '',
|
||||
'modified' => 0,
|
||||
))
|
||||
->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the fetcher, parser, and processors.
|
||||
*
|
||||
* @return
|
||||
* An array containing the fetcher, parser, and processors.
|
||||
*/
|
||||
function _aggregator_get_variables() {
|
||||
// Fetch the feed.
|
||||
$fetcher = variable_get('aggregator_fetcher', 'aggregator');
|
||||
if ($fetcher == 'aggregator') {
|
||||
include_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'aggregator') . '/aggregator.fetcher.inc';
|
||||
}
|
||||
$parser = variable_get('aggregator_parser', 'aggregator');
|
||||
if ($parser == 'aggregator') {
|
||||
include_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'aggregator') . '/aggregator.parser.inc';
|
||||
}
|
||||
$processors = variable_get('aggregator_processors', array('aggregator'));
|
||||
if (in_array('aggregator', $processors)) {
|
||||
include_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'aggregator') . '/aggregator.processor.inc';
|
||||
}
|
||||
return array($fetcher, $parser, $processors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks a news feed for new items.
|
||||
*
|
||||
* @param $feed
|
||||
* An object describing the feed to be refreshed.
|
||||
*/
|
||||
function aggregator_refresh($feed) {
|
||||
// Store feed URL to track changes.
|
||||
$feed_url = $feed->url;
|
||||
|
||||
// Fetch the feed.
|
||||
list($fetcher, $parser, $processors) = _aggregator_get_variables();
|
||||
$success = module_invoke($fetcher, 'aggregator_fetch', $feed);
|
||||
|
||||
// We store the hash of feed data in the database. When refreshing a
|
||||
// feed we compare stored hash and new hash calculated from downloaded
|
||||
// data. If both are equal we say that feed is not updated.
|
||||
$hash = hash('sha256', $feed->source_string);
|
||||
|
||||
if ($success && ($feed->hash != $hash)) {
|
||||
// Parse the feed.
|
||||
if (module_invoke($parser, 'aggregator_parse', $feed)) {
|
||||
// Update feed with parsed data.
|
||||
db_merge('aggregator_feed')
|
||||
->key(array('fid' => $feed->fid))
|
||||
->fields(array(
|
||||
'url' => $feed->url,
|
||||
'link' => empty($feed->link) ? $feed->url : $feed->link,
|
||||
'description' => empty($feed->description) ? '' : $feed->description,
|
||||
'image' => empty($feed->image) ? '' : $feed->image,
|
||||
'hash' => $hash,
|
||||
'etag' => empty($feed->etag) ? '' : $feed->etag,
|
||||
'modified' => empty($feed->modified) ? 0 : $feed->modified,
|
||||
))
|
||||
->execute();
|
||||
|
||||
// Log if feed URL has changed.
|
||||
if ($feed->url != $feed_url) {
|
||||
watchdog('aggregator', 'Updated URL for feed %title to %url.', array('%title' => $feed->title, '%url' => $feed->url));
|
||||
}
|
||||
|
||||
watchdog('aggregator', 'There is new syndicated content from %site.', array('%site' => $feed->title));
|
||||
drupal_set_message(t('There is new syndicated content from %site.', array('%site' => $feed->title)));
|
||||
|
||||
// If there are items on the feed, let all enabled processors do their work on it.
|
||||
if (@count($feed->items)) {
|
||||
foreach ($processors as $processor) {
|
||||
module_invoke($processor, 'aggregator_process', $feed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
drupal_set_message(t('There is no new syndicated content from %site.', array('%site' => $feed->title)));
|
||||
}
|
||||
|
||||
// Regardless of successful or not, indicate that this feed has been checked.
|
||||
db_update('aggregator_feed')
|
||||
->fields(array('checked' => REQUEST_TIME, 'queued' => 0))
|
||||
->condition('fid', $feed->fid)
|
||||
->execute();
|
||||
|
||||
// Expire old feed items.
|
||||
if (function_exists('aggregator_expire')) {
|
||||
aggregator_expire($feed);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads an aggregator feed.
|
||||
*
|
||||
* @param $fid
|
||||
* The feed id.
|
||||
*
|
||||
* @return
|
||||
* An object describing the feed.
|
||||
*/
|
||||
function aggregator_feed_load($fid) {
|
||||
$feeds = &drupal_static(__FUNCTION__);
|
||||
if (!isset($feeds[$fid])) {
|
||||
$feeds[$fid] = db_query('SELECT * FROM {aggregator_feed} WHERE fid = :fid', array(':fid' => $fid))->fetchObject();
|
||||
}
|
||||
|
||||
return $feeds[$fid];
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads an aggregator category.
|
||||
*
|
||||
* @param $cid
|
||||
* The category id.
|
||||
*
|
||||
* @return
|
||||
* An associative array describing the category.
|
||||
*/
|
||||
function aggregator_category_load($cid) {
|
||||
$categories = &drupal_static(__FUNCTION__);
|
||||
if (!isset($categories[$cid])) {
|
||||
$categories[$cid] = db_query('SELECT * FROM {aggregator_category} WHERE cid = :cid', array(':cid' => $cid))->fetchAssoc();
|
||||
}
|
||||
|
||||
return $categories[$cid];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns HTML for an individual feed item for display in the block.
|
||||
*
|
||||
* @param $variables
|
||||
* An associative array containing:
|
||||
* - item: The item to be displayed.
|
||||
* - feed: Not used.
|
||||
*
|
||||
* @ingroup themeable
|
||||
*/
|
||||
function theme_aggregator_block_item($variables) {
|
||||
// Display the external link to the item.
|
||||
return '<a href="' . check_url($variables['item']->link) . '">' . check_plain($variables['item']->title) . "</a>\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the HTML content safely, as allowed.
|
||||
*
|
||||
* @param $value
|
||||
* The content to be filtered.
|
||||
*
|
||||
* @return
|
||||
* The filtered content.
|
||||
*/
|
||||
function aggregator_filter_xss($value) {
|
||||
return filter_xss($value, preg_split('/\s+|<|>/', variable_get('aggregator_allowed_html_tags', '<a> <b> <br> <dd> <dl> <dt> <em> <i> <li> <ol> <p> <strong> <u> <ul>'), -1, PREG_SPLIT_NO_EMPTY));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks and sanitizes the aggregator configuration.
|
||||
*
|
||||
* Goes through all fetchers, parsers and processors and checks whether they
|
||||
* are available. If one is missing, resets to standard configuration.
|
||||
*
|
||||
* @return
|
||||
* TRUE if this function resets the configuration; FALSE if not.
|
||||
*/
|
||||
function aggregator_sanitize_configuration() {
|
||||
$reset = FALSE;
|
||||
list($fetcher, $parser, $processors) = _aggregator_get_variables();
|
||||
if (!module_exists($fetcher)) {
|
||||
$reset = TRUE;
|
||||
}
|
||||
if (!module_exists($parser)) {
|
||||
$reset = TRUE;
|
||||
}
|
||||
foreach ($processors as $processor) {
|
||||
if (!module_exists($processor)) {
|
||||
$reset = TRUE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($reset) {
|
||||
variable_del('aggregator_fetcher');
|
||||
variable_del('aggregator_parser');
|
||||
variable_del('aggregator_processors');
|
||||
return TRUE;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for drupal_map_assoc.
|
||||
*
|
||||
* @param $count
|
||||
* Items count.
|
||||
*
|
||||
* @return
|
||||
* A string that is plural-formatted as "@count items".
|
||||
*/
|
||||
function _aggregator_items($count) {
|
||||
return format_plural($count, '1 item', '@count items');
|
||||
}
|
582
modules/aggregator/aggregator.pages.inc
Normal file
582
modules/aggregator/aggregator.pages.inc
Normal file
|
@ -0,0 +1,582 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* User page callbacks for the Aggregator module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Page callback: Displays the most recent items gathered from any feed.
|
||||
*
|
||||
* @return
|
||||
* The rendered list of items for the feed.
|
||||
*/
|
||||
function aggregator_page_last() {
|
||||
drupal_add_feed('aggregator/rss', variable_get('site_name', 'Drupal') . ' ' . t('aggregator'));
|
||||
|
||||
$items = aggregator_feed_items_load('sum');
|
||||
|
||||
return _aggregator_page_list($items, arg(1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Page callback: Displays all the items captured from the particular feed.
|
||||
*
|
||||
* @param $feed
|
||||
* The feed for which to display all items.
|
||||
*
|
||||
* @return
|
||||
* The rendered list of items for a feed.
|
||||
*
|
||||
* @see aggregator_menu()
|
||||
*/
|
||||
function aggregator_page_source($feed) {
|
||||
drupal_set_title($feed->title);
|
||||
$feed_source = theme('aggregator_feed_source', array('feed' => $feed));
|
||||
|
||||
// It is safe to include the fid in the query because it's loaded from the
|
||||
// database by aggregator_feed_load.
|
||||
$items = aggregator_feed_items_load('source', $feed);
|
||||
|
||||
return _aggregator_page_list($items, arg(3), $feed_source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Page callback: Displays a form with all items captured from a feed.
|
||||
*
|
||||
* @param $feed
|
||||
* The feed for which to list all of the aggregated items.
|
||||
*
|
||||
* @return
|
||||
* The rendered list of items for the feed.
|
||||
*
|
||||
* @see aggregator_page_source()
|
||||
*/
|
||||
function aggregator_page_source_form($form, $form_state, $feed) {
|
||||
return aggregator_page_source($feed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Page callback: Displays all the items aggregated in a particular category.
|
||||
*
|
||||
* @param $category
|
||||
* The category for which to list all of the aggregated items.
|
||||
*
|
||||
* @return
|
||||
* The rendered list of items for the feed.
|
||||
*/
|
||||
function aggregator_page_category($category) {
|
||||
drupal_add_feed('aggregator/rss/' . $category['cid'], variable_get('site_name', 'Drupal') . ' ' . t('aggregator - @title', array('@title' => $category['title'])));
|
||||
|
||||
// It is safe to include the cid in the query because it's loaded from the
|
||||
// database by aggregator_category_load.
|
||||
$items = aggregator_feed_items_load('category', $category);
|
||||
|
||||
return _aggregator_page_list($items, arg(3));
|
||||
}
|
||||
|
||||
/**
|
||||
* Page callback: Displays a form containing items aggregated in a category.
|
||||
*
|
||||
* @param $category
|
||||
* The category for which to list all of the aggregated items.
|
||||
*
|
||||
* @return
|
||||
* The rendered list of items for the feed.
|
||||
*
|
||||
* @see aggregator_page_category()
|
||||
*/
|
||||
function aggregator_page_category_form($form, $form_state, $category) {
|
||||
return aggregator_page_category($category);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads and optionally filters feed items.
|
||||
*
|
||||
* @param $type
|
||||
* The type of filter for the items. Possible values are:
|
||||
* - sum: No filtering.
|
||||
* - source: Filter the feed items, limiting the result to items from a
|
||||
* single source.
|
||||
* - category: Filter the feed items by category.
|
||||
* @param $data
|
||||
* Feed or category data used for filtering. The type and value of $data
|
||||
* depends on $type:
|
||||
* - source: $data is an object with $data->fid identifying the feed used to
|
||||
* as filter.
|
||||
* - category: $data is an array with $data['cid'] being the category id to
|
||||
* filter on.
|
||||
* The $data parameter is not used when $type is 'sum'.
|
||||
*
|
||||
* @return
|
||||
* An array of the feed items.
|
||||
*/
|
||||
function aggregator_feed_items_load($type, $data = NULL) {
|
||||
$items = array();
|
||||
switch ($type) {
|
||||
case 'sum':
|
||||
$query = db_select('aggregator_item', 'i');
|
||||
$query->join('aggregator_feed', 'f', 'i.fid = f.fid');
|
||||
$query->fields('i');
|
||||
$query->addField('f', 'title', 'ftitle');
|
||||
$query->addField('f', 'link', 'flink');
|
||||
break;
|
||||
case 'source':
|
||||
$query = db_select('aggregator_item', 'i');
|
||||
$query
|
||||
->fields('i')
|
||||
->condition('i.fid', $data->fid);
|
||||
break;
|
||||
case 'category':
|
||||
$query = db_select('aggregator_category_item', 'c');
|
||||
$query->leftJoin('aggregator_item', 'i', 'c.iid = i.iid');
|
||||
$query->leftJoin('aggregator_feed', 'f', 'i.fid = f.fid');
|
||||
$query
|
||||
->fields('i')
|
||||
->condition('cid', $data['cid']);
|
||||
$query->addField('f', 'title', 'ftitle');
|
||||
$query->addField('f', 'link', 'flink');
|
||||
break;
|
||||
}
|
||||
|
||||
$result = $query
|
||||
->extend('PagerDefault')
|
||||
->limit(20)
|
||||
->orderBy('i.timestamp', 'DESC')
|
||||
->orderBy('i.iid', 'DESC')
|
||||
->execute();
|
||||
|
||||
foreach ($result as $item) {
|
||||
$item->categories = db_query('SELECT c.title, c.cid FROM {aggregator_category_item} ci LEFT JOIN {aggregator_category} c ON ci.cid = c.cid WHERE ci.iid = :iid ORDER BY c.title', array(':iid' => $item->iid))->fetchAll();
|
||||
$items[] = $item;
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints an aggregator page listing a number of feed items.
|
||||
*
|
||||
* Various menu callbacks use this function to print their feeds.
|
||||
*
|
||||
* @param $items
|
||||
* The items to be listed.
|
||||
* @param $op
|
||||
* Which form should be added to the items. Only 'categorize' is now
|
||||
* recognized.
|
||||
* @param $feed_source
|
||||
* The feed source URL.
|
||||
*
|
||||
* @return
|
||||
* The rendered list of items for the feed.
|
||||
*/
|
||||
function _aggregator_page_list($items, $op, $feed_source = '') {
|
||||
if (user_access('administer news feeds') && ($op == 'categorize')) {
|
||||
// Get form data.
|
||||
$output = aggregator_categorize_items($items, $feed_source);
|
||||
}
|
||||
else {
|
||||
// Assemble themed output.
|
||||
$output = $feed_source;
|
||||
foreach ($items as $item) {
|
||||
$output .= theme('aggregator_item', array('item' => $item));
|
||||
}
|
||||
$output = theme('aggregator_wrapper', array('content' => $output));
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Form constructor to build the page list form.
|
||||
*
|
||||
* @param $items
|
||||
* An array of the feed items.
|
||||
* @param $feed_source
|
||||
* (optional) The feed source URL. Defaults to an empty string.
|
||||
*
|
||||
* @return array
|
||||
* An array of FAPI elements.
|
||||
*
|
||||
* @see aggregator_categorize_items_submit()
|
||||
* @see theme_aggregator_categorize_items()
|
||||
* @ingroup forms
|
||||
*/
|
||||
function aggregator_categorize_items($items, $feed_source = '') {
|
||||
$form['#submit'][] = 'aggregator_categorize_items_submit';
|
||||
$form['#theme'] = 'aggregator_categorize_items';
|
||||
$form['feed_source'] = array(
|
||||
'#value' => $feed_source,
|
||||
);
|
||||
$categories = array();
|
||||
$done = FALSE;
|
||||
$form['items'] = array();
|
||||
$form['categories'] = array(
|
||||
'#tree' => TRUE,
|
||||
);
|
||||
foreach ($items as $item) {
|
||||
$form['items'][$item->iid] = array('#markup' => theme('aggregator_item', array('item' => $item)));
|
||||
$form['categories'][$item->iid] = array();
|
||||
$categories_result = db_query('SELECT c.cid, c.title, ci.iid FROM {aggregator_category} c LEFT JOIN {aggregator_category_item} ci ON c.cid = ci.cid AND ci.iid = :iid', array(':iid' => $item->iid));
|
||||
$selected = array();
|
||||
foreach ($categories_result as $category) {
|
||||
if (!$done) {
|
||||
$categories[$category->cid] = check_plain($category->title);
|
||||
}
|
||||
if ($category->iid) {
|
||||
$selected[] = $category->cid;
|
||||
}
|
||||
}
|
||||
$done = TRUE;
|
||||
$form['categories'][$item->iid] = array(
|
||||
'#type' => variable_get('aggregator_category_selector', 'checkboxes'),
|
||||
'#default_value' => $selected,
|
||||
'#options' => $categories,
|
||||
'#size' => 10,
|
||||
'#multiple' => TRUE
|
||||
);
|
||||
}
|
||||
$form['actions'] = array('#type' => 'actions');
|
||||
$form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save categories'));
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Form submission handler for aggregator_categorize_items().
|
||||
*/
|
||||
function aggregator_categorize_items_submit($form, &$form_state) {
|
||||
if (!empty($form_state['values']['categories'])) {
|
||||
foreach ($form_state['values']['categories'] as $iid => $selection) {
|
||||
db_delete('aggregator_category_item')
|
||||
->condition('iid', $iid)
|
||||
->execute();
|
||||
$insert = db_insert('aggregator_category_item')->fields(array('iid', 'cid'));
|
||||
$has_values = FALSE;
|
||||
foreach ($selection as $cid) {
|
||||
if ($cid && $iid) {
|
||||
$has_values = TRUE;
|
||||
$insert->values(array(
|
||||
'iid' => $iid,
|
||||
'cid' => $cid,
|
||||
));
|
||||
}
|
||||
}
|
||||
if ($has_values) {
|
||||
$insert->execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
drupal_set_message(t('The categories have been saved.'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns HTML for the aggregator page list form for assigning categories.
|
||||
*
|
||||
* @param $variables
|
||||
* An associative array containing:
|
||||
* - form: A render element representing the form.
|
||||
*
|
||||
* @ingroup themeable
|
||||
*/
|
||||
function theme_aggregator_categorize_items($variables) {
|
||||
$form = $variables['form'];
|
||||
|
||||
$output = drupal_render($form['feed_source']);
|
||||
$rows = array();
|
||||
if (!empty($form['items'])) {
|
||||
foreach (element_children($form['items']) as $key) {
|
||||
$rows[] = array(
|
||||
drupal_render($form['items'][$key]),
|
||||
array('data' => drupal_render($form['categories'][$key]), 'class' => array('categorize-item')),
|
||||
);
|
||||
}
|
||||
}
|
||||
$output .= theme('table', array('header' => array('', t('Categorize')), 'rows' => $rows));
|
||||
$output .= drupal_render($form['submit']);
|
||||
$output .= drupal_render_children($form);
|
||||
|
||||
return theme('aggregator_wrapper', array('content' => $output));
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes variables for aggregator-wrapper.tpl.php.
|
||||
*
|
||||
* @see aggregator-wrapper.tpl.php
|
||||
*/
|
||||
function template_preprocess_aggregator_wrapper(&$variables) {
|
||||
$variables['pager'] = theme('pager');
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes variables for aggregator-item.tpl.php.
|
||||
*
|
||||
* @see aggregator-item.tpl.php
|
||||
*/
|
||||
function template_preprocess_aggregator_item(&$variables) {
|
||||
$item = $variables['item'];
|
||||
|
||||
$variables['feed_url'] = check_url($item->link);
|
||||
$variables['feed_title'] = check_plain($item->title);
|
||||
$variables['content'] = aggregator_filter_xss($item->description);
|
||||
|
||||
$variables['source_url'] = '';
|
||||
$variables['source_title'] = '';
|
||||
if (isset($item->ftitle) && isset($item->fid)) {
|
||||
$variables['source_url'] = url("aggregator/sources/$item->fid");
|
||||
$variables['source_title'] = check_plain($item->ftitle);
|
||||
}
|
||||
if (date('Ymd', $item->timestamp) == date('Ymd')) {
|
||||
$variables['source_date'] = t('%ago ago', array('%ago' => format_interval(REQUEST_TIME - $item->timestamp)));
|
||||
}
|
||||
else {
|
||||
$variables['source_date'] = format_date($item->timestamp, 'custom', variable_get('date_format_medium', 'D, m/d/Y - H:i'));
|
||||
}
|
||||
|
||||
$variables['categories'] = array();
|
||||
foreach ($item->categories as $category) {
|
||||
$variables['categories'][$category->cid] = l($category->title, 'aggregator/categories/' . $category->cid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Page callback: Displays all the feeds used by the aggregator.
|
||||
*
|
||||
* @return
|
||||
* An HTML-formatted string.
|
||||
*
|
||||
* @see aggregator_menu()
|
||||
*/
|
||||
function aggregator_page_sources() {
|
||||
$result = db_query('SELECT f.fid, f.title, f.description, f.image, MAX(i.timestamp) AS last FROM {aggregator_feed} f LEFT JOIN {aggregator_item} i ON f.fid = i.fid GROUP BY f.fid, f.title, f.description, f.image ORDER BY last DESC, f.title');
|
||||
|
||||
$output = '';
|
||||
foreach ($result as $feed) {
|
||||
// Most recent items:
|
||||
$summary_items = array();
|
||||
if (variable_get('aggregator_summary_items', 3)) {
|
||||
$items = db_query_range('SELECT i.title, i.timestamp, i.link FROM {aggregator_item} i WHERE i.fid = :fid ORDER BY i.timestamp DESC', 0, variable_get('aggregator_summary_items', 3), array(':fid' => $feed->fid));
|
||||
foreach ($items as $item) {
|
||||
$summary_items[] = theme('aggregator_summary_item', array('item' => $item));
|
||||
}
|
||||
}
|
||||
$feed->url = url('aggregator/sources/' . $feed->fid);
|
||||
$output .= theme('aggregator_summary_items', array('summary_items' => $summary_items, 'source' => $feed));
|
||||
}
|
||||
$output .= theme('feed_icon', array('url' => 'aggregator/opml', 'title' => t('OPML feed')));
|
||||
|
||||
return theme('aggregator_wrapper', array('content' => $output));
|
||||
}
|
||||
|
||||
/**
|
||||
* Page callback: Displays all the categories used by the Aggregator module.
|
||||
*
|
||||
* @return string
|
||||
* An HTML formatted string.
|
||||
*
|
||||
* @see aggregator_menu()
|
||||
*/
|
||||
function aggregator_page_categories() {
|
||||
$result = db_query('SELECT c.cid, c.title, c.description FROM {aggregator_category} c LEFT JOIN {aggregator_category_item} ci ON c.cid = ci.cid LEFT JOIN {aggregator_item} i ON ci.iid = i.iid GROUP BY c.cid, c.title, c.description');
|
||||
|
||||
$output = '';
|
||||
foreach ($result as $category) {
|
||||
if (variable_get('aggregator_summary_items', 3)) {
|
||||
$summary_items = array();
|
||||
$items = db_query_range('SELECT i.title, i.timestamp, i.link, f.title as feed_title, f.link as feed_link FROM {aggregator_category_item} ci LEFT JOIN {aggregator_item} i ON i.iid = ci.iid LEFT JOIN {aggregator_feed} f ON i.fid = f.fid WHERE ci.cid = :cid ORDER BY i.timestamp DESC', 0, variable_get('aggregator_summary_items', 3), array(':cid' => $category->cid));
|
||||
foreach ($items as $item) {
|
||||
$summary_items[] = theme('aggregator_summary_item', array('item' => $item));
|
||||
}
|
||||
}
|
||||
$category->url = url('aggregator/categories/' . $category->cid);
|
||||
$output .= theme('aggregator_summary_items', array('summary_items' => $summary_items, 'source' => $category));
|
||||
}
|
||||
|
||||
return theme('aggregator_wrapper', array('content' => $output));
|
||||
}
|
||||
|
||||
/**
|
||||
* Page callback: Generates an RSS 0.92 feed of aggregator items or categories.
|
||||
*
|
||||
* @return string
|
||||
* An HTML formatted string.
|
||||
*/
|
||||
function aggregator_page_rss() {
|
||||
$result = NULL;
|
||||
// arg(2) is the passed cid, only select for that category.
|
||||
if (arg(2)) {
|
||||
$category = db_query('SELECT cid, title FROM {aggregator_category} WHERE cid = :cid', array(':cid' => arg(2)))->fetchObject();
|
||||
$result = db_query_range('SELECT i.*, f.title AS ftitle, f.link AS flink FROM {aggregator_category_item} c LEFT JOIN {aggregator_item} i ON c.iid = i.iid LEFT JOIN {aggregator_feed} f ON i.fid = f.fid WHERE cid = :cid ORDER BY timestamp DESC, i.iid DESC', 0, variable_get('feed_default_items', 10), array(':cid' => $category->cid));
|
||||
}
|
||||
// Or, get the default aggregator items.
|
||||
else {
|
||||
$category = NULL;
|
||||
$result = db_query_range('SELECT i.*, f.title AS ftitle, f.link AS flink FROM {aggregator_item} i INNER JOIN {aggregator_feed} f ON i.fid = f.fid ORDER BY i.timestamp DESC, i.iid DESC', 0, variable_get('feed_default_items', 10));
|
||||
}
|
||||
|
||||
$feeds = $result->fetchAll();
|
||||
return theme('aggregator_page_rss', array('feeds' => $feeds, 'category' => $category));
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the RSS page for a feed.
|
||||
*
|
||||
* @param $variables
|
||||
* An associative array containing:
|
||||
* - feeds: An array of the feeds to theme.
|
||||
* - category: A common category, if any, for all the feeds.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @ingroup themeable
|
||||
*/
|
||||
function theme_aggregator_page_rss($variables) {
|
||||
$feeds = $variables['feeds'];
|
||||
$category = $variables['category'];
|
||||
|
||||
drupal_add_http_header('Content-Type', 'application/rss+xml; charset=utf-8');
|
||||
|
||||
$items = '';
|
||||
$feed_length = variable_get('feed_item_length', 'fulltext');
|
||||
foreach ($feeds as $feed) {
|
||||
switch ($feed_length) {
|
||||
case 'teaser':
|
||||
$summary = text_summary($feed->description, NULL, variable_get('aggregator_teaser_length', 600));
|
||||
if ($summary != $feed->description) {
|
||||
$summary .= '<p><a href="' . check_url($feed->link) . '">' . t('read more') . "</a></p>\n";
|
||||
}
|
||||
$feed->description = $summary;
|
||||
break;
|
||||
case 'title':
|
||||
$feed->description = '';
|
||||
break;
|
||||
}
|
||||
$items .= format_rss_item($feed->ftitle . ': ' . $feed->title, $feed->link, $feed->description, array('pubDate' => date('r', $feed->timestamp)));
|
||||
}
|
||||
|
||||
$site_name = variable_get('site_name', 'Drupal');
|
||||
$url = url((isset($category) ? 'aggregator/categories/' . $category->cid : 'aggregator'), array('absolute' => TRUE));
|
||||
$description = isset($category) ? t('@site_name - aggregated feeds in category @title', array('@site_name' => $site_name, '@title' => $category->title)) : t('@site_name - aggregated feeds', array('@site_name' => $site_name));
|
||||
|
||||
$output = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
|
||||
$output .= "<rss version=\"2.0\">\n";
|
||||
$output .= format_rss_channel(t('@site_name aggregator', array('@site_name' => $site_name)), $url, $description, $items);
|
||||
$output .= "</rss>\n";
|
||||
|
||||
print $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Page callback: Generates an OPML representation of all feeds.
|
||||
*
|
||||
* @param $cid
|
||||
* (optional) If set, feeds are exported only from a category with this ID.
|
||||
* Otherwise, all feeds are exported. Defaults to NULL.
|
||||
*
|
||||
* @return
|
||||
* An OPML formatted string.
|
||||
*/
|
||||
function aggregator_page_opml($cid = NULL) {
|
||||
if ($cid) {
|
||||
$result = db_query('SELECT f.title, f.url FROM {aggregator_feed} f LEFT JOIN {aggregator_category_feed} c on f.fid = c.fid WHERE c.cid = :cid ORDER BY title', array(':cid' => $cid));
|
||||
}
|
||||
else {
|
||||
$result = db_query('SELECT * FROM {aggregator_feed} ORDER BY title');
|
||||
}
|
||||
|
||||
$feeds = $result->fetchAll();
|
||||
return theme('aggregator_page_opml', array('feeds' => $feeds));
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the OPML page for the feed.
|
||||
*
|
||||
* @param $variables
|
||||
* An associative array containing:
|
||||
* - feeds: An array of the feeds to theme.
|
||||
*
|
||||
* @ingroup themeable
|
||||
*/
|
||||
function theme_aggregator_page_opml($variables) {
|
||||
$feeds = $variables['feeds'];
|
||||
|
||||
drupal_add_http_header('Content-Type', 'text/xml; charset=utf-8');
|
||||
|
||||
$output = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
|
||||
$output .= "<opml version=\"1.1\">\n";
|
||||
$output .= "<head>\n";
|
||||
$output .= '<title>' . check_plain(variable_get('site_name', 'Drupal')) . "</title>\n";
|
||||
$output .= '<dateModified>' . gmdate(DATE_RFC2822, REQUEST_TIME) . "</dateModified>\n";
|
||||
$output .= "</head>\n";
|
||||
$output .= "<body>\n";
|
||||
foreach ($feeds as $feed) {
|
||||
$output .= '<outline text="' . check_plain($feed->title) . '" xmlUrl="' . check_url($feed->url) . "\" />\n";
|
||||
}
|
||||
$output .= "</body>\n";
|
||||
$output .= "</opml>\n";
|
||||
|
||||
print $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes variables for aggregator-summary-items.tpl.php.
|
||||
*
|
||||
* @see aggregator-summary-items.tpl.php
|
||||
*/
|
||||
function template_preprocess_aggregator_summary_items(&$variables) {
|
||||
$variables['title'] = check_plain($variables['source']->title);
|
||||
$variables['summary_list'] = theme('item_list', array('items' => $variables['summary_items']));
|
||||
$variables['source_url'] = $variables['source']->url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes variables for aggregator-summary-item.tpl.php.
|
||||
*
|
||||
* @see aggregator-summary-item.tpl.php
|
||||
*/
|
||||
function template_preprocess_aggregator_summary_item(&$variables) {
|
||||
$item = $variables['item'];
|
||||
|
||||
$variables['feed_url'] = check_url($item->link);
|
||||
$variables['feed_title'] = check_plain($item->title);
|
||||
$variables['feed_age'] = t('%age old', array('%age' => format_interval(REQUEST_TIME - $item->timestamp)));
|
||||
|
||||
$variables['source_url'] = '';
|
||||
$variables['source_title'] = '';
|
||||
if (!empty($item->feed_link)) {
|
||||
$variables['source_url'] = check_url($item->feed_link);
|
||||
$variables['source_title'] = check_plain($item->feed_title);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes variables for aggregator-feed-source.tpl.php.
|
||||
*
|
||||
* @see aggregator-feed-source.tpl.php
|
||||
*/
|
||||
function template_preprocess_aggregator_feed_source(&$variables) {
|
||||
$feed = $variables['feed'];
|
||||
|
||||
$variables['source_icon'] = theme('feed_icon', array('url' => $feed->url, 'title' => t('!title feed', array('!title' => $feed->title))));
|
||||
|
||||
if (!empty($feed->image) && !empty($feed->title) && !empty($feed->link)) {
|
||||
$variables['source_image'] = l(theme('image', array('path' => $feed->image, 'alt' => $feed->title)), $feed->link, array('html' => TRUE, 'attributes' => array('class' => 'feed-image')));
|
||||
}
|
||||
else {
|
||||
$variables['source_image'] = '';
|
||||
}
|
||||
|
||||
$variables['source_description'] = aggregator_filter_xss($feed->description);
|
||||
$variables['source_url'] = check_url(url($feed->link, array('absolute' => TRUE)));
|
||||
|
||||
if ($feed->checked) {
|
||||
$variables['last_checked'] = t('@time ago', array('@time' => format_interval(REQUEST_TIME - $feed->checked)));
|
||||
}
|
||||
else {
|
||||
$variables['last_checked'] = t('never');
|
||||
}
|
||||
|
||||
if (user_access('administer news feeds')) {
|
||||
$variables['last_checked'] = l($variables['last_checked'], 'admin/config/services/aggregator');
|
||||
}
|
||||
}
|
329
modules/aggregator/aggregator.parser.inc
Normal file
329
modules/aggregator/aggregator.parser.inc
Normal file
|
@ -0,0 +1,329 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Parser functions for the aggregator module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Implements hook_aggregator_parse_info().
|
||||
*/
|
||||
function aggregator_aggregator_parse_info() {
|
||||
return array(
|
||||
'title' => t('Default parser'),
|
||||
'description' => t('Parses RSS, Atom and RDF feeds.'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_aggregator_parse().
|
||||
*/
|
||||
function aggregator_aggregator_parse($feed) {
|
||||
global $channel, $image;
|
||||
|
||||
// Filter the input data.
|
||||
if (aggregator_parse_feed($feed->source_string, $feed)) {
|
||||
$modified = empty($feed->http_headers['last-modified']) ? 0 : strtotime($feed->http_headers['last-modified']);
|
||||
|
||||
// Prepare the channel data.
|
||||
foreach ($channel as $key => $value) {
|
||||
$channel[$key] = trim($value);
|
||||
}
|
||||
|
||||
// Prepare the image data (if any).
|
||||
foreach ($image as $key => $value) {
|
||||
$image[$key] = trim($value);
|
||||
}
|
||||
|
||||
$etag = empty($feed->http_headers['etag']) ? '' : $feed->http_headers['etag'];
|
||||
|
||||
// Add parsed data to the feed object.
|
||||
$feed->link = !empty($channel['link']) ? $channel['link'] : '';
|
||||
$feed->description = !empty($channel['description']) ? $channel['description'] : '';
|
||||
$feed->image = !empty($image['url']) ? $image['url'] : '';
|
||||
$feed->etag = $etag;
|
||||
$feed->modified = $modified;
|
||||
|
||||
// Clear the cache.
|
||||
cache_clear_all();
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a feed and stores its items.
|
||||
*
|
||||
* @param $data
|
||||
* The feed data.
|
||||
* @param $feed
|
||||
* An object describing the feed to be parsed.
|
||||
*
|
||||
* @return
|
||||
* FALSE on error, TRUE otherwise.
|
||||
*/
|
||||
function aggregator_parse_feed(&$data, $feed) {
|
||||
global $items, $image, $channel;
|
||||
|
||||
// Unset the global variables before we use them.
|
||||
unset($GLOBALS['element'], $GLOBALS['item'], $GLOBALS['tag']);
|
||||
$items = array();
|
||||
$image = array();
|
||||
$channel = array();
|
||||
|
||||
// Parse the data.
|
||||
$xml_parser = drupal_xml_parser_create($data);
|
||||
xml_set_element_handler($xml_parser, 'aggregator_element_start', 'aggregator_element_end');
|
||||
xml_set_character_data_handler($xml_parser, 'aggregator_element_data');
|
||||
|
||||
if (!xml_parse($xml_parser, $data, 1)) {
|
||||
watchdog('aggregator', 'The feed from %site seems to be broken, due to an error "%error" on line %line.', array('%site' => $feed->title, '%error' => xml_error_string(xml_get_error_code($xml_parser)), '%line' => xml_get_current_line_number($xml_parser)), WATCHDOG_WARNING);
|
||||
drupal_set_message(t('The feed from %site seems to be broken, because of error "%error" on line %line.', array('%site' => $feed->title, '%error' => xml_error_string(xml_get_error_code($xml_parser)), '%line' => xml_get_current_line_number($xml_parser))), 'error');
|
||||
return FALSE;
|
||||
}
|
||||
xml_parser_free($xml_parser);
|
||||
|
||||
// We reverse the array such that we store the first item last, and the last
|
||||
// item first. In the database, the newest item should be at the top.
|
||||
$items = array_reverse($items);
|
||||
|
||||
// Initialize items array.
|
||||
$feed->items = array();
|
||||
foreach ($items as $item) {
|
||||
|
||||
// Prepare the item:
|
||||
foreach ($item as $key => $value) {
|
||||
$item[$key] = trim($value);
|
||||
}
|
||||
|
||||
// Resolve the item's title. If no title is found, we use up to 40
|
||||
// characters of the description ending at a word boundary, but not
|
||||
// splitting potential entities.
|
||||
if (!empty($item['title'])) {
|
||||
$item['title'] = $item['title'];
|
||||
}
|
||||
elseif (!empty($item['description'])) {
|
||||
$item['title'] = preg_replace('/^(.*)[^\w;&].*?$/', "\\1", truncate_utf8($item['description'], 40));
|
||||
}
|
||||
else {
|
||||
$item['title'] = '';
|
||||
}
|
||||
|
||||
// Resolve the items link.
|
||||
if (!empty($item['link'])) {
|
||||
$item['link'] = $item['link'];
|
||||
}
|
||||
else {
|
||||
$item['link'] = $feed->link;
|
||||
}
|
||||
|
||||
// Atom feeds have an ID tag instead of a GUID tag.
|
||||
if (!isset($item['guid'])) {
|
||||
$item['guid'] = isset($item['id']) ? $item['id'] : '';
|
||||
}
|
||||
|
||||
// Atom feeds have a content and/or summary tag instead of a description tag.
|
||||
if (!empty($item['content:encoded'])) {
|
||||
$item['description'] = $item['content:encoded'];
|
||||
}
|
||||
elseif (!empty($item['summary'])) {
|
||||
$item['description'] = $item['summary'];
|
||||
}
|
||||
elseif (!empty($item['content'])) {
|
||||
$item['description'] = $item['content'];
|
||||
}
|
||||
|
||||
// Try to resolve and parse the item's publication date.
|
||||
$date = '';
|
||||
foreach (array('pubdate', 'dc:date', 'dcterms:issued', 'dcterms:created', 'dcterms:modified', 'issued', 'created', 'modified', 'published', 'updated') as $key) {
|
||||
if (!empty($item[$key])) {
|
||||
$date = $item[$key];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$item['timestamp'] = strtotime($date);
|
||||
|
||||
if ($item['timestamp'] === FALSE) {
|
||||
$item['timestamp'] = aggregator_parse_w3cdtf($date); // Aggregator_parse_w3cdtf() returns FALSE on failure.
|
||||
}
|
||||
|
||||
// Resolve dc:creator tag as the item author if author tag is not set.
|
||||
if (empty($item['author']) && !empty($item['dc:creator'])) {
|
||||
$item['author'] = $item['dc:creator'];
|
||||
}
|
||||
|
||||
$item += array('author' => '', 'description' => '');
|
||||
|
||||
// Store on $feed object. This is where processors will look for parsed items.
|
||||
$feed->items[] = $item;
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs an action when an opening tag is encountered.
|
||||
*
|
||||
* Callback function used by xml_parse() within aggregator_parse_feed().
|
||||
*/
|
||||
function aggregator_element_start($parser, $name, $attributes) {
|
||||
global $item, $element, $tag, $items, $channel;
|
||||
|
||||
$name = strtolower($name);
|
||||
switch ($name) {
|
||||
case 'image':
|
||||
case 'textinput':
|
||||
case 'summary':
|
||||
case 'tagline':
|
||||
case 'subtitle':
|
||||
case 'logo':
|
||||
case 'info':
|
||||
$element = $name;
|
||||
break;
|
||||
case 'id':
|
||||
case 'content':
|
||||
if ($element != 'item') {
|
||||
$element = $name;
|
||||
}
|
||||
case 'link':
|
||||
// According to RFC 4287, link elements in Atom feeds without a 'rel'
|
||||
// attribute should be interpreted as though the relation type is
|
||||
// "alternate".
|
||||
if (!empty($attributes['HREF']) && (empty($attributes['REL']) || $attributes['REL'] == 'alternate')) {
|
||||
if ($element == 'item') {
|
||||
$items[$item]['link'] = $attributes['HREF'];
|
||||
}
|
||||
else {
|
||||
$channel['link'] = $attributes['HREF'];
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'item':
|
||||
$element = $name;
|
||||
$item += 1;
|
||||
break;
|
||||
case 'entry':
|
||||
$element = 'item';
|
||||
$item += 1;
|
||||
break;
|
||||
}
|
||||
|
||||
$tag = $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs an action when a closing tag is encountered.
|
||||
*
|
||||
* Callback function used by xml_parse() within aggregator_parse_feed().
|
||||
*/
|
||||
function aggregator_element_end($parser, $name) {
|
||||
global $element;
|
||||
|
||||
switch ($name) {
|
||||
case 'image':
|
||||
case 'textinput':
|
||||
case 'item':
|
||||
case 'entry':
|
||||
case 'info':
|
||||
$element = '';
|
||||
break;
|
||||
case 'id':
|
||||
case 'content':
|
||||
if ($element == $name) {
|
||||
$element = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs an action when data is encountered.
|
||||
*
|
||||
* Callback function used by xml_parse() within aggregator_parse_feed().
|
||||
*/
|
||||
function aggregator_element_data($parser, $data) {
|
||||
global $channel, $element, $items, $item, $image, $tag;
|
||||
$items += array($item => array());
|
||||
switch ($element) {
|
||||
case 'item':
|
||||
$items[$item] += array($tag => '');
|
||||
$items[$item][$tag] .= $data;
|
||||
break;
|
||||
case 'image':
|
||||
case 'logo':
|
||||
$image += array($tag => '');
|
||||
$image[$tag] .= $data;
|
||||
break;
|
||||
case 'link':
|
||||
if ($data) {
|
||||
$items[$item] += array($tag => '');
|
||||
$items[$item][$tag] .= $data;
|
||||
}
|
||||
break;
|
||||
case 'content':
|
||||
$items[$item] += array('content' => '');
|
||||
$items[$item]['content'] .= $data;
|
||||
break;
|
||||
case 'summary':
|
||||
$items[$item] += array('summary' => '');
|
||||
$items[$item]['summary'] .= $data;
|
||||
break;
|
||||
case 'tagline':
|
||||
case 'subtitle':
|
||||
$channel += array('description' => '');
|
||||
$channel['description'] .= $data;
|
||||
break;
|
||||
case 'info':
|
||||
case 'id':
|
||||
case 'textinput':
|
||||
// The sub-element is not supported. However, we must recognize
|
||||
// it or its contents will end up in the item array.
|
||||
break;
|
||||
default:
|
||||
$channel += array($tag => '');
|
||||
$channel[$tag] .= $data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the W3C date/time format, a subset of ISO 8601.
|
||||
*
|
||||
* PHP date parsing functions do not handle this format. See
|
||||
* http://www.w3.org/TR/NOTE-datetime for more information. Originally from
|
||||
* MagpieRSS (http://magpierss.sourceforge.net/).
|
||||
*
|
||||
* @param $date_str
|
||||
* A string with a potentially W3C DTF date.
|
||||
*
|
||||
* @return
|
||||
* A timestamp if parsed successfully or FALSE if not.
|
||||
*/
|
||||
function aggregator_parse_w3cdtf($date_str) {
|
||||
if (preg_match('/(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(:(\d{2}))?(?:([-+])(\d{2}):?(\d{2})|(Z))?/', $date_str, $match)) {
|
||||
list($year, $month, $day, $hours, $minutes, $seconds) = array($match[1], $match[2], $match[3], $match[4], $match[5], $match[6]);
|
||||
// Calculate the epoch for current date assuming GMT.
|
||||
$epoch = gmmktime($hours, $minutes, $seconds, $month, $day, $year);
|
||||
if ($match[10] != 'Z') { // Z is zulu time, aka GMT
|
||||
list($tz_mod, $tz_hour, $tz_min) = array($match[8], $match[9], $match[10]);
|
||||
// Zero out the variables.
|
||||
if (!$tz_hour) {
|
||||
$tz_hour = 0;
|
||||
}
|
||||
if (!$tz_min) {
|
||||
$tz_min = 0;
|
||||
}
|
||||
$offset_secs = (($tz_hour * 60) + $tz_min) * 60;
|
||||
// Is timezone ahead of GMT? If yes, subtract offset.
|
||||
if ($tz_mod == '+') {
|
||||
$offset_secs *= -1;
|
||||
}
|
||||
$epoch += $offset_secs;
|
||||
}
|
||||
return $epoch;
|
||||
}
|
||||
else {
|
||||
return FALSE;
|
||||
}
|
||||
}
|
213
modules/aggregator/aggregator.processor.inc
Normal file
213
modules/aggregator/aggregator.processor.inc
Normal file
|
@ -0,0 +1,213 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Processor functions for the aggregator module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Implements hook_aggregator_process_info().
|
||||
*/
|
||||
function aggregator_aggregator_process_info() {
|
||||
return array(
|
||||
'title' => t('Default processor'),
|
||||
'description' => t('Creates lightweight records from feed items.'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_aggregator_process().
|
||||
*/
|
||||
function aggregator_aggregator_process($feed) {
|
||||
if (is_object($feed)) {
|
||||
if (is_array($feed->items)) {
|
||||
foreach ($feed->items as $item) {
|
||||
// Save this item. Try to avoid duplicate entries as much as possible. If
|
||||
// we find a duplicate entry, we resolve it and pass along its ID is such
|
||||
// that we can update it if needed.
|
||||
if (!empty($item['guid'])) {
|
||||
$entry = db_query("SELECT iid, timestamp FROM {aggregator_item} WHERE fid = :fid AND guid = :guid", array(':fid' => $feed->fid, ':guid' => $item['guid']))->fetchObject();
|
||||
}
|
||||
elseif ($item['link'] && $item['link'] != $feed->link && $item['link'] != $feed->url) {
|
||||
$entry = db_query("SELECT iid, timestamp FROM {aggregator_item} WHERE fid = :fid AND link = :link", array(':fid' => $feed->fid, ':link' => $item['link']))->fetchObject();
|
||||
}
|
||||
else {
|
||||
$entry = db_query("SELECT iid, timestamp FROM {aggregator_item} WHERE fid = :fid AND title = :title", array(':fid' => $feed->fid, ':title' => $item['title']))->fetchObject();
|
||||
}
|
||||
if (!$item['timestamp']) {
|
||||
$item['timestamp'] = isset($entry->timestamp) ? $entry->timestamp : REQUEST_TIME;
|
||||
}
|
||||
|
||||
// Make sure the item title and author fit in the 255 varchar column.
|
||||
$item['title'] = truncate_utf8($item['title'], 255, TRUE, TRUE);
|
||||
$item['author'] = truncate_utf8($item['author'], 255, TRUE, TRUE);
|
||||
aggregator_save_item(array('iid' => (isset($entry->iid) ? $entry->iid : ''), 'fid' => $feed->fid, 'timestamp' => $item['timestamp'], 'title' => $item['title'], 'link' => $item['link'], 'author' => $item['author'], 'description' => $item['description'], 'guid' => $item['guid']));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_aggregator_remove().
|
||||
*/
|
||||
function aggregator_aggregator_remove($feed) {
|
||||
$iids = db_query('SELECT iid FROM {aggregator_item} WHERE fid = :fid', array(':fid' => $feed->fid))->fetchCol();
|
||||
if ($iids) {
|
||||
db_delete('aggregator_category_item')
|
||||
->condition('iid', $iids, 'IN')
|
||||
->execute();
|
||||
}
|
||||
db_delete('aggregator_item')
|
||||
->condition('fid', $feed->fid)
|
||||
->execute();
|
||||
|
||||
drupal_set_message(t('The news items from %site have been removed.', array('%site' => $feed->title)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_form_aggregator_admin_form_alter().
|
||||
*
|
||||
* Form alter aggregator module's own form to keep processor functionality
|
||||
* separate from aggregator API functionality.
|
||||
*/
|
||||
function aggregator_form_aggregator_admin_form_alter(&$form, $form_state) {
|
||||
if (in_array('aggregator', variable_get('aggregator_processors', array('aggregator')))) {
|
||||
$info = module_invoke('aggregator', 'aggregator_process_info');
|
||||
$items = drupal_map_assoc(array(3, 5, 10, 15, 20, 25), '_aggregator_items');
|
||||
$period = drupal_map_assoc(array(3600, 10800, 21600, 32400, 43200, 86400, 172800, 259200, 604800, 1209600, 2419200, 4838400, 9676800), 'format_interval');
|
||||
$period[AGGREGATOR_CLEAR_NEVER] = t('Never');
|
||||
|
||||
// Only wrap into a collapsible fieldset if there is a basic configuration.
|
||||
if (isset($form['basic_conf'])) {
|
||||
$form['modules']['aggregator'] = array(
|
||||
'#type' => 'fieldset',
|
||||
'#title' => t('Default processor settings'),
|
||||
'#description' => $info['description'],
|
||||
'#collapsible' => TRUE,
|
||||
'#collapsed' => !in_array('aggregator', variable_get('aggregator_processors', array('aggregator'))),
|
||||
);
|
||||
}
|
||||
else {
|
||||
$form['modules']['aggregator'] = array();
|
||||
}
|
||||
|
||||
$form['modules']['aggregator']['aggregator_summary_items'] = array(
|
||||
'#type' => 'select',
|
||||
'#title' => t('Number of items shown in listing pages'),
|
||||
'#default_value' => variable_get('aggregator_summary_items', 3),
|
||||
'#empty_value' => 0,
|
||||
'#options' => $items,
|
||||
);
|
||||
|
||||
$form['modules']['aggregator']['aggregator_clear'] = array(
|
||||
'#type' => 'select',
|
||||
'#title' => t('Discard items older than'),
|
||||
'#default_value' => variable_get('aggregator_clear', 9676800),
|
||||
'#options' => $period,
|
||||
'#description' => t('Requires a correctly configured <a href="@cron">cron maintenance task</a>.', array('@cron' => url('admin/reports/status'))),
|
||||
);
|
||||
|
||||
$form['modules']['aggregator']['aggregator_category_selector'] = array(
|
||||
'#type' => 'radios',
|
||||
'#title' => t('Select categories using'),
|
||||
'#default_value' => variable_get('aggregator_category_selector', 'checkboxes'),
|
||||
'#options' => array('checkboxes' => t('checkboxes'),
|
||||
'select' => t('multiple selector')),
|
||||
'#description' => t('For a small number of categories, checkboxes are easier to use, while a multiple selector works well with large numbers of categories.'),
|
||||
);
|
||||
$form['modules']['aggregator']['aggregator_teaser_length'] = array(
|
||||
'#type' => 'select',
|
||||
'#title' => t('Length of trimmed description'),
|
||||
'#default_value' => variable_get('aggregator_teaser_length', 600),
|
||||
'#options' => drupal_map_assoc(array(0, 200, 400, 600, 800, 1000, 1200, 1400, 1600, 1800, 2000), '_aggregator_characters'),
|
||||
'#description' => t("The maximum number of characters used in the trimmed version of content.")
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates display text for teaser length option values.
|
||||
*
|
||||
* Callback for drupal_map_assoc() within
|
||||
* aggregator_form_aggregator_admin_form_alter().
|
||||
*
|
||||
* @param $length
|
||||
* The desired length of teaser text, in bytes.
|
||||
*
|
||||
* @return
|
||||
* A translated string explaining the teaser string length.
|
||||
*/
|
||||
function _aggregator_characters($length) {
|
||||
return ($length == 0) ? t('Unlimited') : format_plural($length, '1 character', '@count characters');
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds/edits/deletes an aggregator item.
|
||||
*
|
||||
* @param $edit
|
||||
* An associative array describing the item to be added/edited/deleted.
|
||||
*/
|
||||
function aggregator_save_item($edit) {
|
||||
if ($edit['title'] && empty($edit['iid'])) {
|
||||
$edit['iid'] = db_insert('aggregator_item')
|
||||
->fields(array(
|
||||
'title' => $edit['title'],
|
||||
'link' => $edit['link'],
|
||||
'author' => $edit['author'],
|
||||
'description' => $edit['description'],
|
||||
'guid' => $edit['guid'],
|
||||
'timestamp' => $edit['timestamp'],
|
||||
'fid' => $edit['fid'],
|
||||
))
|
||||
->execute();
|
||||
}
|
||||
if ($edit['iid'] && !$edit['title']) {
|
||||
db_delete('aggregator_item')
|
||||
->condition('iid', $edit['iid'])
|
||||
->execute();
|
||||
db_delete('aggregator_category_item')
|
||||
->condition('iid', $edit['iid'])
|
||||
->execute();
|
||||
}
|
||||
elseif ($edit['title'] && $edit['link']) {
|
||||
// file the items in the categories indicated by the feed
|
||||
$result = db_query('SELECT cid FROM {aggregator_category_feed} WHERE fid = :fid', array(':fid' => $edit['fid']));
|
||||
foreach ($result as $category) {
|
||||
db_merge('aggregator_category_item')
|
||||
->key(array(
|
||||
'iid' => $edit['iid'],
|
||||
'cid' => $category->cid,
|
||||
))
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Expires items from a feed depending on expiration settings.
|
||||
*
|
||||
* @param $feed
|
||||
* Object describing feed.
|
||||
*/
|
||||
function aggregator_expire($feed) {
|
||||
$aggregator_clear = variable_get('aggregator_clear', 9676800);
|
||||
|
||||
if ($aggregator_clear != AGGREGATOR_CLEAR_NEVER) {
|
||||
// Remove all items that are older than flush item timer.
|
||||
$age = REQUEST_TIME - $aggregator_clear;
|
||||
$iids = db_query('SELECT iid FROM {aggregator_item} WHERE fid = :fid AND timestamp < :timestamp', array(
|
||||
':fid' => $feed->fid,
|
||||
':timestamp' => $age,
|
||||
))
|
||||
->fetchCol();
|
||||
if ($iids) {
|
||||
db_delete('aggregator_category_item')
|
||||
->condition('iid', $iids, 'IN')
|
||||
->execute();
|
||||
db_delete('aggregator_item')
|
||||
->condition('iid', $iids, 'IN')
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
}
|
1070
modules/aggregator/aggregator.test
Normal file
1070
modules/aggregator/aggregator.test
Normal file
File diff suppressed because it is too large
Load diff
12
modules/aggregator/tests/aggregator_test.info
Normal file
12
modules/aggregator/tests/aggregator_test.info
Normal file
|
@ -0,0 +1,12 @@
|
|||
name = "Aggregator module tests"
|
||||
description = "Support module for aggregator related testing."
|
||||
package = Testing
|
||||
version = VERSION
|
||||
core = 7.x
|
||||
hidden = TRUE
|
||||
|
||||
; Information added by Drupal.org packaging script on 2017-06-21
|
||||
version = "7.56"
|
||||
project = "drupal"
|
||||
datestamp = "1498069849"
|
||||
|
58
modules/aggregator/tests/aggregator_test.module
Normal file
58
modules/aggregator/tests/aggregator_test.module
Normal file
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Implements hook_menu().
|
||||
*/
|
||||
function aggregator_test_menu() {
|
||||
$items['aggregator/test-feed'] = array(
|
||||
'title' => 'Test feed static last modified date',
|
||||
'description' => "A cached test feed with a static last modified date.",
|
||||
'page callback' => 'aggregator_test_feed',
|
||||
'access arguments' => array('access content'),
|
||||
'type' => MENU_CALLBACK,
|
||||
);
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Page callback. Generates a test feed and simulates last-modified and etags.
|
||||
*
|
||||
* @param $use_last_modified
|
||||
* Set TRUE to send a last modified header.
|
||||
* @param $use_etag
|
||||
* Set TRUE to send an etag.
|
||||
*/
|
||||
function aggregator_test_feed($use_last_modified = FALSE, $use_etag = FALSE) {
|
||||
$last_modified = strtotime('Sun, 19 Nov 1978 05:00:00 GMT');
|
||||
$etag = drupal_hash_base64($last_modified);
|
||||
|
||||
$if_modified_since = isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) ? strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) : FALSE;
|
||||
$if_none_match = isset($_SERVER['HTTP_IF_NONE_MATCH']) ? stripslashes($_SERVER['HTTP_IF_NONE_MATCH']) : FALSE;
|
||||
|
||||
// Send appropriate response. We respond with a 304 not modified on either
|
||||
// etag or on last modified.
|
||||
if ($use_last_modified) {
|
||||
drupal_add_http_header('Last-Modified', gmdate(DATE_RFC7231, $last_modified));
|
||||
}
|
||||
if ($use_etag) {
|
||||
drupal_add_http_header('ETag', $etag);
|
||||
}
|
||||
// Return 304 not modified if either last modified or etag match.
|
||||
if ($last_modified == $if_modified_since || $etag == $if_none_match) {
|
||||
drupal_add_http_header('Status', '304 Not Modified');
|
||||
return;
|
||||
}
|
||||
|
||||
// The following headers force validation of cache:
|
||||
drupal_add_http_header('Expires', 'Sun, 19 Nov 1978 05:00:00 GMT');
|
||||
drupal_add_http_header('Cache-Control', 'must-revalidate');
|
||||
drupal_add_http_header('Content-Type', 'application/rss+xml; charset=utf-8');
|
||||
|
||||
// Read actual feed from file.
|
||||
$file_name = DRUPAL_ROOT . '/' . drupal_get_path('module', 'aggregator') . '/tests/aggregator_test_rss091.xml';
|
||||
$handle = fopen($file_name, 'r');
|
||||
$feed = fread($handle, filesize($file_name));
|
||||
fclose($handle);
|
||||
|
||||
print $feed;
|
||||
}
|
20
modules/aggregator/tests/aggregator_test_atom.xml
Normal file
20
modules/aggregator/tests/aggregator_test_atom.xml
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
|
||||
<title>Example Feed</title>
|
||||
<link href="http://example.org/" />
|
||||
<updated>2003-12-13T18:30:02Z</updated>
|
||||
<author>
|
||||
<name>John Doe</name>
|
||||
</author>
|
||||
<id>urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6</id>
|
||||
|
||||
<entry>
|
||||
<title>Atom-Powered Robots Run Amok</title>
|
||||
<link href="http://example.org/2003/12/13/atom03" />
|
||||
<id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>
|
||||
<updated>2003-12-13T18:30:02Z</updated>
|
||||
<summary>Some text.</summary>
|
||||
</entry>
|
||||
|
||||
</feed>
|
41
modules/aggregator/tests/aggregator_test_rss091.xml
Normal file
41
modules/aggregator/tests/aggregator_test_rss091.xml
Normal file
|
@ -0,0 +1,41 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="0.91">
|
||||
<channel>
|
||||
<title>Example</title>
|
||||
<link>http://example.com</link>
|
||||
<description>Example updates</description>
|
||||
<language>en-us</language>
|
||||
<copyright>Copyright 2000, Example team.</copyright>
|
||||
<managingEditor>editor@example.com</managingEditor>
|
||||
<webMaster>webmaster@example.com</webMaster>
|
||||
<image>
|
||||
<title>Example</title>
|
||||
<url>http://example.com/images/druplicon.png</url>
|
||||
<link>http://example.com</link>
|
||||
<width>88</width>
|
||||
<height>100</height>
|
||||
<description>Example updates</description>
|
||||
</image>
|
||||
<item>
|
||||
<title>First example feed item title</title>
|
||||
<link>http://example.com/example-turns-one</link>
|
||||
<description>First example feed item description.</description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Second example feed item title. This title is extremely long so that it exceeds the 255 character limit for titles in feed item storage. In fact it's so long that this sentence isn't long enough so I'm rambling a bit to make it longer, nearly there now. Ah now it's long enough so I'll shut up.</title>
|
||||
<link>http://example.com/example-turns-two</link>
|
||||
<description>Second example feed item description.</description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Long link feed item title.</title>
|
||||
<link>http://example.com/tomorrow/and/tomorrow/and/tomorrow/creeps/in/this/petty/pace/from/day/to/day/to/the/last/syllable/of/recorded/time/and/all/our/yesterdays/have/lighted/fools/the/way/to/dusty/death/out/out/brief/candle/life/is/but/a/walking/shadow/a/poor/player/that/struts/and/frets/his/hour/upon/the/stage/and/is/heard/no/more/it/is/a/tale/told/by/an/idiot/full/of/sound/and/fury/signifying/nothing</link>
|
||||
<description>Long link feed item description.</description>
|
||||
</item>
|
||||
<item>
|
||||
<title>Long author feed item title.</title>
|
||||
<link>http://example.com/long/author</link>
|
||||
<author>I wanted to get out and walk eastward toward the park through the soft twilight, but each time I tried to go I became entangled in some wild, strident argument which pulled me back, as if with ropes, into my chair. Yet high over the city our line of yellow windows must have contributed their share of human secrecy to the casual watcher in the darkening streets, and I was him too, looking up and wondering. I was within and without, simultaneously enchanted and repelled by the inexhaustible variety of life.</author>
|
||||
<description>Long author feed item description.</description>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
14
modules/aggregator/tests/aggregator_test_title_entities.xml
Normal file
14
modules/aggregator/tests/aggregator_test_title_entities.xml
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="0.91">
|
||||
<channel>
|
||||
<title>Example with Entities</title>
|
||||
<link>http://example.com</link>
|
||||
<description>Example RSS Feed With HTML Entities in Title</description>
|
||||
<language>en-us</language>
|
||||
<item>
|
||||
<title>Quote" Amp&</title>
|
||||
<link>http://example.com/example-turns-one</link>
|
||||
<description>Some text.</description>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
69
modules/block/block-admin-display-form.tpl.php
Normal file
69
modules/block/block-admin-display-form.tpl.php
Normal file
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Default theme implementation to configure blocks.
|
||||
*
|
||||
* Available variables:
|
||||
* - $block_regions: An array of regions. Keyed by name with the title as value.
|
||||
* - $block_listing: An array of blocks keyed by region and then delta.
|
||||
* - $form_submit: Form submit button.
|
||||
*
|
||||
* Each $block_listing[$region] contains an array of blocks for that region.
|
||||
*
|
||||
* Each $data in $block_listing[$region] contains:
|
||||
* - $data->region_title: Region title for the listed block.
|
||||
* - $data->block_title: Block title.
|
||||
* - $data->region_select: Drop-down menu for assigning a region.
|
||||
* - $data->weight_select: Drop-down menu for setting weights.
|
||||
* - $data->configure_link: Block configuration link.
|
||||
* - $data->delete_link: For deleting user added blocks.
|
||||
*
|
||||
* @see template_preprocess_block_admin_display_form()
|
||||
* @see theme_block_admin_display()
|
||||
*
|
||||
* @ingroup themeable
|
||||
*/
|
||||
?>
|
||||
<?php
|
||||
// Add table javascript.
|
||||
drupal_add_js('misc/tableheader.js');
|
||||
drupal_add_js(drupal_get_path('module', 'block') . '/block.js');
|
||||
foreach ($block_regions as $region => $title) {
|
||||
drupal_add_tabledrag('blocks', 'match', 'sibling', 'block-region-select', 'block-region-' . $region, NULL, FALSE);
|
||||
drupal_add_tabledrag('blocks', 'order', 'sibling', 'block-weight', 'block-weight-' . $region);
|
||||
}
|
||||
?>
|
||||
<table id="blocks" class="sticky-enabled">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?php print t('Block'); ?></th>
|
||||
<th><?php print t('Region'); ?></th>
|
||||
<th><?php print t('Weight'); ?></th>
|
||||
<th colspan="2"><?php print t('Operations'); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php $row = 0; ?>
|
||||
<?php foreach ($block_regions as $region => $title): ?>
|
||||
<tr class="region-title region-title-<?php print $region?>">
|
||||
<td colspan="5"><?php print $title; ?></td>
|
||||
</tr>
|
||||
<tr class="region-message region-<?php print $region?>-message <?php print empty($block_listing[$region]) ? 'region-empty' : 'region-populated'; ?>">
|
||||
<td colspan="5"><em><?php print t('No blocks in this region'); ?></em></td>
|
||||
</tr>
|
||||
<?php foreach ($block_listing[$region] as $delta => $data): ?>
|
||||
<tr class="draggable <?php print $row % 2 == 0 ? 'odd' : 'even'; ?><?php print $data->row_class ? ' ' . $data->row_class : ''; ?>">
|
||||
<td class="block"><?php print $data->block_title; ?></td>
|
||||
<td><?php print $data->region_select; ?></td>
|
||||
<td><?php print $data->weight_select; ?></td>
|
||||
<td><?php print $data->configure_link; ?></td>
|
||||
<td><?php print $data->delete_link; ?></td>
|
||||
</tr>
|
||||
<?php $row++; ?>
|
||||
<?php endforeach; ?>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<?php print $form_submit; ?>
|
700
modules/block/block.admin.inc
Normal file
700
modules/block/block.admin.inc
Normal file
|
@ -0,0 +1,700 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Admin page callbacks for the block module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Menu callback for admin/structure/block/demo.
|
||||
*/
|
||||
function block_admin_demo($theme = NULL) {
|
||||
drupal_add_css(drupal_get_path('module', 'block') . '/block.css');
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Menu callback for admin/structure/block.
|
||||
*
|
||||
* @param $theme
|
||||
* The theme to display the administration page for. If not provided, defaults
|
||||
* to the currently used theme.
|
||||
*/
|
||||
function block_admin_display($theme = NULL) {
|
||||
global $theme_key;
|
||||
|
||||
drupal_theme_initialize();
|
||||
|
||||
if (!isset($theme)) {
|
||||
// If theme is not specifically set, rehash for the current theme.
|
||||
$theme = $theme_key;
|
||||
}
|
||||
|
||||
// Fetch and sort blocks.
|
||||
$blocks = block_admin_display_prepare_blocks($theme);
|
||||
|
||||
return drupal_get_form('block_admin_display_form', $blocks, $theme);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares a list of blocks for display on the blocks administration page.
|
||||
*
|
||||
* @param $theme
|
||||
* The machine-readable name of the theme whose blocks should be returned.
|
||||
*
|
||||
* @return
|
||||
* An array of blocks, as returned by _block_rehash(), sorted by region in
|
||||
* preparation for display on the blocks administration page.
|
||||
*
|
||||
* @see block_admin_display_form()
|
||||
*/
|
||||
function block_admin_display_prepare_blocks($theme) {
|
||||
$blocks = _block_rehash($theme);
|
||||
$compare_theme = &drupal_static('_block_compare:theme');
|
||||
$compare_theme = $theme;
|
||||
usort($blocks, '_block_compare');
|
||||
return $blocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Form constructor for the main block administration form.
|
||||
*
|
||||
* @param $blocks
|
||||
* An array of blocks, as returned by block_admin_display_prepare_blocks().
|
||||
* @param $theme
|
||||
* A string representing the name of the theme to edit blocks for.
|
||||
* @param $block_regions
|
||||
* (optional) An array of regions in which the blocks will be allowed to be
|
||||
* placed. Defaults to all visible regions for the theme whose blocks are
|
||||
* being configured. In all cases, a dummy region for disabled blocks will
|
||||
* also be displayed.
|
||||
*
|
||||
* @return
|
||||
* An array representing the form definition.
|
||||
*
|
||||
* @ingroup forms
|
||||
* @see block_admin_display_form_submit()
|
||||
*/
|
||||
function block_admin_display_form($form, &$form_state, $blocks, $theme, $block_regions = NULL) {
|
||||
|
||||
$form['#attached']['css'] = array(drupal_get_path('module', 'block') . '/block.css');
|
||||
|
||||
// Get a list of block regions if one was not provided.
|
||||
if (!isset($block_regions)) {
|
||||
$block_regions = system_region_list($theme, REGIONS_VISIBLE);
|
||||
}
|
||||
|
||||
// Weights range from -delta to +delta, so delta should be at least half
|
||||
// of the amount of blocks present. This makes sure all blocks in the same
|
||||
// region get an unique weight.
|
||||
$weight_delta = round(count($blocks) / 2);
|
||||
|
||||
// Build the form tree.
|
||||
$form['edited_theme'] = array(
|
||||
'#type' => 'value',
|
||||
'#value' => $theme,
|
||||
);
|
||||
$form['block_regions'] = array(
|
||||
'#type' => 'value',
|
||||
// Add a last region for disabled blocks.
|
||||
'#value' => $block_regions + array(BLOCK_REGION_NONE => BLOCK_REGION_NONE),
|
||||
);
|
||||
$form['blocks'] = array();
|
||||
$form['#tree'] = TRUE;
|
||||
|
||||
foreach ($blocks as $i => $block) {
|
||||
$key = $block['module'] . '_' . $block['delta'];
|
||||
$form['blocks'][$key]['module'] = array(
|
||||
'#type' => 'value',
|
||||
'#value' => $block['module'],
|
||||
);
|
||||
$form['blocks'][$key]['delta'] = array(
|
||||
'#type' => 'value',
|
||||
'#value' => $block['delta'],
|
||||
);
|
||||
$form['blocks'][$key]['info'] = array(
|
||||
'#markup' => check_plain($block['info']),
|
||||
);
|
||||
$form['blocks'][$key]['theme'] = array(
|
||||
'#type' => 'hidden',
|
||||
'#value' => $theme,
|
||||
);
|
||||
$form['blocks'][$key]['weight'] = array(
|
||||
'#type' => 'weight',
|
||||
'#default_value' => $block['weight'],
|
||||
'#delta' => $weight_delta,
|
||||
'#title_display' => 'invisible',
|
||||
'#title' => t('Weight for @block block', array('@block' => $block['info'])),
|
||||
);
|
||||
$form['blocks'][$key]['region'] = array(
|
||||
'#type' => 'select',
|
||||
'#default_value' => $block['region'] != BLOCK_REGION_NONE ? $block['region'] : NULL,
|
||||
'#empty_value' => BLOCK_REGION_NONE,
|
||||
'#title_display' => 'invisible',
|
||||
'#title' => t('Region for @block block', array('@block' => $block['info'])),
|
||||
'#options' => $block_regions,
|
||||
);
|
||||
$form['blocks'][$key]['configure'] = array(
|
||||
'#type' => 'link',
|
||||
'#title' => t('configure'),
|
||||
'#href' => 'admin/structure/block/manage/' . $block['module'] . '/' . $block['delta'] . '/configure',
|
||||
);
|
||||
if ($block['module'] == 'block') {
|
||||
$form['blocks'][$key]['delete'] = array(
|
||||
'#type' => 'link',
|
||||
'#title' => t('delete'),
|
||||
'#href' => 'admin/structure/block/manage/' . $block['module'] . '/' . $block['delta'] . '/delete',
|
||||
);
|
||||
}
|
||||
}
|
||||
// Do not allow disabling the main system content block when it is present.
|
||||
if (isset($form['blocks']['system_main']['region'])) {
|
||||
$form['blocks']['system_main']['region']['#required'] = TRUE;
|
||||
}
|
||||
|
||||
$form['actions'] = array(
|
||||
'#tree' => FALSE,
|
||||
'#type' => 'actions',
|
||||
);
|
||||
$form['actions']['submit'] = array(
|
||||
'#type' => 'submit',
|
||||
'#value' => t('Save blocks'),
|
||||
);
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Form submission handler for block_admin_display_form().
|
||||
*
|
||||
* @see block_admin_display_form()
|
||||
*/
|
||||
function block_admin_display_form_submit($form, &$form_state) {
|
||||
$transaction = db_transaction();
|
||||
try {
|
||||
foreach ($form_state['values']['blocks'] as $block) {
|
||||
$block['status'] = (int) ($block['region'] != BLOCK_REGION_NONE);
|
||||
$block['region'] = $block['status'] ? $block['region'] : '';
|
||||
db_update('block')
|
||||
->fields(array(
|
||||
'status' => $block['status'],
|
||||
'weight' => $block['weight'],
|
||||
'region' => $block['region'],
|
||||
))
|
||||
->condition('module', $block['module'])
|
||||
->condition('delta', $block['delta'])
|
||||
->condition('theme', $block['theme'])
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
catch (Exception $e) {
|
||||
$transaction->rollback();
|
||||
watchdog_exception('block', $e);
|
||||
throw $e;
|
||||
}
|
||||
drupal_set_message(t('The block settings have been updated.'));
|
||||
cache_clear_all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts active blocks by region, then by weight; sorts inactive blocks by name.
|
||||
*
|
||||
* Callback for usort() in block_admin_display_prepare_blocks().
|
||||
*/
|
||||
function _block_compare($a, $b) {
|
||||
global $theme_key;
|
||||
|
||||
// Theme should be set before calling this function, or the current theme
|
||||
// is being used.
|
||||
$theme = &drupal_static(__FUNCTION__ . ':theme');
|
||||
if (!isset($theme)) {
|
||||
$theme = $theme_key;
|
||||
}
|
||||
|
||||
$regions = &drupal_static(__FUNCTION__ . ':regions');
|
||||
// We need the region list to correctly order by region.
|
||||
if (!isset($regions)) {
|
||||
$regions = array_flip(array_keys(system_region_list($theme)));
|
||||
$regions[BLOCK_REGION_NONE] = count($regions);
|
||||
}
|
||||
|
||||
// Separate enabled from disabled.
|
||||
$status = $b['status'] - $a['status'];
|
||||
if ($status) {
|
||||
return $status;
|
||||
}
|
||||
// Sort by region (in the order defined by theme .info file).
|
||||
if ((!empty($a['region']) && !empty($b['region'])) && ($place = ($regions[$a['region']] - $regions[$b['region']]))) {
|
||||
return $place;
|
||||
}
|
||||
// Sort by weight, unless disabled.
|
||||
if ($a['region'] != BLOCK_REGION_NONE) {
|
||||
$weight = $a['weight'] - $b['weight'];
|
||||
if ($weight) {
|
||||
return $weight;
|
||||
}
|
||||
}
|
||||
// Sort by title.
|
||||
return strcmp($a['info'], $b['info']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Form constructor for the block configuration form.
|
||||
*
|
||||
* Also used by block_add_block_form() for adding a new custom block.
|
||||
*
|
||||
* @param $module
|
||||
* Name of the module that implements the block to be configured.
|
||||
* @param $delta
|
||||
* Unique ID of the block within the context of $module.
|
||||
*
|
||||
* @see block_admin_configure_validate()
|
||||
* @see block_admin_configure_submit()
|
||||
* @ingroup forms
|
||||
*/
|
||||
function block_admin_configure($form, &$form_state, $module, $delta) {
|
||||
$block = block_load($module, $delta);
|
||||
$form['module'] = array(
|
||||
'#type' => 'value',
|
||||
'#value' => $block->module,
|
||||
);
|
||||
$form['delta'] = array(
|
||||
'#type' => 'value',
|
||||
'#value' => $block->delta,
|
||||
);
|
||||
|
||||
// Get the block subject for the page title.
|
||||
$info = module_invoke($block->module, 'block_info');
|
||||
if (isset($info[$block->delta])) {
|
||||
drupal_set_title(t("'%name' block", array('%name' => $info[$block->delta]['info'])), PASS_THROUGH);
|
||||
}
|
||||
|
||||
$form['settings']['title'] = array(
|
||||
'#type' => 'textfield',
|
||||
'#title' => t('Block title'),
|
||||
'#maxlength' => 255,
|
||||
'#description' => $block->module == 'block' ? t('The title of the block as shown to the user.') : t('Override the default title for the block. Use <em>!placeholder</em> to display no title, or leave blank to use the default block title.', array('!placeholder' => '<none>')),
|
||||
'#default_value' => isset($block->title) ? $block->title : '',
|
||||
'#weight' => -19,
|
||||
);
|
||||
|
||||
// Module-specific block configuration.
|
||||
if ($settings = module_invoke($block->module, 'block_configure', $block->delta)) {
|
||||
foreach ($settings as $k => $v) {
|
||||
$form['settings'][$k] = $v;
|
||||
}
|
||||
}
|
||||
|
||||
// Region settings.
|
||||
$form['regions'] = array(
|
||||
'#type' => 'fieldset',
|
||||
'#title' => t('Region settings'),
|
||||
'#collapsible' => FALSE,
|
||||
'#description' => t('Specify in which themes and regions this block is displayed.'),
|
||||
'#tree' => TRUE,
|
||||
);
|
||||
|
||||
$theme_default = variable_get('theme_default', 'bartik');
|
||||
$admin_theme = variable_get('admin_theme');
|
||||
foreach (list_themes() as $key => $theme) {
|
||||
// Only display enabled themes
|
||||
if ($theme->status) {
|
||||
$region = db_query("SELECT region FROM {block} WHERE module = :module AND delta = :delta AND theme = :theme", array(
|
||||
':module' => $block->module,
|
||||
':delta' => $block->delta,
|
||||
':theme' => $key,
|
||||
))->fetchField();
|
||||
|
||||
// Use a meaningful title for the main site theme and administrative
|
||||
// theme.
|
||||
$theme_title = $theme->info['name'];
|
||||
if ($key == $theme_default) {
|
||||
$theme_title = t('!theme (default theme)', array('!theme' => $theme_title));
|
||||
}
|
||||
elseif ($admin_theme && $key == $admin_theme) {
|
||||
$theme_title = t('!theme (administration theme)', array('!theme' => $theme_title));
|
||||
}
|
||||
$form['regions'][$key] = array(
|
||||
'#type' => 'select',
|
||||
'#title' => $theme_title,
|
||||
'#default_value' => !empty($region) && $region != -1 ? $region : NULL,
|
||||
'#empty_value' => BLOCK_REGION_NONE,
|
||||
'#options' => system_region_list($key, REGIONS_VISIBLE),
|
||||
'#weight' => ($key == $theme_default ? 9 : 10),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Visibility settings.
|
||||
$form['visibility_title'] = array(
|
||||
'#type' => 'item',
|
||||
'#title' => t('Visibility settings'),
|
||||
);
|
||||
$form['visibility'] = array(
|
||||
'#type' => 'vertical_tabs',
|
||||
'#attached' => array(
|
||||
'js' => array(drupal_get_path('module', 'block') . '/block.js'),
|
||||
),
|
||||
);
|
||||
|
||||
// Per-path visibility.
|
||||
$form['visibility']['path'] = array(
|
||||
'#type' => 'fieldset',
|
||||
'#title' => t('Pages'),
|
||||
'#collapsible' => TRUE,
|
||||
'#collapsed' => TRUE,
|
||||
'#group' => 'visibility',
|
||||
'#weight' => 0,
|
||||
);
|
||||
|
||||
$access = user_access('use PHP for settings');
|
||||
if (isset($block->visibility) && $block->visibility == BLOCK_VISIBILITY_PHP && !$access) {
|
||||
$form['visibility']['path']['visibility'] = array(
|
||||
'#type' => 'value',
|
||||
'#value' => BLOCK_VISIBILITY_PHP,
|
||||
);
|
||||
$form['visibility']['path']['pages'] = array(
|
||||
'#type' => 'value',
|
||||
'#value' => isset($block->pages) ? $block->pages : '',
|
||||
);
|
||||
}
|
||||
else {
|
||||
$options = array(
|
||||
BLOCK_VISIBILITY_NOTLISTED => t('All pages except those listed'),
|
||||
BLOCK_VISIBILITY_LISTED => t('Only the listed pages'),
|
||||
);
|
||||
$description = t("Specify pages by using their paths. Enter one path per line. The '*' character is a wildcard. Example paths are %blog for the blog page and %blog-wildcard for every personal blog. %front is the front page.", array('%blog' => 'blog', '%blog-wildcard' => 'blog/*', '%front' => '<front>'));
|
||||
|
||||
if (module_exists('php') && $access) {
|
||||
$options += array(BLOCK_VISIBILITY_PHP => t('Pages on which this PHP code returns <code>TRUE</code> (experts only)'));
|
||||
$title = t('Pages or PHP code');
|
||||
$description .= ' ' . t('If the PHP option is chosen, enter PHP code between %php. Note that executing incorrect PHP code can break your Drupal site.', array('%php' => '<?php ?>'));
|
||||
}
|
||||
else {
|
||||
$title = t('Pages');
|
||||
}
|
||||
$form['visibility']['path']['visibility'] = array(
|
||||
'#type' => 'radios',
|
||||
'#title' => t('Show block on specific pages'),
|
||||
'#options' => $options,
|
||||
'#default_value' => isset($block->visibility) ? $block->visibility : BLOCK_VISIBILITY_NOTLISTED,
|
||||
);
|
||||
$form['visibility']['path']['pages'] = array(
|
||||
'#type' => 'textarea',
|
||||
'#title' => '<span class="element-invisible">' . $title . '</span>',
|
||||
'#default_value' => isset($block->pages) ? $block->pages : '',
|
||||
'#description' => $description,
|
||||
);
|
||||
}
|
||||
|
||||
// Per-role visibility.
|
||||
$default_role_options = db_query("SELECT rid FROM {block_role} WHERE module = :module AND delta = :delta", array(
|
||||
':module' => $block->module,
|
||||
':delta' => $block->delta,
|
||||
))->fetchCol();
|
||||
$role_options = array_map('check_plain', user_roles());
|
||||
$form['visibility']['role'] = array(
|
||||
'#type' => 'fieldset',
|
||||
'#title' => t('Roles'),
|
||||
'#collapsible' => TRUE,
|
||||
'#collapsed' => TRUE,
|
||||
'#group' => 'visibility',
|
||||
'#weight' => 10,
|
||||
);
|
||||
$form['visibility']['role']['roles'] = array(
|
||||
'#type' => 'checkboxes',
|
||||
'#title' => t('Show block for specific roles'),
|
||||
'#default_value' => $default_role_options,
|
||||
'#options' => $role_options,
|
||||
'#description' => t('Show this block only for the selected role(s). If you select no roles, the block will be visible to all users.'),
|
||||
);
|
||||
|
||||
// Per-user visibility.
|
||||
$form['visibility']['user'] = array(
|
||||
'#type' => 'fieldset',
|
||||
'#title' => t('Users'),
|
||||
'#collapsible' => TRUE,
|
||||
'#collapsed' => TRUE,
|
||||
'#group' => 'visibility',
|
||||
'#weight' => 20,
|
||||
);
|
||||
$form['visibility']['user']['custom'] = array(
|
||||
'#type' => 'radios',
|
||||
'#title' => t('Customizable per user'),
|
||||
'#options' => array(
|
||||
BLOCK_CUSTOM_FIXED => t('Not customizable'),
|
||||
BLOCK_CUSTOM_ENABLED => t('Customizable, visible by default'),
|
||||
BLOCK_CUSTOM_DISABLED => t('Customizable, hidden by default'),
|
||||
),
|
||||
'#description' => t('Allow individual users to customize the visibility of this block in their account settings.'),
|
||||
'#default_value' => isset($block->custom) ? $block->custom : BLOCK_CUSTOM_FIXED,
|
||||
);
|
||||
|
||||
$form['actions'] = array('#type' => 'actions');
|
||||
$form['actions']['submit'] = array(
|
||||
'#type' => 'submit',
|
||||
'#value' => t('Save block'),
|
||||
);
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Form validation handler for block_admin_configure().
|
||||
*
|
||||
* @see block_admin_configure()
|
||||
* @see block_admin_configure_submit()
|
||||
*/
|
||||
function block_admin_configure_validate($form, &$form_state) {
|
||||
if ($form_state['values']['module'] == 'block') {
|
||||
$custom_block_exists = (bool) db_query_range('SELECT 1 FROM {block_custom} WHERE bid <> :bid AND info = :info', 0, 1, array(
|
||||
':bid' => $form_state['values']['delta'],
|
||||
':info' => $form_state['values']['info'],
|
||||
))->fetchField();
|
||||
if (empty($form_state['values']['info']) || $custom_block_exists) {
|
||||
form_set_error('info', t('Ensure that each block description is unique.'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Form submission handler for block_admin_configure().
|
||||
*
|
||||
* @see block_admin_configure()
|
||||
* @see block_admin_configure_validate()
|
||||
*/
|
||||
function block_admin_configure_submit($form, &$form_state) {
|
||||
if (!form_get_errors()) {
|
||||
$transaction = db_transaction();
|
||||
try {
|
||||
db_update('block')
|
||||
->fields(array(
|
||||
'visibility' => (int) $form_state['values']['visibility'],
|
||||
'pages' => trim($form_state['values']['pages']),
|
||||
'custom' => (int) $form_state['values']['custom'],
|
||||
'title' => $form_state['values']['title'],
|
||||
))
|
||||
->condition('module', $form_state['values']['module'])
|
||||
->condition('delta', $form_state['values']['delta'])
|
||||
->execute();
|
||||
|
||||
db_delete('block_role')
|
||||
->condition('module', $form_state['values']['module'])
|
||||
->condition('delta', $form_state['values']['delta'])
|
||||
->execute();
|
||||
$query = db_insert('block_role')->fields(array('rid', 'module', 'delta'));
|
||||
foreach (array_filter($form_state['values']['roles']) as $rid) {
|
||||
$query->values(array(
|
||||
'rid' => $rid,
|
||||
'module' => $form_state['values']['module'],
|
||||
'delta' => $form_state['values']['delta'],
|
||||
));
|
||||
}
|
||||
$query->execute();
|
||||
|
||||
// Store regions per theme for this block
|
||||
foreach ($form_state['values']['regions'] as $theme => $region) {
|
||||
db_merge('block')
|
||||
->key(array('theme' => $theme, 'delta' => $form_state['values']['delta'], 'module' => $form_state['values']['module']))
|
||||
->fields(array(
|
||||
'region' => ($region == BLOCK_REGION_NONE ? '' : $region),
|
||||
'pages' => trim($form_state['values']['pages']),
|
||||
'status' => (int) ($region != BLOCK_REGION_NONE),
|
||||
))
|
||||
->execute();
|
||||
}
|
||||
|
||||
module_invoke($form_state['values']['module'], 'block_save', $form_state['values']['delta'], $form_state['values']);
|
||||
}
|
||||
catch (Exception $e) {
|
||||
$transaction->rollback();
|
||||
watchdog_exception('block', $e);
|
||||
throw $e;
|
||||
}
|
||||
drupal_set_message(t('The block configuration has been saved.'));
|
||||
cache_clear_all();
|
||||
$form_state['redirect'] = 'admin/structure/block';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Form constructor for the add block form.
|
||||
*
|
||||
* @see block_add_block_form_validate()
|
||||
* @see block_add_block_form_submit()
|
||||
* @ingroup forms
|
||||
*/
|
||||
function block_add_block_form($form, &$form_state) {
|
||||
return block_admin_configure($form, $form_state, 'block', NULL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Form validation handler for block_add_block_form().
|
||||
*
|
||||
* @see block_add_block_form()
|
||||
* @see block_add_block_form_submit()
|
||||
*/
|
||||
function block_add_block_form_validate($form, &$form_state) {
|
||||
$custom_block_exists = (bool) db_query_range('SELECT 1 FROM {block_custom} WHERE info = :info', 0, 1, array(':info' => $form_state['values']['info']))->fetchField();
|
||||
|
||||
if (empty($form_state['values']['info']) || $custom_block_exists) {
|
||||
form_set_error('info', t('Ensure that each block description is unique.'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Form submission handler for block_add_block_form().
|
||||
*
|
||||
* Saves the new custom block.
|
||||
*
|
||||
* @see block_add_block_form()
|
||||
* @see block_add_block_form_validate()
|
||||
*/
|
||||
function block_add_block_form_submit($form, &$form_state) {
|
||||
$delta = db_insert('block_custom')
|
||||
->fields(array(
|
||||
'body' => $form_state['values']['body']['value'],
|
||||
'info' => $form_state['values']['info'],
|
||||
'format' => $form_state['values']['body']['format'],
|
||||
))
|
||||
->execute();
|
||||
// Store block delta to allow other modules to work with new block.
|
||||
$form_state['values']['delta'] = $delta;
|
||||
|
||||
$query = db_insert('block')->fields(array('visibility', 'pages', 'custom', 'title', 'module', 'theme', 'status', 'weight', 'delta', 'cache'));
|
||||
foreach (list_themes() as $key => $theme) {
|
||||
if ($theme->status) {
|
||||
$query->values(array(
|
||||
'visibility' => (int) $form_state['values']['visibility'],
|
||||
'pages' => trim($form_state['values']['pages']),
|
||||
'custom' => (int) $form_state['values']['custom'],
|
||||
'title' => $form_state['values']['title'],
|
||||
'module' => $form_state['values']['module'],
|
||||
'theme' => $theme->name,
|
||||
'status' => 0,
|
||||
'weight' => 0,
|
||||
'delta' => $delta,
|
||||
'cache' => DRUPAL_NO_CACHE,
|
||||
));
|
||||
}
|
||||
}
|
||||
$query->execute();
|
||||
|
||||
$query = db_insert('block_role')->fields(array('rid', 'module', 'delta'));
|
||||
foreach (array_filter($form_state['values']['roles']) as $rid) {
|
||||
$query->values(array(
|
||||
'rid' => $rid,
|
||||
'module' => $form_state['values']['module'],
|
||||
'delta' => $delta,
|
||||
));
|
||||
}
|
||||
$query->execute();
|
||||
|
||||
// Store regions per theme for this block
|
||||
foreach ($form_state['values']['regions'] as $theme => $region) {
|
||||
db_merge('block')
|
||||
->key(array('theme' => $theme, 'delta' => $delta, 'module' => $form_state['values']['module']))
|
||||
->fields(array(
|
||||
'region' => ($region == BLOCK_REGION_NONE ? '' : $region),
|
||||
'pages' => trim($form_state['values']['pages']),
|
||||
'status' => (int) ($region != BLOCK_REGION_NONE),
|
||||
))
|
||||
->execute();
|
||||
}
|
||||
|
||||
drupal_set_message(t('The block has been created.'));
|
||||
cache_clear_all();
|
||||
$form_state['redirect'] = 'admin/structure/block';
|
||||
}
|
||||
|
||||
/**
|
||||
* Form constructor for the custom block deletion form.
|
||||
*
|
||||
* @param $module
|
||||
* The name of the module that implements the block to be deleted. This should
|
||||
* always equal 'block' since it only allows custom blocks to be deleted.
|
||||
* @param $delta
|
||||
* The unique ID of the block within the context of $module.
|
||||
*
|
||||
* @see block_custom_block_delete_submit()
|
||||
*/
|
||||
function block_custom_block_delete($form, &$form_state, $module, $delta) {
|
||||
$block = block_load($module, $delta);
|
||||
$custom_block = block_custom_block_get($block->delta);
|
||||
$form['info'] = array('#type' => 'hidden', '#value' => $custom_block['info'] ? $custom_block['info'] : $custom_block['title']);
|
||||
$form['bid'] = array('#type' => 'hidden', '#value' => $block->delta);
|
||||
|
||||
return confirm_form($form, t('Are you sure you want to delete the block %name?', array('%name' => $custom_block['info'])), 'admin/structure/block', '', t('Delete'), t('Cancel'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Form submission handler for block_custom_block_delete().
|
||||
*
|
||||
* @see block_custom_block_delete()
|
||||
*/
|
||||
function block_custom_block_delete_submit($form, &$form_state) {
|
||||
db_delete('block_custom')
|
||||
->condition('bid', $form_state['values']['bid'])
|
||||
->execute();
|
||||
db_delete('block')
|
||||
->condition('module', 'block')
|
||||
->condition('delta', $form_state['values']['bid'])
|
||||
->execute();
|
||||
db_delete('block_role')
|
||||
->condition('module', 'block')
|
||||
->condition('delta', $form_state['values']['bid'])
|
||||
->execute();
|
||||
drupal_set_message(t('The block %name has been removed.', array('%name' => $form_state['values']['info'])));
|
||||
cache_clear_all();
|
||||
$form_state['redirect'] = 'admin/structure/block';
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes variables for block-admin-display-form.tpl.php.
|
||||
*
|
||||
* The $variables array contains the following arguments:
|
||||
* - $form
|
||||
*
|
||||
* @see block-admin-display.tpl.php
|
||||
* @see theme_block_admin_display()
|
||||
*/
|
||||
function template_preprocess_block_admin_display_form(&$variables) {
|
||||
$variables['block_regions'] = $variables['form']['block_regions']['#value'];
|
||||
if (isset($variables['block_regions'][BLOCK_REGION_NONE])) {
|
||||
$variables['block_regions'][BLOCK_REGION_NONE] = t('Disabled');
|
||||
}
|
||||
|
||||
foreach ($variables['block_regions'] as $key => $value) {
|
||||
// Initialize an empty array for the region.
|
||||
$variables['block_listing'][$key] = array();
|
||||
}
|
||||
|
||||
// Initialize disabled blocks array.
|
||||
$variables['block_listing'][BLOCK_REGION_NONE] = array();
|
||||
|
||||
// Add each block in the form to the appropriate place in the block listing.
|
||||
foreach (element_children($variables['form']['blocks']) as $i) {
|
||||
$block = &$variables['form']['blocks'][$i];
|
||||
|
||||
// Fetch the region for the current block.
|
||||
$region = (isset($block['region']['#default_value']) ? $block['region']['#default_value'] : BLOCK_REGION_NONE);
|
||||
|
||||
// Set special classes needed for table drag and drop.
|
||||
$block['region']['#attributes']['class'] = array('block-region-select', 'block-region-' . $region);
|
||||
$block['weight']['#attributes']['class'] = array('block-weight', 'block-weight-' . $region);
|
||||
|
||||
$variables['block_listing'][$region][$i] = new stdClass();
|
||||
$variables['block_listing'][$region][$i]->row_class = !empty($block['#attributes']['class']) ? implode(' ', $block['#attributes']['class']) : '';
|
||||
$variables['block_listing'][$region][$i]->block_modified = !empty($block['#attributes']['class']) && in_array('block-modified', $block['#attributes']['class']);
|
||||
$variables['block_listing'][$region][$i]->block_title = drupal_render($block['info']);
|
||||
$variables['block_listing'][$region][$i]->region_select = drupal_render($block['region']) . drupal_render($block['theme']);
|
||||
$variables['block_listing'][$region][$i]->weight_select = drupal_render($block['weight']);
|
||||
$variables['block_listing'][$region][$i]->configure_link = drupal_render($block['configure']);
|
||||
$variables['block_listing'][$region][$i]->delete_link = !empty($block['delete']) ? drupal_render($block['delete']) : '';
|
||||
$variables['block_listing'][$region][$i]->printed = FALSE;
|
||||
}
|
||||
|
||||
$variables['form_submit'] = drupal_render_children($variables['form']);
|
||||
}
|
||||
|
393
modules/block/block.api.php
Normal file
393
modules/block/block.api.php
Normal file
|
@ -0,0 +1,393 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Hooks provided by the Block module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @addtogroup hooks
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* Define all blocks provided by the module.
|
||||
*
|
||||
* This hook declares to Drupal what blocks are provided by your module and can
|
||||
* optionally specify initial block configuration settings.
|
||||
*
|
||||
* In hook_block_info(), each block your module provides is given a unique
|
||||
* identifier referred to as "delta" (the array key in the return value). Delta
|
||||
* values only need to be unique within your module, and they are used in the
|
||||
* following ways:
|
||||
* - Passed into the other block hooks in your module as an argument to identify
|
||||
* the block being configured or viewed.
|
||||
* - Used to construct the default HTML ID of "block-MODULE-DELTA" applied to
|
||||
* each block when it is rendered. This ID may then be used for CSS styling or
|
||||
* JavaScript programming.
|
||||
* - Used to define a theming template suggestion of block__MODULE__DELTA, for
|
||||
* advanced theming possibilities.
|
||||
* - Used by other modules to identify your block in hook_block_info_alter() and
|
||||
* other alter hooks.
|
||||
* The values of delta can be strings or numbers, but because of the uses above
|
||||
* it is preferable to use descriptive strings whenever possible, and only use a
|
||||
* numeric identifier if you have to (for instance if your module allows users
|
||||
* to create several similar blocks that you identify within your module code
|
||||
* with numeric IDs). The maximum length for delta values is 32 bytes.
|
||||
*
|
||||
* @return
|
||||
* An associative array whose keys define the delta for each block and whose
|
||||
* values contain the block descriptions. Each block description is itself an
|
||||
* associative array, with the following key-value pairs:
|
||||
* - info: (required) The human-readable administrative name of the block.
|
||||
* This is used to identify the block on administration screens, and is not
|
||||
* displayed to non-administrative users.
|
||||
* - cache: (optional) A bitmask describing what kind of caching is
|
||||
* appropriate for the block. Drupal provides the following bitmask
|
||||
* constants for defining cache granularity:
|
||||
* - DRUPAL_CACHE_PER_ROLE (default): The block can change depending on the
|
||||
* roles the user viewing the page belongs to.
|
||||
* - DRUPAL_CACHE_PER_USER: The block can change depending on the user
|
||||
* viewing the page. This setting can be resource-consuming for sites with
|
||||
* large number of users, and should only be used when
|
||||
* DRUPAL_CACHE_PER_ROLE is not sufficient.
|
||||
* - DRUPAL_CACHE_PER_PAGE: The block can change depending on the page being
|
||||
* viewed.
|
||||
* - DRUPAL_CACHE_GLOBAL: The block is the same for every user on every page
|
||||
* where it is visible.
|
||||
* - DRUPAL_CACHE_CUSTOM: The module implements its own caching system.
|
||||
* - DRUPAL_NO_CACHE: The block should not get cached.
|
||||
* - properties: (optional) Array of additional metadata to add to the block.
|
||||
* Common properties include:
|
||||
* - administrative: Boolean that categorizes this block as usable in an
|
||||
* administrative context. This might include blocks that help an
|
||||
* administrator approve/deny comments, or view recently created user
|
||||
* accounts.
|
||||
* - weight: (optional) Initial value for the ordering weight of this block.
|
||||
* Most modules do not provide an initial value, and any value provided can
|
||||
* be modified by a user on the block configuration screen.
|
||||
* - status: (optional) Initial value for block enabled status. (1 = enabled,
|
||||
* 0 = disabled). Most modules do not provide an initial value, and any
|
||||
* value provided can be modified by a user on the block configuration
|
||||
* screen.
|
||||
* - region: (optional) Initial value for theme region within which this
|
||||
* block is set. Most modules do not provide an initial value, and any value
|
||||
* provided can be modified by a user on the block configuration screen.
|
||||
* Note: If you set a region that isn't available in the currently enabled
|
||||
* theme, the block will be disabled.
|
||||
* - visibility: (optional) Initial value for the visibility flag, which tells
|
||||
* how to interpret the 'pages' value. Possible values are:
|
||||
* - BLOCK_VISIBILITY_NOTLISTED: Show on all pages except listed pages.
|
||||
* 'pages' lists the paths where the block should not be shown.
|
||||
* - BLOCK_VISIBILITY_LISTED: Show only on listed pages. 'pages' lists the
|
||||
* paths where the block should be shown.
|
||||
* - BLOCK_VISIBILITY_PHP: Use custom PHP code to determine visibility.
|
||||
* 'pages' gives the PHP code to use.
|
||||
* Most modules do not provide an initial value for 'visibility' or 'pages',
|
||||
* and any value provided can be modified by a user on the block
|
||||
* configuration screen.
|
||||
* - pages: (optional) See 'visibility' above. A string that contains one or
|
||||
* more page paths separated by "\n", "\r", or "\r\n" when 'visibility' is
|
||||
* set to BLOCK_VISIBILITY_NOTLISTED or BLOCK_VISIBILITY_LISTED (example:
|
||||
* "<front>\nnode/1"), or custom PHP code when 'visibility' is set to
|
||||
* BLOCK_VISIBILITY_PHP. Paths may use '*' as a wildcard (matching any
|
||||
* number of characters); '<front>' designates the site's front page. For
|
||||
* BLOCK_VISIBILITY_PHP, the PHP code's return value should be TRUE if the
|
||||
* block is to be made visible or FALSE if the block should not be visible.
|
||||
*
|
||||
* For a detailed usage example, see block_example.module.
|
||||
*
|
||||
* @see hook_block_configure()
|
||||
* @see hook_block_save()
|
||||
* @see hook_block_view()
|
||||
* @see hook_block_info_alter()
|
||||
*/
|
||||
function hook_block_info() {
|
||||
// This example comes from node.module.
|
||||
$blocks['syndicate'] = array(
|
||||
'info' => t('Syndicate'),
|
||||
'cache' => DRUPAL_NO_CACHE
|
||||
);
|
||||
|
||||
$blocks['recent'] = array(
|
||||
'info' => t('Recent content'),
|
||||
// DRUPAL_CACHE_PER_ROLE will be assumed.
|
||||
);
|
||||
|
||||
return $blocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change block definition before saving to the database.
|
||||
*
|
||||
* @param $blocks
|
||||
* A multidimensional array of blocks keyed by the defining module and delta;
|
||||
* the values are blocks returned by hook_block_info(). This hook is fired
|
||||
* after the blocks are collected from hook_block_info() and the database,
|
||||
* right before saving back to the database.
|
||||
* @param $theme
|
||||
* The theme these blocks belong to.
|
||||
* @param $code_blocks
|
||||
* The blocks as defined in hook_block_info() before being overwritten by the
|
||||
* database data.
|
||||
*
|
||||
* @see hook_block_info()
|
||||
*/
|
||||
function hook_block_info_alter(&$blocks, $theme, $code_blocks) {
|
||||
// Disable the login block.
|
||||
$blocks['user']['login']['status'] = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define a configuration form for a block.
|
||||
*
|
||||
* @param $delta
|
||||
* Which block is being configured. This is a unique identifier for the block
|
||||
* within the module, defined in hook_block_info().
|
||||
*
|
||||
* @return
|
||||
* A configuration form, if one is needed for your block beyond the standard
|
||||
* elements that the block module provides (block title, visibility, etc.).
|
||||
*
|
||||
* For a detailed usage example, see block_example.module.
|
||||
*
|
||||
* @see hook_block_info()
|
||||
* @see hook_block_save()
|
||||
*/
|
||||
function hook_block_configure($delta = '') {
|
||||
// This example comes from node.module.
|
||||
$form = array();
|
||||
if ($delta == 'recent') {
|
||||
$form['node_recent_block_count'] = array(
|
||||
'#type' => 'select',
|
||||
'#title' => t('Number of recent content items to display'),
|
||||
'#default_value' => variable_get('node_recent_block_count', 10),
|
||||
'#options' => drupal_map_assoc(array(2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 25, 30)),
|
||||
);
|
||||
}
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the configuration options from hook_block_configure().
|
||||
*
|
||||
* This hook allows you to save the block-specific configuration settings
|
||||
* defined within your hook_block_configure().
|
||||
*
|
||||
* @param $delta
|
||||
* Which block is being configured. This is a unique identifier for the block
|
||||
* within the module, defined in hook_block_info().
|
||||
* @param $edit
|
||||
* The submitted form data from the configuration form.
|
||||
*
|
||||
* For a detailed usage example, see block_example.module.
|
||||
*
|
||||
* @see hook_block_configure()
|
||||
* @see hook_block_info()
|
||||
*/
|
||||
function hook_block_save($delta = '', $edit = array()) {
|
||||
// This example comes from node.module.
|
||||
if ($delta == 'recent') {
|
||||
variable_set('node_recent_block_count', $edit['node_recent_block_count']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a rendered or renderable view of a block.
|
||||
*
|
||||
* @param $delta
|
||||
* Which block to render. This is a unique identifier for the block
|
||||
* within the module, defined in hook_block_info().
|
||||
*
|
||||
* @return
|
||||
* Either an empty array so the block will not be shown or an array containing
|
||||
* the following elements:
|
||||
* - subject: The default localized title of the block. If the block does not
|
||||
* have a default title, this should be set to NULL.
|
||||
* - content: The content of the block's body. This may be a renderable array
|
||||
* (preferable) or a string containing rendered HTML content. If the content
|
||||
* is empty the block will not be shown.
|
||||
*
|
||||
* For a detailed usage example, see block_example.module.
|
||||
*
|
||||
* @see hook_block_info()
|
||||
* @see hook_block_view_alter()
|
||||
* @see hook_block_view_MODULE_DELTA_alter()
|
||||
*/
|
||||
function hook_block_view($delta = '') {
|
||||
// This example is adapted from node.module.
|
||||
$block = array();
|
||||
|
||||
switch ($delta) {
|
||||
case 'syndicate':
|
||||
$block['subject'] = t('Syndicate');
|
||||
$block['content'] = array(
|
||||
'#theme' => 'feed_icon',
|
||||
'#url' => 'rss.xml',
|
||||
'#title' => t('Syndicate'),
|
||||
);
|
||||
break;
|
||||
|
||||
case 'recent':
|
||||
if (user_access('access content')) {
|
||||
$block['subject'] = t('Recent content');
|
||||
if ($nodes = node_get_recent(variable_get('node_recent_block_count', 10))) {
|
||||
$block['content'] = array(
|
||||
'#theme' => 'node_recent_block',
|
||||
'#nodes' => $nodes,
|
||||
);
|
||||
} else {
|
||||
$block['content'] = t('No content available.');
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
return $block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform alterations to the content of a block.
|
||||
*
|
||||
* This hook allows you to modify any data returned by hook_block_view().
|
||||
*
|
||||
* Note that instead of hook_block_view_alter(), which is called for all
|
||||
* blocks, you can also use hook_block_view_MODULE_DELTA_alter() to alter a
|
||||
* specific block.
|
||||
*
|
||||
* @param $data
|
||||
* The data as returned from the hook_block_view() implementation of the
|
||||
* module that defined the block. This could be an empty array or NULL value
|
||||
* (if the block is empty) or an array containing:
|
||||
* - subject: The default localized title of the block.
|
||||
* - content: Either a string or a renderable array representing the content
|
||||
* of the block. You should check that the content is an array before trying
|
||||
* to modify parts of the renderable structure.
|
||||
* @param $block
|
||||
* The block object, as loaded from the database, having the main properties:
|
||||
* - module: The name of the module that defined the block.
|
||||
* - delta: The unique identifier for the block within that module, as defined
|
||||
* in hook_block_info().
|
||||
*
|
||||
* @see hook_block_view_MODULE_DELTA_alter()
|
||||
* @see hook_block_view()
|
||||
*/
|
||||
function hook_block_view_alter(&$data, $block) {
|
||||
// Remove the contextual links on all blocks that provide them.
|
||||
if (is_array($data['content']) && isset($data['content']['#contextual_links'])) {
|
||||
unset($data['content']['#contextual_links']);
|
||||
}
|
||||
// Add a theme wrapper function defined by the current module to all blocks
|
||||
// provided by the "somemodule" module.
|
||||
if (is_array($data['content']) && $block->module == 'somemodule') {
|
||||
$data['content']['#theme_wrappers'][] = 'mymodule_special_block';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform alterations to a specific block.
|
||||
*
|
||||
* Modules can implement hook_block_view_MODULE_DELTA_alter() to modify a
|
||||
* specific block, rather than implementing hook_block_view_alter().
|
||||
*
|
||||
* @param $data
|
||||
* The data as returned from the hook_block_view() implementation of the
|
||||
* module that defined the block. This could be an empty array or NULL value
|
||||
* (if the block is empty) or an array containing:
|
||||
* - subject: The localized title of the block.
|
||||
* - content: Either a string or a renderable array representing the content
|
||||
* of the block. You should check that the content is an array before trying
|
||||
* to modify parts of the renderable structure.
|
||||
* @param $block
|
||||
* The block object, as loaded from the database, having the main properties:
|
||||
* - module: The name of the module that defined the block.
|
||||
* - delta: The unique identifier for the block within that module, as defined
|
||||
* in hook_block_info().
|
||||
*
|
||||
* @see hook_block_view_alter()
|
||||
* @see hook_block_view()
|
||||
*/
|
||||
function hook_block_view_MODULE_DELTA_alter(&$data, $block) {
|
||||
// This code will only run for a specific block. For example, if MODULE_DELTA
|
||||
// in the function definition above is set to "mymodule_somedelta", the code
|
||||
// will only run on the "somedelta" block provided by the "mymodule" module.
|
||||
|
||||
// Change the title of the "somedelta" block provided by the "mymodule"
|
||||
// module.
|
||||
$data['subject'] = t('New title of the block');
|
||||
}
|
||||
|
||||
/**
|
||||
* Act on blocks prior to rendering.
|
||||
*
|
||||
* This hook allows you to add, remove or modify blocks in the block list. The
|
||||
* block list contains the block definitions, not the rendered blocks. The
|
||||
* blocks are rendered after the modules have had a chance to manipulate the
|
||||
* block list.
|
||||
*
|
||||
* You can also set $block->content here, which will override the content of the
|
||||
* block and prevent hook_block_view() from running.
|
||||
*
|
||||
* @param $blocks
|
||||
* An array of $blocks, keyed by the block ID.
|
||||
*/
|
||||
function hook_block_list_alter(&$blocks) {
|
||||
global $language, $theme_key;
|
||||
|
||||
// This example shows how to achieve language specific visibility setting for
|
||||
// blocks.
|
||||
|
||||
$result = db_query('SELECT module, delta, language FROM {my_table}');
|
||||
$block_languages = array();
|
||||
foreach ($result as $record) {
|
||||
$block_languages[$record->module][$record->delta][$record->language] = TRUE;
|
||||
}
|
||||
|
||||
foreach ($blocks as $key => $block) {
|
||||
// Any module using this alter should inspect the data before changing it,
|
||||
// to ensure it is what they expect.
|
||||
if (!isset($block->theme) || !isset($block->status) || $block->theme != $theme_key || $block->status != 1) {
|
||||
// This block was added by a contrib module, leave it in the list.
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($block_languages[$block->module][$block->delta])) {
|
||||
// No language setting for this block, leave it in the list.
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($block_languages[$block->module][$block->delta][$language->language])) {
|
||||
// This block should not be displayed with the active language, remove
|
||||
// from the list.
|
||||
unset($blocks[$key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Act on block cache ID (cid) parts before the cid is generated.
|
||||
*
|
||||
* This hook allows you to add, remove or modify the custom keys used to
|
||||
* generate a block cache ID (by default, these keys are set to the block
|
||||
* module and delta). These keys will be combined with the standard ones
|
||||
* provided by drupal_render_cid_parts() to generate the final block cache ID.
|
||||
*
|
||||
* To change the cache granularity used by drupal_render_cid_parts(), this hook
|
||||
* cannot be used; instead, set the 'cache' key in the block's definition in
|
||||
* hook_block_info().
|
||||
*
|
||||
* @params $cid_parts
|
||||
* An array of elements used to build the cid.
|
||||
* @param $block
|
||||
* The block object being acted on.
|
||||
*
|
||||
* @see _block_get_cache_id()
|
||||
*/
|
||||
function hook_block_cid_parts_alter(&$cid_parts, $block) {
|
||||
global $user;
|
||||
// This example shows how to cache a block based on the user's timezone.
|
||||
$cid_parts[] = $user->timezone;
|
||||
}
|
||||
|
||||
/**
|
||||
* @} End of "addtogroup hooks".
|
||||
*/
|
36
modules/block/block.css
Normal file
36
modules/block/block.css
Normal file
|
@ -0,0 +1,36 @@
|
|||
|
||||
#blocks tr.region-title td {
|
||||
font-weight: bold;
|
||||
}
|
||||
#blocks tr.region-message {
|
||||
font-weight: normal;
|
||||
color: #999;
|
||||
}
|
||||
#blocks tr.region-populated {
|
||||
display: none;
|
||||
}
|
||||
.block-region {
|
||||
background-color: #ff6;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 4px;
|
||||
padding: 3px;
|
||||
}
|
||||
a.block-demo-backlink,
|
||||
a.block-demo-backlink:link,
|
||||
a.block-demo-backlink:visited {
|
||||
background-color: #B4D7F0;
|
||||
-moz-border-radius: 0 0 10px 10px;
|
||||
-webkit-border-radius: 0 0 10px 10px;
|
||||
border-radius: 0 0 10px 10px;
|
||||
color: #000;
|
||||
font-family: "Lucida Grande", Verdana, sans-serif;
|
||||
font-size: small;
|
||||
line-height: 20px;
|
||||
left: 20px; /*LTR*/
|
||||
padding: 5px 10px;
|
||||
position: fixed;
|
||||
z-index: 499;
|
||||
}
|
||||
a.block-demo-backlink:hover {
|
||||
text-decoration: underline;
|
||||
}
|
13
modules/block/block.info
Normal file
13
modules/block/block.info
Normal file
|
@ -0,0 +1,13 @@
|
|||
name = Block
|
||||
description = Controls the visual building blocks a page is constructed with. Blocks are boxes of content rendered into an area, or region, of a web page.
|
||||
package = Core
|
||||
version = VERSION
|
||||
core = 7.x
|
||||
files[] = block.test
|
||||
configure = admin/structure/block
|
||||
|
||||
; Information added by Drupal.org packaging script on 2017-06-21
|
||||
version = "7.56"
|
||||
project = "drupal"
|
||||
datestamp = "1498069849"
|
||||
|
493
modules/block/block.install
Normal file
493
modules/block/block.install
Normal file
|
@ -0,0 +1,493 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Install, update and uninstall functions for the block module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Implements hook_schema().
|
||||
*/
|
||||
function block_schema() {
|
||||
$schema['block'] = array(
|
||||
'description' => 'Stores block settings, such as region and visibility settings.',
|
||||
'fields' => array(
|
||||
'bid' => array(
|
||||
'type' => 'serial',
|
||||
'not null' => TRUE,
|
||||
'description' => 'Primary Key: Unique block ID.',
|
||||
),
|
||||
'module' => array(
|
||||
'type' => 'varchar',
|
||||
'length' => 64,
|
||||
'not null' => TRUE,
|
||||
'default' => '',
|
||||
'description' => "The module from which the block originates; for example, 'user' for the Who's Online block, and 'block' for any custom blocks.",
|
||||
),
|
||||
'delta' => array(
|
||||
'type' => 'varchar',
|
||||
'length' => 32,
|
||||
'not null' => TRUE,
|
||||
'default' => '0',
|
||||
'description' => 'Unique ID for block within a module.',
|
||||
),
|
||||
'theme' => array(
|
||||
'type' => 'varchar',
|
||||
'length' => 64,
|
||||
'not null' => TRUE,
|
||||
'default' => '',
|
||||
'description' => 'The theme under which the block settings apply.',
|
||||
),
|
||||
'status' => array(
|
||||
'type' => 'int',
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
'size' => 'tiny',
|
||||
'description' => 'Block enabled status. (1 = enabled, 0 = disabled)',
|
||||
),
|
||||
'weight' => array(
|
||||
'type' => 'int',
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
'description' => 'Block weight within region.',
|
||||
),
|
||||
'region' => array(
|
||||
'type' => 'varchar',
|
||||
'length' => 64,
|
||||
'not null' => TRUE,
|
||||
'default' => '',
|
||||
'description' => 'Theme region within which the block is set.',
|
||||
),
|
||||
'custom' => array(
|
||||
'type' => 'int',
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
'size' => 'tiny',
|
||||
'description' => 'Flag to indicate how users may control visibility of the block. (0 = Users cannot control, 1 = On by default, but can be hidden, 2 = Hidden by default, but can be shown)',
|
||||
),
|
||||
'visibility' => array(
|
||||
'type' => 'int',
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
'size' => 'tiny',
|
||||
'description' => 'Flag to indicate how to show blocks on pages. (0 = Show on all pages except listed pages, 1 = Show only on listed pages, 2 = Use custom PHP code to determine visibility)',
|
||||
),
|
||||
'pages' => array(
|
||||
'type' => 'text',
|
||||
'not null' => TRUE,
|
||||
'description' => 'Contents of the "Pages" block; contains either a list of paths on which to include/exclude the block or PHP code, depending on "visibility" setting.',
|
||||
),
|
||||
'title' => array(
|
||||
'type' => 'varchar',
|
||||
'length' => 255,
|
||||
'not null' => TRUE,
|
||||
'default' => '',
|
||||
'description' => 'Custom title for the block. (Empty string will use block default title, <none> will remove the title, text will cause block to use specified title.)',
|
||||
'translatable' => TRUE,
|
||||
),
|
||||
'cache' => array(
|
||||
'type' => 'int',
|
||||
'not null' => TRUE,
|
||||
'default' => 1,
|
||||
'size' => 'tiny',
|
||||
'description' => 'Binary flag to indicate block cache mode. (-2: Custom cache, -1: Do not cache, 1: Cache per role, 2: Cache per user, 4: Cache per page, 8: Block cache global) See DRUPAL_CACHE_* constants in ../includes/common.inc for more detailed information.',
|
||||
),
|
||||
),
|
||||
'primary key' => array('bid'),
|
||||
'unique keys' => array(
|
||||
'tmd' => array('theme', 'module', 'delta'),
|
||||
),
|
||||
'indexes' => array(
|
||||
'list' => array('theme', 'status', 'region', 'weight', 'module'),
|
||||
),
|
||||
);
|
||||
|
||||
$schema['block_role'] = array(
|
||||
'description' => 'Sets up access permissions for blocks based on user roles',
|
||||
'fields' => array(
|
||||
'module' => array(
|
||||
'type' => 'varchar',
|
||||
'length' => 64,
|
||||
'not null' => TRUE,
|
||||
'description' => "The block's origin module, from {block}.module.",
|
||||
),
|
||||
'delta' => array(
|
||||
'type' => 'varchar',
|
||||
'length' => 32,
|
||||
'not null' => TRUE,
|
||||
'description' => "The block's unique delta within module, from {block}.delta.",
|
||||
),
|
||||
'rid' => array(
|
||||
'type' => 'int',
|
||||
'unsigned' => TRUE,
|
||||
'not null' => TRUE,
|
||||
'description' => "The user's role ID from {users_roles}.rid.",
|
||||
),
|
||||
),
|
||||
'primary key' => array('module', 'delta', 'rid'),
|
||||
'indexes' => array(
|
||||
'rid' => array('rid'),
|
||||
),
|
||||
);
|
||||
|
||||
$schema['block_custom'] = array(
|
||||
'description' => 'Stores contents of custom-made blocks.',
|
||||
'fields' => array(
|
||||
'bid' => array(
|
||||
'type' => 'serial',
|
||||
'unsigned' => TRUE,
|
||||
'not null' => TRUE,
|
||||
'description' => "The block's {block}.bid.",
|
||||
),
|
||||
'body' => array(
|
||||
'type' => 'text',
|
||||
'not null' => FALSE,
|
||||
'size' => 'big',
|
||||
'description' => 'Block contents.',
|
||||
'translatable' => TRUE,
|
||||
),
|
||||
'info' => array(
|
||||
'type' => 'varchar',
|
||||
'length' => 128,
|
||||
'not null' => TRUE,
|
||||
'default' => '',
|
||||
'description' => 'Block description.',
|
||||
),
|
||||
'format' => array(
|
||||
'type' => 'varchar',
|
||||
'length' => 255,
|
||||
'not null' => FALSE,
|
||||
'description' => 'The {filter_format}.format of the block body.',
|
||||
),
|
||||
),
|
||||
'unique keys' => array(
|
||||
'info' => array('info'),
|
||||
),
|
||||
'primary key' => array('bid'),
|
||||
);
|
||||
|
||||
$schema['cache_block'] = drupal_get_schema_unprocessed('system', 'cache');
|
||||
$schema['cache_block']['description'] = 'Cache table for the Block module to store already built blocks, identified by module, delta, and various contexts which may change the block, such as theme, locale, and caching mode defined for the block.';
|
||||
|
||||
return $schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_install().
|
||||
*/
|
||||
function block_install() {
|
||||
|
||||
// Block should go first so that other modules can alter its output
|
||||
// during hook_page_alter(). Almost everything on the page is a block,
|
||||
// so before block module runs, there will not be much to alter.
|
||||
db_update('system')
|
||||
->fields(array('weight' => -5))
|
||||
->condition('name', 'block')
|
||||
->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_update_dependencies().
|
||||
*/
|
||||
function block_update_dependencies() {
|
||||
// block_update_7005() needs to query the {filter_format} table to get a list
|
||||
// of existing text formats, so it must run after filter_update_7000(), which
|
||||
// creates that table.
|
||||
$dependencies['block'][7005] = array(
|
||||
'filter' => 7000,
|
||||
);
|
||||
|
||||
return $dependencies;
|
||||
}
|
||||
|
||||
/**
|
||||
* @addtogroup updates-6.x-to-7.x
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* Set system.weight to a low value for block module.
|
||||
*
|
||||
* Block should go first so that other modules can alter its output
|
||||
* during hook_page_alter(). Almost everything on the page is a block,
|
||||
* so before block module runs, there will not be much to alter.
|
||||
*/
|
||||
function block_update_7000() {
|
||||
db_update('system')
|
||||
->fields(array('weight' => '-5'))
|
||||
->condition('name', 'block')
|
||||
->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename {blocks} table to {block}, {blocks_roles} to {block_role} and
|
||||
* {boxes} to {block_custom}.
|
||||
*/
|
||||
function block_update_7002() {
|
||||
db_drop_index('blocks', 'list');
|
||||
db_rename_table('blocks', 'block');
|
||||
db_rename_table('blocks_roles', 'block_role');
|
||||
db_rename_table('boxes', 'block_custom');
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the weight column to normal int.
|
||||
*/
|
||||
function block_update_7003() {
|
||||
db_change_field('block', 'weight', 'weight', array(
|
||||
'type' => 'int',
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
'description' => 'Block weight within region.',
|
||||
), array(
|
||||
'indexes' => array(
|
||||
'list' => array('theme', 'status', 'region', 'weight', 'module'),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add new blocks to new regions, migrate custom variables to blocks.
|
||||
*/
|
||||
function block_update_7004() {
|
||||
// Collect a list of themes with blocks.
|
||||
$themes_with_blocks = array();
|
||||
$result = db_query("SELECT s.name FROM {system} s INNER JOIN {block} b ON s.name = b.theme WHERE s.type = 'theme' GROUP by s.name");
|
||||
|
||||
$insert = db_insert('block')->fields(array('module', 'delta', 'theme', 'status', 'weight', 'region', 'pages', 'cache'));
|
||||
foreach ($result as $theme) {
|
||||
$themes_with_blocks[] = $theme->name;
|
||||
// Add new system generated help block.
|
||||
$insert->values(array(
|
||||
'module' => 'system',
|
||||
'delta' => 'help',
|
||||
'theme' => $theme->name,
|
||||
'status' => 1,
|
||||
'weight' => 0,
|
||||
'region' => 'help',
|
||||
'pages' => '',
|
||||
'cache' => DRUPAL_CACHE_PER_ROLE,
|
||||
));
|
||||
// Add new system generated main page content block.
|
||||
$insert->values(array(
|
||||
'module' => 'system',
|
||||
'delta' => 'main',
|
||||
'theme' => $theme->name,
|
||||
'status' => 1,
|
||||
'weight' => 0,
|
||||
'region' => 'content',
|
||||
'pages' => '',
|
||||
'cache' => DRUPAL_NO_CACHE,
|
||||
));
|
||||
}
|
||||
$insert->execute();
|
||||
|
||||
// Migrate blocks from left/right regions to first/second regions.
|
||||
db_update('block')
|
||||
->fields(array('region' => 'sidebar_first'))
|
||||
->condition('region', 'left')
|
||||
->execute();
|
||||
db_update('block')
|
||||
->fields(array('region' => 'sidebar_second'))
|
||||
->condition('region', 'right')
|
||||
->execute();
|
||||
|
||||
// Migrate contact form information.
|
||||
$default_format = variable_get('filter_default_format', 1);
|
||||
if ($contact_help = variable_get('contact_form_information', '')) {
|
||||
$bid = db_insert('block_custom')
|
||||
->fields(array(
|
||||
'body' => $contact_help,
|
||||
'info' => 'Contact page help',
|
||||
'format' => $default_format,
|
||||
))
|
||||
->execute();
|
||||
|
||||
$insert = db_insert('block')->fields(array('module', 'delta', 'theme', 'status', 'weight', 'region', 'visibility', 'pages', 'cache'));
|
||||
foreach ($themes_with_blocks as $theme) {
|
||||
// Add contact help block for themes, which had blocks.
|
||||
$insert->values(array(
|
||||
'module' => 'block',
|
||||
'delta' => $bid,
|
||||
'theme' => $theme,
|
||||
'status' => 1,
|
||||
'weight' => 5,
|
||||
'region' => 'help',
|
||||
'visibility' => BLOCK_VISIBILITY_LISTED,
|
||||
'pages' => 'contact',
|
||||
'cache' => DRUPAL_NO_CACHE,
|
||||
));
|
||||
}
|
||||
drupal_set_message('The contact form information setting was migrated to <a href="' . url('admin/structure/block/manage/block/' . $bid . '/configure') . '">a custom block</a> and set up to only show on the site-wide contact page. The block was set to use the default text format, which might differ from the HTML based format used before. Check the block and ensure that the output is right.');
|
||||
}
|
||||
$insert->execute();
|
||||
|
||||
// Migrate user help setting.
|
||||
if ($user_help = variable_get('user_registration_help', '')) {
|
||||
$bid = db_insert('block_custom')->fields(array('body' => $user_help, 'info' => 'User registration guidelines', 'format' => $default_format))->execute();
|
||||
|
||||
$insert = db_insert('block')->fields(array('module', 'delta', 'theme', 'status', 'weight', 'region', 'visibility', 'pages', 'cache'));
|
||||
foreach ($themes_with_blocks as $theme) {
|
||||
// Add user registration help block for themes, which had blocks.
|
||||
$insert->values(array(
|
||||
'module' => 'block',
|
||||
'delta' => $bid,
|
||||
'theme' => $theme,
|
||||
'status' => 1,
|
||||
'weight' => 5,
|
||||
'region' => 'help',
|
||||
'visibility' => BLOCK_VISIBILITY_LISTED,
|
||||
'pages' => 'user/register',
|
||||
'cache' => DRUPAL_NO_CACHE,
|
||||
));
|
||||
}
|
||||
drupal_set_message('The user registration guidelines were migrated to <a href="' . url('admin/structure/block/manage/block/' . $bid . '/configure') . '">a custom block</a> and set up to only show on the user registration page. The block was set to use the default text format, which might differ from the HTML based format used before. Check the block and ensure that the output is right.');
|
||||
$insert->execute();
|
||||
}
|
||||
|
||||
// Migrate site mission setting.
|
||||
if ($mission = variable_get('site_mission')) {
|
||||
$bid = db_insert('block_custom')->fields(array('body' => $mission, 'info' => 'Site mission', 'format' => $default_format))->execute();
|
||||
|
||||
$insert = db_insert('block')->fields(array('module', 'delta', 'theme', 'status', 'weight', 'region', 'visibility', 'pages', 'cache'));
|
||||
foreach ($themes_with_blocks as $theme) {
|
||||
// Add mission block for themes, which had blocks.
|
||||
$insert->values(array(
|
||||
'module' => 'block',
|
||||
'delta' => $bid,
|
||||
'theme' => $theme,
|
||||
'status' => 1,
|
||||
'weight' => 0,
|
||||
'region' => 'highlighted',
|
||||
'visibility' => BLOCK_VISIBILITY_LISTED,
|
||||
'pages' => '<front>',
|
||||
'cache' => DRUPAL_NO_CACHE,
|
||||
));
|
||||
}
|
||||
drupal_set_message('The site mission was migrated to <a href="' . url('admin/structure/block/manage/block/' . $bid . '/configure') . '">a custom block</a> and set up to only show on the front page in the highlighted content region. The block was set to use the default text format, which might differ from the HTML based format used before. Check the block and ensure that the output is right. If your theme does not have a highlighted content region, you might need to <a href="' . url('admin/structure/block') . '">relocate the block</a>.');
|
||||
$insert->execute();
|
||||
// Migrate mission to RSS site description.
|
||||
variable_set('feed_description', $mission);
|
||||
}
|
||||
|
||||
// Migrate site footer message to a custom block.
|
||||
if ($footer_message = variable_get('site_footer', '')) {
|
||||
$bid = db_insert('block_custom')->fields(array('body' => $footer_message, 'info' => 'Footer message', 'format' => $default_format))->execute();
|
||||
|
||||
$insert = db_insert('block')->fields(array('module', 'delta', 'theme', 'status', 'weight', 'region', 'pages', 'cache'));
|
||||
foreach ($themes_with_blocks as $theme) {
|
||||
// Add site footer block for themes, which had blocks.
|
||||
// Set low weight, so the block comes early (it used to be
|
||||
// before the other blocks).
|
||||
$insert->values(array(
|
||||
'module' => 'block',
|
||||
'delta' => $bid,
|
||||
'theme' => $theme,
|
||||
'status' => 1,
|
||||
'weight' => -10,
|
||||
'region' => 'footer',
|
||||
'pages' => '',
|
||||
'cache' => DRUPAL_NO_CACHE,
|
||||
));
|
||||
}
|
||||
drupal_set_message('The footer message was migrated to <a href="' . url('admin/structure/block/manage/block/' . $bid . '/configure') . '">a custom block</a> and set up to appear in the footer. The block was set to use the default text format, which might differ from the HTML based format used before. Check the block and ensure that the output is right. If your theme does not have a footer region, you might need to <a href="' . url('admin/structure/block') . '">relocate the block</a>.');
|
||||
$insert->execute();
|
||||
}
|
||||
|
||||
// Remove the variables (even if they were saved empty on the admin interface),
|
||||
// to avoid keeping clutter in the variables table.
|
||||
variable_del('contact_form_information');
|
||||
variable_del('user_registration_help');
|
||||
variable_del('site_mission');
|
||||
variable_del('site_footer');
|
||||
|
||||
// Rebuild theme data, so the new 'help' region is identified.
|
||||
system_rebuild_theme_data();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the {block_custom}.format column.
|
||||
*/
|
||||
function block_update_7005() {
|
||||
// For an explanation of these updates, see the code comments in
|
||||
// user_update_7010().
|
||||
db_change_field('block_custom', 'format', 'format', array(
|
||||
'type' => 'int',
|
||||
'unsigned' => TRUE,
|
||||
'not null' => FALSE,
|
||||
'description' => 'The {filter_format}.format of the block body.',
|
||||
));
|
||||
db_update('block_custom')
|
||||
->fields(array('format' => NULL))
|
||||
->condition('body', '')
|
||||
->condition('format', 0)
|
||||
->execute();
|
||||
$existing_formats = db_query("SELECT format FROM {filter_format}")->fetchCol();
|
||||
$default_format = variable_get('filter_default_format', 1);
|
||||
db_update('block_custom')
|
||||
->fields(array('format' => $default_format))
|
||||
->isNotNull('format')
|
||||
->condition('format', $existing_formats, 'NOT IN')
|
||||
->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Recreates cache_block table.
|
||||
*
|
||||
* Converts fields that hold serialized variables from text to blob.
|
||||
* Removes 'headers' column.
|
||||
*/
|
||||
function block_update_7006() {
|
||||
$schema = system_schema_cache_7054();
|
||||
|
||||
db_drop_table('cache_block');
|
||||
db_create_table('cache_block', $schema);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change {block_custom}.format into varchar.
|
||||
*/
|
||||
function block_update_7007() {
|
||||
db_change_field('block_custom', 'format', 'format', array(
|
||||
'type' => 'varchar',
|
||||
'length' => 255,
|
||||
'not null' => FALSE,
|
||||
'description' => 'The {filter_format}.format of the block body.',
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @} End of "addtogroup updates-6.x-to-7.x".
|
||||
*/
|
||||
|
||||
/**
|
||||
* @addtogroup updates-7.x-extra
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* Update database to match Drupal 7 schema.
|
||||
*/
|
||||
function block_update_7008() {
|
||||
db_drop_field('block', 'throttle');
|
||||
}
|
||||
|
||||
/**
|
||||
* Increase {block}.title length to 255 characters.
|
||||
*/
|
||||
function block_update_7009() {
|
||||
db_change_field('block', 'title', 'title',
|
||||
array(
|
||||
'type' => 'varchar',
|
||||
'length' => 255,
|
||||
'not null' => TRUE,
|
||||
'default' => '',
|
||||
'description' => 'Custom title for the block. (Empty string will use block default title, <none> will remove the title, text will cause block to use specified title.)',
|
||||
'translatable' => TRUE,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @} End of "addtogroup updates-7.x-extra".
|
||||
*/
|
152
modules/block/block.js
Normal file
152
modules/block/block.js
Normal file
|
@ -0,0 +1,152 @@
|
|||
(function ($) {
|
||||
|
||||
/**
|
||||
* Provide the summary information for the block settings vertical tabs.
|
||||
*/
|
||||
Drupal.behaviors.blockSettingsSummary = {
|
||||
attach: function (context) {
|
||||
// The drupalSetSummary method required for this behavior is not available
|
||||
// on the Blocks administration page, so we need to make sure this
|
||||
// behavior is processed only if drupalSetSummary is defined.
|
||||
if (typeof jQuery.fn.drupalSetSummary == 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
$('fieldset#edit-path', context).drupalSetSummary(function (context) {
|
||||
if (!$('textarea[name="pages"]', context).val()) {
|
||||
return Drupal.t('Not restricted');
|
||||
}
|
||||
else {
|
||||
return Drupal.t('Restricted to certain pages');
|
||||
}
|
||||
});
|
||||
|
||||
$('fieldset#edit-node-type', context).drupalSetSummary(function (context) {
|
||||
var vals = [];
|
||||
$('input[type="checkbox"]:checked', context).each(function () {
|
||||
vals.push($.trim($(this).next('label').html()));
|
||||
});
|
||||
if (!vals.length) {
|
||||
vals.push(Drupal.t('Not restricted'));
|
||||
}
|
||||
return vals.join(', ');
|
||||
});
|
||||
|
||||
$('fieldset#edit-role', context).drupalSetSummary(function (context) {
|
||||
var vals = [];
|
||||
$('input[type="checkbox"]:checked', context).each(function () {
|
||||
vals.push($.trim($(this).next('label').html()));
|
||||
});
|
||||
if (!vals.length) {
|
||||
vals.push(Drupal.t('Not restricted'));
|
||||
}
|
||||
return vals.join(', ');
|
||||
});
|
||||
|
||||
$('fieldset#edit-user', context).drupalSetSummary(function (context) {
|
||||
var $radio = $('input[name="custom"]:checked', context);
|
||||
if ($radio.val() == 0) {
|
||||
return Drupal.t('Not customizable');
|
||||
}
|
||||
else {
|
||||
return $radio.next('label').html();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Move a block in the blocks table from one region to another via select list.
|
||||
*
|
||||
* This behavior is dependent on the tableDrag behavior, since it uses the
|
||||
* objects initialized in that behavior to update the row.
|
||||
*/
|
||||
Drupal.behaviors.blockDrag = {
|
||||
attach: function (context, settings) {
|
||||
// tableDrag is required and we should be on the blocks admin page.
|
||||
if (typeof Drupal.tableDrag == 'undefined' || typeof Drupal.tableDrag.blocks == 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
var table = $('table#blocks');
|
||||
var tableDrag = Drupal.tableDrag.blocks; // Get the blocks tableDrag object.
|
||||
|
||||
// Add a handler for when a row is swapped, update empty regions.
|
||||
tableDrag.row.prototype.onSwap = function (swappedRow) {
|
||||
checkEmptyRegions(table, this);
|
||||
};
|
||||
|
||||
// A custom message for the blocks page specifically.
|
||||
Drupal.theme.tableDragChangedWarning = function () {
|
||||
return '<div class="messages warning">' + Drupal.theme('tableDragChangedMarker') + ' ' + Drupal.t('The changes to these blocks will not be saved until the <em>Save blocks</em> button is clicked.') + '</div>';
|
||||
};
|
||||
|
||||
// Add a handler so when a row is dropped, update fields dropped into new regions.
|
||||
tableDrag.onDrop = function () {
|
||||
dragObject = this;
|
||||
// Use "region-message" row instead of "region" row because
|
||||
// "region-{region_name}-message" is less prone to regexp match errors.
|
||||
var regionRow = $(dragObject.rowObject.element).prevAll('tr.region-message').get(0);
|
||||
var regionName = regionRow.className.replace(/([^ ]+[ ]+)*region-([^ ]+)-message([ ]+[^ ]+)*/, '$2');
|
||||
var regionField = $('select.block-region-select', dragObject.rowObject.element);
|
||||
// Check whether the newly picked region is available for this block.
|
||||
if ($('option[value=' + regionName + ']', regionField).length == 0) {
|
||||
// If not, alert the user and keep the block in its old region setting.
|
||||
alert(Drupal.t('The block cannot be placed in this region.'));
|
||||
// Simulate that there was a selected element change, so the row is put
|
||||
// back to from where the user tried to drag it.
|
||||
regionField.change();
|
||||
}
|
||||
else if ($(dragObject.rowObject.element).prev('tr').is('.region-message')) {
|
||||
var weightField = $('select.block-weight', dragObject.rowObject.element);
|
||||
var oldRegionName = weightField[0].className.replace(/([^ ]+[ ]+)*block-weight-([^ ]+)([ ]+[^ ]+)*/, '$2');
|
||||
|
||||
if (!regionField.is('.block-region-' + regionName)) {
|
||||
regionField.removeClass('block-region-' + oldRegionName).addClass('block-region-' + regionName);
|
||||
weightField.removeClass('block-weight-' + oldRegionName).addClass('block-weight-' + regionName);
|
||||
regionField.val(regionName);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Add the behavior to each region select list.
|
||||
$('select.block-region-select', context).once('block-region-select', function () {
|
||||
$(this).change(function (event) {
|
||||
// Make our new row and select field.
|
||||
var row = $(this).closest('tr');
|
||||
var select = $(this);
|
||||
tableDrag.rowObject = new tableDrag.row(row);
|
||||
|
||||
// Find the correct region and insert the row as the last in the region.
|
||||
table.find('.region-' + select[0].value + '-message').nextUntil('.region-message').last().before(row);
|
||||
|
||||
// Modify empty regions with added or removed fields.
|
||||
checkEmptyRegions(table, row);
|
||||
// Remove focus from selectbox.
|
||||
select.get(0).blur();
|
||||
});
|
||||
});
|
||||
|
||||
var checkEmptyRegions = function (table, rowObject) {
|
||||
$('tr.region-message', table).each(function () {
|
||||
// If the dragged row is in this region, but above the message row, swap it down one space.
|
||||
if ($(this).prev('tr').get(0) == rowObject.element) {
|
||||
// Prevent a recursion problem when using the keyboard to move rows up.
|
||||
if ((rowObject.method != 'keyboard' || rowObject.direction == 'down')) {
|
||||
rowObject.swap('after', this);
|
||||
}
|
||||
}
|
||||
// This region has become empty.
|
||||
if ($(this).next('tr').is(':not(.draggable)') || $(this).next('tr').length == 0) {
|
||||
$(this).removeClass('region-populated').addClass('region-empty');
|
||||
}
|
||||
// This region has become populated.
|
||||
else if ($(this).is('.region-empty')) {
|
||||
$(this).removeClass('region-empty').addClass('region-populated');
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
})(jQuery);
|
1113
modules/block/block.module
Normal file
1113
modules/block/block.module
Normal file
File diff suppressed because it is too large
Load diff
979
modules/block/block.test
Normal file
979
modules/block/block.test
Normal file
|
@ -0,0 +1,979 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Tests for block.module.
|
||||
*/
|
||||
|
||||
class BlockTestCase extends DrupalWebTestCase {
|
||||
protected $regions;
|
||||
protected $admin_user;
|
||||
|
||||
public static function getInfo() {
|
||||
return array(
|
||||
'name' => 'Block functionality',
|
||||
'description' => 'Add, edit and delete custom block. Configure and move a module-defined block.',
|
||||
'group' => 'Block',
|
||||
);
|
||||
}
|
||||
|
||||
function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
// Create and log in an administrative user having access to the Full HTML
|
||||
// text format.
|
||||
$full_html_format = filter_format_load('full_html');
|
||||
$this->admin_user = $this->drupalCreateUser(array(
|
||||
'administer blocks',
|
||||
filter_permission_name($full_html_format),
|
||||
'access administration pages',
|
||||
));
|
||||
$this->drupalLogin($this->admin_user);
|
||||
|
||||
// Define the existing regions
|
||||
$this->regions = array();
|
||||
$this->regions[] = 'header';
|
||||
$this->regions[] = 'sidebar_first';
|
||||
$this->regions[] = 'content';
|
||||
$this->regions[] = 'sidebar_second';
|
||||
$this->regions[] = 'footer';
|
||||
}
|
||||
|
||||
/**
|
||||
* Test creating custom block, moving it to a specific region and then deleting it.
|
||||
*/
|
||||
function testCustomBlock() {
|
||||
// Confirm that the add block link appears on block overview pages.
|
||||
$this->drupalGet('admin/structure/block');
|
||||
$this->assertRaw(l('Add block', 'admin/structure/block/add'), 'Add block link is present on block overview page for default theme.');
|
||||
$this->drupalGet('admin/structure/block/list/seven');
|
||||
$this->assertRaw(l('Add block', 'admin/structure/block/list/seven/add'), 'Add block link is present on block overview page for non-default theme.');
|
||||
|
||||
// Confirm that hidden regions are not shown as options for block placement
|
||||
// when adding a new block.
|
||||
theme_enable(array('stark'));
|
||||
$themes = list_themes();
|
||||
$this->drupalGet('admin/structure/block/add');
|
||||
foreach ($themes as $key => $theme) {
|
||||
if ($theme->status) {
|
||||
foreach ($theme->info['regions_hidden'] as $hidden_region) {
|
||||
$elements = $this->xpath('//select[@id=:id]//option[@value=:value]', array(':id' => 'edit-regions-' . $key, ':value' => $hidden_region));
|
||||
$this->assertFalse(isset($elements[0]), format_string('The hidden region @region is not available for @theme.', array('@region' => $hidden_region, '@theme' => $key)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add a new custom block by filling out the input form on the admin/structure/block/add page.
|
||||
$custom_block = array();
|
||||
$custom_block['info'] = $this->randomName(8);
|
||||
$custom_block['title'] = $this->randomName(8);
|
||||
$custom_block['body[value]'] = $this->randomName(32);
|
||||
$this->drupalPost('admin/structure/block/add', $custom_block, t('Save block'));
|
||||
|
||||
// Confirm that the custom block has been created, and then query the created bid.
|
||||
$this->assertText(t('The block has been created.'), 'Custom block successfully created.');
|
||||
$bid = db_query("SELECT bid FROM {block_custom} WHERE info = :info", array(':info' => $custom_block['info']))->fetchField();
|
||||
|
||||
// Check to see if the custom block was created by checking that it's in the database.
|
||||
$this->assertTrue($bid, 'Custom block found in database');
|
||||
|
||||
// Check that block_block_view() returns the correct title and content.
|
||||
$data = block_block_view($bid);
|
||||
$format = db_query("SELECT format FROM {block_custom} WHERE bid = :bid", array(':bid' => $bid))->fetchField();
|
||||
$this->assertTrue(array_key_exists('subject', $data) && empty($data['subject']), 'block_block_view() provides an empty block subject, since custom blocks do not have default titles.');
|
||||
$this->assertEqual(check_markup($custom_block['body[value]'], $format), $data['content'], 'block_block_view() provides correct block content.');
|
||||
|
||||
// Check whether the block can be moved to all available regions.
|
||||
$custom_block['module'] = 'block';
|
||||
$custom_block['delta'] = $bid;
|
||||
foreach ($this->regions as $region) {
|
||||
$this->moveBlockToRegion($custom_block, $region);
|
||||
}
|
||||
|
||||
// Verify presence of configure and delete links for custom block.
|
||||
$this->drupalGet('admin/structure/block');
|
||||
$this->assertLinkByHref('admin/structure/block/manage/block/' . $bid . '/configure', 0, 'Custom block configure link found.');
|
||||
$this->assertLinkByHref('admin/structure/block/manage/block/' . $bid . '/delete', 0, 'Custom block delete link found.');
|
||||
|
||||
// Set visibility only for authenticated users, to verify delete functionality.
|
||||
$edit = array();
|
||||
$edit['roles[' . DRUPAL_AUTHENTICATED_RID . ']'] = TRUE;
|
||||
$this->drupalPost('admin/structure/block/manage/block/' . $bid . '/configure', $edit, t('Save block'));
|
||||
|
||||
// Delete the created custom block & verify that it's been deleted and no longer appearing on the page.
|
||||
$this->clickLink(t('delete'));
|
||||
$this->drupalPost('admin/structure/block/manage/block/' . $bid . '/delete', array(), t('Delete'));
|
||||
$this->assertRaw(t('The block %title has been removed.', array('%title' => $custom_block['info'])), 'Custom block successfully deleted.');
|
||||
$this->assertNoText(t($custom_block['title']), 'Custom block no longer appears on page.');
|
||||
$count = db_query("SELECT 1 FROM {block_role} WHERE module = :module AND delta = :delta", array(':module' => $custom_block['module'], ':delta' => $custom_block['delta']))->fetchField();
|
||||
$this->assertFalse($count, 'Table block_role being cleaned.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test creating custom block using Full HTML.
|
||||
*/
|
||||
function testCustomBlockFormat() {
|
||||
// Add a new custom block by filling out the input form on the admin/structure/block/add page.
|
||||
$custom_block = array();
|
||||
$custom_block['info'] = $this->randomName(8);
|
||||
$custom_block['title'] = $this->randomName(8);
|
||||
$custom_block['body[value]'] = '<h1>Full HTML</h1>';
|
||||
$full_html_format = filter_format_load('full_html');
|
||||
$custom_block['body[format]'] = $full_html_format->format;
|
||||
$this->drupalPost('admin/structure/block/add', $custom_block, t('Save block'));
|
||||
|
||||
// Set the created custom block to a specific region.
|
||||
$bid = db_query("SELECT bid FROM {block_custom} WHERE info = :info", array(':info' => $custom_block['info']))->fetchField();
|
||||
$edit = array();
|
||||
$edit['blocks[block_' . $bid . '][region]'] = $this->regions[1];
|
||||
$this->drupalPost('admin/structure/block', $edit, t('Save blocks'));
|
||||
|
||||
// Confirm that the custom block is being displayed using configured text format.
|
||||
$this->drupalGet('node');
|
||||
$this->assertRaw('<h1>Full HTML</h1>', 'Custom block successfully being displayed using Full HTML.');
|
||||
|
||||
// Confirm that a user without access to Full HTML can not see the body field,
|
||||
// but can still submit the form without errors.
|
||||
$block_admin = $this->drupalCreateUser(array('administer blocks'));
|
||||
$this->drupalLogin($block_admin);
|
||||
$this->drupalGet('admin/structure/block/manage/block/' . $bid . '/configure');
|
||||
$this->assertFieldByXPath("//textarea[@name='body[value]' and @disabled='disabled']", t('This field has been disabled because you do not have sufficient permissions to edit it.'), 'Body field contains denied message');
|
||||
$this->drupalPost('admin/structure/block/manage/block/' . $bid . '/configure', array(), t('Save block'));
|
||||
$this->assertNoText(t('Ensure that each block description is unique.'));
|
||||
|
||||
// Confirm that the custom block is still being displayed using configured text format.
|
||||
$this->drupalGet('node');
|
||||
$this->assertRaw('<h1>Full HTML</h1>', 'Custom block successfully being displayed using Full HTML.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test block visibility.
|
||||
*/
|
||||
function testBlockVisibility() {
|
||||
$block = array();
|
||||
|
||||
// Create a random title for the block
|
||||
$title = $this->randomName(8);
|
||||
|
||||
// Create the custom block
|
||||
$custom_block = array();
|
||||
$custom_block['info'] = $this->randomName(8);
|
||||
$custom_block['title'] = $title;
|
||||
$custom_block['body[value]'] = $this->randomName(32);
|
||||
$this->drupalPost('admin/structure/block/add', $custom_block, t('Save block'));
|
||||
|
||||
$bid = db_query("SELECT bid FROM {block_custom} WHERE info = :info", array(':info' => $custom_block['info']))->fetchField();
|
||||
$block['module'] = 'block';
|
||||
$block['delta'] = $bid;
|
||||
$block['title'] = $title;
|
||||
|
||||
// Set the block to be hidden on any user path, and to be shown only to
|
||||
// authenticated users.
|
||||
$edit = array();
|
||||
$edit['pages'] = 'user*';
|
||||
$edit['roles[' . DRUPAL_AUTHENTICATED_RID . ']'] = TRUE;
|
||||
$this->drupalPost('admin/structure/block/manage/' . $block['module'] . '/' . $block['delta'] . '/configure', $edit, t('Save block'));
|
||||
|
||||
// Move block to the first sidebar.
|
||||
$this->moveBlockToRegion($block, $this->regions[1]);
|
||||
|
||||
$this->drupalGet('');
|
||||
$this->assertText($title, 'Block was displayed on the front page.');
|
||||
|
||||
$this->drupalGet('user');
|
||||
$this->assertNoText($title, 'Block was not displayed according to block visibility rules.');
|
||||
|
||||
$this->drupalGet('USER/' . $this->admin_user->uid);
|
||||
$this->assertNoText($title, 'Block was not displayed according to block visibility rules regardless of path case.');
|
||||
|
||||
// Confirm that the block is not displayed to anonymous users.
|
||||
$this->drupalLogout();
|
||||
$this->drupalGet('');
|
||||
$this->assertNoText($title, 'Block was not displayed to anonymous users.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test block visibility when using "pages" restriction but leaving
|
||||
* "pages" textarea empty
|
||||
*/
|
||||
function testBlockVisibilityListedEmpty() {
|
||||
$block = array();
|
||||
|
||||
// Create a random title for the block
|
||||
$title = $this->randomName(8);
|
||||
|
||||
// Create the custom block
|
||||
$custom_block = array();
|
||||
$custom_block['info'] = $this->randomName(8);
|
||||
$custom_block['title'] = $title;
|
||||
$custom_block['body[value]'] = $this->randomName(32);
|
||||
$this->drupalPost('admin/structure/block/add', $custom_block, t('Save block'));
|
||||
|
||||
$bid = db_query("SELECT bid FROM {block_custom} WHERE info = :info", array(':info' => $custom_block['info']))->fetchField();
|
||||
$block['module'] = 'block';
|
||||
$block['delta'] = $bid;
|
||||
$block['title'] = $title;
|
||||
|
||||
// Move block to the first sidebar.
|
||||
$this->moveBlockToRegion($block, $this->regions[1]);
|
||||
|
||||
// Set the block to be hidden on any user path, and to be shown only to
|
||||
// authenticated users.
|
||||
$edit = array();
|
||||
$edit['visibility'] = BLOCK_VISIBILITY_LISTED;
|
||||
$this->drupalPost('admin/structure/block/manage/' . $block['module'] . '/' . $block['delta'] . '/configure', $edit, t('Save block'));
|
||||
|
||||
$this->drupalGet('');
|
||||
$this->assertNoText($title, 'Block was not displayed according to block visibility rules.');
|
||||
|
||||
$this->drupalGet('user');
|
||||
$this->assertNoText($title, 'Block was not displayed according to block visibility rules regardless of path case.');
|
||||
|
||||
// Confirm that the block is not displayed to anonymous users.
|
||||
$this->drupalLogout();
|
||||
$this->drupalGet('');
|
||||
$this->assertNoText($title, 'Block was not displayed to anonymous users.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test user customization of block visibility.
|
||||
*/
|
||||
function testBlockVisibilityPerUser() {
|
||||
$block = array();
|
||||
|
||||
// Create a random title for the block.
|
||||
$title = $this->randomName(8);
|
||||
|
||||
// Create our custom test block.
|
||||
$custom_block = array();
|
||||
$custom_block['info'] = $this->randomName(8);
|
||||
$custom_block['title'] = $title;
|
||||
$custom_block['body[value]'] = $this->randomName(32);
|
||||
$this->drupalPost('admin/structure/block/add', $custom_block, t('Save block'));
|
||||
|
||||
$bid = db_query("SELECT bid FROM {block_custom} WHERE info = :info", array(':info' => $custom_block['info']))->fetchField();
|
||||
$block['module'] = 'block';
|
||||
$block['delta'] = $bid;
|
||||
$block['title'] = $title;
|
||||
|
||||
// Move block to the first sidebar.
|
||||
$this->moveBlockToRegion($block, $this->regions[1]);
|
||||
|
||||
// Set the block to be customizable per user, visible by default.
|
||||
$edit = array();
|
||||
$edit['custom'] = BLOCK_CUSTOM_ENABLED;
|
||||
$this->drupalPost('admin/structure/block/manage/' . $block['module'] . '/' . $block['delta'] . '/configure', $edit, t('Save block'));
|
||||
|
||||
// Disable block visibility for the admin user.
|
||||
$edit = array();
|
||||
$edit['block[' . $block['module'] . '][' . $block['delta'] . ']'] = FALSE;
|
||||
$this->drupalPost('user/' . $this->admin_user->uid . '/edit', $edit, t('Save'));
|
||||
|
||||
$this->drupalGet('');
|
||||
$this->assertNoText($block['title'], 'Block was not displayed according to per user block visibility setting.');
|
||||
|
||||
// Set the block to be customizable per user, hidden by default.
|
||||
$edit = array();
|
||||
$edit['custom'] = BLOCK_CUSTOM_DISABLED;
|
||||
$this->drupalPost('admin/structure/block/manage/' . $block['module'] . '/' . $block['delta'] . '/configure', $edit, t('Save block'));
|
||||
|
||||
// Enable block visibility for the admin user.
|
||||
$edit = array();
|
||||
$edit['block[' . $block['module'] . '][' . $block['delta'] . ']'] = TRUE;
|
||||
$this->drupalPost('user/' . $this->admin_user->uid . '/edit', $edit, t('Save'));
|
||||
|
||||
$this->drupalGet('');
|
||||
$this->assertText($block['title'], 'Block was displayed according to per user block visibility setting.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test configuring and moving a module-define block to specific regions.
|
||||
*/
|
||||
function testBlock() {
|
||||
// Select the Navigation block to be configured and moved.
|
||||
$block = array();
|
||||
$block['module'] = 'system';
|
||||
$block['delta'] = 'management';
|
||||
$block['title'] = $this->randomName(8);
|
||||
|
||||
// Set block title to confirm that interface works and override any custom titles.
|
||||
$this->drupalPost('admin/structure/block/manage/' . $block['module'] . '/' . $block['delta'] . '/configure', array('title' => $block['title']), t('Save block'));
|
||||
$this->assertText(t('The block configuration has been saved.'), 'Block title set.');
|
||||
$bid = db_query("SELECT bid FROM {block} WHERE module = :module AND delta = :delta", array(
|
||||
':module' => $block['module'],
|
||||
':delta' => $block['delta'],
|
||||
))->fetchField();
|
||||
|
||||
// Check to see if the block was created by checking that it's in the database.
|
||||
$this->assertTrue($bid, 'Block found in database');
|
||||
|
||||
// Check whether the block can be moved to all available regions.
|
||||
foreach ($this->regions as $region) {
|
||||
$this->moveBlockToRegion($block, $region);
|
||||
}
|
||||
|
||||
// Set the block to the disabled region.
|
||||
$edit = array();
|
||||
$edit['blocks[' . $block['module'] . '_' . $block['delta'] . '][region]'] = '-1';
|
||||
$this->drupalPost('admin/structure/block', $edit, t('Save blocks'));
|
||||
|
||||
// Confirm that the block was moved to the proper region.
|
||||
$this->assertText(t('The block settings have been updated.'), 'Block successfully move to disabled region.');
|
||||
$this->assertNoText(t($block['title']), 'Block no longer appears on page.');
|
||||
|
||||
// Confirm that the region's xpath is not available.
|
||||
$xpath = $this->buildXPathQuery('//div[@id=:id]/*', array(':id' => 'block-block-' . $bid));
|
||||
$this->assertNoFieldByXPath($xpath, FALSE, 'Custom block found in no regions.');
|
||||
|
||||
// For convenience of developers, put the navigation block back.
|
||||
$edit = array();
|
||||
$edit['blocks[' . $block['module'] . '_' . $block['delta'] . '][region]'] = $this->regions[1];
|
||||
$this->drupalPost('admin/structure/block', $edit, t('Save blocks'));
|
||||
$this->assertText(t('The block settings have been updated.'), 'Block successfully move to first sidebar region.');
|
||||
|
||||
$this->drupalPost('admin/structure/block/manage/' . $block['module'] . '/' . $block['delta'] . '/configure', array('title' => 'Navigation'), t('Save block'));
|
||||
$this->assertText(t('The block configuration has been saved.'), 'Block title set.');
|
||||
}
|
||||
|
||||
function moveBlockToRegion($block, $region) {
|
||||
// Set the created block to a specific region.
|
||||
$edit = array();
|
||||
$edit['blocks[' . $block['module'] . '_' . $block['delta'] . '][region]'] = $region;
|
||||
$this->drupalPost('admin/structure/block', $edit, t('Save blocks'));
|
||||
|
||||
// Confirm that the block was moved to the proper region.
|
||||
$this->assertText(t('The block settings have been updated.'), format_string('Block successfully moved to %region_name region.', array( '%region_name' => $region)));
|
||||
|
||||
// Confirm that the block is being displayed.
|
||||
$this->drupalGet('node');
|
||||
$this->assertText(t($block['title']), 'Block successfully being displayed on the page.');
|
||||
|
||||
// Confirm that the custom block was found at the proper region.
|
||||
$xpath = $this->buildXPathQuery('//div[@class=:region-class]//div[@id=:block-id]/*', array(
|
||||
':region-class' => 'region region-' . str_replace('_', '-', $region),
|
||||
':block-id' => 'block-' . $block['module'] . '-' . $block['delta'],
|
||||
));
|
||||
$this->assertFieldByXPath($xpath, NULL, format_string('Custom block found in %region_name region.', array('%region_name' => $region)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test _block_rehash().
|
||||
*/
|
||||
function testBlockRehash() {
|
||||
module_enable(array('block_test'));
|
||||
$this->assertTrue(module_exists('block_test'), 'Test block module enabled.');
|
||||
|
||||
// Our new block should be inserted in the database when we visit the
|
||||
// block management page.
|
||||
$this->drupalGet('admin/structure/block');
|
||||
// Our test block's caching should default to DRUPAL_CACHE_PER_ROLE.
|
||||
$current_caching = db_query("SELECT cache FROM {block} WHERE module = 'block_test' AND delta = 'test_cache'")->fetchField();
|
||||
$this->assertEqual($current_caching, DRUPAL_CACHE_PER_ROLE, 'Test block cache mode defaults to DRUPAL_CACHE_PER_ROLE.');
|
||||
|
||||
// Disable caching for this block.
|
||||
variable_set('block_test_caching', DRUPAL_NO_CACHE);
|
||||
// Flushing all caches should call _block_rehash().
|
||||
drupal_flush_all_caches();
|
||||
// Verify that the database is updated with the new caching mode.
|
||||
$current_caching = db_query("SELECT cache FROM {block} WHERE module = 'block_test' AND delta = 'test_cache'")->fetchField();
|
||||
$this->assertEqual($current_caching, DRUPAL_NO_CACHE, "Test block's database entry updated to DRUPAL_NO_CACHE.");
|
||||
}
|
||||
}
|
||||
|
||||
class NonDefaultBlockAdmin extends DrupalWebTestCase {
|
||||
public static function getInfo() {
|
||||
return array(
|
||||
'name' => 'Non default theme admin',
|
||||
'description' => 'Check the administer page for non default theme.',
|
||||
'group' => 'Block',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test non-default theme admin.
|
||||
*/
|
||||
function testNonDefaultBlockAdmin() {
|
||||
$admin_user = $this->drupalCreateUser(array('administer blocks', 'administer themes'));
|
||||
$this->drupalLogin($admin_user);
|
||||
theme_enable(array('stark'));
|
||||
$this->drupalGet('admin/structure/block/list/stark');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test blocks correctly initialized when picking a new default theme.
|
||||
*/
|
||||
class NewDefaultThemeBlocks extends DrupalWebTestCase {
|
||||
public static function getInfo() {
|
||||
return array(
|
||||
'name' => 'New default theme blocks',
|
||||
'description' => 'Checks that the new default theme gets blocks.',
|
||||
'group' => 'Block',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the enabled Bartik blocks are correctly copied over.
|
||||
*/
|
||||
function testNewDefaultThemeBlocks() {
|
||||
// Create administrative user.
|
||||
$admin_user = $this->drupalCreateUser(array('administer themes'));
|
||||
$this->drupalLogin($admin_user);
|
||||
|
||||
// Ensure no other theme's blocks are in the block table yet.
|
||||
$themes = array();
|
||||
$themes['default'] = variable_get('theme_default', 'bartik');
|
||||
if ($admin_theme = variable_get('admin_theme')) {
|
||||
$themes['admin'] = $admin_theme;
|
||||
}
|
||||
$count = db_query_range('SELECT 1 FROM {block} WHERE theme NOT IN (:themes)', 0, 1, array(':themes' => $themes))->fetchField();
|
||||
$this->assertFalse($count, 'Only the default theme and the admin theme have blocks.');
|
||||
|
||||
// Populate list of all blocks for matching against new theme.
|
||||
$blocks = array();
|
||||
$result = db_query('SELECT * FROM {block} WHERE theme = :theme', array(':theme' => $themes['default']));
|
||||
foreach ($result as $block) {
|
||||
// $block->theme and $block->bid will not match, so remove them.
|
||||
unset($block->theme, $block->bid);
|
||||
$blocks[$block->module][$block->delta] = $block;
|
||||
}
|
||||
|
||||
// Turn on the Stark theme and ensure that it contains all of the blocks
|
||||
// the default theme had.
|
||||
theme_enable(array('stark'));
|
||||
variable_set('theme_default', 'stark');
|
||||
$result = db_query('SELECT * FROM {block} WHERE theme = :theme', array(':theme' => 'stark'));
|
||||
foreach ($result as $block) {
|
||||
unset($block->theme, $block->bid);
|
||||
$this->assertEqual($blocks[$block->module][$block->delta], $block, format_string('Block %name matched', array('%name' => $block->module . '-' . $block->delta)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the block system with admin themes.
|
||||
*/
|
||||
class BlockAdminThemeTestCase extends DrupalWebTestCase {
|
||||
public static function getInfo() {
|
||||
return array(
|
||||
'name' => 'Admin theme block admin accessibility',
|
||||
'description' => "Check whether the block administer page for a disabled theme accessible if and only if it's the admin theme.",
|
||||
'group' => 'Block',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for the accessibility of the admin theme on the block admin page.
|
||||
*/
|
||||
function testAdminTheme() {
|
||||
// Create administrative user.
|
||||
$admin_user = $this->drupalCreateUser(array('administer blocks', 'administer themes'));
|
||||
$this->drupalLogin($admin_user);
|
||||
|
||||
// Ensure that access to block admin page is denied when theme is disabled.
|
||||
$this->drupalGet('admin/structure/block/list/stark');
|
||||
$this->assertResponse(403, 'The block admin page for a disabled theme can not be accessed');
|
||||
|
||||
// Enable admin theme and confirm that tab is accessible.
|
||||
$edit['admin_theme'] = 'stark';
|
||||
$this->drupalPost('admin/appearance', $edit, t('Save configuration'));
|
||||
$this->drupalGet('admin/structure/block/list/stark');
|
||||
$this->assertResponse(200, 'The block admin page for the admin theme can be accessed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test block caching.
|
||||
*/
|
||||
class BlockCacheTestCase extends DrupalWebTestCase {
|
||||
protected $admin_user;
|
||||
protected $normal_user;
|
||||
protected $normal_user_alt;
|
||||
|
||||
public static function getInfo() {
|
||||
return array(
|
||||
'name' => 'Block caching',
|
||||
'description' => 'Test block caching.',
|
||||
'group' => 'Block',
|
||||
);
|
||||
}
|
||||
|
||||
function setUp() {
|
||||
parent::setUp('block_test');
|
||||
|
||||
// Create an admin user, log in and enable test blocks.
|
||||
$this->admin_user = $this->drupalCreateUser(array('administer blocks', 'access administration pages'));
|
||||
$this->drupalLogin($this->admin_user);
|
||||
|
||||
// Create additional users to test caching modes.
|
||||
$this->normal_user = $this->drupalCreateUser();
|
||||
$this->normal_user_alt = $this->drupalCreateUser();
|
||||
// Sync the roles, since drupalCreateUser() creates separate roles for
|
||||
// the same permission sets.
|
||||
user_save($this->normal_user_alt, array('roles' => $this->normal_user->roles));
|
||||
$this->normal_user_alt->roles = $this->normal_user->roles;
|
||||
|
||||
// Enable block caching.
|
||||
variable_set('block_cache', TRUE);
|
||||
|
||||
// Enable our test block.
|
||||
$edit['blocks[block_test_test_cache][region]'] = 'sidebar_first';
|
||||
$this->drupalPost('admin/structure/block', $edit, t('Save blocks'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test DRUPAL_CACHE_PER_ROLE.
|
||||
*/
|
||||
function testCachePerRole() {
|
||||
$this->setCacheMode(DRUPAL_CACHE_PER_ROLE);
|
||||
|
||||
// Enable our test block. Set some content for it to display.
|
||||
$current_content = $this->randomName();
|
||||
variable_set('block_test_content', $current_content);
|
||||
$this->drupalLogin($this->normal_user);
|
||||
$this->drupalGet('');
|
||||
$this->assertText($current_content, 'Block content displays.');
|
||||
|
||||
// Change the content, but the cached copy should still be served.
|
||||
$old_content = $current_content;
|
||||
$current_content = $this->randomName();
|
||||
variable_set('block_test_content', $current_content);
|
||||
$this->drupalGet('');
|
||||
$this->assertText($old_content, 'Block is served from the cache.');
|
||||
|
||||
// Clear the cache and verify that the stale data is no longer there.
|
||||
cache_clear_all();
|
||||
$this->drupalGet('');
|
||||
$this->assertNoText($old_content, 'Block cache clear removes stale cache data.');
|
||||
$this->assertText($current_content, 'Fresh block content is displayed after clearing the cache.');
|
||||
|
||||
// Test whether the cached data is served for the correct users.
|
||||
$old_content = $current_content;
|
||||
$current_content = $this->randomName();
|
||||
variable_set('block_test_content', $current_content);
|
||||
$this->drupalLogout();
|
||||
$this->drupalGet('');
|
||||
$this->assertNoText($old_content, 'Anonymous user does not see content cached per-role for normal user.');
|
||||
|
||||
$this->drupalLogin($this->normal_user_alt);
|
||||
$this->drupalGet('');
|
||||
$this->assertText($old_content, 'User with the same roles sees per-role cached content.');
|
||||
|
||||
$this->drupalLogin($this->admin_user);
|
||||
$this->drupalGet('');
|
||||
$this->assertNoText($old_content, 'Admin user does not see content cached per-role for normal user.');
|
||||
|
||||
$this->drupalLogin($this->normal_user);
|
||||
$this->drupalGet('');
|
||||
$this->assertText($old_content, 'Block is served from the per-role cache.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test DRUPAL_CACHE_GLOBAL.
|
||||
*/
|
||||
function testCacheGlobal() {
|
||||
$this->setCacheMode(DRUPAL_CACHE_GLOBAL);
|
||||
$current_content = $this->randomName();
|
||||
variable_set('block_test_content', $current_content);
|
||||
|
||||
$this->drupalGet('');
|
||||
$this->assertText($current_content, 'Block content displays.');
|
||||
|
||||
$old_content = $current_content;
|
||||
$current_content = $this->randomName();
|
||||
variable_set('block_test_content', $current_content);
|
||||
|
||||
$this->drupalLogout();
|
||||
$this->drupalGet('user');
|
||||
$this->assertText($old_content, 'Block content served from global cache.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test DRUPAL_NO_CACHE.
|
||||
*/
|
||||
function testNoCache() {
|
||||
$this->setCacheMode(DRUPAL_NO_CACHE);
|
||||
$current_content = $this->randomName();
|
||||
variable_set('block_test_content', $current_content);
|
||||
|
||||
// If DRUPAL_NO_CACHE has no effect, the next request would be cached.
|
||||
$this->drupalGet('');
|
||||
$this->assertText($current_content, 'Block content displays.');
|
||||
|
||||
// A cached copy should not be served.
|
||||
$current_content = $this->randomName();
|
||||
variable_set('block_test_content', $current_content);
|
||||
$this->drupalGet('');
|
||||
$this->assertText($current_content, 'DRUPAL_NO_CACHE prevents blocks from being cached.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test DRUPAL_CACHE_PER_USER.
|
||||
*/
|
||||
function testCachePerUser() {
|
||||
$this->setCacheMode(DRUPAL_CACHE_PER_USER);
|
||||
$current_content = $this->randomName();
|
||||
variable_set('block_test_content', $current_content);
|
||||
$this->drupalLogin($this->normal_user);
|
||||
|
||||
$this->drupalGet('');
|
||||
$this->assertText($current_content, 'Block content displays.');
|
||||
|
||||
$old_content = $current_content;
|
||||
$current_content = $this->randomName();
|
||||
variable_set('block_test_content', $current_content);
|
||||
|
||||
$this->drupalGet('');
|
||||
$this->assertText($old_content, 'Block is served from per-user cache.');
|
||||
|
||||
$this->drupalLogin($this->normal_user_alt);
|
||||
$this->drupalGet('');
|
||||
$this->assertText($current_content, 'Per-user block cache is not served for other users.');
|
||||
|
||||
$this->drupalLogin($this->normal_user);
|
||||
$this->drupalGet('');
|
||||
$this->assertText($old_content, 'Per-user block cache is persistent.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test DRUPAL_CACHE_PER_PAGE.
|
||||
*/
|
||||
function testCachePerPage() {
|
||||
$this->setCacheMode(DRUPAL_CACHE_PER_PAGE);
|
||||
$current_content = $this->randomName();
|
||||
variable_set('block_test_content', $current_content);
|
||||
|
||||
$this->drupalGet('node');
|
||||
$this->assertText($current_content, 'Block content displays on the node page.');
|
||||
|
||||
$old_content = $current_content;
|
||||
$current_content = $this->randomName();
|
||||
variable_set('block_test_content', $current_content);
|
||||
|
||||
$this->drupalGet('user');
|
||||
$this->assertNoText($old_content, 'Block content cached for the node page does not show up for the user page.');
|
||||
$this->drupalGet('node');
|
||||
$this->assertText($old_content, 'Block content cached for the node page.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Private helper method to set the test block's cache mode.
|
||||
*/
|
||||
private function setCacheMode($cache_mode) {
|
||||
db_update('block')
|
||||
->fields(array('cache' => $cache_mode))
|
||||
->condition('module', 'block_test')
|
||||
->execute();
|
||||
|
||||
$current_mode = db_query("SELECT cache FROM {block} WHERE module = 'block_test'")->fetchField();
|
||||
if ($current_mode != $cache_mode) {
|
||||
$this->fail(t('Unable to set cache mode to %mode. Current mode: %current_mode', array('%mode' => $cache_mode, '%current_mode' => $current_mode)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test block HTML id validity.
|
||||
*/
|
||||
class BlockHTMLIdTestCase extends DrupalWebTestCase {
|
||||
|
||||
public static function getInfo() {
|
||||
return array(
|
||||
'name' => 'Block HTML id',
|
||||
'description' => 'Test block HTML id validity.',
|
||||
'group' => 'Block',
|
||||
);
|
||||
}
|
||||
|
||||
function setUp() {
|
||||
parent::setUp('block_test');
|
||||
|
||||
// Create an admin user, log in and enable test blocks.
|
||||
$this->admin_user = $this->drupalCreateUser(array('administer blocks', 'access administration pages'));
|
||||
$this->drupalLogin($this->admin_user);
|
||||
|
||||
// Enable our test block.
|
||||
$edit['blocks[block_test_test_html_id][region]'] = 'sidebar_first';
|
||||
$this->drupalPost('admin/structure/block', $edit, t('Save blocks'));
|
||||
|
||||
// Make sure the block has some content so it will appear
|
||||
$current_content = $this->randomName();
|
||||
variable_set('block_test_content', $current_content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test valid HTML id.
|
||||
*/
|
||||
function testHTMLId() {
|
||||
$this->drupalGet('');
|
||||
$this->assertRaw('block-block-test-test-html-id', 'HTML id for test block is valid.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Unit tests for template_preprocess_block().
|
||||
*/
|
||||
class BlockTemplateSuggestionsUnitTest extends DrupalUnitTestCase {
|
||||
public static function getInfo() {
|
||||
return array(
|
||||
'name' => 'Block template suggestions',
|
||||
'description' => 'Test the template_preprocess_block() function.',
|
||||
'group' => 'Block',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if template_preprocess_block() handles the suggestions right.
|
||||
*/
|
||||
function testBlockThemeHookSuggestions() {
|
||||
// Define block delta with underscore to be preprocessed
|
||||
$block1 = new stdClass();
|
||||
$block1->module = 'block';
|
||||
$block1->delta = 'underscore_test';
|
||||
$block1->region = 'footer';
|
||||
$variables1 = array();
|
||||
$variables1['elements']['#block'] = $block1;
|
||||
$variables1['elements']['#children'] = '';
|
||||
template_preprocess_block($variables1);
|
||||
$this->assertEqual($variables1['theme_hook_suggestions'], array('block__footer', 'block__block', 'block__block__underscore_test'), 'Found expected block suggestions for delta with underscore');
|
||||
|
||||
// Define block delta with hyphens to be preprocessed. Hyphens should be
|
||||
// replaced with underscores.
|
||||
$block2 = new stdClass();
|
||||
$block2->module = 'block';
|
||||
$block2->delta = 'hyphen-test';
|
||||
$block2->region = 'footer';
|
||||
$variables2 = array();
|
||||
$variables2['elements']['#block'] = $block2;
|
||||
$variables2['elements']['#children'] = '';
|
||||
template_preprocess_block($variables2);
|
||||
$this->assertEqual($variables2['theme_hook_suggestions'], array('block__footer', 'block__block', 'block__block__hyphen_test'), 'Hyphens (-) in block delta were replaced by underscore (_)');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests for hook_block_view_MODULE_DELTA_alter().
|
||||
*/
|
||||
class BlockViewModuleDeltaAlterWebTest extends DrupalWebTestCase {
|
||||
|
||||
public static function getInfo() {
|
||||
return array(
|
||||
'name' => 'Block view module delta alter',
|
||||
'description' => 'Test the hook_block_view_MODULE_DELTA_alter() hook.',
|
||||
'group' => 'Block',
|
||||
);
|
||||
}
|
||||
|
||||
public function setUp() {
|
||||
parent::setUp(array('block_test'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that the alter hook is called, even if the delta contains a hyphen.
|
||||
*/
|
||||
public function testBlockViewModuleDeltaAlter() {
|
||||
$block = new stdClass;
|
||||
$block->module = 'block_test';
|
||||
$block->delta = 'test_underscore';
|
||||
$block->title = '';
|
||||
$render_array = _block_render_blocks(array('region' => $block));
|
||||
$render = array_pop($render_array);
|
||||
$test_underscore = $render->content['#markup'];
|
||||
$this->assertEqual($test_underscore, 'hook_block_view_MODULE_DELTA_alter', 'Found expected altered block content for delta with underscore');
|
||||
|
||||
$block = new stdClass;
|
||||
$block->module = 'block_test';
|
||||
$block->delta = 'test-hyphen';
|
||||
$block->title = '';
|
||||
$render_array = _block_render_blocks(array('region' => $block));
|
||||
$render = array_pop($render_array);
|
||||
$test_hyphen = $render->content['#markup'];
|
||||
$this->assertEqual($test_hyphen, 'hook_block_view_MODULE_DELTA_alter', 'Hyphens (-) in block delta were replaced by underscore (_)');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that hidden regions do not inherit blocks when a theme is enabled.
|
||||
*/
|
||||
class BlockHiddenRegionTestCase extends DrupalWebTestCase {
|
||||
public static function getInfo() {
|
||||
return array(
|
||||
'name' => 'Blocks not in hidden region',
|
||||
'description' => 'Checks that a newly enabled theme does not inherit blocks to its hidden regions.',
|
||||
'group' => 'Block',
|
||||
);
|
||||
}
|
||||
|
||||
function setUp() {
|
||||
parent::setUp(array('block_test'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that hidden regions do not inherit blocks when a theme is enabled.
|
||||
*/
|
||||
function testBlockNotInHiddenRegion() {
|
||||
// Create administrative user.
|
||||
$admin_user = $this->drupalCreateUser(array('administer blocks', 'administer themes', 'search content'));
|
||||
$this->drupalLogin($admin_user);
|
||||
|
||||
// Enable "block_test_theme" and set it as the default theme.
|
||||
$theme = 'block_test_theme';
|
||||
theme_enable(array($theme));
|
||||
variable_set('theme_default', $theme);
|
||||
menu_rebuild();
|
||||
|
||||
// Ensure that "block_test_theme" is set as the default theme.
|
||||
$this->drupalGet('admin/structure/block');
|
||||
$this->assertText('Block test theme(' . t('active tab') . ')', 'Default local task on blocks admin page is the block test theme.');
|
||||
|
||||
// Ensure that the search form block is displayed.
|
||||
$this->drupalGet('');
|
||||
$this->assertText('Search form', 'Block was displayed on the front page.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that a block assigned to an invalid region triggers the warning.
|
||||
*/
|
||||
class BlockInvalidRegionTestCase extends DrupalWebTestCase {
|
||||
public static function getInfo() {
|
||||
return array(
|
||||
'name' => 'Blocks in invalid regions',
|
||||
'description' => 'Checks that an active block assigned to a non-existing region triggers the warning message and is disabled.',
|
||||
'group' => 'Block',
|
||||
);
|
||||
}
|
||||
|
||||
function setUp() {
|
||||
parent::setUp(array('block', 'block_test'));
|
||||
// Create an admin user.
|
||||
$admin_user = $this->drupalCreateUser(array('administer site configuration', 'access administration pages'));
|
||||
$this->drupalLogin($admin_user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that blocks assigned to invalid regions work correctly.
|
||||
*/
|
||||
function testBlockInInvalidRegion() {
|
||||
// Enable a test block in the default theme and place it in an invalid region.
|
||||
db_merge('block')
|
||||
->key(array(
|
||||
'module' => 'block_test',
|
||||
'delta' => 'test_html_id',
|
||||
'theme' => variable_get('theme_default', 'stark'),
|
||||
))
|
||||
->fields(array(
|
||||
'status' => 1,
|
||||
'region' => 'invalid_region',
|
||||
'cache' => DRUPAL_NO_CACHE,
|
||||
))
|
||||
->execute();
|
||||
|
||||
$warning_message = t('The block %info was assigned to the invalid region %region and has been disabled.', array('%info' => t('Test block html id'), '%region' => 'invalid_region'));
|
||||
|
||||
// Clearing the cache should disable the test block placed in the invalid region.
|
||||
$this->drupalPost('admin/config/development/performance', array(), 'Clear all caches');
|
||||
$this->assertRaw($warning_message, 'Enabled block was in the invalid region and has been disabled.');
|
||||
|
||||
// Clear the cache to check if the warning message is not triggered.
|
||||
$this->drupalPost('admin/config/development/performance', array(), 'Clear all caches');
|
||||
$this->assertNoRaw($warning_message, 'Disabled block in the invalid region will not trigger the warning.');
|
||||
|
||||
// Place disabled test block in the invalid region of the default theme.
|
||||
db_merge('block')
|
||||
->key(array(
|
||||
'module' => 'block_test',
|
||||
'delta' => 'test_html_id',
|
||||
'theme' => variable_get('theme_default', 'stark'),
|
||||
))
|
||||
->fields(array(
|
||||
'region' => 'invalid_region',
|
||||
'cache' => DRUPAL_NO_CACHE,
|
||||
))
|
||||
->execute();
|
||||
|
||||
// Clear the cache to check if the warning message is not triggered.
|
||||
$this->drupalPost('admin/config/development/performance', array(), 'Clear all caches');
|
||||
$this->assertNoRaw($warning_message, 'Disabled block in the invalid region will not trigger the warning.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that block rehashing works correctly.
|
||||
*/
|
||||
class BlockHashTestCase extends DrupalWebTestCase {
|
||||
public static function getInfo() {
|
||||
return array(
|
||||
'name' => 'Block rehash',
|
||||
'description' => 'Checks _block_rehash() functionality.',
|
||||
'group' => 'Block',
|
||||
);
|
||||
}
|
||||
|
||||
function setUp() {
|
||||
parent::setUp(array('block'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that block rehashing does not write to the database too often.
|
||||
*/
|
||||
function testBlockRehash() {
|
||||
// No hook_block_info_alter(), no save.
|
||||
$this->doRehash();
|
||||
module_enable(array('block_test'), FALSE);
|
||||
// Save the new blocks, check that the new blocks exist by checking weight.
|
||||
_block_rehash();
|
||||
$this->assertWeight(0);
|
||||
// Now hook_block_info_alter() exists but no blocks are saved on a second
|
||||
// rehash.
|
||||
$this->doRehash();
|
||||
$this->assertWeight(0);
|
||||
// Now hook_block_info_alter() exists and is changing one block which
|
||||
// should be saved.
|
||||
$GLOBALS['conf']['block_test_info_alter'] = 1;
|
||||
$this->doRehash(TRUE);
|
||||
$this->assertWeight(10000);
|
||||
// Now hook_block_info_alter() exists but already changed the block's
|
||||
// weight before, so it should not be saved again.
|
||||
$this->doRehash();
|
||||
$this->assertWeight(10000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a block rehash and checks several related assertions.
|
||||
*
|
||||
* @param $alter_active
|
||||
* Set to TRUE if the block_test module's hook_block_info_alter()
|
||||
* implementation is expected to make a change that results in an existing
|
||||
* block needing to be resaved to the database. Defaults to FALSE.
|
||||
*/
|
||||
function doRehash($alter_active = FALSE) {
|
||||
$saves = 0;
|
||||
foreach (_block_rehash() as $block) {
|
||||
$module = $block['module'];
|
||||
$delta = $block['delta'];
|
||||
if ($alter_active && $module == 'block_test' && $delta == 'test_html_id') {
|
||||
$this->assertFalse(empty($block['saved']), "$module $delta saved");
|
||||
$saves++;
|
||||
}
|
||||
else {
|
||||
$this->assertTrue(empty($block['saved']), "$module $delta not saved");
|
||||
}
|
||||
}
|
||||
$this->assertEqual($alter_active, $saves);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the block_test module's block has a given weight.
|
||||
*
|
||||
* @param $weight
|
||||
* The expected weight.
|
||||
*/
|
||||
function assertWeight($weight) {
|
||||
$db_weight = db_query('SELECT weight FROM {block} WHERE module = :module AND delta = :delta', array(':module' => 'block_test', ':delta' => 'test_html_id'))->fetchField();
|
||||
// By casting to string the assert fails on FALSE.
|
||||
$this->assertIdentical((string) $db_weight, (string) $weight);
|
||||
}
|
||||
}
|
58
modules/block/block.tpl.php
Normal file
58
modules/block/block.tpl.php
Normal file
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Default theme implementation to display a block.
|
||||
*
|
||||
* Available variables:
|
||||
* - $block->subject: Block title.
|
||||
* - $content: Block content.
|
||||
* - $block->module: Module that generated the block.
|
||||
* - $block->delta: An ID for the block, unique within each module.
|
||||
* - $block->region: The block region embedding the current block.
|
||||
* - $classes: String of classes that can be used to style contextually through
|
||||
* CSS. It can be manipulated through the variable $classes_array from
|
||||
* preprocess functions. The default values can be one or more of the
|
||||
* following:
|
||||
* - block: The current template type, i.e., "theming hook".
|
||||
* - block-[module]: The module generating the block. For example, the user
|
||||
* module is responsible for handling the default user navigation block. In
|
||||
* that case the class would be 'block-user'.
|
||||
* - $title_prefix (array): An array containing additional output populated by
|
||||
* modules, intended to be displayed in front of the main title tag that
|
||||
* appears in the template.
|
||||
* - $title_suffix (array): An array containing additional output populated by
|
||||
* modules, intended to be displayed after the main title tag that appears in
|
||||
* the template.
|
||||
*
|
||||
* Helper variables:
|
||||
* - $classes_array: Array of html class attribute values. It is flattened
|
||||
* into a string within the variable $classes.
|
||||
* - $block_zebra: Outputs 'odd' and 'even' dependent on each block region.
|
||||
* - $zebra: Same output as $block_zebra but independent of any block region.
|
||||
* - $block_id: Counter dependent on each block region.
|
||||
* - $id: Same output as $block_id but independent of any block region.
|
||||
* - $is_front: Flags true when presented in the front page.
|
||||
* - $logged_in: Flags true when the current user is a logged-in member.
|
||||
* - $is_admin: Flags true when the current user is an administrator.
|
||||
* - $block_html_id: A valid HTML ID and guaranteed unique.
|
||||
*
|
||||
* @see template_preprocess()
|
||||
* @see template_preprocess_block()
|
||||
* @see template_process()
|
||||
*
|
||||
* @ingroup themeable
|
||||
*/
|
||||
?>
|
||||
<div id="<?php print $block_html_id; ?>" class="<?php print $classes; ?>"<?php print $attributes; ?>>
|
||||
|
||||
<?php print render($title_prefix); ?>
|
||||
<?php if ($block->subject): ?>
|
||||
<h2<?php print $title_attributes; ?>><?php print $block->subject ?></h2>
|
||||
<?php endif;?>
|
||||
<?php print render($title_suffix); ?>
|
||||
|
||||
<div class="content"<?php print $content_attributes; ?>>
|
||||
<?php print $content ?>
|
||||
</div>
|
||||
</div>
|
12
modules/block/tests/block_test.info
Normal file
12
modules/block/tests/block_test.info
Normal file
|
@ -0,0 +1,12 @@
|
|||
name = Block test
|
||||
description = Provides test blocks.
|
||||
package = Testing
|
||||
version = VERSION
|
||||
core = 7.x
|
||||
hidden = TRUE
|
||||
|
||||
; Information added by Drupal.org packaging script on 2017-06-21
|
||||
version = "7.56"
|
||||
project = "drupal"
|
||||
datestamp = "1498069849"
|
||||
|
67
modules/block/tests/block_test.module
Normal file
67
modules/block/tests/block_test.module
Normal file
|
@ -0,0 +1,67 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Provide test blocks.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Implements hook_system_theme_info().
|
||||
*/
|
||||
function block_test_system_theme_info() {
|
||||
$themes['block_test_theme'] = drupal_get_path('module', 'block_test') . '/themes/block_test_theme/block_test_theme.info';
|
||||
return $themes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_block_info().
|
||||
*/
|
||||
function block_test_block_info() {
|
||||
$blocks['test_cache'] = array(
|
||||
'info' => t('Test block caching'),
|
||||
'cache' => variable_get('block_test_caching', DRUPAL_CACHE_PER_ROLE),
|
||||
);
|
||||
|
||||
$blocks['test_underscore'] = array(
|
||||
'info' => t('Test underscore'),
|
||||
);
|
||||
|
||||
$blocks['test-hyphen'] = array(
|
||||
'info' => t('Test hyphen'),
|
||||
);
|
||||
|
||||
$blocks['test_html_id'] = array(
|
||||
'info' => t('Test block html id'),
|
||||
);
|
||||
return $blocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_block_view().
|
||||
*/
|
||||
function block_test_block_view($delta = 0) {
|
||||
return array('content' => variable_get('block_test_content', ''));
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_block_view_MODULE_DELTA_alter().
|
||||
*/
|
||||
function block_test_block_view_block_test_test_underscore_alter(&$data, $block) {
|
||||
$data['content'] = 'hook_block_view_MODULE_DELTA_alter';
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_block_view_MODULE_DELTA_alter().
|
||||
*/
|
||||
function block_test_block_view_block_test_test_hyphen_alter(&$data, $block) {
|
||||
$data['content'] = 'hook_block_view_MODULE_DELTA_alter';
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_block_info_alter().
|
||||
*/
|
||||
function block_test_block_info_alter(&$blocks) {
|
||||
if (variable_get('block_test_info_alter')) {
|
||||
$blocks['block_test']['test_html_id']['weight'] = 10000;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
name = Block test theme
|
||||
description = Theme for testing the block system
|
||||
core = 7.x
|
||||
hidden = TRUE
|
||||
|
||||
regions[sidebar_first] = Left sidebar
|
||||
regions_hidden[] = sidebar_first
|
||||
regions[sidebar_second] = Right sidebar
|
||||
regions_hidden[] = sidebar_second
|
||||
regions[content] = Content
|
||||
regions[header] = Header
|
||||
regions[footer] = Footer
|
||||
regions[highlighted] = Highlighted
|
||||
regions[help] = Help
|
||||
|
||||
; Information added by Drupal.org packaging script on 2017-06-21
|
||||
version = "7.56"
|
||||
project = "drupal"
|
||||
datestamp = "1498069849"
|
||||
|
78
modules/block/tests/themes/block_test_theme/page.tpl.php
Normal file
78
modules/block/tests/themes/block_test_theme/page.tpl.php
Normal file
|
@ -0,0 +1,78 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Custom theme implementation to display a single Drupal page without
|
||||
* sidebars. The sidebars are hidden by regions_hidden for this theme, so
|
||||
* the default page.tpl.php will not work without throwing exceptions.
|
||||
*/
|
||||
?>
|
||||
|
||||
<div id="page-wrapper"><div id="page">
|
||||
|
||||
<div id="header"><div class="section clearfix">
|
||||
|
||||
<?php if ($logo): ?>
|
||||
<a href="<?php print $front_page; ?>" title="<?php print t('Home'); ?>" rel="home" id="logo">
|
||||
<img src="<?php print $logo; ?>" alt="<?php print t('Home'); ?>" />
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($site_name || $site_slogan): ?>
|
||||
<div id="name-and-slogan">
|
||||
<?php if ($site_name): ?>
|
||||
<?php if ($title): ?>
|
||||
<div id="site-name"><strong>
|
||||
<a href="<?php print $front_page; ?>" title="<?php print t('Home'); ?>" rel="home"><span><?php print $site_name; ?></span></a>
|
||||
</strong></div>
|
||||
<?php else: /* Use h1 when the content title is empty */ ?>
|
||||
<h1 id="site-name">
|
||||
<a href="<?php print $front_page; ?>" title="<?php print t('Home'); ?>" rel="home"><span><?php print $site_name; ?></span></a>
|
||||
</h1>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($site_slogan): ?>
|
||||
<div id="site-slogan"><?php print $site_slogan; ?></div>
|
||||
<?php endif; ?>
|
||||
</div> <!-- /#name-and-slogan -->
|
||||
<?php endif; ?>
|
||||
|
||||
<?php print render($page['header']); ?>
|
||||
|
||||
</div></div> <!-- /.section, /#header -->
|
||||
|
||||
<?php if ($main_menu || $secondary_menu): ?>
|
||||
<div id="navigation"><div class="section">
|
||||
<?php print theme('links__system_main_menu', array('links' => $main_menu, 'attributes' => array('id' => 'main-menu', 'class' => array('links', 'inline', 'clearfix')), 'heading' => t('Main menu'))); ?>
|
||||
<?php print theme('links__system_secondary_menu', array('links' => $secondary_menu, 'attributes' => array('id' => 'secondary-menu', 'class' => array('links', 'inline', 'clearfix')), 'heading' => t('Secondary menu'))); ?>
|
||||
</div></div> <!-- /.section, /#navigation -->
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($breadcrumb): ?>
|
||||
<div id="breadcrumb"><?php print $breadcrumb; ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php print $messages; ?>
|
||||
|
||||
<div id="main-wrapper"><div id="main" class="clearfix">
|
||||
|
||||
<div id="content" class="column"><div class="section">
|
||||
<?php if ($page['highlighted']): ?><div id="highlighted"><?php print render($page['highlighted']); ?></div><?php endif; ?>
|
||||
<a id="main-content"></a>
|
||||
<?php print render($title_prefix); ?>
|
||||
<?php if ($title): ?><h1 class="title" id="page-title"><?php print $title; ?></h1><?php endif; ?>
|
||||
<?php print render($title_suffix); ?>
|
||||
<?php if ($tabs = render($tabs)): ?><div class="tabs"><?php print $tabs; ?></div><?php endif; ?>
|
||||
<?php print render($page['help']); ?>
|
||||
<?php if ($action_links): ?><ul class="action-links"><?php print render($action_links); ?></ul><?php endif; ?>
|
||||
<?php print render($page['content']); ?>
|
||||
<?php print $feed_icons; ?>
|
||||
</div></div> <!-- /.section, /#content -->
|
||||
</div></div> <!-- /#main, /#main-wrapper -->
|
||||
|
||||
<div id="footer"><div class="section">
|
||||
<?php print render($page['footer']); ?>
|
||||
</div></div> <!-- /.section, /#footer -->
|
||||
|
||||
</div></div> <!-- /#page, /#page-wrapper -->
|
12
modules/blog/blog.info
Normal file
12
modules/blog/blog.info
Normal file
|
@ -0,0 +1,12 @@
|
|||
name = Blog
|
||||
description = Enables multi-user blogs.
|
||||
package = Core
|
||||
version = VERSION
|
||||
core = 7.x
|
||||
files[] = blog.test
|
||||
|
||||
; Information added by Drupal.org packaging script on 2017-06-21
|
||||
version = "7.56"
|
||||
project = "drupal"
|
||||
datestamp = "1498069849"
|
||||
|
23
modules/blog/blog.install
Normal file
23
modules/blog/blog.install
Normal file
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Install, update and uninstall functions for the blog module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Implements hook_install().
|
||||
*/
|
||||
function blog_install() {
|
||||
// Ensure the blog node type is available.
|
||||
node_types_rebuild();
|
||||
$types = node_type_get_types();
|
||||
node_add_body_field($types['blog']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_uninstall().
|
||||
*/
|
||||
function blog_uninstall() {
|
||||
variable_del('blog_block_count');
|
||||
}
|
272
modules/blog/blog.module
Normal file
272
modules/blog/blog.module
Normal file
|
@ -0,0 +1,272 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Enables multi-user blogs.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Implements hook_node_info().
|
||||
*/
|
||||
function blog_node_info() {
|
||||
return array(
|
||||
'blog' => array(
|
||||
'name' => t('Blog entry'),
|
||||
'base' => 'blog',
|
||||
'description' => t('Use for multi-user blogs. Every user gets a personal blog.'),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_user_view().
|
||||
*/
|
||||
function blog_user_view($account) {
|
||||
if (user_access('create blog content', $account)) {
|
||||
$account->content['summary']['blog'] = array(
|
||||
'#type' => 'user_profile_item',
|
||||
'#title' => t('Blog'),
|
||||
// l() escapes the attributes, so we should not escape !username here.
|
||||
'#markup' => l(t('View recent blog entries'), "blog/$account->uid", array('attributes' => array('title' => t("Read !username's latest blog entries.", array('!username' => format_username($account)))))),
|
||||
'#attributes' => array('class' => array('blog')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_help().
|
||||
*/
|
||||
function blog_help($path, $arg) {
|
||||
switch ($path) {
|
||||
case 'admin/help#blog':
|
||||
$output = '<h3>' . t('About') . '</h3>';
|
||||
$output .= '<p>' . t("The Blog module allows registered users to maintain an online journal, or <em>blog</em>. Blogs are made up of individual <em>blog entries</em>. By default, the blog entries are displayed by creation time in descending order, with comments enabled, and are promoted to the site's front page. For more information, see the online handbook entry for <a href='@blog'>Blog module</a>.", array('@blog' => 'http://drupal.org/documentation/modules/blog/')) . '</p>';
|
||||
$output .= '<h3>' . t('Uses') . '</h3>';
|
||||
$output .= '<dl>';
|
||||
$output .= '<dt>' . t('Single-user blogs') . '</dt>';
|
||||
$output .= '<dd>' . t("Each user's blog entries are automatically displayed with a link to the user's main blog page. You can create as many single-user blogs as you have site users with permission to create blog content.") . '</dd>';
|
||||
$output .= '<dt>' . t('Multi-user blogs') . '</dt>';
|
||||
$output .= '<dd>' . t("Blog entries from each single-user blog are also aggregated into one central multi-user blog, which displays the blog content of all users in a single listing.") . '</dd>';
|
||||
$output .= '<dt>' . t('Navigation') . '</dt>';
|
||||
$output .= '<dd>' . t("There is an optional <em>Blogs</em> menu item added to the Navigation menu, which displays all blogs available on your site, and a <em>My blog</em> item displaying the current user's blog entries.") . '</dd>';
|
||||
$output .= '<dt>' . t('Blocks') . '</dt>';
|
||||
$output .= '<dd>' . t('The Blog module also creates a default <em>Recent blog posts</em> block that may be enabled at the <a href="@blocks">blocks administration page</a>.', array('@blocks' => url('admin/structure/block'))) . '</dd>';
|
||||
$output .= '</dl>';
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_form().
|
||||
*/
|
||||
function blog_form($node, $form_state) {
|
||||
return node_content_form($node, $form_state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_view().
|
||||
*/
|
||||
function blog_view($node, $view_mode) {
|
||||
if ($view_mode == 'full' && node_is_page($node)) {
|
||||
// Breadcrumb navigation. l() escapes title, so we should not escape !name.
|
||||
drupal_set_breadcrumb(array(l(t('Home'), NULL), l(t('Blogs'), 'blog'), l(t("!name's blog", array('!name' => format_username($node))), 'blog/' . $node->uid)));
|
||||
}
|
||||
return $node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_node_view().
|
||||
*/
|
||||
function blog_node_view($node, $view_mode) {
|
||||
if ($view_mode != 'rss') {
|
||||
if ($node->type == 'blog' && (arg(0) != 'blog' || arg(1) != $node->uid)) {
|
||||
// This goes to l(), which escapes !username in both title and attributes.
|
||||
$links['blog_usernames_blog'] = array(
|
||||
'title' => t("!username's blog", array('!username' => format_username($node))),
|
||||
'href' => "blog/$node->uid",
|
||||
'attributes' => array('title' => t("Read !username's latest blog entries.", array('!username' => format_username($node)))),
|
||||
);
|
||||
$node->content['links']['blog'] = array(
|
||||
'#theme' => 'links__node__blog',
|
||||
'#links' => $links,
|
||||
'#attributes' => array('class' => array('links', 'inline')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_menu().
|
||||
*/
|
||||
function blog_menu() {
|
||||
$items['blog'] = array(
|
||||
'title' => 'Blogs',
|
||||
'page callback' => 'blog_page_last',
|
||||
'access arguments' => array('access content'),
|
||||
'type' => MENU_SUGGESTED_ITEM,
|
||||
'file' => 'blog.pages.inc',
|
||||
);
|
||||
$items['blog/%user_uid_optional'] = array(
|
||||
'title' => 'My blog',
|
||||
'page callback' => 'blog_page_user',
|
||||
'page arguments' => array(1),
|
||||
'access callback' => 'blog_page_user_access',
|
||||
'access arguments' => array(1),
|
||||
'file' => 'blog.pages.inc',
|
||||
);
|
||||
$items['blog/%user/feed'] = array(
|
||||
'title' => 'Blogs',
|
||||
'page callback' => 'blog_feed_user',
|
||||
'page arguments' => array(1),
|
||||
'access callback' => 'blog_page_user_access',
|
||||
'access arguments' => array(1),
|
||||
'type' => MENU_CALLBACK,
|
||||
'file' => 'blog.pages.inc',
|
||||
);
|
||||
$items['blog/feed'] = array(
|
||||
'title' => 'Blogs',
|
||||
'page callback' => 'blog_feed_last',
|
||||
'access arguments' => array('access content'),
|
||||
'type' => MENU_CALLBACK,
|
||||
'file' => 'blog.pages.inc',
|
||||
);
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_menu_local_tasks_alter().
|
||||
*/
|
||||
function blog_menu_local_tasks_alter(&$data, $router_item, $root_path) {
|
||||
global $user;
|
||||
|
||||
// Add action link to 'node/add/blog' on 'blog' page.
|
||||
if ($root_path == 'blog') {
|
||||
$item = menu_get_item('node/add/blog');
|
||||
if ($item['access']) {
|
||||
$item['title'] = t('Create new blog entry');
|
||||
$data['actions']['output'][] = array(
|
||||
'#theme' => 'menu_local_action',
|
||||
'#link' => $item,
|
||||
);
|
||||
}
|
||||
}
|
||||
// Provide a helper action link to the author on the 'blog/%' page.
|
||||
elseif ($root_path == 'blog/%' && isset($router_item['page_arguments'][0]->uid) && $router_item['page_arguments'][0]->uid == $user->uid) {
|
||||
$data['actions']['output']['blog'] = array(
|
||||
'#theme' => 'menu_local_action',
|
||||
);
|
||||
if (user_access('create blog content')) {
|
||||
$data['actions']['output']['blog']['#link']['title'] = t('Post new blog entry.');
|
||||
$data['actions']['output']['blog']['#link']['href'] = 'node/add/blog';
|
||||
}
|
||||
else {
|
||||
$data['actions']['output']['blog']['#link']['title'] = t('You are not allowed to post a new blog entry.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Access callback for user blog pages.
|
||||
*/
|
||||
function blog_page_user_access($account) {
|
||||
// The visitor must be able to access the site's content.
|
||||
// For a blog to 'exist' the user must either be able to
|
||||
// create new blog entries, or it must have existing posts.
|
||||
return $account->uid && user_access('access content') && (user_access('create blog content', $account) || _blog_post_exists($account));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to determine if a user has blog posts already.
|
||||
*/
|
||||
function _blog_post_exists($account) {
|
||||
return (bool)db_select('node', 'n')
|
||||
->fields('n', array('nid'))
|
||||
->condition('type', 'blog')
|
||||
->condition('uid', $account->uid)
|
||||
->condition('status', 1)
|
||||
->range(0, 1)
|
||||
->addTag('node_access')
|
||||
->execute()
|
||||
->fetchField();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_block_info().
|
||||
*/
|
||||
function blog_block_info() {
|
||||
$block['recent']['info'] = t('Recent blog posts');
|
||||
$block['recent']['properties']['administrative'] = TRUE;
|
||||
return $block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_block_configure().
|
||||
*/
|
||||
function blog_block_configure($delta = '') {
|
||||
if ($delta == 'recent') {
|
||||
$form['blog_block_count'] = array(
|
||||
'#type' => 'select',
|
||||
'#title' => t('Number of recent blog posts to display'),
|
||||
'#default_value' => variable_get('blog_block_count', 10),
|
||||
'#options' => drupal_map_assoc(array(2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 25, 30)),
|
||||
);
|
||||
return $form;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_block_save().
|
||||
*/
|
||||
function blog_block_save($delta = '', $edit = array()) {
|
||||
if ($delta == 'recent') {
|
||||
variable_set('blog_block_count', $edit['blog_block_count']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_block_view().
|
||||
*
|
||||
* Displays the most recent 10 blog titles.
|
||||
*/
|
||||
function blog_block_view($delta = '') {
|
||||
global $user;
|
||||
|
||||
if (user_access('access content')) {
|
||||
$result = db_select('node', 'n')
|
||||
->fields('n', array('nid', 'title', 'created'))
|
||||
->condition('type', 'blog')
|
||||
->condition('status', 1)
|
||||
->orderBy('created', 'DESC')
|
||||
->range(0, variable_get('blog_block_count', 10))
|
||||
->addTag('node_access')
|
||||
->execute();
|
||||
|
||||
if ($node_title_list = node_title_list($result)) {
|
||||
$block['subject'] = t('Recent blog posts');
|
||||
$block['content']['blog_list'] = $node_title_list;
|
||||
$block['content']['blog_more'] = array(
|
||||
'#theme' => 'more_link',
|
||||
'#url' => 'blog',
|
||||
'#title' => t('Read the latest blog entries.'),
|
||||
);
|
||||
|
||||
return $block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_rdf_mapping().
|
||||
*/
|
||||
function blog_rdf_mapping() {
|
||||
return array(
|
||||
array(
|
||||
'type' => 'node',
|
||||
'bundle' => 'blog',
|
||||
'mapping' => array(
|
||||
'rdftype' => array('sioc:Post', 'sioct:BlogPost'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
127
modules/blog/blog.pages.inc
Normal file
127
modules/blog/blog.pages.inc
Normal file
|
@ -0,0 +1,127 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Page callback file for the blog module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Menu callback; displays a Drupal page containing recent blog entries of a given user.
|
||||
*/
|
||||
function blog_page_user($account) {
|
||||
global $user;
|
||||
|
||||
drupal_set_title($title = t("@name's blog", array('@name' => format_username($account))), PASS_THROUGH);
|
||||
|
||||
$build = array();
|
||||
|
||||
$query = db_select('node', 'n')->extend('PagerDefault');
|
||||
$nids = $query
|
||||
->fields('n', array('nid', 'sticky', 'created'))
|
||||
->condition('type', 'blog')
|
||||
->condition('uid', $account->uid)
|
||||
->condition('status', 1)
|
||||
->orderBy('sticky', 'DESC')
|
||||
->orderBy('created', 'DESC')
|
||||
->limit(variable_get('default_nodes_main', 10))
|
||||
->addTag('node_access')
|
||||
->execute()
|
||||
->fetchCol();
|
||||
|
||||
if (!empty($nids)) {
|
||||
$nodes = node_load_multiple($nids);
|
||||
$build += node_view_multiple($nodes);
|
||||
$build['pager'] = array(
|
||||
'#theme' => 'pager',
|
||||
'#weight' => 5,
|
||||
);
|
||||
}
|
||||
else {
|
||||
if ($account->uid == $user->uid) {
|
||||
drupal_set_message(t('You have not created any blog entries.'));
|
||||
}
|
||||
else {
|
||||
drupal_set_message(t('!author has not created any blog entries.', array('!author' => theme('username', array('account' => $account)))));
|
||||
}
|
||||
}
|
||||
drupal_add_feed('blog/' . $account->uid . '/feed', t('RSS - !title', array('!title' => $title)));
|
||||
|
||||
return $build;
|
||||
}
|
||||
|
||||
/**
|
||||
* Menu callback; displays a Drupal page containing recent blog entries of all users.
|
||||
*/
|
||||
function blog_page_last() {
|
||||
global $user;
|
||||
$build = array();
|
||||
|
||||
$query = db_select('node', 'n')->extend('PagerDefault');
|
||||
$nids = $query
|
||||
->fields('n', array('nid', 'sticky', 'created'))
|
||||
->condition('type', 'blog')
|
||||
->condition('status', 1)
|
||||
->orderBy('sticky', 'DESC')
|
||||
->orderBy('created', 'DESC')
|
||||
->limit(variable_get('default_nodes_main', 10))
|
||||
->addTag('node_access')
|
||||
->execute()
|
||||
->fetchCol();
|
||||
|
||||
if (!empty($nids)) {
|
||||
$nodes = node_load_multiple($nids);
|
||||
$build += node_view_multiple($nodes);
|
||||
$build['pager'] = array(
|
||||
'#theme' => 'pager',
|
||||
'#weight' => 5,
|
||||
);
|
||||
}
|
||||
else {
|
||||
drupal_set_message(t('No blog entries have been created.'));
|
||||
}
|
||||
drupal_add_feed('blog/feed', t('RSS - blogs'));
|
||||
|
||||
return $build;
|
||||
}
|
||||
|
||||
/**
|
||||
* Menu callback; displays an RSS feed containing recent blog entries of a given user.
|
||||
*/
|
||||
function blog_feed_user($account) {
|
||||
|
||||
$nids = db_select('node', 'n')
|
||||
->fields('n', array('nid', 'created'))
|
||||
->condition('type', 'blog')
|
||||
->condition('uid', $account->uid)
|
||||
->condition('status', 1)
|
||||
->orderBy('created', 'DESC')
|
||||
->range(0, variable_get('feed_default_items', 10))
|
||||
->addTag('node_access')
|
||||
->execute()
|
||||
->fetchCol();
|
||||
|
||||
$channel['title'] = t("!name's blog", array('!name' => format_username($account)));
|
||||
$channel['link'] = url('blog/' . $account->uid, array('absolute' => TRUE));
|
||||
|
||||
node_feed($nids, $channel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Menu callback; displays an RSS feed containing recent blog entries of all users.
|
||||
*/
|
||||
function blog_feed_last() {
|
||||
$nids = db_select('node', 'n')
|
||||
->fields('n', array('nid', 'created'))
|
||||
->condition('type', 'blog')
|
||||
->condition('status', 1)
|
||||
->orderBy('created', 'DESC')
|
||||
->range(0, variable_get('feed_default_items', 10))
|
||||
->addTag('node_access')
|
||||
->execute()
|
||||
->fetchCol();
|
||||
|
||||
$channel['title'] = t('!site_name blogs', array('!site_name' => variable_get('site_name', 'Drupal')));
|
||||
$channel['link'] = url('blog', array('absolute' => TRUE));
|
||||
|
||||
node_feed($nids, $channel);
|
||||
}
|
213
modules/blog/blog.test
Normal file
213
modules/blog/blog.test
Normal file
|
@ -0,0 +1,213 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Tests for blog.module.
|
||||
*/
|
||||
|
||||
class BlogTestCase extends DrupalWebTestCase {
|
||||
protected $big_user;
|
||||
protected $own_user;
|
||||
protected $any_user;
|
||||
|
||||
public static function getInfo() {
|
||||
return array(
|
||||
'name' => 'Blog functionality',
|
||||
'description' => 'Create, view, edit, delete, and change blog entries and verify its consistency in the database.',
|
||||
'group' => 'Blog',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable modules and create users with specific permissions.
|
||||
*/
|
||||
function setUp() {
|
||||
parent::setUp('blog');
|
||||
// Create users.
|
||||
$this->big_user = $this->drupalCreateUser(array('administer blocks'));
|
||||
$this->own_user = $this->drupalCreateUser(array('create blog content', 'edit own blog content', 'delete own blog content'));
|
||||
$this->any_user = $this->drupalCreateUser(array('create blog content', 'edit any blog content', 'delete any blog content', 'access administration pages'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm that the "You are not allowed to post a new blog entry." message
|
||||
* shows up if a user submitted blog entries, has been denied that
|
||||
* permission, and goes to the blog page.
|
||||
*/
|
||||
function testUnprivilegedUser() {
|
||||
// Create a blog node for a user with no blog permissions.
|
||||
$this->drupalCreateNode(array('type' => 'blog', 'uid' => $this->big_user->uid));
|
||||
|
||||
$this->drupalLogin($this->big_user);
|
||||
|
||||
$this->drupalGet('blog/' . $this->big_user->uid);
|
||||
$this->assertResponse(200);
|
||||
$this->assertTitle(t("@name's blog", array('@name' => format_username($this->big_user))) . ' | Drupal', 'Blog title was displayed');
|
||||
$this->assertText(t('You are not allowed to post a new blog entry.'), 'No new entries can be posted without the right permission');
|
||||
}
|
||||
|
||||
/**
|
||||
* View the blog of a user with no blog entries as another user.
|
||||
*/
|
||||
function testBlogPageNoEntries() {
|
||||
$this->drupalLogin($this->big_user);
|
||||
|
||||
$this->drupalGet('blog/' . $this->own_user->uid);
|
||||
$this->assertResponse(200);
|
||||
$this->assertTitle(t("@name's blog", array('@name' => format_username($this->own_user))) . ' | Drupal', 'Blog title was displayed');
|
||||
$this->assertText(t('@author has not created any blog entries.', array('@author' => format_username($this->own_user))), 'Users blog displayed with no entries');
|
||||
}
|
||||
|
||||
/**
|
||||
* Login users, create blog nodes, and test blog functionality through the admin and user interfaces.
|
||||
*/
|
||||
function testBlog() {
|
||||
// Login the admin user.
|
||||
$this->drupalLogin($this->big_user);
|
||||
// Enable the recent blog block.
|
||||
$edit = array();
|
||||
$edit['blocks[blog_recent][region]'] = 'sidebar_second';
|
||||
$this->drupalPost('admin/structure/block', $edit, t('Save blocks'));
|
||||
$this->assertResponse(200);
|
||||
// Verify ability to change number of recent blog posts in block.
|
||||
$edit = array();
|
||||
$edit['blog_block_count'] = 5;
|
||||
$this->drupalPost('admin/structure/block/manage/blog/recent/configure', $edit, t('Save block'));
|
||||
$this->assertEqual(variable_get('blog_block_count', 10), 5, 'Number of recent blog posts changed.');
|
||||
|
||||
// Do basic tests for each user.
|
||||
$this->doBasicTests($this->any_user, TRUE);
|
||||
$this->doBasicTests($this->own_user, FALSE);
|
||||
|
||||
// Create another blog node for the any blog user.
|
||||
$node = $this->drupalCreateNode(array('type' => 'blog', 'uid' => $this->any_user->uid));
|
||||
// Verify the own blog user only has access to the blog view node.
|
||||
$this->verifyBlogs($this->any_user, $node, FALSE, 403);
|
||||
|
||||
// Create another blog node for the own blog user.
|
||||
$node = $this->drupalCreateNode(array('type' => 'blog', 'uid' => $this->own_user->uid));
|
||||
// Login the any blog user.
|
||||
$this->drupalLogin($this->any_user);
|
||||
// Verify the any blog user has access to all the blog nodes.
|
||||
$this->verifyBlogs($this->own_user, $node, TRUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run basic tests on the indicated user.
|
||||
*
|
||||
* @param object $user
|
||||
* The logged in user.
|
||||
* @param boolean $admin
|
||||
* User has 'access administration pages' privilege.
|
||||
*/
|
||||
private function doBasicTests($user, $admin) {
|
||||
// Login the user.
|
||||
$this->drupalLogin($user);
|
||||
// Create blog node.
|
||||
$node = $this->drupalCreateNode(array('type' => 'blog'));
|
||||
// Verify the user has access to all the blog nodes.
|
||||
$this->verifyBlogs($user, $node, $admin);
|
||||
// Create one more node to test the blog page with more than one node
|
||||
$this->drupalCreateNode(array('type' => 'blog', 'uid' => $user->uid));
|
||||
// Verify the blog links are displayed.
|
||||
$this->verifyBlogLinks($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the logged in user has the desired access to the various blog nodes.
|
||||
*
|
||||
* @param object $node_user
|
||||
* The user who creates the node.
|
||||
* @param object $node
|
||||
* A node object.
|
||||
* @param boolean $admin
|
||||
* User has 'access administration pages' privilege.
|
||||
* @param integer $response
|
||||
* HTTP response code.
|
||||
*/
|
||||
private function verifyBlogs($node_user, $node, $admin, $response = 200) {
|
||||
$response2 = ($admin) ? 200 : 403;
|
||||
|
||||
// View blog help node.
|
||||
$this->drupalGet('admin/help/blog');
|
||||
$this->assertResponse($response2);
|
||||
if ($response2 == 200) {
|
||||
$this->assertTitle(t('Blog | Drupal'), 'Blog help node was displayed');
|
||||
$this->assertText(t('Blog'), 'Blog help node was displayed');
|
||||
}
|
||||
|
||||
// Verify the blog block was displayed.
|
||||
$this->drupalGet('');
|
||||
$this->assertResponse(200);
|
||||
$this->assertText(t('Recent blog posts'), 'Blog block was displayed');
|
||||
|
||||
// View blog node.
|
||||
$this->drupalGet('node/' . $node->nid);
|
||||
$this->assertResponse(200);
|
||||
$this->assertTitle($node->title . ' | Drupal', 'Blog node was displayed');
|
||||
$breadcrumb = array(
|
||||
l(t('Home'), NULL),
|
||||
l(t('Blogs'), 'blog'),
|
||||
l(t("!name's blog", array('!name' => format_username($node_user))), 'blog/' . $node_user->uid),
|
||||
);
|
||||
$this->assertRaw(theme('breadcrumb', array('breadcrumb' => $breadcrumb)), 'Breadcrumbs were displayed');
|
||||
|
||||
// View blog edit node.
|
||||
$this->drupalGet('node/' . $node->nid . '/edit');
|
||||
$this->assertResponse($response);
|
||||
if ($response == 200) {
|
||||
$this->assertTitle('Edit Blog entry ' . $node->title . ' | Drupal', 'Blog edit node was displayed');
|
||||
}
|
||||
|
||||
if ($response == 200) {
|
||||
// Edit blog node.
|
||||
$edit = array();
|
||||
$langcode = LANGUAGE_NONE;
|
||||
$edit["title"] = 'node/' . $node->nid;
|
||||
$edit["body[$langcode][0][value]"] = $this->randomName(256);
|
||||
$this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save'));
|
||||
$this->assertRaw(t('Blog entry %title has been updated.', array('%title' => $edit["title"])), 'Blog node was edited');
|
||||
|
||||
// Delete blog node.
|
||||
$this->drupalPost('node/' . $node->nid . '/delete', array(), t('Delete'));
|
||||
$this->assertResponse($response);
|
||||
$this->assertRaw(t('Blog entry %title has been deleted.', array('%title' => $edit["title"])), 'Blog node was deleted');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the blog links are displayed to the logged in user.
|
||||
*
|
||||
* @param object $user
|
||||
* The logged in user.
|
||||
*/
|
||||
private function verifyBlogLinks($user) {
|
||||
// Confirm blog entries link exists on the user page.
|
||||
$this->drupalGet('user/' . $user->uid);
|
||||
$this->assertResponse(200);
|
||||
$this->assertText(t('View recent blog entries'), 'View recent blog entries link was displayed');
|
||||
|
||||
// Confirm the recent blog entries link goes to the user's blog page.
|
||||
$this->clickLink('View recent blog entries');
|
||||
$this->assertTitle(t("@name's blog | Drupal", array('@name' => format_username($user))), 'View recent blog entries link target was correct');
|
||||
|
||||
// Confirm a blog page was displayed.
|
||||
$this->drupalGet('blog');
|
||||
$this->assertResponse(200);
|
||||
$this->assertTitle('Blogs | Drupal', 'Blog page was displayed');
|
||||
$this->assertText(t('Home'), 'Breadcrumbs were displayed');
|
||||
$this->assertLink(t('Create new blog entry'));
|
||||
|
||||
// Confirm a blog page was displayed per user.
|
||||
$this->drupalGet('blog/' . $user->uid);
|
||||
$this->assertTitle(t("@name's blog | Drupal", array('@name' => format_username($user))), 'User blog node was displayed');
|
||||
|
||||
// Confirm a blog feed was displayed.
|
||||
$this->drupalGet('blog/feed');
|
||||
$this->assertTitle(t('Drupal blogs'), 'Blog feed was displayed');
|
||||
|
||||
// Confirm a blog feed was displayed per user.
|
||||
$this->drupalGet('blog/' . $user->uid . '/feed');
|
||||
$this->assertTitle(t("@name's blog", array('@name' => format_username($user))), 'User blog feed was displayed');
|
||||
}
|
||||
}
|
23
modules/book/book-all-books-block.tpl.php
Normal file
23
modules/book/book-all-books-block.tpl.php
Normal file
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Default theme implementation for rendering book outlines within a block.
|
||||
*
|
||||
* This template is used only when the block is configured to "show block on all
|
||||
* pages", which presents multiple independent books on all pages.
|
||||
*
|
||||
* Available variables:
|
||||
* - $book_menus: Array of book outlines keyed to the parent book ID. Call
|
||||
* render() on each to print it as an unordered list.
|
||||
*
|
||||
* @see template_preprocess_book_all_books_block()
|
||||
*
|
||||
* @ingroup themeable
|
||||
*/
|
||||
?>
|
||||
<?php foreach ($book_menus as $book_id => $menu): ?>
|
||||
<div id="book-block-menu-<?php print $book_id; ?>" class="book-block-menu">
|
||||
<?php print render($menu); ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
52
modules/book/book-export-html.tpl.php
Normal file
52
modules/book/book-export-html.tpl.php
Normal file
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Default theme implementation for printed version of book outline.
|
||||
*
|
||||
* Available variables:
|
||||
* - $title: Top level node title.
|
||||
* - $head: Header tags.
|
||||
* - $language: Language code. e.g. "en" for english.
|
||||
* - $language_rtl: TRUE or FALSE depending on right to left language scripts.
|
||||
* - $base_url: URL to home page.
|
||||
* - $contents: Nodes within the current outline rendered through
|
||||
* book-node-export-html.tpl.php.
|
||||
*
|
||||
* @see template_preprocess_book_export_html()
|
||||
*
|
||||
* @ingroup themeable
|
||||
*/
|
||||
?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" lang="<?php print $language->language; ?>" xml:lang="<?php print $language->language; ?>" dir="<?php print $dir; ?>">
|
||||
<head>
|
||||
<title><?php print $title; ?></title>
|
||||
<?php print $head; ?>
|
||||
<base href="<?php print $base_url; ?>" />
|
||||
<link type="text/css" rel="stylesheet" href="misc/print.css" />
|
||||
<?php if ($language_rtl): ?>
|
||||
<link type="text/css" rel="stylesheet" href="misc/print-rtl.css" />
|
||||
<?php endif; ?>
|
||||
</head>
|
||||
<body>
|
||||
<?php
|
||||
/**
|
||||
* The given node is /embedded to its absolute depth in a top level
|
||||
* section/. For example, a child node with depth 2 in the hierarchy is
|
||||
* contained in (otherwise empty) <div> elements corresponding to
|
||||
* depth 0 and depth 1. This is intended to support WYSIWYG output - e.g.,
|
||||
* level 3 sections always look like level 3 sections, no matter their
|
||||
* depth relative to the node selected to be exported as printer-friendly
|
||||
* HTML.
|
||||
*/
|
||||
$div_close = '';
|
||||
?>
|
||||
<?php for ($i = 1; $i < $depth; $i++): ?>
|
||||
<div class="section-<?php print $i; ?>">
|
||||
<?php $div_close .= '</div>'; ?>
|
||||
<?php endfor; ?>
|
||||
<?php print $contents; ?>
|
||||
<?php print $div_close; ?>
|
||||
</body>
|
||||
</html>
|
54
modules/book/book-navigation.tpl.php
Normal file
54
modules/book/book-navigation.tpl.php
Normal file
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Default theme implementation to navigate books.
|
||||
*
|
||||
* Presented under nodes that are a part of book outlines.
|
||||
*
|
||||
* Available variables:
|
||||
* - $tree: The immediate children of the current node rendered as an unordered
|
||||
* list.
|
||||
* - $current_depth: Depth of the current node within the book outline. Provided
|
||||
* for context.
|
||||
* - $prev_url: URL to the previous node.
|
||||
* - $prev_title: Title of the previous node.
|
||||
* - $parent_url: URL to the parent node.
|
||||
* - $parent_title: Title of the parent node. Not printed by default. Provided
|
||||
* as an option.
|
||||
* - $next_url: URL to the next node.
|
||||
* - $next_title: Title of the next node.
|
||||
* - $has_links: Flags TRUE whenever the previous, parent or next data has a
|
||||
* value.
|
||||
* - $book_id: The book ID of the current outline being viewed. Same as the node
|
||||
* ID containing the entire outline. Provided for context.
|
||||
* - $book_url: The book/node URL of the current outline being viewed. Provided
|
||||
* as an option. Not used by default.
|
||||
* - $book_title: The book/node title of the current outline being viewed.
|
||||
* Provided as an option. Not used by default.
|
||||
*
|
||||
* @see template_preprocess_book_navigation()
|
||||
*
|
||||
* @ingroup themeable
|
||||
*/
|
||||
?>
|
||||
<?php if ($tree || $has_links): ?>
|
||||
<div id="book-navigation-<?php print $book_id; ?>" class="book-navigation">
|
||||
<?php print $tree; ?>
|
||||
|
||||
<?php if ($has_links): ?>
|
||||
<div class="page-links clearfix">
|
||||
<?php if ($prev_url): ?>
|
||||
<a href="<?php print $prev_url; ?>" class="page-previous" title="<?php print t('Go to previous page'); ?>"><?php print t('‹ ') . $prev_title; ?></a>
|
||||
<?php endif; ?>
|
||||
<?php if ($parent_url): ?>
|
||||
<a href="<?php print $parent_url; ?>" class="page-up" title="<?php print t('Go to parent page'); ?>"><?php print t('up'); ?></a>
|
||||
<?php endif; ?>
|
||||
<?php if ($next_url): ?>
|
||||
<a href="<?php print $next_url; ?>" class="page-next" title="<?php print t('Go to next page'); ?>"><?php print $next_title . t(' ›'); ?></a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
<?php endif; ?>
|
25
modules/book/book-node-export-html.tpl.php
Normal file
25
modules/book/book-node-export-html.tpl.php
Normal file
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Default theme implementation for a single node in a printer-friendly outline.
|
||||
*
|
||||
* @see book-node-export-html.tpl.php
|
||||
* Where it is collected and printed out.
|
||||
*
|
||||
* Available variables:
|
||||
* - $depth: Depth of the current node inside the outline.
|
||||
* - $title: Node title.
|
||||
* - $content: Node content.
|
||||
* - $children: All the child nodes recursively rendered through this file.
|
||||
*
|
||||
* @see template_preprocess_book_node_export_html()
|
||||
*
|
||||
* @ingroup themeable
|
||||
*/
|
||||
?>
|
||||
<div id="node-<?php print $node->nid; ?>" class="section-<?php print $depth; ?>">
|
||||
<h1 class="book-heading"><?php print $title; ?></h1>
|
||||
<?php print $content; ?>
|
||||
<?php print $children; ?>
|
||||
</div>
|
15
modules/book/book-rtl.css
Normal file
15
modules/book/book-rtl.css
Normal file
|
@ -0,0 +1,15 @@
|
|||
/**
|
||||
* @file
|
||||
* Right-to-Left styling for the Book module.
|
||||
*/
|
||||
|
||||
.book-navigation .menu {
|
||||
padding: 1em 3em 0 0;
|
||||
}
|
||||
|
||||
.book-navigation .page-previous {
|
||||
float: right;
|
||||
}
|
||||
.book-navigation .page-up {
|
||||
float: right;
|
||||
}
|
289
modules/book/book.admin.inc
Normal file
289
modules/book/book.admin.inc
Normal file
|
@ -0,0 +1,289 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Administration page callbacks for the Book module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns an administrative overview of all books.
|
||||
*
|
||||
* @return string
|
||||
* A HTML-formatted string with the administrative page content.
|
||||
*
|
||||
* @see book_menu()
|
||||
*/
|
||||
function book_admin_overview() {
|
||||
$rows = array();
|
||||
|
||||
$headers = array(t('Book'), t('Operations'));
|
||||
|
||||
// Add any recognized books to the table list.
|
||||
foreach (book_get_books() as $book) {
|
||||
$rows[] = array(l($book['title'], $book['href'], $book['options']), l(t('edit order and titles'), 'admin/content/book/' . $book['nid']));
|
||||
}
|
||||
|
||||
return theme('table', array('header' => $headers, 'rows' => $rows, 'empty' => t('No books available.')));
|
||||
}
|
||||
|
||||
/**
|
||||
* Form constructor for the book settings form.
|
||||
*
|
||||
* @see book_admin_settings_validate()
|
||||
*
|
||||
* @ingroup forms
|
||||
*/
|
||||
function book_admin_settings() {
|
||||
$types = node_type_get_names();
|
||||
$form['book_allowed_types'] = array(
|
||||
'#type' => 'checkboxes',
|
||||
'#title' => t('Content types allowed in book outlines'),
|
||||
'#default_value' => variable_get('book_allowed_types', array('book')),
|
||||
'#options' => $types,
|
||||
'#description' => t('Users with the %outline-perm permission can add all content types.', array('%outline-perm' => t('Administer book outlines'))),
|
||||
'#required' => TRUE,
|
||||
);
|
||||
$form['book_child_type'] = array(
|
||||
'#type' => 'radios',
|
||||
'#title' => t('Content type for child pages'),
|
||||
'#default_value' => variable_get('book_child_type', 'book'),
|
||||
'#options' => $types,
|
||||
'#required' => TRUE,
|
||||
);
|
||||
$form['array_filter'] = array('#type' => 'value', '#value' => TRUE);
|
||||
$form['#validate'][] = 'book_admin_settings_validate';
|
||||
|
||||
return system_settings_form($form);
|
||||
}
|
||||
|
||||
/**
|
||||
* Form validation handler for book_admin_settings().
|
||||
*
|
||||
* @see book_admin_settings_submit()
|
||||
*/
|
||||
function book_admin_settings_validate($form, &$form_state) {
|
||||
$child_type = $form_state['values']['book_child_type'];
|
||||
if (empty($form_state['values']['book_allowed_types'][$child_type])) {
|
||||
form_set_error('book_child_type', t('The content type for the %add-child link must be one of those selected as an allowed book outline type.', array('%add-child' => t('Add child page'))));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Form constructor for administering a single book's hierarchy.
|
||||
*
|
||||
* @see book_admin_edit_submit()
|
||||
*
|
||||
* @param $node
|
||||
* The node of the top-level page in the book.
|
||||
*
|
||||
* @see book_admin_edit_validate()
|
||||
* @see book_admin_edit_submit()
|
||||
* @ingroup forms
|
||||
*/
|
||||
function book_admin_edit($form, $form_state, $node) {
|
||||
drupal_set_title($node->title);
|
||||
$form['#node'] = $node;
|
||||
_book_admin_table($node, $form);
|
||||
$form['save'] = array(
|
||||
'#type' => 'submit',
|
||||
'#value' => t('Save book pages'),
|
||||
);
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Form validation handler for book_admin_edit().
|
||||
*
|
||||
* Checks that the book has not been changed while using the form.
|
||||
*
|
||||
* @see book_admin_edit_submit()
|
||||
*/
|
||||
function book_admin_edit_validate($form, &$form_state) {
|
||||
if ($form_state['values']['tree_hash'] != $form_state['values']['tree_current_hash']) {
|
||||
form_set_error('', t('This book has been modified by another user, the changes could not be saved.'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Form submission handler for book_admin_edit().
|
||||
*
|
||||
* This function takes care to save parent menu items before their children.
|
||||
* Saving menu items in the incorrect order can break the menu tree.
|
||||
*
|
||||
* @see book_admin_edit_validate()
|
||||
* @see menu_overview_form_submit()
|
||||
*/
|
||||
function book_admin_edit_submit($form, &$form_state) {
|
||||
// Save elements in the same order as defined in post rather than the form.
|
||||
// This ensures parents are updated before their children, preventing orphans.
|
||||
$order = array_flip(array_keys($form_state['input']['table']));
|
||||
$form['table'] = array_merge($order, $form['table']);
|
||||
|
||||
foreach (element_children($form['table']) as $key) {
|
||||
if ($form['table'][$key]['#item']) {
|
||||
$row = $form['table'][$key];
|
||||
$values = $form_state['values']['table'][$key];
|
||||
|
||||
// Update menu item if moved.
|
||||
if ($row['plid']['#default_value'] != $values['plid'] || $row['weight']['#default_value'] != $values['weight']) {
|
||||
$row['#item']['plid'] = $values['plid'];
|
||||
$row['#item']['weight'] = $values['weight'];
|
||||
menu_link_save($row['#item']);
|
||||
}
|
||||
|
||||
// Update the title if changed.
|
||||
if ($row['title']['#default_value'] != $values['title']) {
|
||||
$node = node_load($values['nid']);
|
||||
$langcode = LANGUAGE_NONE;
|
||||
$node->title = $values['title'];
|
||||
$node->book['link_title'] = $values['title'];
|
||||
$node->revision = 1;
|
||||
$node->log = t('Title changed from %original to %current.', array('%original' => $node->title, '%current' => $values['title']));
|
||||
|
||||
node_save($node);
|
||||
watchdog('content', 'book: updated %title.', array('%title' => $node->title), WATCHDOG_NOTICE, l(t('view'), 'node/' . $node->nid));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drupal_set_message(t('Updated book %title.', array('%title' => $form['#node']->title)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the table portion of the form for the book administration page.
|
||||
*
|
||||
* @param $node
|
||||
* The node of the top-level page in the book.
|
||||
* @param $form
|
||||
* The form that is being modified, passed by reference.
|
||||
*
|
||||
* @see book_admin_edit()
|
||||
*/
|
||||
function _book_admin_table($node, &$form) {
|
||||
$form['table'] = array(
|
||||
'#theme' => 'book_admin_table',
|
||||
'#tree' => TRUE,
|
||||
);
|
||||
|
||||
$tree = book_menu_subtree_data($node->book);
|
||||
$tree = array_shift($tree); // Do not include the book item itself.
|
||||
if ($tree['below']) {
|
||||
$hash = drupal_hash_base64(serialize($tree['below']));
|
||||
// Store the hash value as a hidden form element so that we can detect
|
||||
// if another user changed the book hierarchy.
|
||||
$form['tree_hash'] = array(
|
||||
'#type' => 'hidden',
|
||||
'#default_value' => $hash,
|
||||
);
|
||||
$form['tree_current_hash'] = array(
|
||||
'#type' => 'value',
|
||||
'#value' => $hash,
|
||||
);
|
||||
_book_admin_table_tree($tree['below'], $form['table']);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Helps build the main table in the book administration page form.
|
||||
*
|
||||
* @param $tree
|
||||
* A subtree of the book menu hierarchy.
|
||||
* @param $form
|
||||
* The form that is being modified, passed by reference.
|
||||
*
|
||||
* @return
|
||||
* The modified form array.
|
||||
*
|
||||
* @see book_admin_edit()
|
||||
*/
|
||||
function _book_admin_table_tree($tree, &$form) {
|
||||
// The delta must be big enough to give each node a distinct value.
|
||||
$count = count($tree);
|
||||
$delta = ($count < 30) ? 15 : intval($count / 2) + 1;
|
||||
|
||||
foreach ($tree as $data) {
|
||||
$form['book-admin-' . $data['link']['nid']] = array(
|
||||
'#item' => $data['link'],
|
||||
'nid' => array('#type' => 'value', '#value' => $data['link']['nid']),
|
||||
'depth' => array('#type' => 'value', '#value' => $data['link']['depth']),
|
||||
'href' => array('#type' => 'value', '#value' => $data['link']['href']),
|
||||
'title' => array(
|
||||
'#type' => 'textfield',
|
||||
'#default_value' => $data['link']['link_title'],
|
||||
'#maxlength' => 255,
|
||||
'#size' => 40,
|
||||
),
|
||||
'weight' => array(
|
||||
'#type' => 'weight',
|
||||
'#default_value' => $data['link']['weight'],
|
||||
'#delta' => max($delta, abs($data['link']['weight'])),
|
||||
'#title' => t('Weight for @title', array('@title' => $data['link']['title'])),
|
||||
'#title_display' => 'invisible',
|
||||
),
|
||||
'plid' => array(
|
||||
'#type' => 'hidden',
|
||||
'#default_value' => $data['link']['plid'],
|
||||
),
|
||||
'mlid' => array(
|
||||
'#type' => 'hidden',
|
||||
'#default_value' => $data['link']['mlid'],
|
||||
),
|
||||
);
|
||||
if ($data['below']) {
|
||||
_book_admin_table_tree($data['below'], $form);
|
||||
}
|
||||
}
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns HTML for a book administration form.
|
||||
*
|
||||
* @param $variables
|
||||
* An associative array containing:
|
||||
* - form: A render element representing the form.
|
||||
*
|
||||
* @see book_admin_table()
|
||||
* @ingroup themeable
|
||||
*/
|
||||
function theme_book_admin_table($variables) {
|
||||
$form = $variables['form'];
|
||||
|
||||
drupal_add_tabledrag('book-outline', 'match', 'parent', 'book-plid', 'book-plid', 'book-mlid', TRUE, MENU_MAX_DEPTH - 2);
|
||||
drupal_add_tabledrag('book-outline', 'order', 'sibling', 'book-weight');
|
||||
|
||||
$header = array(t('Title'), t('Weight'), t('Parent'), array('data' => t('Operations'), 'colspan' => '3'));
|
||||
|
||||
$rows = array();
|
||||
$destination = drupal_get_destination();
|
||||
$access = user_access('administer nodes');
|
||||
foreach (element_children($form) as $key) {
|
||||
$nid = $form[$key]['nid']['#value'];
|
||||
$href = $form[$key]['href']['#value'];
|
||||
|
||||
// Add special classes to be used with tabledrag.js.
|
||||
$form[$key]['plid']['#attributes']['class'] = array('book-plid');
|
||||
$form[$key]['mlid']['#attributes']['class'] = array('book-mlid');
|
||||
$form[$key]['weight']['#attributes']['class'] = array('book-weight');
|
||||
|
||||
$data = array(
|
||||
theme('indentation', array('size' => $form[$key]['depth']['#value'] - 2)) . drupal_render($form[$key]['title']),
|
||||
drupal_render($form[$key]['weight']),
|
||||
drupal_render($form[$key]['plid']) . drupal_render($form[$key]['mlid']),
|
||||
l(t('view'), $href),
|
||||
$access ? l(t('edit'), 'node/' . $nid . '/edit', array('query' => $destination)) : ' ',
|
||||
$access ? l(t('delete'), 'node/' . $nid . '/delete', array('query' => $destination) ) : ' ',
|
||||
);
|
||||
$row = array('data' => $data);
|
||||
if (isset($form[$key]['#attributes'])) {
|
||||
$row = array_merge($row, $form[$key]['#attributes']);
|
||||
}
|
||||
$row['class'][] = 'draggable';
|
||||
$rows[] = $row;
|
||||
}
|
||||
|
||||
return theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array('id' => 'book-outline'), 'empty' => t('No book content available.')));
|
||||
}
|
58
modules/book/book.css
Normal file
58
modules/book/book.css
Normal file
|
@ -0,0 +1,58 @@
|
|||
/**
|
||||
* @file
|
||||
* Styling for the Book module.
|
||||
*/
|
||||
|
||||
.book-navigation .menu {
|
||||
border-top: 1px solid #888;
|
||||
padding: 1em 0 0 3em; /* LTR */
|
||||
}
|
||||
.book-navigation .page-links {
|
||||
border-top: 1px solid #888;
|
||||
border-bottom: 1px solid #888;
|
||||
text-align: center;
|
||||
padding: 0.5em;
|
||||
}
|
||||
.book-navigation .page-previous {
|
||||
text-align: left;
|
||||
width: 42%;
|
||||
display: block;
|
||||
float: left; /* LTR */
|
||||
}
|
||||
.book-navigation .page-up {
|
||||
margin: 0 5%;
|
||||
width: 4%;
|
||||
display: block;
|
||||
float: left; /* LTR */
|
||||
}
|
||||
.book-navigation .page-next {
|
||||
text-align: right;
|
||||
width: 42%;
|
||||
display: block;
|
||||
float: right;
|
||||
}
|
||||
#book-outline {
|
||||
min-width: 56em;
|
||||
}
|
||||
.book-outline-form .form-item {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
html.js #edit-book-pick-book {
|
||||
display: none;
|
||||
}
|
||||
.form-item-book-bid .description {
|
||||
clear: both;
|
||||
}
|
||||
#book-admin-edit select {
|
||||
margin-right: 24px;
|
||||
}
|
||||
#book-admin-edit select.progress-disabled {
|
||||
margin-right: 0;
|
||||
}
|
||||
#book-admin-edit tr.ajax-new-content {
|
||||
background-color: #ffd;
|
||||
}
|
||||
#book-admin-edit .form-item {
|
||||
float: left;
|
||||
}
|
14
modules/book/book.info
Normal file
14
modules/book/book.info
Normal file
|
@ -0,0 +1,14 @@
|
|||
name = Book
|
||||
description = Allows users to create and organize related content in an outline.
|
||||
package = Core
|
||||
version = VERSION
|
||||
core = 7.x
|
||||
files[] = book.test
|
||||
configure = admin/content/book/settings
|
||||
stylesheets[all][] = book.css
|
||||
|
||||
; Information added by Drupal.org packaging script on 2017-06-21
|
||||
version = "7.56"
|
||||
project = "drupal"
|
||||
datestamp = "1498069849"
|
||||
|
95
modules/book/book.install
Normal file
95
modules/book/book.install
Normal file
|
@ -0,0 +1,95 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Install, update and uninstall functions for the book module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Implements hook_install().
|
||||
*/
|
||||
function book_install() {
|
||||
// Add the node type.
|
||||
_book_install_type_create();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_uninstall().
|
||||
*/
|
||||
function book_uninstall() {
|
||||
variable_del('book_allowed_types');
|
||||
variable_del('book_child_type');
|
||||
variable_del('book_block_mode');
|
||||
|
||||
// Delete menu links.
|
||||
db_delete('menu_links')
|
||||
->condition('module', 'book')
|
||||
->execute();
|
||||
menu_cache_clear_all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the book content type.
|
||||
*/
|
||||
function _book_install_type_create() {
|
||||
// Create an additional node type.
|
||||
$book_node_type = array(
|
||||
'type' => 'book',
|
||||
'name' => t('Book page'),
|
||||
'base' => 'node_content',
|
||||
'description' => t('<em>Books</em> have a built-in hierarchical navigation. Use for handbooks or tutorials.'),
|
||||
'custom' => 1,
|
||||
'modified' => 1,
|
||||
'locked' => 0,
|
||||
);
|
||||
|
||||
$book_node_type = node_type_set_defaults($book_node_type);
|
||||
node_type_save($book_node_type);
|
||||
node_add_body_field($book_node_type);
|
||||
// Default to not promoted.
|
||||
variable_set('node_options_book', array('status'));
|
||||
// Use this default type for adding content to books.
|
||||
variable_set('book_allowed_types', array('book'));
|
||||
variable_set('book_child_type', 'book');
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_schema().
|
||||
*/
|
||||
function book_schema() {
|
||||
$schema['book'] = array(
|
||||
'description' => 'Stores book outline information. Uniquely connects each node in the outline to a link in {menu_links}',
|
||||
'fields' => array(
|
||||
'mlid' => array(
|
||||
'type' => 'int',
|
||||
'unsigned' => TRUE,
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
'description' => "The book page's {menu_links}.mlid.",
|
||||
),
|
||||
'nid' => array(
|
||||
'type' => 'int',
|
||||
'unsigned' => TRUE,
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
'description' => "The book page's {node}.nid.",
|
||||
),
|
||||
'bid' => array(
|
||||
'type' => 'int',
|
||||
'unsigned' => TRUE,
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
'description' => "The book ID is the {book}.nid of the top-level page.",
|
||||
),
|
||||
),
|
||||
'primary key' => array('mlid'),
|
||||
'unique keys' => array(
|
||||
'nid' => array('nid'),
|
||||
),
|
||||
'indexes' => array(
|
||||
'bid' => array('bid'),
|
||||
),
|
||||
);
|
||||
|
||||
return $schema;
|
||||
}
|
27
modules/book/book.js
Normal file
27
modules/book/book.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
/**
|
||||
* @file
|
||||
* Javascript behaviors for the Book module.
|
||||
*/
|
||||
|
||||
(function ($) {
|
||||
|
||||
Drupal.behaviors.bookFieldsetSummaries = {
|
||||
attach: function (context) {
|
||||
$('fieldset.book-outline-form', context).drupalSetSummary(function (context) {
|
||||
var $select = $('.form-item-book-bid select');
|
||||
var val = $select.val();
|
||||
|
||||
if (val === '0') {
|
||||
return Drupal.t('Not in book');
|
||||
}
|
||||
else if (val === 'new') {
|
||||
return Drupal.t('New book');
|
||||
}
|
||||
else {
|
||||
return Drupal.checkPlain($select.find(':selected').text());
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
})(jQuery);
|
1437
modules/book/book.module
Normal file
1437
modules/book/book.module
Normal file
File diff suppressed because it is too large
Load diff
247
modules/book/book.pages.inc
Normal file
247
modules/book/book.pages.inc
Normal file
|
@ -0,0 +1,247 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* User page callbacks for the book module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Menu callback: Prints a listing of all books.
|
||||
*
|
||||
* @return string
|
||||
* A HTML-formatted string with the listing of all books content.
|
||||
*
|
||||
* @see book_menu()
|
||||
*/
|
||||
function book_render() {
|
||||
$book_list = array();
|
||||
foreach (book_get_books() as $book) {
|
||||
$book_list[] = l($book['title'], $book['href'], $book['options']);
|
||||
}
|
||||
|
||||
return theme('item_list', array('items' => $book_list));
|
||||
}
|
||||
|
||||
/**
|
||||
* Menu callback; Generates representations of a book page and its children.
|
||||
*
|
||||
* The function delegates the generation of output to helper functions. The
|
||||
* function name is derived by prepending 'book_export_' to the given output
|
||||
* type. So, e.g., a type of 'html' results in a call to the function
|
||||
* book_export_html().
|
||||
*
|
||||
* @param $type
|
||||
* A string encoding the type of output requested. The following types are
|
||||
* currently supported in book module:
|
||||
* - html: Printer-friendly HTML.
|
||||
* Other types may be supported in contributed modules.
|
||||
* @param $nid
|
||||
* An integer representing the node id (nid) of the node to export
|
||||
*
|
||||
* @return
|
||||
* A string representing the node and its children in the book hierarchy in a
|
||||
* format determined by the $type parameter.
|
||||
*
|
||||
* @see book_menu()
|
||||
*/
|
||||
function book_export($type, $nid) {
|
||||
// Check that the node exists and that the current user has access to it.
|
||||
$node = node_load($nid);
|
||||
if (!$node) {
|
||||
return MENU_NOT_FOUND;
|
||||
}
|
||||
if (!node_access('view', $node)) {
|
||||
return MENU_ACCESS_DENIED;
|
||||
}
|
||||
|
||||
$type = drupal_strtolower($type);
|
||||
|
||||
$export_function = 'book_export_' . $type;
|
||||
|
||||
if (function_exists($export_function)) {
|
||||
print call_user_func($export_function, $nid);
|
||||
}
|
||||
else {
|
||||
drupal_set_message(t('Unknown export format.'));
|
||||
drupal_not_found();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates HTML for export when invoked by book_export().
|
||||
*
|
||||
* The given node is embedded to its absolute depth in a top level section. For
|
||||
* example, a child node with depth 2 in the hierarchy is contained in
|
||||
* (otherwise empty) <div> elements corresponding to depth 0 and depth 1.
|
||||
* This is intended to support WYSIWYG output - e.g., level 3 sections always
|
||||
* look like level 3 sections, no matter their depth relative to the node
|
||||
* selected to be exported as printer-friendly HTML.
|
||||
*
|
||||
* @param $nid
|
||||
* An integer representing the node id (nid) of the node to export.
|
||||
*
|
||||
* @return
|
||||
* A string containing HTML representing the node and its children in
|
||||
* the book hierarchy.
|
||||
*/
|
||||
function book_export_html($nid) {
|
||||
if (user_access('access printer-friendly version')) {
|
||||
$node = node_load($nid);
|
||||
if (isset($node->book)) {
|
||||
$tree = book_menu_subtree_data($node->book);
|
||||
$contents = book_export_traverse($tree, 'book_node_export');
|
||||
return theme('book_export_html', array('title' => $node->title, 'contents' => $contents, 'depth' => $node->book['depth']));
|
||||
}
|
||||
else {
|
||||
drupal_not_found();
|
||||
}
|
||||
}
|
||||
else {
|
||||
drupal_access_denied();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Menu callback: Shows the outline form for a single node.
|
||||
*
|
||||
* @param $node
|
||||
* The book node for which to show the outline.
|
||||
*
|
||||
* @return string
|
||||
* A HTML-formatted string with the outline form for a single node.
|
||||
*
|
||||
* @see book_menu()
|
||||
*/
|
||||
function book_outline($node) {
|
||||
drupal_set_title($node->title);
|
||||
return drupal_get_form('book_outline_form', $node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Form constructor for the book outline form.
|
||||
*
|
||||
* Allows handling of all book outline operations via the outline tab.
|
||||
*
|
||||
* @param $node
|
||||
* The book node for which to show the outline.
|
||||
*
|
||||
* @see book_outline_form_submit()
|
||||
* @see book_remove_button_submit()
|
||||
* @ingroup forms
|
||||
*/
|
||||
function book_outline_form($form, &$form_state, $node) {
|
||||
if (!isset($node->book)) {
|
||||
// The node is not part of any book yet - set default options.
|
||||
$node->book = _book_link_defaults($node->nid);
|
||||
}
|
||||
else {
|
||||
$node->book['original_bid'] = $node->book['bid'];
|
||||
}
|
||||
|
||||
// Find the depth limit for the parent select.
|
||||
if (!isset($node->book['parent_depth_limit'])) {
|
||||
$node->book['parent_depth_limit'] = _book_parent_depth_limit($node->book);
|
||||
}
|
||||
$form['#node'] = $node;
|
||||
$form['#id'] = 'book-outline';
|
||||
_book_add_form_elements($form, $form_state, $node);
|
||||
|
||||
$form['book']['#collapsible'] = FALSE;
|
||||
|
||||
$form['update'] = array(
|
||||
'#type' => 'submit',
|
||||
'#value' => $node->book['original_bid'] ? t('Update book outline') : t('Add to book outline'),
|
||||
'#weight' => 15,
|
||||
);
|
||||
|
||||
$form['remove'] = array(
|
||||
'#type' => 'submit',
|
||||
'#value' => t('Remove from book outline'),
|
||||
'#access' => _book_node_is_removable($node),
|
||||
'#weight' => 20,
|
||||
'#submit' => array('book_remove_button_submit'),
|
||||
);
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Form submission handler for book_outline_form().
|
||||
*
|
||||
* Redirects to removal confirmation form.
|
||||
*
|
||||
* @see book_outline_form_submit()
|
||||
*/
|
||||
function book_remove_button_submit($form, &$form_state) {
|
||||
$form_state['redirect'] = 'node/' . $form['#node']->nid . '/outline/remove';
|
||||
}
|
||||
|
||||
/**
|
||||
* Form submission handler for book_outline_form().
|
||||
*
|
||||
* @see book_remove_button_submit()
|
||||
*/
|
||||
function book_outline_form_submit($form, &$form_state) {
|
||||
$node = $form['#node'];
|
||||
$form_state['redirect'] = "node/" . $node->nid;
|
||||
$book_link = $form_state['values']['book'];
|
||||
if (!$book_link['bid']) {
|
||||
drupal_set_message(t('No changes were made'));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$book_link['menu_name'] = book_menu_name($book_link['bid']);
|
||||
$node->book = $book_link;
|
||||
if (_book_update_outline($node)) {
|
||||
if ($node->book['parent_mismatch']) {
|
||||
// This will usually only happen when JS is disabled.
|
||||
drupal_set_message(t('The post has been added to the selected book. You may now position it relative to other pages.'));
|
||||
$form_state['redirect'] = "node/" . $node->nid . "/outline";
|
||||
}
|
||||
else {
|
||||
drupal_set_message(t('The book outline has been updated.'));
|
||||
}
|
||||
}
|
||||
else {
|
||||
drupal_set_message(t('There was an error adding the post to the book.'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Form constructor to confirm removal of a node from a book.
|
||||
*
|
||||
* @param $node
|
||||
* The node to delete.
|
||||
*
|
||||
* @see book_remove_form_submit()
|
||||
* @ingroup forms
|
||||
*/
|
||||
function book_remove_form($form, &$form_state, $node) {
|
||||
$form['#node'] = $node;
|
||||
$title = array('%title' => $node->title);
|
||||
|
||||
if ($node->book['has_children']) {
|
||||
$description = t('%title has associated child pages, which will be relocated automatically to maintain their connection to the book. To recreate the hierarchy (as it was before removing this page), %title may be added again using the Outline tab, and each of its former child pages will need to be relocated manually.', $title);
|
||||
}
|
||||
else {
|
||||
$description = t('%title may be added to hierarchy again using the Outline tab.', $title);
|
||||
}
|
||||
|
||||
return confirm_form($form, t('Are you sure you want to remove %title from the book hierarchy?', $title), 'node/' . $node->nid, $description, t('Remove'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Form submission handler for book_remove_form().
|
||||
*/
|
||||
function book_remove_form_submit($form, &$form_state) {
|
||||
$node = $form['#node'];
|
||||
if (_book_node_is_removable($node)) {
|
||||
menu_link_delete($node->book['mlid']);
|
||||
db_delete('book')
|
||||
->condition('nid', $node->nid)
|
||||
->execute();
|
||||
drupal_set_message(t('The post has been removed from the book.'));
|
||||
}
|
||||
$form_state['redirect'] = 'node/' . $node->nid;
|
||||
}
|
398
modules/book/book.test
Normal file
398
modules/book/book.test
Normal file
|
@ -0,0 +1,398 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Tests for book.module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Tests the functionality of the Book module.
|
||||
*/
|
||||
class BookTestCase extends DrupalWebTestCase {
|
||||
|
||||
/**
|
||||
* A book node.
|
||||
*
|
||||
* @var object
|
||||
*/
|
||||
protected $book;
|
||||
|
||||
/**
|
||||
* A user with permission to create and edit books.
|
||||
*
|
||||
* @var object
|
||||
*/
|
||||
protected $book_author;
|
||||
|
||||
/**
|
||||
* A user with permission to view a book and access printer-friendly version.
|
||||
*
|
||||
* @var object
|
||||
*/
|
||||
protected $web_user;
|
||||
|
||||
/**
|
||||
* A user with permission to create and edit books and to administer blocks.
|
||||
*
|
||||
* @var object
|
||||
*/
|
||||
protected $admin_user;
|
||||
|
||||
public static function getInfo() {
|
||||
return array(
|
||||
'name' => 'Book functionality',
|
||||
'description' => 'Create a book, add pages, and test book interface.',
|
||||
'group' => 'Book',
|
||||
);
|
||||
}
|
||||
|
||||
function setUp() {
|
||||
parent::setUp(array('book', 'node_access_test'));
|
||||
|
||||
// node_access_test requires a node_access_rebuild().
|
||||
node_access_rebuild();
|
||||
|
||||
// Create users.
|
||||
$this->book_author = $this->drupalCreateUser(array('create new books', 'create book content', 'edit own book content', 'add content to books'));
|
||||
$this->web_user = $this->drupalCreateUser(array('access printer-friendly version', 'node test view'));
|
||||
$this->admin_user = $this->drupalCreateUser(array('create new books', 'create book content', 'edit own book content', 'add content to books', 'administer blocks', 'administer permissions', 'administer book outlines', 'node test view'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new book with a page hierarchy.
|
||||
*/
|
||||
function createBook() {
|
||||
// Create new book.
|
||||
$this->drupalLogin($this->book_author);
|
||||
|
||||
$this->book = $this->createBookNode('new');
|
||||
$book = $this->book;
|
||||
|
||||
/*
|
||||
* Add page hierarchy to book.
|
||||
* Book
|
||||
* |- Node 0
|
||||
* |- Node 1
|
||||
* |- Node 2
|
||||
* |- Node 3
|
||||
* |- Node 4
|
||||
*/
|
||||
$nodes = array();
|
||||
$nodes[] = $this->createBookNode($book->nid); // Node 0.
|
||||
$nodes[] = $this->createBookNode($book->nid, $nodes[0]->book['mlid']); // Node 1.
|
||||
$nodes[] = $this->createBookNode($book->nid, $nodes[0]->book['mlid']); // Node 2.
|
||||
$nodes[] = $this->createBookNode($book->nid); // Node 3.
|
||||
$nodes[] = $this->createBookNode($book->nid); // Node 4.
|
||||
|
||||
$this->drupalLogout();
|
||||
|
||||
return $nodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests book functionality through node interfaces.
|
||||
*/
|
||||
function testBook() {
|
||||
// Create new book.
|
||||
$nodes = $this->createBook();
|
||||
$book = $this->book;
|
||||
|
||||
$this->drupalLogin($this->web_user);
|
||||
|
||||
// Check that book pages display along with the correct outlines and
|
||||
// previous/next links.
|
||||
$this->checkBookNode($book, array($nodes[0], $nodes[3], $nodes[4]), FALSE, FALSE, $nodes[0], array());
|
||||
$this->checkBookNode($nodes[0], array($nodes[1], $nodes[2]), $book, $book, $nodes[1], array($book));
|
||||
$this->checkBookNode($nodes[1], NULL, $nodes[0], $nodes[0], $nodes[2], array($book, $nodes[0]));
|
||||
$this->checkBookNode($nodes[2], NULL, $nodes[1], $nodes[0], $nodes[3], array($book, $nodes[0]));
|
||||
$this->checkBookNode($nodes[3], NULL, $nodes[2], $book, $nodes[4], array($book));
|
||||
$this->checkBookNode($nodes[4], NULL, $nodes[3], $book, FALSE, array($book));
|
||||
|
||||
$this->drupalLogout();
|
||||
|
||||
// Create a second book, and move an existing book page into it.
|
||||
$this->drupalLogin($this->book_author);
|
||||
$other_book = $this->createBookNode('new');
|
||||
$node = $this->createBookNode($book->nid);
|
||||
$edit = array('book[bid]' => $other_book->nid);
|
||||
$this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save'));
|
||||
|
||||
$this->drupalLogout();
|
||||
$this->drupalLogin($this->web_user);
|
||||
|
||||
// Check that the nodes in the second book are displayed correctly.
|
||||
// First we must set $this->book to the second book, so that the
|
||||
// correct regex will be generated for testing the outline.
|
||||
$this->book = $other_book;
|
||||
$this->checkBookNode($other_book, array($node), FALSE, FALSE, $node, array());
|
||||
$this->checkBookNode($node, NULL, $other_book, $other_book, FALSE, array($other_book));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the outline of sub-pages; previous, up, and next.
|
||||
*
|
||||
* Also checks the printer friendly version of the outline.
|
||||
*
|
||||
* @param $node
|
||||
* Node to check.
|
||||
* @param $nodes
|
||||
* Nodes that should be in outline.
|
||||
* @param $previous
|
||||
* (optional) Previous link node. Defaults to FALSE.
|
||||
* @param $up
|
||||
* (optional) Up link node. Defaults to FALSE.
|
||||
* @param $next
|
||||
* (optional) Next link node. Defaults to FALSE.
|
||||
* @param $breadcrumb
|
||||
* The nodes that should be displayed in the breadcrumb.
|
||||
*/
|
||||
function checkBookNode($node, $nodes, $previous = FALSE, $up = FALSE, $next = FALSE, array $breadcrumb) {
|
||||
// $number does not use drupal_static as it should not be reset
|
||||
// since it uniquely identifies each call to checkBookNode().
|
||||
static $number = 0;
|
||||
$this->drupalGet('node/' . $node->nid);
|
||||
|
||||
// Check outline structure.
|
||||
if ($nodes !== NULL) {
|
||||
$this->assertPattern($this->generateOutlinePattern($nodes), format_string('Node %number outline confirmed.', array('%number' => $number)));
|
||||
}
|
||||
else {
|
||||
$this->pass(format_string('Node %number does not have outline.', array('%number' => $number)));
|
||||
}
|
||||
|
||||
// Check previous, up, and next links.
|
||||
if ($previous) {
|
||||
$this->assertRaw(l('‹ ' . $previous->title, 'node/' . $previous->nid, array('attributes' => array('class' => array('page-previous'), 'title' => t('Go to previous page')))), 'Previous page link found.');
|
||||
}
|
||||
|
||||
if ($up) {
|
||||
$this->assertRaw(l('up', 'node/' . $up->nid, array('attributes' => array('class' => array('page-up'), 'title' => t('Go to parent page')))), 'Up page link found.');
|
||||
}
|
||||
|
||||
if ($next) {
|
||||
$this->assertRaw(l($next->title . ' ›', 'node/' . $next->nid, array('attributes' => array('class' => array('page-next'), 'title' => t('Go to next page')))), 'Next page link found.');
|
||||
}
|
||||
|
||||
// Compute the expected breadcrumb.
|
||||
$expected_breadcrumb = array();
|
||||
$expected_breadcrumb[] = url('');
|
||||
foreach ($breadcrumb as $a_node) {
|
||||
$expected_breadcrumb[] = url('node/' . $a_node->nid);
|
||||
}
|
||||
|
||||
// Fetch links in the current breadcrumb.
|
||||
$links = $this->xpath('//div[@class="breadcrumb"]/a');
|
||||
$got_breadcrumb = array();
|
||||
foreach ($links as $link) {
|
||||
$got_breadcrumb[] = (string) $link['href'];
|
||||
}
|
||||
|
||||
// Compare expected and got breadcrumbs.
|
||||
$this->assertIdentical($expected_breadcrumb, $got_breadcrumb, 'The breadcrumb is correctly displayed on the page.');
|
||||
|
||||
// Check printer friendly version.
|
||||
$this->drupalGet('book/export/html/' . $node->nid);
|
||||
$this->assertText($node->title, 'Printer friendly title found.');
|
||||
$this->assertRaw(check_markup($node->body[LANGUAGE_NONE][0]['value'], $node->body[LANGUAGE_NONE][0]['format']), 'Printer friendly body found.');
|
||||
|
||||
$number++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a regular expression to check for the sub-nodes in the outline.
|
||||
*
|
||||
* @param array $nodes
|
||||
* An array of nodes to check in outline.
|
||||
*
|
||||
* @return
|
||||
* A regular expression that locates sub-nodes of the outline.
|
||||
*/
|
||||
function generateOutlinePattern($nodes) {
|
||||
$outline = '';
|
||||
foreach ($nodes as $node) {
|
||||
$outline .= '(node\/' . $node->nid . ')(.*?)(' . $node->title . ')(.*?)';
|
||||
}
|
||||
|
||||
return '/<div id="book-navigation-' . $this->book->nid . '"(.*?)<ul(.*?)' . $outline . '<\/ul>/s';
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a book node.
|
||||
*
|
||||
* @param $book_nid
|
||||
* A book node ID or set to 'new' to create a new book.
|
||||
* @param $parent
|
||||
* (optional) Parent book reference ID. Defaults to NULL.
|
||||
*/
|
||||
function createBookNode($book_nid, $parent = NULL) {
|
||||
// $number does not use drupal_static as it should not be reset
|
||||
// since it uniquely identifies each call to createBookNode().
|
||||
static $number = 0; // Used to ensure that when sorted nodes stay in same order.
|
||||
|
||||
$edit = array();
|
||||
$langcode = LANGUAGE_NONE;
|
||||
$edit["title"] = $number . ' - SimpleTest test node ' . $this->randomName(10);
|
||||
$edit["body[$langcode][0][value]"] = 'SimpleTest test body ' . $this->randomName(32) . ' ' . $this->randomName(32);
|
||||
$edit['book[bid]'] = $book_nid;
|
||||
|
||||
if ($parent !== NULL) {
|
||||
$this->drupalPost('node/add/book', $edit, t('Change book (update list of parents)'));
|
||||
|
||||
$edit['book[plid]'] = $parent;
|
||||
$this->drupalPost(NULL, $edit, t('Save'));
|
||||
}
|
||||
else {
|
||||
$this->drupalPost('node/add/book', $edit, t('Save'));
|
||||
}
|
||||
|
||||
// Check to make sure the book node was created.
|
||||
$node = $this->drupalGetNodeByTitle($edit['title']);
|
||||
$this->assertNotNull(($node === FALSE ? NULL : $node), 'Book node found in database.');
|
||||
$number++;
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests book export ("printer-friendly version") functionality.
|
||||
*/
|
||||
function testBookExport() {
|
||||
// Create a book.
|
||||
$nodes = $this->createBook();
|
||||
|
||||
// Login as web user and view printer-friendly version.
|
||||
$this->drupalLogin($this->web_user);
|
||||
$this->drupalGet('node/' . $this->book->nid);
|
||||
$this->clickLink(t('Printer-friendly version'));
|
||||
|
||||
// Make sure each part of the book is there.
|
||||
foreach ($nodes as $node) {
|
||||
$this->assertText($node->title, 'Node title found in printer friendly version.');
|
||||
$this->assertRaw(check_markup($node->body[LANGUAGE_NONE][0]['value'], $node->body[LANGUAGE_NONE][0]['format']), 'Node body found in printer friendly version.');
|
||||
}
|
||||
|
||||
// Make sure we can't export an unsupported format.
|
||||
$this->drupalGet('book/export/foobar/' . $this->book->nid);
|
||||
$this->assertResponse('404', 'Unsupported export format returned "not found".');
|
||||
|
||||
// Make sure we get a 404 on a not existing book node.
|
||||
$this->drupalGet('book/export/html/123');
|
||||
$this->assertResponse('404', 'Not existing book node returned "not found".');
|
||||
|
||||
// Make sure an anonymous user cannot view printer-friendly version.
|
||||
$this->drupalLogout();
|
||||
|
||||
// Load the book and verify there is no printer-friendly version link.
|
||||
$this->drupalGet('node/' . $this->book->nid);
|
||||
$this->assertNoLink(t('Printer-friendly version'), 'Anonymous user is not shown link to printer-friendly version.');
|
||||
|
||||
// Try getting the URL directly, and verify it fails.
|
||||
$this->drupalGet('book/export/html/' . $this->book->nid);
|
||||
$this->assertResponse('403', 'Anonymous user properly forbidden.');
|
||||
|
||||
// Now grant anonymous users permission to view the printer-friendly
|
||||
// version and verify that node access restrictions still prevent them from
|
||||
// seeing it.
|
||||
user_role_grant_permissions(DRUPAL_ANONYMOUS_RID, array('access printer-friendly version'));
|
||||
$this->drupalGet('book/export/html/' . $this->book->nid);
|
||||
$this->assertResponse('403', 'Anonymous user properly forbidden from seeing the printer-friendly version when denied by node access.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the functionality of the book navigation block.
|
||||
*/
|
||||
function testBookNavigationBlock() {
|
||||
$this->drupalLogin($this->admin_user);
|
||||
|
||||
// Set block title to confirm that the interface is available.
|
||||
$block_title = $this->randomName(16);
|
||||
$this->drupalPost('admin/structure/block/manage/book/navigation/configure', array('title' => $block_title), t('Save block'));
|
||||
$this->assertText(t('The block configuration has been saved.'), 'Block configuration set.');
|
||||
|
||||
// Set the block to a region to confirm block is available.
|
||||
$edit = array();
|
||||
$edit['blocks[book_navigation][region]'] = 'footer';
|
||||
$this->drupalPost('admin/structure/block', $edit, t('Save blocks'));
|
||||
$this->assertText(t('The block settings have been updated.'), 'Block successfully move to footer region.');
|
||||
|
||||
// Give anonymous users the permission 'node test view'.
|
||||
$edit = array();
|
||||
$edit[DRUPAL_ANONYMOUS_RID . '[node test view]'] = TRUE;
|
||||
$this->drupalPost('admin/people/permissions/' . DRUPAL_ANONYMOUS_RID, $edit, t('Save permissions'));
|
||||
$this->assertText(t('The changes have been saved.'), "Permission 'node test view' successfully assigned to anonymous users.");
|
||||
|
||||
// Test correct display of the block.
|
||||
$nodes = $this->createBook();
|
||||
$this->drupalGet('<front>');
|
||||
$this->assertText($block_title, 'Book navigation block is displayed.');
|
||||
$this->assertText($this->book->title, format_string('Link to book root (@title) is displayed.', array('@title' => $nodes[0]->title)));
|
||||
$this->assertNoText($nodes[0]->title, 'No links to individual book pages are displayed.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the book navigation block when an access module is enabled.
|
||||
*/
|
||||
function testNavigationBlockOnAccessModuleEnabled() {
|
||||
$this->drupalLogin($this->admin_user);
|
||||
$edit = array();
|
||||
|
||||
// Set the block title.
|
||||
$block_title = $this->randomName(16);
|
||||
$edit['title'] = $block_title;
|
||||
|
||||
// Set block display to 'Show block only on book pages'.
|
||||
$edit['book_block_mode'] = 'book pages';
|
||||
$this->drupalPost('admin/structure/block/manage/book/navigation/configure', $edit, t('Save block'));
|
||||
$this->assertText(t('The block configuration has been saved.'), 'Block configuration set.');
|
||||
|
||||
// Set the block to a region to confirm block is available.
|
||||
$edit = array();
|
||||
$edit['blocks[book_navigation][region]'] = 'footer';
|
||||
$this->drupalPost('admin/structure/block', $edit, t('Save blocks'));
|
||||
$this->assertText(t('The block settings have been updated.'), 'Block successfully move to footer region.');
|
||||
|
||||
// Give anonymous users the permission 'node test view'.
|
||||
$edit = array();
|
||||
$edit[DRUPAL_ANONYMOUS_RID . '[node test view]'] = TRUE;
|
||||
$this->drupalPost('admin/people/permissions/' . DRUPAL_ANONYMOUS_RID, $edit, t('Save permissions'));
|
||||
$this->assertText(t('The changes have been saved.'), "Permission 'node test view' successfully assigned to anonymous users.");
|
||||
|
||||
// Create a book.
|
||||
$this->createBook();
|
||||
|
||||
// Test correct display of the block to registered users.
|
||||
$this->drupalLogin($this->web_user);
|
||||
$this->drupalGet('node/' . $this->book->nid);
|
||||
$this->assertText($block_title, 'Book navigation block is displayed to registered users.');
|
||||
$this->drupalLogout();
|
||||
|
||||
// Test correct display of the block to anonymous users.
|
||||
$this->drupalGet('node/' . $this->book->nid);
|
||||
$this->assertText($block_title, 'Book navigation block is displayed to anonymous users.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the access for deleting top-level book nodes.
|
||||
*/
|
||||
function testBookDelete() {
|
||||
$nodes = $this->createBook();
|
||||
$this->drupalLogin($this->admin_user);
|
||||
$edit = array();
|
||||
|
||||
// Test access to delete top-level and child book nodes.
|
||||
$this->drupalGet('node/' . $this->book->nid . '/outline/remove');
|
||||
$this->assertResponse('403', 'Deleting top-level book node properly forbidden.');
|
||||
$this->drupalPost('node/' . $nodes[4]->nid . '/outline/remove', $edit, t('Remove'));
|
||||
$node4 = node_load($nodes[4]->nid, NULL, TRUE);
|
||||
$this->assertTrue(empty($node4->book), 'Deleting child book node properly allowed.');
|
||||
|
||||
// Delete all child book nodes and retest top-level node deletion.
|
||||
foreach ($nodes as $node) {
|
||||
$nids[] = $node->nid;
|
||||
}
|
||||
node_delete_multiple($nids);
|
||||
$this->drupalPost('node/' . $this->book->nid . '/outline/remove', $edit, t('Remove'));
|
||||
$node = node_load($this->book->nid, NULL, TRUE);
|
||||
$this->assertTrue(empty($node->book), 'Deleting childless top-level book node properly allowed.');
|
||||
}
|
||||
}
|
48
modules/color/color-rtl.css
Normal file
48
modules/color/color-rtl.css
Normal file
|
@ -0,0 +1,48 @@
|
|||
/**
|
||||
* @file
|
||||
* Right-to-left specific stylesheet for the Color module.
|
||||
*/
|
||||
|
||||
#placeholder {
|
||||
left: 0;
|
||||
right: auto;
|
||||
}
|
||||
|
||||
/* Palette */
|
||||
.color-form .form-item {
|
||||
padding-left: 0;
|
||||
padding-right: 1em;
|
||||
}
|
||||
.color-form label {
|
||||
float: right;
|
||||
clear: right;
|
||||
}
|
||||
.color-form .form-text,
|
||||
.color-form .form-select {
|
||||
float: right;
|
||||
}
|
||||
.color-form .form-text {
|
||||
margin-right: 0;
|
||||
margin-left: 5px;
|
||||
}
|
||||
#palette .hook {
|
||||
float: right;
|
||||
}
|
||||
#palette .down,
|
||||
#palette .up,
|
||||
#palette .both {
|
||||
background: url(images/hook-rtl.png) no-repeat 0 0;
|
||||
}
|
||||
#palette .up {
|
||||
background-position: 0 -27px;
|
||||
}
|
||||
#palette .both {
|
||||
background-position: 0 -54px;
|
||||
}
|
||||
#palette .lock {
|
||||
float: right;
|
||||
right: -10px;
|
||||
}
|
||||
html.js #preview {
|
||||
float: right;
|
||||
}
|
85
modules/color/color.css
Normal file
85
modules/color/color.css
Normal file
|
@ -0,0 +1,85 @@
|
|||
/**
|
||||
* @file
|
||||
* Stylesheet for the administration pages of the Color module.
|
||||
*/
|
||||
|
||||
/* Farbtastic placement */
|
||||
.color-form {
|
||||
max-width: 50em;
|
||||
position: relative;
|
||||
}
|
||||
#placeholder {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0; /* LTR */
|
||||
}
|
||||
|
||||
/* Palette */
|
||||
.color-form .form-item {
|
||||
height: 2em;
|
||||
line-height: 2em;
|
||||
padding-left: 1em; /* LTR */
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
.color-form label {
|
||||
float: left; /* LTR */
|
||||
clear: left; /* LTR */
|
||||
width: 10em;
|
||||
}
|
||||
.color-form .form-text,
|
||||
.color-form .form-select {
|
||||
float: left; /* LTR */
|
||||
}
|
||||
.color-form .form-text {
|
||||
text-align: center;
|
||||
margin-right: 5px; /* LTR */
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#palette .hook {
|
||||
float: left; /* LTR */
|
||||
margin-top: 3px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
#palette .down,
|
||||
#palette .up,
|
||||
#palette .both {
|
||||
background: url(images/hook.png) no-repeat 100% 0; /* LTR */
|
||||
}
|
||||
#palette .up {
|
||||
background-position: 100% -27px; /* LTR */
|
||||
}
|
||||
#palette .both {
|
||||
background-position: 100% -54px; /* LTR */
|
||||
}
|
||||
|
||||
#palette .lock {
|
||||
float: left; /* LTR */
|
||||
position: relative;
|
||||
top: -1.4em;
|
||||
left: -10px; /* LTR */
|
||||
width: 20px;
|
||||
height: 25px;
|
||||
background: url(images/lock.png) no-repeat 50% 2px;
|
||||
cursor: pointer;
|
||||
}
|
||||
#palette .unlocked {
|
||||
background-position: 50% -22px;
|
||||
}
|
||||
#palette .form-item {
|
||||
width: 20em;
|
||||
}
|
||||
#palette .item-selected {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
/* Preview */
|
||||
#preview {
|
||||
display: none;
|
||||
}
|
||||
html.js #preview {
|
||||
display: block;
|
||||
position: relative;
|
||||
float: left; /* LTR */
|
||||
}
|
12
modules/color/color.info
Normal file
12
modules/color/color.info
Normal file
|
@ -0,0 +1,12 @@
|
|||
name = Color
|
||||
description = Allows administrators to change the color scheme of compatible themes.
|
||||
package = Core
|
||||
version = VERSION
|
||||
core = 7.x
|
||||
files[] = color.test
|
||||
|
||||
; Information added by Drupal.org packaging script on 2017-06-21
|
||||
version = "7.56"
|
||||
project = "drupal"
|
||||
datestamp = "1498069849"
|
||||
|
66
modules/color/color.install
Normal file
66
modules/color/color.install
Normal file
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Install, update and uninstall functions for the color module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Implements hook_requirements().
|
||||
*/
|
||||
function color_requirements($phase) {
|
||||
$requirements = array();
|
||||
|
||||
if ($phase == 'runtime') {
|
||||
// Check for the PHP GD library.
|
||||
if (function_exists('imagegd2')) {
|
||||
$info = gd_info();
|
||||
$requirements['color_gd'] = array(
|
||||
'value' => $info['GD Version'],
|
||||
);
|
||||
|
||||
// Check for PNG support.
|
||||
if (function_exists('imagecreatefrompng')) {
|
||||
$requirements['color_gd']['severity'] = REQUIREMENT_OK;
|
||||
}
|
||||
else {
|
||||
$requirements['color_gd']['severity'] = REQUIREMENT_WARNING;
|
||||
$requirements['color_gd']['description'] = t('The GD library for PHP is enabled, but was compiled without PNG support. Check the <a href="@url">PHP image documentation</a> for information on how to correct this.', array('@url' => 'http://www.php.net/manual/ref.image.php'));
|
||||
}
|
||||
}
|
||||
else {
|
||||
$requirements['color_gd'] = array(
|
||||
'value' => t('Not installed'),
|
||||
'severity' => REQUIREMENT_ERROR,
|
||||
'description' => t('The GD library for PHP is missing or outdated. Check the <a href="@url">PHP image documentation</a> for information on how to correct this.', array('@url' => 'http://www.php.net/manual/book.image.php')),
|
||||
);
|
||||
}
|
||||
$requirements['color_gd']['title'] = t('GD library PNG support');
|
||||
}
|
||||
|
||||
return $requirements;
|
||||
}
|
||||
|
||||
/**
|
||||
* @addtogroup updates-7.x-extra
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* Warn site administrator if unsafe CSS color codes are found in the database.
|
||||
*/
|
||||
function color_update_7001() {
|
||||
$theme_palettes = db_query("SELECT name FROM {variable} WHERE name LIKE 'color_%_palette'")->fetchCol();
|
||||
foreach ($theme_palettes as $name) {
|
||||
$palette = variable_get($name, array());
|
||||
foreach ($palette as $key => $color) {
|
||||
if (!preg_match('/^#([a-f0-9]{3}){1,2}$/iD', $color)) {
|
||||
drupal_set_message('Some of the custom CSS color codes specified via the color module are invalid. Please examine the themes which are making use of the color module at the <a href="'. url('admin/appearance/settings') .'">Appearance settings</a> page to verify their CSS color values.', 'warning');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @} End of "addtogroup updates-7.x-extra".
|
||||
*/
|
250
modules/color/color.js
Normal file
250
modules/color/color.js
Normal file
|
@ -0,0 +1,250 @@
|
|||
/**
|
||||
* @file
|
||||
* Attaches the behaviors for the Color module.
|
||||
*/
|
||||
|
||||
(function ($) {
|
||||
|
||||
Drupal.behaviors.color = {
|
||||
attach: function (context, settings) {
|
||||
var i, j, colors, field_name;
|
||||
// This behavior attaches by ID, so is only valid once on a page.
|
||||
var form = $('#system-theme-settings .color-form', context).once('color');
|
||||
if (form.length == 0) {
|
||||
return;
|
||||
}
|
||||
var inputs = [];
|
||||
var hooks = [];
|
||||
var locks = [];
|
||||
var focused = null;
|
||||
|
||||
// Add Farbtastic.
|
||||
$(form).prepend('<div id="placeholder"></div>').addClass('color-processed');
|
||||
var farb = $.farbtastic('#placeholder');
|
||||
|
||||
// Decode reference colors to HSL.
|
||||
var reference = settings.color.reference;
|
||||
for (i in reference) {
|
||||
reference[i] = farb.RGBToHSL(farb.unpack(reference[i]));
|
||||
}
|
||||
|
||||
// Build a preview.
|
||||
var height = [];
|
||||
var width = [];
|
||||
// Loop through all defined gradients.
|
||||
for (i in settings.gradients) {
|
||||
// Add element to display the gradient.
|
||||
$('#preview').once('color').append('<div id="gradient-' + i + '"></div>');
|
||||
var gradient = $('#preview #gradient-' + i);
|
||||
// Add height of current gradient to the list (divided by 10).
|
||||
height.push(parseInt(gradient.css('height'), 10) / 10);
|
||||
// Add width of current gradient to the list (divided by 10).
|
||||
width.push(parseInt(gradient.css('width'), 10) / 10);
|
||||
// Add rows (or columns for horizontal gradients).
|
||||
// Each gradient line should have a height (or width for horizontal
|
||||
// gradients) of 10px (because we divided the height/width by 10 above).
|
||||
for (j = 0; j < (settings.gradients[i]['direction'] == 'vertical' ? height[i] : width[i]); ++j) {
|
||||
gradient.append('<div class="gradient-line"></div>');
|
||||
}
|
||||
}
|
||||
|
||||
// Fix preview background in IE6.
|
||||
if (navigator.appVersion.match(/MSIE [0-6]\./)) {
|
||||
var e = $('#preview #img')[0];
|
||||
var image = e.currentStyle.backgroundImage;
|
||||
e.style.backgroundImage = 'none';
|
||||
e.style.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled=true, sizingMethod=crop, src='" + image.substring(5, image.length - 2) + "')";
|
||||
}
|
||||
|
||||
// Set up colorScheme selector.
|
||||
$('#edit-scheme', form).change(function () {
|
||||
var schemes = settings.color.schemes, colorScheme = this.options[this.selectedIndex].value;
|
||||
if (colorScheme != '' && schemes[colorScheme]) {
|
||||
// Get colors of active scheme.
|
||||
colors = schemes[colorScheme];
|
||||
for (field_name in colors) {
|
||||
callback($('#edit-palette-' + field_name), colors[field_name], false, true);
|
||||
}
|
||||
preview();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Renders the preview.
|
||||
*/
|
||||
function preview() {
|
||||
Drupal.color.callback(context, settings, form, farb, height, width);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shifts a given color, using a reference pair (ref in HSL).
|
||||
*
|
||||
* This algorithm ensures relative ordering on the saturation and luminance
|
||||
* axes is preserved, and performs a simple hue shift.
|
||||
*
|
||||
* It is also symmetrical. If: shift_color(c, a, b) == d, then
|
||||
* shift_color(d, b, a) == c.
|
||||
*/
|
||||
function shift_color(given, ref1, ref2) {
|
||||
// Convert to HSL.
|
||||
given = farb.RGBToHSL(farb.unpack(given));
|
||||
|
||||
// Hue: apply delta.
|
||||
given[0] += ref2[0] - ref1[0];
|
||||
|
||||
// Saturation: interpolate.
|
||||
if (ref1[1] == 0 || ref2[1] == 0) {
|
||||
given[1] = ref2[1];
|
||||
}
|
||||
else {
|
||||
var d = ref1[1] / ref2[1];
|
||||
if (d > 1) {
|
||||
given[1] /= d;
|
||||
}
|
||||
else {
|
||||
given[1] = 1 - (1 - given[1]) * d;
|
||||
}
|
||||
}
|
||||
|
||||
// Luminance: interpolate.
|
||||
if (ref1[2] == 0 || ref2[2] == 0) {
|
||||
given[2] = ref2[2];
|
||||
}
|
||||
else {
|
||||
var d = ref1[2] / ref2[2];
|
||||
if (d > 1) {
|
||||
given[2] /= d;
|
||||
}
|
||||
else {
|
||||
given[2] = 1 - (1 - given[2]) * d;
|
||||
}
|
||||
}
|
||||
|
||||
return farb.pack(farb.HSLToRGB(given));
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for Farbtastic when a new color is chosen.
|
||||
*/
|
||||
function callback(input, color, propagate, colorScheme) {
|
||||
var matched;
|
||||
// Set background/foreground colors.
|
||||
$(input).css({
|
||||
backgroundColor: color,
|
||||
'color': farb.RGBToHSL(farb.unpack(color))[2] > 0.5 ? '#000' : '#fff'
|
||||
});
|
||||
|
||||
// Change input value.
|
||||
if ($(input).val() && $(input).val() != color) {
|
||||
$(input).val(color);
|
||||
|
||||
// Update locked values.
|
||||
if (propagate) {
|
||||
i = input.i;
|
||||
for (j = i + 1; ; ++j) {
|
||||
if (!locks[j - 1] || $(locks[j - 1]).is('.unlocked')) break;
|
||||
matched = shift_color(color, reference[input.key], reference[inputs[j].key]);
|
||||
callback(inputs[j], matched, false);
|
||||
}
|
||||
for (j = i - 1; ; --j) {
|
||||
if (!locks[j] || $(locks[j]).is('.unlocked')) break;
|
||||
matched = shift_color(color, reference[input.key], reference[inputs[j].key]);
|
||||
callback(inputs[j], matched, false);
|
||||
}
|
||||
|
||||
// Update preview.
|
||||
preview();
|
||||
}
|
||||
|
||||
// Reset colorScheme selector.
|
||||
if (!colorScheme) {
|
||||
resetScheme();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the color scheme selector.
|
||||
*/
|
||||
function resetScheme() {
|
||||
$('#edit-scheme', form).each(function () {
|
||||
this.selectedIndex = this.options.length - 1;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Focuses Farbtastic on a particular field.
|
||||
*/
|
||||
function focus() {
|
||||
var input = this;
|
||||
// Remove old bindings.
|
||||
focused && $(focused).unbind('keyup', farb.updateValue)
|
||||
.unbind('keyup', preview).unbind('keyup', resetScheme)
|
||||
.parent().removeClass('item-selected');
|
||||
|
||||
// Add new bindings.
|
||||
focused = this;
|
||||
farb.linkTo(function (color) { callback(input, color, true, false); });
|
||||
farb.setColor(this.value);
|
||||
$(focused).keyup(farb.updateValue).keyup(preview).keyup(resetScheme)
|
||||
.parent().addClass('item-selected');
|
||||
}
|
||||
|
||||
// Initialize color fields.
|
||||
$('#palette input.form-text', form)
|
||||
.each(function () {
|
||||
// Extract palette field name
|
||||
this.key = this.id.substring(13);
|
||||
|
||||
// Link to color picker temporarily to initialize.
|
||||
farb.linkTo(function () {}).setColor('#000').linkTo(this);
|
||||
|
||||
// Add lock.
|
||||
var i = inputs.length;
|
||||
if (inputs.length) {
|
||||
var lock = $('<div class="lock"></div>').toggle(
|
||||
function () {
|
||||
$(this).addClass('unlocked');
|
||||
$(hooks[i - 1]).attr('class',
|
||||
locks[i - 2] && $(locks[i - 2]).is(':not(.unlocked)') ? 'hook up' : 'hook'
|
||||
);
|
||||
$(hooks[i]).attr('class',
|
||||
locks[i] && $(locks[i]).is(':not(.unlocked)') ? 'hook down' : 'hook'
|
||||
);
|
||||
},
|
||||
function () {
|
||||
$(this).removeClass('unlocked');
|
||||
$(hooks[i - 1]).attr('class',
|
||||
locks[i - 2] && $(locks[i - 2]).is(':not(.unlocked)') ? 'hook both' : 'hook down'
|
||||
);
|
||||
$(hooks[i]).attr('class',
|
||||
locks[i] && $(locks[i]).is(':not(.unlocked)') ? 'hook both' : 'hook up'
|
||||
);
|
||||
}
|
||||
);
|
||||
$(this).after(lock);
|
||||
locks.push(lock);
|
||||
};
|
||||
|
||||
// Add hook.
|
||||
var hook = $('<div class="hook"></div>');
|
||||
$(this).after(hook);
|
||||
hooks.push(hook);
|
||||
|
||||
$(this).parent().find('.lock').click();
|
||||
this.i = i;
|
||||
inputs.push(this);
|
||||
})
|
||||
.focus(focus);
|
||||
|
||||
$('#palette label', form);
|
||||
|
||||
// Focus first color.
|
||||
focus.call(inputs[0]);
|
||||
|
||||
// Render preview.
|
||||
preview();
|
||||
}
|
||||
};
|
||||
|
||||
})(jQuery);
|
815
modules/color/color.module
Normal file
815
modules/color/color.module
Normal file
|
@ -0,0 +1,815 @@
|
|||
<?php
|
||||
/**
|
||||
* @file
|
||||
* Allows users to change the color scheme of themes.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Implements hook_help().
|
||||
*/
|
||||
function color_help($path, $arg) {
|
||||
switch ($path) {
|
||||
case 'admin/help#color':
|
||||
$output = '<h3>' . t('About') . '</h3>';
|
||||
$output .= '<p>' . t('The Color module allows users with the <em>Administer site configuration</em> permission to quickly and easily change the color scheme of themes that have been built to be compatible with it. For more information, see the online handbook entry for <a href="@color">Color module</a>.', array('@color' => 'http://drupal.org/documentation/modules/color')) . '</p>';
|
||||
$output .= '<h3>' . t('Uses') . '</h3>';
|
||||
$output .= '<dl>';
|
||||
$output .= '<dt>' . t('Changing colors') . '</dt>';
|
||||
$output .= '<dd>' . t("Using the Color module allows you to easily change the color of links, backgrounds, text, and other theme elements. To change the color settings for a compatible theme, select the <em>Settings</em> link for your theme on the <a href='@configure'>Themes administration page</a>. If you don't see a color picker on that page, then your theme is not compatible with the color module. If you are sure that the theme does indeed support the color module, but the color picker does not appear, then <a href='@troubleshoot'>follow these troubleshooting procedures</a>.", array('@configure' => url('admin/appearance'), '@troubleshoot' => 'http://drupal.org/node/109457')) . '</dd>';
|
||||
$output .= '<dd>' . t("The Color module saves a modified copy of the theme's specified stylesheets in the files directory. This means that if you make any manual changes to your theme's stylesheet, <em>you must save your color settings again, even if they haven't changed</em>. This step is required because the module stylesheets (in the files directory) need to be recreated to include your changes.") . '</dd>';
|
||||
$output .= '</dl>';
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_theme().
|
||||
*/
|
||||
function color_theme() {
|
||||
return array(
|
||||
'color_scheme_form' => array(
|
||||
'render element' => 'form',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_form_FORM_ID_alter().
|
||||
*/
|
||||
function color_form_system_theme_settings_alter(&$form, &$form_state) {
|
||||
if (isset($form_state['build_info']['args'][0]) && ($theme = $form_state['build_info']['args'][0]) && color_get_info($theme) && function_exists('gd_info')) {
|
||||
$form['color'] = array(
|
||||
'#type' => 'fieldset',
|
||||
'#title' => t('Color scheme'),
|
||||
'#weight' => -1,
|
||||
'#attributes' => array('id' => 'color_scheme_form'),
|
||||
'#theme' => 'color_scheme_form',
|
||||
);
|
||||
$form['color'] += color_scheme_form($form, $form_state, $theme);
|
||||
$form['#validate'][] = 'color_scheme_form_validate';
|
||||
$form['#submit'][] = 'color_scheme_form_submit';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_form_FORM_ID_alter().
|
||||
*/
|
||||
function color_form_system_themes_alter(&$form, &$form_state) {
|
||||
_color_theme_select_form_alter($form, $form_state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for hook_form_FORM_ID_alter() implementations.
|
||||
*/
|
||||
function _color_theme_select_form_alter(&$form, &$form_state) {
|
||||
// Use the generated screenshot in the theme list.
|
||||
$themes = list_themes();
|
||||
foreach (element_children($form) as $theme) {
|
||||
if ($screenshot = variable_get('color_' . $theme . '_screenshot')) {
|
||||
if (isset($form[$theme]['screenshot'])) {
|
||||
$form[$theme]['screenshot']['#markup'] = theme('image', array('path' => $screenshot, 'title' => '', 'attributes' => array('class' => array('screenshot'))));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces style sheets with color-altered style sheets.
|
||||
*
|
||||
* A theme that supports the color module should call this function from its
|
||||
* THEME_process_html() function, so that the correct style sheets are
|
||||
* included when html.tpl.php is rendered.
|
||||
*
|
||||
* @see theme()
|
||||
*/
|
||||
function _color_html_alter(&$vars) {
|
||||
global $theme_key;
|
||||
$themes = list_themes();
|
||||
|
||||
// Override stylesheets.
|
||||
$color_paths = variable_get('color_' . $theme_key . '_stylesheets', array());
|
||||
if (!empty($color_paths)) {
|
||||
|
||||
foreach ($themes[$theme_key]->stylesheets['all'] as $base_filename => $old_path) {
|
||||
// Loop over the path array with recolored CSS files to find matching
|
||||
// paths which could replace the non-recolored paths.
|
||||
foreach ($color_paths as $color_path) {
|
||||
// Color module currently requires unique file names to be used,
|
||||
// which allows us to compare different file paths.
|
||||
if (drupal_basename($old_path) == drupal_basename($color_path)) {
|
||||
// Replace the path to the new css file.
|
||||
// This keeps the order of the stylesheets intact.
|
||||
$vars['css'][$old_path]['data'] = $color_path;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$vars['styles'] = drupal_get_css($vars['css']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the logo with a color-altered logo.
|
||||
*
|
||||
* A theme that supports the color module should call this function from its
|
||||
* THEME_process_page() function, so that the correct logo is included when
|
||||
* page.tpl.php is rendered.
|
||||
*
|
||||
* @see theme()
|
||||
*/
|
||||
function _color_page_alter(&$vars) {
|
||||
global $theme_key;
|
||||
|
||||
// Override logo.
|
||||
$logo = variable_get('color_' . $theme_key . '_logo');
|
||||
if ($logo && $vars['logo'] && preg_match('!' . $theme_key . '/logo.png$!', $vars['logo'])) {
|
||||
$vars['logo'] = file_create_url($logo);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the Color module information for a particular theme.
|
||||
*/
|
||||
function color_get_info($theme) {
|
||||
static $theme_info = array();
|
||||
|
||||
if (isset($theme_info[$theme])) {
|
||||
return $theme_info[$theme];
|
||||
}
|
||||
|
||||
$path = drupal_get_path('theme', $theme);
|
||||
$file = DRUPAL_ROOT . '/' . $path . '/color/color.inc';
|
||||
if ($path && file_exists($file)) {
|
||||
include $file;
|
||||
$theme_info[$theme] = $info;
|
||||
return $info;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the color palette for a particular theme.
|
||||
*/
|
||||
function color_get_palette($theme, $default = FALSE) {
|
||||
// Fetch and expand default palette.
|
||||
$info = color_get_info($theme);
|
||||
$palette = $info['schemes']['default']['colors'];
|
||||
|
||||
// Load variable.
|
||||
return $default ? $palette : variable_get('color_' . $theme . '_palette', $palette);
|
||||
}
|
||||
|
||||
/**
|
||||
* Form constructor for the color configuration form for a particular theme.
|
||||
*
|
||||
* @param $theme
|
||||
* The machine name of the theme whose color settings are being configured.
|
||||
*
|
||||
* @see color_scheme_form_validate()
|
||||
* @see color_scheme_form_submit()
|
||||
* @ingroup forms
|
||||
*/
|
||||
function color_scheme_form($complete_form, &$form_state, $theme) {
|
||||
$base = drupal_get_path('module', 'color');
|
||||
$info = color_get_info($theme);
|
||||
|
||||
$info['schemes'][''] = array('title' => t('Custom'), 'colors' => array());
|
||||
$color_sets = array();
|
||||
$schemes = array();
|
||||
foreach ($info['schemes'] as $key => $scheme) {
|
||||
$color_sets[$key] = $scheme['title'];
|
||||
$schemes[$key] = $scheme['colors'];
|
||||
$schemes[$key] += $info['schemes']['default']['colors'];
|
||||
}
|
||||
|
||||
// See if we're using a predefined scheme.
|
||||
// Note: we use the original theme when the default scheme is chosen.
|
||||
$current_scheme = variable_get('color_' . $theme . '_palette', array());
|
||||
foreach ($schemes as $key => $scheme) {
|
||||
if ($current_scheme == $scheme) {
|
||||
$scheme_name = $key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (empty($scheme_name)) {
|
||||
if (empty($current_scheme)) {
|
||||
$scheme_name = 'default';
|
||||
}
|
||||
else {
|
||||
$scheme_name = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Add scheme selector.
|
||||
$form['scheme'] = array(
|
||||
'#type' => 'select',
|
||||
'#title' => t('Color set'),
|
||||
'#options' => $color_sets,
|
||||
'#default_value' => $scheme_name,
|
||||
'#attached' => array(
|
||||
// Add Farbtastic color picker.
|
||||
'library' => array(
|
||||
array('system', 'farbtastic'),
|
||||
),
|
||||
// Add custom CSS.
|
||||
'css' => array(
|
||||
$base . '/color.css' => array(),
|
||||
),
|
||||
// Add custom JavaScript.
|
||||
'js' => array(
|
||||
$base . '/color.js',
|
||||
array(
|
||||
'data' => array(
|
||||
'color' => array(
|
||||
'reference' => color_get_palette($theme, TRUE),
|
||||
'schemes' => $schemes,
|
||||
),
|
||||
'gradients' => $info['gradients'],
|
||||
),
|
||||
'type' => 'setting',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Add palette fields.
|
||||
$palette = color_get_palette($theme);
|
||||
$names = $info['fields'];
|
||||
$form['palette']['#tree'] = TRUE;
|
||||
foreach ($palette as $name => $value) {
|
||||
if (isset($names[$name])) {
|
||||
$form['palette'][$name] = array(
|
||||
'#type' => 'textfield',
|
||||
'#title' => check_plain($names[$name]),
|
||||
'#value_callback' => 'color_palette_color_value',
|
||||
'#default_value' => $value,
|
||||
'#size' => 8,
|
||||
);
|
||||
}
|
||||
}
|
||||
$form['theme'] = array('#type' => 'value', '#value' => $theme);
|
||||
$form['info'] = array('#type' => 'value', '#value' => $info);
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns HTML for a theme's color form.
|
||||
*
|
||||
* @param $variables
|
||||
* An associative array containing:
|
||||
* - form: A render element representing the form.
|
||||
*
|
||||
* @ingroup themeable
|
||||
*/
|
||||
function theme_color_scheme_form($variables) {
|
||||
$form = $variables['form'];
|
||||
|
||||
$theme = $form['theme']['#value'];
|
||||
$info = $form['info']['#value'];
|
||||
$path = drupal_get_path('theme', $theme) . '/';
|
||||
drupal_add_css($path . $info['preview_css']);
|
||||
|
||||
$preview_js_path = isset($info['preview_js']) ? $path . $info['preview_js'] : drupal_get_path('module', 'color') . '/' . 'preview.js';
|
||||
// Add the JS at a weight below color.js.
|
||||
drupal_add_js($preview_js_path, array('weight' => -1));
|
||||
|
||||
$output = '';
|
||||
$output .= '<div class="color-form clearfix">';
|
||||
// Color schemes
|
||||
$output .= drupal_render($form['scheme']);
|
||||
// Palette
|
||||
$output .= '<div id="palette" class="clearfix">';
|
||||
foreach (element_children($form['palette']) as $name) {
|
||||
$output .= drupal_render($form['palette'][$name]);
|
||||
}
|
||||
$output .= '</div>';
|
||||
// Preview
|
||||
$output .= drupal_render_children($form);
|
||||
$output .= '<h2>' . t('Preview') . '</h2>';
|
||||
// Attempt to load preview HTML if the theme provides it.
|
||||
$preview_html_path = DRUPAL_ROOT . '/' . (isset($info['preview_html']) ? drupal_get_path('theme', $theme) . '/' . $info['preview_html'] : drupal_get_path('module', 'color') . '/preview.html');
|
||||
$output .= file_get_contents($preview_html_path);
|
||||
// Close the wrapper div.
|
||||
$output .= '</div>';
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the value for a palette color field.
|
||||
*
|
||||
* @param $element
|
||||
* The form element whose value is being populated.
|
||||
* @param $input
|
||||
* The incoming input to populate the form element. If this is FALSE,
|
||||
* the element's default value should be returned.
|
||||
* @param $form_state
|
||||
* A keyed array containing the current state of the form.
|
||||
*
|
||||
* @return
|
||||
* The data that will appear in the $form_state['values'] collection for this
|
||||
* element. Return nothing to use the default.
|
||||
*/
|
||||
function color_palette_color_value($element, $input = FALSE, $form_state = array()) {
|
||||
// If we suspect a possible cross-site request forgery attack, only accept
|
||||
// hexadecimal CSS color strings from user input, to avoid problems when this
|
||||
// value is used in the JavaScript preview.
|
||||
if ($input !== FALSE) {
|
||||
// Start with the provided value for this textfield, and validate that if
|
||||
// necessary, falling back on the default value.
|
||||
$value = form_type_textfield_value($element, $input, $form_state);
|
||||
if (!$value || !isset($form_state['complete form']['#token']) || color_valid_hexadecimal_string($value) || drupal_valid_token($form_state['values']['form_token'], $form_state['complete form']['#token'])) {
|
||||
return $value;
|
||||
}
|
||||
else {
|
||||
return $element['#default_value'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a hexadecimal CSS color string is valid.
|
||||
*
|
||||
* @param $color
|
||||
* The string to check.
|
||||
*
|
||||
* @return
|
||||
* TRUE if the string is a valid hexadecimal CSS color string, or FALSE if it
|
||||
* isn't.
|
||||
*/
|
||||
function color_valid_hexadecimal_string($color) {
|
||||
return preg_match('/^#([a-f0-9]{3}){1,2}$/iD', $color);
|
||||
}
|
||||
|
||||
/**
|
||||
* Form validation handler for color_scheme_form().
|
||||
*
|
||||
* @see color_scheme_form_submit()
|
||||
*/
|
||||
function color_scheme_form_validate($form, &$form_state) {
|
||||
// Only accept hexadecimal CSS color strings to avoid XSS upon use.
|
||||
foreach ($form_state['values']['palette'] as $key => $color) {
|
||||
if (!color_valid_hexadecimal_string($color)) {
|
||||
form_set_error('palette][' . $key, t('%name must be a valid hexadecimal CSS color value.', array('%name' => $form['color']['palette'][$key]['#title'])));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Form submission handler for color_scheme_form().
|
||||
*
|
||||
* @see color_scheme_form_validate()
|
||||
*/
|
||||
function color_scheme_form_submit($form, &$form_state) {
|
||||
// Get theme coloring info.
|
||||
if (!isset($form_state['values']['info'])) {
|
||||
return;
|
||||
}
|
||||
$theme = $form_state['values']['theme'];
|
||||
$info = $form_state['values']['info'];
|
||||
|
||||
// Resolve palette.
|
||||
$palette = $form_state['values']['palette'];
|
||||
if ($form_state['values']['scheme'] != '') {
|
||||
foreach ($palette as $key => $color) {
|
||||
if (isset($info['schemes'][$form_state['values']['scheme']]['colors'][$key])) {
|
||||
$palette[$key] = $info['schemes'][$form_state['values']['scheme']]['colors'][$key];
|
||||
}
|
||||
}
|
||||
$palette += $info['schemes']['default']['colors'];
|
||||
}
|
||||
|
||||
// Make sure enough memory is available, if PHP's memory limit is compiled in.
|
||||
if (function_exists('memory_get_usage')) {
|
||||
// Fetch source image dimensions.
|
||||
$source = drupal_get_path('theme', $theme) . '/' . $info['base_image'];
|
||||
list($width, $height) = getimagesize($source);
|
||||
|
||||
// We need at least a copy of the source and a target buffer of the same
|
||||
// size (both at 32bpp).
|
||||
$required = $width * $height * 8;
|
||||
// We intend to prevent color scheme changes if there isn't enough memory
|
||||
// available. memory_get_usage(TRUE) returns a more accurate number than
|
||||
// memory_get_usage(), therefore we won't inadvertently reject a color
|
||||
// scheme change based on a faulty memory calculation.
|
||||
$usage = memory_get_usage(TRUE);
|
||||
$memory_limit = ini_get('memory_limit');
|
||||
$size = parse_size($memory_limit);
|
||||
if (!drupal_check_memory_limit($usage + $required, $memory_limit)) {
|
||||
drupal_set_message(t('There is not enough memory available to PHP to change this theme\'s color scheme. You need at least %size more. Check the <a href="@url">PHP documentation</a> for more information.', array('%size' => format_size($usage + $required - $size), '@url' => 'http://www.php.net/manual/ini.core.php#ini.sect.resource-limits')), 'error');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete old files.
|
||||
foreach (variable_get('color_' . $theme . '_files', array()) as $file) {
|
||||
@drupal_unlink($file);
|
||||
}
|
||||
if (isset($file) && $file = dirname($file)) {
|
||||
@drupal_rmdir($file);
|
||||
}
|
||||
|
||||
// Don't render the default colorscheme, use the standard theme instead.
|
||||
if (implode(',', color_get_palette($theme, TRUE)) == implode(',', $palette)) {
|
||||
variable_del('color_' . $theme . '_palette');
|
||||
variable_del('color_' . $theme . '_stylesheets');
|
||||
variable_del('color_' . $theme . '_logo');
|
||||
variable_del('color_' . $theme . '_files');
|
||||
variable_del('color_' . $theme . '_screenshot');
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare target locations for generated files.
|
||||
$id = $theme . '-' . substr(hash('sha256', serialize($palette) . microtime()), 0, 8);
|
||||
$paths['color'] = 'public://color';
|
||||
$paths['target'] = $paths['color'] . '/' . $id;
|
||||
foreach ($paths as $path) {
|
||||
file_prepare_directory($path, FILE_CREATE_DIRECTORY);
|
||||
}
|
||||
$paths['target'] = $paths['target'] . '/';
|
||||
$paths['id'] = $id;
|
||||
$paths['source'] = drupal_get_path('theme', $theme) . '/';
|
||||
$paths['files'] = $paths['map'] = array();
|
||||
|
||||
// Save palette and logo location.
|
||||
variable_set('color_' . $theme . '_palette', $palette);
|
||||
variable_set('color_' . $theme . '_logo', $paths['target'] . 'logo.png');
|
||||
|
||||
// Copy over neutral images.
|
||||
foreach ($info['copy'] as $file) {
|
||||
$base = drupal_basename($file);
|
||||
$source = $paths['source'] . $file;
|
||||
$filepath = file_unmanaged_copy($source, $paths['target'] . $base);
|
||||
$paths['map'][$file] = $base;
|
||||
$paths['files'][] = $filepath;
|
||||
}
|
||||
|
||||
// Render new images, if image has been provided.
|
||||
if ($info['base_image']) {
|
||||
_color_render_images($theme, $info, $paths, $palette);
|
||||
}
|
||||
|
||||
// Rewrite theme stylesheets.
|
||||
$css = array();
|
||||
foreach ($info['css'] as $stylesheet) {
|
||||
// Build a temporary array with LTR and RTL files.
|
||||
$files = array();
|
||||
if (file_exists($paths['source'] . $stylesheet)) {
|
||||
$files[] = $stylesheet;
|
||||
|
||||
$rtl_file = str_replace('.css', '-rtl.css', $stylesheet);
|
||||
if (file_exists($paths['source'] . $rtl_file)) {
|
||||
$files[] = $rtl_file;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($files as $file) {
|
||||
// Aggregate @imports recursively for each configured top level CSS file
|
||||
// without optimization. Aggregation and optimization will be
|
||||
// handled by drupal_build_css_cache() only.
|
||||
$style = drupal_load_stylesheet($paths['source'] . $file, FALSE);
|
||||
|
||||
// Return the path to where this CSS file originated from, stripping
|
||||
// off the name of the file at the end of the path.
|
||||
$base = base_path() . dirname($paths['source'] . $file) . '/';
|
||||
_drupal_build_css_path(NULL, $base);
|
||||
|
||||
// Prefix all paths within this CSS file, ignoring absolute paths.
|
||||
$style = preg_replace_callback('/url\([\'"]?(?![a-z]+:|\/+)([^\'")]+)[\'"]?\)/i', '_drupal_build_css_path', $style);
|
||||
|
||||
// Rewrite stylesheet with new colors.
|
||||
$style = _color_rewrite_stylesheet($theme, $info, $paths, $palette, $style);
|
||||
$base_file = drupal_basename($file);
|
||||
$css[] = $paths['target'] . $base_file;
|
||||
_color_save_stylesheet($paths['target'] . $base_file, $style, $paths);
|
||||
}
|
||||
}
|
||||
|
||||
// Maintain list of files.
|
||||
variable_set('color_' . $theme . '_stylesheets', $css);
|
||||
variable_set('color_' . $theme . '_files', $paths['files']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewrites the stylesheet to match the colors in the palette.
|
||||
*/
|
||||
function _color_rewrite_stylesheet($theme, &$info, &$paths, $palette, $style) {
|
||||
$themes = list_themes();
|
||||
// Prepare color conversion table.
|
||||
$conversion = $palette;
|
||||
foreach ($conversion as $k => $v) {
|
||||
$conversion[$k] = drupal_strtolower($v);
|
||||
}
|
||||
$default = color_get_palette($theme, TRUE);
|
||||
|
||||
// Split off the "Don't touch" section of the stylesheet.
|
||||
$split = "Color Module: Don't touch";
|
||||
if (strpos($style, $split) !== FALSE) {
|
||||
list($style, $fixed) = explode($split, $style);
|
||||
}
|
||||
|
||||
// Find all colors in the stylesheet and the chunks in between.
|
||||
$style = preg_split('/(#[0-9a-f]{6}|#[0-9a-f]{3})/i', $style, -1, PREG_SPLIT_DELIM_CAPTURE);
|
||||
$is_color = FALSE;
|
||||
$output = '';
|
||||
$base = 'base';
|
||||
|
||||
// Iterate over all the parts.
|
||||
foreach ($style as $chunk) {
|
||||
if ($is_color) {
|
||||
$chunk = drupal_strtolower($chunk);
|
||||
// Check if this is one of the colors in the default palette.
|
||||
if ($key = array_search($chunk, $default)) {
|
||||
$chunk = $conversion[$key];
|
||||
}
|
||||
// Not a pre-set color. Extrapolate from the base.
|
||||
else {
|
||||
$chunk = _color_shift($palette[$base], $default[$base], $chunk, $info['blend_target']);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Determine the most suitable base color for the next color.
|
||||
|
||||
// 'a' declarations. Use link.
|
||||
if (preg_match('@[^a-z0-9_-](a)[^a-z0-9_-][^/{]*{[^{]+$@i', $chunk)) {
|
||||
$base = 'link';
|
||||
}
|
||||
// 'color:' styles. Use text.
|
||||
elseif (preg_match('/(?<!-)color[^{:]*:[^{#]*$/i', $chunk)) {
|
||||
$base = 'text';
|
||||
}
|
||||
// Reset back to base.
|
||||
else {
|
||||
$base = 'base';
|
||||
}
|
||||
}
|
||||
$output .= $chunk;
|
||||
$is_color = !$is_color;
|
||||
}
|
||||
// Append fixed colors segment.
|
||||
if (isset($fixed)) {
|
||||
$output .= $fixed;
|
||||
}
|
||||
|
||||
// Replace paths to images.
|
||||
foreach ($paths['map'] as $before => $after) {
|
||||
$before = base_path() . $paths['source'] . $before;
|
||||
$before = preg_replace('`(^|/)(?!../)([^/]+)/../`', '$1', $before);
|
||||
$output = str_replace($before, $after, $output);
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the rewritten stylesheet to disk.
|
||||
*/
|
||||
function _color_save_stylesheet($file, $style, &$paths) {
|
||||
$filepath = file_unmanaged_save_data($style, $file, FILE_EXISTS_REPLACE);
|
||||
$paths['files'][] = $filepath;
|
||||
|
||||
// Set standard file permissions for webserver-generated files.
|
||||
drupal_chmod($file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders images that match a given palette.
|
||||
*/
|
||||
function _color_render_images($theme, &$info, &$paths, $palette) {
|
||||
// Prepare template image.
|
||||
$source = $paths['source'] . '/' . $info['base_image'];
|
||||
$source = imagecreatefrompng($source);
|
||||
$width = imagesx($source);
|
||||
$height = imagesy($source);
|
||||
|
||||
// Prepare target buffer.
|
||||
$target = imagecreatetruecolor($width, $height);
|
||||
imagealphablending($target, TRUE);
|
||||
|
||||
// Fill regions of solid color.
|
||||
foreach ($info['fill'] as $color => $fill) {
|
||||
imagefilledrectangle($target, $fill[0], $fill[1], $fill[0] + $fill[2], $fill[1] + $fill[3], _color_gd($target, $palette[$color]));
|
||||
}
|
||||
|
||||
// Render gradients.
|
||||
foreach ($info['gradients'] as $gradient) {
|
||||
// Get direction of the gradient.
|
||||
if (isset($gradient['direction']) && $gradient['direction'] == 'horizontal') {
|
||||
// Horizontal gradient.
|
||||
for ($x = 0; $x < $gradient['dimension'][2]; $x++) {
|
||||
$color = _color_blend($target, $palette[$gradient['colors'][0]], $palette[$gradient['colors'][1]], $x / ($gradient['dimension'][2] - 1));
|
||||
imagefilledrectangle($target, ($gradient['dimension'][0] + $x), $gradient['dimension'][1], ($gradient['dimension'][0] + $x + 1), ($gradient['dimension'][1] + $gradient['dimension'][3]), $color);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Vertical gradient.
|
||||
for ($y = 0; $y < $gradient['dimension'][3]; $y++) {
|
||||
$color = _color_blend($target, $palette[$gradient['colors'][0]], $palette[$gradient['colors'][1]], $y / ($gradient['dimension'][3] - 1));
|
||||
imagefilledrectangle($target, $gradient['dimension'][0], $gradient['dimension'][1] + $y, $gradient['dimension'][0] + $gradient['dimension'][2], $gradient['dimension'][1] + $y + 1, $color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Blend over template.
|
||||
imagecopy($target, $source, 0, 0, 0, 0, $width, $height);
|
||||
|
||||
// Clean up template image.
|
||||
imagedestroy($source);
|
||||
|
||||
// Cut out slices.
|
||||
foreach ($info['slices'] as $file => $coord) {
|
||||
list($x, $y, $width, $height) = $coord;
|
||||
$base = drupal_basename($file);
|
||||
$image = drupal_realpath($paths['target'] . $base);
|
||||
|
||||
// Cut out slice.
|
||||
if ($file == 'screenshot.png') {
|
||||
$slice = imagecreatetruecolor(150, 90);
|
||||
imagecopyresampled($slice, $target, 0, 0, $x, $y, 150, 90, $width, $height);
|
||||
variable_set('color_' . $theme . '_screenshot', $image);
|
||||
}
|
||||
else {
|
||||
$slice = imagecreatetruecolor($width, $height);
|
||||
imagecopy($slice, $target, 0, 0, $x, $y, $width, $height);
|
||||
}
|
||||
|
||||
// Save image.
|
||||
imagepng($slice, $image);
|
||||
imagedestroy($slice);
|
||||
$paths['files'][] = $image;
|
||||
|
||||
// Set standard file permissions for webserver-generated files
|
||||
drupal_chmod($image);
|
||||
|
||||
// Build before/after map of image paths.
|
||||
$paths['map'][$file] = $base;
|
||||
}
|
||||
|
||||
// Clean up target buffer.
|
||||
imagedestroy($target);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shifts a given color, using a reference pair and a target blend color.
|
||||
*
|
||||
* Note: this function is significantly different from the JS version, as it
|
||||
* is written to match the blended images perfectly.
|
||||
*
|
||||
* Constraint: if (ref2 == target + (ref1 - target) * delta) for some fraction
|
||||
* delta then (return == target + (given - target) * delta).
|
||||
*
|
||||
* Loose constraint: Preserve relative positions in saturation and luminance
|
||||
* space.
|
||||
*/
|
||||
function _color_shift($given, $ref1, $ref2, $target) {
|
||||
// We assume that ref2 is a blend of ref1 and target and find
|
||||
// delta based on the length of the difference vectors.
|
||||
|
||||
// delta = 1 - |ref2 - ref1| / |white - ref1|
|
||||
$target = _color_unpack($target, TRUE);
|
||||
$ref1 = _color_unpack($ref1, TRUE);
|
||||
$ref2 = _color_unpack($ref2, TRUE);
|
||||
$numerator = 0;
|
||||
$denominator = 0;
|
||||
for ($i = 0; $i < 3; ++$i) {
|
||||
$numerator += ($ref2[$i] - $ref1[$i]) * ($ref2[$i] - $ref1[$i]);
|
||||
$denominator += ($target[$i] - $ref1[$i]) * ($target[$i] - $ref1[$i]);
|
||||
}
|
||||
$delta = ($denominator > 0) ? (1 - sqrt($numerator / $denominator)) : 0;
|
||||
|
||||
// Calculate the color that ref2 would be if the assumption was true.
|
||||
for ($i = 0; $i < 3; ++$i) {
|
||||
$ref3[$i] = $target[$i] + ($ref1[$i] - $target[$i]) * $delta;
|
||||
}
|
||||
|
||||
// If the assumption is not true, there is a difference between ref2 and ref3.
|
||||
// We measure this in HSL space. Notation: x' = hsl(x).
|
||||
$ref2 = _color_rgb2hsl($ref2);
|
||||
$ref3 = _color_rgb2hsl($ref3);
|
||||
for ($i = 0; $i < 3; ++$i) {
|
||||
$shift[$i] = $ref2[$i] - $ref3[$i];
|
||||
}
|
||||
|
||||
// Take the given color, and blend it towards the target.
|
||||
$given = _color_unpack($given, TRUE);
|
||||
for ($i = 0; $i < 3; ++$i) {
|
||||
$result[$i] = $target[$i] + ($given[$i] - $target[$i]) * $delta;
|
||||
}
|
||||
|
||||
// Finally, we apply the extra shift in HSL space.
|
||||
// Note: if ref2 is a pure blend of ref1 and target, then |shift| = 0.
|
||||
$result = _color_rgb2hsl($result);
|
||||
for ($i = 0; $i < 3; ++$i) {
|
||||
$result[$i] = min(1, max(0, $result[$i] + $shift[$i]));
|
||||
}
|
||||
$result = _color_hsl2rgb($result);
|
||||
|
||||
// Return hex color.
|
||||
return _color_pack($result, TRUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a hex triplet into a GD color.
|
||||
*/
|
||||
function _color_gd($img, $hex) {
|
||||
$c = array_merge(array($img), _color_unpack($hex));
|
||||
return call_user_func_array('imagecolorallocate', $c);
|
||||
}
|
||||
|
||||
/**
|
||||
* Blends two hex colors and returns the GD color.
|
||||
*/
|
||||
function _color_blend($img, $hex1, $hex2, $alpha) {
|
||||
$in1 = _color_unpack($hex1);
|
||||
$in2 = _color_unpack($hex2);
|
||||
$out = array($img);
|
||||
for ($i = 0; $i < 3; ++$i) {
|
||||
$out[] = $in1[$i] + ($in2[$i] - $in1[$i]) * $alpha;
|
||||
}
|
||||
|
||||
return call_user_func_array('imagecolorallocate', $out);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a hex color into an RGB triplet.
|
||||
*/
|
||||
function _color_unpack($hex, $normalize = FALSE) {
|
||||
if (strlen($hex) == 4) {
|
||||
$hex = $hex[1] . $hex[1] . $hex[2] . $hex[2] . $hex[3] . $hex[3];
|
||||
}
|
||||
$c = hexdec($hex);
|
||||
for ($i = 16; $i >= 0; $i -= 8) {
|
||||
$out[] = (($c >> $i) & 0xFF) / ($normalize ? 255 : 1);
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an RGB triplet to a hex color.
|
||||
*/
|
||||
function _color_pack($rgb, $normalize = FALSE) {
|
||||
$out = 0;
|
||||
foreach ($rgb as $k => $v) {
|
||||
$out |= (($v * ($normalize ? 255 : 1)) << (16 - $k * 8));
|
||||
}
|
||||
|
||||
return '#' . str_pad(dechex($out), 6, 0, STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an HSL triplet into RGB.
|
||||
*/
|
||||
function _color_hsl2rgb($hsl) {
|
||||
$h = $hsl[0];
|
||||
$s = $hsl[1];
|
||||
$l = $hsl[2];
|
||||
$m2 = ($l <= 0.5) ? $l * ($s + 1) : $l + $s - $l*$s;
|
||||
$m1 = $l * 2 - $m2;
|
||||
|
||||
return array(
|
||||
_color_hue2rgb($m1, $m2, $h + 0.33333),
|
||||
_color_hue2rgb($m1, $m2, $h),
|
||||
_color_hue2rgb($m1, $m2, $h - 0.33333),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for _color_hsl2rgb().
|
||||
*/
|
||||
function _color_hue2rgb($m1, $m2, $h) {
|
||||
$h = ($h < 0) ? $h + 1 : (($h > 1) ? $h - 1 : $h);
|
||||
if ($h * 6 < 1) return $m1 + ($m2 - $m1) * $h * 6;
|
||||
if ($h * 2 < 1) return $m2;
|
||||
if ($h * 3 < 2) return $m1 + ($m2 - $m1) * (0.66666 - $h) * 6;
|
||||
|
||||
return $m1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an RGB triplet to HSL.
|
||||
*/
|
||||
function _color_rgb2hsl($rgb) {
|
||||
$r = $rgb[0];
|
||||
$g = $rgb[1];
|
||||
$b = $rgb[2];
|
||||
$min = min($r, min($g, $b));
|
||||
$max = max($r, max($g, $b));
|
||||
$delta = $max - $min;
|
||||
$l = ($min + $max) / 2;
|
||||
$s = 0;
|
||||
|
||||
if ($l > 0 && $l < 1) {
|
||||
$s = $delta / ($l < 0.5 ? (2 * $l) : (2 - 2 * $l));
|
||||
}
|
||||
|
||||
$h = 0;
|
||||
if ($delta > 0) {
|
||||
if ($max == $r && $max != $g) $h += ($g - $b) / $delta;
|
||||
if ($max == $g && $max != $b) $h += (2 + ($b - $r) / $delta);
|
||||
if ($max == $b && $max != $r) $h += (4 + ($r - $g) / $delta);
|
||||
$h /= 6;
|
||||
}
|
||||
|
||||
return array($h, $s, $l);
|
||||
}
|
133
modules/color/color.test
Normal file
133
modules/color/color.test
Normal file
|
@ -0,0 +1,133 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Tests for color module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Tests the Color module functionality.
|
||||
*/
|
||||
class ColorTestCase extends DrupalWebTestCase {
|
||||
protected $big_user;
|
||||
protected $themes;
|
||||
protected $colorTests;
|
||||
|
||||
public static function getInfo() {
|
||||
return array(
|
||||
'name' => 'Color functionality',
|
||||
'description' => 'Modify the Bartik and Garland theme colors and make sure the changes are reflected on the frontend',
|
||||
'group' => 'Color',
|
||||
);
|
||||
}
|
||||
|
||||
function setUp() {
|
||||
parent::setUp('color');
|
||||
|
||||
// Create users.
|
||||
$this->big_user = $this->drupalCreateUser(array('administer themes'));
|
||||
|
||||
// This tests the color module in both Bartik and Garland.
|
||||
$this->themes = array(
|
||||
'bartik' => array(
|
||||
'palette_input' => 'palette[bg]',
|
||||
'scheme' => 'slate',
|
||||
'scheme_color' => '#3b3b3b',
|
||||
),
|
||||
'garland' => array(
|
||||
'palette_input' => 'palette[link]',
|
||||
'scheme' => 'greenbeam',
|
||||
'scheme_color' => '#0c7a00',
|
||||
),
|
||||
);
|
||||
theme_enable(array_keys($this->themes));
|
||||
|
||||
// Array filled with valid and not valid color values
|
||||
$this->colorTests = array(
|
||||
'#000' => TRUE,
|
||||
'#123456' => TRUE,
|
||||
'#abcdef' => TRUE,
|
||||
'#0' => FALSE,
|
||||
'#00' => FALSE,
|
||||
'#0000' => FALSE,
|
||||
'#00000' => FALSE,
|
||||
'123456' => FALSE,
|
||||
'#00000g' => FALSE,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the Color module functionality.
|
||||
*/
|
||||
function testColor() {
|
||||
foreach ($this->themes as $theme => $test_values) {
|
||||
$this->_testColor($theme, $test_values);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the Color module functionality using the given theme.
|
||||
*/
|
||||
function _testColor($theme, $test_values) {
|
||||
variable_set('theme_default', $theme);
|
||||
$settings_path = 'admin/appearance/settings/' . $theme;
|
||||
|
||||
$this->drupalLogin($this->big_user);
|
||||
$this->drupalGet($settings_path);
|
||||
$this->assertResponse(200);
|
||||
$edit['scheme'] = '';
|
||||
$edit[$test_values['palette_input']] = '#123456';
|
||||
$this->drupalPost($settings_path, $edit, t('Save configuration'));
|
||||
|
||||
$this->drupalGet('<front>');
|
||||
$stylesheets = variable_get('color_' . $theme . '_stylesheets', array());
|
||||
$this->assertPattern('|' . file_create_url($stylesheets[0]) . '|', 'Make sure the color stylesheet is included in the content. (' . $theme . ')');
|
||||
|
||||
$stylesheet_content = join("\n", file($stylesheets[0]));
|
||||
$this->assertTrue(strpos($stylesheet_content, 'color: #123456') !== FALSE, 'Make sure the color we changed is in the color stylesheet. (' . $theme . ')');
|
||||
|
||||
$this->drupalGet($settings_path);
|
||||
$this->assertResponse(200);
|
||||
$edit['scheme'] = $test_values['scheme'];
|
||||
$this->drupalPost($settings_path, $edit, t('Save configuration'));
|
||||
|
||||
$this->drupalGet('<front>');
|
||||
$stylesheets = variable_get('color_' . $theme . '_stylesheets', array());
|
||||
$stylesheet_content = join("\n", file($stylesheets[0]));
|
||||
$this->assertTrue(strpos($stylesheet_content, 'color: ' . $test_values['scheme_color']) !== FALSE, 'Make sure the color we changed is in the color stylesheet. (' . $theme . ')');
|
||||
|
||||
// Test with aggregated CSS turned on.
|
||||
variable_set('preprocess_css', 1);
|
||||
$this->drupalGet('<front>');
|
||||
$stylesheets = variable_get('drupal_css_cache_files', array());
|
||||
$stylesheet_content = '';
|
||||
foreach ($stylesheets as $key => $uri) {
|
||||
$stylesheet_content .= join("\n", file(drupal_realpath($uri)));
|
||||
}
|
||||
$this->assertTrue(strpos($stylesheet_content, 'public://') === FALSE, 'Make sure the color paths have been translated to local paths. (' . $theme . ')');
|
||||
variable_set('preprocess_css', 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests whether the provided color is valid.
|
||||
*/
|
||||
function testValidColor() {
|
||||
variable_set('theme_default', 'bartik');
|
||||
$settings_path = 'admin/appearance/settings/bartik';
|
||||
|
||||
$this->drupalLogin($this->big_user);
|
||||
$edit['scheme'] = '';
|
||||
|
||||
foreach ($this->colorTests as $color => $is_valid) {
|
||||
$edit['palette[bg]'] = $color;
|
||||
$this->drupalPost($settings_path, $edit, t('Save configuration'));
|
||||
|
||||
if ($is_valid) {
|
||||
$this->assertText('The configuration options have been saved.');
|
||||
}
|
||||
else {
|
||||
$this->assertText('Main background must be a valid hexadecimal CSS color value.');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
BIN
modules/color/images/hook-rtl.png
Normal file
BIN
modules/color/images/hook-rtl.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 116 B |
BIN
modules/color/images/hook.png
Normal file
BIN
modules/color/images/hook.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 116 B |
BIN
modules/color/images/lock.png
Normal file
BIN
modules/color/images/lock.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 230 B |
7
modules/color/preview.html
Normal file
7
modules/color/preview.html
Normal file
|
@ -0,0 +1,7 @@
|
|||
<div id="preview">
|
||||
<div id="text">
|
||||
<h2>Lorem ipsum dolor</h2>
|
||||
<p>Sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud <a href="#">exercitation ullamco</a> laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
|
||||
</div>
|
||||
<div id="img"></div>
|
||||
</div>
|
38
modules/color/preview.js
Normal file
38
modules/color/preview.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
/**
|
||||
* @file
|
||||
* Attaches preview-related behavior for the Color module.
|
||||
*/
|
||||
|
||||
(function ($) {
|
||||
Drupal.color = {
|
||||
callback: function(context, settings, form, farb, height, width) {
|
||||
// Solid background.
|
||||
$('#preview', form).css('backgroundColor', $('#palette input[name="palette[base]"]', form).val());
|
||||
|
||||
// Text preview
|
||||
$('#text', form).css('color', $('#palette input[name="palette[text]"]', form).val());
|
||||
$('#text a, #text h2', form).css('color', $('#palette input[name="palette[link]"]', form).val());
|
||||
|
||||
// Set up gradients if there are some.
|
||||
var color_start, color_end;
|
||||
for (i in settings.gradients) {
|
||||
color_start = farb.unpack($('#palette input[name="palette[' + settings.gradients[i]['colors'][0] + ']"]', form).val());
|
||||
color_end = farb.unpack($('#palette input[name="palette[' + settings.gradients[i]['colors'][1] + ']"]', form).val());
|
||||
if (color_start && color_end) {
|
||||
var delta = [];
|
||||
for (j in color_start) {
|
||||
delta[j] = (color_end[j] - color_start[j]) / (settings.gradients[i]['vertical'] ? height[i] : width[i]);
|
||||
}
|
||||
var accum = color_start;
|
||||
// Render gradient lines.
|
||||
$('#gradient-' + i + ' > div', form).each(function () {
|
||||
for (j in accum) {
|
||||
accum[j] += delta[j];
|
||||
}
|
||||
this.style.backgroundColor = farb.pack(accum);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
})(jQuery);
|
32
modules/comment/comment-node-form.js
Normal file
32
modules/comment/comment-node-form.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
|
||||
(function ($) {
|
||||
|
||||
Drupal.behaviors.commentFieldsetSummaries = {
|
||||
attach: function (context) {
|
||||
$('fieldset.comment-node-settings-form', context).drupalSetSummary(function (context) {
|
||||
return Drupal.checkPlain($('.form-item-comment input:checked', context).next('label').text());
|
||||
});
|
||||
|
||||
// Provide the summary for the node type form.
|
||||
$('fieldset.comment-node-type-settings-form', context).drupalSetSummary(function(context) {
|
||||
var vals = [];
|
||||
|
||||
// Default comment setting.
|
||||
vals.push($(".form-item-comment select option:selected", context).text());
|
||||
|
||||
// Threading.
|
||||
var threading = $(".form-item-comment-default-mode input:checked", context).next('label').text();
|
||||
if (threading) {
|
||||
vals.push(threading);
|
||||
}
|
||||
|
||||
// Comments per page.
|
||||
var number = $(".form-item-comment-default-per-page select option:selected", context).val();
|
||||
vals.push(Drupal.t('@number comments per page', {'@number': number}));
|
||||
|
||||
return Drupal.checkPlain(vals.join(', '));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
})(jQuery);
|
5
modules/comment/comment-rtl.css
Normal file
5
modules/comment/comment-rtl.css
Normal file
|
@ -0,0 +1,5 @@
|
|||
|
||||
.indented {
|
||||
margin-left: 0;
|
||||
margin-right: 25px;
|
||||
}
|
52
modules/comment/comment-wrapper.tpl.php
Normal file
52
modules/comment/comment-wrapper.tpl.php
Normal file
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Default theme implementation to provide an HTML container for comments.
|
||||
*
|
||||
* Available variables:
|
||||
* - $content: The array of content-related elements for the node. Use
|
||||
* render($content) to print them all, or
|
||||
* print a subset such as render($content['comment_form']).
|
||||
* - $classes: String of classes that can be used to style contextually through
|
||||
* CSS. It can be manipulated through the variable $classes_array from
|
||||
* preprocess functions. The default value has the following:
|
||||
* - comment-wrapper: The current template type, i.e., "theming hook".
|
||||
* - $title_prefix (array): An array containing additional output populated by
|
||||
* modules, intended to be displayed in front of the main title tag that
|
||||
* appears in the template.
|
||||
* - $title_suffix (array): An array containing additional output populated by
|
||||
* modules, intended to be displayed after the main title tag that appears in
|
||||
* the template.
|
||||
*
|
||||
* The following variables are provided for contextual information.
|
||||
* - $node: Node object the comments are attached to.
|
||||
* The constants below the variables show the possible values and should be
|
||||
* used for comparison.
|
||||
* - $display_mode
|
||||
* - COMMENT_MODE_FLAT
|
||||
* - COMMENT_MODE_THREADED
|
||||
*
|
||||
* Other variables:
|
||||
* - $classes_array: Array of html class attribute values. It is flattened
|
||||
* into a string within the variable $classes.
|
||||
*
|
||||
* @see template_preprocess_comment_wrapper()
|
||||
*
|
||||
* @ingroup themeable
|
||||
*/
|
||||
?>
|
||||
<div id="comments" class="<?php print $classes; ?>"<?php print $attributes; ?>>
|
||||
<?php if ($content['comments'] && $node->type != 'forum'): ?>
|
||||
<?php print render($title_prefix); ?>
|
||||
<h2 class="title"><?php print t('Comments'); ?></h2>
|
||||
<?php print render($title_suffix); ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php print render($content['comments']); ?>
|
||||
|
||||
<?php if ($content['comment_form']): ?>
|
||||
<h2 class="title comment-form"><?php print t('Add new comment'); ?></h2>
|
||||
<?php print render($content['comment_form']); ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
284
modules/comment/comment.admin.inc
Normal file
284
modules/comment/comment.admin.inc
Normal file
|
@ -0,0 +1,284 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Admin page callbacks for the comment module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Menu callback; present an administrative comment listing.
|
||||
*/
|
||||
function comment_admin($type = 'new') {
|
||||
$edit = $_POST;
|
||||
|
||||
if (isset($edit['operation']) && ($edit['operation'] == 'delete') && isset($edit['comments']) && $edit['comments']) {
|
||||
return drupal_get_form('comment_multiple_delete_confirm');
|
||||
}
|
||||
else {
|
||||
return drupal_get_form('comment_admin_overview', $type);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Form builder for the comment overview administration form.
|
||||
*
|
||||
* @param $arg
|
||||
* Current path's fourth component: the type of overview form ('approval' or
|
||||
* 'new').
|
||||
*
|
||||
* @ingroup forms
|
||||
* @see comment_admin_overview_validate()
|
||||
* @see comment_admin_overview_submit()
|
||||
* @see theme_comment_admin_overview()
|
||||
*/
|
||||
function comment_admin_overview($form, &$form_state, $arg) {
|
||||
// Build an 'Update options' form.
|
||||
$form['options'] = array(
|
||||
'#type' => 'fieldset',
|
||||
'#title' => t('Update options'),
|
||||
'#attributes' => array('class' => array('container-inline')),
|
||||
);
|
||||
|
||||
if ($arg == 'approval') {
|
||||
$options['publish'] = t('Publish the selected comments');
|
||||
}
|
||||
else {
|
||||
$options['unpublish'] = t('Unpublish the selected comments');
|
||||
}
|
||||
$options['delete'] = t('Delete the selected comments');
|
||||
|
||||
$form['options']['operation'] = array(
|
||||
'#type' => 'select',
|
||||
'#title' => t('Operation'),
|
||||
'#title_display' => 'invisible',
|
||||
'#options' => $options,
|
||||
'#default_value' => 'publish',
|
||||
);
|
||||
$form['options']['submit'] = array(
|
||||
'#type' => 'submit',
|
||||
'#value' => t('Update'),
|
||||
);
|
||||
|
||||
// Load the comments that need to be displayed.
|
||||
$status = ($arg == 'approval') ? COMMENT_NOT_PUBLISHED : COMMENT_PUBLISHED;
|
||||
$header = array(
|
||||
'subject' => array('data' => t('Subject'), 'field' => 'subject'),
|
||||
'author' => array('data' => t('Author'), 'field' => 'name'),
|
||||
'posted_in' => array('data' => t('Posted in'), 'field' => 'node_title'),
|
||||
'changed' => array('data' => t('Updated'), 'field' => 'c.changed', 'sort' => 'desc'),
|
||||
'operations' => array('data' => t('Operations')),
|
||||
);
|
||||
|
||||
$query = db_select('comment', 'c')->extend('PagerDefault')->extend('TableSort');
|
||||
$query->join('node', 'n', 'n.nid = c.nid');
|
||||
$query->addField('n', 'title', 'node_title');
|
||||
$query->addTag('node_access');
|
||||
$result = $query
|
||||
->fields('c', array('cid', 'subject', 'name', 'changed'))
|
||||
->condition('c.status', $status)
|
||||
->limit(50)
|
||||
->orderByHeader($header)
|
||||
->execute();
|
||||
|
||||
$cids = array();
|
||||
|
||||
// We collect a sorted list of node_titles during the query to attach to the
|
||||
// comments later.
|
||||
foreach ($result as $row) {
|
||||
$cids[] = $row->cid;
|
||||
$node_titles[] = $row->node_title;
|
||||
}
|
||||
$comments = comment_load_multiple($cids);
|
||||
|
||||
// Build a table listing the appropriate comments.
|
||||
$options = array();
|
||||
$destination = drupal_get_destination();
|
||||
|
||||
foreach ($comments as $comment) {
|
||||
// Remove the first node title from the node_titles array and attach to
|
||||
// the comment.
|
||||
$comment->node_title = array_shift($node_titles);
|
||||
$comment_body = field_get_items('comment', $comment, 'comment_body');
|
||||
$options[$comment->cid] = array(
|
||||
'subject' => array(
|
||||
'data' => array(
|
||||
'#type' => 'link',
|
||||
'#title' => $comment->subject,
|
||||
'#href' => 'comment/' . $comment->cid,
|
||||
'#options' => array('attributes' => array('title' => truncate_utf8($comment_body[0]['value'], 128)), 'fragment' => 'comment-' . $comment->cid),
|
||||
),
|
||||
),
|
||||
'author' => theme('username', array('account' => $comment)),
|
||||
'posted_in' => array(
|
||||
'data' => array(
|
||||
'#type' => 'link',
|
||||
'#title' => $comment->node_title,
|
||||
'#href' => 'node/' . $comment->nid,
|
||||
),
|
||||
),
|
||||
'changed' => format_date($comment->changed, 'short'),
|
||||
'operations' => array(
|
||||
'data' => array(
|
||||
'#type' => 'link',
|
||||
'#title' => t('edit'),
|
||||
'#href' => 'comment/' . $comment->cid . '/edit',
|
||||
'#options' => array('query' => $destination),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
$form['comments'] = array(
|
||||
'#type' => 'tableselect',
|
||||
'#header' => $header,
|
||||
'#options' => $options,
|
||||
'#empty' => t('No comments available.'),
|
||||
);
|
||||
|
||||
$form['pager'] = array('#theme' => 'pager');
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate comment_admin_overview form submissions.
|
||||
*/
|
||||
function comment_admin_overview_validate($form, &$form_state) {
|
||||
$form_state['values']['comments'] = array_diff($form_state['values']['comments'], array(0));
|
||||
// We can't execute any 'Update options' if no comments were selected.
|
||||
if (count($form_state['values']['comments']) == 0) {
|
||||
form_set_error('', t('Select one or more comments to perform the update on.'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process comment_admin_overview form submissions.
|
||||
*
|
||||
* Execute the chosen 'Update option' on the selected comments, such as
|
||||
* publishing, unpublishing or deleting.
|
||||
*/
|
||||
function comment_admin_overview_submit($form, &$form_state) {
|
||||
$operation = $form_state['values']['operation'];
|
||||
$cids = $form_state['values']['comments'];
|
||||
|
||||
if ($operation == 'delete') {
|
||||
comment_delete_multiple($cids);
|
||||
}
|
||||
else {
|
||||
foreach ($cids as $cid => $value) {
|
||||
$comment = comment_load($value);
|
||||
|
||||
if ($operation == 'unpublish') {
|
||||
$comment->status = COMMENT_NOT_PUBLISHED;
|
||||
}
|
||||
elseif ($operation == 'publish') {
|
||||
$comment->status = COMMENT_PUBLISHED;
|
||||
}
|
||||
comment_save($comment);
|
||||
}
|
||||
}
|
||||
drupal_set_message(t('The update has been performed.'));
|
||||
$form_state['redirect'] = 'admin/content/comment';
|
||||
cache_clear_all();
|
||||
}
|
||||
|
||||
/**
|
||||
* List the selected comments and verify that the admin wants to delete them.
|
||||
*
|
||||
* @param $form_state
|
||||
* An associative array containing the current state of the form.
|
||||
* @return
|
||||
* TRUE if the comments should be deleted, FALSE otherwise.
|
||||
* @ingroup forms
|
||||
* @see comment_multiple_delete_confirm_submit()
|
||||
*/
|
||||
function comment_multiple_delete_confirm($form, &$form_state) {
|
||||
$edit = $form_state['input'];
|
||||
|
||||
$form['comments'] = array(
|
||||
'#prefix' => '<ul>',
|
||||
'#suffix' => '</ul>',
|
||||
'#tree' => TRUE,
|
||||
);
|
||||
// array_filter() returns only elements with actual values.
|
||||
$comment_counter = 0;
|
||||
foreach (array_filter($edit['comments']) as $cid => $value) {
|
||||
$comment = comment_load($cid);
|
||||
if (is_object($comment) && is_numeric($comment->cid)) {
|
||||
$subject = db_query('SELECT subject FROM {comment} WHERE cid = :cid', array(':cid' => $cid))->fetchField();
|
||||
$form['comments'][$cid] = array('#type' => 'hidden', '#value' => $cid, '#prefix' => '<li>', '#suffix' => check_plain($subject) . '</li>');
|
||||
$comment_counter++;
|
||||
}
|
||||
}
|
||||
$form['operation'] = array('#type' => 'hidden', '#value' => 'delete');
|
||||
|
||||
if (!$comment_counter) {
|
||||
drupal_set_message(t('There do not appear to be any comments to delete, or your selected comment was deleted by another administrator.'));
|
||||
drupal_goto('admin/content/comment');
|
||||
}
|
||||
else {
|
||||
return confirm_form($form,
|
||||
t('Are you sure you want to delete these comments and all their children?'),
|
||||
'admin/content/comment', t('This action cannot be undone.'),
|
||||
t('Delete comments'), t('Cancel'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process comment_multiple_delete_confirm form submissions.
|
||||
*/
|
||||
function comment_multiple_delete_confirm_submit($form, &$form_state) {
|
||||
if ($form_state['values']['confirm']) {
|
||||
comment_delete_multiple(array_keys($form_state['values']['comments']));
|
||||
cache_clear_all();
|
||||
$count = count($form_state['values']['comments']);
|
||||
watchdog('content', 'Deleted @count comments.', array('@count' => $count));
|
||||
drupal_set_message(format_plural($count, 'Deleted 1 comment.', 'Deleted @count comments.'));
|
||||
}
|
||||
$form_state['redirect'] = 'admin/content/comment';
|
||||
}
|
||||
|
||||
/**
|
||||
* Page callback for comment deletions.
|
||||
*/
|
||||
function comment_confirm_delete_page($cid) {
|
||||
if ($comment = comment_load($cid)) {
|
||||
return drupal_get_form('comment_confirm_delete', $comment);
|
||||
}
|
||||
return MENU_NOT_FOUND;
|
||||
}
|
||||
|
||||
/**
|
||||
* Form builder; Builds the confirmation form for deleting a single comment.
|
||||
*
|
||||
* @ingroup forms
|
||||
* @see comment_confirm_delete_submit()
|
||||
*/
|
||||
function comment_confirm_delete($form, &$form_state, $comment) {
|
||||
$form['#comment'] = $comment;
|
||||
// Always provide entity id in the same form key as in the entity edit form.
|
||||
$form['cid'] = array('#type' => 'value', '#value' => $comment->cid);
|
||||
return confirm_form(
|
||||
$form,
|
||||
t('Are you sure you want to delete the comment %title?', array('%title' => $comment->subject)),
|
||||
'node/' . $comment->nid,
|
||||
t('Any replies to this comment will be lost. This action cannot be undone.'),
|
||||
t('Delete'),
|
||||
t('Cancel'),
|
||||
'comment_confirm_delete');
|
||||
}
|
||||
|
||||
/**
|
||||
* Process comment_confirm_delete form submissions.
|
||||
*/
|
||||
function comment_confirm_delete_submit($form, &$form_state) {
|
||||
$comment = $form['#comment'];
|
||||
// Delete the comment and its replies.
|
||||
comment_delete($comment->cid);
|
||||
drupal_set_message(t('The comment and all its replies have been deleted.'));
|
||||
watchdog('content', 'Deleted comment @cid and its replies.', array('@cid' => $comment->cid));
|
||||
// Clear the cache so an anonymous user sees that his comment was deleted.
|
||||
cache_clear_all();
|
||||
|
||||
$form_state['redirect'] = "node/$comment->nid";
|
||||
}
|
145
modules/comment/comment.api.php
Normal file
145
modules/comment/comment.api.php
Normal file
|
@ -0,0 +1,145 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Hooks provided by the Comment module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @addtogroup hooks
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* The comment passed validation and is about to be saved.
|
||||
*
|
||||
* Modules may make changes to the comment before it is saved to the database.
|
||||
*
|
||||
* @param $comment
|
||||
* The comment object.
|
||||
*/
|
||||
function hook_comment_presave($comment) {
|
||||
// Remove leading & trailing spaces from the comment subject.
|
||||
$comment->subject = trim($comment->subject);
|
||||
}
|
||||
|
||||
/**
|
||||
* The comment is being inserted.
|
||||
*
|
||||
* @param $comment
|
||||
* The comment object.
|
||||
*/
|
||||
function hook_comment_insert($comment) {
|
||||
// Reindex the node when comments are added.
|
||||
search_touch_node($comment->nid);
|
||||
}
|
||||
|
||||
/**
|
||||
* The comment is being updated.
|
||||
*
|
||||
* @param $comment
|
||||
* The comment object.
|
||||
*/
|
||||
function hook_comment_update($comment) {
|
||||
// Reindex the node when comments are updated.
|
||||
search_touch_node($comment->nid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Comments are being loaded from the database.
|
||||
*
|
||||
* @param $comments
|
||||
* An array of comment objects indexed by cid.
|
||||
*/
|
||||
function hook_comment_load($comments) {
|
||||
$result = db_query('SELECT cid, foo FROM {mytable} WHERE cid IN (:cids)', array(':cids' => array_keys($comments)));
|
||||
foreach ($result as $record) {
|
||||
$comments[$record->cid]->foo = $record->foo;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The comment is being viewed. This hook can be used to add additional data to the comment before theming.
|
||||
*
|
||||
* @param $comment
|
||||
* Passes in the comment the action is being performed on.
|
||||
* @param $view_mode
|
||||
* View mode, e.g. 'full', 'teaser'...
|
||||
* @param $langcode
|
||||
* The language code used for rendering.
|
||||
*
|
||||
* @see hook_entity_view()
|
||||
*/
|
||||
function hook_comment_view($comment, $view_mode, $langcode) {
|
||||
// how old is the comment
|
||||
$comment->time_ago = time() - $comment->changed;
|
||||
}
|
||||
|
||||
/**
|
||||
* The comment was built; the module may modify the structured content.
|
||||
*
|
||||
* This hook is called after the content has been assembled in a structured array
|
||||
* and may be used for doing processing which requires that the complete comment
|
||||
* content structure has been built.
|
||||
*
|
||||
* If the module wishes to act on the rendered HTML of the comment rather than the
|
||||
* structured content array, it may use this hook to add a #post_render callback.
|
||||
* Alternatively, it could also implement hook_preprocess_comment(). See
|
||||
* drupal_render() and theme() documentation respectively for details.
|
||||
*
|
||||
* @param $build
|
||||
* A renderable array representing the comment.
|
||||
*
|
||||
* @see comment_view()
|
||||
* @see hook_entity_view_alter()
|
||||
*/
|
||||
function hook_comment_view_alter(&$build) {
|
||||
// Check for the existence of a field added by another module.
|
||||
if ($build['#view_mode'] == 'full' && isset($build['an_additional_field'])) {
|
||||
// Change its weight.
|
||||
$build['an_additional_field']['#weight'] = -10;
|
||||
}
|
||||
|
||||
// Add a #post_render callback to act on the rendered HTML of the comment.
|
||||
$build['#post_render'][] = 'my_module_comment_post_render';
|
||||
}
|
||||
|
||||
/**
|
||||
* The comment is being published by the moderator.
|
||||
*
|
||||
* @param $comment
|
||||
* Passes in the comment the action is being performed on.
|
||||
* @return
|
||||
* Nothing.
|
||||
*/
|
||||
function hook_comment_publish($comment) {
|
||||
drupal_set_message(t('Comment: @subject has been published', array('@subject' => $comment->subject)));
|
||||
}
|
||||
|
||||
/**
|
||||
* The comment is being unpublished by the moderator.
|
||||
*
|
||||
* @param $comment
|
||||
* Passes in the comment the action is being performed on.
|
||||
* @return
|
||||
* Nothing.
|
||||
*/
|
||||
function hook_comment_unpublish($comment) {
|
||||
drupal_set_message(t('Comment: @subject has been unpublished', array('@subject' => $comment->subject)));
|
||||
}
|
||||
|
||||
/**
|
||||
* The comment is being deleted by the moderator.
|
||||
*
|
||||
* @param $comment
|
||||
* Passes in the comment the action is being performed on.
|
||||
* @return
|
||||
* Nothing.
|
||||
*/
|
||||
function hook_comment_delete($comment) {
|
||||
drupal_set_message(t('Comment: @subject has been deleted', array('@subject' => $comment->subject)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @} End of "addtogroup hooks".
|
||||
*/
|
13
modules/comment/comment.css
Normal file
13
modules/comment/comment.css
Normal file
|
@ -0,0 +1,13 @@
|
|||
|
||||
#comments {
|
||||
margin-top: 15px;
|
||||
}
|
||||
.indented {
|
||||
margin-left: 25px; /* LTR */
|
||||
}
|
||||
.comment-unpublished {
|
||||
background-color: #fff4f4;
|
||||
}
|
||||
.comment-preview {
|
||||
background-color: #ffffea;
|
||||
}
|
16
modules/comment/comment.info
Normal file
16
modules/comment/comment.info
Normal file
|
@ -0,0 +1,16 @@
|
|||
name = Comment
|
||||
description = Allows users to comment on and discuss published content.
|
||||
package = Core
|
||||
version = VERSION
|
||||
core = 7.x
|
||||
dependencies[] = text
|
||||
files[] = comment.module
|
||||
files[] = comment.test
|
||||
configure = admin/content/comment
|
||||
stylesheets[all][] = comment.css
|
||||
|
||||
; Information added by Drupal.org packaging script on 2017-06-21
|
||||
version = "7.56"
|
||||
project = "drupal"
|
||||
datestamp = "1498069849"
|
||||
|
578
modules/comment/comment.install
Normal file
578
modules/comment/comment.install
Normal file
|
@ -0,0 +1,578 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Install, update and uninstall functions for the comment module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Implements hook_uninstall().
|
||||
*/
|
||||
function comment_uninstall() {
|
||||
// Delete comment_body field.
|
||||
field_delete_field('comment_body');
|
||||
|
||||
// Remove variables.
|
||||
variable_del('comment_block_count');
|
||||
$node_types = array_keys(node_type_get_types());
|
||||
foreach ($node_types as $node_type) {
|
||||
field_attach_delete_bundle('comment', 'comment_node_' . $node_type);
|
||||
variable_del('comment_' . $node_type);
|
||||
variable_del('comment_anonymous_' . $node_type);
|
||||
variable_del('comment_controls_' . $node_type);
|
||||
variable_del('comment_default_mode_' . $node_type);
|
||||
variable_del('comment_default_order_' . $node_type);
|
||||
variable_del('comment_default_per_page_' . $node_type);
|
||||
variable_del('comment_form_location_' . $node_type);
|
||||
variable_del('comment_preview_' . $node_type);
|
||||
variable_del('comment_subject_field_' . $node_type);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_enable().
|
||||
*/
|
||||
function comment_enable() {
|
||||
// Insert records into the node_comment_statistics for nodes that are missing.
|
||||
$query = db_select('node', 'n');
|
||||
$query->leftJoin('node_comment_statistics', 'ncs', 'ncs.nid = n.nid');
|
||||
$query->addField('n', 'created', 'last_comment_timestamp');
|
||||
$query->addField('n', 'uid', 'last_comment_uid');
|
||||
$query->addField('n', 'nid');
|
||||
$query->addExpression('0', 'comment_count');
|
||||
$query->addExpression('NULL', 'last_comment_name');
|
||||
$query->isNull('ncs.comment_count');
|
||||
|
||||
db_insert('node_comment_statistics')
|
||||
->from($query)
|
||||
->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_modules_enabled().
|
||||
*
|
||||
* Creates comment body fields for node types existing before the comment module
|
||||
* is enabled. We use hook_modules_enabled() rather than hook_enable() so we can
|
||||
* react to node types of existing modules, and those of modules being enabled
|
||||
* both before and after comment module in the loop of module_enable().
|
||||
*
|
||||
* There is a separate comment bundle for each node type to allow for
|
||||
* per-node-type customization of comment fields. Each one of these bundles
|
||||
* needs a comment body field instance. A comment bundle is needed even for
|
||||
* node types whose comments are disabled by default, because individual nodes
|
||||
* may override that default.
|
||||
*
|
||||
* @see comment_node_type_insert()
|
||||
*/
|
||||
function comment_modules_enabled($modules) {
|
||||
// Only react if comment module is one of the modules being enabled.
|
||||
// hook_node_type_insert() is used to create body fields while the comment
|
||||
// module is enabled.
|
||||
if (in_array('comment', $modules)) {
|
||||
// Ensure that the list of node types reflects newly enabled modules.
|
||||
node_types_rebuild();
|
||||
|
||||
// Create comment body fields for each node type, if needed.
|
||||
foreach (node_type_get_types() as $type => $info) {
|
||||
_comment_body_field_create($info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_update_dependencies().
|
||||
*/
|
||||
function comment_update_dependencies() {
|
||||
// comment_update_7005() creates the comment body field and therefore must
|
||||
// run after all Field modules have been enabled, which happens in
|
||||
// system_update_7027().
|
||||
$dependencies['comment'][7005] = array(
|
||||
'system' => 7027,
|
||||
);
|
||||
|
||||
// comment_update_7006() needs to query the {filter_format} table to get a
|
||||
// list of existing text formats, so it must run after filter_update_7000(),
|
||||
// which creates that table.
|
||||
$dependencies['comment'][7006] = array(
|
||||
'filter' => 7000,
|
||||
);
|
||||
|
||||
return $dependencies;
|
||||
}
|
||||
|
||||
/**
|
||||
* @addtogroup updates-6.x-to-7.x
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* Rename comment display setting variables.
|
||||
*/
|
||||
function comment_update_7000() {
|
||||
$types = _update_7000_node_get_types();
|
||||
foreach ($types as $type => $type_object) {
|
||||
variable_del('comment_default_order' . $type);
|
||||
|
||||
// Drupal 6 had four display modes:
|
||||
// - COMMENT_MODE_FLAT_COLLAPSED = 1
|
||||
// - COMMENT_MODE_FLAT_EXPANDED = 2
|
||||
// - COMMENT_MODE_THREADED_COLLAPSED = 3
|
||||
// - COMMENT_MODE_THREADED_EXPANDED = 4
|
||||
//
|
||||
// Drupal 7 doesn't support collapsed/expanded modes anymore, so we
|
||||
// migrate all the flat modes to COMMENT_MODE_FLAT (0) and all the threaded
|
||||
// modes to COMMENT_MODE_THREADED (1).
|
||||
$setting = variable_get('comment_default_mode_' . $type, 4);
|
||||
if ($setting == 3 || $setting == 4) {
|
||||
variable_set('comment_default_mode_' . $type, 1);
|
||||
}
|
||||
else {
|
||||
variable_set('comment_default_mode_' . $type, 0);
|
||||
}
|
||||
|
||||
// There were only two comment modes in the past:
|
||||
// - 1 was 'required' previously, convert into DRUPAL_REQUIRED (2).
|
||||
// - 0 was 'optional' previously, convert into DRUPAL_OPTIONAL (1).
|
||||
$preview = variable_get('comment_preview_' . $type, 1) ? 2 : 1;
|
||||
variable_set('comment_preview_' . $type, $preview);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change comment status from published being 0 to being 1
|
||||
*/
|
||||
function comment_update_7001() {
|
||||
// Choose a temporary status value different from the existing status values.
|
||||
$tmp_status = db_query('SELECT MAX(status) FROM {comments}')->fetchField() + 1;
|
||||
|
||||
$changes = array(
|
||||
0 => $tmp_status,
|
||||
1 => 0,
|
||||
$tmp_status => 1,
|
||||
);
|
||||
|
||||
foreach ($changes as $old => $new) {
|
||||
db_update('comments')
|
||||
->fields(array('status' => $new))
|
||||
->condition('status', $old)
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename {comments} table to {comment} and upgrade it.
|
||||
*/
|
||||
function comment_update_7002() {
|
||||
db_rename_table('comments', 'comment');
|
||||
|
||||
// Add user-related indexes. These may already exist from Drupal 6.
|
||||
if (!db_index_exists('comment', 'comment_uid')) {
|
||||
db_add_index('comment', 'comment_uid', array('uid'));
|
||||
db_add_index('node_comment_statistics', 'last_comment_uid', array('last_comment_uid'));
|
||||
}
|
||||
|
||||
// Create a language column.
|
||||
db_add_field('comment', 'language', array(
|
||||
'type' => 'varchar',
|
||||
'length' => 12,
|
||||
'not null' => TRUE,
|
||||
'default' => '',
|
||||
));
|
||||
db_add_index('comment', 'comment_nid_language', array('nid', 'language'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Split {comment}.timestamp into 'created' and 'changed', improve indexing on {comment}.
|
||||
*/
|
||||
function comment_update_7003() {
|
||||
// Drop the old indexes.
|
||||
db_drop_index('comment', 'status');
|
||||
db_drop_index('comment', 'pid');
|
||||
|
||||
// Create a created column.
|
||||
db_add_field('comment', 'created', array(
|
||||
'type' => 'int',
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
));
|
||||
|
||||
// Rename the timestamp column to changed.
|
||||
db_change_field('comment', 'timestamp', 'changed', array(
|
||||
'type' => 'int',
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
));
|
||||
|
||||
// Migrate the data.
|
||||
// @todo db_update() should support this.
|
||||
db_query('UPDATE {comment} SET created = changed');
|
||||
|
||||
// Recreate the indexes.
|
||||
// The 'comment_num_new' index is optimized for comment_num_new()
|
||||
// and comment_new_page_count().
|
||||
db_add_index('comment', 'comment_num_new', array('nid', 'status', 'created', 'cid', 'thread'));
|
||||
db_add_index('comment', 'comment_pid_status', array('pid', 'status'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Upgrade the {node_comment_statistics} table.
|
||||
*/
|
||||
function comment_update_7004() {
|
||||
db_add_field('node_comment_statistics', 'cid', array(
|
||||
'type' => 'int',
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
'description' => 'The {comment}.cid of the last comment.',
|
||||
));
|
||||
db_add_index('node_comment_statistics', 'cid', array('cid'));
|
||||
|
||||
// The comment_count index may have been added in Drupal 6.
|
||||
if (!db_index_exists('node_comment_statistics', 'comment_count')) {
|
||||
// Add an index on the comment_count.
|
||||
db_add_index('node_comment_statistics', 'comment_count', array('comment_count'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the comment_body field.
|
||||
*/
|
||||
function comment_update_7005() {
|
||||
// Create comment body field.
|
||||
$field = array(
|
||||
'field_name' => 'comment_body',
|
||||
'type' => 'text_long',
|
||||
'module' => 'text',
|
||||
'entity_types' => array(
|
||||
'comment',
|
||||
),
|
||||
'settings' => array(),
|
||||
'cardinality' => 1,
|
||||
);
|
||||
_update_7000_field_create_field($field);
|
||||
|
||||
// Add the field to comments for all existing bundles.
|
||||
$generic_instance = array(
|
||||
'entity_type' => 'comment',
|
||||
'label' => t('Comment'),
|
||||
'settings' => array(
|
||||
'text_processing' => 1,
|
||||
),
|
||||
'required' => TRUE,
|
||||
'display' => array(
|
||||
'default' => array(
|
||||
'label' => 'hidden',
|
||||
'type' => 'text_default',
|
||||
'weight' => 0,
|
||||
'settings' => array(),
|
||||
'module' => 'text',
|
||||
),
|
||||
),
|
||||
'widget' => array(
|
||||
'type' => 'text_textarea',
|
||||
'settings' => array(
|
||||
'rows' => 5,
|
||||
),
|
||||
'weight' => 0,
|
||||
'module' => 'text',
|
||||
),
|
||||
'description' => '',
|
||||
);
|
||||
|
||||
$types = _update_7000_node_get_types();
|
||||
foreach ($types as $type => $type_object) {
|
||||
$instance = $generic_instance;
|
||||
$instance['bundle'] = 'comment_node_' . $type;
|
||||
_update_7000_field_create_instance($field, $instance);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate data from the comment field to field storage.
|
||||
*/
|
||||
function comment_update_7006(&$sandbox) {
|
||||
// This is a multipass update. First set up some comment variables.
|
||||
if (empty($sandbox['total'])) {
|
||||
$comments = (bool) db_query_range('SELECT 1 FROM {comment}', 0, 1)->fetchField();
|
||||
$sandbox['types'] = array();
|
||||
if ($comments) {
|
||||
$sandbox['types'] = array_keys(_update_7000_node_get_types());
|
||||
}
|
||||
$sandbox['total'] = count($sandbox['types']);
|
||||
}
|
||||
|
||||
if (!empty($sandbox['types'])) {
|
||||
$type = array_shift($sandbox['types']);
|
||||
|
||||
$query = db_select('comment', 'c');
|
||||
$query->innerJoin('node', 'n', 'c.nid = n.nid AND n.type = :type', array(':type' => $type));
|
||||
$query->addField('c', 'cid', 'entity_id');
|
||||
$query->addExpression("'comment_node_$type'", 'bundle');
|
||||
$query->addExpression("'comment'", 'entity_type');
|
||||
$query->addExpression('0', 'deleted');
|
||||
$query->addExpression("'" . LANGUAGE_NONE . "'", 'language');
|
||||
$query->addExpression('0', 'delta');
|
||||
$query->addField('c', 'comment', 'comment_body_value');
|
||||
$query->addField('c', 'format', 'comment_body_format');
|
||||
|
||||
db_insert('field_data_comment_body')
|
||||
->from($query)
|
||||
->execute();
|
||||
|
||||
$sandbox['#finished'] = 1 - count($sandbox['types']) / $sandbox['total'];
|
||||
}
|
||||
|
||||
// On the last pass of the update, $sandbox['types'] will be empty.
|
||||
if (empty($sandbox['types'])) {
|
||||
// Update the comment body text formats. For an explanation of these
|
||||
// updates, see the code comments in user_update_7010().
|
||||
db_update('field_data_comment_body')
|
||||
->fields(array('comment_body_format' => NULL))
|
||||
->condition('comment_body_value', '')
|
||||
->condition('comment_body_format', 0)
|
||||
->execute();
|
||||
$existing_formats = db_query("SELECT format FROM {filter_format}")->fetchCol();
|
||||
$default_format = variable_get('filter_default_format', 1);
|
||||
db_update('field_data_comment_body')
|
||||
->fields(array('comment_body_format' => $default_format))
|
||||
->isNotNull('comment_body_format')
|
||||
->condition('comment_body_format', $existing_formats, 'NOT IN')
|
||||
->execute();
|
||||
|
||||
// Finally, remove the old comment data.
|
||||
db_drop_field('comment', 'comment');
|
||||
db_drop_field('comment', 'format');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @} End of "addtogroup updates-6.x-to-7.x".
|
||||
*/
|
||||
|
||||
/**
|
||||
* @addtogroup updates-7.x-extra
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* Add an index to the created column.
|
||||
*/
|
||||
function comment_update_7007() {
|
||||
db_add_index('comment', 'comment_created', array('created'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update database to match Drupal 7 schema.
|
||||
*/
|
||||
function comment_update_7008() {
|
||||
// Update default status to 1.
|
||||
db_change_field('comment', 'status', 'status', array(
|
||||
'type' => 'int',
|
||||
'unsigned' => TRUE,
|
||||
'not null' => TRUE,
|
||||
'default' => 1,
|
||||
'size' => 'tiny',
|
||||
));
|
||||
|
||||
// Realign indexes.
|
||||
db_drop_index('comment', 'comment_status_pid');
|
||||
db_add_index('comment', 'comment_status_pid', array('pid', 'status'));
|
||||
db_drop_index('comment', 'comment_pid_status');
|
||||
db_drop_index('comment', 'nid');
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the last_comment_timestamp column description.
|
||||
*/
|
||||
function comment_update_7009() {
|
||||
db_change_field('node_comment_statistics', 'last_comment_timestamp', 'last_comment_timestamp', array(
|
||||
'type' => 'int',
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
'description' => 'The Unix timestamp of the last comment that was posted within this node, from {comment}.changed.',
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @} End of "addtogroup updates-7.x-extra".
|
||||
*/
|
||||
|
||||
/**
|
||||
* Implements hook_schema().
|
||||
*/
|
||||
function comment_schema() {
|
||||
$schema['comment'] = array(
|
||||
'description' => 'Stores comments and associated data.',
|
||||
'fields' => array(
|
||||
'cid' => array(
|
||||
'type' => 'serial',
|
||||
'not null' => TRUE,
|
||||
'description' => 'Primary Key: Unique comment ID.',
|
||||
),
|
||||
'pid' => array(
|
||||
'type' => 'int',
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
'description' => 'The {comment}.cid to which this comment is a reply. If set to 0, this comment is not a reply to an existing comment.',
|
||||
),
|
||||
'nid' => array(
|
||||
'type' => 'int',
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
'description' => 'The {node}.nid to which this comment is a reply.',
|
||||
),
|
||||
'uid' => array(
|
||||
'type' => 'int',
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
'description' => 'The {users}.uid who authored the comment. If set to 0, this comment was created by an anonymous user.',
|
||||
),
|
||||
'subject' => array(
|
||||
'type' => 'varchar',
|
||||
'length' => 64,
|
||||
'not null' => TRUE,
|
||||
'default' => '',
|
||||
'description' => 'The comment title.',
|
||||
),
|
||||
'hostname' => array(
|
||||
'type' => 'varchar',
|
||||
'length' => 128,
|
||||
'not null' => TRUE,
|
||||
'default' => '',
|
||||
'description' => "The author's host name.",
|
||||
),
|
||||
'created' => array(
|
||||
'type' => 'int',
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
'description' => 'The time that the comment was created, as a Unix timestamp.',
|
||||
),
|
||||
'changed' => array(
|
||||
'type' => 'int',
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
'description' => 'The time that the comment was last edited, as a Unix timestamp.',
|
||||
),
|
||||
'status' => array(
|
||||
'type' => 'int',
|
||||
'unsigned' => TRUE,
|
||||
'not null' => TRUE,
|
||||
'default' => 1,
|
||||
'size' => 'tiny',
|
||||
'description' => 'The published status of a comment. (0 = Not Published, 1 = Published)',
|
||||
),
|
||||
'thread' => array(
|
||||
'type' => 'varchar',
|
||||
'length' => 255,
|
||||
'not null' => TRUE,
|
||||
'description' => "The vancode representation of the comment's place in a thread.",
|
||||
),
|
||||
'name' => array(
|
||||
'type' => 'varchar',
|
||||
'length' => 60,
|
||||
'not null' => FALSE,
|
||||
'description' => "The comment author's name. Uses {users}.name if the user is logged in, otherwise uses the value typed into the comment form.",
|
||||
),
|
||||
'mail' => array(
|
||||
'type' => 'varchar',
|
||||
'length' => 64,
|
||||
'not null' => FALSE,
|
||||
'description' => "The comment author's e-mail address from the comment form, if user is anonymous, and the 'Anonymous users may/must leave their contact information' setting is turned on.",
|
||||
),
|
||||
'homepage' => array(
|
||||
'type' => 'varchar',
|
||||
'length' => 255,
|
||||
'not null' => FALSE,
|
||||
'description' => "The comment author's home page address from the comment form, if user is anonymous, and the 'Anonymous users may/must leave their contact information' setting is turned on.",
|
||||
),
|
||||
'language' => array(
|
||||
'description' => 'The {languages}.language of this comment.',
|
||||
'type' => 'varchar',
|
||||
'length' => 12,
|
||||
'not null' => TRUE,
|
||||
'default' => '',
|
||||
),
|
||||
),
|
||||
'indexes' => array(
|
||||
'comment_status_pid' => array('pid', 'status'),
|
||||
'comment_num_new' => array('nid', 'status', 'created', 'cid', 'thread'),
|
||||
'comment_uid' => array('uid'),
|
||||
'comment_nid_language' => array('nid', 'language'),
|
||||
'comment_created' => array('created'),
|
||||
),
|
||||
'primary key' => array('cid'),
|
||||
'foreign keys' => array(
|
||||
'comment_node' => array(
|
||||
'table' => 'node',
|
||||
'columns' => array('nid' => 'nid'),
|
||||
),
|
||||
'comment_author' => array(
|
||||
'table' => 'users',
|
||||
'columns' => array('uid' => 'uid'),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
$schema['node_comment_statistics'] = array(
|
||||
'description' => 'Maintains statistics of node and comments posts to show "new" and "updated" flags.',
|
||||
'fields' => array(
|
||||
'nid' => array(
|
||||
'type' => 'int',
|
||||
'unsigned' => TRUE,
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
'description' => 'The {node}.nid for which the statistics are compiled.',
|
||||
),
|
||||
'cid' => array(
|
||||
'type' => 'int',
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
'description' => 'The {comment}.cid of the last comment.',
|
||||
),
|
||||
'last_comment_timestamp' => array(
|
||||
'type' => 'int',
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
'description' => 'The Unix timestamp of the last comment that was posted within this node, from {comment}.changed.',
|
||||
),
|
||||
'last_comment_name' => array(
|
||||
'type' => 'varchar',
|
||||
'length' => 60,
|
||||
'not null' => FALSE,
|
||||
'description' => 'The name of the latest author to post a comment on this node, from {comment}.name.',
|
||||
),
|
||||
'last_comment_uid' => array(
|
||||
'type' => 'int',
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
'description' => 'The user ID of the latest author to post a comment on this node, from {comment}.uid.',
|
||||
),
|
||||
'comment_count' => array(
|
||||
'type' => 'int',
|
||||
'unsigned' => TRUE,
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
'description' => 'The total number of comments on this node.',
|
||||
),
|
||||
),
|
||||
'primary key' => array('nid'),
|
||||
'indexes' => array(
|
||||
'node_comment_timestamp' => array('last_comment_timestamp'),
|
||||
'comment_count' => array('comment_count'),
|
||||
'last_comment_uid' => array('last_comment_uid'),
|
||||
),
|
||||
'foreign keys' => array(
|
||||
'statistics_node' => array(
|
||||
'table' => 'node',
|
||||
'columns' => array('nid' => 'nid'),
|
||||
),
|
||||
'last_comment_author' => array(
|
||||
'table' => 'users',
|
||||
'columns' => array(
|
||||
'last_comment_uid' => 'uid',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return $schema;
|
||||
}
|
2746
modules/comment/comment.module
Normal file
2746
modules/comment/comment.module
Normal file
File diff suppressed because it is too large
Load diff
123
modules/comment/comment.pages.inc
Normal file
123
modules/comment/comment.pages.inc
Normal file
|
@ -0,0 +1,123 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* User page callbacks for the comment module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This function is responsible for generating a comment reply form.
|
||||
* There are several cases that have to be handled, including:
|
||||
* - replies to comments
|
||||
* - replies to nodes
|
||||
* - attempts to reply to nodes that can no longer accept comments
|
||||
* - respecting access permissions ('access comments', 'post comments', etc.)
|
||||
*
|
||||
* The node or comment that is being replied to must appear above the comment
|
||||
* form to provide the user context while authoring the comment.
|
||||
*
|
||||
* @param $node
|
||||
* Every comment belongs to a node. This is that node.
|
||||
*
|
||||
* @param $pid
|
||||
* Some comments are replies to other comments. In those cases, $pid is the parent
|
||||
* comment's cid.
|
||||
*
|
||||
* @return array
|
||||
* An associative array containing:
|
||||
* - An array for rendering the node or parent comment.
|
||||
* - comment_node: If the comment is a reply to the node.
|
||||
* - comment_parent: If the comment is a reply to another comment.
|
||||
* - comment_form: The comment form as a renderable array.
|
||||
*/
|
||||
function comment_reply($node, $pid = NULL) {
|
||||
// Set the breadcrumb trail.
|
||||
drupal_set_breadcrumb(array(l(t('Home'), NULL), l($node->title, 'node/' . $node->nid)));
|
||||
$op = isset($_POST['op']) ? $_POST['op'] : '';
|
||||
$build = array();
|
||||
|
||||
// The user is previewing a comment prior to submitting it.
|
||||
if ($op == t('Preview')) {
|
||||
if (user_access('post comments')) {
|
||||
$build['comment_form'] = drupal_get_form("comment_node_{$node->type}_form", (object) array('pid' => $pid, 'nid' => $node->nid));
|
||||
}
|
||||
else {
|
||||
drupal_set_message(t('You are not authorized to post comments.'), 'error');
|
||||
drupal_goto("node/$node->nid");
|
||||
}
|
||||
}
|
||||
else {
|
||||
// $pid indicates that this is a reply to a comment.
|
||||
if ($pid) {
|
||||
if (user_access('access comments')) {
|
||||
// Load the comment whose cid = $pid
|
||||
$comment = db_query('SELECT c.*, u.uid, u.name AS registered_name, u.signature, u.signature_format, u.picture, u.data FROM {comment} c INNER JOIN {users} u ON c.uid = u.uid WHERE c.cid = :cid AND c.status = :status', array(
|
||||
':cid' => $pid,
|
||||
':status' => COMMENT_PUBLISHED,
|
||||
))->fetchObject();
|
||||
if ($comment) {
|
||||
// If that comment exists, make sure that the current comment and the
|
||||
// parent comment both belong to the same parent node.
|
||||
if ($comment->nid != $node->nid) {
|
||||
// Attempting to reply to a comment not belonging to the current nid.
|
||||
drupal_set_message(t('The comment you are replying to does not exist.'), 'error');
|
||||
drupal_goto("node/$node->nid");
|
||||
}
|
||||
// Display the parent comment
|
||||
$comment->node_type = 'comment_node_' . $node->type;
|
||||
field_attach_load('comment', array($comment->cid => $comment));
|
||||
$comment->name = $comment->uid ? $comment->registered_name : $comment->name;
|
||||
$build['comment_parent'] = comment_view($comment, $node);
|
||||
}
|
||||
else {
|
||||
drupal_set_message(t('The comment you are replying to does not exist.'), 'error');
|
||||
drupal_goto("node/$node->nid");
|
||||
}
|
||||
}
|
||||
else {
|
||||
drupal_set_message(t('You are not authorized to view comments.'), 'error');
|
||||
drupal_goto("node/$node->nid");
|
||||
}
|
||||
}
|
||||
// This is the case where the comment is in response to a node. Display the node.
|
||||
elseif (user_access('access content')) {
|
||||
$build['comment_node'] = node_view($node);
|
||||
}
|
||||
|
||||
// Should we show the reply box?
|
||||
if ($node->comment != COMMENT_NODE_OPEN) {
|
||||
drupal_set_message(t("This discussion is closed: you can't post new comments."), 'error');
|
||||
drupal_goto("node/$node->nid");
|
||||
}
|
||||
elseif (user_access('post comments')) {
|
||||
$edit = array('nid' => $node->nid, 'pid' => $pid);
|
||||
$build['comment_form'] = drupal_get_form("comment_node_{$node->type}_form", (object) $edit);
|
||||
}
|
||||
else {
|
||||
drupal_set_message(t('You are not authorized to post comments.'), 'error');
|
||||
drupal_goto("node/$node->nid");
|
||||
}
|
||||
}
|
||||
|
||||
return $build;
|
||||
}
|
||||
|
||||
/**
|
||||
* Menu callback; publish specified comment.
|
||||
*
|
||||
* @param $cid
|
||||
* A comment identifier.
|
||||
*/
|
||||
function comment_approve($cid) {
|
||||
if (!isset($_GET['token']) || !drupal_valid_token($_GET['token'], "comment/$cid/approve")) {
|
||||
return MENU_ACCESS_DENIED;
|
||||
}
|
||||
if ($comment = comment_load($cid)) {
|
||||
$comment->status = COMMENT_PUBLISHED;
|
||||
comment_save($comment);
|
||||
|
||||
drupal_set_message(t('Comment approved.'));
|
||||
drupal_goto('node/' . $comment->nid);
|
||||
}
|
||||
return MENU_NOT_FOUND;
|
||||
}
|
2266
modules/comment/comment.test
Normal file
2266
modules/comment/comment.test
Normal file
File diff suppressed because it is too large
Load diff
243
modules/comment/comment.tokens.inc
Normal file
243
modules/comment/comment.tokens.inc
Normal file
|
@ -0,0 +1,243 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Builds placeholder replacement tokens for comment-related data.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Implements hook_token_info().
|
||||
*/
|
||||
function comment_token_info() {
|
||||
$type = array(
|
||||
'name' => t('Comments'),
|
||||
'description' => t('Tokens for comments posted on the site.'),
|
||||
'needs-data' => 'comment',
|
||||
);
|
||||
|
||||
// Comment-related tokens for nodes
|
||||
$node['comment-count'] = array(
|
||||
'name' => t("Comment count"),
|
||||
'description' => t("The number of comments posted on a node."),
|
||||
);
|
||||
$node['comment-count-new'] = array(
|
||||
'name' => t("New comment count"),
|
||||
'description' => t("The number of comments posted on a node since the reader last viewed it."),
|
||||
);
|
||||
|
||||
// Core comment tokens
|
||||
$comment['cid'] = array(
|
||||
'name' => t("Comment ID"),
|
||||
'description' => t("The unique ID of the comment."),
|
||||
);
|
||||
$comment['hostname'] = array(
|
||||
'name' => t("IP Address"),
|
||||
'description' => t("The IP address of the computer the comment was posted from."),
|
||||
);
|
||||
$comment['name'] = array(
|
||||
'name' => t("Name"),
|
||||
'description' => t("The name left by the comment author."),
|
||||
);
|
||||
$comment['mail'] = array(
|
||||
'name' => t("Email address"),
|
||||
'description' => t("The email address left by the comment author."),
|
||||
);
|
||||
$comment['homepage'] = array(
|
||||
'name' => t("Home page"),
|
||||
'description' => t("The home page URL left by the comment author."),
|
||||
);
|
||||
$comment['title'] = array(
|
||||
'name' => t("Title"),
|
||||
'description' => t("The title of the comment."),
|
||||
);
|
||||
$comment['body'] = array(
|
||||
'name' => t("Content"),
|
||||
'description' => t("The formatted content of the comment itself."),
|
||||
);
|
||||
$comment['url'] = array(
|
||||
'name' => t("URL"),
|
||||
'description' => t("The URL of the comment."),
|
||||
);
|
||||
$comment['edit-url'] = array(
|
||||
'name' => t("Edit URL"),
|
||||
'description' => t("The URL of the comment's edit page."),
|
||||
);
|
||||
|
||||
// Chained tokens for comments
|
||||
$comment['created'] = array(
|
||||
'name' => t("Date created"),
|
||||
'description' => t("The date the comment was posted."),
|
||||
'type' => 'date',
|
||||
);
|
||||
$comment['changed'] = array(
|
||||
'name' => t("Date changed"),
|
||||
'description' => t("The date the comment was most recently updated."),
|
||||
'type' => 'date',
|
||||
);
|
||||
$comment['parent'] = array(
|
||||
'name' => t("Parent"),
|
||||
'description' => t("The comment's parent, if comment threading is active."),
|
||||
'type' => 'comment',
|
||||
);
|
||||
$comment['node'] = array(
|
||||
'name' => t("Node"),
|
||||
'description' => t("The node the comment was posted to."),
|
||||
'type' => 'node',
|
||||
);
|
||||
$comment['author'] = array(
|
||||
'name' => t("Author"),
|
||||
'description' => t("The author of the comment, if they were logged in."),
|
||||
'type' => 'user',
|
||||
);
|
||||
|
||||
return array(
|
||||
'types' => array('comment' => $type),
|
||||
'tokens' => array(
|
||||
'node' => $node,
|
||||
'comment' => $comment,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_tokens().
|
||||
*/
|
||||
function comment_tokens($type, $tokens, array $data = array(), array $options = array()) {
|
||||
$url_options = array('absolute' => TRUE);
|
||||
if (isset($options['language'])) {
|
||||
$url_options['language'] = $options['language'];
|
||||
$language_code = $options['language']->language;
|
||||
}
|
||||
else {
|
||||
$language_code = NULL;
|
||||
}
|
||||
$sanitize = !empty($options['sanitize']);
|
||||
|
||||
$replacements = array();
|
||||
|
||||
if ($type == 'comment' && !empty($data['comment'])) {
|
||||
$comment = $data['comment'];
|
||||
|
||||
foreach ($tokens as $name => $original) {
|
||||
switch ($name) {
|
||||
// Simple key values on the comment.
|
||||
case 'cid':
|
||||
$replacements[$original] = $comment->cid;
|
||||
break;
|
||||
|
||||
// Poster identity information for comments
|
||||
case 'hostname':
|
||||
$replacements[$original] = $sanitize ? check_plain($comment->hostname) : $comment->hostname;
|
||||
break;
|
||||
|
||||
case 'name':
|
||||
$name = ($comment->uid == 0) ? variable_get('anonymous', t('Anonymous')) : $comment->name;
|
||||
$replacements[$original] = $sanitize ? filter_xss($name) : $name;
|
||||
break;
|
||||
|
||||
case 'mail':
|
||||
if ($comment->uid != 0) {
|
||||
$account = user_load($comment->uid);
|
||||
$mail = $account->mail;
|
||||
}
|
||||
else {
|
||||
$mail = $comment->mail;
|
||||
}
|
||||
$replacements[$original] = $sanitize ? check_plain($mail) : $mail;
|
||||
break;
|
||||
|
||||
case 'homepage':
|
||||
$replacements[$original] = $sanitize ? check_url($comment->homepage) : $comment->homepage;
|
||||
break;
|
||||
|
||||
case 'title':
|
||||
$replacements[$original] = $sanitize ? filter_xss($comment->subject) : $comment->subject;
|
||||
break;
|
||||
|
||||
case 'body':
|
||||
if ($items = field_get_items('comment', $comment, 'comment_body', $language_code)) {
|
||||
$instance = field_info_instance('comment', 'body', 'comment_body');
|
||||
$field_langcode = field_language('comment', $comment, 'comment_body', $language_code);
|
||||
$replacements[$original] = $sanitize ? _text_sanitize($instance, $field_langcode, $items[0], 'value') : $items[0]['value'];
|
||||
}
|
||||
break;
|
||||
|
||||
// Comment related URLs.
|
||||
case 'url':
|
||||
$url_options['fragment'] = 'comment-' . $comment->cid;
|
||||
$replacements[$original] = url('comment/' . $comment->cid, $url_options);
|
||||
break;
|
||||
|
||||
case 'edit-url':
|
||||
$url_options['fragment'] = NULL;
|
||||
$replacements[$original] = url('comment/' . $comment->cid . '/edit', $url_options);
|
||||
break;
|
||||
|
||||
// Default values for the chained tokens handled below.
|
||||
case 'author':
|
||||
$replacements[$original] = $sanitize ? filter_xss($comment->name) : $comment->name;
|
||||
break;
|
||||
|
||||
case 'parent':
|
||||
if (!empty($comment->pid)) {
|
||||
$parent = comment_load($comment->pid);
|
||||
$replacements[$original] = $sanitize ? filter_xss($parent->subject) : $parent->subject;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'created':
|
||||
$replacements[$original] = format_date($comment->created, 'medium', '', NULL, $language_code);
|
||||
break;
|
||||
|
||||
case 'changed':
|
||||
$replacements[$original] = format_date($comment->changed, 'medium', '', NULL, $language_code);
|
||||
break;
|
||||
|
||||
case 'node':
|
||||
$node = node_load($comment->nid);
|
||||
$title = $node->title;
|
||||
$replacements[$original] = $sanitize ? filter_xss($title) : $title;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Chained token relationships.
|
||||
if ($node_tokens = token_find_with_prefix($tokens, 'node')) {
|
||||
$node = node_load($comment->nid);
|
||||
$replacements += token_generate('node', $node_tokens, array('node' => $node), $options);
|
||||
}
|
||||
|
||||
if ($date_tokens = token_find_with_prefix($tokens, 'created')) {
|
||||
$replacements += token_generate('date', $date_tokens, array('date' => $comment->created), $options);
|
||||
}
|
||||
|
||||
if ($date_tokens = token_find_with_prefix($tokens, 'changed')) {
|
||||
$replacements += token_generate('date', $date_tokens, array('date' => $comment->changed), $options);
|
||||
}
|
||||
|
||||
if (($parent_tokens = token_find_with_prefix($tokens, 'parent')) && $parent = comment_load($comment->pid)) {
|
||||
$replacements += token_generate('comment', $parent_tokens, array('comment' => $parent), $options);
|
||||
}
|
||||
|
||||
if (($author_tokens = token_find_with_prefix($tokens, 'author')) && $account = user_load($comment->uid)) {
|
||||
$replacements += token_generate('user', $author_tokens, array('user' => $account), $options);
|
||||
}
|
||||
}
|
||||
elseif ($type == 'node' & !empty($data['node'])) {
|
||||
$node = $data['node'];
|
||||
|
||||
foreach ($tokens as $name => $original) {
|
||||
switch($name) {
|
||||
case 'comment-count':
|
||||
$replacements[$original] = $node->comment_count;
|
||||
break;
|
||||
|
||||
case 'comment-count-new':
|
||||
$replacements[$original] = comment_num_new($node->nid);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $replacements;
|
||||
}
|
92
modules/comment/comment.tpl.php
Normal file
92
modules/comment/comment.tpl.php
Normal file
|
@ -0,0 +1,92 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Default theme implementation for comments.
|
||||
*
|
||||
* Available variables:
|
||||
* - $author: Comment author. Can be link or plain text.
|
||||
* - $content: An array of comment items. Use render($content) to print them all, or
|
||||
* print a subset such as render($content['field_example']). Use
|
||||
* hide($content['field_example']) to temporarily suppress the printing of a
|
||||
* given element.
|
||||
* - $created: Formatted date and time for when the comment was created.
|
||||
* Preprocess functions can reformat it by calling format_date() with the
|
||||
* desired parameters on the $comment->created variable.
|
||||
* - $changed: Formatted date and time for when the comment was last changed.
|
||||
* Preprocess functions can reformat it by calling format_date() with the
|
||||
* desired parameters on the $comment->changed variable.
|
||||
* - $new: New comment marker.
|
||||
* - $permalink: Comment permalink.
|
||||
* - $submitted: Submission information created from $author and $created during
|
||||
* template_preprocess_comment().
|
||||
* - $picture: Authors picture.
|
||||
* - $signature: Authors signature.
|
||||
* - $status: Comment status. Possible values are:
|
||||
* comment-unpublished, comment-published or comment-preview.
|
||||
* - $title: Linked title.
|
||||
* - $classes: String of classes that can be used to style contextually through
|
||||
* CSS. It can be manipulated through the variable $classes_array from
|
||||
* preprocess functions. The default values can be one or more of the following:
|
||||
* - comment: The current template type, i.e., "theming hook".
|
||||
* - comment-by-anonymous: Comment by an unregistered user.
|
||||
* - comment-by-node-author: Comment by the author of the parent node.
|
||||
* - comment-preview: When previewing a new or edited comment.
|
||||
* The following applies only to viewers who are registered users:
|
||||
* - comment-unpublished: An unpublished comment visible only to administrators.
|
||||
* - comment-by-viewer: Comment by the user currently viewing the page.
|
||||
* - comment-new: New comment since last the visit.
|
||||
* - $title_prefix (array): An array containing additional output populated by
|
||||
* modules, intended to be displayed in front of the main title tag that
|
||||
* appears in the template.
|
||||
* - $title_suffix (array): An array containing additional output populated by
|
||||
* modules, intended to be displayed after the main title tag that appears in
|
||||
* the template.
|
||||
*
|
||||
* These two variables are provided for context:
|
||||
* - $comment: Full comment object.
|
||||
* - $node: Node object the comments are attached to.
|
||||
*
|
||||
* Other variables:
|
||||
* - $classes_array: Array of html class attribute values. It is flattened
|
||||
* into a string within the variable $classes.
|
||||
*
|
||||
* @see template_preprocess()
|
||||
* @see template_preprocess_comment()
|
||||
* @see template_process()
|
||||
* @see theme_comment()
|
||||
*
|
||||
* @ingroup themeable
|
||||
*/
|
||||
?>
|
||||
<div class="<?php print $classes; ?> clearfix"<?php print $attributes; ?>>
|
||||
<?php print $picture ?>
|
||||
|
||||
<?php if ($new): ?>
|
||||
<span class="new"><?php print $new ?></span>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php print render($title_prefix); ?>
|
||||
<h3<?php print $title_attributes; ?>><?php print $title ?></h3>
|
||||
<?php print render($title_suffix); ?>
|
||||
|
||||
<div class="submitted">
|
||||
<?php print $permalink; ?>
|
||||
<?php print $submitted; ?>
|
||||
</div>
|
||||
|
||||
<div class="content"<?php print $content_attributes; ?>>
|
||||
<?php
|
||||
// We hide the comments and links now so that we can render them later.
|
||||
hide($content['links']);
|
||||
print render($content);
|
||||
?>
|
||||
<?php if ($signature): ?>
|
||||
<div class="user-signature clearfix">
|
||||
<?php print $signature ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php print render($content['links']) ?>
|
||||
</div>
|
228
modules/contact/contact.admin.inc
Normal file
228
modules/contact/contact.admin.inc
Normal file
|
@ -0,0 +1,228 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Admin page callbacks for the Contact module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Categories/list tab.
|
||||
*/
|
||||
function contact_category_list() {
|
||||
$header = array(
|
||||
t('Category'),
|
||||
t('Recipients'),
|
||||
t('Selected'),
|
||||
array('data' => t('Operations'), 'colspan' => 2),
|
||||
);
|
||||
$rows = array();
|
||||
|
||||
// Get all the contact categories from the database.
|
||||
$categories = db_select('contact', 'c')
|
||||
->addTag('translatable')
|
||||
->fields('c', array('cid', 'category', 'recipients', 'selected'))
|
||||
->orderBy('weight')
|
||||
->orderBy('category')
|
||||
->execute()
|
||||
->fetchAll();
|
||||
|
||||
// Loop through the categories and add them to the table.
|
||||
foreach ($categories as $category) {
|
||||
$rows[] = array(
|
||||
check_plain($category->category),
|
||||
check_plain($category->recipients),
|
||||
($category->selected ? t('Yes') : t('No')),
|
||||
l(t('Edit'), 'admin/structure/contact/edit/' . $category->cid),
|
||||
l(t('Delete'), 'admin/structure/contact/delete/' . $category->cid),
|
||||
);
|
||||
}
|
||||
|
||||
if (!$rows) {
|
||||
$rows[] = array(array(
|
||||
'data' => t('No categories available.'),
|
||||
'colspan' => 5,
|
||||
));
|
||||
}
|
||||
|
||||
$build['category_table'] = array(
|
||||
'#theme' => 'table',
|
||||
'#header' => $header,
|
||||
'#rows' => $rows,
|
||||
);
|
||||
return $build;
|
||||
}
|
||||
|
||||
/**
|
||||
* Form constructor for the category edit form.
|
||||
*
|
||||
* @param $category
|
||||
* An array describing the category to be edited. May be empty for new
|
||||
* categories. Recognized array keys are:
|
||||
* - category: The name of the category.
|
||||
* - recipients: A comma-separated list of recipients.
|
||||
* - reply: (optional) The body of the auto-reply message.
|
||||
* - weight: The weight of the category.
|
||||
* - selected: Boolean indicating whether the category should be selected by
|
||||
* default.
|
||||
* - cid: The category ID for which the form is to be displayed.
|
||||
*
|
||||
* @see contact_category_edit_form_validate()
|
||||
* @see contact_category_edit_form_submit()
|
||||
* @ingroup forms
|
||||
*/
|
||||
function contact_category_edit_form($form, &$form_state, array $category = array()) {
|
||||
// If this is a new category, add the default values.
|
||||
$category += array(
|
||||
'category' => '',
|
||||
'recipients' => '',
|
||||
'reply' => '',
|
||||
'weight' => 0,
|
||||
'selected' => 0,
|
||||
'cid' => NULL,
|
||||
);
|
||||
|
||||
$form['category'] = array(
|
||||
'#type' => 'textfield',
|
||||
'#title' => t('Category'),
|
||||
'#maxlength' => 255,
|
||||
'#default_value' => $category['category'],
|
||||
'#description' => t("Example: 'website feedback' or 'product information'."),
|
||||
'#required' => TRUE,
|
||||
);
|
||||
$form['recipients'] = array(
|
||||
'#type' => 'textarea',
|
||||
'#title' => t('Recipients'),
|
||||
'#default_value' => $category['recipients'],
|
||||
'#description' => t("Example: 'webmaster@example.com' or 'sales@example.com,support@example.com' . To specify multiple recipients, separate each e-mail address with a comma."),
|
||||
'#required' => TRUE,
|
||||
);
|
||||
$form['reply'] = array(
|
||||
'#type' => 'textarea',
|
||||
'#title' => t('Auto-reply'),
|
||||
'#default_value' => $category['reply'],
|
||||
'#description' => t('Optional auto-reply. Leave empty if you do not want to send the user an auto-reply message.'),
|
||||
);
|
||||
$form['weight'] = array(
|
||||
'#type' => 'weight',
|
||||
'#title' => t('Weight'),
|
||||
'#default_value' => $category['weight'],
|
||||
'#description' => t('When listing categories, those with lighter (smaller) weights get listed before categories with heavier (larger) weights. Categories with equal weights are sorted alphabetically.'),
|
||||
);
|
||||
$form['selected'] = array(
|
||||
'#type' => 'select',
|
||||
'#title' => t('Selected'),
|
||||
'#options' => array(
|
||||
0 => t('No'),
|
||||
1 => t('Yes'),
|
||||
),
|
||||
'#default_value' => $category['selected'],
|
||||
'#description' => t('Set this to <em>Yes</em> if you would like this category to be selected by default.'),
|
||||
);
|
||||
$form['cid'] = array(
|
||||
'#type' => 'value',
|
||||
'#value' => $category['cid'],
|
||||
);
|
||||
$form['actions'] = array('#type' => 'actions');
|
||||
$form['actions']['submit'] = array(
|
||||
'#type' => 'submit',
|
||||
'#value' => t('Save'),
|
||||
);
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Form validation handler for contact_category_edit_form().
|
||||
*
|
||||
* @see contact_category_edit_form_submit()
|
||||
*/
|
||||
function contact_category_edit_form_validate($form, &$form_state) {
|
||||
// Validate and each e-mail recipient.
|
||||
$recipients = explode(',', $form_state['values']['recipients']);
|
||||
|
||||
// When creating a new contact form, or renaming the category on an existing
|
||||
// contact form, make sure that the given category is unique.
|
||||
$category = $form_state['values']['category'];
|
||||
$query = db_select('contact', 'c')->condition('c.category', $category, '=');
|
||||
if (!empty($form_state['values']['cid'])) {
|
||||
$query->condition('c.cid', $form_state['values']['cid'], '<>');
|
||||
}
|
||||
if ($query->countQuery()->execute()->fetchField()) {
|
||||
form_set_error('category', t('A contact form with category %category already exists.', array('%category' => $category)));
|
||||
}
|
||||
|
||||
foreach ($recipients as &$recipient) {
|
||||
$recipient = trim($recipient);
|
||||
if (!valid_email_address($recipient)) {
|
||||
form_set_error('recipients', t('%recipient is an invalid e-mail address.', array('%recipient' => $recipient)));
|
||||
}
|
||||
}
|
||||
$form_state['values']['recipients'] = implode(',', $recipients);
|
||||
}
|
||||
|
||||
/**
|
||||
* Form submission handler for contact_category_edit_form().
|
||||
*
|
||||
* @see contact_category_edit_form_validate()
|
||||
*/
|
||||
function contact_category_edit_form_submit($form, &$form_state) {
|
||||
if ($form_state['values']['selected']) {
|
||||
// Unselect all other contact categories.
|
||||
db_update('contact')
|
||||
->fields(array('selected' => '0'))
|
||||
->execute();
|
||||
}
|
||||
|
||||
if (empty($form_state['values']['cid'])) {
|
||||
drupal_write_record('contact', $form_state['values']);
|
||||
}
|
||||
else {
|
||||
drupal_write_record('contact', $form_state['values'], array('cid'));
|
||||
}
|
||||
|
||||
drupal_set_message(t('Category %category has been saved.', array('%category' => $form_state['values']['category'])));
|
||||
watchdog('contact', 'Category %category has been saved.', array('%category' => $form_state['values']['category']), WATCHDOG_NOTICE, l(t('Edit'), 'admin/structure/contact/edit/' . $form_state['values']['cid']));
|
||||
$form_state['redirect'] = 'admin/structure/contact';
|
||||
}
|
||||
|
||||
/**
|
||||
* Form constructor for the contact category deletion form.
|
||||
*
|
||||
* @param $contact
|
||||
* Array describing the contact category to be deleted. See the documentation
|
||||
* of contact_category_edit_form() for the recognized keys.
|
||||
*
|
||||
* @see contact_menu()
|
||||
* @see contact_category_delete_form_submit()
|
||||
*/
|
||||
function contact_category_delete_form($form, &$form_state, array $contact) {
|
||||
$form['contact'] = array(
|
||||
'#type' => 'value',
|
||||
'#value' => $contact,
|
||||
);
|
||||
|
||||
return confirm_form(
|
||||
$form,
|
||||
t('Are you sure you want to delete %category?', array('%category' => $contact['category'])),
|
||||
'admin/structure/contact',
|
||||
t('This action cannot be undone.'),
|
||||
t('Delete'),
|
||||
t('Cancel')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Form submission handler for contact_category_delete_form().
|
||||
*/
|
||||
function contact_category_delete_form_submit($form, &$form_state) {
|
||||
$contact = $form['contact']['#value'];
|
||||
|
||||
db_delete('contact')
|
||||
->condition('cid', $contact['cid'])
|
||||
->execute();
|
||||
|
||||
drupal_set_message(t('Category %category has been deleted.', array('%category' => $contact['category'])));
|
||||
watchdog('contact', 'Category %category has been deleted.', array('%category' => $contact['category']), WATCHDOG_NOTICE);
|
||||
|
||||
$form_state['redirect'] = 'admin/structure/contact';
|
||||
}
|
13
modules/contact/contact.info
Normal file
13
modules/contact/contact.info
Normal file
|
@ -0,0 +1,13 @@
|
|||
name = Contact
|
||||
description = Enables the use of both personal and site-wide contact forms.
|
||||
package = Core
|
||||
version = VERSION
|
||||
core = 7.x
|
||||
files[] = contact.test
|
||||
configure = admin/structure/contact
|
||||
|
||||
; Information added by Drupal.org packaging script on 2017-06-21
|
||||
version = "7.56"
|
||||
project = "drupal"
|
||||
datestamp = "1498069849"
|
||||
|
168
modules/contact/contact.install
Normal file
168
modules/contact/contact.install
Normal file
|
@ -0,0 +1,168 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Install, update and uninstall functions for the contact module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Implements hook_schema().
|
||||
*/
|
||||
function contact_schema() {
|
||||
$schema['contact'] = array(
|
||||
'description' => 'Contact form category settings.',
|
||||
'fields' => array(
|
||||
'cid' => array(
|
||||
'type' => 'serial',
|
||||
'unsigned' => TRUE,
|
||||
'not null' => TRUE,
|
||||
'description' => 'Primary Key: Unique category ID.',
|
||||
),
|
||||
'category' => array(
|
||||
'type' => 'varchar',
|
||||
'length' => 255,
|
||||
'not null' => TRUE,
|
||||
'default' => '',
|
||||
'description' => 'Category name.',
|
||||
'translatable' => TRUE,
|
||||
),
|
||||
'recipients' => array(
|
||||
'type' => 'text',
|
||||
'not null' => TRUE,
|
||||
'size' => 'big',
|
||||
'description' => 'Comma-separated list of recipient e-mail addresses.',
|
||||
),
|
||||
'reply' => array(
|
||||
'type' => 'text',
|
||||
'not null' => TRUE,
|
||||
'size' => 'big',
|
||||
'description' => 'Text of the auto-reply message.',
|
||||
),
|
||||
'weight' => array(
|
||||
'type' => 'int',
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
'description' => "The category's weight.",
|
||||
),
|
||||
'selected' => array(
|
||||
'type' => 'int',
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
'size' => 'tiny',
|
||||
'description' => 'Flag to indicate whether or not category is selected by default. (1 = Yes, 0 = No)',
|
||||
),
|
||||
),
|
||||
'primary key' => array('cid'),
|
||||
'unique keys' => array(
|
||||
'category' => array('category'),
|
||||
),
|
||||
'indexes' => array(
|
||||
'list' => array('weight', 'category'),
|
||||
),
|
||||
);
|
||||
|
||||
return $schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_install().
|
||||
*/
|
||||
function contact_install() {
|
||||
// Insert a default contact category.
|
||||
db_insert('contact')
|
||||
->fields(array(
|
||||
'category' => 'Website feedback',
|
||||
'recipients' => variable_get('site_mail', ini_get('sendmail_from')),
|
||||
'selected' => 1,
|
||||
'reply' => '',
|
||||
))
|
||||
->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_uninstall().
|
||||
*/
|
||||
function contact_uninstall() {
|
||||
variable_del('contact_default_status');
|
||||
variable_del('contact_threshold_limit');
|
||||
variable_del('contact_threshold_window');
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_update_dependencies().
|
||||
*/
|
||||
function contact_update_dependencies() {
|
||||
// contact_update_7001() relies on the {role_permission} table being updated
|
||||
// to the new format and filled with data.
|
||||
$dependencies['contact'][7001] = array(
|
||||
'system' => 7007,
|
||||
);
|
||||
|
||||
// contact_update_7002() relies on the {role_permission} table having the
|
||||
// module field, which is created in user_update_7006().
|
||||
$dependencies['contact'][7002] = array(
|
||||
'user' => 7006,
|
||||
);
|
||||
|
||||
return $dependencies;
|
||||
}
|
||||
|
||||
/**
|
||||
* @addtogroup updates-6.x-to-7.x
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* Rename the threshold limit variable.
|
||||
*/
|
||||
function contact_update_7000() {
|
||||
variable_set('contact_threshold_limit', variable_get('contact_hourly_threshold', 5));
|
||||
variable_del('contact_hourly_threshold');
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename the administer contact forms permission.
|
||||
*/
|
||||
function contact_update_7001() {
|
||||
db_update('role_permission')
|
||||
->fields(array('permission' => 'administer contact forms'))
|
||||
->condition('permission', 'administer site-wide contact form')
|
||||
->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable the 'access user contact forms' for registered users by default.
|
||||
*/
|
||||
function contact_update_7002() {
|
||||
// Do not use user_role_grant_permission() since it relies on
|
||||
// hook_permission(), which will not run for contact module if it is
|
||||
// disabled.
|
||||
db_merge('role_permission')
|
||||
->key(array(
|
||||
'rid' => DRUPAL_AUTHENTICATED_RID,
|
||||
'permission' => 'access user contact forms',
|
||||
'module' => 'contact',
|
||||
))
|
||||
->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the weight column to normal int.
|
||||
*/
|
||||
function contact_update_7003() {
|
||||
db_drop_index('contact', 'list');
|
||||
db_change_field('contact', 'weight', 'weight', array(
|
||||
'type' => 'int',
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
'description' => "The category's weight.",
|
||||
), array(
|
||||
'indexes' => array(
|
||||
'list' => array('weight', 'category'),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @} End of "addtogroup updates-6.x-to-7.x".
|
||||
*/
|
266
modules/contact/contact.module
Normal file
266
modules/contact/contact.module
Normal file
|
@ -0,0 +1,266 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Enables the use of personal and site-wide contact forms.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Implements hook_help().
|
||||
*/
|
||||
function contact_help($path, $arg) {
|
||||
switch ($path) {
|
||||
case 'admin/help#contact':
|
||||
$output = '';
|
||||
$output .= '<h3>' . t('About') . '</h3>';
|
||||
$output .= '<p>' . t('The Contact module allows visitors to contact site administrators and other users. Users specify a subject, write their message, and can have a copy of their message sent to their own e-mail address. For more information, see the online handbook entry for <a href="@contact">Contact module</a>.', array('@contact' => 'http://drupal.org/documentation/modules/contact/')) . '</p>';
|
||||
$output .= '<h3>' . t('Uses') . '</h3>';
|
||||
$output .= '<dl>';
|
||||
$output .= '<dt>' . t('User contact forms') . '</dt>';
|
||||
$output .= '<dd>' . t('Site users can be contacted with a user contact form that keeps their e-mail address private. Users may enable or disable their personal contact forms by editing their <em>My account</em> page. If enabled, a <em>Contact</em> tab leads to a personal contact form displayed on their user profile. Site administrators are still able to use the contact form, even if has been disabled. The <em>Contact</em> tab is not shown when you view your own profile.') . '</dd>';
|
||||
$output .= '<dt>' . t('Site-wide contact forms') . '</dt>';
|
||||
$output .= '<dd>' . t('The <a href="@contact">Contact page</a> provides a simple form for users with the <em>Use the site-wide contact form</em> permission to send comments, feedback, or other requests. You can create categories for directing the contact form messages to a set of defined recipients. Common categories for a business site, for example, might include "Website feedback" (messages are forwarded to website administrators) and "Product information" (messages are forwarded to members of the sales department). E-mail addresses defined within a category are not displayed publicly.', array('@contact' => url('contact'))) . '</p>';
|
||||
$output .= '<dt>' . t('Navigation') . '</dt>';
|
||||
$output .= '<dd>' . t("When the site-wide contact form is enabled, a link in the main <em>Navigation</em> menu is created, but the link is disabled by default. This menu link can be enabled on the <a href='@menu'>Menus administration page</a>.", array('@contact' => url('contact'), '@menu' => url('admin/structure/menu'))) . '</dd>';
|
||||
$output .= '<dt>' . t('Customization') . '</dt>';
|
||||
$output .= '<dd>' . t('If you would like additional text to appear on the site-wide or personal contact page, use a block. You can create and edit blocks on the <a href="@blocks">Blocks administration page</a>.', array('@blocks' => url('admin/structure/block'))) . '</dd>';
|
||||
$output .= '</dl>';
|
||||
return $output;
|
||||
case 'admin/structure/contact':
|
||||
$output = '<p>' . t('Add one or more categories on this page to set up your site-wide <a href="@form">contact form</a>.', array('@form' => url('contact'))) . '</p>';
|
||||
$output .= '<p>' . t('A <em>Contact</em> menu item (disabled by default) is added to the Navigation menu, which you can modify on the <a href="@menu-settings">Menus administration page</a>.', array('@menu-settings' => url('admin/structure/menu'))) . '</p>';
|
||||
$output .= '<p>' . t('If you would like additional text to appear on the site-wide contact page, use a block. You can create and edit blocks on the <a href="@blocks">Blocks administration page</a>.', array('@blocks' => url('admin/structure/block'))) . '</p>';
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_permission().
|
||||
*/
|
||||
function contact_permission() {
|
||||
return array(
|
||||
'administer contact forms' => array(
|
||||
'title' => t('Administer contact forms and contact form settings'),
|
||||
),
|
||||
'access site-wide contact form' => array(
|
||||
'title' => t('Use the site-wide contact form'),
|
||||
),
|
||||
'access user contact forms' => array(
|
||||
'title' => t("Use users' personal contact forms"),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_menu().
|
||||
*/
|
||||
function contact_menu() {
|
||||
$items['admin/structure/contact'] = array(
|
||||
'title' => 'Contact form',
|
||||
'description' => 'Create a system contact form and set up categories for the form to use.',
|
||||
'page callback' => 'contact_category_list',
|
||||
'access arguments' => array('administer contact forms'),
|
||||
'file' => 'contact.admin.inc',
|
||||
);
|
||||
$items['admin/structure/contact/add'] = array(
|
||||
'title' => 'Add category',
|
||||
'page callback' => 'drupal_get_form',
|
||||
'page arguments' => array('contact_category_edit_form'),
|
||||
'access arguments' => array('administer contact forms'),
|
||||
'type' => MENU_LOCAL_ACTION,
|
||||
'weight' => 1,
|
||||
'file' => 'contact.admin.inc',
|
||||
);
|
||||
$items['admin/structure/contact/edit/%contact'] = array(
|
||||
'title' => 'Edit contact category',
|
||||
'page callback' => 'drupal_get_form',
|
||||
'page arguments' => array('contact_category_edit_form', 4),
|
||||
'access arguments' => array('administer contact forms'),
|
||||
'file' => 'contact.admin.inc',
|
||||
);
|
||||
$items['admin/structure/contact/delete/%contact'] = array(
|
||||
'title' => 'Delete contact',
|
||||
'page callback' => 'drupal_get_form',
|
||||
'page arguments' => array('contact_category_delete_form', 4),
|
||||
'access arguments' => array('administer contact forms'),
|
||||
'file' => 'contact.admin.inc',
|
||||
);
|
||||
$items['contact'] = array(
|
||||
'title' => 'Contact',
|
||||
'page callback' => 'drupal_get_form',
|
||||
'page arguments' => array('contact_site_form'),
|
||||
'access arguments' => array('access site-wide contact form'),
|
||||
'type' => MENU_SUGGESTED_ITEM,
|
||||
'file' => 'contact.pages.inc',
|
||||
);
|
||||
$items['user/%user/contact'] = array(
|
||||
'title' => 'Contact',
|
||||
'page callback' => 'drupal_get_form',
|
||||
'page arguments' => array('contact_personal_form', 1),
|
||||
'type' => MENU_LOCAL_TASK,
|
||||
'access callback' => '_contact_personal_tab_access',
|
||||
'access arguments' => array(1),
|
||||
'weight' => 2,
|
||||
'file' => 'contact.pages.inc',
|
||||
);
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Menu access callback for a user's personal contact form.
|
||||
*
|
||||
* @param $account
|
||||
* The user object of the user whose contact form is being requested.
|
||||
*/
|
||||
function _contact_personal_tab_access($account) {
|
||||
global $user;
|
||||
|
||||
// Anonymous users cannot have contact forms.
|
||||
if (!$account->uid) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// User administrators should always have access to personal contact forms.
|
||||
if (user_access('administer users')) {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// Users may not contact themselves.
|
||||
if ($user->uid == $account->uid) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// If the requested user has disabled their contact form, or this preference
|
||||
// has not yet been saved, do not allow users to contact them.
|
||||
if (empty($account->data['contact'])) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// If requested user has been blocked, do not allow users to contact them.
|
||||
if (empty($account->status)) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
return user_access('access user contact forms');
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a contact category.
|
||||
*
|
||||
* @param $cid
|
||||
* The contact category ID.
|
||||
*
|
||||
* @return
|
||||
* An array with the contact category's data.
|
||||
*/
|
||||
function contact_load($cid) {
|
||||
return db_select('contact', 'c')
|
||||
->addTag('translatable')
|
||||
->fields('c')
|
||||
->condition('cid', $cid)
|
||||
->execute()
|
||||
->fetchAssoc();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_mail().
|
||||
*/
|
||||
function contact_mail($key, &$message, $params) {
|
||||
$language = $message['language'];
|
||||
$variables = array(
|
||||
'!site-name' => variable_get('site_name', 'Drupal'),
|
||||
'!subject' => $params['subject'],
|
||||
'!category' => isset($params['category']['category']) ? $params['category']['category'] : '',
|
||||
'!form-url' => url($_GET['q'], array('absolute' => TRUE, 'language' => $language)),
|
||||
'!sender-name' => format_username($params['sender']),
|
||||
'!sender-url' => $params['sender']->uid ? url('user/' . $params['sender']->uid, array('absolute' => TRUE, 'language' => $language)) : $params['sender']->mail,
|
||||
);
|
||||
|
||||
switch ($key) {
|
||||
case 'page_mail':
|
||||
case 'page_copy':
|
||||
$message['subject'] .= t('[!category] !subject', $variables, array('langcode' => $language->language));
|
||||
$message['body'][] = t("!sender-name (!sender-url) sent a message using the contact form at !form-url.", $variables, array('langcode' => $language->language));
|
||||
$message['body'][] = $params['message'];
|
||||
break;
|
||||
|
||||
case 'page_autoreply':
|
||||
$message['subject'] .= t('[!category] !subject', $variables, array('langcode' => $language->language));
|
||||
$message['body'][] = $params['category']['reply'];
|
||||
break;
|
||||
|
||||
case 'user_mail':
|
||||
case 'user_copy':
|
||||
$variables += array(
|
||||
'!recipient-name' => format_username($params['recipient']),
|
||||
'!recipient-edit-url' => url('user/' . $params['recipient']->uid . '/edit', array('absolute' => TRUE, 'language' => $language)),
|
||||
);
|
||||
$message['subject'] .= t('[!site-name] !subject', $variables, array('langcode' => $language->language));
|
||||
$message['body'][] = t('Hello !recipient-name,', $variables, array('langcode' => $language->language));
|
||||
$message['body'][] = t("!sender-name (!sender-url) has sent you a message via your contact form (!form-url) at !site-name.", $variables, array('langcode' => $language->language));
|
||||
$message['body'][] = t("If you don't want to receive such e-mails, you can change your settings at !recipient-edit-url.", $variables, array('langcode' => $language->language));
|
||||
$message['body'][] = t('Message:', array(), array('langcode' => $language->language));
|
||||
$message['body'][] = $params['message'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_form_FORM_ID_alter().
|
||||
*
|
||||
* Add the enable personal contact form to an individual user's account page.
|
||||
*
|
||||
* @see user_profile_form()
|
||||
*/
|
||||
function contact_form_user_profile_form_alter(&$form, &$form_state) {
|
||||
if ($form['#user_category'] == 'account') {
|
||||
$account = $form['#user'];
|
||||
$form['contact'] = array(
|
||||
'#type' => 'fieldset',
|
||||
'#title' => t('Contact settings'),
|
||||
'#weight' => 5,
|
||||
'#collapsible' => TRUE,
|
||||
);
|
||||
$form['contact']['contact'] = array(
|
||||
'#type' => 'checkbox',
|
||||
'#title' => t('Personal contact form'),
|
||||
'#default_value' => !empty($account->data['contact']) ? $account->data['contact'] : FALSE,
|
||||
'#description' => t('Allow other users to contact you via a <a href="@url">personal contact form</a> which keeps your e-mail address hidden. Note that some privileged users such as site administrators are still able to contact you even if you choose to disable this feature.', array('@url' => url("user/$account->uid/contact"))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_user_presave().
|
||||
*/
|
||||
function contact_user_presave(&$edit, $account, $category) {
|
||||
if (isset($edit['contact'])) {
|
||||
// Set new value.
|
||||
$edit['data']['contact'] = $edit['contact'];
|
||||
}
|
||||
elseif (!isset($account->data['contact'])) {
|
||||
// Use default if none has been set.
|
||||
$edit['data']['contact'] = variable_get('contact_default_status', 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_form_FORM_ID_alter().
|
||||
*
|
||||
* Add the default personal contact setting on the user settings page.
|
||||
*
|
||||
* @see user_admin_settings()
|
||||
*/
|
||||
function contact_form_user_admin_settings_alter(&$form, &$form_state) {
|
||||
$form['contact'] = array(
|
||||
'#type' => 'fieldset',
|
||||
'#title' => t('Contact settings'),
|
||||
'#weight' => 0,
|
||||
);
|
||||
$form['contact']['contact_default_status'] = array(
|
||||
'#type' => 'checkbox',
|
||||
'#title' => t('Enable the personal contact form by default for new users.'),
|
||||
'#description' => t('Changing this setting will not affect existing users.'),
|
||||
'#default_value' => variable_get('contact_default_status', 1),
|
||||
);
|
||||
}
|
300
modules/contact/contact.pages.inc
Normal file
300
modules/contact/contact.pages.inc
Normal file
|
@ -0,0 +1,300 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Page callbacks for the Contact module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Form constructor for the site-wide contact form.
|
||||
*
|
||||
* @see contact_site_form_validate()
|
||||
* @see contact_site_form_submit()
|
||||
* @ingroup forms
|
||||
*/
|
||||
function contact_site_form($form, &$form_state) {
|
||||
global $user;
|
||||
|
||||
// Check if flood control has been activated for sending e-mails.
|
||||
$limit = variable_get('contact_threshold_limit', 5);
|
||||
$window = variable_get('contact_threshold_window', 3600);
|
||||
if (!flood_is_allowed('contact', $limit, $window) && !user_access('administer contact forms')) {
|
||||
drupal_set_message(t("You cannot send more than %limit messages in @interval. Try again later.", array('%limit' => $limit, '@interval' => format_interval($window))), 'error');
|
||||
drupal_access_denied();
|
||||
drupal_exit();
|
||||
}
|
||||
|
||||
// Get an array of the categories and the current default category.
|
||||
$categories = db_select('contact', 'c')
|
||||
->addTag('translatable')
|
||||
->fields('c', array('cid', 'category'))
|
||||
->orderBy('weight')
|
||||
->orderBy('category')
|
||||
->execute()
|
||||
->fetchAllKeyed();
|
||||
$default_category = db_query("SELECT cid FROM {contact} WHERE selected = 1")->fetchField();
|
||||
|
||||
// If there are no categories, do not display the form.
|
||||
if (!$categories) {
|
||||
if (user_access('administer contact forms')) {
|
||||
drupal_set_message(t('The contact form has not been configured. <a href="@add">Add one or more categories</a> to the form.', array('@add' => url('admin/structure/contact/add'))), 'error');
|
||||
}
|
||||
else {
|
||||
drupal_not_found();
|
||||
drupal_exit();
|
||||
}
|
||||
}
|
||||
|
||||
// If there is more than one category available and no default category has
|
||||
// been selected, prepend a default placeholder value.
|
||||
if (!$default_category) {
|
||||
if (count($categories) > 1) {
|
||||
$categories = array(0 => t('- Please choose -')) + $categories;
|
||||
}
|
||||
else {
|
||||
$default_category = key($categories);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$user->uid) {
|
||||
$form['#attached']['library'][] = array('system', 'jquery.cookie');
|
||||
$form['#attributes']['class'][] = 'user-info-from-cookie';
|
||||
}
|
||||
|
||||
$form['#attributes']['class'][] = 'contact-form';
|
||||
$form['name'] = array(
|
||||
'#type' => 'textfield',
|
||||
'#title' => t('Your name'),
|
||||
'#maxlength' => 255,
|
||||
'#default_value' => $user->uid ? format_username($user) : '',
|
||||
'#required' => TRUE,
|
||||
);
|
||||
$form['mail'] = array(
|
||||
'#type' => 'textfield',
|
||||
'#title' => t('Your e-mail address'),
|
||||
'#maxlength' => 255,
|
||||
'#default_value' => $user->uid ? $user->mail : '',
|
||||
'#required' => TRUE,
|
||||
);
|
||||
$form['subject'] = array(
|
||||
'#type' => 'textfield',
|
||||
'#title' => t('Subject'),
|
||||
'#maxlength' => 255,
|
||||
'#required' => TRUE,
|
||||
);
|
||||
$form['cid'] = array(
|
||||
'#type' => 'select',
|
||||
'#title' => t('Category'),
|
||||
'#default_value' => $default_category,
|
||||
'#options' => $categories,
|
||||
'#required' => TRUE,
|
||||
'#access' => count($categories) > 1,
|
||||
);
|
||||
$form['message'] = array(
|
||||
'#type' => 'textarea',
|
||||
'#title' => t('Message'),
|
||||
'#required' => TRUE,
|
||||
);
|
||||
// We do not allow anonymous users to send themselves a copy
|
||||
// because it can be abused to spam people.
|
||||
$form['copy'] = array(
|
||||
'#type' => 'checkbox',
|
||||
'#title' => t('Send yourself a copy.'),
|
||||
'#access' => $user->uid,
|
||||
);
|
||||
$form['actions'] = array('#type' => 'actions');
|
||||
$form['actions']['submit'] = array(
|
||||
'#type' => 'submit',
|
||||
'#value' => t('Send message'),
|
||||
);
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Form validation handler for contact_site_form().
|
||||
*
|
||||
* @see contact_site_form_submit()
|
||||
*/
|
||||
function contact_site_form_validate($form, &$form_state) {
|
||||
if (!$form_state['values']['cid']) {
|
||||
form_set_error('cid', t('You must select a valid category.'));
|
||||
}
|
||||
if (!valid_email_address($form_state['values']['mail'])) {
|
||||
form_set_error('mail', t('You must enter a valid e-mail address.'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Form submission handler for contact_site_form().
|
||||
*
|
||||
* @see contact_site_form_validate()
|
||||
*/
|
||||
function contact_site_form_submit($form, &$form_state) {
|
||||
global $user, $language;
|
||||
|
||||
$values = $form_state['values'];
|
||||
$values['sender'] = clone $user;
|
||||
$values['sender']->name = $values['name'];
|
||||
$values['sender']->mail = $values['mail'];
|
||||
$values['category'] = contact_load($values['cid']);
|
||||
|
||||
// Save the anonymous user information to a cookie for reuse.
|
||||
if (!$user->uid) {
|
||||
user_cookie_save(array_intersect_key($values, array_flip(array('name', 'mail'))));
|
||||
}
|
||||
|
||||
// Get the to and from e-mail addresses.
|
||||
$to = $values['category']['recipients'];
|
||||
$from = $values['sender']->mail;
|
||||
|
||||
// Send the e-mail to the recipients using the site default language.
|
||||
drupal_mail('contact', 'page_mail', $to, language_default(), $values, $from);
|
||||
|
||||
// If the user requests it, send a copy using the current language.
|
||||
if ($values['copy']) {
|
||||
drupal_mail('contact', 'page_copy', $from, $language, $values, $from);
|
||||
}
|
||||
|
||||
// Send an auto-reply if necessary using the current language.
|
||||
if ($values['category']['reply']) {
|
||||
drupal_mail('contact', 'page_autoreply', $from, $language, $values, $to);
|
||||
}
|
||||
|
||||
flood_register_event('contact', variable_get('contact_threshold_window', 3600));
|
||||
watchdog('mail', '%sender-name (@sender-from) sent an e-mail regarding %category.', array('%sender-name' => $values['name'], '@sender-from' => $from, '%category' => $values['category']['category']));
|
||||
|
||||
// Jump to home page rather than back to contact page to avoid
|
||||
// contradictory messages if flood control has been activated.
|
||||
drupal_set_message(t('Your message has been sent.'));
|
||||
$form_state['redirect'] = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Form constructor for the personal contact form.
|
||||
*
|
||||
* Path: user/%user/contact
|
||||
*
|
||||
* @see contact_menu()
|
||||
* @see contact_personal_form_validate()
|
||||
* @see contact_personal_form_submit()
|
||||
* @ingroup forms
|
||||
*/
|
||||
function contact_personal_form($form, &$form_state, $recipient) {
|
||||
global $user;
|
||||
|
||||
// Check if flood control has been activated for sending e-mails.
|
||||
$limit = variable_get('contact_threshold_limit', 5);
|
||||
$window = variable_get('contact_threshold_window', 3600);
|
||||
if (!flood_is_allowed('contact', $limit, $window) && !user_access('administer contact forms') && !user_access('administer users')) {
|
||||
drupal_set_message(t("You cannot send more than %limit messages in @interval. Try again later.", array('%limit' => $limit, '@interval' => format_interval($window))), 'error');
|
||||
drupal_access_denied();
|
||||
drupal_exit();
|
||||
}
|
||||
|
||||
drupal_set_title(t('Contact @username', array('@username' => format_username($recipient))), PASS_THROUGH);
|
||||
|
||||
if (!$user->uid) {
|
||||
$form['#attached']['library'][] = array('system', 'jquery.cookie');
|
||||
$form['#attributes']['class'][] = 'user-info-from-cookie';
|
||||
}
|
||||
|
||||
$form['#attributes']['class'][] = 'contact-form';
|
||||
$form['recipient'] = array(
|
||||
'#type' => 'value',
|
||||
'#value' => $recipient,
|
||||
);
|
||||
$form['name'] = array(
|
||||
'#type' => 'textfield',
|
||||
'#title' => t('Your name'),
|
||||
'#maxlength' => 255,
|
||||
'#default_value' => $user->uid ? format_username($user) : '',
|
||||
'#required' => TRUE,
|
||||
);
|
||||
$form['mail'] = array(
|
||||
'#type' => 'textfield',
|
||||
'#title' => t('Your e-mail address'),
|
||||
'#maxlength' => 255,
|
||||
'#default_value' => $user->uid ? $user->mail : '',
|
||||
'#required' => TRUE,
|
||||
);
|
||||
$form['to'] = array(
|
||||
'#type' => 'item',
|
||||
'#title' => t('To'),
|
||||
'#markup' => theme('username', array('account' => $recipient)),
|
||||
);
|
||||
$form['subject'] = array(
|
||||
'#type' => 'textfield',
|
||||
'#title' => t('Subject'),
|
||||
'#maxlength' => 50,
|
||||
'#required' => TRUE,
|
||||
);
|
||||
$form['message'] = array(
|
||||
'#type' => 'textarea',
|
||||
'#title' => t('Message'),
|
||||
'#rows' => 15,
|
||||
'#required' => TRUE,
|
||||
);
|
||||
// We do not allow anonymous users to send themselves a copy
|
||||
// because it can be abused to spam people.
|
||||
$form['copy'] = array(
|
||||
'#type' => 'checkbox',
|
||||
'#title' => t('Send yourself a copy.'),
|
||||
'#access' => $user->uid,
|
||||
);
|
||||
$form['actions'] = array('#type' => 'actions');
|
||||
$form['actions']['submit'] = array(
|
||||
'#type' => 'submit',
|
||||
'#value' => t('Send message'),
|
||||
);
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Form validation handler for contact_personal_form().
|
||||
*
|
||||
* @see contact_personal_form_submit()
|
||||
*/
|
||||
function contact_personal_form_validate($form, &$form_state) {
|
||||
if (!valid_email_address($form_state['values']['mail'])) {
|
||||
form_set_error('mail', t('You must enter a valid e-mail address.'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Form submission handler for contact_personal_form().
|
||||
*
|
||||
* @see contact_personal_form_validate()
|
||||
*/
|
||||
function contact_personal_form_submit($form, &$form_state) {
|
||||
global $user, $language;
|
||||
|
||||
$values = $form_state['values'];
|
||||
$values['sender'] = clone $user;
|
||||
$values['sender']->name = $values['name'];
|
||||
$values['sender']->mail = $values['mail'];
|
||||
|
||||
// Save the anonymous user information to a cookie for reuse.
|
||||
if (!$user->uid) {
|
||||
user_cookie_save(array_intersect_key($values, array_flip(array('name', 'mail'))));
|
||||
}
|
||||
|
||||
// Get the to and from e-mail addresses.
|
||||
$to = $values['recipient']->mail;
|
||||
$from = $values['sender']->mail;
|
||||
|
||||
// Send the e-mail in the requested user language.
|
||||
drupal_mail('contact', 'user_mail', $to, user_preferred_language($values['recipient']), $values, $from);
|
||||
|
||||
// Send a copy if requested, using current page language.
|
||||
if ($values['copy']) {
|
||||
drupal_mail('contact', 'user_copy', $from, $language, $values, $from);
|
||||
}
|
||||
|
||||
flood_register_event('contact', variable_get('contact_threshold_window', 3600));
|
||||
watchdog('mail', '%sender-name (@sender-from) sent %recipient-name an e-mail.', array('%sender-name' => $values['name'], '@sender-from' => $from, '%recipient-name' => $values['recipient']->name));
|
||||
|
||||
// Jump to the contacted user's profile page.
|
||||
drupal_set_message(t('Your message has been sent.'));
|
||||
$form_state['redirect'] = user_access('access user profiles') ? 'user/' . $values['recipient']->uid : '';
|
||||
}
|
456
modules/contact/contact.test
Normal file
456
modules/contact/contact.test
Normal file
|
@ -0,0 +1,456 @@
|
|||
<?php
|
||||
/**
|
||||
* @file
|
||||
* Tests for the Contact module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Tests the site-wide contact form.
|
||||
*/
|
||||
class ContactSitewideTestCase extends DrupalWebTestCase {
|
||||
public static function getInfo() {
|
||||
return array(
|
||||
'name' => 'Site-wide contact form',
|
||||
'description' => 'Tests site-wide contact form functionality.',
|
||||
'group' => 'Contact',
|
||||
);
|
||||
}
|
||||
|
||||
function setUp() {
|
||||
parent::setUp('contact');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests configuration options and the site-wide contact form.
|
||||
*/
|
||||
function testSiteWideContact() {
|
||||
// Create and login administrative user.
|
||||
$admin_user = $this->drupalCreateUser(array('access site-wide contact form', 'administer contact forms', 'administer users'));
|
||||
$this->drupalLogin($admin_user);
|
||||
|
||||
$flood_limit = 3;
|
||||
variable_set('contact_threshold_limit', $flood_limit);
|
||||
variable_set('contact_threshold_window', 600);
|
||||
|
||||
// Set settings.
|
||||
$edit = array();
|
||||
$edit['contact_default_status'] = TRUE;
|
||||
$this->drupalPost('admin/config/people/accounts', $edit, t('Save configuration'));
|
||||
$this->assertText(t('The configuration options have been saved.'), 'Setting successfully saved.');
|
||||
|
||||
// Delete old categories to ensure that new categories are used.
|
||||
$this->deleteCategories();
|
||||
|
||||
// Ensure that the contact form won't be shown without categories.
|
||||
user_role_grant_permissions(DRUPAL_ANONYMOUS_RID, array('access site-wide contact form'));
|
||||
$this->drupalLogout();
|
||||
$this->drupalGet('contact');
|
||||
$this->assertResponse(404);
|
||||
$this->drupalLogin($admin_user);
|
||||
$this->drupalGet('contact');
|
||||
$this->assertResponse(200);
|
||||
$this->assertText(t('The contact form has not been configured.'));
|
||||
|
||||
// Add categories.
|
||||
// Test invalid recipients.
|
||||
$invalid_recipients = array('invalid', 'invalid@', 'invalid@site.', '@site.', '@site.com');
|
||||
foreach ($invalid_recipients as $invalid_recipient) {
|
||||
$this->addCategory($this->randomName(16), $invalid_recipient, '', FALSE);
|
||||
$this->assertRaw(t('%recipient is an invalid e-mail address.', array('%recipient' => $invalid_recipient)), format_string('Caught invalid recipient (@invalid_recipient).', array('@invalid_recipient' => $invalid_recipient)));
|
||||
}
|
||||
|
||||
// Test validation of empty category and recipients fields.
|
||||
$this->addCategory($category = '', '', '', TRUE);
|
||||
$this->assertText(t('Category field is required.'), 'Caught empty category field');
|
||||
$this->assertText(t('Recipients field is required.'), 'Caught empty recipients field.');
|
||||
|
||||
// Create first valid category.
|
||||
$recipients = array('simpletest@example.com', 'simpletest2@example.com', 'simpletest3@example.com');
|
||||
$this->addCategory($category = $this->randomName(16), implode(',', array($recipients[0])), '', TRUE);
|
||||
$this->assertRaw(t('Category %category has been saved.', array('%category' => $category)), 'Category successfully saved.');
|
||||
|
||||
// Make sure the newly created category is included in the list of categories.
|
||||
$this->assertNoUniqueText($category, 'New category included in categories list.');
|
||||
|
||||
// Test update contact form category.
|
||||
$categories = $this->getCategories();
|
||||
$category_id = $this->updateCategory($categories, $category = $this->randomName(16), $recipients_str = implode(',', array($recipients[0], $recipients[1])), $reply = $this->randomName(30), FALSE);
|
||||
$category_array = db_query("SELECT category, recipients, reply, selected FROM {contact} WHERE cid = :cid", array(':cid' => $category_id))->fetchAssoc();
|
||||
$this->assertEqual($category_array['category'], $category);
|
||||
$this->assertEqual($category_array['recipients'], $recipients_str);
|
||||
$this->assertEqual($category_array['reply'], $reply);
|
||||
$this->assertFalse($category_array['selected']);
|
||||
$this->assertRaw(t('Category %category has been saved.', array('%category' => $category)), 'Category successfully saved.');
|
||||
|
||||
// Ensure that the contact form is shown without a category selection input.
|
||||
user_role_grant_permissions(DRUPAL_ANONYMOUS_RID, array('access site-wide contact form'));
|
||||
$this->drupalLogout();
|
||||
$this->drupalGet('contact');
|
||||
$this->assertText(t('Your e-mail address'), 'Contact form is shown when there is one category.');
|
||||
$this->assertNoText(t('Category'), 'When there is only one category, the category selection element is hidden.');
|
||||
$this->drupalLogin($admin_user);
|
||||
|
||||
// Add more categories.
|
||||
$this->addCategory($category = $this->randomName(16), implode(',', array($recipients[0], $recipients[1])), '', FALSE);
|
||||
$this->assertRaw(t('Category %category has been saved.', array('%category' => $category)), 'Category successfully saved.');
|
||||
|
||||
$this->addCategory($category = $this->randomName(16), implode(',', array($recipients[0], $recipients[1], $recipients[2])), '', FALSE);
|
||||
$this->assertRaw(t('Category %category has been saved.', array('%category' => $category)), 'Category successfully saved.');
|
||||
|
||||
// Try adding a category that already exists.
|
||||
$this->addCategory($category, '', '', FALSE);
|
||||
$this->assertNoRaw(t('Category %category has been saved.', array('%category' => $category)), 'Category not saved.');
|
||||
$this->assertRaw(t('A contact form with category %category already exists.', array('%category' => $category)), 'Duplicate category error found.');
|
||||
|
||||
// Clear flood table in preparation for flood test and allow other checks to complete.
|
||||
db_delete('flood')->execute();
|
||||
$num_records_after = db_query("SELECT COUNT(*) FROM {flood}")->fetchField();
|
||||
$this->assertIdentical($num_records_after, '0', 'Flood table emptied.');
|
||||
$this->drupalLogout();
|
||||
|
||||
// Check to see that anonymous user cannot see contact page without permission.
|
||||
user_role_revoke_permissions(DRUPAL_ANONYMOUS_RID, array('access site-wide contact form'));
|
||||
$this->drupalGet('contact');
|
||||
$this->assertResponse(403, 'Access denied to anonymous user without permission.');
|
||||
|
||||
// Give anonymous user permission and see that page is viewable.
|
||||
user_role_grant_permissions(DRUPAL_ANONYMOUS_RID, array('access site-wide contact form'));
|
||||
$this->drupalGet('contact');
|
||||
$this->assertResponse(200, 'Access granted to anonymous user with permission.');
|
||||
|
||||
// Submit contact form with invalid values.
|
||||
$this->submitContact('', $recipients[0], $this->randomName(16), $categories[0], $this->randomName(64));
|
||||
$this->assertText(t('Your name field is required.'), 'Name required.');
|
||||
|
||||
$this->submitContact($this->randomName(16), '', $this->randomName(16), $categories[0], $this->randomName(64));
|
||||
$this->assertText(t('Your e-mail address field is required.'), 'E-mail required.');
|
||||
|
||||
$this->submitContact($this->randomName(16), $invalid_recipients[0], $this->randomName(16), $categories[0], $this->randomName(64));
|
||||
$this->assertText(t('You must enter a valid e-mail address.'), 'Valid e-mail required.');
|
||||
|
||||
$this->submitContact($this->randomName(16), $recipients[0], '', $categories[0], $this->randomName(64));
|
||||
$this->assertText(t('Subject field is required.'), 'Subject required.');
|
||||
|
||||
$this->submitContact($this->randomName(16), $recipients[0], $this->randomName(16), $categories[0], '');
|
||||
$this->assertText(t('Message field is required.'), 'Message required.');
|
||||
|
||||
// Test contact form with no default category selected.
|
||||
db_update('contact')
|
||||
->fields(array('selected' => 0))
|
||||
->execute();
|
||||
$this->drupalGet('contact');
|
||||
$this->assertRaw(t('- Please choose -'), 'Without selected categories the visitor is asked to chose a category.');
|
||||
|
||||
// Submit contact form with invalid category id (cid 0).
|
||||
$this->submitContact($this->randomName(16), $recipients[0], $this->randomName(16), 0, '');
|
||||
$this->assertText(t('You must select a valid category.'), 'Valid category required.');
|
||||
|
||||
// Submit contact form with correct values and check flood interval.
|
||||
for ($i = 0; $i < $flood_limit; $i++) {
|
||||
$this->submitContact($this->randomName(16), $recipients[0], $this->randomName(16), $categories[0], $this->randomName(64));
|
||||
$this->assertText(t('Your message has been sent.'), 'Message sent.');
|
||||
}
|
||||
// Submit contact form one over limit.
|
||||
$this->drupalGet('contact');
|
||||
$this->assertResponse(403, 'Access denied to anonymous user after reaching message treshold.');
|
||||
$this->assertRaw(t('You cannot send more than %number messages in @interval. Try again later.', array('%number' => variable_get('contact_threshold_limit', 3), '@interval' => format_interval(600))), 'Message threshold reached.');
|
||||
|
||||
// Delete created categories.
|
||||
$this->drupalLogin($admin_user);
|
||||
$this->deleteCategories();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests auto-reply on the site-wide contact form.
|
||||
*/
|
||||
function testAutoReply() {
|
||||
// Create and login administrative user.
|
||||
$admin_user = $this->drupalCreateUser(array('access site-wide contact form', 'administer contact forms', 'administer permissions', 'administer users'));
|
||||
$this->drupalLogin($admin_user);
|
||||
|
||||
// Set up three categories, 2 with an auto-reply and one without.
|
||||
$foo_autoreply = $this->randomName(40);
|
||||
$bar_autoreply = $this->randomName(40);
|
||||
$this->addCategory('foo', 'foo@example.com', $foo_autoreply, FALSE);
|
||||
$this->addCategory('bar', 'bar@example.com', $bar_autoreply, FALSE);
|
||||
$this->addCategory('no_autoreply', 'bar@example.com', '', FALSE);
|
||||
|
||||
// Test the auto-reply for category 'foo'.
|
||||
$email = $this->randomName(32) . '@example.com';
|
||||
$subject = $this->randomName(64);
|
||||
$this->submitContact($this->randomName(16), $email, $subject, 2, $this->randomString(128));
|
||||
|
||||
// We are testing the auto-reply, so there should be one e-mail going to the sender.
|
||||
$captured_emails = $this->drupalGetMails(array('id' => 'contact_page_autoreply', 'to' => $email, 'from' => 'foo@example.com'));
|
||||
$this->assertEqual(count($captured_emails), 1, 'Auto-reply e-mail was sent to the sender for category "foo".', 'Contact');
|
||||
$this->assertEqual($captured_emails[0]['body'], drupal_html_to_text($foo_autoreply), 'Auto-reply e-mail body is correct for category "foo".', 'Contact');
|
||||
|
||||
// Test the auto-reply for category 'bar'.
|
||||
$email = $this->randomName(32) . '@example.com';
|
||||
$this->submitContact($this->randomName(16), $email, $this->randomString(64), 3, $this->randomString(128));
|
||||
|
||||
// Auto-reply for category 'bar' should result in one auto-reply e-mail to the sender.
|
||||
$captured_emails = $this->drupalGetMails(array('id' => 'contact_page_autoreply', 'to' => $email, 'from' => 'bar@example.com'));
|
||||
$this->assertEqual(count($captured_emails), 1, 'Auto-reply e-mail was sent to the sender for category "bar".', 'Contact');
|
||||
$this->assertEqual($captured_emails[0]['body'], drupal_html_to_text($bar_autoreply), 'Auto-reply e-mail body is correct for category "bar".', 'Contact');
|
||||
|
||||
// Verify that no auto-reply is sent when the auto-reply field is left blank.
|
||||
$email = $this->randomName(32) . '@example.com';
|
||||
$this->submitContact($this->randomName(16), $email, $this->randomString(64), 4, $this->randomString(128));
|
||||
$captured_emails = $this->drupalGetMails(array('id' => 'contact_page_autoreply', 'to' => $email, 'from' => 'no_autoreply@example.com'));
|
||||
$this->assertEqual(count($captured_emails), 0, 'No auto-reply e-mail was sent to the sender for category "no-autoreply".', 'Contact');
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a category.
|
||||
*
|
||||
* @param string $category
|
||||
* The category name.
|
||||
* @param string $recipients
|
||||
* The list of recipient e-mail addresses.
|
||||
* @param string $reply
|
||||
* The auto-reply text that is sent to a user upon completing the contact
|
||||
* form.
|
||||
* @param boolean $selected
|
||||
* Boolean indicating whether the category should be selected by default.
|
||||
*/
|
||||
function addCategory($category, $recipients, $reply, $selected) {
|
||||
$edit = array();
|
||||
$edit['category'] = $category;
|
||||
$edit['recipients'] = $recipients;
|
||||
$edit['reply'] = $reply;
|
||||
$edit['selected'] = ($selected ? '1' : '0');
|
||||
$this->drupalPost('admin/structure/contact/add', $edit, t('Save'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a category.
|
||||
*
|
||||
* @param string $category
|
||||
* The category name.
|
||||
* @param string $recipients
|
||||
* The list of recipient e-mail addresses.
|
||||
* @param string $reply
|
||||
* The auto-reply text that is sent to a user upon completing the contact
|
||||
* form.
|
||||
* @param boolean $selected
|
||||
* Boolean indicating whether the category should be selected by default.
|
||||
*/
|
||||
function updateCategory($categories, $category, $recipients, $reply, $selected) {
|
||||
$category_id = $categories[array_rand($categories)];
|
||||
$edit = array();
|
||||
$edit['category'] = $category;
|
||||
$edit['recipients'] = $recipients;
|
||||
$edit['reply'] = $reply;
|
||||
$edit['selected'] = ($selected ? '1' : '0');
|
||||
$this->drupalPost('admin/structure/contact/edit/' . $category_id, $edit, t('Save'));
|
||||
return ($category_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits the contact form.
|
||||
*
|
||||
* @param string $name
|
||||
* The name of the sender.
|
||||
* @param string $mail
|
||||
* The e-mail address of the sender.
|
||||
* @param string $subject
|
||||
* The subject of the message.
|
||||
* @param integer $cid
|
||||
* The category ID of the message.
|
||||
* @param string $message
|
||||
* The message body.
|
||||
*/
|
||||
function submitContact($name, $mail, $subject, $cid, $message) {
|
||||
$edit = array();
|
||||
$edit['name'] = $name;
|
||||
$edit['mail'] = $mail;
|
||||
$edit['subject'] = $subject;
|
||||
$edit['cid'] = $cid;
|
||||
$edit['message'] = $message;
|
||||
$this->drupalPost('contact', $edit, t('Send message'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all categories.
|
||||
*/
|
||||
function deleteCategories() {
|
||||
$categories = $this->getCategories();
|
||||
foreach ($categories as $category) {
|
||||
$category_name = db_query("SELECT category FROM {contact} WHERE cid = :cid", array(':cid' => $category))->fetchField();
|
||||
$this->drupalPost('admin/structure/contact/delete/' . $category, array(), t('Delete'));
|
||||
$this->assertRaw(t('Category %category has been deleted.', array('%category' => $category_name)), 'Category deleted successfully.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of all category IDs.
|
||||
*
|
||||
* @return array
|
||||
* A list of the category IDs.
|
||||
*/
|
||||
function getCategories() {
|
||||
$categories = db_query('SELECT cid FROM {contact}')->fetchCol();
|
||||
return $categories;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the personal contact form.
|
||||
*/
|
||||
class ContactPersonalTestCase extends DrupalWebTestCase {
|
||||
private $admin_user;
|
||||
private $web_user;
|
||||
private $contact_user;
|
||||
|
||||
public static function getInfo() {
|
||||
return array(
|
||||
'name' => 'Personal contact form',
|
||||
'description' => 'Tests personal contact form functionality.',
|
||||
'group' => 'Contact',
|
||||
);
|
||||
}
|
||||
|
||||
function setUp() {
|
||||
parent::setUp('contact');
|
||||
|
||||
// Create an admin user.
|
||||
$this->admin_user = $this->drupalCreateUser(array('administer contact forms', 'administer users'));
|
||||
|
||||
// Create some normal users with their contact forms enabled by default.
|
||||
variable_set('contact_default_status', TRUE);
|
||||
$this->web_user = $this->drupalCreateUser(array('access user contact forms'));
|
||||
$this->contact_user = $this->drupalCreateUser();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests access to the personal contact form.
|
||||
*/
|
||||
function testPersonalContactAccess() {
|
||||
// Test allowed access to user with contact form enabled.
|
||||
$this->drupalLogin($this->web_user);
|
||||
$this->drupalGet('user/' . $this->contact_user->uid . '/contact');
|
||||
$this->assertResponse(200);
|
||||
|
||||
// Test denied access to the user's own contact form.
|
||||
$this->drupalGet('user/' . $this->web_user->uid . '/contact');
|
||||
$this->assertResponse(403);
|
||||
|
||||
// Test always denied access to the anonymous user contact form.
|
||||
$this->drupalGet('user/0/contact');
|
||||
$this->assertResponse(403);
|
||||
|
||||
// Test that anonymous users can access the contact form.
|
||||
$this->drupalLogout();
|
||||
user_role_grant_permissions(DRUPAL_ANONYMOUS_RID, array('access user contact forms'));
|
||||
$this->drupalGet('user/' . $this->contact_user->uid . '/contact');
|
||||
$this->assertResponse(200);
|
||||
|
||||
// Test that users can disable their contact form.
|
||||
$this->drupalLogin($this->contact_user);
|
||||
$edit = array('contact' => FALSE);
|
||||
$this->drupalPost('user/' . $this->contact_user->uid . '/edit', $edit, 'Save');
|
||||
$this->drupalLogout();
|
||||
$this->drupalGet('user/' . $this->contact_user->uid . '/contact');
|
||||
$this->assertResponse(403);
|
||||
|
||||
// Test that user's contact status stays disabled when saving.
|
||||
$contact_user_temp = user_load($this->contact_user->uid, TRUE);
|
||||
user_save($contact_user_temp);
|
||||
$this->drupalGet('user/' . $this->contact_user->uid . '/contact');
|
||||
$this->assertResponse(403);
|
||||
|
||||
// Test that users can enable their contact form.
|
||||
$this->drupalLogin($this->contact_user);
|
||||
$edit = array('contact' => TRUE);
|
||||
$this->drupalPost('user/' . $this->contact_user->uid . '/edit', $edit, 'Save');
|
||||
$this->drupalLogout();
|
||||
$this->drupalGet('user/' . $this->contact_user->uid . '/contact');
|
||||
$this->assertResponse(200);
|
||||
|
||||
// Revoke the personal contact permission for the anonymous user.
|
||||
user_role_revoke_permissions(DRUPAL_ANONYMOUS_RID, array('access user contact forms'));
|
||||
$this->drupalGet('user/' . $this->contact_user->uid . '/contact');
|
||||
$this->assertResponse(403);
|
||||
|
||||
// Disable the personal contact form.
|
||||
$this->drupalLogin($this->admin_user);
|
||||
$edit = array('contact_default_status' => FALSE);
|
||||
$this->drupalPost('admin/config/people/accounts', $edit, t('Save configuration'));
|
||||
$this->assertText(t('The configuration options have been saved.'), 'Setting successfully saved.');
|
||||
$this->drupalLogout();
|
||||
|
||||
// Re-create our contacted user with personal contact forms disabled by
|
||||
// default.
|
||||
$this->contact_user = $this->drupalCreateUser();
|
||||
|
||||
// Test denied access to a user with contact form disabled.
|
||||
$this->drupalLogin($this->web_user);
|
||||
$this->drupalGet('user/' . $this->contact_user->uid . '/contact');
|
||||
$this->assertResponse(403);
|
||||
|
||||
// Test allowed access for admin user to a user with contact form disabled.
|
||||
$this->drupalLogin($this->admin_user);
|
||||
$this->drupalGet('user/' . $this->contact_user->uid . '/contact');
|
||||
$this->assertResponse(200);
|
||||
|
||||
// Re-create our contacted user as a blocked user.
|
||||
$this->contact_user = $this->drupalCreateUser();
|
||||
user_save($this->contact_user, array('status' => 0));
|
||||
|
||||
// Test that blocked users can still be contacted by admin.
|
||||
$this->drupalGet('user/' . $this->contact_user->uid . '/contact');
|
||||
$this->assertResponse(200);
|
||||
|
||||
// Test that blocked users cannot be contacted by non-admins.
|
||||
$this->drupalLogin($this->web_user);
|
||||
$this->drupalGet('user/' . $this->contact_user->uid . '/contact');
|
||||
$this->assertResponse(403);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the personal contact form flood protection.
|
||||
*/
|
||||
function testPersonalContactFlood() {
|
||||
$flood_limit = 3;
|
||||
variable_set('contact_threshold_limit', $flood_limit);
|
||||
|
||||
// Clear flood table in preparation for flood test and allow other checks to complete.
|
||||
db_delete('flood')->execute();
|
||||
$num_records_flood = db_query("SELECT COUNT(*) FROM {flood}")->fetchField();
|
||||
$this->assertIdentical($num_records_flood, '0', 'Flood table emptied.');
|
||||
|
||||
$this->drupalLogin($this->web_user);
|
||||
|
||||
// Submit contact form with correct values and check flood interval.
|
||||
for ($i = 0; $i < $flood_limit; $i++) {
|
||||
$this->submitPersonalContact($this->contact_user);
|
||||
$this->assertText(t('Your message has been sent.'), 'Message sent.');
|
||||
}
|
||||
|
||||
// Submit contact form one over limit.
|
||||
$this->drupalGet('user/' . $this->contact_user->uid. '/contact');
|
||||
$this->assertRaw(t('You cannot send more than %number messages in @interval. Try again later.', array('%number' => $flood_limit, '@interval' => format_interval(variable_get('contact_threshold_window', 3600)))), 'Normal user denied access to flooded contact form.');
|
||||
|
||||
// Test that the admin user can still access the contact form even though
|
||||
// the flood limit was reached.
|
||||
$this->drupalLogin($this->admin_user);
|
||||
$this->assertNoText('Try again later.', 'Admin user not denied access to flooded contact form.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills out a user's personal contact form and submits it.
|
||||
*
|
||||
* @param $account
|
||||
* A user object of the user being contacted.
|
||||
* @param $message
|
||||
* An optional array with the form fields being used.
|
||||
*/
|
||||
protected function submitPersonalContact($account, array $message = array()) {
|
||||
$message += array(
|
||||
'subject' => $this->randomName(16),
|
||||
'message' => $this->randomName(64),
|
||||
);
|
||||
$this->drupalPost('user/' . $account->uid . '/contact', $message, t('Send message'));
|
||||
}
|
||||
}
|
20
modules/contextual/contextual-rtl.css
Normal file
20
modules/contextual/contextual-rtl.css
Normal file
|
@ -0,0 +1,20 @@
|
|||
/**
|
||||
* @file
|
||||
* Stylesheet specific to right-to-left languages.
|
||||
*/
|
||||
|
||||
div.contextual-links-wrapper {
|
||||
left: 5px;
|
||||
right: auto;
|
||||
}
|
||||
div.contextual-links-wrapper ul.contextual-links {
|
||||
-moz-border-radius: 0 4px 4px 4px;
|
||||
-webkit-border-top-left-radius: 0;
|
||||
-webkit-border-top-right-radius: 4px;
|
||||
border-radius: 0 4px 4px 4px;
|
||||
left: 0;
|
||||
right: auto;
|
||||
}
|
||||
a.contextual-links-trigger {
|
||||
text-indent: -90px;
|
||||
}
|
40
modules/contextual/contextual.api.php
Normal file
40
modules/contextual/contextual.api.php
Normal file
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Hooks provided by Contextual module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @addtogroup hooks
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* Alter a contextual links element before it is rendered.
|
||||
*
|
||||
* This hook is invoked by contextual_pre_render_links(). The renderable array
|
||||
* of #type 'contextual_links', containing the entire contextual links data that
|
||||
* is passed in by reference. Further links may be added or existing links can
|
||||
* be altered.
|
||||
*
|
||||
* @param $element
|
||||
* A renderable array representing the contextual links.
|
||||
* @param $items
|
||||
* An associative array containing the original contextual link items, as
|
||||
* generated by menu_contextual_links(), which were used to build
|
||||
* $element['#links'].
|
||||
*
|
||||
* @see hook_menu_contextual_links_alter()
|
||||
* @see contextual_pre_render_links()
|
||||
* @see contextual_element_info()
|
||||
*/
|
||||
function hook_contextual_links_view_alter(&$element, $items) {
|
||||
// Add another class to all contextual link lists to facilitate custom
|
||||
// styling.
|
||||
$element['#attributes']['class'][] = 'custom-class';
|
||||
}
|
||||
|
||||
/**
|
||||
* @} End of "addtogroup hooks".
|
||||
*/
|
103
modules/contextual/contextual.css
Normal file
103
modules/contextual/contextual.css
Normal file
|
@ -0,0 +1,103 @@
|
|||
/**
|
||||
* @file
|
||||
* Stylesheet for the Contextual module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Contextual links regions.
|
||||
*/
|
||||
.contextual-links-region {
|
||||
outline: none;
|
||||
position: relative;
|
||||
}
|
||||
.contextual-links-region-active {
|
||||
outline: #999 dashed 1px;
|
||||
}
|
||||
|
||||
/**
|
||||
* Contextual links.
|
||||
*/
|
||||
div.contextual-links-wrapper {
|
||||
display: none;
|
||||
font-size: 90%;
|
||||
position: absolute;
|
||||
right: 5px; /* LTR */
|
||||
top: 2px;
|
||||
z-index: 999;
|
||||
}
|
||||
html.js div.contextual-links-wrapper {
|
||||
display: block;
|
||||
}
|
||||
a.contextual-links-trigger {
|
||||
background: transparent url(images/gear-select.png) no-repeat 2px 0;
|
||||
border: 1px solid transparent;
|
||||
display: none;
|
||||
height: 18px;
|
||||
margin: 0;
|
||||
padding: 0 2px;
|
||||
outline: none;
|
||||
text-indent: 34px; /* LTR */
|
||||
width: 28px;
|
||||
overflow: hidden;
|
||||
-khtml-border-radius: 4px;
|
||||
-moz-border-radius: 4px;
|
||||
-webkit-border-radius: 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
a.contextual-links-trigger:hover,
|
||||
div.contextual-links-active a.contextual-links-trigger {
|
||||
background-position: 2px -18px;
|
||||
}
|
||||
div.contextual-links-active a.contextual-links-trigger {
|
||||
background-color: #fff;
|
||||
border-color: #ccc;
|
||||
border-bottom: none;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
-moz-border-radius: 4px 4px 0 0;
|
||||
-webkit-border-bottom-left-radius: 0;
|
||||
-webkit-border-bottom-right-radius: 0;
|
||||
border-radius: 4px 4px 0 0;
|
||||
}
|
||||
div.contextual-links-wrapper ul.contextual-links {
|
||||
background-color: #fff;
|
||||
border: 1px solid #ccc;
|
||||
display: none;
|
||||
margin: 0;
|
||||
padding: 0.25em 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
text-align: left;
|
||||
top: 18px;
|
||||
white-space: nowrap;
|
||||
-moz-border-radius: 4px 0 4px 4px; /* LTR */
|
||||
-webkit-border-bottom-left-radius: 4px;
|
||||
-webkit-border-bottom-right-radius: 4px;
|
||||
-webkit-border-top-right-radius: 0; /* LTR */
|
||||
-webkit-border-top-left-radius: 4px; /* LTR */
|
||||
border-radius: 4px 0 4px 4px; /* LTR */
|
||||
}
|
||||
a.contextual-links-trigger-active,
|
||||
div.contextual-links-active a.contextual-links-trigger,
|
||||
div.contextual-links-active ul.contextual-links {
|
||||
display: block;
|
||||
}
|
||||
ul.contextual-links li {
|
||||
line-height: 100%;
|
||||
list-style: none;
|
||||
list-style-image: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
div.contextual-links-wrapper a {
|
||||
text-decoration: none;
|
||||
}
|
||||
ul.contextual-links li a {
|
||||
color: #333 !important;
|
||||
display: block;
|
||||
margin: 0.25em 0;
|
||||
padding: 0.25em 1em 0.25em 0.5em;
|
||||
}
|
||||
ul.contextual-links li a:hover {
|
||||
background-color: #bfdcee;
|
||||
}
|
12
modules/contextual/contextual.info
Normal file
12
modules/contextual/contextual.info
Normal file
|
@ -0,0 +1,12 @@
|
|||
name = Contextual links
|
||||
description = Provides contextual links to perform actions related to elements on a page.
|
||||
package = Core
|
||||
version = VERSION
|
||||
core = 7.x
|
||||
files[] = contextual.test
|
||||
|
||||
; Information added by Drupal.org packaging script on 2017-06-21
|
||||
version = "7.56"
|
||||
project = "drupal"
|
||||
datestamp = "1498069849"
|
||||
|
52
modules/contextual/contextual.js
Normal file
52
modules/contextual/contextual.js
Normal file
|
@ -0,0 +1,52 @@
|
|||
/**
|
||||
* @file
|
||||
* Attaches behaviors for the Contextual module.
|
||||
*/
|
||||
|
||||
(function ($) {
|
||||
|
||||
Drupal.contextualLinks = Drupal.contextualLinks || {};
|
||||
|
||||
/**
|
||||
* Attaches outline behavior for regions associated with contextual links.
|
||||
*/
|
||||
Drupal.behaviors.contextualLinks = {
|
||||
attach: function (context) {
|
||||
$('div.contextual-links-wrapper', context).once('contextual-links', function () {
|
||||
var $wrapper = $(this);
|
||||
var $region = $wrapper.closest('.contextual-links-region');
|
||||
var $links = $wrapper.find('ul.contextual-links');
|
||||
var $trigger = $('<a class="contextual-links-trigger" href="#" />').text(Drupal.t('Configure')).click(
|
||||
function () {
|
||||
$links.stop(true, true).slideToggle(100);
|
||||
$wrapper.toggleClass('contextual-links-active');
|
||||
return false;
|
||||
}
|
||||
);
|
||||
// Attach hover behavior to trigger and ul.contextual-links.
|
||||
$trigger.add($links).hover(
|
||||
function () { $region.addClass('contextual-links-region-active'); },
|
||||
function () { $region.removeClass('contextual-links-region-active'); }
|
||||
);
|
||||
// Hide the contextual links when user clicks a link or rolls out of the .contextual-links-region.
|
||||
$region.bind('mouseleave click', Drupal.contextualLinks.mouseleave);
|
||||
$region.hover(
|
||||
function() { $trigger.addClass('contextual-links-trigger-active'); },
|
||||
function() { $trigger.removeClass('contextual-links-trigger-active'); }
|
||||
);
|
||||
// Prepend the trigger.
|
||||
$wrapper.prepend($trigger);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Disables outline for the region contextual links are associated with.
|
||||
*/
|
||||
Drupal.contextualLinks.mouseleave = function () {
|
||||
$(this)
|
||||
.find('.contextual-links-active').removeClass('contextual-links-active')
|
||||
.find('ul.contextual-links').hide();
|
||||
};
|
||||
|
||||
})(jQuery);
|
169
modules/contextual/contextual.module
Normal file
169
modules/contextual/contextual.module
Normal file
|
@ -0,0 +1,169 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Adds contextual links to perform actions related to elements on a page.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Implements hook_help().
|
||||
*/
|
||||
function contextual_help($path, $arg) {
|
||||
switch ($path) {
|
||||
case 'admin/help#contextual':
|
||||
$output = '';
|
||||
$output .= '<h3>' . t('About') . '</h3>';
|
||||
$output .= '<p>' . t('The Contextual links module displays links related to regions of pages on your site to users with <em>access contextual links</em> permission. For more information, see the online handbook entry for <a href="@contextual">Contextual links module</a>.', array('@contextual' => 'http://drupal.org/documentation/modules/contextual')) . '</p>';
|
||||
$output .= '<h3>' . t('Uses') . '</h3>';
|
||||
$output .= '<dl>';
|
||||
$output .= '<dt>' . t('Displaying contextual links') . '</dt>';
|
||||
$output .= '<dd>' . t('Contextual links are supplied by modules, to give you quick access to tasks associated with regions of pages on your site. For instance, if you have a custom menu block displayed in a sidebar of your site, the Blocks and Menus modules will supply links to configure the block and edit the menu. The Contextual links module collects these links into a list for display by your theme, and also adds JavaScript code to the page to hide the links initially, and display them when your mouse hovers over the block.') . '</dd>';
|
||||
$output .= '</dl>';
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_permission().
|
||||
*/
|
||||
function contextual_permission() {
|
||||
return array(
|
||||
'access contextual links' => array(
|
||||
'title' => t('Use contextual links'),
|
||||
'description' => t('Use contextual links to perform actions related to elements on a page.'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_library().
|
||||
*/
|
||||
function contextual_library() {
|
||||
$path = drupal_get_path('module', 'contextual');
|
||||
$libraries['contextual-links'] = array(
|
||||
'title' => 'Contextual links',
|
||||
'website' => 'http://drupal.org/node/473268',
|
||||
'version' => '1.0',
|
||||
'js' => array(
|
||||
$path . '/contextual.js' => array(),
|
||||
),
|
||||
'css' => array(
|
||||
$path . '/contextual.css' => array(),
|
||||
),
|
||||
);
|
||||
return $libraries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_element_info().
|
||||
*/
|
||||
function contextual_element_info() {
|
||||
$types['contextual_links'] = array(
|
||||
'#pre_render' => array('contextual_pre_render_links'),
|
||||
'#theme' => 'links__contextual',
|
||||
'#links' => array(),
|
||||
'#prefix' => '<div class="contextual-links-wrapper">',
|
||||
'#suffix' => '</div>',
|
||||
'#attributes' => array(
|
||||
'class' => array('contextual-links'),
|
||||
),
|
||||
'#attached' => array(
|
||||
'library' => array(
|
||||
array('contextual', 'contextual-links'),
|
||||
),
|
||||
),
|
||||
);
|
||||
return $types;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_preprocess().
|
||||
*
|
||||
* @see contextual_pre_render_links()
|
||||
*/
|
||||
function contextual_preprocess(&$variables, $hook) {
|
||||
// Nothing to do here if the user is not permitted to access contextual links.
|
||||
if (!user_access('access contextual links')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$hooks = theme_get_registry(FALSE);
|
||||
|
||||
// Determine the primary theme function argument.
|
||||
if (!empty($hooks[$hook]['variables'])) {
|
||||
$keys = array_keys($hooks[$hook]['variables']);
|
||||
$key = $keys[0];
|
||||
}
|
||||
elseif (!empty($hooks[$hook]['render element'])) {
|
||||
$key = $hooks[$hook]['render element'];
|
||||
}
|
||||
if (!empty($key) && isset($variables[$key])) {
|
||||
$element = $variables[$key];
|
||||
}
|
||||
|
||||
if (isset($element) && is_array($element) && !empty($element['#contextual_links'])) {
|
||||
// Initialize the template variable as a renderable array.
|
||||
$variables['title_suffix']['contextual_links'] = array(
|
||||
'#type' => 'contextual_links',
|
||||
'#contextual_links' => $element['#contextual_links'],
|
||||
'#element' => $element,
|
||||
);
|
||||
// Mark this element as potentially having contextual links attached to it.
|
||||
$variables['classes_array'][] = 'contextual-links-region';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a renderable array for contextual links.
|
||||
*
|
||||
* @param $element
|
||||
* A renderable array containing a #contextual_links property, which is a
|
||||
* keyed array. Each key is the name of the implementing module, and each
|
||||
* value is an array that forms the function arguments for
|
||||
* menu_contextual_links(). For example:
|
||||
* @code
|
||||
* array('#contextual_links' => array(
|
||||
* 'block' => array('admin/structure/block/manage', array('system', 'navigation')),
|
||||
* 'menu' => array('admin/structure/menu/manage', array('navigation')),
|
||||
* ))
|
||||
* @endcode
|
||||
*
|
||||
* @return
|
||||
* A renderable array representing contextual links.
|
||||
*
|
||||
* @see menu_contextual_links()
|
||||
* @see contextual_element_info()
|
||||
*/
|
||||
function contextual_pre_render_links($element) {
|
||||
// Retrieve contextual menu links.
|
||||
$items = array();
|
||||
foreach ($element['#contextual_links'] as $module => $args) {
|
||||
$items += menu_contextual_links($module, $args[0], $args[1]);
|
||||
}
|
||||
|
||||
// Transform contextual links into parameters suitable for theme_link().
|
||||
$links = array();
|
||||
foreach ($items as $class => $item) {
|
||||
$class = drupal_html_class($class);
|
||||
$links[$class] = array(
|
||||
'title' => $item['title'],
|
||||
'href' => $item['href'],
|
||||
);
|
||||
// @todo theme_links() should *really* use the same parameters as l().
|
||||
$item['localized_options'] += array('query' => array());
|
||||
$item['localized_options']['query'] += drupal_get_destination();
|
||||
$links[$class] += $item['localized_options'];
|
||||
}
|
||||
$element['#links'] = $links;
|
||||
|
||||
// Allow modules to alter the renderable contextual links element.
|
||||
drupal_alter('contextual_links_view', $element, $items);
|
||||
|
||||
// If there are no links, tell drupal_render() to abort rendering.
|
||||
if (empty($element['#links'])) {
|
||||
$element['#printed'] = TRUE;
|
||||
}
|
||||
|
||||
return $element;
|
||||
}
|
||||
|
49
modules/contextual/contextual.test
Normal file
49
modules/contextual/contextual.test
Normal file
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Tests for contextual.module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Tests accessible links after inaccessible links on dynamic context.
|
||||
*/
|
||||
class ContextualDynamicContextTestCase extends DrupalWebTestCase {
|
||||
protected $profile = 'testing';
|
||||
|
||||
public static function getInfo() {
|
||||
return array(
|
||||
'name' => 'Contextual links on node lists',
|
||||
'description' => 'Tests if contextual links are showing on the front page depending on permissions.',
|
||||
'group' => 'Contextual',
|
||||
);
|
||||
}
|
||||
|
||||
function setUp() {
|
||||
parent::setUp(array('contextual', 'node'));
|
||||
$this->drupalCreateContentType(array('type' => 'page', 'name' => 'Basic page'));
|
||||
$this->drupalCreateContentType(array('type' => 'article', 'name' => 'Article'));
|
||||
$web_user = $this->drupalCreateUser(array('access content', 'access contextual links', 'edit any article content'));
|
||||
$this->drupalLogin($web_user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests contextual links on node lists with different permissions.
|
||||
*/
|
||||
function testNodeLinks() {
|
||||
// Create three nodes in the following order:
|
||||
// - An article, which should be user-editable.
|
||||
// - A page, which should not be user-editable.
|
||||
// - A second article, which should also be user-editable.
|
||||
$node1 = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1));
|
||||
$node2 = $this->drupalCreateNode(array('type' => 'page', 'promote' => 1));
|
||||
$node3 = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1));
|
||||
|
||||
// Now, on the front page, all article nodes should have contextual edit
|
||||
// links. The page node in between should not.
|
||||
$this->drupalGet('node');
|
||||
$this->assertRaw('node/' . $node1->nid . '/edit', 'Edit link for oldest article node showing.');
|
||||
$this->assertNoRaw('node/' . $node2->nid . '/edit', 'No edit link for page nodes.');
|
||||
$this->assertRaw('node/' . $node3->nid . '/edit', 'Edit link for most recent article node showing.');
|
||||
}
|
||||
}
|
BIN
modules/contextual/images/gear-select.png
Normal file
BIN
modules/contextual/images/gear-select.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 506 B |
30
modules/dashboard/dashboard-rtl.css
Normal file
30
modules/dashboard/dashboard-rtl.css
Normal file
|
@ -0,0 +1,30 @@
|
|||
/**
|
||||
* @file
|
||||
* Right-to-left specific stylesheet for the Dashboard module.
|
||||
*/
|
||||
|
||||
#dashboard div.dashboard-region {
|
||||
float: right;
|
||||
}
|
||||
#dashboard #disabled-blocks .block, #dashboard .block-placeholder {
|
||||
float: right;
|
||||
margin: 3px 0 3px 3px;
|
||||
padding: 6px 8px 6px 4px;
|
||||
}
|
||||
#dashboard .canvas-content a.button {
|
||||
margin: 0 10px 0 0;
|
||||
}
|
||||
#dashboard .ui-sortable .block h2 {
|
||||
background-position: right -39px;
|
||||
padding: 0 19px;
|
||||
}
|
||||
#dashboard.customize-inactive #disabled-blocks .block:hover h2 {
|
||||
background-position: right -39px;
|
||||
}
|
||||
#dashboard.customize-inactive .dashboard-region .ui-sortable .block:hover h2 {
|
||||
background-position: right -36px;
|
||||
}
|
||||
#dashboard div#dashboard_main {
|
||||
margin-left: 1%;
|
||||
margin-right: 0;
|
||||
}
|
42
modules/dashboard/dashboard.api.php
Normal file
42
modules/dashboard/dashboard.api.php
Normal file
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Hooks provided by the Dashboard module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @addtogroup hooks
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* Add regions to the dashboard.
|
||||
*
|
||||
* @return
|
||||
* An array whose keys are the names of the dashboard regions and whose
|
||||
* values are the titles that will be displayed in the blocks administration
|
||||
* interface. The keys are also used as theme wrapper functions.
|
||||
*/
|
||||
function hook_dashboard_regions() {
|
||||
// Define a new dashboard region. Your module can also then define
|
||||
// theme_mymodule_dashboard_region() as a theme wrapper function to control
|
||||
// the region's appearance.
|
||||
return array('mymodule_dashboard_region' => "My module's dashboard region");
|
||||
}
|
||||
|
||||
/**
|
||||
* Alter dashboard regions provided by modules.
|
||||
*
|
||||
* @param $regions
|
||||
* An array containing all dashboard regions, in the format provided by
|
||||
* hook_dashboard_regions().
|
||||
*/
|
||||
function hook_dashboard_regions_alter(&$regions) {
|
||||
// Remove the sidebar region defined by the core dashboard module.
|
||||
unset($regions['dashboard_sidebar']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @} End of "addtogroup hooks".
|
||||
*/
|
134
modules/dashboard/dashboard.css
Normal file
134
modules/dashboard/dashboard.css
Normal file
|
@ -0,0 +1,134 @@
|
|||
/**
|
||||
* @file
|
||||
* Stylesheet for the Dashboard module.
|
||||
*/
|
||||
|
||||
#dashboard div.dashboard-region {
|
||||
float: left;
|
||||
min-height: 1px;
|
||||
}
|
||||
|
||||
#dashboard div#dashboard_main {
|
||||
width: 65%;
|
||||
margin-right: 1%; /* LTR */
|
||||
}
|
||||
|
||||
#dashboard div#dashboard_sidebar {
|
||||
width: 33%;
|
||||
}
|
||||
|
||||
#dashboard div.block {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
#dashboard .dashboard-region .block {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
#dashboard div.block h2 {
|
||||
float: none;
|
||||
}
|
||||
|
||||
#dashboard #disabled-blocks .block,
|
||||
#dashboard .block-placeholder {
|
||||
background: #e2e1dc;
|
||||
padding: 6px 4px 6px 8px; /* LTR */
|
||||
margin: 3px 3px 3px 0; /* LTR */
|
||||
float: left; /* LTR */
|
||||
-moz-border-radius: 4px;
|
||||
-webkit-border-radius: 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
#dashboard .dashboard-add-other-blocks {
|
||||
margin: 10px 0 0 0;
|
||||
}
|
||||
|
||||
#dashboard .ui-sortable {
|
||||
border: 2px dashed #ccc;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
#dashboard .canvas-content {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
#dashboard #disabled-blocks .ui-sortable {
|
||||
padding: 0;
|
||||
background-color: #777;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
#dashboard .canvas-content a.button {
|
||||
margin: 0 0 0 10px; /* LTR */
|
||||
color: #5a5a5a;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#dashboard .region {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
#dashboard #disabled-blocks .region {
|
||||
background-color: #E0E0D8;
|
||||
border: #ccc 1px solid;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
#dashboard #disabled-blocks {
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
#dashboard #disabled-blocks h2 {
|
||||
display: inline;
|
||||
font-weight: normal;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#dashboard #disabled-blocks .block {
|
||||
background: #444;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#dashboard.customize-inactive #disabled-blocks .block:hover {
|
||||
background: #0074BD;
|
||||
}
|
||||
|
||||
#dashboard #disabled-blocks .block .content,
|
||||
#dashboard .ui-sortable-helper .content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#dashboard .ui-sortable .block {
|
||||
cursor: move;
|
||||
min-height: 1px;
|
||||
}
|
||||
|
||||
#dashboard .ui-sortable .block h2 {
|
||||
background: transparent url(../../misc/draggable.png) no-repeat 0px -39px;
|
||||
padding: 0 17px;
|
||||
}
|
||||
|
||||
#dashboard.customize-inactive #disabled-blocks .block:hover h2 {
|
||||
background: #0074BD url(../../misc/draggable.png) no-repeat 0px -39px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#dashboard.customize-inactive .dashboard-region .ui-sortable .block:hover h2 {
|
||||
background: #0074BD url(../../misc/draggable.png) no-repeat;
|
||||
background-position: 3px -36px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#dashboard .dashboard-region .block-placeholder {
|
||||
margin: 0 0 20px 0;
|
||||
padding: 0;
|
||||
display: block;
|
||||
height: 1.6em;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#dashboard #disabled-blocks .block-placeholder {
|
||||
width: 30px;
|
||||
height: 1.6em;
|
||||
}
|
14
modules/dashboard/dashboard.info
Normal file
14
modules/dashboard/dashboard.info
Normal file
|
@ -0,0 +1,14 @@
|
|||
name = Dashboard
|
||||
description = Provides a dashboard page in the administrative interface for organizing administrative tasks and tracking information within your site.
|
||||
core = 7.x
|
||||
package = Core
|
||||
version = VERSION
|
||||
files[] = dashboard.test
|
||||
dependencies[] = block
|
||||
configure = admin/dashboard/customize
|
||||
|
||||
; Information added by Drupal.org packaging script on 2017-06-21
|
||||
version = "7.56"
|
||||
project = "drupal"
|
||||
datestamp = "1498069849"
|
||||
|
78
modules/dashboard/dashboard.install
Normal file
78
modules/dashboard/dashboard.install
Normal file
|
@ -0,0 +1,78 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Install, update and uninstall functions for the dashboard module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Implements hook_disable().
|
||||
*
|
||||
* Stash a list of blocks enabled on the dashboard, so they can be re-enabled
|
||||
* if the dashboard is re-enabled. Then disable those blocks, since the
|
||||
* dashboard regions will no longer be defined.
|
||||
*/
|
||||
function dashboard_disable() {
|
||||
// Stash a list of currently enabled blocks.
|
||||
$stashed_blocks = array();
|
||||
|
||||
$result = db_select('block', 'b')
|
||||
->fields('b', array('module', 'delta', 'region'))
|
||||
->condition('b.region', dashboard_regions(), 'IN')
|
||||
->execute();
|
||||
|
||||
foreach ($result as $block) {
|
||||
$stashed_blocks[] = array(
|
||||
'module' => $block->module,
|
||||
'delta' => $block->delta,
|
||||
'region' => $block->region,
|
||||
);
|
||||
}
|
||||
variable_set('dashboard_stashed_blocks', $stashed_blocks);
|
||||
|
||||
// Disable the dashboard blocks.
|
||||
db_update('block')
|
||||
->fields(array(
|
||||
'status' => 0,
|
||||
'region' => BLOCK_REGION_NONE,
|
||||
))
|
||||
->condition('region', dashboard_regions(), 'IN')
|
||||
->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_enable().
|
||||
*
|
||||
* Restores blocks to the dashboard that were there when the dashboard module
|
||||
* was disabled.
|
||||
*/
|
||||
function dashboard_enable() {
|
||||
global $theme_key;
|
||||
if (!$stashed_blocks = variable_get('dashboard_stashed_blocks')) {
|
||||
return;
|
||||
}
|
||||
if (!$admin_theme = variable_get('admin_theme')) {
|
||||
drupal_theme_initialize();
|
||||
$admin_theme = $theme_key;
|
||||
}
|
||||
foreach ($stashed_blocks as $block) {
|
||||
db_update('block')
|
||||
->fields(array(
|
||||
'status' => 1,
|
||||
'region' => $block['region']
|
||||
))
|
||||
->condition('module', $block['module'])
|
||||
->condition('delta', $block['delta'])
|
||||
->condition('theme', $admin_theme)
|
||||
->condition('status', 0)
|
||||
->execute();
|
||||
}
|
||||
variable_del('dashboard_stashed_blocks');
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_uninstall().
|
||||
*/
|
||||
function dashboard_uninstall() {
|
||||
variable_del('dashboard_stashed_blocks');
|
||||
}
|
223
modules/dashboard/dashboard.js
Normal file
223
modules/dashboard/dashboard.js
Normal file
|
@ -0,0 +1,223 @@
|
|||
/**
|
||||
* @file
|
||||
* Attaches behaviors for the Dashboard module.
|
||||
*/
|
||||
|
||||
(function ($) {
|
||||
|
||||
/**
|
||||
* Implements Drupal.behaviors for the Dashboard module.
|
||||
*/
|
||||
Drupal.behaviors.dashboard = {
|
||||
attach: function (context, settings) {
|
||||
$('#dashboard', context).once(function () {
|
||||
$(this).prepend('<div class="customize clearfix"><ul class="action-links"><li><a href="#">' + Drupal.t('Customize dashboard') + '</a></li></ul><div class="canvas"></div></div>');
|
||||
$('.customize .action-links a', this).click(Drupal.behaviors.dashboard.enterCustomizeMode);
|
||||
});
|
||||
Drupal.behaviors.dashboard.addPlaceholders();
|
||||
if (Drupal.settings.dashboard.launchCustomize) {
|
||||
Drupal.behaviors.dashboard.enterCustomizeMode();
|
||||
}
|
||||
},
|
||||
|
||||
addPlaceholders: function() {
|
||||
$('#dashboard .dashboard-region .region').each(function () {
|
||||
var empty_text = "";
|
||||
// If the region is empty
|
||||
if ($('.block', this).length == 0) {
|
||||
// Check if we are in customize mode and grab the correct empty text
|
||||
if ($('#dashboard').hasClass('customize-mode')) {
|
||||
empty_text = Drupal.settings.dashboard.emptyRegionTextActive;
|
||||
} else {
|
||||
empty_text = Drupal.settings.dashboard.emptyRegionTextInactive;
|
||||
}
|
||||
// We need a placeholder.
|
||||
if ($('.dashboard-placeholder', this).length == 0) {
|
||||
$(this).append('<div class="dashboard-placeholder"></div>');
|
||||
}
|
||||
$('.dashboard-placeholder', this).html(empty_text);
|
||||
}
|
||||
else {
|
||||
$('.dashboard-placeholder', this).remove();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Enters "customize" mode by displaying disabled blocks.
|
||||
*/
|
||||
enterCustomizeMode: function () {
|
||||
$('#dashboard').addClass('customize-mode customize-inactive');
|
||||
Drupal.behaviors.dashboard.addPlaceholders();
|
||||
// Hide the customize link
|
||||
$('#dashboard .customize .action-links').hide();
|
||||
// Load up the disabled blocks
|
||||
$('div.customize .canvas').load(Drupal.settings.dashboard.drawer, Drupal.behaviors.dashboard.setupDrawer);
|
||||
},
|
||||
|
||||
/**
|
||||
* Exits "customize" mode by simply forcing a page refresh.
|
||||
*/
|
||||
exitCustomizeMode: function () {
|
||||
$('#dashboard').removeClass('customize-mode customize-inactive');
|
||||
Drupal.behaviors.dashboard.addPlaceholders();
|
||||
location.href = Drupal.settings.dashboard.dashboard;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets up the drag-and-drop behavior and the 'close' button.
|
||||
*/
|
||||
setupDrawer: function () {
|
||||
$('div.customize .canvas-content input').click(Drupal.behaviors.dashboard.exitCustomizeMode);
|
||||
$('div.customize .canvas-content').append('<a class="button" href="' + Drupal.settings.dashboard.dashboard + '">' + Drupal.t('Done') + '</a>');
|
||||
|
||||
// Initialize drag-and-drop.
|
||||
var regions = $('#dashboard div.region');
|
||||
regions.sortable({
|
||||
connectWith: regions,
|
||||
cursor: 'move',
|
||||
cursorAt: {top:0},
|
||||
dropOnEmpty: true,
|
||||
items: '> div.block, > div.disabled-block',
|
||||
placeholder: 'block-placeholder clearfix',
|
||||
tolerance: 'pointer',
|
||||
start: Drupal.behaviors.dashboard.start,
|
||||
over: Drupal.behaviors.dashboard.over,
|
||||
sort: Drupal.behaviors.dashboard.sort,
|
||||
update: Drupal.behaviors.dashboard.update
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Makes the block appear as a disabled block while dragging.
|
||||
*
|
||||
* This function is called on the jQuery UI Sortable "start" event.
|
||||
*
|
||||
* @param event
|
||||
* The event that triggered this callback.
|
||||
* @param ui
|
||||
* An object containing information about the item that is being dragged.
|
||||
*/
|
||||
start: function (event, ui) {
|
||||
$('#dashboard').removeClass('customize-inactive');
|
||||
var item = $(ui.item);
|
||||
|
||||
// If the block is already in disabled state, don't do anything.
|
||||
if (!item.hasClass('disabled-block')) {
|
||||
item.css({height: 'auto'});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Adapts block's width to the region it is moved into while dragging.
|
||||
*
|
||||
* This function is called on the jQuery UI Sortable "over" event.
|
||||
*
|
||||
* @param event
|
||||
* The event that triggered this callback.
|
||||
* @param ui
|
||||
* An object containing information about the item that is being dragged.
|
||||
*/
|
||||
over: function (event, ui) {
|
||||
var item = $(ui.item);
|
||||
|
||||
// If the block is in disabled state, remove width.
|
||||
if ($(this).closest('#disabled-blocks').length) {
|
||||
item.css('width', '');
|
||||
}
|
||||
else {
|
||||
item.css('width', $(this).width());
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Adapts a block's position to stay connected with the mouse pointer.
|
||||
*
|
||||
* This function is called on the jQuery UI Sortable "sort" event.
|
||||
*
|
||||
* @param event
|
||||
* The event that triggered this callback.
|
||||
* @param ui
|
||||
* An object containing information about the item that is being dragged.
|
||||
*/
|
||||
sort: function (event, ui) {
|
||||
var item = $(ui.item);
|
||||
|
||||
if (event.pageX > ui.offset.left + item.width()) {
|
||||
item.css('left', event.pageX);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Sends block order to the server, and expand previously disabled blocks.
|
||||
*
|
||||
* This function is called on the jQuery UI Sortable "update" event.
|
||||
*
|
||||
* @param event
|
||||
* The event that triggered this callback.
|
||||
* @param ui
|
||||
* An object containing information about the item that was just dropped.
|
||||
*/
|
||||
update: function (event, ui) {
|
||||
$('#dashboard').addClass('customize-inactive');
|
||||
var item = $(ui.item);
|
||||
|
||||
// If the user dragged a disabled block, load the block contents.
|
||||
if (item.hasClass('disabled-block')) {
|
||||
var module, delta, itemClass;
|
||||
itemClass = item.attr('class');
|
||||
// Determine the block module and delta.
|
||||
module = itemClass.match(/\bmodule-(\S+)\b/)[1];
|
||||
delta = itemClass.match(/\bdelta-(\S+)\b/)[1];
|
||||
|
||||
// Load the newly enabled block's content.
|
||||
$.get(Drupal.settings.dashboard.blockContent + '/' + module + '/' + delta, {},
|
||||
function (block) {
|
||||
if (block) {
|
||||
item.html(block);
|
||||
}
|
||||
|
||||
if (item.find('div.content').is(':empty')) {
|
||||
item.find('div.content').html(Drupal.settings.dashboard.emptyBlockText);
|
||||
}
|
||||
|
||||
Drupal.attachBehaviors(item);
|
||||
},
|
||||
'html'
|
||||
);
|
||||
// Remove the "disabled-block" class, so we don't reload its content the
|
||||
// next time it's dragged.
|
||||
item.removeClass("disabled-block");
|
||||
}
|
||||
|
||||
Drupal.behaviors.dashboard.addPlaceholders();
|
||||
|
||||
// Let the server know what the new block order is.
|
||||
$.post(Drupal.settings.dashboard.updatePath, {
|
||||
'form_token': Drupal.settings.dashboard.formToken,
|
||||
'regions': Drupal.behaviors.dashboard.getOrder
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the current order of the blocks in each of the sortable regions.
|
||||
*
|
||||
* @return
|
||||
* The current order of the blocks, in query string format.
|
||||
*/
|
||||
getOrder: function () {
|
||||
var order = [];
|
||||
$('#dashboard div.region').each(function () {
|
||||
var region = $(this).parent().attr('id').replace(/-/g, '_');
|
||||
var blocks = $(this).sortable('toArray');
|
||||
$.each(blocks, function() {
|
||||
order.push(region + '[]=' + this);
|
||||
});
|
||||
});
|
||||
order = order.join('&');
|
||||
return order;
|
||||
}
|
||||
};
|
||||
|
||||
})(jQuery);
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue