Initial code using Drupal 6.38

This commit is contained in:
Manuel Cillero 2017-07-24 15:21:05 +02:00
commit 4824608a33
467 changed files with 90887 additions and 0 deletions

8
modules/README.txt Normal file
View file

@ -0,0 +1,8 @@
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

View file

@ -0,0 +1,34 @@
<?php
/**
* @file aggregator-feed-source.tpl.php
* Default theme implementation to present the source of the feed.
*
* The contents are render 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()
*/
?>
<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>

View file

@ -0,0 +1,45 @@
<?php
/**
* @file aggregator-item.tpl.php
* 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()
*/
?>
<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>

View file

@ -0,0 +1,4 @@
#aggregator .feed-source .feed-icon {
float: left;
}

View file

@ -0,0 +1,18 @@
<?php
/**
* @file aggregator-summary-item.tpl.php
* 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()
*/
?>
<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; ?>

View file

@ -0,0 +1,23 @@
<?php
/**
* @file aggregator-summary-items.tpl.php
* 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()
*/
?>
<h2><?php print $title; ?></h2>
<?php print $summary_list; ?>
<div class="links">
<a href="<?php print $source_url; ?>"><?php print t('More'); ?></a>
</div>

View file

@ -0,0 +1,18 @@
<?php
/**
* @file comment-wrapper.tpl.php
* 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_comment_wrapper()
*/
?>
<div id="aggregator">
<?php print $content; ?>
<?php print $pager; ?>
</div>

View file

@ -0,0 +1,351 @@
<?php
/**
* @file
* Admin page callbacks for the aggregator module.
*/
/**
* Menu callback; displays the aggregator administration page.
*/
function aggregator_admin_overview() {
return aggregator_view();
}
/**
* Displays the aggregator administration page.
*
* @return
* The page HTML.
*/
function aggregator_view() {
$result = db_query('SELECT f.*, 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.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();
while ($feed = db_fetch_object($result)) {
$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(time() - $feed->checked))) : t('never')),
($feed->checked ? t('%time left', array('%time' => format_interval($feed->checked + $feed->refresh - time()))) : t('never')),
l(t('edit'), "admin/content/aggregator/edit/feed/$feed->fid"),
l(t('remove items'), "admin/content/aggregator/remove/$feed->fid"),
l(t('update items'), "admin/content/aggregator/update/$feed->fid", array('query' => array('token' => drupal_get_token("aggregator/update/$feed->fid")))),
);
}
$output .= theme('table', $header, $rows);
$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();
while ($category = db_fetch_object($result)) {
$rows[] = array(l($category->title, "aggregator/categories/$category->cid"), format_plural($category->items, '1 item', '@count items'), l(t('edit'), "admin/content/aggregator/edit/category/$category->cid"));
}
$output .= theme('table', $header, $rows);
return $output;
}
/**
* Form builder; Generate a form to add/edit feed sources.
*
* @ingroup forms
* @see aggregator_form_feed_validate()
* @see aggregator_form_feed_submit()
*/
function aggregator_form_feed(&$form_state, $edit = array('refresh' => 900, 'title' => '', 'url' => '', 'fid' => NULL)) {
$period = drupal_map_assoc(array(900, 1800, 3600, 7200, 10800, 21600, 32400, 43200, 64800, 86400, 172800, 259200, 604800, 1209600, 2419200), 'format_interval');
if ($edit['refresh'] == '') {
$edit['refresh'] = 3600;
}
$form['title'] = array('#type' => 'textfield',
'#title' => t('Title'),
'#default_value' => $edit['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' => $edit['url'],
'#maxlength' => 255,
'#description' => t('The fully-qualified URL of the feed.'),
'#required' => TRUE,
);
$form['refresh'] = array('#type' => 'select',
'#title' => t('Update interval'),
'#default_value' => $edit['refresh'],
'#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'))),
);
// 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 = %d ORDER BY title', $edit['fid']);
while ($category = db_fetch_object($categories)) {
$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['submit'] = array('#type' => 'submit', '#value' => t('Save'));
if ($edit['fid']) {
$form['delete'] = array('#type' => 'submit', '#value' => t('Delete'));
$form['fid'] = array('#type' => 'hidden', '#value' => $edit['fid']);
}
return $form;
}
/**
* Validate aggregator_form_feed form submissions.
*/
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. Please 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 = '%s' OR url = '%s') AND fid <> %d", $form_state['values']['title'], $form_state['values']['url'], $form_state['values']['fid']);
}
else {
$result = db_query("SELECT title, url FROM {aggregator_feed} WHERE title = '%s' OR url = '%s'", $form_state['values']['title'], $form_state['values']['url']);
}
while ($feed = db_fetch_object($result)) {
if (strcasecmp($feed->title, $form_state['values']['title']) == 0) {
form_set_error('title', t('A feed named %feed already exists. Please 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. Please enter a unique URL.', array('%url' => $form_state['values']['url'])));
}
}
}
}
/**
* Process aggregator_form_feed form submissions.
*
* @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/content/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/content/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/content/aggregator'));
drupal_set_message(t('The feed %feed has been added.', array('%feed' => $form_state['values']['title'])));
}
}
function aggregator_admin_remove_feed($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/content/aggregator',
t('This action cannot be undone.'),
t('Remove items'),
t('Cancel')
);
}
/**
* Remove all items from a feed and redirect to the overview page.
*
* @param $feed
* An associative array describing the feed to be cleared.
*/
function aggregator_admin_remove_feed_submit($form, &$form_state) {
aggregator_remove($form_state['values']['feed']);
$form_state['redirect'] = 'admin/content/aggregator';
}
/**
* Menu callback; refreshes a feed, then redirects to the overview page.
*
* @param $feed
* An associative array 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 drupal_access_denied();
}
aggregator_refresh($feed);
drupal_goto('admin/content/aggregator');
}
/**
* Form builder; Configure the aggregator system.
*
* @ingroup forms
* @see system_settings_form()
*/
function aggregator_admin_settings() {
$items = array(0 => t('none')) + 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');
$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. (Tags in this list are not removed by Drupal.)')
);
$form['aggregator_summary_items'] = array(
'#type' => 'select', '#title' => t('Items shown in sources and categories pages') ,
'#default_value' => variable_get('aggregator_summary_items', 3), '#options' => $items,
'#description' => t('Number of feed items displayed in feed and category summary pages.')
);
$form['aggregator_clear'] = array(
'#type' => 'select', '#title' => t('Discard items older than'),
'#default_value' => variable_get('aggregator_clear', 9676800), '#options' => $period,
'#description' => t('The length of time to retain feed items before discarding. (Requires a correctly configured <a href="@cron">cron maintenance task</a>.)', array('@cron' => url('admin/reports/status')))
);
$form['aggregator_category_selector'] = array(
'#type' => 'radios', '#title' => t('Category selection type'), '#default_value' => variable_get('aggregator_category_selector', 'checkboxes'),
'#options' => array('checkboxes' => t('checkboxes'), 'select' => t('multiple selector')),
'#description' => t('The type of category selection widget displayed on categorization pages. (For a small number of categories, checkboxes are easier to use, while a multiple selector work well with large numbers of categories.)')
);
return system_settings_form($form);
}
/**
* Form builder; Generate a form to add/edit/delete aggregator categories.
*
* @ingroup forms
* @see aggregator_form_category_validate()
* @see aggregator_form_category_submit()
*/
function aggregator_form_category(&$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['submit'] = array('#type' => 'submit', '#value' => t('Save'));
if ($edit['cid']) {
$form['delete'] = array('#type' => 'submit', '#value' => t('Delete'));
$form['cid'] = array('#type' => 'hidden', '#value' => $edit['cid']);
}
return $form;
}
/**
* Validate aggregator_form_feed form submissions.
*/
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_fetch_object(db_query("SELECT cid FROM {aggregator_category} WHERE title = '%s' AND cid <> %d", $form_state['values']['title'], $form_state['values']['cid']));
}
else {
$category = db_fetch_object(db_query("SELECT cid FROM {aggregator_category} WHERE title = '%s'", $form_state['values']['title']));
}
if ($category) {
form_set_error('title', t('A category named %category already exists. Please enter a unique title.', array('%category' => $form_state['values']['title'])));
}
}
}
/**
* Process aggregator_form_category form submissions.
*
* @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/content/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/content/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/content/aggregator'));
drupal_set_message(t('The category %category has been added.', array('%category' => $form_state['values']['title'])));
}
}

View file

@ -0,0 +1,37 @@
#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;
}

View file

@ -0,0 +1,11 @@
name = Aggregator
description = "Aggregates syndicated content (RSS, RDF, and Atom feeds)."
package = Core - optional
version = VERSION
core = 6.x
; Information added by Drupal.org packaging script on 2016-02-24
version = "6.38"
project = "drupal"
datestamp = "1456343372"

View file

@ -0,0 +1,240 @@
<?php
/**
* Implementation of hook_install().
*/
function aggregator_install() {
// Create tables.
drupal_install_schema('aggregator');
}
/**
* Implementation of hook_uninstall().
*/
function aggregator_uninstall() {
// Remove tables.
drupal_uninstall_schema('aggregator');
variable_del('aggregator_allowed_html_tags');
variable_del('aggregator_summary_items');
variable_del('aggregator_clear');
variable_del('aggregator_category_selector');
}
/**
* Implementation of 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')),
);
$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')),
);
$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' => 'varchar',
'length' => 255,
'not null' => TRUE,
'default' => '',
'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.',
),
'link' => array(
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
'default' => '',
'description' => 'The parent website of the feed; comes from the &lt;link&gt; element in the feed.',
),
'description' => array(
'type' => 'text',
'not null' => TRUE,
'size' => 'big',
'description' => "The parent website's description; comes from the &lt;description&gt; element in the feed.",
),
'image' => array(
'type' => 'text',
'not null' => TRUE,
'size' => 'big',
'description' => 'An image representing the feed.',
),
'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'),
'unique keys' => array(
'url' => array('url'),
'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' => 'varchar',
'length' => 255,
'not null' => TRUE,
'default' => '',
'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' => 'Post date of feed item, as a Unix timestamp.',
),
'guid' => array(
'type' => 'varchar',
'length' => 255,
'not null' => FALSE,
'description' => 'Unique identifier for the feed item.',
)
),
'primary key' => array('iid'),
'indexes' => array('fid' => array('fid')),
);
return $schema;
}

View file

@ -0,0 +1,940 @@
<?php
/**
* @file
* Used to aggregate syndicated content (RSS, RDF, and Atom).
*/
/**
* Implementation of hook_help().
*/
function aggregator_help($path, $arg) {
switch ($path) {
case 'admin/help#aggregator':
$output = '<p>'. t('The aggregator is a powerful on-site syndicator and news reader that gathers 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 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('Feeds contain feed items, or individual posts published by the site providing the feed. Feeds may be grouped in categories, generally by topic. Users view feed items in the <a href="@aggregator">main aggregator display</a> or by <a href="@aggregator-sources">their source</a>. Administrators can <a href="@feededit">add, edit and delete feeds</a> and choose how often to check each feed for newly updated items. The most recent items in either a feed or category can be displayed as a block through the <a href="@admin-block">blocks administration page</a>. A <a href="@aggregator-opml">machine-readable OPML file</a> of all feeds is available. A correctly configured <a href="@cron">cron maintenance task</a> is required to update feeds automatically.', array('@aggregator' => url('aggregator'), '@aggregator-sources' => url('aggregator/sources'), '@feededit' => url('admin/content/aggregator'), '@admin-block' => url('admin/build/block'), '@aggregator-opml' => url('aggregator/opml'), '@cron' => url('admin/reports/status'))) .'</p>';
$output .= '<p>'. t('For more information, see the online handbook entry for <a href="@aggregator">Aggregator module</a>.', array('@aggregator' => 'http://drupal.org/handbook/modules/aggregator/')) .'</p>';
return $output;
case 'admin/content/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/content/aggregator/add/feed'), '@block' => url('admin/build/block'))) .'</p>';
return $output;
case 'admin/content/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/content/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>';
}
}
/**
* Implementation of hook_theme()
*/
function aggregator_theme() {
return array(
'aggregator_wrapper' => array(
'arguments' => array('content' => NULL),
'file' => 'aggregator.pages.inc',
'template' => 'aggregator-wrapper',
),
'aggregator_categorize_items' => array(
'arguments' => array('form' => NULL),
'file' => 'aggregator.pages.inc',
),
'aggregator_feed_source' => array(
'arguments' => array('feed' => NULL),
'file' => 'aggregator.pages.inc',
'template' => 'aggregator-feed-source',
),
'aggregator_block_item' => array(
'arguments' => array('item' => NULL, 'feed' => 0),
),
'aggregator_summary_items' => array(
'arguments' => array('summary_items' => NULL, 'source' => NULL),
'file' => 'aggregator.pages.inc',
'template' => 'aggregator-summary-items',
),
'aggregator_summary_item' => array(
'arguments' => array('item' => NULL),
'file' => 'aggregator.pages.inc',
'template' => 'aggregator-summary-item',
),
'aggregator_item' => array(
'arguments' => array('item' => NULL),
'file' => 'aggregator.pages.inc',
'template' => 'aggregator-item',
),
'aggregator_page_opml' => array(
'arguments' => array('feeds' => NULL),
'file' => 'aggregator.pages.inc',
),
'aggregator_page_rss' => array(
'arguments' => array('feeds' => NULL, 'category' => NULL),
'file' => 'aggregator.pages.inc',
),
);
}
/**
* Implementation of hook_menu().
*/
function aggregator_menu() {
$items['admin/content/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'),
'file' => 'aggregator.admin.inc',
);
$items['admin/content/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_TASK,
'parent' => 'admin/content/aggregator',
'file' => 'aggregator.admin.inc',
);
$items['admin/content/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_TASK,
'parent' => 'admin/content/aggregator',
'file' => 'aggregator.admin.inc',
);
$items['admin/content/aggregator/remove/%aggregator_feed'] = array(
'title' => 'Remove items',
'page callback' => 'drupal_get_form',
'page arguments' => array('aggregator_admin_remove_feed', 4),
'access arguments' => array('administer news feeds'),
'type' => MENU_CALLBACK,
'file' => 'aggregator.admin.inc',
);
$items['admin/content/aggregator/update/%aggregator_feed'] = array(
'title' => 'Update items',
'page callback' => 'aggregator_admin_refresh_feed',
'page arguments' => array(4),
'access arguments' => array('administer news feeds'),
'type' => MENU_CALLBACK,
'file' => 'aggregator.admin.inc',
);
$items['admin/content/aggregator/list'] = array(
'title' => 'List',
'type' => MENU_DEFAULT_LOCAL_TASK,
'weight' => -10,
);
$items['admin/content/aggregator/settings'] = array(
'title' => 'Settings',
'page callback' => 'drupal_get_form',
'page arguments' => array('aggregator_admin_settings'),
'type' => MENU_LOCAL_TASK,
'weight' => 10,
'access arguments' => array('administer news feeds'),
'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 callback' => 'user_access',
'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', 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'),
'type' => MENU_CALLBACK,
'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', 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/content/aggregator/edit/feed/%aggregator_feed'] = array(
'title' => 'Edit feed',
'page callback' => 'drupal_get_form',
'page arguments' => array('aggregator_form_feed', 5),
'access arguments' => array('administer news feeds'),
'type' => MENU_CALLBACK,
'file' => 'aggregator.admin.inc',
);
$items['admin/content/aggregator/edit/category/%aggregator_category'] = array(
'title' => 'Edit category',
'page callback' => 'drupal_get_form',
'page arguments' => array('aggregator_form_category', 5),
'access arguments' => array('administer news feeds'),
'type' => MENU_CALLBACK,
'file' => 'aggregator.admin.inc',
);
return $items;
}
/**
* Menu callback.
*
* @return
* An aggregator category title.
*/
function _aggregator_category_title($category) {
return $category['title'];
}
/**
* Implementation of hook_init().
*/
function aggregator_init() {
drupal_add_css(drupal_get_path('module', 'aggregator') .'/aggregator.css');
}
/**
* Find out 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') && db_result(db_query('SELECT COUNT(*) FROM {aggregator_category}'));
}
/**
* Implementation of hook_perm().
*/
function aggregator_perm() {
return array('administer news feeds', 'access news feeds');
}
/**
* Implementation of hook_cron().
*
* Checks news feeds for updates once their refresh interval has elapsed.
*/
function aggregator_cron() {
$result = db_query('SELECT * FROM {aggregator_feed} WHERE checked + refresh < %d', time());
while ($feed = db_fetch_array($result)) {
aggregator_refresh($feed);
}
}
/**
* Implementation of hook_block().
*
* Generates blocks for the latest news items in each category and feed.
*/
function aggregator_block($op = 'list', $delta = 0, $edit = array()) {
if ($op == 'list') {
$result = db_query('SELECT cid, title FROM {aggregator_category} ORDER BY title');
while ($category = db_fetch_object($result)) {
$block['category-'. $category->cid]['info'] = t('!title category latest items', array('!title' => $category->title));
}
$result = db_query('SELECT fid, title FROM {aggregator_feed} ORDER BY fid');
while ($feed = db_fetch_object($result)) {
$block['feed-'. $feed->fid]['info'] = t('!title feed latest items', array('!title' => $feed->title));
}
}
else if ($op == 'configure') {
list($type, $id) = explode('-', $delta);
if ($type == 'category') {
$value = db_result(db_query('SELECT block FROM {aggregator_category} WHERE cid = %d', $id));
}
else {
$value = db_result(db_query('SELECT block FROM {aggregator_feed} WHERE fid = %d', $id));
}
$form['block'] = array('#type' => 'select', '#title' => t('Number of news items in block'), '#default_value' => $value, '#options' => drupal_map_assoc(array(2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)));
return $form;
}
else if ($op == 'save') {
list($type, $id) = explode('-', $delta);
if ($type == 'category') {
$value = db_query('UPDATE {aggregator_category} SET block = %d WHERE cid = %d', $edit['block'], $id);
}
else {
$value = db_query('UPDATE {aggregator_feed} SET block = %d WHERE fid = %d', $edit['block'], $id);
}
}
else if ($op == 'view') {
if (user_access('access news feeds')) {
list($type, $id) = explode('-', $delta);
switch ($type) {
case 'feed':
if ($feed = db_fetch_object(db_query('SELECT fid, title, block FROM {aggregator_feed} WHERE fid = %d', $id))) {
$block['subject'] = check_plain($feed->title);
$result = db_query_range('SELECT * FROM {aggregator_item} WHERE fid = %d ORDER BY timestamp DESC, iid DESC', $feed->fid, 0, $feed->block);
$read_more = theme('more_link', url('aggregator/sources/'. $feed->fid), t("View this feed's recent news."));
}
break;
case 'category':
if ($category = db_fetch_object(db_query('SELECT cid, title, block FROM {aggregator_category} WHERE cid = %d', $id))) {
$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 = %d ORDER BY i.timestamp DESC, i.iid DESC', $category->cid, 0, $category->block);
$read_more = theme('more_link', url('aggregator/categories/'. $category->cid), t("View this category's recent news."));
}
break;
}
$items = array();
while ($item = db_fetch_object($result)) {
$items[] = theme('aggregator_block_item', $item);
}
// Only display the block if there are items to show.
if (count($items) > 0) {
$block['content'] = theme('item_list', $items) . $read_more;
}
}
}
if (isset($block)) {
return $block;
}
}
/**
* Add/edit/delete 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_query("UPDATE {aggregator_category} SET title = '%s', description = '%s' WHERE cid = %d", $edit['title'], $edit['description'], $edit['cid']);
$op = 'update';
}
else {
db_query('DELETE FROM {aggregator_category} WHERE cid = %d', $edit['cid']);
// Make sure there is no active block for this category.
db_query("DELETE FROM {blocks} WHERE module = '%s' AND delta = '%s'", 'aggregator', 'category-' . $edit['cid']);
$edit['title'] = '';
$op = 'delete';
}
}
else if (!empty($edit['title'])) {
// A single unique id for bundles and feeds, to use in blocks
db_query("INSERT INTO {aggregator_category} (title, description, block) VALUES ('%s', '%s', 5)", $edit['title'], $edit['description']);
$link_path .= db_last_insert_id('aggregator_category', 'cid');
$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_query('DELETE FROM {aggregator_category_feed} WHERE fid = %d', $edit['fid']);
}
if (!empty($edit['fid']) && !empty($edit['title'])) {
db_query("UPDATE {aggregator_feed} SET title = '%s', url = '%s', refresh = %d WHERE fid = %d", $edit['title'], $edit['url'], $edit['refresh'], $edit['fid']);
}
else if (!empty($edit['fid'])) {
$items = array();
$result = db_query('SELECT iid FROM {aggregator_item} WHERE fid = %d', $edit['fid']);
while ($item = db_fetch_object($result)) {
$items[] = "iid = $item->iid";
}
if (!empty($items)) {
db_query('DELETE FROM {aggregator_category_item} WHERE '. implode(' OR ', $items));
}
db_query('DELETE FROM {aggregator_feed} WHERE fid = %d', $edit['fid']);
db_query('DELETE FROM {aggregator_item} WHERE fid = %d', $edit['fid']);
// Make sure there is no active block for this feed.
db_query("DELETE FROM {blocks} WHERE module = '%s' AND delta = '%s'", 'aggregator', 'feed-' . $edit['fid']);
}
else if (!empty($edit['title'])) {
db_query("INSERT INTO {aggregator_feed} (title, url, refresh, block, description, image) VALUES ('%s', '%s', %d, 5, '', '')", $edit['title'], $edit['url'], $edit['refresh']);
// A single unique id for bundles and feeds, to use in blocks.
$edit['fid'] = db_last_insert_id('aggregator_feed', 'fid');
}
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_query('INSERT INTO {aggregator_category_feed} (fid, cid) VALUES (%d, %d)', $edit['fid'], $cid);
}
}
}
}
}
/**
* Removes all items from a feed.
*
* @param $feed
* An associative array describing the feed to be cleared.
*/
function aggregator_remove($feed) {
$result = db_query('SELECT iid FROM {aggregator_item} WHERE fid = %d', $feed['fid']);
while ($item = db_fetch_object($result)) {
$items[] = "iid = $item->iid";
}
if (!empty($items)) {
db_query('DELETE FROM {aggregator_category_item} WHERE '. implode(' OR ', $items));
}
db_query('DELETE FROM {aggregator_item} WHERE fid = %d', $feed['fid']);
db_query("UPDATE {aggregator_feed} SET checked = 0, etag = '', modified = 0 WHERE fid = %d", $feed['fid']);
drupal_set_message(t('The news items from %site have been removed.', array('%site' => $feed['title'])));
}
/**
* Call-back function used by the XML parser.
*/
function aggregator_element_start($parser, $name, $attributes) {
global $item, $element, $tag, $items, $channel;
switch ($name) {
case 'IMAGE':
case 'TEXTINPUT':
case 'CONTENT':
case 'SUMMARY':
case 'TAGLINE':
case 'SUBTITLE':
case 'LOGO':
case 'INFO':
$element = $name;
break;
case 'ID':
if ($element != 'ITEM') {
$element = $name;
}
case 'LINK':
if (!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;
}
/**
* Call-back function used by the XML parser.
*/
function aggregator_element_end($parser, $name) {
global $element;
switch ($name) {
case 'IMAGE':
case 'TEXTINPUT':
case 'ITEM':
case 'ENTRY':
case 'CONTENT':
case 'INFO':
$element = '';
break;
case 'ID':
if ($element == 'ID') {
$element = '';
}
}
}
/**
* Call-back function used by the XML parser.
*/
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;
}
}
/**
* Checks a news feed for new items.
*
* @param $feed
* An associative array describing the feed to be refreshed.
*/
function aggregator_refresh($feed) {
global $channel, $image;
// Generate conditional GET headers.
$headers = array();
if ($feed['etag']) {
$headers['If-None-Match'] = $feed['etag'];
}
if ($feed['modified']) {
$headers['If-Modified-Since'] = gmdate('D, d M Y H:i:s', $feed['modified']) .' GMT';
}
// Request feed.
$result = drupal_http_request($feed['url'], $headers);
// Process HTTP response code.
switch ($result->code) {
case 304:
db_query('UPDATE {aggregator_feed} SET checked = %d WHERE fid = %d', time(), $feed['fid']);
drupal_set_message(t('There is no new syndicated content from %site.', array('%site' => $feed['title'])));
break;
case 301:
$feed['url'] = $result->redirect_url;
watchdog('aggregator', 'Updated URL for feed %title to %url.', array('%title' => $feed['title'], '%url' => $feed['url']));
// Deliberate no break.
case 200:
case 302:
case 307:
// Filter the input data:
if (aggregator_parse_feed($result->data, $feed)) {
$modified = empty($result->headers['Last-Modified']) ? 0 : strtotime($result->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);
}
if (!empty($image['LINK']) && !empty($image['URL']) && !empty($image['TITLE'])) {
// Note, we should really use theme_image() here but that only works with local images it won't work with images fetched with a URL unless PHP version > 5
$image = '<a href="'. check_url($image['LINK']) .'" class="feed-image"><img src="'. check_url($image['URL']) .'" alt="'. check_plain($image['TITLE']) .'" /></a>';
}
else {
$image = NULL;
}
$etag = empty($result->headers['ETag']) ? '' : $result->headers['ETag'];
// Update the feed data.
db_query("UPDATE {aggregator_feed} SET url = '%s', checked = %d, link = '%s', description = '%s', image = '%s', etag = '%s', modified = %d WHERE fid = %d", $feed['url'], time(), $channel['LINK'], $channel['DESCRIPTION'], $image, $etag, $modified, $feed['fid']);
// Clear the cache.
cache_clear_all();
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'])));
}
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)));
}
}
/**
* Parse 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]);
// calc 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? then subtract offset
if ($tz_mod == '+') {
$offset_secs *= -1;
}
$epoch += $offset_secs;
}
return $epoch;
}
else {
return FALSE;
}
}
/**
* Parse a feed and store its items.
*
* @param $data
* The feed data.
* @param $feed
* An associative array describing the feed to be parsed.
* @return
* 0 on error, 1 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 0;
}
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 variables.
$title = $link = $author = $description = $guid = NULL;
foreach ($items as $item) {
unset($title, $link, $author, $description, $guid);
// 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'])) {
$title = $item['TITLE'];
}
elseif (!empty($item['DESCRIPTION'])) {
$title = preg_replace('/^(.*)[^\w;&].*?$/', "\\1", truncate_utf8($item['DESCRIPTION'], 40));
}
else {
$title = '';
}
// Resolve the items link.
if (!empty($item['LINK'])) {
$link = $item['LINK'];
}
else {
$link = $feed['link'];
}
// Atom feeds use ID rather than GUID.
if (isset($item['GUID'])) {
$guid = $item['GUID'];
}
elseif (isset($item['ID'])) {
$guid = $item['ID'];
}
else {
$guid = '';
}
// Atom feeds have a CONTENT and/or SUMMARY tag instead of a DESCRIPTION tag.
if (!empty($item['CONTENT:ENCODED'])) {
$item['DESCRIPTION'] = $item['CONTENT:ENCODED'];
}
else if (!empty($item['SUMMARY'])) {
$item['DESCRIPTION'] = $item['SUMMARY'];
}
else if (!empty($item['CONTENT'])) {
$item['DESCRIPTION'] = $item['CONTENT'];
}
// Try to resolve and parse the item's publication date. If no date is
// found, we use the current date instead.
$date = 'now';
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;
}
}
$timestamp = strtotime($date); // As of PHP 5.1.0, strtotime returns FALSE on failure instead of -1.
if ($timestamp <= 0) {
$timestamp = aggregator_parse_w3cdtf($date); // Returns FALSE on failure
if (!$timestamp) {
$timestamp = time(); // better than nothing
}
}
// 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'];
}
// 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($guid)) {
$entry = db_fetch_object(db_query("SELECT iid FROM {aggregator_item} WHERE fid = %d AND guid = '%s'", $feed['fid'], $guid));
}
else if ($link && $link != $feed['link'] && $link != $feed['url']) {
$entry = db_fetch_object(db_query("SELECT iid FROM {aggregator_item} WHERE fid = %d AND link = '%s'", $feed['fid'], $link));
}
else {
$entry = db_fetch_object(db_query("SELECT iid FROM {aggregator_item} WHERE fid = %d AND title = '%s'", $feed['fid'], $title));
}
$item += array('AUTHOR' => '', 'DESCRIPTION' => '');
aggregator_save_item(array('iid' => (isset($entry->iid) ? $entry->iid: ''), 'fid' => $feed['fid'], 'timestamp' => $timestamp, 'title' => $title, 'link' => $link, 'author' => $item['AUTHOR'], 'description' => $item['DESCRIPTION'], 'guid' => $guid));
}
// Remove all items that are older than flush item timer.
$age = time() - variable_get('aggregator_clear', 9676800);
$result = db_query('SELECT iid FROM {aggregator_item} WHERE fid = %d AND timestamp < %d', $feed['fid'], $age);
$items = array();
$num_rows = FALSE;
while ($item = db_fetch_object($result)) {
$items[] = $item->iid;
$num_rows = TRUE;
}
if ($num_rows) {
db_query('DELETE FROM {aggregator_category_item} WHERE iid IN ('. implode(', ', $items) .')');
db_query('DELETE FROM {aggregator_item} WHERE fid = %d AND timestamp < %d', $feed['fid'], $age);
}
return 1;
}
/**
* Add/edit/delete an aggregator item.
*
* @param $edit
* An associative array describing the item to be added/edited/deleted.
*/
function aggregator_save_item($edit) {
if ($edit['iid'] && $edit['title']) {
db_query("UPDATE {aggregator_item} SET title = '%s', link = '%s', author = '%s', description = '%s', guid = '%s', timestamp = %d WHERE iid = %d", $edit['title'], $edit['link'], $edit['author'], $edit['description'], $edit['guid'], $edit['timestamp'], $edit['iid']);
}
else if ($edit['iid']) {
db_query('DELETE FROM {aggregator_item} WHERE iid = %d', $edit['iid']);
db_query('DELETE FROM {aggregator_category_item} WHERE iid = %d', $edit['iid']);
}
else if ($edit['title'] && $edit['link']) {
db_query("INSERT INTO {aggregator_item} (fid, title, link, author, description, timestamp, guid) VALUES (%d, '%s', '%s', '%s', '%s', %d, '%s')", $edit['fid'], $edit['title'], $edit['link'], $edit['author'], $edit['description'], $edit['timestamp'], $edit['guid']);
$edit['iid'] = db_last_insert_id('aggregator_item', 'iid');
// file the items in the categories indicated by the feed
$categories = db_query('SELECT cid FROM {aggregator_category_feed} WHERE fid = %d', $edit['fid']);
while ($category = db_fetch_object($categories)) {
db_query('INSERT INTO {aggregator_category_item} (cid, iid) VALUES (%d, %d)', $category->cid, $edit['iid']);
}
}
}
/**
* Load an aggregator feed.
*
* @param $fid
* The feed id.
* @return
* An associative array describing the feed.
*/
function aggregator_feed_load($fid) {
static $feeds;
if (!isset($feeds[$fid])) {
$feeds[$fid] = db_fetch_array(db_query('SELECT * FROM {aggregator_feed} WHERE fid = %d', $fid));
}
return $feeds[$fid];
}
/**
* Load an aggregator category.
*
* @param $cid
* The category id.
* @return
* An associative array describing the category.
*/
function aggregator_category_load($cid) {
static $categories;
if (!isset($categories[$cid])) {
$categories[$cid] = db_fetch_array(db_query('SELECT * FROM {aggregator_category} WHERE cid = %d', $cid));
}
return $categories[$cid];
}
/**
* Format an individual feed item for display in the block.
*
* @param $item
* The item to be displayed.
* @param $feed
* Not used.
* @return
* The item HTML.
* @ingroup themeable
*/
function theme_aggregator_block_item($item, $feed = 0) {
global $user;
$output = '';
if ($user->uid && module_exists('blog') && user_access('create blog entries')) {
if ($image = theme('image', 'misc/blog.png', t('blog it'), t('blog it'))) {
$output .= '<div class="icon">'. l($image, 'node/add/blog', array('attributes' => array('title' => t('Comment on this news item in your personal blog.'), 'class' => 'blog-it'), 'query' => "iid=$item->iid", 'html' => TRUE)) .'</div>';
}
}
// Display the external link to the item.
$output .= '<a href="'. check_url($item->link) .'">'. check_plain($item->title) ."</a>\n";
return $output;
}
/**
* Safely render HTML content, 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));
}
/**
* Helper function for drupal_map_assoc.
*
* @param $count
* Items count.
* @return
* Plural-formatted "@count items"
*/
function _aggregator_items($count) {
return format_plural($count, '1 item', '@count items');
}

View file

@ -0,0 +1,490 @@
<?php
/**
* @file
* User page callbacks for the aggregator module.
*/
/**
* Menu callback; displays the most recent items gathered from any feed.
*
* @return
* The items HTML.
*/
function aggregator_page_last() {
drupal_add_feed(url('aggregator/rss'), variable_get('site_name', 'Drupal') .' '. t('aggregator'));
$items = aggregator_feed_items_load('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');
return _aggregator_page_list($items, arg(1));
}
/**
* Menu callback; displays all the items captured from a particular feed.
*
* If there are two arguments then this function is the categorize form.
*
* @param $arg1
* If there are two arguments then $arg1 is $form_state. Otherwise, $arg1 is $feed.
* @param $arg2
* If there are two arguments then $arg2 is feed.
* @return
* The items HTML.
*/
function aggregator_page_source($arg1, $arg2 = NULL) {
// If there are two arguments then this function is the categorize form, and
// $arg1 is $form_state and $arg2 is $feed. Otherwise, $arg1 is $feed.
$feed = is_array($arg2) ? $arg2 : $arg1;
$feed = (object)$feed;
drupal_set_title(check_plain($feed->title));
$feed_source = theme('aggregator_feed_source', $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('SELECT * FROM {aggregator_item} WHERE fid = '. $feed->fid .' ORDER BY timestamp DESC, iid DESC');
return _aggregator_page_list($items, arg(3), $feed_source);
}
/**
* Menu callback; displays all the items aggregated in a particular category.
*
* If there are two arguments then this function is called as a form.
*
* @param $arg1
* If there are two arguments then $arg1 is $form_state. Otherwise, $arg1 is $category.
* @param $arg2
* If there are two arguments then $arg2 is $category.
* @return
* The items HTML.
*/
function aggregator_page_category($arg1, $arg2 = NULL) {
// If there are two arguments then we are called as a form, $arg1 is
// $form_state and $arg2 is $category. Otherwise, $arg1 is $category.
$category = is_array($arg2) ? $arg2 : $arg1;
drupal_add_feed(url('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('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 = '. $category['cid'] .' ORDER BY timestamp DESC, i.iid DESC');
return _aggregator_page_list($items, arg(3));
}
/**
* Load feed items by passing a SQL query.
*
* @param $sql
* The query to be executed.
* @return
* An array of the feed items.
*/
function aggregator_feed_items_load($sql) {
$items = array();
if (isset($sql)) {
$result = pager_query($sql, 20);
while ($item = db_fetch_object($result)) {
$result_category = 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 = %d ORDER BY c.title', $item->iid);
$item->categories = array();
while ($item_categories = db_fetch_object($result_category)) {
$item->categories[] = $item_categories;
}
$items[$item->iid] = $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 items HTML.
*/
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', $item);
}
$output = theme('aggregator_wrapper', $output);
}
return $output;
}
/**
* Form builder; build the page list form.
*
* @param $items
* An array of the feed items.
* @param $feed_source
* The feed source URL.
* @return
* The form structure.
* @ingroup forms
* @see aggregator_categorize_items_validate()
* @see aggregator_categorize_items_submit()
*/
function aggregator_categorize_items($items, $feed_source = '') {
$form['#submit'][] = 'aggregator_categorize_items_submit';
$form['#validate'][] = 'aggregator_categorize_items_validate';
$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('#value' => theme('aggregator_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 = %d', $item->iid);
$selected = array();
while ($category = db_fetch_object($categories_result)) {
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['submit'] = array('#type' => 'submit', '#value' => t('Save categories'));
return $form;
}
/**
* Validate aggregator_categorize_items form submissions.
*/
function aggregator_categorize_items_validate($form, &$form_state) {
if (!user_access('administer news feeds')) {
form_error($form, t('You are not allowed to categorize this feed item.'));
}
}
/**
* Process aggregator_categorize_items form submissions.
*/
function aggregator_categorize_items_submit($form, &$form_state) {
if (!empty($form_state['values']['categories'])) {
foreach ($form_state['values']['categories'] as $iid => $selection) {
db_query('DELETE FROM {aggregator_category_item} WHERE iid = %d', $iid);
foreach ($selection as $cid) {
if ($cid) {
db_query('INSERT INTO {aggregator_category_item} (cid, iid) VALUES (%d, %d)', $cid, $iid);
}
}
}
}
drupal_set_message(t('The categories have been saved.'));
}
/**
* Theme the page list form for assigning categories.
*
* @param $form
* An associative array containing the structure of the form.
* @return
* The output HTML.
* @ingroup themeable
*/
function theme_aggregator_categorize_items($form) {
$output = drupal_render($form['feed_source']);
$rows = array();
if ($form['items']) {
foreach (element_children($form['items']) as $key) {
if (is_array($form['items'][$key])) {
$rows[] = array(
drupal_render($form['items'][$key]),
array('data' => drupal_render($form['categories'][$key]), 'class' => 'categorize-item'),
);
}
}
}
$output .= theme('table', array('', t('Categorize')), $rows);
$output .= drupal_render($form['submit']);
$output .= drupal_render($form);
return theme('aggregator_wrapper', $output);
}
/**
* Process variables for aggregator-wrapper.tpl.php.
*
* @see aggregator-wrapper.tpl.php
*/
function template_preprocess_aggregator_wrapper(&$variables) {
$variables['pager'] = theme('pager', NULL, 20, 0);
}
/**
* Process 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(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);
}
}
/**
* Menu callback; displays all the feeds used by the aggregator.
*/
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 = '';
while ($feed = db_fetch_object($result)) {
// 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 = %d ORDER BY i.timestamp DESC', $feed->fid, 0, variable_get('aggregator_summary_items', 3));
while ($item = db_fetch_object($items)) {
$summary_items[] = theme('aggregator_summary_item', $item);
}
}
$feed->url = url('aggregator/sources/'. $feed->fid);
$output .= theme('aggregator_summary_items', $summary_items, $feed);
}
$output .= theme('feed_icon', url('aggregator/opml'), t('OPML feed'));
return theme('aggregator_wrapper', $output);
}
/**
* Menu callback; displays all the categories used by the aggregator.
*/
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 = '';
while ($category = db_fetch_object($result)) {
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 = %d ORDER BY i.timestamp DESC', $category->cid, 0, variable_get('aggregator_summary_items', 3));
while ($item = db_fetch_object($items)) {
$summary_items[] = theme('aggregator_summary_item', $item);
}
}
$category->url = url('aggregator/categories/'. $category->cid);
$output .= theme('aggregator_summary_items', $summary_items, $category);
}
return theme('aggregator_wrapper', $output);
}
/**
* Menu callback; generate an RSS 0.92 feed of aggregator items or categories.
*/
function aggregator_page_rss() {
$result = NULL;
// arg(2) is the passed cid, only select for that category
if (arg(2)) {
$category = db_fetch_object(db_query('SELECT cid, title FROM {aggregator_category} WHERE cid = %d', arg(2)));
$sql = '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 = %d ORDER BY timestamp DESC, i.iid DESC';
$result = db_query_range($sql, $category->cid, 0, variable_get('feed_default_items', 10));
}
// or, get the default aggregator items
else {
$category = NULL;
$sql = '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';
$result = db_query_range($sql, 0, variable_get('feed_default_items', 10));
}
$feeds = array();
while ($item = db_fetch_object($result)) {
$feeds[] = $item;
}
return theme('aggregator_page_rss', $feeds, $category);
}
/**
* Theme the RSS output.
*
* @param $feeds
* An array of the feeds to theme.
* @param $category
* A common category, if any, for all the feeds.
* @ingroup themeable
*/
function theme_aggregator_page_rss($feeds, $category = NULL) {
drupal_set_header('Content-Type: application/rss+xml; charset=utf-8');
$items = '';
$feed_length = variable_get('feed_item_length', 'teaser');
foreach ($feeds as $feed) {
switch ($feed_length) {
case 'teaser':
$teaser = node_teaser($feed->description);
if ($teaser != $feed->description) {
$teaser .= '<p><a href="'. check_url($feed->link) .'">'. t('read more') ."</a></p>\n";
}
$feed->description = $teaser;
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;
}
/**
* Menu callback; generates an OPML representation of all feeds.
*
* @param $cid
* If set, feeds are exported only from a category with this ID. Otherwise, all feeds are exported.
* @return
* The output XML.
*/
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 = %d ORDER BY title', $cid);
}
else {
$result = db_query('SELECT * FROM {aggregator_feed} ORDER BY title');
}
$feeds = array();
while ($item = db_fetch_object($result)) {
$feeds[] = $item;
}
return theme('aggregator_page_opml', $feeds);
}
/**
* Theme the OPML feed output.
*
* @param $feeds
* An array of the feeds to theme.
* @ingroup themeable
*/
function theme_aggregator_page_opml($feeds) {
drupal_set_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('r') ."</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;
}
/**
* Process variables for aggregator-summary-items.tpl.php.
*
* @see aggregator-summary-item.tpl.php
*/
function template_preprocess_aggregator_summary_items(&$variables) {
$variables['title'] = check_plain($variables['source']->title);
$variables['summary_list'] = theme('item_list', $variables['summary_items']);
$variables['source_url'] = $variables['source']->url;
}
/**
* Process 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(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);
}
}
/**
* Process 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', $feed->url, t('!title feed', array('!title' => $feed->title)));
$variables['source_image'] = $feed->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(time() - $feed->checked)));
}
else {
$variables['last_checked'] = t('never');
}
if (user_access('administer news feeds')) {
$variables['last_checked'] = l($variables['last_checked'], 'admin/content/aggregator');
}
}

View file

@ -0,0 +1,75 @@
<?php
/**
* @file block-admin-display-form.tpl.php
* 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.
* - $throttle: TRUE or FALSE depending on throttle module being enabled.
*
* 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->throttle_check: Checkbox to enable throttling.
* - $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()
*/
?>
<?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>
<?php if ($throttle): ?>
<th><?php print t('Throttle'); ?></th>
<?php endif; ?>
<th colspan="2"><?php print t('Operations'); ?></th>
</tr>
</thead>
<tbody>
<?php $row = 0; ?>
<?php foreach ($block_regions as $region => $title): ?>
<tr class="region region-<?php print $region?>">
<td colspan="<?php print $throttle ? '6' : '5'; ?>" class="region"><?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="<?php print $throttle ? '6' : '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>
<?php if ($throttle): ?>
<td><?php print $data->throttle_check; ?></td>
<?php endif; ?>
<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; ?>

View file

@ -0,0 +1,404 @@
<?php
/**
* @file
* Admin page callbacks for the block module.
*/
/**
* Menu callback for admin/build/block.
*/
function block_admin_display($theme = NULL) {
global $custom_theme;
// If non-default theme configuration has been selected, set the custom theme.
$custom_theme = isset($theme) ? $theme : variable_get('theme_default', 'garland');
// Fetch and sort blocks
$blocks = _block_rehash();
usort($blocks, '_block_compare');
return drupal_get_form('block_admin_display_form', $blocks, $theme);
}
/**
* Generate main blocks administration form.
*/
function block_admin_display_form(&$form_state, $blocks, $theme = NULL) {
global $theme_key, $custom_theme;
// Add CSS
drupal_add_css(drupal_get_path('module', 'block') .'/block.css', 'module', 'all', FALSE);
// If non-default theme configuration has been selected, set the custom theme.
$custom_theme = isset($theme) ? $theme : variable_get('theme_default', 'garland');
init_theme();
$throttle = module_exists('throttle');
$block_regions = system_region_list($theme_key) + array(BLOCK_REGION_NONE => '<'. t('none') .'>');
// 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 form tree
$form = array(
'#action' => arg(4) ? url('admin/build/block/list/'. $theme_key) : url('admin/build/block'),
'#tree' => TRUE,
);
foreach ($blocks as $i => $block) {
$key = $block['module'] .'_'. $block['delta'];
$form[$key]['module'] = array(
'#type' => 'value',
'#value' => $block['module'],
);
$form[$key]['delta'] = array(
'#type' => 'value',
'#value' => $block['delta'],
);
$form[$key]['info'] = array(
'#value' => check_plain($block['info'])
);
$form[$key]['theme'] = array(
'#type' => 'hidden',
'#value' => $theme_key
);
$form[$key]['weight'] = array(
'#type' => 'weight',
'#default_value' => $block['weight'],
'#delta' => $weight_delta,
);
$form[$key]['region'] = array(
'#type' => 'select',
'#default_value' => $block['region'],
'#options' => $block_regions,
);
if ($throttle) {
$form[$key]['throttle'] = array('#type' => 'checkbox', '#default_value' => isset($block['throttle']) ? $block['throttle'] : FALSE);
}
$form[$key]['configure'] = array('#value' => l(t('configure'), 'admin/build/block/configure/'. $block['module'] .'/'. $block['delta']));
if ($block['module'] == 'block') {
$form[$key]['delete'] = array('#value' => l(t('delete'), 'admin/build/block/delete/'. $block['delta']));
}
}
$form['submit'] = array(
'#type' => 'submit',
'#value' => t('Save blocks'),
);
return $form;
}
/**
* Process main blocks administration form submission.
*/
function block_admin_display_form_submit($form, &$form_state) {
foreach ($form_state['values'] as $block) {
$block['status'] = $block['region'] != BLOCK_REGION_NONE;
$block['region'] = $block['status'] ? $block['region'] : '';
db_query("UPDATE {blocks} SET status = %d, weight = %d, region = '%s', throttle = %d WHERE module = '%s' AND delta = '%s' AND theme = '%s'", $block['status'], $block['weight'], $block['region'], isset($block['throttle']) ? $block['throttle'] : 0, $block['module'], $block['delta'], $block['theme']);
}
drupal_set_message(t('The block settings have been updated.'));
cache_clear_all();
}
/**
* Helper function for sorting blocks on admin/build/block.
*
* Active blocks are sorted by region, then by weight.
* Disabled blocks are sorted by name.
*/
function _block_compare($a, $b) {
global $theme_key;
static $regions;
// We need the region list to correctly order by region.
if (!isset($regions)) {
$regions = array_flip(array_keys(system_region_list($theme_key)));
$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.
$weight = $a['weight'] - $b['weight'];
if ($weight) {
return $weight;
}
// Sort by title.
return strcmp($a['info'], $b['info']);
}
/**
* Menu callback; displays the block configuration form.
*/
function block_admin_configure(&$form_state, $module = NULL, $delta = 0) {
$form['module'] = array('#type' => 'value', '#value' => $module);
$form['delta'] = array('#type' => 'value', '#value' => $delta);
$edit = db_fetch_array(db_query("SELECT pages, visibility, custom, title FROM {blocks} WHERE module = '%s' AND delta = '%s'", $module, $delta));
$form['block_settings'] = array(
'#type' => 'fieldset',
'#title' => t('Block specific settings'),
'#collapsible' => TRUE,
);
$form['block_settings']['title'] = array(
'#type' => 'textfield',
'#title' => t('Block title'),
'#maxlength' => 64,
'#description' => $module == 'block' ? t('The title of the block as shown to the user.') : t('Override the default title for the block. Use <em>&lt;none&gt;</em> to display no title, or leave blank to use the default block title.'),
'#default_value' => $edit['title'],
'#weight' => -18,
);
// Module-specific block configurations.
if ($settings = module_invoke($module, 'block', 'configure', $delta)) {
foreach ($settings as $k => $v) {
$form['block_settings'][$k] = $v;
}
}
// Get the block subject for the page title.
$info = module_invoke($module, 'block', 'list');
if (isset($info[$delta])) {
drupal_set_title(t("'%name' block", array('%name' => $info[$delta]['info'])));
}
// Standard block configurations.
$form['user_vis_settings'] = array(
'#type' => 'fieldset',
'#title' => t('User specific visibility settings'),
'#collapsible' => TRUE,
);
$form['user_vis_settings']['custom'] = array(
'#type' => 'radios',
'#title' => t('Custom visibility settings'),
'#options' => array(
t('Users cannot control whether or not they see this block.'),
t('Show this block by default, but let individual users hide it.'),
t('Hide this block by default but let individual users show it.')
),
'#description' => t('Allow individual users to customize the visibility of this block in their account settings.'),
'#default_value' => $edit['custom'],
);
// Role-based visibility settings
$default_role_options = array();
$result = db_query("SELECT rid FROM {blocks_roles} WHERE module = '%s' AND delta = '%s'", $module, $delta);
while ($role = db_fetch_object($result)) {
$default_role_options[] = $role->rid;
}
$result = db_query('SELECT rid, name FROM {role} ORDER BY name');
$role_options = array();
while ($role = db_fetch_object($result)) {
$role_options[$role->rid] = $role->name;
}
$form['role_vis_settings'] = array(
'#type' => 'fieldset',
'#title' => t('Role specific visibility settings'),
'#collapsible' => TRUE,
);
$form['role_vis_settings']['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.'),
);
$form['page_vis_settings'] = array(
'#type' => 'fieldset',
'#title' => t('Page specific visibility settings'),
'#collapsible' => TRUE,
);
$access = user_access('use PHP for block visibility');
if ($edit['visibility'] == 2 && !$access) {
$form['page_vis_settings'] = array();
$form['page_vis_settings']['visibility'] = array('#type' => 'value', '#value' => 2);
$form['page_vis_settings']['pages'] = array('#type' => 'value', '#value' => $edit['pages']);
}
else {
$options = array(t('Show on every page except the listed pages.'), t('Show on only the listed pages.'));
$description = t("Enter one page per line as Drupal paths. 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 ($access) {
$options[] = t('Show if the following PHP code returns <code>TRUE</code> (PHP-mode, experts only).');
$description .= ' '. t('If the PHP-mode is chosen, enter PHP code between %php. Note that executing incorrect PHP-code can break your Drupal site.', array('%php' => '<?php ?>'));
}
$form['page_vis_settings']['visibility'] = array(
'#type' => 'radios',
'#title' => t('Show block on specific pages'),
'#options' => $options,
'#default_value' => $edit['visibility'],
);
$form['page_vis_settings']['pages'] = array(
'#type' => 'textarea',
'#title' => t('Pages'),
'#default_value' => $edit['pages'],
'#description' => $description,
);
}
$form['submit'] = array(
'#type' => 'submit',
'#value' => t('Save block'),
);
return $form;
}
function block_admin_configure_validate($form, &$form_state) {
if ($form_state['values']['module'] == 'block') {
if (empty($form_state['values']['info']) || db_result(db_query("SELECT COUNT(*) FROM {boxes} WHERE bid != %d AND info = '%s'", $form_state['values']['delta'], $form_state['values']['info']))) {
form_set_error('info', t('Please ensure that each block description is unique.'));
}
}
}
function block_admin_configure_submit($form, &$form_state) {
if (!form_get_errors()) {
db_query("UPDATE {blocks} SET visibility = %d, pages = '%s', custom = %d, title = '%s' WHERE module = '%s' AND delta = '%s'", $form_state['values']['visibility'], trim($form_state['values']['pages']), $form_state['values']['custom'], $form_state['values']['title'], $form_state['values']['module'], $form_state['values']['delta']);
db_query("DELETE FROM {blocks_roles} WHERE module = '%s' AND delta = '%s'", $form_state['values']['module'], $form_state['values']['delta']);
foreach (array_filter($form_state['values']['roles']) as $rid) {
db_query("INSERT INTO {blocks_roles} (rid, module, delta) VALUES (%d, '%s', '%s')", $rid, $form_state['values']['module'], $form_state['values']['delta']);
}
module_invoke($form_state['values']['module'], 'block', 'save', $form_state['values']['delta'], $form_state['values']);
drupal_set_message(t('The block configuration has been saved.'));
cache_clear_all();
$form_state['redirect'] = 'admin/build/block';
return;
}
}
/**
* Menu callback: display the custom block addition form.
*/
function block_add_block_form(&$form_state) {
return block_admin_configure($form_state, 'block', NULL);
}
function block_add_block_form_validate($form, &$form_state) {
if (empty($form_state['values']['info']) || db_result(db_query("SELECT COUNT(*) FROM {boxes} WHERE info = '%s'", $form_state['values']['info']))) {
form_set_error('info', t('Please ensure that each block description is unique.'));
}
}
/**
* Save the new custom block.
*/
function block_add_block_form_submit($form, &$form_state) {
db_query("INSERT INTO {boxes} (body, info, format) VALUES ('%s', '%s', %d)", $form_state['values']['body'], $form_state['values']['info'], $form_state['values']['format']);
$delta = db_last_insert_id('boxes', 'bid');
foreach (list_themes() as $key => $theme) {
if ($theme->status) {
db_query("INSERT INTO {blocks} (visibility, pages, custom, title, module, theme, status, weight, delta, cache) VALUES(%d, '%s', %d, '%s', '%s', '%s', %d, %d, '%s', %d)", $form_state['values']['visibility'], trim($form_state['values']['pages']), $form_state['values']['custom'], $form_state['values']['title'], $form_state['values']['module'], $theme->name, 0, 0, $delta, BLOCK_NO_CACHE);
}
}
foreach (array_filter($form_state['values']['roles']) as $rid) {
db_query("INSERT INTO {blocks_roles} (rid, module, delta) VALUES (%d, '%s', '%s')", $rid, $form_state['values']['module'], $delta);
}
drupal_set_message(t('The block has been created.'));
cache_clear_all();
$form_state['redirect'] = 'admin/build/block';
return;
}
/**
* Menu callback; confirm deletion of custom blocks.
*/
function block_box_delete(&$form_state, $bid = 0) {
$box = block_box_get($bid);
$form['info'] = array('#type' => 'hidden', '#value' => $box['info']);
$form['bid'] = array('#type' => 'hidden', '#value' => $bid);
return confirm_form($form, t('Are you sure you want to delete the block %name?', array('%name' => $box['info'])), 'admin/build/block', '', t('Delete'), t('Cancel'));
}
/**
* Deletion of custom blocks.
*/
function block_box_delete_submit($form, &$form_state) {
db_query('DELETE FROM {boxes} WHERE bid = %d', $form_state['values']['bid']);
db_query("DELETE FROM {blocks} WHERE module = 'block' AND delta = '%s'", $form_state['values']['bid']);
drupal_set_message(t('The block %name has been removed.', array('%name' => $form_state['values']['info'])));
cache_clear_all();
$form_state['redirect'] = 'admin/build/block';
return;
}
/**
* Process variables for block-admin-display.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) {
global $theme_key;
$block_regions = system_region_list($theme_key);
$variables['throttle'] = module_exists('throttle');
$variables['block_regions'] = $block_regions + array(BLOCK_REGION_NONE => t('Disabled'));
foreach ($block_regions as $key => $value) {
// Highlight regions on page to provide visual reference.
drupal_set_content($key, '<div class="block-region">'. $value .'</div>');
// Initialize an empty array for the region.
$variables['block_listing'][$key] = array();
}
// Initialize disabled blocks array.
$variables['block_listing'][BLOCK_REGION_NONE] = array();
// Set up to track previous region in loop.
$last_region = '';
foreach (element_children($variables['form']) as $i) {
$block = &$variables['form'][$i];
// Only take form elements that are blocks.
if (isset($block['info'])) {
// Fetch region for current block.
$region = $block['region']['#default_value'];
// Set special classes needed for table drag and drop.
$variables['form'][$i]['region']['#attributes']['class'] = 'block-region-select block-region-'. $region;
$variables['form'][$i]['weight']['#attributes']['class'] = 'block-weight block-weight-'. $region;
$variables['block_listing'][$region][$i] = new stdClass();
$variables['block_listing'][$region][$i]->row_class = isset($block['#attributes']['class']) ? $block['#attributes']['class'] : '';
$variables['block_listing'][$region][$i]->block_modified = isset($block['#attributes']['class']) && strpos($block['#attributes']['class'], 'block-modified') !== FALSE ? TRUE : FALSE;
$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]->throttle_check = $variables['throttle'] ? drupal_render($block['throttle']) : '';
$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;
$last_region = $region;
}
}
$variables['form_submit'] = drupal_render($variables['form']);
}

17
modules/block/block.css Normal file
View file

@ -0,0 +1,17 @@
#blocks td.region {
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;
}

11
modules/block/block.info Normal file
View file

@ -0,0 +1,11 @@
name = Block
description = Controls the boxes that are displayed around the main content.
package = Core - required
version = VERSION
core = 6.x
; Information added by Drupal.org packaging script on 2016-02-24
version = "6.38"
project = "drupal"
datestamp = "1456343372"

178
modules/block/block.install Normal file
View file

@ -0,0 +1,178 @@
<?php
/**
* Implementation of hook_schema().
*/
function block_schema() {
$schema['blocks'] = 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,
'size' => 'tiny',
'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)',
),
'throttle' => array(
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'size' => 'tiny',
'description' => 'Flag to indicate whether or not to remove block when website traffic is high. (1 = throttle, 0 = do not throttle)',
),
'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' => 64,
'not null' => TRUE,
'default' => '',
'description' => 'Custom title for the block. (Empty string will use block default title, &lt;none&gt; will remove the title, text will cause block to use specified title.)',
),
'cache' => array(
'type' => 'int',
'not null' => TRUE,
'default' => 1,
'size' => 'tiny',
'description' => 'Binary flag to indicate block cache mode. (-1: Do not cache, 1: Cache per role, 2: Cache per user, 4: Cache per page, 8: Block cache global) See BLOCK_CACHE_* constants in block.module 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['blocks_roles'] = 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 {blocks}.module.",
),
'delta' => array(
'type' => 'varchar',
'length' => 32,
'not null' => TRUE,
'description' => "The block's unique delta within module, from {blocks}.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['boxes'] = array(
'description' => 'Stores contents of custom-made blocks.',
'fields' => array(
'bid' => array(
'type' => 'serial',
'unsigned' => TRUE,
'not null' => TRUE,
'description' => "The block's {blocks}.bid.",
),
'body' => array(
'type' => 'text',
'not null' => FALSE,
'size' => 'big',
'description' => 'Block contents.',
),
'info' => array(
'type' => 'varchar',
'length' => 128,
'not null' => TRUE,
'default' => '',
'description' => 'Block description.',
),
'format' => array(
'type' => 'int',
'size' => 'small',
'not null' => TRUE,
'default' => 0,
'description' => "Block body's {filter_formats}.format; for example, 1 = Filtered HTML.",
)
),
'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;
}

94
modules/block/block.js Normal file
View file

@ -0,0 +1,94 @@
/**
* 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 = function(context) {
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="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;
if ($(dragObject.rowObject.element).prev('tr').is('.region-message')) {
var regionRow = $(dragObject.rowObject.element).prev('tr').get(0);
var regionName = regionRow.className.replace(/([^ ]+[ ]+)*region-([^ ]+)-message([ ]+[^ ]+)*/, '$2');
var regionField = $('select.block-region-select', dragObject.rowObject.element);
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:not(.blockregionselect-processed)', context).each(function() {
$(this).change(function(event) {
// Make our new row and select field.
var row = $(this).parents('tr:first');
var select = $(this);
tableDrag.rowObject = new tableDrag.row(row);
// Find the correct region and insert the row as the first in the region.
$('tr.region-message', table).each(function() {
if ($(this).is('.region-' + select[0].value + '-message')) {
// Add the new row and remove the old one.
$(this).after(row);
// Manually update weights and restripe.
tableDrag.updateFields(row.get(0));
tableDrag.rowObject.changed = true;
if (tableDrag.oldRowElement) {
$(tableDrag.oldRowElement).removeClass('drag-previous');
}
tableDrag.oldRowElement = row.get(0);
tableDrag.restripeTable();
tableDrag.rowObject.markChanged();
tableDrag.oldRowElement = row;
$(row).addClass('drag-previous');
}
});
// Modify empty regions with added or removed fields.
checkEmptyRegions(table, row);
// Remove focus from selectbox.
select.get(0).blur();
});
$(this).addClass('blockregionselect-processed');
});
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').size() == 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');
}
});
};
};

605
modules/block/block.module Normal file
View file

@ -0,0 +1,605 @@
<?php
/**
* @file
* Controls the boxes that are displayed around the main content.
*/
/**
* Denotes that a block is not enabled in any region and should not
* be shown.
*/
define('BLOCK_REGION_NONE', -1);
/**
* Constants defining cache granularity for blocks.
*
* Modules specify the caching patterns for their blocks using binary
* combinations of these constants in their hook_block(op 'list'):
* $block[delta]['cache'] = BLOCK_CACHE_PER_ROLE | BLOCK_CACHE_PER_PAGE;
* BLOCK_CACHE_PER_ROLE is used as a default when no caching pattern is
* specified.
*
* The block cache is cleared in cache_clear_all(), and uses the same clearing
* policy than page cache (node, comment, user, taxonomy added or updated...).
* Blocks requiring more fine-grained clearing might consider disabling the
* built-in block cache (BLOCK_NO_CACHE) and roll their own.
*
* Note that user 1 is excluded from block caching.
*/
/**
* The block should not get cached. This setting should be used:
* - for simple blocks (notably those that do not perform any db query),
* where querying the db cache would be more expensive than directly generating
* the content.
* - for blocks that change too frequently.
*/
define('BLOCK_NO_CACHE', -1);
/**
* The block can change depending on the roles the user viewing the page belongs to.
* This is the default setting, used when the block does not specify anything.
*/
define('BLOCK_CACHE_PER_ROLE', 0x0001);
/**
* 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 thus should only be used when BLOCK_CACHE_PER_ROLE is not sufficient.
*/
define('BLOCK_CACHE_PER_USER', 0x0002);
/**
* The block can change depending on the page being viewed.
*/
define('BLOCK_CACHE_PER_PAGE', 0x0004);
/**
* The block is the same for every user on every page where it is visible.
*/
define('BLOCK_CACHE_GLOBAL', 0x0008);
/**
* Implementation of hook_help().
*/
function block_help($path, $arg) {
switch ($path) {
case 'admin/help#block':
$output = '<p>'. t('Blocks are boxes of content rendered into an area, or region, of a web page. The default theme Garland, for example, implements the regions "left sidebar", "right sidebar", "content", "header", and "footer", and a block may appear in any one of these areas. The <a href="@blocks">blocks administration page</a> provides a drag-and-drop interface for assigning a block to a region, and for controlling the order of blocks within regions.', array('@blocks' => url('admin/build/block'))) .'</p>';
$output .= '<p>'. t('Although blocks are usually generated automatically by modules (like the <em>User login</em> block, for example), administrators can also define custom blocks. Custom blocks have a title, description, and body. The body of the block can be as long as necessary, and can contain content supported by any available <a href="@input-format">input format</a>.', array('@input-format' => url('admin/settings/filters'))) .'</p>';
$output .= '<p>'. t('When working with blocks, remember that:') .'</p>';
$output .= '<ul><li>'. t('since not all themes implement the same regions, or display regions in the same way, blocks are positioned on a per-theme basis.') .'</li>';
$output .= '<li>'. t('disabled blocks, or blocks not in a region, are never shown.') .'</li>';
$output .= '<li>'. t('when throttle module is enabled, throttled blocks (blocks with the <em>Throttle</em> checkbox selected) are hidden during high server loads.') .'</li>';
$output .= '<li>'. t('blocks can be configured to be visible only on certain pages.') .'</li>';
$output .= '<li>'. t('blocks can be configured to be visible only when specific conditions are true.') .'</li>';
$output .= '<li>'. t('blocks can be configured to be visible only for certain user roles.') .'</li>';
$output .= '<li>'. t('when allowed by an administrator, specific blocks may be enabled or disabled on a per-user basis using the <em>My account</em> page.') .'</li>';
$output .= '<li>'. t('some dynamic blocks, such as those generated by modules, will be displayed only on certain pages.') .'</li></ul>';
$output .= '<p>'. t('For more information, see the online handbook entry for <a href="@block">Block module</a>.', array('@block' => 'http://drupal.org/handbook/modules/block/')) .'</p>';
return $output;
case 'admin/build/block':
$throttle = module_exists('throttle');
$output = '<p>'. t('This page provides a drag-and-drop interface for assigning a block to a region, and for controlling the order of blocks within regions. To change the region or order of a block, grab a drag-and-drop handle under the <em>Block</em> column and drag the block to a new location in the list. (Grab a handle by clicking and holding the mouse while hovering over a handle icon.) Since not all themes implement the same regions, or display regions in the same way, blocks are positioned on a per-theme basis. Remember that your changes will not be saved until you click the <em>Save blocks</em> button at the bottom of the page.') .'</p>';
if ($throttle) {
$output .= '<p>'. t('To reduce CPU usage, database traffic or bandwidth, blocks may be automatically disabled during high server loads by selecting their <em>Throttle</em> checkbox. Adjust throttle thresholds on the <a href="@throttleconfig">throttle configuration page</a>.', array('@throttleconfig' => url('admin/settings/throttle'))) .'</p>';
}
$output .= '<p>'. t('Click the <em>configure</em> link next to each block to configure its specific title and visibility settings. Use the <a href="@add-block">add block page</a> to create a custom block.', array('@add-block' => url('admin/build/block/add'))) .'</p>';
return $output;
case 'admin/build/block/add':
return '<p>'. t('Use this page to create a new custom block. New blocks are disabled by default, and must be moved to a region on the <a href="@blocks">blocks administration page</a> to be visible.', array('@blocks' => url('admin/build/block'))) .'</p>';
}
}
/**
* Implementation of hook_theme()
*/
function block_theme() {
return array(
'block_admin_display_form' => array(
'template' => 'block-admin-display-form',
'file' => 'block.admin.inc',
'arguments' => array('form' => NULL),
),
);
}
/**
* Implementation of hook_perm().
*/
function block_perm() {
return array('administer blocks', 'use PHP for block visibility');
}
/**
* Implementation of hook_menu().
*/
function block_menu() {
$items['admin/build/block'] = array(
'title' => 'Blocks',
'description' => 'Configure what block content appears in your site\'s sidebars and other regions.',
'page callback' => 'block_admin_display',
'access arguments' => array('administer blocks'),
'file' => 'block.admin.inc',
);
$items['admin/build/block/list'] = array(
'title' => 'List',
'type' => MENU_DEFAULT_LOCAL_TASK,
'weight' => -10,
);
$items['admin/build/block/list/js'] = array(
'title' => 'JavaScript List Form',
'page callback' => 'block_admin_display_js',
'access arguments' => array('administer blocks'),
'type' => MENU_CALLBACK,
'file' => 'block.admin.inc',
);
$items['admin/build/block/configure'] = array(
'title' => 'Configure block',
'page callback' => 'drupal_get_form',
'page arguments' => array('block_admin_configure'),
'access arguments' => array('administer blocks'),
'type' => MENU_CALLBACK,
'file' => 'block.admin.inc',
);
$items['admin/build/block/delete'] = array(
'title' => 'Delete block',
'page callback' => 'drupal_get_form',
'page arguments' => array('block_box_delete'),
'access arguments' => array('administer blocks'),
'type' => MENU_CALLBACK,
'file' => 'block.admin.inc',
);
$items['admin/build/block/add'] = array(
'title' => 'Add block',
'page callback' => 'drupal_get_form',
'page arguments' => array('block_add_block_form'),
'access arguments' => array('administer blocks'),
'type' => MENU_LOCAL_TASK,
'file' => 'block.admin.inc',
);
$default = variable_get('theme_default', 'garland');
foreach (list_themes() as $key => $theme) {
$items['admin/build/block/list/'. $key] = array(
'title' => check_plain($theme->info['name']),
'page arguments' => array($key),
'type' => $key == $default ? MENU_DEFAULT_LOCAL_TASK : MENU_LOCAL_TASK,
'weight' => $key == $default ? -10 : 0,
'file' => 'block.admin.inc',
'access callback' => '_block_themes_access',
'access arguments' => array($theme),
);
}
return $items;
}
/**
* Menu item access callback - only admin or enabled themes can be accessed
*/
function _block_themes_access($theme) {
return user_access('administer blocks') && ($theme->status || $theme->name == variable_get('admin_theme', '0'));
}
/**
* Implementation of hook_block().
*
* Generates the administrator-defined blocks for display.
*/
function block_block($op = 'list', $delta = 0, $edit = array()) {
switch ($op) {
case 'list':
$blocks = array();
$result = db_query('SELECT bid, info FROM {boxes} ORDER BY info');
while ($block = db_fetch_object($result)) {
$blocks[$block->bid]['info'] = $block->info;
// Not worth caching.
$blocks[$block->bid]['cache'] = BLOCK_NO_CACHE;
}
return $blocks;
case 'configure':
$box = array('format' => FILTER_FORMAT_DEFAULT);
if ($delta) {
$box = block_box_get($delta);
}
if (filter_access($box['format'])) {
return block_box_form($box);
}
break;
case 'save':
block_box_save($edit, $delta);
break;
case 'view':
$block = db_fetch_object(db_query('SELECT body, format FROM {boxes} WHERE bid = %d', $delta));
$data['content'] = check_markup($block->body, $block->format, FALSE);
return $data;
}
}
/**
* Update the 'blocks' DB table with the blocks currently exported by modules.
*
* @param $theme
* The theme to rehash blocks for. If not provided, defaults to the currently
* used theme.
*
* @return
* Blocks currently exported by modules.
*/
function _block_rehash($theme = NULL) {
global $theme_key;
init_theme();
if (!isset($theme)) {
$theme = $theme_key;
}
$result = db_query("SELECT * FROM {blocks} WHERE theme = '%s'", $theme);
$old_blocks = array();
while ($old_block = db_fetch_array($result)) {
$old_blocks[$old_block['module']][$old_block['delta']] = $old_block;
}
$blocks = array();
// Valid region names for the theme.
$regions = system_region_list($theme);
foreach (module_list() as $module) {
$module_blocks = module_invoke($module, 'block', 'list');
if ($module_blocks) {
foreach ($module_blocks as $delta => $block) {
if (empty($old_blocks[$module][$delta])) {
// If it's a new block, add identifiers.
$block['module'] = $module;
$block['delta'] = $delta;
$block['theme'] = $theme;
if (!isset($block['pages'])) {
// {block}.pages is type 'text', so it cannot have a
// default value, and not null, so we need to provide
// value if the module did not.
$block['pages'] = '';
}
// Add defaults and save it into the database.
drupal_write_record('blocks', $block);
// Set region to none if not enabled.
$block['region'] = $block['status'] ? $block['region'] : BLOCK_REGION_NONE;
// Add to the list of blocks we return.
$blocks[] = $block;
}
else {
// If it's an existing block, database settings should overwrite
// the code. The only exceptions are 'cache' which is only definable
// and updatable in the code, and 'info' which is not stored in
// the database.
// Update the cache mode only; the other values don't need to change.
if (isset($block['cache']) && $block['cache'] != $old_blocks[$module][$delta]['cache']) {
db_query("UPDATE {blocks} SET cache = %d WHERE bid = %d", $block['cache'], $old_blocks[$module][$delta]['bid']);
}
// Add 'info' to this block.
$old_blocks[$module][$delta]['info'] = $block['info'];
// If the region name does not exist, disable the block and assign it to none.
if (!empty($old_blocks[$module][$delta]['region']) && !isset($regions[$old_blocks[$module][$delta]['region']])) {
drupal_set_message(t('The block %info was assigned to the invalid region %region and has been disabled.', array('%info' => $old_blocks[$module][$delta]['info'], '%region' => $old_blocks[$module][$delta]['region'])), 'warning');
$old_blocks[$module][$delta]['status'] = 0;
$old_blocks[$module][$delta]['region'] = BLOCK_REGION_NONE;
}
else {
$old_blocks[$module][$delta]['region'] = $old_blocks[$module][$delta]['status'] ? $old_blocks[$module][$delta]['region'] : BLOCK_REGION_NONE;
}
// Add this block to the list of blocks we return.
$blocks[] = $old_blocks[$module][$delta];
// Remove this block from the list of blocks to be deleted.
unset($old_blocks[$module][$delta]);
}
}
}
}
// Remove blocks that are no longer defined by the code from the database.
foreach ($old_blocks as $module => $old_module_blocks) {
// This cleanup does not apply to disabled modules, to avoid configuration
// being lost when modules are disabled.
if (module_exists($module)) {
foreach ($old_module_blocks as $delta => $block) {
db_query("DELETE FROM {blocks} WHERE module = '%s' AND delta = '%s' AND theme = '%s'", $module, $delta, $theme);
}
}
}
return $blocks;
}
/**
* Implementation of hook_flush_caches().
*/
function block_flush_caches() {
// Rehash blocks for active themes. We don't use list_themes() here,
// because if MAINTENANCE_MODE is defined it skips reading the database,
// and we can't tell which themes are active.
$result = db_query("SELECT name FROM {system} WHERE type = 'theme' AND status = 1");
while ($theme = db_result($result)) {
_block_rehash($theme);
}
}
/**
* Returns information from database about a user-created (custom) block.
*
* @param $bid
* ID of the block to get information for.
* @return
* Associative array of information stored in the database for this block.
* Array keys:
* - bid: Block ID.
* - info: Block description.
* - body: Block contents.
* - format: Filter ID of the filter format for the body.
*/
function block_box_get($bid) {
return db_fetch_array(db_query("SELECT * FROM {boxes} WHERE bid = %d", $bid));
}
/**
* Define the custom block form.
*/
function block_box_form($edit = array()) {
$edit += array(
'info' => '',
'body' => '',
);
$form['info'] = array(
'#type' => 'textfield',
'#title' => t('Block description'),
'#default_value' => $edit['info'],
'#maxlength' => 64,
'#description' => t('A brief description of your block. Used on the <a href="@overview">block overview page</a>.', array('@overview' => url('admin/build/block'))),
'#required' => TRUE,
'#weight' => -19,
);
$form['body_field']['#weight'] = -17;
$form['body_field']['body'] = array(
'#type' => 'textarea',
'#title' => t('Block body'),
'#default_value' => $edit['body'],
'#rows' => 15,
'#description' => t('The content of the block as shown to the user.'),
'#weight' => -17,
);
if (!isset($edit['format'])) {
$edit['format'] = FILTER_FORMAT_DEFAULT;
}
$form['body_field']['format'] = filter_form($edit['format'], -16);
return $form;
}
/**
* Saves a user-created block in the database.
*
* @param $edit
* Associative array of fields to save. Array keys:
* - info: Block description.
* - body: Block contents.
* - format: Filter ID of the filter format for the body.
* @param $delta
* Block ID of the block to save.
* @return
* Always returns TRUE.
*/
function block_box_save($edit, $delta) {
if (!filter_access($edit['format'])) {
$edit['format'] = FILTER_FORMAT_DEFAULT;
}
db_query("UPDATE {boxes} SET body = '%s', info = '%s', format = %d WHERE bid = %d", $edit['body'], $edit['info'], $edit['format'], $delta);
return TRUE;
}
/**
* Implementation of hook_user().
*
* Allow users to decide which custom blocks to display when they visit
* the site.
*/
function block_user($type, $edit, &$account, $category = NULL) {
switch ($type) {
case 'form':
if ($category == 'account') {
$rids = array_keys($account->roles);
$result = db_query("SELECT DISTINCT b.* FROM {blocks} b LEFT JOIN {blocks_roles} r ON b.module = r.module AND b.delta = r.delta WHERE b.status = 1 AND b.custom != 0 AND (r.rid IN (". db_placeholders($rids) .") OR r.rid IS NULL) ORDER BY b.weight, b.module", $rids);
$form['block'] = array('#type' => 'fieldset', '#title' => t('Block configuration'), '#weight' => 3, '#collapsible' => TRUE, '#tree' => TRUE);
while ($block = db_fetch_object($result)) {
$data = module_invoke($block->module, 'block', 'list');
if ($data[$block->delta]['info']) {
$return = TRUE;
$form['block'][$block->module][$block->delta] = array('#type' => 'checkbox', '#title' => check_plain($data[$block->delta]['info']), '#default_value' => isset($account->block[$block->module][$block->delta]) ? $account->block[$block->module][$block->delta] : ($block->custom == 1));
}
}
if (!empty($return)) {
return $form;
}
}
break;
case 'validate':
if (empty($edit['block'])) {
$edit['block'] = array();
}
return $edit;
}
}
/**
* Return all blocks in the specified region for the current user.
*
* @param $region
* The name of a region.
*
* @return
* An array of block objects, indexed with module name and block delta
* concatenated with an underscore, thus: MODULE_DELTA. If you are displaying
* your blocks in one or two sidebars, you may check whether this array is
* empty to see how many columns are going to be displayed.
*
* @todo
* Now that the blocks table has a primary key, we should use that as the
* array key instead of MODULE_DELTA.
*/
function block_list($region) {
global $user, $theme_key;
static $blocks = array();
if (!count($blocks)) {
$rids = array_keys($user->roles);
$result = db_query(db_rewrite_sql("SELECT DISTINCT b.* FROM {blocks} b LEFT JOIN {blocks_roles} r ON b.module = r.module AND b.delta = r.delta WHERE b.theme = '%s' AND b.status = 1 AND (r.rid IN (". db_placeholders($rids) .") OR r.rid IS NULL) ORDER BY b.region, b.weight, b.module", 'b', 'bid'), array_merge(array($theme_key), $rids));
while ($block = db_fetch_object($result)) {
if (!isset($blocks[$block->region])) {
$blocks[$block->region] = array();
}
// Use the user's block visibility setting, if necessary
if ($block->custom != 0) {
if ($user->uid && isset($user->block[$block->module][$block->delta])) {
$enabled = $user->block[$block->module][$block->delta];
}
else {
$enabled = ($block->custom == 1);
}
}
else {
$enabled = TRUE;
}
// Match path if necessary
if ($block->pages) {
if ($block->visibility < 2) {
$path = drupal_get_path_alias($_GET['q']);
// Compare with the internal and path alias (if any).
$page_match = drupal_match_path($path, $block->pages);
if ($path != $_GET['q']) {
$page_match = $page_match || drupal_match_path($_GET['q'], $block->pages);
}
// When $block->visibility has a value of 0, the block is displayed on
// all pages except those listed in $block->pages. When set to 1, it
// is displayed only on those pages listed in $block->pages.
$page_match = !($block->visibility xor $page_match);
}
else {
$page_match = drupal_eval($block->pages);
}
}
else {
$page_match = TRUE;
}
$block->enabled = $enabled;
$block->page_match = $page_match;
$blocks[$block->region]["{$block->module}_{$block->delta}"] = $block;
}
}
// Create an empty array if there were no entries
if (!isset($blocks[$region])) {
$blocks[$region] = array();
}
foreach ($blocks[$region] as $key => $block) {
// Render the block content if it has not been created already.
if (!isset($block->content)) {
// Erase the block from the static array - we'll put it back if it has content.
unset($blocks[$region][$key]);
if ($block->enabled && $block->page_match) {
// Check the current throttle status and see if block should be displayed
// based on server load.
if (!($block->throttle && (module_invoke('throttle', 'status') > 0))) {
// Try fetching the block from cache. Block caching is not compatible with
// node_access modules. We also preserve the submission of forms in blocks,
// by fetching from cache only if the request method is 'GET'.
if (!count(module_implements('node_grants')) && $_SERVER['REQUEST_METHOD'] == 'GET' && ($cid = _block_get_cache_id($block)) && ($cache = cache_get($cid, 'cache_block'))) {
$array = $cache->data;
}
else {
$array = module_invoke($block->module, 'block', 'view', $block->delta);
if (isset($cid)) {
cache_set($cid, $array, 'cache_block', CACHE_TEMPORARY);
}
}
if (isset($array) && is_array($array)) {
foreach ($array as $k => $v) {
$block->$k = $v;
}
}
}
if (isset($block->content) && $block->content) {
// Override default block title if a custom display title is present.
if ($block->title) {
// Check plain here to allow module generated titles to keep any markup.
$block->subject = $block->title == '<none>' ? '' : check_plain($block->title);
}
if (!isset($block->subject)) {
$block->subject = '';
}
$blocks[$block->region]["{$block->module}_{$block->delta}"] = $block;
}
}
}
}
return $blocks[$region];
}
/**
* Assemble the cache_id to use for a given block.
*
* The cache_id string reflects the viewing context for the current block
* instance, obtained by concatenating the relevant context information
* (user, page, ...) according to the block's cache settings (BLOCK_CACHE_*
* constants). Two block instances can use the same cached content when
* they share the same cache_id.
*
* Theme and language contexts are automatically differenciated.
*
* @param $block
* @return
* The string used as cache_id for the block.
*/
function _block_get_cache_id($block) {
global $theme, $base_root, $user;
// User 1 being out of the regular 'roles define permissions' schema,
// it brings too many chances of having unwanted output get in the cache
// and later be served to other users. We therefore exclude user 1 from
// block caching.
if (variable_get('block_cache', 0) && $block->cache != BLOCK_NO_CACHE && $user->uid != 1) {
$cid_parts = array();
// Start with common sub-patterns: block identification, theme, language.
$cid_parts[] = $block->module;
$cid_parts[] = $block->delta;
$cid_parts[] = $theme;
if (module_exists('locale')) {
global $language;
$cid_parts[] = $language->language;
}
// 'PER_ROLE' and 'PER_USER' are mutually exclusive. 'PER_USER' can be a
// resource drag for sites with many users, so when a module is being
// equivocal, we favor the less expensive 'PER_ROLE' pattern.
if ($block->cache & BLOCK_CACHE_PER_ROLE) {
$cid_parts[] = 'r.'. implode(',', array_keys($user->roles));
}
elseif ($block->cache & BLOCK_CACHE_PER_USER) {
$cid_parts[] = "u.$user->uid";
}
if ($block->cache & BLOCK_CACHE_PER_PAGE) {
$cid_parts[] = $base_root . request_uri();
}
return implode(':', $cid_parts);
}
}

11
modules/blog/blog.info Normal file
View file

@ -0,0 +1,11 @@
name = Blog
description = Enables keeping easily and regularly updated user web pages or blogs.
package = Core - optional
version = VERSION
core = 6.x
; Information added by Drupal.org packaging script on 2016-02-24
version = "6.38"
project = "drupal"
datestamp = "1456343372"

212
modules/blog/blog.module Normal file
View file

@ -0,0 +1,212 @@
<?php
/**
* @file
* Enables keeping an easily and regularly updated web page or a blog.
*/
/**
* Implementation of hook_node_info().
*/
function blog_node_info() {
return array(
'blog' => array(
'name' => t('Blog entry'),
'module' => 'blog',
'description' => t('A <em>blog entry</em> is a single post to an online journal, or <em>blog</em>.'),
)
);
}
/**
* Implementation of hook_perm().
*/
function blog_perm() {
return array('create blog entries', 'delete own blog entries', 'delete any blog entry', 'edit own blog entries', 'edit any blog entry');
}
/**
* Implementation of hook_access().
*/
function blog_access($op, $node, $account) {
switch ($op) {
case 'create':
// Anonymous users cannot post even if they have the permission.
return user_access('create blog entries', $account) && $account->uid ? TRUE : NULL;
case 'update':
return user_access('edit any blog entry', $account) || (user_access('edit own blog entries', $account) && ($node->uid == $account->uid)) ? TRUE : NULL;
case 'delete':
return user_access('delete any blog entry', $account) || (user_access('delete own blog entries', $account) && ($node->uid == $account->uid)) ? TRUE : NULL;
}
}
/**
* Implementation of hook_user().
*/
function blog_user($type, &$edit, &$user) {
if ($type == 'view' && user_access('create blog entries', $user)) {
$user->content['summary']['blog'] = array(
'#type' => 'user_profile_item',
'#title' => t('Blog'),
// l() escapes the attributes, so we should not escape !username here.
'#value' => l(t('View recent blog entries'), "blog/$user->uid", array('attributes' => array('title' => t("Read !username's latest blog entries.", array('!username' => $user->name))))),
'#attributes' => array('class' => 'blog'),
);
}
}
/**
* Implementation of hook_help().
*/
function blog_help($path, $arg) {
switch ($path) {
case 'admin/help#blog':
$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>, and the blog entries are most often displayed in descending order by creation time.') .'</p>';
$output .= '<p>'. 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. The <em>Blog entry</em> menu item under <em>Create content</em> allows new blog entries to be created.') .'</p>';
$output .= '<p>'. t('Each blog entry is displayed with an automatic link to other blogs created by the same user. By default, blog entries have comments enabled and are automatically promoted to the site front page. The blog module also creates a <em>Recent blog posts</em> block that may be enabled at the <a href="@blocks">blocks administration page</a>.', array('@blocks' => url('admin/build/block'))) .'</p>';
$output .= '<p>'. t('When using the aggregator module an automatic <em>blog it</em> icon is displayed next to the items in a feed\'s <em>latest items</em> block. Clicking this icon populates a <em>blog entry</em> with a title (the title of the feed item) and body (a link to the source item on its original site and illustrative content suitable for use in a block quote). Blog authors can use this feature to easily comment on items of interest that appear in aggregator feeds from other sites. To use this feature, be sure to <a href="@modules">enable</a> the aggregator module, <a href="@feeds">add and configure</a> a feed from another site, and <a href="@blocks">position</a> the feed\'s <em>latest items</em> block.', array('@modules' => url('admin/build/modules'), '@feeds' => url('admin/content/aggregator'), '@blocks' => url('admin/build/block'))) .'</p>';
$output .= '<p>'. t('For more information, see the online handbook entry for <a href="@blog">Blog module</a>.', array('@blog' => 'http://drupal.org/handbook/modules/blog/')) .'</p>';
return $output;
}
}
/**
* Implementation of hook_form().
*/
function blog_form(&$node) {
global $nid;
$iid = isset($_GET['iid']) ? (int)$_GET['iid'] : 0;
$type = node_get_types('type', $node);
if (empty($node->body)) {
// If the user clicked a "blog it" link, we load the data from the
// database and quote it in the blog.
if ($nid && $blog = node_load($nid)) {
$node->body = '<em>'. $blog->body .'</em> ['. l($blog->name, "node/$nid") .']';
}
if ($iid && $item = db_fetch_object(db_query('SELECT i.*, f.title as ftitle, f.link as flink FROM {aggregator_item} i, {aggregator_feed} f WHERE i.iid = %d AND i.fid = f.fid', $iid))) {
$node->title = $item->title;
// Note: $item->description has been validated on aggregation.
$node->body = '<a href="'. check_url($item->link) .'">'. check_plain($item->title) .'</a> - <em>'. $item->description .'</em> [<a href="'. check_url($item->flink) .'">'. check_plain($item->ftitle) ."</a>]\n";
}
}
$form['title'] = array('#type' => 'textfield', '#title' => check_plain($type->title_label), '#required' => TRUE, '#default_value' => !empty($node->title) ? $node->title : NULL, '#weight' => -5);
$form['body_field'] = node_body_field($node, $type->body_label, $type->min_word_count);
return $form;
}
/**
* Implementation of hook_view().
*/
function blog_view($node, $teaser = FALSE, $page = FALSE) {
if ($page) {
// Breadcrumb navigation. l() escapes the 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' => $node->name)), 'blog/'. $node->uid)));
}
return node_prepare($node, $teaser);
}
/**
* Implementation of hook_link().
*/
function blog_link($type, $node = NULL, $teaser = FALSE) {
$links = array();
if ($type == 'node' && $node->type == 'blog') {
if (arg(0) != 'blog' || arg(1) != $node->uid) {
// This goes to l() and therefore escapes !username in both the title and attributes.
$links['blog_usernames_blog'] = array(
'title' => t("!username's blog", array('!username' => $node->name)),
'href' => "blog/$node->uid",
'attributes' => array('title' => t("Read !username's latest blog entries.", array('!username' => $node->name)))
);
}
}
return $links;
}
/**
* Implementation of 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;
}
/**
* 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 entries', $account) || _blog_post_exists($account));
}
/**
* Helper function to determine if a user has blog posts already.
*/
function _blog_post_exists($account) {
return (bool)db_result(db_query_range(db_rewrite_sql("SELECT 1 FROM {node} n WHERE n.type = 'blog' AND n.uid = %d AND n.status = 1"), $account->uid, 0, 1));
}
/**
* Implementation of hook_block().
*
* Displays the most recent 10 blog titles.
*/
function blog_block($op = 'list', $delta = 0) {
global $user;
if ($op == 'list') {
$block[0]['info'] = t('Recent blog posts');
return $block;
}
else if ($op == 'view') {
if (user_access('access content')) {
$result = db_query_range(db_rewrite_sql("SELECT n.nid, n.title, n.created FROM {node} n WHERE n.type = 'blog' AND n.status = 1 ORDER BY n.created DESC"), 0, 10);
if ($node_title_list = node_title_list($result)) {
$block['content'] = $node_title_list;
$block['content'] .= theme('more_link', url('blog'), t('Read the latest blog entries.'));
$block['subject'] = t('Recent blog posts');
return $block;
}
}
}
}

114
modules/blog/blog.pages.inc Normal file
View file

@ -0,0 +1,114 @@
<?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' => $account->name)));
$items = array();
if (($account->uid == $user->uid) && user_access('create blog entries')) {
$items[] = l(t('Post new blog entry.'), "node/add/blog");
}
else if ($account->uid == $user->uid) {
$items[] = t('You are not allowed to post a new blog entry.');
}
$output = theme('item_list', $items);
$result = pager_query(db_rewrite_sql("SELECT n.nid, n.sticky, n.created FROM {node} n WHERE n.type = 'blog' AND n.uid = %d AND n.status = 1 ORDER BY n.sticky DESC, n.created DESC"), variable_get('default_nodes_main', 10), 0, NULL, $account->uid);
$has_posts = FALSE;
while ($node = db_fetch_object($result)) {
$output .= node_view(node_load($node->nid), 1);
$has_posts = TRUE;
}
if ($has_posts) {
$output .= theme('pager', NULL, variable_get('default_nodes_main', 10));
}
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', $account))));
}
}
drupal_add_feed(url('blog/'. $account->uid .'/feed'), t('RSS - !title', array('!title' => $title)));
return $output;
}
/**
* Menu callback; displays a Drupal page containing recent blog entries of all users.
*/
function blog_page_last() {
global $user;
$output = '';
$items = array();
if (user_access('create blog entries')) {
$items[] = l(t('Create new blog entry.'), "node/add/blog");
}
$output = theme('item_list', $items);
$result = pager_query(db_rewrite_sql("SELECT n.nid, n.sticky, n.created FROM {node} n WHERE n.type = 'blog' AND n.status = 1 ORDER BY n.sticky DESC, n.created DESC"), variable_get('default_nodes_main', 10));
$has_posts = FALSE;
while ($node = db_fetch_object($result)) {
$output .= node_view(node_load($node->nid), 1);
$has_posts = TRUE;
}
if ($has_posts) {
$output .= theme('pager', NULL, variable_get('default_nodes_main', 10));
}
else {
drupal_set_message(t('No blog entries have been created.'));
}
drupal_add_feed(url('blog/feed'), t('RSS - blogs'));
return $output;
}
/**
* Menu callback; displays an RSS feed containing recent blog entries of a given user.
*/
function blog_feed_user($account) {
$result = db_query_range(db_rewrite_sql("SELECT n.nid, n.created FROM {node} n WHERE n.type = 'blog' AND n.uid = %d AND n.status = 1 ORDER BY n.created DESC"), $account->uid, 0, variable_get('feed_default_items', 10));
$channel['title'] = t("!name's blog", array('!name' => $account->name));
$channel['link'] = url('blog/'. $account->uid, array('absolute' => TRUE));
$items = array();
while ($row = db_fetch_object($result)) {
$items[] = $row->nid;
}
node_feed($items, $channel);
}
/**
* Menu callback; displays an RSS feed containing recent blog entries of all users.
*/
function blog_feed_last() {
$result = db_query_range(db_rewrite_sql("SELECT n.nid, n.created FROM {node} n WHERE n.type = 'blog' AND n.status = 1 ORDER BY n.created DESC"), 0, variable_get('feed_default_items', 10));
$channel['title'] = t('!site_name blogs', array('!site_name' => variable_get('site_name', 'Drupal')));
$channel['link'] = url('blog', array('absolute' => TRUE));
$items = array();
while ($row = db_fetch_object($result)) {
$items[] = $row->nid;
}
node_feed($items, $channel);
}

View file

@ -0,0 +1,11 @@
name = Blog API
description = Allows users to post content using applications that support XML-RPC blog APIs.
package = Core - optional
version = VERSION
core = 6.x
; Information added by Drupal.org packaging script on 2016-02-24
version = "6.38"
project = "drupal"
datestamp = "1456343372"

View file

@ -0,0 +1,124 @@
<?php
/**
* Implementation of hook_install().
*/
function blogapi_install() {
// Create tables.
drupal_install_schema('blogapi');
}
/**
* Implementation of hook_uninstall().
*/
function blogapi_uninstall() {
// Remove tables.
drupal_uninstall_schema('blogapi');
}
/**
* Implementation of hook_schema().
*/
function blogapi_schema() {
//This table was introduced in Drupal 6.4
$schema['blogapi_files'] = array(
'description' => 'Stores information for files uploaded via the blogapi.',
'fields' => array(
'fid' => array(
'description' => 'Primary Key: Unique file ID.',
'type' => 'serial',
),
'uid' => array(
'description' => 'The {users}.uid of the user who is associated with the file.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0),
'filepath' => array(
'description' => 'Path of the file relative to Drupal root.',
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
'default' => ''),
'filesize' => array(
'description' => 'The size of the file in bytes.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0),
),
'primary key' => array('fid'),
'indexes' => array(
'uid' => array('uid'),
),
);
return $schema;
}
/**
* @addtogroup updates-5.x-to-6.x
* @{
*/
/**
* Inform users about the new permission.
*/
function blogapi_update_6000() {
drupal_set_message("Blog API module does not depend on blog module's permissions anymore, but provides its own 'administer content with blog api' permission instead. Until <a href=\"". url('admin/user/permissions', array('fragment' => 'module-blogapi')) .'">this permission is assigned</a> to at least one user role, only the site administrator will be able to use Blog API features.');
return array();
}
/**
* Add blogapi_files table to enable size restriction for BlogAPI file uploads.
*
* This table was introduced in Drupal 6.4.
*/
function blogapi_update_6001() {
$schema['blogapi_files'] = array(
'description' => 'Stores information for files uploaded via the blogapi.',
'fields' => array(
'fid' => array(
'description' => 'Primary Key: Unique file ID.',
'type' => 'serial',
),
'uid' => array(
'description' => 'The {users}.uid of the user who is associated with the file.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0),
'filepath' => array(
'description' => 'Path of the file relative to Drupal root.',
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
'default' => ''),
'filesize' => array(
'description' => 'The size of the file in bytes.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0),
),
'primary key' => array('fid'),
'indexes' => array(
'uid' => array('uid'),
),
);
$ret = array();
if (!db_table_exists('blogapi_files')) {
db_create_table($ret, 'blogapi_files', $schema['blogapi_files']);
}
return $ret;
}
/**
* @} End of "addtogroup updates-5.x-to-6.x".
* The next series of updates should start at 7000.
*/

View file

@ -0,0 +1,949 @@
<?php
/**
* @file
* Enable users to post using applications that support XML-RPC blog APIs.
*/
/**
* Implementation of hook_help().
*/
function blogapi_help($path, $arg) {
switch ($path) {
case 'admin/help#blogapi':
$output = '<p>'. t("The Blog API module allows your site's users to access and post to their blogs from external blogging clients. External blogging clients are available for a wide range of desktop operating systems, and generally provide a feature-rich graphical environment for creating and editing posts.") .'</p>';
$output .= '<p>'. t('<a href="@ecto-link">Ecto</a>, a blogging client available for both Mac OS X and Microsoft Windows, can be used with Blog API. Blog API also supports <a href="@blogger-api">Blogger API</a>, <a href="@metaweblog-api">MetaWeblog API</a>, and most of the <a href="@movabletype-api">Movable Type API</a>. Blogging clients and other services (e.g. <a href="@flickr">Flickr\'s</a> "post to blog") that support these APIs may also be compatible.', array('@ecto-link' => url('http://infinite-sushi.com/software/ecto/'), '@blogger-api' => url('http://www.blogger.com/developers/api/1_docs/'), '@metaweblog-api' => url('http://www.xmlrpc.com/metaWeblogApi'), '@movabletype-api' => url('http://www.movabletype.org/docs/mtmanual_programmatic.html'), '@flickr' => url('http://www.flickr.com'))) .'</p>';
$output .= '<p>'. t('Select the content types available to external clients on the <a href="@blogapi-settings">Blog API settings page</a>. If supported and available, each content type will be displayed as a separate "blog" by the external client.', array('@blogapi-settings' => url('admin/settings/blogapi'))) .'</p>';
$output .= '<p>'. t('For more information, see the online handbook entry for <a href="@blogapi">Blog API module</a>.', array('@blogapi' => url('http://drupal.org/handbook/modules/blogapi/'))) .'</p>';
return $output;
}
}
/**
* Implementation of hook_perm().
*/
function blogapi_perm() {
return array('administer content with blog api');
}
/**
* Implementation of hook_xmlrpc().
*/
function blogapi_xmlrpc() {
return array(
array(
'blogger.getUsersBlogs',
'blogapi_blogger_get_users_blogs',
array('array', 'string', 'string', 'string'),
t('Returns a list of blogs to which an author has posting privileges.')),
array(
'blogger.getUserInfo',
'blogapi_blogger_get_user_info',
array('struct', 'string', 'string', 'string'),
t('Returns information about an author in the system.')),
array(
'blogger.newPost',
'blogapi_blogger_new_post',
array('string', 'string', 'string', 'string', 'string', 'string', 'boolean'),
t('Creates a new post, and optionally publishes it.')),
array(
'blogger.editPost',
'blogapi_blogger_edit_post',
array('boolean', 'string', 'string', 'string', 'string', 'string', 'boolean'),
t('Updates the information about an existing post.')),
array(
'blogger.getPost',
'blogapi_blogger_get_post',
array('struct', 'string', 'string', 'string', 'string'),
t('Returns information about a specific post.')),
array(
'blogger.deletePost',
'blogapi_blogger_delete_post',
array('boolean', 'string', 'string', 'string', 'string', 'boolean'),
t('Deletes a post.')),
array(
'blogger.getRecentPosts',
'blogapi_blogger_get_recent_posts',
array('array', 'string', 'string', 'string', 'string', 'int'),
t('Returns a list of the most recent posts in the system.')),
array(
'metaWeblog.newPost',
'blogapi_metaweblog_new_post',
array('string', 'string', 'string', 'string', 'struct', 'boolean'),
t('Creates a new post, and optionally publishes it.')),
array(
'metaWeblog.editPost',
'blogapi_metaweblog_edit_post',
array('boolean', 'string', 'string', 'string', 'struct', 'boolean'),
t('Updates information about an existing post.')),
array(
'metaWeblog.getPost',
'blogapi_metaweblog_get_post',
array('struct', 'string', 'string', 'string'),
t('Returns information about a specific post.')),
array(
'metaWeblog.newMediaObject',
'blogapi_metaweblog_new_media_object',
array('string', 'string', 'string', 'string', 'struct'),
t('Uploads a file to your webserver.')),
array(
'metaWeblog.getCategories',
'blogapi_metaweblog_get_category_list',
array('struct', 'string', 'string', 'string'),
t('Returns a list of all categories to which the post is assigned.')),
array(
'metaWeblog.getRecentPosts',
'blogapi_metaweblog_get_recent_posts',
array('array', 'string', 'string', 'string', 'int'),
t('Returns a list of the most recent posts in the system.')),
array(
'mt.getRecentPostTitles',
'blogapi_mt_get_recent_post_titles',
array('array', 'string', 'string', 'string', 'int'),
t('Returns a bandwidth-friendly list of the most recent posts in the system.')),
array(
'mt.getCategoryList',
'blogapi_mt_get_category_list',
array('array', 'string', 'string', 'string'),
t('Returns a list of all categories defined in the blog.')),
array(
'mt.getPostCategories',
'blogapi_mt_get_post_categories',
array('array', 'string', 'string', 'string'),
t('Returns a list of all categories to which the post is assigned.')),
array(
'mt.setPostCategories',
'blogapi_mt_set_post_categories',
array('boolean', 'string', 'string', 'string', 'array'),
t('Sets the categories for a post.')),
array(
'mt.supportedMethods',
'xmlrpc_server_list_methods',
array('array'),
t('Retrieve information about the XML-RPC methods supported by the server.')),
array(
'mt.supportedTextFilters',
'blogapi_mt_supported_text_filters',
array('array'),
t('Retrieve information about the text formatting plugins supported by the server.')),
array(
'mt.publishPost',
'blogapi_mt_publish_post',
array('boolean', 'string', 'string', 'string'),
t('Publish (rebuild) all of the static files related to an entry from your blog. Equivalent to saving an entry in the system (but without the ping).')));
}
/**
* Blogging API callback. Finds the URL of a user's blog.
*/
function blogapi_blogger_get_users_blogs($appid, $username, $password) {
$user = blogapi_validate_user($username, $password);
if ($user->uid) {
$types = _blogapi_get_node_types();
$structs = array();
foreach ($types as $type) {
$structs[] = array('url' => url('blog/'. $user->uid, array('absolute' => TRUE)), 'blogid' => $type, 'blogName' => $user->name .": ". $type);
}
return $structs;
}
else {
return blogapi_error($user);
}
}
/**
* Blogging API callback. Returns profile information about a user.
*/
function blogapi_blogger_get_user_info($appkey, $username, $password) {
$user = blogapi_validate_user($username, $password);
if ($user->uid) {
$name = explode(' ', $user->realname ? $user->realname : $user->name, 2);
return array(
'userid' => $user->uid,
'lastname' => $name[1],
'firstname' => $name[0],
'nickname' => $user->name,
'email' => $user->mail,
'url' => url('blog/'. $user->uid, array('absolute' => TRUE)));
}
else {
return blogapi_error($user);
}
}
/**
* Blogging API callback. Inserts a new blog post as a node.
*/
function blogapi_blogger_new_post($appkey, $blogid, $username, $password, $content, $publish) {
$user = blogapi_validate_user($username, $password);
if (!$user->uid) {
return blogapi_error($user);
}
if (($error = _blogapi_validate_blogid($blogid)) !== TRUE) {
// Return an error if not configured type.
return $error;
}
$edit = array();
$edit['type'] = $blogid;
// get the node type defaults
$node_type_default = variable_get('node_options_'. $edit['type'], array('status', 'promote'));
$edit['uid'] = $user->uid;
$edit['name'] = $user->name;
$edit['promote'] = in_array('promote', $node_type_default);
$edit['comment'] = variable_get('comment_'. $edit['type'], 2);
$edit['revision'] = in_array('revision', $node_type_default);
$edit['format'] = FILTER_FORMAT_DEFAULT;
$edit['status'] = $publish;
// check for bloggerAPI vs. metaWeblogAPI
if (is_array($content)) {
$edit['title'] = $content['title'];
$edit['body'] = $content['description'];
_blogapi_mt_extra($edit, $content);
}
else {
$edit['title'] = blogapi_blogger_title($content);
$edit['body'] = $content;
}
if (!node_access('create', $edit['type'])) {
return blogapi_error(t('You do not have permission to create this type of post.'));
}
if (user_access('administer nodes') && !isset($edit['date'])) {
$edit['date'] = format_date(time(), 'custom', 'Y-m-d H:i:s O');
}
node_invoke_nodeapi($edit, 'blogapi new');
$valid = blogapi_status_error_check($edit, $publish);
if ($valid !== TRUE) {
return $valid;
}
node_validate($edit);
if ($errors = form_get_errors()) {
return blogapi_error(implode("\n", $errors));
}
$node = node_submit($edit);
node_save($node);
if ($node->nid) {
watchdog('content', '@type: added %title using blog API.', array('@type' => $node->type, '%title' => $node->title), WATCHDOG_NOTICE, l(t('view'), "node/$node->nid"));
// blogger.newPost returns a string so we cast the nid to a string by putting it in double quotes:
return "$node->nid";
}
return blogapi_error(t('Error storing post.'));
}
/**
* Blogging API callback. Modifies the specified blog node.
*/
function blogapi_blogger_edit_post($appkey, $postid, $username, $password, $content, $publish) {
$user = blogapi_validate_user($username, $password);
if (!$user->uid) {
return blogapi_error($user);
}
$node = node_load($postid);
if (!$node) {
return blogapi_error(t('n/a'));
}
// Let the teaser be re-generated.
unset($node->teaser);
if (!node_access('update', $node)) {
return blogapi_error(t('You do not have permission to update this post.'));
}
// Save the original status for validation of permissions.
$original_status = $node->status;
$node->status = $publish;
// check for bloggerAPI vs. metaWeblogAPI
if (is_array($content)) {
$node->title = $content['title'];
$node->body = $content['description'];
_blogapi_mt_extra($node, $content);
}
else {
$node->title = blogapi_blogger_title($content);
$node->body = $content;
}
node_invoke_nodeapi($node, 'blogapi edit');
$valid = blogapi_status_error_check($node, $original_status);
if ($valid !== TRUE) {
return $valid;
}
node_validate($node);
if ($errors = form_get_errors()) {
return blogapi_error(implode("\n", $errors));
}
if (user_access('administer nodes') && !isset($edit['date'])) {
$node->date = format_date($node->created, 'custom', 'Y-m-d H:i:s O');
}
$node = node_submit($node);
node_save($node);
if ($node->nid) {
watchdog('content', '@type: updated %title using Blog API.', array('@type' => $node->type, '%title' => $node->title), WATCHDOG_NOTICE, l(t('view'), "node/$node->nid"));
return TRUE;
}
return blogapi_error(t('Error storing post.'));
}
/**
* Blogging API callback. Returns a specified blog node.
*/
function blogapi_blogger_get_post($appkey, $postid, $username, $password) {
$user = blogapi_validate_user($username, $password);
if (!$user->uid) {
return blogapi_error($user);
}
$node = node_load($postid);
return _blogapi_get_post($node, TRUE);
}
/**
* Check that the user has permission to save the node with the chosen status.
*
* @return
* TRUE if no error, or the blogapi_error().
*/
function blogapi_status_error_check($node, $original_status) {
$node = (object) $node;
$node_type_default = variable_get('node_options_'. $node->type, array('status', 'promote'));
// If we don't have the 'administer nodes' permission and the status is
// changing or for a new node the status is not the content type's default,
// then return an error.
if (!user_access('administer nodes') && (($node->status != $original_status) || (empty($node->nid) && $node->status != in_array('status', $node_type_default)))) {
if ($node->status) {
return blogapi_error(t('You do not have permission to publish this type of post. Please save it as a draft instead.'));
}
else {
return blogapi_error(t('You do not have permission to save this post as a draft. Please publish it instead.'));
}
}
return TRUE;
}
/**
* Blogging API callback. Removes the specified blog node.
*/
function blogapi_blogger_delete_post($appkey, $postid, $username, $password, $publish) {
$user = blogapi_validate_user($username, $password);
if (!$user->uid) {
return blogapi_error($user);
}
node_delete($postid);
return TRUE;
}
/**
* Blogging API callback. Returns the latest few postings in a user's blog. $bodies TRUE
* <a href="http://movabletype.org/docs/mtmanual_programmatic.html#item_mt%2EgetRecentPostTitles">
* returns a bandwidth-friendly list</a>.
*/
function blogapi_blogger_get_recent_posts($appkey, $blogid, $username, $password, $number_of_posts, $bodies = TRUE) {
// Remove unused appkey (from bloggerAPI).
$user = blogapi_validate_user($username, $password);
if (!$user->uid) {
return blogapi_error($user);
}
if (($error = _blogapi_validate_blogid($blogid)) !== TRUE) {
// Return an error if not configured type.
return $error;
}
if ($bodies) {
$result = db_query_range("SELECT n.nid, n.title, r.body, r.format, n.comment, n.created, u.name FROM {node} n, {node_revisions} r, {users} u WHERE n.uid = u.uid AND n.vid = r.vid AND n.type = '%s' AND n.uid = %d ORDER BY n.created DESC", $blogid, $user->uid, 0, $number_of_posts);
}
else {
$result = db_query_range("SELECT n.nid, n.title, n.created, u.name FROM {node} n, {users} u WHERE n.uid = u.uid AND n.type = '%s' AND n.uid = %d ORDER BY n.created DESC", $blogid, $user->uid, 0, $number_of_posts);
}
$blogs = array();
while ($blog = db_fetch_object($result)) {
$blogs[] = _blogapi_get_post($blog, $bodies);
}
return $blogs;
}
function blogapi_metaweblog_new_post($blogid, $username, $password, $content, $publish) {
return blogapi_blogger_new_post('0123456789ABCDEF', $blogid, $username, $password, $content, $publish);
}
function blogapi_metaweblog_edit_post($postid, $username, $password, $content, $publish) {
return blogapi_blogger_edit_post('0123456789ABCDEF', $postid, $username, $password, $content, $publish);
}
function blogapi_metaweblog_get_post($postid, $username, $password) {
return blogapi_blogger_get_post('01234567890ABCDEF', $postid, $username, $password);
}
/**
* Blogging API callback. Inserts a file into Drupal.
*/
function blogapi_metaweblog_new_media_object($blogid, $username, $password, $file) {
$user = blogapi_validate_user($username, $password);
if (!$user->uid) {
return blogapi_error($user);
}
$usersize = 0;
$uploadsize = 0;
$roles = array_intersect(user_roles(FALSE, 'administer content with blog api'), $user->roles);
foreach ($roles as $rid => $name) {
$extensions .= ' '. strtolower(variable_get("blogapi_extensions_$rid", variable_get('blogapi_extensions_default', 'jpg jpeg gif png txt doc xls pdf ppt pps odt ods odp')));
$usersize= max($usersize, variable_get("blogapi_usersize_$rid", variable_get('blogapi_usersize_default', 1)) * 1024 * 1024);
$uploadsize = max($uploadsize, variable_get("blogapi_uploadsize_$rid", variable_get('blogapi_uploadsize_default', 1)) * 1024 * 1024);
}
$filesize = strlen($file['bits']);
if ($filesize > $uploadsize) {
return blogapi_error(t('It is not possible to upload the file, because it exceeded the maximum filesize of @maxsize.', array('@maxsize' => format_size($uploadsize))));
}
if (_blogapi_space_used($user->uid) + $filesize > $usersize) {
return blogapi_error(t('The file can not be attached to this post, because the disk quota of @quota has been reached.', array('@quota' => format_size($usersize))));
}
// Only allow files with whitelisted extensions and convert remaining dots to
// underscores to prevent attacks via non-terminal executable extensions with
// files such as exploit.php.jpg.
$whitelist = array_unique(explode(' ', trim($extensions)));
$name = basename($file['name']);
if ($extension_position = strrpos($name, '.')) {
$filename = drupal_substr($name, 0, $extension_position);
$final_extension = drupal_substr($name, $extension_position + 1);
if (!in_array(strtolower($final_extension), $whitelist)) {
return blogapi_error(t('It is not possible to upload the file, because it is only possible to upload files with the following extensions: @extensions', array('@extensions' => implode(' ', $whitelist))));
}
$filename = str_replace('.', '_', $filename);
$filename .= '.'. $final_extension;
}
$data = $file['bits'];
if (!$data) {
return blogapi_error(t('No file sent.'));
}
if (!$file = file_save_data($data, $filename)) {
return blogapi_error(t('Error storing file.'));
}
$row = new stdClass();
$row->uid = $user->uid;
$row->filepath = $file;
$row->filesize = $filesize;
drupal_write_record('blogapi_files', $row);
// Return the successful result.
return array('url' => file_create_url($file), 'struct');
}
/**
* Blogging API callback. Returns a list of the taxonomy terms that can be
* associated with a blog node.
*/
function blogapi_metaweblog_get_category_list($blogid, $username, $password) {
$user = blogapi_validate_user($username, $password);
if (!$user->uid) {
return blogapi_error($user);
}
if (($error = _blogapi_validate_blogid($blogid)) !== TRUE) {
// Return an error if not configured type.
return $error;
}
$vocabularies = module_invoke('taxonomy', 'get_vocabularies', $blogid, 'vid');
$categories = array();
if ($vocabularies) {
foreach ($vocabularies as $vocabulary) {
$terms = module_invoke('taxonomy', 'get_tree', $vocabulary->vid, 0, -1);
foreach ($terms as $term) {
$term_name = $term->name;
foreach (module_invoke('taxonomy', 'get_parents', $term->tid, 'tid') as $parent) {
$term_name = $parent->name .'/'. $term_name;
}
$categories[] = array('categoryName' => $term_name, 'categoryId' => $term->tid);
}
}
}
return $categories;
}
function blogapi_metaweblog_get_recent_posts($blogid, $username, $password, $number_of_posts) {
return blogapi_blogger_get_recent_posts('0123456789ABCDEF', $blogid, $username, $password, $number_of_posts, TRUE);
}
function blogapi_mt_get_recent_post_titles($blogid, $username, $password, $number_of_posts) {
return blogapi_blogger_get_recent_posts('0123456789ABCDEF', $blogid, $username, $password, $number_of_posts, FALSE);
}
function blogapi_mt_get_category_list($blogid, $username, $password) {
return blogapi_metaweblog_get_category_list($blogid, $username, $password);
}
/**
* Blogging API callback. Returns a list of the taxonomy terms that are
* assigned to a particular node.
*/
function blogapi_mt_get_post_categories($postid, $username, $password) {
$user = blogapi_validate_user($username, $password);
if (!$user->uid) {
return blogapi_error($user);
}
$node = node_load($postid);
$terms = module_invoke('taxonomy', 'node_get_terms', $node, 'tid');
$categories = array();
foreach ($terms as $term) {
$term_name = $term->name;
foreach (module_invoke('taxonomy', 'get_parents', $term->tid, 'tid') as $parent) {
$term_name = $parent->name .'/'. $term_name;
}
$categories[] = array('categoryName' => $term_name, 'categoryId' => $term->tid, 'isPrimary' => TRUE);
}
return $categories;
}
/**
* Blogging API callback. Assigns taxonomy terms to a particular node.
*/
function blogapi_mt_set_post_categories($postid, $username, $password, $categories) {
$user = blogapi_validate_user($username, $password);
if (!$user->uid) {
return blogapi_error($user);
}
$node = node_load($postid);
$node->taxonomy = array();
foreach ($categories as $category) {
$node->taxonomy[] = $category['categoryId'];
}
$validated = blogapi_mt_validate_terms($node);
if ($validated !== TRUE) {
return $validated;
}
node_save($node);
return TRUE;
}
/**
* Blogging API helper - find allowed taxonomy terms for a node type.
*/
function blogapi_mt_validate_terms($node) {
// We do a lot of heavy lifting here since taxonomy module doesn't have a
// stand-alone validation function.
if (module_exists('taxonomy')) {
$found_terms = array();
if (!empty($node->taxonomy)) {
$term_list = array_unique($node->taxonomy);
$params = $term_list;
$params[] = $node->type;
$result = db_query(db_rewrite_sql("SELECT t.tid, t.vid FROM {term_data} t INNER JOIN {vocabulary_node_types} n ON t.vid = n.vid WHERE t.tid IN (". db_placeholders($term_list) .") AND n.type = '%s'", 't', 'tid'), $params);
$found_terms = array();
$found_count = 0;
while ($term = db_fetch_object($result)) {
$found_terms[$term->vid][$term->tid] = $term->tid;
$found_count++;
}
// If the counts don't match, some terms are invalid or not accessible to this user.
if (count($term_list) != $found_count) {
return blogapi_error(t('Invalid categories submitted.'));
}
}
// Look up all the vocabularies for this node type.
$result2 = db_query(db_rewrite_sql("SELECT v.vid, v.name, v.required, v.multiple FROM {vocabulary} v INNER JOIN {vocabulary_node_types} n ON v.vid = n.vid WHERE n.type = '%s'", 'v', 'vid'), $node->type);
// Check each vocabulary associated with this node type.
while ($vocabulary = db_fetch_object($result2)) {
// Required vocabularies must have at least one term.
if ($vocabulary->required && empty($found_terms[$vocabulary->vid])) {
return blogapi_error(t('A category from the @vocabulary_name vocabulary is required.', array('@vocabulary_name' => $vocabulary->name)));
}
// Vocabularies that don't allow multiple terms may have at most one.
if (!($vocabulary->multiple) && (isset($found_terms[$vocabulary->vid]) && count($found_terms[$vocabulary->vid]) > 1)) {
return blogapi_error(t('You may only choose one category from the @vocabulary_name vocabulary.'), array('@vocabulary_name' => $vocabulary->name));
}
}
}
elseif (!empty($node->taxonomy)) {
return blogapi_error(t('Error saving categories. This feature is not available.'));
}
return TRUE;
}
/**
* Blogging API callback. Sends a list of available input formats.
*/
function blogapi_mt_supported_text_filters() {
// NOTE: we're only using anonymous' formats because the MT spec
// does not allow for per-user formats.
$formats = filter_formats();
$filters = array();
foreach ($formats as $format) {
$filter['key'] = $format->format;
$filter['label'] = $format->name;
$filters[] = $filter;
}
return $filters;
}
/**
* Blogging API callback. Publishes the given node
*/
function blogapi_mt_publish_post($postid, $username, $password) {
$user = blogapi_validate_user($username, $password);
if (!$user->uid) {
return blogapi_error($user);
}
$node = node_load($postid);
if (!$node) {
return blogapi_error(t('Invalid post.'));
}
// Nothing needs to be done if already published.
if ($node->status) {
return;
}
if (!node_access('update', $node) || !user_access('administer nodes')) {
return blogapi_error(t('You do not have permission to update this post.'));
}
$node->status = 1;
node_save($node);
return TRUE;
}
/**
* Prepare an error message for returning to the XMLRPC caller.
*/
function blogapi_error($message) {
static $xmlrpcusererr;
if (!is_array($message)) {
$message = array($message);
}
$message = implode(' ', $message);
return xmlrpc_error($xmlrpcusererr + 1, strip_tags($message));
}
/**
* Ensure that the given user has permission to edit a blog.
*/
function blogapi_validate_user($username, $password) {
global $user;
$user = user_authenticate(array('name' => $username, 'pass' => $password));
if ($user->uid) {
if (user_access('administer content with blog api', $user)) {
return $user;
}
else {
return t('You do not have permission to edit this blog.');
}
}
else {
return t('Wrong username or password.');
}
}
/**
* For the blogger API, extract the node title from the contents field.
*/
function blogapi_blogger_title(&$contents) {
if (eregi('<title>([^<]*)</title>', $contents, $title)) {
$title = strip_tags($title[0]);
$contents = ereg_replace('<title>[^<]*</title>', '', $contents);
}
else {
list($title, $contents) = explode("\n", $contents, 2);
}
return $title;
}
function blogapi_admin_settings() {
$node_types = array_map('check_plain', node_get_types('names'));
$defaults = isset($node_types['blog']) ? array('blog' => 1) : array();
$form['blogapi_node_types'] = array(
'#type' => 'checkboxes',
'#title' => t('Enable for external blogging clients'),
'#required' => TRUE,
'#default_value' => variable_get('blogapi_node_types', $defaults),
'#options' => $node_types,
'#description' => t('Select the content types available to external blogging clients via Blog API. If supported, each enabled content type will be displayed as a separate "blog" by the external client.')
);
$blogapi_extensions_default = variable_get('blogapi_extensions_default', 'jpg jpeg gif png txt doc xls pdf ppt pps odt ods odp');
$blogapi_uploadsize_default = variable_get('blogapi_uploadsize_default', 1);
$blogapi_usersize_default = variable_get('blogapi_usersize_default', 1);
$form['settings_general'] = array(
'#type' => 'fieldset',
'#title' => t('File settings'),
'#collapsible' => TRUE,
);
$form['settings_general']['blogapi_extensions_default'] = array(
'#type' => 'textfield',
'#title' => t('Default permitted file extensions'),
'#default_value' => $blogapi_extensions_default,
'#maxlength' => 255,
'#description' => t('Default extensions that users can upload. Separate extensions with a space and do not include the leading dot.'),
);
$form['settings_general']['blogapi_uploadsize_default'] = array(
'#type' => 'textfield',
'#title' => t('Default maximum file size per upload'),
'#default_value' => $blogapi_uploadsize_default,
'#size' => 5,
'#maxlength' => 5,
'#description' => t('The default maximum file size a user can upload.'),
'#field_suffix' => t('MB')
);
$form['settings_general']['blogapi_usersize_default'] = array(
'#type' => 'textfield',
'#title' => t('Default total file size per user'),
'#default_value' => $blogapi_usersize_default,
'#size' => 5,
'#maxlength' => 5,
'#description' => t('The default maximum size of all files a user can have on the site.'),
'#field_suffix' => t('MB')
);
$form['settings_general']['upload_max_size'] = array('#value' => '<p>'. t('Your PHP settings limit the maximum file size per upload to %size.', array('%size' => format_size(file_upload_max_size()))).'</p>');
$roles = user_roles(0, 'administer content with blog api');
$form['roles'] = array('#type' => 'value', '#value' => $roles);
foreach ($roles as $rid => $role) {
$form['settings_role_'. $rid] = array(
'#type' => 'fieldset',
'#title' => t('Settings for @role', array('@role' => $role)),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
);
$form['settings_role_'. $rid]['blogapi_extensions_'. $rid] = array(
'#type' => 'textfield',
'#title' => t('Permitted file extensions'),
'#default_value' => variable_get('blogapi_extensions_'. $rid, $blogapi_extensions_default),
'#maxlength' => 255,
'#description' => t('Extensions that users in this role can upload. Separate extensions with a space and do not include the leading dot.'),
);
$form['settings_role_'. $rid]['blogapi_uploadsize_'. $rid] = array(
'#type' => 'textfield',
'#title' => t('Maximum file size per upload'),
'#default_value' => variable_get('blogapi_uploadsize_'. $rid, $blogapi_uploadsize_default),
'#size' => 5,
'#maxlength' => 5,
'#description' => t('The maximum size of a file a user can upload (in megabytes).'),
);
$form['settings_role_'. $rid]['blogapi_usersize_'. $rid] = array(
'#type' => 'textfield',
'#title' => t('Total file size per user'),
'#default_value' => variable_get('blogapi_usersize_'. $rid, $blogapi_usersize_default),
'#size' => 5,
'#maxlength' => 5,
'#description' => t('The maximum size of all files a user can have on the site (in megabytes).'),
);
}
return system_settings_form($form);
}
function blogapi_menu() {
$items['blogapi/rsd'] = array(
'title' => 'RSD',
'page callback' => 'blogapi_rsd',
'access arguments' => array('access content'),
'type' => MENU_CALLBACK,
);
$items['admin/settings/blogapi'] = array(
'title' => 'Blog API',
'description' => 'Configure the content types available to external blogging clients.',
'page callback' => 'drupal_get_form',
'page arguments' => array('blogapi_admin_settings'),
'access arguments' => array('administer site configuration'),
'type' => MENU_NORMAL_ITEM,
);
return $items;
}
function blogapi_init() {
if (drupal_is_front_page()) {
drupal_add_link(array('rel' => 'EditURI',
'type' => 'application/rsd+xml',
'title' => t('RSD'),
'href' => url('blogapi/rsd', array('absolute' => TRUE))));
}
}
function blogapi_rsd() {
global $base_url;
$xmlrpc = $base_url .'/xmlrpc.php';
$base = url('', array('absolute' => TRUE));
$blogid = 1; # until we figure out how to handle multiple bloggers
drupal_set_header('Content-Type: application/rsd+xml; charset=utf-8');
print <<<__RSD__
<?xml version="1.0"?>
<rsd version="1.0" xmlns="http://archipelago.phrasewise.com/rsd">
<service>
<engineName>Drupal</engineName>
<engineLink>http://drupal.org/</engineLink>
<homePageLink>$base</homePageLink>
<apis>
<api name="MetaWeblog" preferred="false" apiLink="$xmlrpc" blogID="$blogid" />
<api name="Blogger" preferred="false" apiLink="$xmlrpc" blogID="$blogid" />
<api name="MovableType" preferred="true" apiLink="$xmlrpc" blogID="$blogid" />
</apis>
</service>
</rsd>
__RSD__;
}
/**
* Handles extra information sent by clients according to MovableType's spec.
*/
function _blogapi_mt_extra(&$node, $struct) {
if (is_array($node)) {
$was_array = TRUE;
$node = (object)$node;
}
// mt_allow_comments
if (array_key_exists('mt_allow_comments', $struct)) {
switch ($struct['mt_allow_comments']) {
case 0:
$node->comment = COMMENT_NODE_DISABLED;
break;
case 1:
$node->comment = COMMENT_NODE_READ_WRITE;
break;
case 2:
$node->comment = COMMENT_NODE_READ_ONLY;
break;
}
}
// merge the 3 body sections (description, mt_excerpt, mt_text_more) into
// one body
if ($struct['mt_excerpt']) {
$node->body = $struct['mt_excerpt'] .'<!--break-->'. $node->body;
}
if ($struct['mt_text_more']) {
$node->body = $node->body .'<!--extended-->'. $struct['mt_text_more'];
}
// mt_convert_breaks
if ($struct['mt_convert_breaks']) {
$node->format = $struct['mt_convert_breaks'];
}
// dateCreated
if ($struct['dateCreated']) {
$node->date = format_date(mktime($struct['dateCreated']->hour, $struct['dateCreated']->minute, $struct['dateCreated']->second, $struct['dateCreated']->month, $struct['dateCreated']->day, $struct['dateCreated']->year), 'custom', 'Y-m-d H:i:s O');
}
if ($was_array) {
$node = (array)$node;
}
}
function _blogapi_get_post($node, $bodies = TRUE) {
$xmlrpcval = array(
'userid' => $node->name,
'dateCreated' => xmlrpc_date($node->created),
'title' => $node->title,
'postid' => $node->nid,
'link' => url('node/'. $node->nid, array('absolute' => TRUE)),
'permaLink' => url('node/'. $node->nid, array('absolute' => TRUE)),
);
if ($bodies) {
if ($node->comment == 1) {
$comment = 2;
}
else if ($node->comment == 2) {
$comment = 1;
}
$xmlrpcval['content'] = "<title>$node->title</title>$node->body";
$xmlrpcval['description'] = $node->body;
// Add MT specific fields
$xmlrpcval['mt_allow_comments'] = (int) $comment;
$xmlrpcval['mt_convert_breaks'] = $node->format;
}
return $xmlrpcval;
}
/**
* Validate blog ID, which maps to a content type in Drupal.
*
* Only content types configured to work with Blog API are supported.
*
* @return
* TRUE if the content type is supported and the user has permission
* to post, or a blogapi_error() XML construct otherwise.
*/
function _blogapi_validate_blogid($blogid) {
$types = _blogapi_get_node_types();
if (in_array($blogid, $types, TRUE)) {
return TRUE;
}
return blogapi_error(t("Blog API module is not configured to support the %type content type, or you don't have sufficient permissions to post this type of content.", array('%type' => $blogid)));
}
function _blogapi_get_node_types() {
$available_types = array_keys(array_filter(variable_get('blogapi_node_types', array('blog' => 1))));
$types = array();
foreach (node_get_types() as $type => $name) {
if (node_access('create', $type) && in_array($type, $available_types)) {
$types[] = $type;
}
}
return $types;
}
function _blogapi_space_used($uid) {
return db_result(db_query('SELECT SUM(filesize) FROM {blogapi_files} f WHERE f.uid = %d', $uid));
}

View file

@ -0,0 +1,21 @@
<?php
/**
* @file book-all-books-block.tpl.php
* 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 rendered as an unordered list. It is
* keyed to the parent book ID which is also the ID of the parent node
* containing an entire outline.
*
* @see template_preprocess_book_all_books_block()
*/
?>
<?php foreach ($book_menus as $book_id => $menu) : ?>
<div id="book-block-menu-<?php print $book_id; ?>" class="book-block-menu">
<?php print $menu; ?>
</div>
<?php endforeach; ?>

View file

@ -0,0 +1,52 @@
<?php
/**
* @file book-export-html.tpl.php
* 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.
* - $content: Nodes within the current outline rendered through
* book-node-export-html.tpl.php.
*
* @see template_preprocess_book_export_html()
*/
?>
<!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; ?>">
<head>
<?php print $head; ?>
<title><?php print $title; ?></title>
<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) &lt;div&gt; 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>

View file

@ -0,0 +1,51 @@
<?php
/**
* @file book-navigation.tpl.php
* 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()
*/
?>
<?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 clear-block">
<?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; ?>

View file

@ -0,0 +1,24 @@
<?php
/**
* @file book-node-export-html.tpl.php
* Default theme implementation for rendering 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()
*/
?>
<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>

View file

@ -0,0 +1,7 @@
.book-navigation .page-previous {
float: right;
}
.book-navigation .page-up {
float: right;
}

249
modules/book/book.admin.inc Normal file
View file

@ -0,0 +1,249 @@
<?php
/**
* @file
* Admin page callbacks for the book module.
*/
/**
* Returns an administrative overview of all books.
*/
function book_admin_overview() {
$rows = array();
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']));
}
$headers = array(t('Book'), t('Operations'));
return theme('table', $headers, $rows);
}
/**
* Builds and returns the book settings form.
*
* @see book_admin_settings_validate()
*
* @ingroup forms
*/
function book_admin_settings() {
$types = node_get_types('names');
$form['book_allowed_types'] = array(
'#type' => 'checkboxes',
'#title' => t('Allowed book outline types'),
'#default_value' => variable_get('book_allowed_types', array('book')),
'#options' => $types,
'#description' => t('Select content types which users with the %add-perm permission will be allowed to add to the book hierarchy. Users with the %outline-perm permission can add all content types.', array('%add-perm' => t('add content to books'), '%outline-perm' => t('administer book outlines'))),
'#required' => TRUE,
);
$form['book_child_type'] = array(
'#type' => 'radios',
'#title' => t('Default child page type'),
'#default_value' => variable_get('book_child_type', 'book'),
'#options' => $types,
'#description' => 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'))),
'#required' => TRUE,
);
$form['array_filter'] = array('#type' => 'value', '#value' => TRUE);
$form['#validate'][] = 'book_admin_settings_validate';
return system_settings_form($form);
}
/**
* Validate the book settings form.
*
* @see book_admin_settings()
*/
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'))));
}
}
/**
* Build the form to administrate the hierarchy of a single book.
*
* @see book_admin_edit_submit()
*
* @ingroup forms.
*/
function book_admin_edit($form_state, $node) {
drupal_set_title(check_plain($node->title));
$form = array();
$form['#node'] = $node;
_book_admin_table($node, $form);
$form['save'] = array(
'#type' => 'submit',
'#value' => t('Save book pages'),
);
return $form;
}
/**
* Check that the book has not been changed while using the form.
*
* @see book_admin_edit()
*/
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_state['rebuild'] = TRUE;
}
}
/**
* Handle submission of the book administrative page form.
*
* 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()
* @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['#post']['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'], FALSE);
$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)));
}
/**
* Build the table portion of the form for the book administration page.
*
* @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 = sha1(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']);
}
}
/**
* Recursive helper to build the main table in the book administration page form.
*
* @see book_admin_edit()
*/
function _book_admin_table_tree($tree, &$form) {
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' => 15,
),
'plid' => array(
'#type' => 'textfield',
'#default_value' => $data['link']['plid'],
'#size' => 6,
),
'mlid' => array(
'#type' => 'hidden',
'#default_value' => $data['link']['mlid'],
),
);
if ($data['below']) {
_book_admin_table_tree($data['below'], $form);
}
}
return $form;
}
/**
* Theme function for the book administration page form.
*
* @ingroup themeable
* @see book_admin_table()
*/
function theme_book_admin_table($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'] = 'book-plid';
$form[$key]['mlid']['#attributes']['class'] = 'book-mlid';
$form[$key]['weight']['#attributes']['class'] = 'book-weight';
$data = array(
theme('indentation', $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)) : '&nbsp',
$access ? l(t('delete'), 'node/'. $nid .'/delete', array('query' => $destination) ) : '&nbsp',
);
$row = array('data' => $data);
if (isset($form[$key]['#attributes'])) {
$row = array_merge($row, $form[$key]['#attributes']);
}
$row['class'] = empty($row['class']) ? 'draggable' : $row['class'] .' draggable';
$rows[] = $row;
}
return theme('table', $header, $rows, array('id' => 'book-outline'));
}

51
modules/book/book.css Normal file
View file

@ -0,0 +1,51 @@
.book-navigation .menu {
border-top: 1px solid #888;
padding: 1em 0 0 3em;
}
.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;
}
#edit-book-bid-wrapper .description {
clear: both;
}
#book-admin-edit select {
margin-right: 24px;
}
#book-admin-edit select.progress-disabled {
margin-right: 0;
}
#book-admin-edit tr.ahah-new-content {
background-color: #ffd;
}
#book-admin-edit .form-item {
float: left;
}

11
modules/book/book.info Normal file
View file

@ -0,0 +1,11 @@
name = Book
description = Allows users to structure site pages in a hierarchy or outline.
package = Core - optional
version = VERSION
core = 6.x
; Information added by Drupal.org packaging script on 2016-02-24
version = "6.38"
project = "drupal"
datestamp = "1456343372"

289
modules/book/book.install Normal file
View file

@ -0,0 +1,289 @@
<?php
/**
* Implementation of hook_install().
*/
function book_install() {
// Create tables.
drupal_install_schema('book');
// Add the node type.
_book_install_type_create();
}
/**
* Implementation of hook_uninstall().
*/
function book_uninstall() {
// Delete menu links.
db_query("DELETE FROM {menu_links} WHERE module = 'book'");
menu_cache_clear_all();
// Remove tables.
drupal_uninstall_schema('book');
}
function _book_install_type_create() {
// Create an additional node type
$book_node_type = array(
'type' => 'book',
'name' => t('Book page'),
'module' => 'node',
'description' => t('A <em>book page</em> is a page of content, organized into a collection of related entries collectively known as a <em>book</em>. A <em>book page</em> automatically displays links to adjacent pages, providing a simple navigation system for organizing and reviewing structured content.'),
'custom' => TRUE,
'modified' => TRUE,
'locked' => FALSE,
);
$book_node_type = (object)_node_type_set_defaults($book_node_type);
node_type_save($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');
}
/**
* Drupal 5.x to 6.x update.
*
* This function moves any existing book hierarchy into the new structure used
* in the 6.x module. Rather than storing the hierarchy in the {book} table,
* the menu API is used to store the hierarchy in the {menu_links} table and the
* {book} table serves to uniquely connect a node to a menu link.
*
* In order to accomplish this, the current hierarchy is processed using a stack.
* The stack insures that each parent is processed before any of its children
* in the book hierarchy, and is compatible with batched update processing.
*
*/
function book_update_6000() {
$ret = array();
// Set up for a multi-part update.
if (!isset($_SESSION['book_update_6000'])) {
$schema['book'] = array(
'fields' => array(
'mlid' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0),
'nid' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0),
'bid' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0),
),
'primary key' => array('mlid'),
'unique keys' => array(
'nid' => array('nid'),
),
'indexes' => array(
'bid' => array('bid'),
),
);
// Add the node type.
_book_install_type_create();
// Fix role permissions to account for the changed names
// Setup the array holding strings to match and the corresponding
// strings to replace them with.
$replace = array(
'outline posts in books' => 'administer book outlines',
'create book pages' => 'create book content',
'edit book pages' => 'edit any book content',
'edit own book pages' => 'edit own book content',
'see printer-friendly version' => 'access printer-friendly version',
);
// Loop over all the roles, and do the necessary transformations.
$query = db_query("SELECT rid, perm FROM {permission} ORDER BY rid");
while ($role = db_fetch_object($query)) {
// Replace all the old permissions with the corresponding new permissions.
$fixed_perm = strtr($role->perm, $replace);
// If the user could previously create book pages, they should get the new
// 'add content to books' permission.
if (strpos($role->perm, 'create book pages') !== FALSE) {
$fixed_perm .= ', add content to books';
}
// Only save if the permissions have changed.
if ($fixed_perm != $role->perm) {
$ret[] = update_sql("UPDATE {permission} SET perm = '$fixed_perm' WHERE rid = $role->rid");
}
}
// Determine whether there are any existing nodes in the book hierarchy.
if (db_result(db_query("SELECT COUNT(*) FROM {book}"))) {
// Temporary table for the old book hierarchy; we'll discard revision info.
$schema['book_temp'] = array(
'fields' => array(
'nid' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0),
'parent' => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
'weight' => array('type' => 'int', 'not null' => TRUE, 'default' => 0, 'size' => 'tiny')
),
'indexes' => array(
'parent' => array('parent')
),
'primary key' => array('nid'),
);
db_create_table($ret, 'book_temp', $schema['book_temp']);
// Insert each node in the old table into the temporary table.
$ret[] = update_sql("INSERT INTO {book_temp} (nid, parent, weight) SELECT b.nid, b.parent, b.weight FROM {book} b INNER JOIN {node} n on b.vid = n.vid");
$ret[] = update_sql("DROP TABLE {book}");
db_create_table($ret, 'book', $schema['book']);
$_SESSION['book_update_6000_orphans']['from'] = 0;
$_SESSION['book_update_6000'] = array();
$result = db_query("SELECT * from {book_temp} WHERE parent = 0");
// Collect all books - top-level nodes.
while ($a = db_fetch_array($result)) {
$_SESSION['book_update_6000'][] = $a;
}
$ret['#finished'] = FALSE;
return $ret;
}
else {
// No exising nodes in the hierarchy, so drop the table and re-create it.
$ret[] = update_sql("DROP TABLE {book}");
db_create_table($ret, 'book', $schema['book']);
return $ret;
}
}
elseif ($_SESSION['book_update_6000_orphans']) {
// Do the first batched part of the update - collect orphans.
$update_count = 400; // Update this many at a time
$result = db_query_range("SELECT * FROM {book_temp}", $_SESSION['book_update_6000_orphans']['from'], $update_count);
$has_rows = FALSE;
// Go through the next $update_count book pages and locate the orphans.
while ($book = db_fetch_array($result)) {
$has_rows = TRUE;
// Orphans are defined as nodes whose parent does not exist in the table.
if ($book['parent'] && !db_result(db_query("SELECT COUNT(*) FROM {book_temp} WHERE nid = %d", $book['parent']))) {
if (empty($_SESSION['book_update_6000_orphans']['book'])) {
// The first orphan becomes the parent for all other orphans.
$book['parent'] = 0;
$_SESSION['book_update_6000_orphans']['book'] = $book;
$ret[] = array('success' => TRUE, 'query' => 'Relocated orphan book pages.');
}
else {
// Re-assign the parent value of the book, and add it to the stack.
$book['parent'] = $_SESSION['book_update_6000_orphans']['book']['nid'];
$_SESSION['book_update_6000'][] = $book;
}
}
}
if ($has_rows) {
$_SESSION['book_update_6000_orphans']['from'] += $update_count;
}
else {
// Done with this part
if (!empty($_SESSION['book_update_6000_orphans']['book'])) {
// The orphans' parent is added last, so it will be processed first.
$_SESSION['book_update_6000'][] = $_SESSION['book_update_6000_orphans']['book'];
}
$_SESSION['book_update_6000_orphans'] = FALSE;
}
$ret['#finished'] = FALSE;
return $ret;
}
else {
// Do the next batched part of the update
$update_count = 100; // Update this many at a time
while ($update_count && $_SESSION['book_update_6000']) {
// Get the last node off the stack.
$book = array_pop($_SESSION['book_update_6000']);
// Add all of this node's children to the stack
$result = db_query("SELECT * FROM {book_temp} WHERE parent = %d", $book['nid']);
while ($a = db_fetch_array($result)) {
$_SESSION['book_update_6000'][] = $a;
}
if ($book['parent']) {
// If its not a top level page, get its parent's mlid.
$parent = db_fetch_array(db_query("SELECT b.mlid AS plid, b.bid FROM {book} b WHERE b.nid = %d", $book['parent']));
$book = array_merge($book, $parent);
}
else {
// There is not a parent - this is a new book.
$book['plid'] = 0;
$book['bid'] = $book['nid'];
}
$book += array(
'module' => 'book',
'link_path' => 'node/'. $book['nid'],
'router_path' => 'node/%',
'menu_name' => 'book-toc-'. $book['bid'],
);
$book = array_merge($book, db_fetch_array(db_query("SELECT title AS link_title FROM {node} WHERE nid = %d", $book['nid'])));
// Items with depth > MENU_MAX_DEPTH cannot be saved.
if (menu_link_save($book)) {
db_query("INSERT INTO {book} (mlid, nid, bid) VALUES (%d, %d, %d)", $book['mlid'], $book['nid'], $book['bid']);
}
else {
// The depth was greater then MENU_MAX_DEPTH, so attach it to the
// closest valid parent.
$book['plid'] = db_result(db_query("SELECT plid FROM {menu_links} WHERE mlid = %d", $book['plid']));
if (menu_link_save($book)) {
db_query("INSERT INTO {book} (mlid, nid, bid) VALUES (%d, %d, %d)", $book['mlid'], $book['nid'], $book['bid']);
}
}
$update_count--;
}
$ret['#finished'] = FALSE;
}
if (empty($_SESSION['book_update_6000'])) {
$ret['#finished'] = TRUE;
$ret[] = array('success' => TRUE, 'query' => 'Relocated existing book pages.');
$ret[] = update_sql("DROP TABLE {book_temp}");
unset($_SESSION['book_update_6000']);
unset($_SESSION['book_update_6000_orphans']);
}
return $ret;
}
/**
* Implementation of 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;
}

1095
modules/book/book.module Normal file

File diff suppressed because it is too large Load diff

266
modules/book/book.pages.inc Normal file
View file

@ -0,0 +1,266 @@
<?php
/**
* @file
* User page callbacks for the book module.
*/
/**
* Menu callback; prints a listing of all books.
*/
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', $book_list);
}
/**
* Menu callback; Generates various representation 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: HTML (printer friendly output)
*
* 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.
*/
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();
}
}
/**
* This function is called by book_export() to generate HTML for 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) &lt;div&gt; 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')) {
$export_data = array();
$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', $node->title, $contents, $node->book['depth']);
}
else {
drupal_not_found();
}
}
else {
drupal_access_denied();
}
}
/**
* Menu callback; show the outline form for a single node.
*/
function book_outline($node) {
drupal_set_title(check_plain($node->title));
return drupal_get_form('book_outline_form', $node);
}
/**
* Build the form to handle all book outline operations via the outline tab.
*
* @see book_outline_form_submit()
* @see book_remove_button_submit()
*
* @ingroup forms
*/
function book_outline_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, $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' => $node->nid != $node->book['bid'] && $node->book['bid'],
'#weight' => 20,
'#submit' => array('book_remove_button_submit'),
);
return $form;
}
/**
* Button submit function to redirect to removal confirm form.
*
* @see book_outline_form()
*/
function book_remove_button_submit($form, &$form_state) {
$form_state['redirect'] = 'node/'. $form['#node']->nid .'/outline/remove';
}
/**
* Handles book outline form submissions from the outline tab.
*
* @see book_outline_form()
*/
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');
}
}
/**
* Menu callback; builds a form to confirm removal of a node from the book.
*
* @see book_remove_form_submit()
*
* @ingroup forms
*/
function book_remove_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'));
}
/**
* Confirm form submit function to remove a node from the book.
*
* @see book_remove_form()
*/
function book_remove_form_submit($form, &$form_state) {
$node = $form['#node'];
if ($node->nid != $node->book['bid']) {
// Only allowed when this is not a book (top-level page).
menu_link_delete($node->book['mlid']);
db_query('DELETE FROM {book} WHERE nid = %d', $node->nid);
drupal_set_message(t('The post has been removed from the book.'));
}
$form_state['redirect'] = 'node/'. $node->nid;
}
/**
* Renders a new parent page select element when the book selection changes.
*
* This function is called via AJAX when the selected book is changed on a node
* or book outline form. It creates a new parent page select element, adds it
* to the cached form, and then returns the rendered element so it can be
* displayed on the form.
*
* @return
* The rendered parent page select element.
*/
function book_form_update() {
$bid = $_POST['book']['bid'];
if ($form = form_get_cache($_POST['form_build_id'], $form_state)) {
// Validate the bid.
if (isset($form['book']['bid']['#options'][$bid])) {
$book_link = $form['#node']->book;
$book_link['bid'] = $bid;
// Get the new options and update the cache.
$form['book']['plid'] = _book_parent_select($book_link);
form_set_cache($_POST['form_build_id'], $form, $form_state);
// Build and render the new select element, then return it in JSON format.
$form_state = array();
$form['#post'] = array();
$form = form_builder($form['form_id']['#value'] , $form, $form_state);
$output = drupal_render($form['book']['plid']);
drupal_json(array('status' => TRUE, 'data' => $output));
}
else {
drupal_json(array('status' => FALSE, 'data' => ''));
}
}
else {
drupal_json(array('status' => FALSE, 'data' => ''));
}
exit();
}

View file

@ -0,0 +1,43 @@
#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;
}

78
modules/color/color.css Normal file
View file

@ -0,0 +1,78 @@
/* 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 */
}

11
modules/color/color.info Normal file
View file

@ -0,0 +1,11 @@
name = Color
description = Allows the user to change the color scheme of certain themes.
package = Core - optional
version = VERSION
core = 6.x
; Information added by Drupal.org packaging script on 2016-02-24
version = "6.38"
project = "drupal"
datestamp = "1456343372"

View file

@ -0,0 +1,51 @@
<?php
function color_requirements($phase) {
$requirements = array();
if ($phase == 'runtime') {
// Check GD library
if (function_exists('imagegd2')) {
$info = gd_info();
$requirements['gd'] = array(
'value' => $info['GD Version'],
);
// Check PNG support
if (function_exists('imagecreatefrompng')) {
$requirements['gd']['severity'] = REQUIREMENT_OK;
}
else {
$requirements['gd']['severity'] = REQUIREMENT_ERROR;
$requirements['gd']['description'] = t('The GD library for PHP is enabled, but was compiled without PNG support. Please check the <a href="@url">PHP image documentation</a> for information on how to correct this.', array('@url' => 'http://www.php.net/manual/en/ref.image.php'));
}
}
else {
$requirements['gd'] = array(
'value' => t('Not installed'),
'severity' => REQUIREMENT_ERROR,
'description' => t('The GD library for PHP is missing or outdated. Please check the <a href="@url">PHP image documentation</a> for information on how to correct this.', array('@url' => 'http://www.php.net/manual/en/ref.image.php')),
);
}
$requirements['gd']['title'] = t('GD library');
}
return $requirements;
}
/**
* Warn site administrator if unsafe CSS color codes are found in the database.
*/
function color_update_6001() {
$ret = array();
$result = db_query("SELECT name FROM {variable} WHERE name LIKE 'color_%_palette'");
while ($variable = db_fetch_array($result)) {
$palette = variable_get($variable['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');
}
}
}
return $ret;
}

250
modules/color/color.js Normal file
View file

@ -0,0 +1,250 @@
Drupal.behaviors.color = function (context) {
// This behavior attaches by ID, so is only valid once on a page.
if ($('#color_scheme_form .color-form.color-processed').size()) {
return;
}
var form = $('#color_scheme_form .color-form', context);
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 = Drupal.settings.color.reference;
for (i in reference) {
reference[i] = farb.RGBToHSL(farb.unpack(reference[i]));
}
// Build preview
$('#preview:not(.color-processed)')
.append('<div id="gradient"></div>')
.addClass('color-processed');
var gradient = $('#preview #gradient');
var h = parseInt(gradient.css('height')) / 10;
for (i = 0; i < h; ++i) {
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 colors = this.options[this.selectedIndex].value;
if (colors != '') {
colors = colors.split(',');
for (i in colors) {
callback(inputs[i], colors[i], false, true);
}
preview();
}
});
/**
* Render the preview.
*/
function preview() {
// Solid background
$('#preview', form).css('backgroundColor', inputs[0].value);
// Text preview
$('#text', form).css('color', inputs[4].value);
$('#text a, #text h2', form).css('color', inputs[1].value);
// Set up gradient
var top = farb.unpack(inputs[2].value);
var bottom = farb.unpack(inputs[3].value);
if (top && bottom) {
var delta = [];
for (i in top) {
delta[i] = (bottom[i] - top[i]) / h;
}
var accum = top;
// Render gradient lines
$('#gradient > div', form).each(function () {
for (i in accum) {
accum[i] += delta[i];
}
this.style.backgroundColor = farb.pack(accum);
});
}
}
/**
* Shift 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) {
// Set background/foreground color
$(input).css({
backgroundColor: color,
'color': farb.RGBToHSL(farb.unpack(color))[2] > 0.5 ? '#000' : '#fff'
});
// Change input value
if (input.value && input.value != color) {
input.value = color;
// Update locked values
if (propagate) {
var i = input.i;
for (j = i + 1; ; ++j) {
if (!locks[j - 1] || $(locks[j - 1]).is('.unlocked')) break;
var 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;
var 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();
}
}
}
/**
* Reset the color scheme selector.
*/
function resetScheme() {
$('#edit-scheme', form).each(function () {
this.selectedIndex = this.options.length - 1;
});
}
// Focus the 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();
};

685
modules/color/color.module Normal file
View file

@ -0,0 +1,685 @@
<?php
/**
* Implementation of hook_help().
*/
function color_help($path, $arg) {
switch ($path) {
case 'admin/help#color':
$output = '<p>'. t('The color module allows a site administrator to quickly and easily change the color scheme of certain themes. Although not all themes support color module, both Garland (the default theme) and Minnelli were designed to take advantage of its features. By using color module with a compatible theme, you can easily change the color of links, backgrounds, text, and other theme elements. Color module requires that your <a href="@url">file download method</a> be set to public.', array('@url' => url('admin/settings/file-system'))) .'</p>';
$output .= '<p>'. t("It is important to remember that 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, you must save your color settings again, even if they haven't changed. This causes the color module generated version of the stylesheets in the files directory to be recreated using the new version of the original file.") .'</p>';
$output .= '<p>'. t('To change the color settings for a compatible theme, select the "configure" link for the theme on the <a href="@themes">themes administration page</a>.', array('@themes' => url('admin/build/themes'))) .'</p>';
$output .= '<p>'. t('For more information, see the online handbook entry for <a href="@color">Color module</a>.', array('@color' => 'http://drupal.org/handbook/modules/color/')) .'</p>';
return $output;
}
}
/**
* Implementation of hook_theme().
*/
function color_theme() {
return array(
'color_scheme_form' => array(
'arguments' => array('form' => NULL),
),
);
}
/**
* Implementation of hook_form_alter().
*/
function color_form_alter(&$form, $form_state, $form_id) {
// Insert the color changer into the theme settings page.
if ($form_id == 'system_theme_settings' && color_get_info(arg(4)) && function_exists('gd_info')) {
if (variable_get('file_downloads', FILE_DOWNLOADS_PUBLIC) != FILE_DOWNLOADS_PUBLIC) {
// Disables the color changer when the private download method is used.
// TODO: This should be solved in a different way. See issue #181003.
drupal_set_message(t('The color picker only works if the <a href="@url">download method</a> is set to public.', array('@url' => url('admin/settings/file-system'))), 'warning');
}
else {
$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_state, arg(4));
$form['#validate'][] = 'color_scheme_form_validate';
$form['#submit'][] = 'color_scheme_form_submit';
}
}
// Use the generated screenshot in the theme list.
if ($form_id == 'system_theme_select_form' || $form_id == 'system_themes') {
$themes = list_themes();
foreach (element_children($form) as $theme) {
if ($screenshot = variable_get('color_'. $theme .'_screenshot', NULL)) {
if (isset($form[$theme]['screenshot'])) {
$form[$theme]['screenshot']['#value'] = theme('image', $screenshot, '', '', array('class' => 'screenshot'), FALSE);
}
}
}
}
}
/**
* Callback for the theme to alter the resources used.
*/
function _color_page_alter(&$vars) {
global $language, $theme_key;
// Override stylesheets.
$color_paths = variable_get('color_'. $theme_key .'_stylesheets', array());
if (!empty($color_paths)) {
// Loop over theme CSS files and try to rebuild CSS array with rewritten
// stylesheets. Keep the orginal order intact for CSS cascading.
$new_theme_css = array();
foreach ($vars['css']['all']['theme'] as $old_path => $old_preprocess) {
// Add the non-colored stylesheet first as we might not find a
// re-colored stylesheet for replacement later.
$new_theme_css[$old_path] = $old_preprocess;
// 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 (basename($old_path) == basename($color_path)) {
// Pull out the non-colored and add rewritten stylesheet.
unset($new_theme_css[$old_path]);
$new_theme_css[$color_path] = $old_preprocess;
// If the current language is RTL and the CSS file had an RTL variant,
// pull out the non-colored and add rewritten RTL stylesheet.
if ($language->direction == LANGUAGE_RTL) {
$rtl_old_path = str_replace('.css', '-rtl.css', $old_path);
$rtl_color_path = str_replace('.css', '-rtl.css', $color_path);
if (file_exists($rtl_color_path)) {
unset($new_theme_css[$rtl_old_path]);
$new_theme_css[$rtl_color_path] = $old_preprocess;
}
}
break;
}
}
}
$vars['css']['all']['theme'] = $new_theme_css;
$vars['styles'] = drupal_get_css($vars['css']);
}
// Override logo.
$logo = variable_get('color_'. $theme_key .'_logo', NULL);
if ($logo && $vars['logo'] && preg_match('!'. $theme_key .'/logo.png$!', $vars['logo'])) {
$vars['logo'] = base_path() . $logo;
}
}
/**
* Retrieve the color.module info for a particular theme.
*/
function color_get_info($theme) {
$path = drupal_get_path('theme', $theme);
$file = $path .'/color/color.inc';
if ($path && file_exists($file)) {
include $file;
return $info;
}
}
/**
* Helper function to retrieve the color palette for a particular theme.
*/
function color_get_palette($theme, $default = false) {
// Fetch and expand default palette
$fields = array('base', 'link', 'top', 'bottom', 'text');
$info = color_get_info($theme);
$keys = array_keys($info['schemes']);
foreach (explode(',', array_shift($keys)) as $k => $scheme) {
$palette[$fields[$k]] = $scheme;
}
// Load variable
return $default ? $palette : variable_get('color_'. $theme .'_palette', $palette);
}
/**
* Form callback. Returns the configuration form.
*/
function color_scheme_form(&$form_state, $theme) {
$base = drupal_get_path('module', 'color');
$info = color_get_info($theme);
// Add Farbtastic color picker
drupal_add_css('misc/farbtastic/farbtastic.css', 'module', 'all', FALSE);
drupal_add_js('misc/farbtastic/farbtastic.js');
// Add custom CSS/JS
drupal_add_css($base .'/color.css', 'module', 'all', FALSE);
drupal_add_js($base .'/color.js');
drupal_add_js(array('color' => array(
'reference' => color_get_palette($theme, true)
)), 'setting');
// See if we're using a predefined scheme
$current = implode(',', variable_get('color_'. $theme .'_palette', array()));
// Note: we use the original theme when the default scheme is chosen.
$current = isset($info['schemes'][$current]) ? $current : ($current == '' ? reset($info['schemes']) : '');
// Add scheme selector
$info['schemes'][''] = t('Custom');
$form['scheme'] = array(
'#type' => 'select',
'#title' => t('Color set'),
'#options' => $info['schemes'],
'#default_value' => $current,
);
// Add palette fields
$palette = color_get_palette($theme);
$names = array(
'base' => t('Base color'),
'link' => t('Link color'),
'top' => t('Header top'),
'bottom' => t('Header bottom'),
'text' => t('Text color')
);
$form['palette']['#tree'] = true;
foreach ($palette as $name => $value) {
$form['palette'][$name] = array(
'#type' => 'textfield',
'#title' => $names[$name],
'#default_value' => $value,
'#size' => 8,
);
}
$form['theme'] = array('#type' => 'value', '#value' => arg(4));
$form['info'] = array('#type' => 'value', '#value' => $info);
return $form;
}
/**
* Theme color form.
*
* @ingroup themeable
*/
function theme_color_scheme_form($form) {
// Include stylesheet
$theme = $form['theme']['#value'];
$info = $form['info']['#value'];
$path = drupal_get_path('theme', $theme) .'/';
drupal_add_css($path . $info['preview_css']);
$output = '';
// Wrapper
$output .= '<div class="color-form clear-block">';
// Color schemes
$output .= drupal_render($form['scheme']);
// Palette
$output .= '<div id="palette" class="clear-block">';
foreach (element_children($form['palette']) as $name) {
$output .= drupal_render($form['palette'][$name]);
}
$output .= '</div>';
// Preview
$output .= drupal_render($form);
$output .= '<h2>'. t('Preview') .'</h2>';
$output .= '<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" style="background-image: url('. base_path() . $path . $info['preview_image'] .')"></div></div>';
// Close wrapper
$output .= '</div>';
return $output;
}
/**
* Validation handler for color change form.
*/
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 (!preg_match('/^#([a-f0-9]{3}){1,2}$/iD', $color)) {
form_set_error('palette][' . $key, t('%name must be a valid hexadecimal CSS color value.', array('%name' => $form['color']['palette'][$key]['#title'])));
}
}
}
/**
* Submit handler for color change form.
*/
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'] != '') {
$scheme = explode(',', $form_state['values']['scheme']);
foreach ($palette as $k => $color) {
$palette[$k] = array_shift($scheme);
}
}
// 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;
$usage = memory_get_usage();
$limit = parse_size(ini_get('memory_limit'));
if ($usage + $required > $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 - $limit), '@url' => 'http://www.php.net/manual/en/ini.core.php#ini.sect.resource-limits')), 'error');
return;
}
}
// Delete old files
foreach (variable_get('color_'. $theme .'_files', array()) as $file) {
@unlink($file);
}
if (isset($file) && $file = dirname($file)) {
@rmdir($file);
}
// Don't render the default colorscheme, use the standard theme instead.
if (implode(',', color_get_palette($theme, true)) == implode(',', $palette)
|| $form_state['values']['op'] == t('Reset to defaults')) {
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(md5(serialize($palette) . microtime()), 0, 8);
$paths['color'] = file_directory_path() .'/color';
$paths['target'] = $paths['color'] .'/'. $id;
foreach ($paths as $path) {
file_check_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 = basename($file);
$source = $paths['source'] . $file;
file_copy($source, $paths['target'] . $base);
$paths['map'][$file] = $base;
$paths['files'][] = $paths['target'] . $base;
}
// Render new images, if image 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 = 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']);
}
/**
* Rewrite 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;
unset($conversion['base']);
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 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.
else if (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;
}
/**
* Save the rewritten stylesheet to disk.
*/
function _color_save_stylesheet($file, $style, &$paths) {
// Write new stylesheet.
file_save_data($style, $file, FILE_EXISTS_REPLACE);
$paths['files'][] = $file;
// Set standard file permissions for webserver-generated files.
@chmod($file, 0664);
}
/**
* Render 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 gradient.
for ($y = 0; $y < $info['gradient'][3]; ++$y) {
$color = _color_blend($target, $palette['top'], $palette['bottom'], $y / ($info['gradient'][3] - 1));
imagefilledrectangle($target, $info['gradient'][0], $info['gradient'][1] + $y, $info['gradient'][0] + $info['gradient'][2], $info['gradient'][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 = basename($file);
$image = $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
@chmod(realpath($image), 0664);
// Build before/after map of image paths.
$paths['map'][$file] = $base;
}
// Clean up target buffer.
imagedestroy($target);
}
/**
* Shift 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);
}
/**
* Convert 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);
}
/**
* Blend two hex colors and return 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);
}
/**
* Convert 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;
}
/**
* Convert 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);
}
/**
* Convert a 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;
}
/**
* Convert 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);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 B

View file

@ -0,0 +1,20 @@
<?php
/**
* @file comment-folded.tpl.php
* Default theme implementation for folded comments.
*
* Available variables:
* - $title: Linked title to full comment.
* - $new: New comment marker.
* - $author: Comment author. Can be link or plain text.
* - $date: Date and time of posting.
* - $comment: Full comment object.
*
* @see template_preprocess_comment_folded()
* @see theme_comment_folded()
*/
?>
<div class="comment-folded">
<span class="subject"><?php print $title .' '. $new; ?></span><span class="credit"><?php print t('by') .' '. $author; ?></span>
</div>

View file

@ -0,0 +1,5 @@
.indented {
margin-left: 0;
margin-right: 25px;
}

View file

@ -0,0 +1,34 @@
<?php
/**
* @file comment-wrapper.tpl.php
* Default theme implementation to wrap comments.
*
* Available variables:
* - $content: All comments for a given page. Also contains sorting controls
* and comment forms if the site is configured for it.
*
* 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_COLLAPSED
* - COMMENT_MODE_FLAT_EXPANDED
* - COMMENT_MODE_THREADED_COLLAPSED
* - COMMENT_MODE_THREADED_EXPANDED
* - $display_order
* - COMMENT_ORDER_NEWEST_FIRST
* - COMMENT_ORDER_OLDEST_FIRST
* - $comment_controls_state
* - COMMENT_CONTROLS_ABOVE
* - COMMENT_CONTROLS_BELOW
* - COMMENT_CONTROLS_ABOVE_BELOW
* - COMMENT_CONTROLS_HIDDEN
*
* @see template_preprocess_comment_wrapper()
*/
?>
<div id="comments">
<?php print $content; ?>
</div>

View file

@ -0,0 +1,294 @@
<?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, arg(4));
}
}
/**
* Form builder; Builds the comment overview form for the admin.
*
* @param $type
* Not used.
* @param $arg
* Current path's fourth component deciding the form type (Published comments/Approval queue)
* @return
* The form structure.
* @ingroup forms
* @see comment_admin_overview_validate()
* @see comment_admin_overview_submit()
* @see theme_comment_admin_overview()
*/
function comment_admin_overview($type = 'new', $arg) {
// build an 'Update options' form
$form['options'] = array(
'#type' => 'fieldset', '#title' => t('Update options'),
'#prefix' => '<div class="container-inline">', '#suffix' => '</div>'
);
$options = array();
foreach (comment_operations($arg == 'approval' ? 'publish' : 'unpublish') as $key => $value) {
$options[$key] = $value[0];
}
$form['options']['operation'] = array('#type' => 'select', '#options' => $options, '#default_value' => 'publish');
$form['options']['submit'] = array('#type' => 'submit', '#value' => t('Update'));
// load the comments that we want to display
$status = ($arg == 'approval') ? COMMENT_NOT_PUBLISHED : COMMENT_PUBLISHED;
$form['header'] = array('#type' => 'value', '#value' => array(
theme('table_select_header_cell'),
array('data' => t('Subject'), 'field' => 'subject'),
array('data' => t('Author'), 'field' => 'name'),
array('data' => t('Posted in'), 'field' => 'node_title'),
array('data' => t('Time'), 'field' => 'timestamp', 'sort' => 'desc'),
array('data' => t('Operations'))
));
$result = pager_query('SELECT c.subject, c.nid, c.cid, c.comment, c.timestamp, c.status, c.name, c.homepage, u.name AS registered_name, u.uid, n.title as node_title FROM {comments} c INNER JOIN {users} u ON u.uid = c.uid INNER JOIN {node} n ON n.nid = c.nid WHERE c.status = %d'. tablesort_sql($form['header']['#value']), 50, 0, NULL, $status);
// build a table listing the appropriate comments
$destination = drupal_get_destination();
while ($comment = db_fetch_object($result)) {
$comments[$comment->cid] = '';
$comment->name = $comment->uid ? $comment->registered_name : $comment->name;
$form['subject'][$comment->cid] = array('#value' => l($comment->subject, 'node/'. $comment->nid, array('attributes' => array('title' => truncate_utf8($comment->comment, 128)), 'fragment' => 'comment-'. $comment->cid)));
$form['username'][$comment->cid] = array('#value' => theme('username', $comment));
$form['node_title'][$comment->cid] = array('#value' => l($comment->node_title, 'node/'. $comment->nid));
$form['timestamp'][$comment->cid] = array('#value' => format_date($comment->timestamp, 'small'));
$form['operations'][$comment->cid] = array('#value' => l(t('edit'), 'comment/edit/'. $comment->cid, array('query' => $destination)));
}
$form['comments'] = array('#type' => 'checkboxes', '#options' => isset($comments) ? $comments: array());
$form['pager'] = array('#value' => theme('pager', NULL, 50, 0));
return $form;
}
/**
* Validate comment_admin_overview form submissions.
*
* We can't execute any 'Update options' if no comments were selected.
*/
function comment_admin_overview_validate($form, &$form_state) {
$form_state['values']['comments'] = array_diff($form_state['values']['comments'], array(0));
if (count($form_state['values']['comments']) == 0) {
form_set_error('', t('Please 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) {
$operations = comment_operations();
if (!empty($operations[$form_state['values']['operation']][1])) {
// extract the appropriate database query operation
$query = $operations[$form_state['values']['operation']][1];
foreach ($form_state['values']['comments'] as $cid => $value) {
if ($value) {
// perform the update action, then refresh node statistics
db_query($query, $cid);
$comment = _comment_load($cid);
_comment_update_node_statistics($comment->nid);
// Allow modules to respond to the updating of a comment.
comment_invoke_comment($comment, $form_state['values']['operation']);
// Add an entry to the watchdog log.
watchdog('content', 'Comment: updated %subject.', array('%subject' => $comment->subject), WATCHDOG_NOTICE, l(t('view'), 'node/'. $comment->nid, array('fragment' => 'comment-'. $comment->cid)));
}
}
cache_clear_all();
drupal_set_message(t('The update has been performed.'));
$form_state['redirect'] = 'admin/content/comment';
}
}
/**
* Theme the comment admin form.
*
* @param $form
* An associative array containing the structure of the form.
* @ingroup themeable
*/
function theme_comment_admin_overview($form) {
$output = drupal_render($form['options']);
if (isset($form['subject']) && is_array($form['subject'])) {
foreach (element_children($form['subject']) as $key) {
$row = array();
$row[] = drupal_render($form['comments'][$key]);
$row[] = drupal_render($form['subject'][$key]);
$row[] = drupal_render($form['username'][$key]);
$row[] = drupal_render($form['node_title'][$key]);
$row[] = drupal_render($form['timestamp'][$key]);
$row[] = drupal_render($form['operations'][$key]);
$rows[] = $row;
}
}
else {
$rows[] = array(array('data' => t('No comments available.'), 'colspan' => '6'));
}
$output .= theme('table', $form['header']['#value'], $rows);
if ($form['pager']['#value']) {
$output .= drupal_render($form['pager']);
}
$output .= drupal_render($form);
return $output;
}
/**
* List the selected comments and verify that the admin really 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_state) {
$edit = $form_state['post'];
$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_result(db_query('SELECT subject FROM {comments} WHERE cid = %d', $cid));
$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.
*
* Perform the actual comment deletion.
*/
function comment_multiple_delete_confirm_submit($form, &$form_state) {
if ($form_state['values']['confirm']) {
foreach ($form_state['values']['comments'] as $cid => $value) {
$comment = _comment_load($cid);
_comment_delete_thread($comment);
_comment_update_node_statistics($comment->nid);
}
cache_clear_all();
drupal_set_message(t('The comments have been deleted.'));
}
$form_state['redirect'] = 'admin/content/comment';
}
/**
* Menu callback; delete a comment.
*
* @param $cid
* The comment do be deleted.
*/
function comment_delete($cid = NULL) {
$comment = db_fetch_object(db_query('SELECT c.*, u.name AS registered_name, u.uid FROM {comments} c INNER JOIN {users} u ON u.uid = c.uid WHERE c.cid = %d', $cid));
$comment->name = $comment->uid ? $comment->registered_name : $comment->name;
$output = '';
if (is_object($comment) && is_numeric($comment->cid)) {
$output = drupal_get_form('comment_confirm_delete', $comment);
}
else {
drupal_set_message(t('The comment no longer exists.'));
}
return $output;
}
/**
* Form builder; Builds the confirmation form for deleting a single comment.
*
* @ingroup forms
* @see comment_confirm_delete_submit()
*/
function comment_confirm_delete(&$form_state, $comment) {
$form = array();
$form['#comment'] = $comment;
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) {
drupal_set_message(t('The comment and all its replies have been deleted.'));
$comment = $form['#comment'];
// Delete comment and its replies.
_comment_delete_thread($comment);
_comment_update_node_statistics($comment->nid);
// Clear the cache so an anonymous user sees that his comment was deleted.
cache_clear_all();
$form_state['redirect'] = "node/$comment->nid";
}
/**
* Perform the actual deletion of a comment and all its replies.
*
* @param $comment
* An associative array describing the comment to be deleted.
*/
function _comment_delete_thread($comment) {
if (!is_object($comment) || !is_numeric($comment->cid)) {
watchdog('content', 'Cannot delete non-existent comment.', array(), WATCHDOG_WARNING);
return;
}
// Delete the comment:
db_query('DELETE FROM {comments} WHERE cid = %d', $comment->cid);
watchdog('content', 'Comment: deleted %subject.', array('%subject' => $comment->subject));
comment_invoke_comment($comment, 'delete');
// Delete the comment's replies
$result = db_query('SELECT c.*, u.name AS registered_name, u.uid FROM {comments} c INNER JOIN {users} u ON u.uid = c.uid WHERE pid = %d', $comment->cid);
while ($comment = db_fetch_object($result)) {
$comment->name = $comment->uid ? $comment->registered_name : $comment->name;
_comment_delete_thread($comment);
}
}

View file

@ -0,0 +1,10 @@
.indented {
margin-left: 25px; /* LTR */
}
.comment-unpublished {
background-color: #fff4f4;
}
.preview .comment {
background-color: #ffffea;
}

View file

@ -0,0 +1,11 @@
name = Comment
description = Allows users to comment on and discuss published content.
package = Core - optional
version = VERSION
core = 6.x
; Information added by Drupal.org packaging script on 2016-02-24
version = "6.38"
project = "drupal"
datestamp = "1456343372"

View file

@ -0,0 +1,249 @@
<?php
/**
* Implementation of hook_enable().
*/
function comment_enable() {
// Insert records into the node_comment_statistics for nodes that are missing.
db_query("INSERT INTO {node_comment_statistics} (nid, last_comment_timestamp, last_comment_name, last_comment_uid, comment_count) SELECT n.nid, n.changed, NULL, n.uid, 0 FROM {node} n LEFT JOIN {node_comment_statistics} c ON n.nid = c.nid WHERE c.comment_count IS NULL");
}
/**
* Changed node_comment_statistics to use node->changed to avoid future timestamps.
*/
function comment_update_1() {
// Change any future last comment timestamps to now.
db_query('UPDATE {node_comment_statistics} SET last_comment_timestamp = %d WHERE last_comment_timestamp > %d', time(), time());
// Unstuck node indexing timestamp if needed.
if (($last = variable_get('node_cron_last', FALSE)) !== FALSE) {
variable_set('node_cron_last', min(time(), $last));
}
return array();
}
function comment_update_6001() {
$ret[] = update_sql("ALTER TABLE {comments} DROP score");
$ret[] = update_sql("ALTER TABLE {comments} DROP users");
return $ret;
}
/**
* Changed comment settings from global to per-node -- copy global
* settings to all node types.
*/
function comment_update_6002() {
// Comment module might not be enabled when this is run, but we need the
// constants defined by the module for this update.
drupal_load('module', 'comment');
$settings = array(
'comment_default_mode' => COMMENT_MODE_THREADED_EXPANDED,
'comment_default_order' => COMMENT_ORDER_NEWEST_FIRST,
'comment_default_per_page' => 50,
'comment_controls' => COMMENT_CONTROLS_HIDDEN,
'comment_anonymous' => COMMENT_ANONYMOUS_MAYNOT_CONTACT,
'comment_subject_field' => 1,
'comment_preview' => COMMENT_PREVIEW_REQUIRED,
'comment_form_location' => COMMENT_FORM_SEPARATE_PAGE,
);
$types = node_get_types();
foreach ($settings as $setting => $default) {
$value = variable_get($setting, $default);
foreach ($types as $type => $object) {
variable_set($setting .'_'. $type, $value);
}
variable_del($setting);
}
return array(array('success' => TRUE, 'query' => 'Global comment settings copied to all node types.'));
}
/**
* Add index to parent ID field.
*/
function comment_update_6003() {
$ret = array();
db_add_index($ret, 'comments', 'pid', array('pid'));
return $ret;
}
/**
* @addtogroup updates-6.x-extra
* @{
*/
/**
* Add index to to node_comment_statistics on comment_count
*/
function comment_update_6004() {
$ret = array();
db_add_index($ret, 'node_comment_statistics', 'comment_count', array('comment_count'));
return $ret;
}
/**
* Add indices to uid fields.
*/
function comment_update_6005() {
$ret = array();
db_add_index($ret, 'comments', 'comment_uid', array('uid'));
db_add_index($ret, 'node_comment_statistics', 'last_comment_uid', array('last_comment_uid'));
return $ret;
}
/**
* @} End of "addtogroup updates-6.x-extra".
* The next series of updates should start at 7000.
*/
/**
* Implementation of hook_schema().
*/
function comment_schema() {
$schema['comments'] = 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 {comments}.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.',
),
'comment' => array(
'type' => 'text',
'not null' => TRUE,
'size' => 'big',
'description' => 'The comment body.',
),
'hostname' => array(
'type' => 'varchar',
'length' => 128,
'not null' => TRUE,
'default' => '',
'description' => "The author's host name.",
),
'timestamp' => array(
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'description' => 'The time that the comment was created, or last edited by its author, as a Unix timestamp.',
),
'status' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
'size' => 'tiny',
'description' => 'The published status of a comment. (0 = Published, 1 = Not Published)',
),
'format' => array(
'type' => 'int',
'size' => 'small',
'not null' => TRUE,
'default' => 0,
'description' => 'The {filter_formats}.format of the comment body.',
),
'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.",
)
),
'indexes' => array(
'pid' => array('pid'),
'nid' => array('nid'),
'comment_uid' => array('uid'),
'status' => array('status'), // This index is probably unused
),
'primary key' => array('cid'),
);
$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.',
),
'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 {comments}.timestamp.',
),
'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 {comments}.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 {comments}.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'),
),
);
return $schema;
}

View file

@ -0,0 +1,34 @@
Drupal.behaviors.comment = function (context) {
var parts = new Array("name", "homepage", "mail");
var cookie = '';
for (i=0;i<3;i++) {
cookie = Drupal.comment.getCookie('comment_info_' + parts[i]);
if (cookie != '') {
$("#comment-form input[name=" + parts[i] + "]:not(.comment-processed)", context)
.val(cookie)
.addClass('comment-processed');
}
}
};
Drupal.comment = {};
Drupal.comment.getCookie = function(name) {
var search = name + '=';
var returnValue = '';
if (document.cookie.length > 0) {
offset = document.cookie.indexOf(search);
if (offset != -1) {
offset += search.length;
var end = document.cookie.indexOf(';', offset);
if (end == -1) {
end = document.cookie.length;
}
returnValue = decodeURIComponent(document.cookie.substring(offset, end).replace(/\+/g, '%20'));
}
}
return returnValue;
};

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,115 @@
<?php
/**
* @file
* User page callbacks for the comment module.
*/
/**
* Form builder; generate a comment editing form.
*
* @param $cid
* ID of the comment to be edited.
* @ingroup forms
*/
function comment_edit($cid) {
global $user;
$comment = db_fetch_object(db_query('SELECT c.*, u.uid, u.name AS registered_name, u.data FROM {comments} c INNER JOIN {users} u ON c.uid = u.uid WHERE c.cid = %d', $cid));
$comment = drupal_unpack($comment);
$comment->name = $comment->uid ? $comment->registered_name : $comment->name;
if (comment_access('edit', $comment)) {
return comment_form_box((array)$comment);
}
else {
drupal_access_denied();
}
}
/**
* 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
* The rendered parent node or comment plus the new comment form.
*/
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'] : '';
$output = '';
if (user_access('access comments')) {
// The user is previewing a comment prior to submitting it.
if ($op == t('Preview')) {
if (user_access('post comments')) {
$output .= comment_form_box(array('pid' => $pid, 'nid' => $node->nid), NULL);
}
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) {
// load the comment whose cid = $pid
if ($comment = db_fetch_object(db_query('SELECT c.*, u.uid, u.name AS registered_name, u.signature, u.signature_format, u.picture, u.data FROM {comments} c INNER JOIN {users} u ON c.uid = u.uid WHERE c.cid = %d AND c.status = %d', $pid, COMMENT_PUBLISHED))) {
// 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 = drupal_unpack($comment);
$comment->name = $comment->uid ? $comment->registered_name : $comment->name;
$output .= theme('comment_view', $comment, $node);
}
else {
drupal_set_message(t('The comment you are replying to does not exist.'), 'error');
drupal_goto("node/$node->nid");
}
}
// This is the case where the comment is in response to a node. Display the node.
else if (user_access('access content')) {
$output .= node_view($node);
}
// Should we show the reply box?
if (node_comment_mode($node->nid) != COMMENT_NODE_READ_WRITE) {
drupal_set_message(t("This discussion is closed: you can't post new comments."), 'error');
drupal_goto("node/$node->nid");
}
else if (user_access('post comments')) {
$output .= comment_form_box(array('pid' => $pid, 'nid' => $node->nid), t('Reply'));
}
else {
drupal_set_message(t('You are not authorized to post comments.'), 'error');
drupal_goto("node/$node->nid");
}
}
}
else {
drupal_set_message(t('You are not authorized to view comments.'), 'error');
drupal_goto("node/$node->nid");
}
return $output;
}

View file

@ -0,0 +1,51 @@
<?php
/**
* @file comment.tpl.php
* Default theme implementation for comments.
*
* Available variables:
* - $author: Comment author. Can be link or plain text.
* - $content: Body of the post.
* - $date: Date and time of posting.
* - $links: Various operational links.
* - $new: New comment marker.
* - $picture: Authors picture.
* - $signature: Authors signature.
* - $status: Comment status. Possible values are:
* comment-unpublished, comment-published or comment-preview.
* - $submitted: By line with date and time.
* - $title: Linked title.
*
* These two variables are provided for context.
* - $comment: Full comment object.
* - $node: Node object the comments are attached to.
*
* @see template_preprocess_comment()
* @see theme_comment()
*/
?>
<div class="comment<?php print ($comment->new) ? ' comment-new' : ''; print ' '. $status ?> clear-block">
<?php print $picture ?>
<?php if ($comment->new): ?>
<span class="new"><?php print $new ?></span>
<?php endif; ?>
<h3><?php print $title ?></h3>
<div class="submitted">
<?php print $submitted ?>
</div>
<div class="content">
<?php print $content ?>
<?php if ($signature): ?>
<div class="user-signature clear-block">
<?php print $signature ?>
</div>
<?php endif; ?>
</div>
<?php print $links ?>
</div>

View file

@ -0,0 +1,172 @@
<?php
/**
* @file
* Admin page callbacks for the contact module.
*/
/**
* Categories/list tab.
*/
function contact_admin_categories() {
$result = db_query('SELECT cid, category, recipients, selected FROM {contact} ORDER BY weight, category');
$rows = array();
while ($category = db_fetch_object($result)) {
$rows[] = array(check_plain($category->category), check_plain($category->recipients), ($category->selected ? t('Yes') : t('No')), l(t('edit'), 'admin/build/contact/edit/'. $category->cid), l(t('delete'), 'admin/build/contact/delete/'. $category->cid));
}
$header = array(t('Category'), t('Recipients'), t('Selected'), array('data' => t('Operations'), 'colspan' => 2));
return theme('table', $header, $rows);
}
/**
* Category edit page.
*/
function contact_admin_edit($form_state = array(), $op, $contact = NULL) {
if (empty($contact) || $op == 'add') {
$contact = array(
'category' => '',
'recipients' => '',
'reply' => '',
'weight' => 0,
'selected' => 0,
'cid' => NULL,
);
}
$form['contact_op'] = array('#type' => 'value', '#value' => $op);
$form['category'] = array('#type' => 'textfield',
'#title' => t('Category'),
'#maxlength' => 255,
'#default_value' => $contact['category'],
'#description' => t("Example: 'website feedback' or 'product information'."),
'#required' => TRUE,
);
$form['recipients'] = array('#type' => 'textarea',
'#title' => t('Recipients'),
'#default_value' => $contact['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' => $contact['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' => $contact['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' => $contact['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' => $contact['cid'],
);
$form['submit'] = array('#type' => 'submit',
'#value' => t('Save'),
);
return $form;
}
/**
* Validate the contact category edit page form submission.
*/
function contact_admin_edit_validate($form, &$form_state) {
if (empty($form_state['values']['category'])) {
form_set_error('category', t('You must enter a category.'));
}
if (empty($form_state['values']['recipients'])) {
form_set_error('recipients', t('You must enter one or more recipients.'));
}
else {
$recipients = explode(',', $form_state['values']['recipients']);
foreach ($recipients as $recipient) {
if (!valid_email_address(trim($recipient))) {
form_set_error('recipients', t('%recipient is an invalid e-mail address.', array('%recipient' => $recipient)));
}
}
}
}
/**
* Process the contact category edit page form submission.
*/
function contact_admin_edit_submit($form, &$form_state) {
if ($form_state['values']['selected']) {
// Unselect all other contact categories.
db_query('UPDATE {contact} SET selected = 0');
}
$recipients = explode(',', $form_state['values']['recipients']);
foreach ($recipients as $key => $recipient) {
// E-mail address validation has already been done in _validate.
$recipients[$key] = trim($recipient);
}
$form_state['values']['recipients'] = implode(',', $recipients);
if (empty($form_state['values']['cid']) || $form_state['values']['contact_op'] == 'add') {
drupal_write_record('contact', $form_state['values']);
drupal_set_message(t('Category %category has been added.', array('%category' => $form_state['values']['category'])));
watchdog('mail', 'Contact form: category %category added.', array('%category' => $form_state['values']['category']), WATCHDOG_NOTICE, l(t('view'), 'admin/build/contact'));
}
else {
drupal_write_record('contact', $form_state['values'], 'cid');
drupal_set_message(t('Category %category has been updated.', array('%category' => $form_state['values']['category'])));
watchdog('mail', 'Contact form: category %category updated.', array('%category' => $form_state['values']['category']), WATCHDOG_NOTICE, l(t('view'), 'admin/build/contact'));
}
$form_state['redirect'] = 'admin/build/contact';
return;
}
/**
* Category delete page.
*/
function contact_admin_delete(&$form_state, $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/build/contact', t('This action cannot be undone.'), t('Delete'), t('Cancel'));
}
/**
* Process category delete form submission.
*/
function contact_admin_delete_submit($form, &$form_state) {
$contact = $form_state['values']['contact'];
db_query("DELETE FROM {contact} WHERE cid = %d", $contact['cid']);
drupal_set_message(t('Category %category has been deleted.', array('%category' => $contact['category'])));
watchdog('mail', 'Contact form: category %category deleted.', array('%category' => $contact['category']), WATCHDOG_NOTICE);
$form_state['redirect'] = 'admin/build/contact';
return;
}
function contact_admin_settings() {
$form['contact_form_information'] = array('#type' => 'textarea',
'#title' => t('Additional information'),
'#default_value' => variable_get('contact_form_information', t('You can leave a message using the contact form below.')),
'#description' => t('Information to show on the <a href="@form">contact page</a>. Can be anything from submission guidelines to your postal address or telephone number.', array('@form' => url('contact'))),
);
$form['contact_hourly_threshold'] = array('#type' => 'select',
'#title' => t('Hourly threshold'),
'#options' => drupal_map_assoc(array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 20, 30, 40, 50)),
'#default_value' => variable_get('contact_hourly_threshold', 3),
'#description' => t('The maximum number of contact form submissions a user can perform per hour.'),
);
$form['contact_default_status'] = array(
'#type' => 'checkbox',
'#title' => t('Enable personal contact form by default'),
'#default_value' => variable_get('contact_default_status', 1),
'#description' => t('Default status of the personal contact form for new users.'),
);
return system_settings_form($form);
}

View file

@ -0,0 +1,11 @@
name = Contact
description = Enables the use of both personal and site-wide contact forms.
package = Core - optional
version = VERSION
core = 6.x
; Information added by Drupal.org packaging script on 2016-02-24
version = "6.38"
project = "drupal"
datestamp = "1456343372"

View file

@ -0,0 +1,80 @@
<?php
/**
* Implementation of hook_install().
*/
function contact_install() {
// Create tables.
drupal_install_schema('contact');
}
/**
* Implementation of hook_uninstall().
*/
function contact_uninstall() {
// Remove tables.
drupal_uninstall_schema('contact');
variable_del('contact_default_status');
variable_del('contact_form_information');
variable_del('contact_hourly_threshold');
}
/**
* Implementation of 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.',
),
'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,
'size' => 'tiny',
'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;
}

View file

@ -0,0 +1,212 @@
<?php
/**
* @file
* Enables the use of personal and site-wide contact forms.
*/
/**
* Implementation of hook_help().
*/
function contact_help($path, $arg) {
switch ($path) {
case 'admin/help#contact':
$output = '<p>'. t('The contact module facilitates communication via e-mail, by allowing your site\'s visitors to contact one another (personal contact forms), and by providing a simple way to direct messages to a set of administrator-defined recipients (the <a href="@contact">contact page</a>). With either form, users specify a subject, write their message, and (optionally) have a copy of their message sent to their own e-mail address.', array('@contact' => url('contact'))) .'</p>';
$output .= '<p>'. t("Personal contact forms allow users to be contacted via e-mail, while keeping recipient e-mail addresses 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 leading to their personal contact form is available on their user profile. Site administrators have access to all personal contact forms (even if they have been disabled). The <em>Contact</em> tab is only visible when viewing another user's profile (users do not see their own <em>Contact</em> tab).") .'</p>';
$output .= '<p>'. t('The <a href="@contact">contact page</a> provides a simple form for visitors to leave comments, feedback, or other requests. Messages are routed by selecting a category from a list of administrator-defined options; each category has its own set of e-mail recipients. Common categories for a business site include, for example, "Website feedback" (messages are forwarded to web site administrators) and "Product information" (messages are forwarded to members of the sales department). The actual e-mail addresses defined within a category are not displayed. Only users in roles with the <em>access site-wide contact form</em> permission may access the <a href="@contact">contact page</a>.', array('@contact' => url('contact'))) .'</p>';
$output .= '<p>'. t('A link to your site\'s <a href="@contact">contact page</a> from the main <em>Navigation</em> menu is created, but is disabled by default. Create a similar link on another menu by adding a menu item pointing to the path "contact"', array('@contact' => url('contact'))) .'</p>';
$output .= '<p>'. t('Customize the <a href="@contact">contact page</a> with additional information (like physical location, mailing address, and telephone number) using the <a href="@contact-settings">contact form settings page</a>. The <a href="@contact-settings">settings page</a> also provides configuration options for the maximum number of contact form submissions a user may perform per hour, and the default status of users\' personal contact forms.', array('@contact-settings' => url('admin/build/contact/settings'), '@contact' => url('contact'))) .'</p>';
$output .= '<p>'. t('For more information, see the online handbook entry for <a href="@contact">Contact module</a>.', array('@contact' => url('http://drupal.org/handbook/modules/contact/', array('absolute' => TRUE)))) .'</p>';
return $output;
case 'admin/build/contact':
$output = '<p>'. t('This page lets you set up <a href="@form">your site-wide contact form</a>. To do so, add one or more categories. You can associate different recipients with each category to route e-mails to different people. For example, you can route website feedback to the webmaster and direct product information requests to the sales department. On the <a href="@settings">settings page</a>, you can customize the information shown above the contact form. This can be useful to provide additional contact information such as your postal address and telephone number.', array('@settings' => url('admin/build/contact/settings'), '@form' => url('contact'))) .'</p>';
if (!module_exists('menu')) {
$menu_note = t('The menu item can be customized and configured only once the menu module has been <a href="@modules-page">enabled</a>.', array('@modules-page' => url('admin/settings/modules')));
}
else {
$menu_note = '';
}
$output .= '<p>'. t('The contact module also adds a <a href="@menu-settings">menu item</a> (disabled by default) to the navigation block.', array('@menu-settings' => url('admin/build/menu'))) .' '. $menu_note .'</p>';
return $output;
}
}
/**
* Implementation of hook_perm
*/
function contact_perm() {
return array('access site-wide contact form', 'administer site-wide contact form');
}
/**
* Implementation of hook_menu().
*/
function contact_menu() {
$items['admin/build/contact'] = array(
'title' => 'Contact form',
'description' => 'Create a system contact form and set up categories for the form to use.',
'page callback' => 'contact_admin_categories',
'access arguments' => array('administer site-wide contact form'),
'file' => 'contact.admin.inc',
);
$items['admin/build/contact/list'] = array(
'title' => 'List',
'page callback' => 'contact_admin_categories',
'type' => MENU_DEFAULT_LOCAL_TASK,
'file' => 'contact.admin.inc',
);
$items['admin/build/contact/add'] = array(
'title' => 'Add category',
'page callback' => 'drupal_get_form',
'page arguments' => array('contact_admin_edit', 3),
'access arguments' => array('administer site-wide contact form'),
'type' => MENU_LOCAL_TASK,
'weight' => 1,
'file' => 'contact.admin.inc',
);
$items['admin/build/contact/edit/%contact'] = array(
'title' => 'Edit contact category',
'page callback' => 'drupal_get_form',
'page arguments' => array('contact_admin_edit', 3, 4),
'access arguments' => array('administer site-wide contact form'),
'type' => MENU_CALLBACK,
'file' => 'contact.admin.inc',
);
$items['admin/build/contact/delete/%contact'] = array(
'title' => 'Delete contact',
'page callback' => 'drupal_get_form',
'page arguments' => array('contact_admin_delete', 4),
'access arguments' => array('administer site-wide contact form'),
'type' => MENU_CALLBACK,
'file' => 'contact.admin.inc',
);
$items['admin/build/contact/settings'] = array(
'title' => 'Settings',
'page callback' => 'drupal_get_form',
'page arguments' => array('contact_admin_settings'),
'access arguments' => array('administer site-wide contact form'),
'type' => MENU_LOCAL_TASK,
'weight' => 2,
'file' => 'contact.admin.inc',
);
$items['contact'] = array(
'title' => 'Contact',
'page callback' => 'contact_site_page',
'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' => 'contact_user_page',
'page arguments' => array(1),
'type' => MENU_LOCAL_TASK,
'access callback' => '_contact_user_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
* A user account object.
* @return
* TRUE if the current user has access to the requested user's contact form,
* or FALSE otherwise.
*/
function _contact_user_tab_access($account) {
global $user;
// Anonymous users cannot use or have contact forms.
if (!$user->uid || !$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->contact)) {
return FALSE;
}
return TRUE;
}
/**
* Load the data for a single contact category.
*/
function contact_load($cid) {
$contact = db_fetch_array(db_query("SELECT * FROM {contact} WHERE cid = %d", $cid));
return empty($contact) ? FALSE : $contact;
}
/**
* Implementation of hook_user().
*
* Allows the user the option of enabling/disabling his personal contact form.
*/
function contact_user($type, &$edit, &$user, $category = NULL) {
if ($type == 'form' && $category == 'account') {
$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($edit['contact']) ? $edit['contact'] : FALSE,
'#description' => t('Allow other users to contact you by e-mail via <a href="@url">your personal contact form</a>. Note that while your e-mail address is not made public to other members of the community, privileged users such as site administrators are able to contact you even if you choose not to enable this feature.', array('@url' => url("user/$user->uid/contact"))),
);
return $form;
}
elseif ($type == 'validate') {
return array('contact' => isset($edit['contact']) ? $edit['contact'] : FALSE);
}
elseif ($type == 'insert') {
$edit['contact'] = variable_get('contact_default_status', 1);
}
}
/**
* Implementation of hook_mail().
*/
function contact_mail($key, &$message, $params) {
$language = $message['language'];
switch ($key) {
case 'page_mail':
case 'page_copy':
$contact = $params['contact'];
$message['subject'] .= t('[!category] !subject', array('!category' => $contact['category'], '!subject' => $params['subject']), $language->language);
$message['body'][] = t("!name sent a message using the contact form at !form.", array('!name' => $params['name'], '!form' => url($_GET['q'], array('absolute' => TRUE, 'language' => $language))), $language->language);
$message['body'][] = $params['message'];
break;
case 'page_autoreply':
$contact = $params['contact'];
$message['subject'] .= t('[!category] !subject', array('!category' => $contact['category'], '!subject' => $params['subject']), $language->language);
$message['body'][] = $contact['reply'];
break;
case 'user_mail':
case 'user_copy':
$user = $params['user'];
$account = $params['account'];
$message['subject'] .= '['. variable_get('site_name', 'Drupal') .'] '. $params['subject'];
$message['body'][] = "$account->name,";
$message['body'][] = t("!name (!name-url) has sent you a message via your contact form (!form-url) at !site.", array('!name' => $user->name, '!name-url' => url("user/$user->uid", array('absolute' => TRUE, 'language' => $language)), '!form-url' => url($_GET['q'], array('absolute' => TRUE, 'language' => $language)), '!site' => variable_get('site_name', 'Drupal')), $language->language);
$message['body'][] = t("If you don't want to receive such e-mails, you can change your settings at !url.", array('!url' => url("user/$account->uid", array('absolute' => TRUE, 'language' => $language))), $language->language);
$message['body'][] = t('Message:', NULL, $language->language);
$message['body'][] = $params['message'];
break;
}
}

View file

@ -0,0 +1,235 @@
<?php
/**
* @file
* User page callbacks for the contact module.
*/
/**
* Site-wide contact page.
*/
function contact_site_page() {
global $user;
if (!flood_is_allowed('contact', variable_get('contact_hourly_threshold', 3))) {
$output = t("You cannot send more than %number messages per hour. Please try again later.", array('%number' => variable_get('contact_hourly_threshold', 3)));
}
else {
$output = drupal_get_form('contact_mail_page');
}
return $output;
}
function contact_mail_page() {
global $user;
$form = $categories = array();
$result = db_query('SELECT cid, category, selected FROM {contact} ORDER BY weight, category');
while ($category = db_fetch_object($result)) {
$categories[$category->cid] = $category->category;
if ($category->selected) {
$default_category = $category->cid;
}
}
if (count($categories) > 0) {
$form['#token'] = $user->uid ? $user->name . $user->mail : '';
$form['contact_information'] = array('#value' => filter_xss_admin(variable_get('contact_form_information', t('You can leave a message using the contact form below.'))));
$form['name'] = array('#type' => 'textfield',
'#title' => t('Your name'),
'#maxlength' => 255,
'#default_value' => $user->uid ? $user->name : '',
'#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,
);
if (count($categories) > 1) {
// If there is more than one category available and no default category has been selected,
// prepend a default placeholder value.
if (!isset($default_category)) {
$default_category = t('- Please choose -');
$categories = array($default_category) + $categories;
}
$form['cid'] = array('#type' => 'select',
'#title' => t('Category'),
'#default_value' => $default_category,
'#options' => $categories,
'#required' => TRUE,
);
}
else {
// If there is only one category, store its cid.
$category_keys = array_keys($categories);
$form['cid'] = array('#type' => 'value',
'#value' => array_shift($category_keys),
);
}
$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.
if ($user->uid) {
$form['copy'] = array('#type' => 'checkbox',
'#title' => t('Send yourself a copy.'),
);
}
else {
$form['copy'] = array('#type' => 'value', '#value' => FALSE);
}
$form['submit'] = array('#type' => 'submit',
'#value' => t('Send e-mail'),
);
}
else {
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/build/contact/add'))), 'error');
}
return $form;
}
/**
* Validate the site-wide contact page form submission.
*/
function contact_mail_page_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.'));
}
}
/**
* Process the site-wide contact page form submission.
*/
function contact_mail_page_submit($form, &$form_state) {
global $language;
$values = $form_state['values'];
// E-mail address of the sender: as the form field is a text field,
// all instances of \r and \n have been automatically stripped from it.
$from = $values['mail'];
// Load category properties and save form values for email composition.
$contact = contact_load($values['cid']);
$values['contact'] = $contact;
// Send the e-mail to the recipients using the site default language.
drupal_mail('contact', 'page_mail', $contact['recipients'], 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 ($contact['reply']) {
drupal_mail('contact', 'page_autoreply', $from, $language, $values, $contact['recipients']);
}
flood_register_event('contact');
watchdog('mail', '%name-from sent an e-mail regarding %category.', array('%name-from' => $values['name'] ." [$from]", '%category' => $contact['category']));
drupal_set_message(t('Your message has been sent.'));
// Jump to home page rather than back to contact page to avoid
// contradictory messages if flood control has been activated.
$form_state['redirect'] = '';
}
/**
* Personal contact page.
*/
function contact_user_page($account) {
global $user;
if (!valid_email_address($user->mail)) {
$output = t('You need to provide a valid e-mail address to contact other users. Please update your <a href="@url">user information</a> and try again.', array('@url' => url("user/$user->uid/edit")));
}
else if (!flood_is_allowed('contact', variable_get('contact_hourly_threshold', 3))) {
$output = t('You cannot contact more than %number users per hour. Please try again later.', array('%number' => variable_get('contact_hourly_threshold', 3)));
}
else {
drupal_set_title(check_plain($account->name));
$output = drupal_get_form('contact_mail_user', $account);
}
return $output;
}
function contact_mail_user(&$form_state, $recipient) {
global $user;
$form['#token'] = $user->name . $user->mail;
$form['recipient'] = array('#type' => 'value', '#value' => $recipient);
$form['from'] = array('#type' => 'item',
'#title' => t('From'),
'#value' => theme('username', $user) .' &lt;'. check_plain($user->mail) .'&gt;',
);
$form['to'] = array('#type' => 'item',
'#title' => t('To'),
'#value' => theme('username', $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,
);
$form['copy'] = array('#type' => 'checkbox',
'#title' => t('Send yourself a copy.'),
);
$form['submit'] = array('#type' => 'submit',
'#value' => t('Send e-mail'),
);
return $form;
}
/**
* Process the personal contact page form submission.
*/
function contact_mail_user_submit($form, &$form_state) {
global $user, $language;
$account = $form_state['values']['recipient'];
// Send from the current user to the requested user.
$to = $account->mail;
$from = $user->mail;
// Save both users and all form values for email composition.
$values = $form_state['values'];
$values['account'] = $account;
$values['user'] = $user;
// Send the e-mail in the requested user language.
drupal_mail('contact', 'user_mail', $to, user_preferred_language($account), $values, $from);
// Send a copy if requested, using current page language.
if ($form_state['values']['copy']) {
drupal_mail('contact', 'user_copy', $from, $language, $values, $from);
}
flood_register_event('contact');
watchdog('mail', '%name-from sent %name-to an e-mail.', array('%name-from' => $user->name, '%name-to' => $account->name));
drupal_set_message(t('The message has been sent.'));
// Back to the requested users profile page.
$form_state['redirect'] = "user/$account->uid";
}

View file

@ -0,0 +1,6 @@
#edit-type-wrapper, #edit-severity-wrapper {
float: right;
padding-right: 0;
padding-left: .8em;
}

View file

@ -0,0 +1,329 @@
<?php
/**
* @file
* Administrative page callbacks for the dblog module.
*/
/**
* dblog module settings form.
*
* @ingroup forms
* @see system_settings_form()
*/
function dblog_admin_settings() {
$form['dblog_row_limit'] = array(
'#type' => 'select',
'#title' => t('Discard log entries above the following row limit'),
'#default_value' => variable_get('dblog_row_limit', 1000),
'#options' => drupal_map_assoc(array(100, 1000, 10000, 100000, 1000000)),
'#description' => t('The maximum number of rows to keep in the database log. Older entries will be automatically discarded. (Requires a correctly configured <a href="@cron">cron maintenance task</a>.)', array('@cron' => url('admin/reports/status')))
);
return system_settings_form($form);
}
/**
* Menu callback; displays a listing of log messages.
*/
function dblog_overview() {
$filter = dblog_build_filter_query();
$rows = array();
$icons = array(
WATCHDOG_DEBUG => '',
WATCHDOG_INFO => '',
WATCHDOG_NOTICE => '',
WATCHDOG_WARNING => theme('image', 'misc/watchdog-warning.png', t('warning'), t('warning')),
WATCHDOG_ERROR => theme('image', 'misc/watchdog-error.png', t('error'), t('error')),
WATCHDOG_CRITICAL => theme('image', 'misc/watchdog-error.png', t('critical'), t('critical')),
WATCHDOG_ALERT => theme('image', 'misc/watchdog-error.png', t('alert'), t('alert')),
WATCHDOG_EMERG => theme('image', 'misc/watchdog-error.png', t('emergency'), t('emergency')),
);
$classes = array(
WATCHDOG_DEBUG => 'dblog-debug',
WATCHDOG_INFO => 'dblog-info',
WATCHDOG_NOTICE => 'dblog-notice',
WATCHDOG_WARNING => 'dblog-warning',
WATCHDOG_ERROR => 'dblog-error',
WATCHDOG_CRITICAL => 'dblog-critical',
WATCHDOG_ALERT => 'dblog-alert',
WATCHDOG_EMERG => 'dblog-emerg',
);
$output = drupal_get_form('dblog_filter_form');
$header = array(
' ',
array('data' => t('Type'), 'field' => 'w.type'),
array('data' => t('Date'), 'field' => 'w.wid', 'sort' => 'desc'),
t('Message'),
array('data' => t('User'), 'field' => 'u.name'),
array('data' => t('Operations')),
);
$sql = "SELECT w.wid, w.uid, w.severity, w.type, w.timestamp, w.message, w.variables, w.link, u.name FROM {watchdog} w INNER JOIN {users} u ON w.uid = u.uid";
$tablesort = tablesort_sql($header);
if (!empty($filter['where'])) {
$result = pager_query($sql ." WHERE ". $filter['where'] . $tablesort, 50, 0, NULL, $filter['args']);
}
else {
$result = pager_query($sql . $tablesort, 50);
}
while ($dblog = db_fetch_object($result)) {
$rows[] = array('data' =>
array(
// Cells
$icons[$dblog->severity],
t($dblog->type),
format_date($dblog->timestamp, 'small'),
l(truncate_utf8(_dblog_format_message($dblog), 56, TRUE, TRUE), 'admin/reports/event/'. $dblog->wid, array('html' => TRUE)),
theme('username', $dblog),
filter_xss($dblog->link),
),
// Attributes for tr
'class' => "dblog-". preg_replace('/[^a-z]/i', '-', $dblog->type) .' '. $classes[$dblog->severity]
);
}
if (!$rows) {
$rows[] = array(array('data' => t('No log messages available.'), 'colspan' => 6));
}
$output .= theme('table', $header, $rows, array('id' => 'admin-dblog'));
$output .= theme('pager', NULL, 50, 0);
return $output;
}
/**
* Menu callback; generic function to display a page of the most frequent
* dblog events of a specified type.
*/
function dblog_top($type) {
$header = array(
array('data' => t('Count'), 'field' => 'count', 'sort' => 'desc'),
array('data' => t('Message'), 'field' => 'message')
);
$result = pager_query("SELECT COUNT(wid) AS count, message, variables FROM {watchdog} WHERE type = '%s' GROUP BY message, variables ". tablesort_sql($header), 30, 0, "SELECT COUNT(DISTINCT(message)) FROM {watchdog} WHERE type = '%s'", $type);
$rows = array();
while ($dblog = db_fetch_object($result)) {
$rows[] = array($dblog->count, truncate_utf8(_dblog_format_message($dblog), 56, TRUE, TRUE));
}
if (empty($rows)) {
$rows[] = array(array('data' => t('No log messages available.'), 'colspan' => 2));
}
$output = theme('table', $header, $rows);
$output .= theme('pager', NULL, 30, 0);
return $output;
}
/**
* Menu callback; displays details about a log message.
*/
function dblog_event($id) {
$severity = watchdog_severity_levels();
$output = '';
$result = db_query('SELECT w.*, u.name, u.uid FROM {watchdog} w INNER JOIN {users} u ON w.uid = u.uid WHERE w.wid = %d', $id);
if ($dblog = db_fetch_object($result)) {
$rows = array(
array(
array('data' => t('Type'), 'header' => TRUE),
t($dblog->type),
),
array(
array('data' => t('Date'), 'header' => TRUE),
format_date($dblog->timestamp, 'large'),
),
array(
array('data' => t('User'), 'header' => TRUE),
theme('username', $dblog),
),
array(
array('data' => t('Location'), 'header' => TRUE),
l($dblog->location, $dblog->location),
),
array(
array('data' => t('Referrer'), 'header' => TRUE),
l($dblog->referer, $dblog->referer),
),
array(
array('data' => t('Message'), 'header' => TRUE),
_dblog_format_message($dblog),
),
array(
array('data' => t('Severity'), 'header' => TRUE),
$severity[$dblog->severity],
),
array(
array('data' => t('Hostname'), 'header' => TRUE),
check_plain($dblog->hostname),
),
array(
array('data' => t('Operations'), 'header' => TRUE),
$dblog->link,
),
);
$attributes = array('class' => 'dblog-event');
$output = theme('table', array(), $rows, $attributes);
}
return $output;
}
/**
* Build query for dblog administration filters based on session.
*/
function dblog_build_filter_query() {
if (empty($_SESSION['dblog_overview_filter'])) {
return;
}
$filters = dblog_filters();
// Build query
$where = $args = array();
foreach ($_SESSION['dblog_overview_filter'] as $key => $filter) {
$filter_where = array();
foreach ($filter as $value) {
$filter_where[] = $filters[$key]['where'];
$args[] = $value;
}
if (!empty($filter_where)) {
$where[] = '('. implode(' OR ', $filter_where) .')';
}
}
$where = !empty($where) ? implode(' AND ', $where) : '';
return array(
'where' => $where,
'args' => $args,
);
}
/**
* List dblog administration filters that can be applied.
*/
function dblog_filters() {
$filters = array();
foreach (_dblog_get_message_types() as $type) {
$types[$type] = t($type);
}
if (!empty($types)) {
$filters['type'] = array(
'title' => t('Type'),
'where' => "w.type = '%s'",
'options' => $types,
);
}
$filters['severity'] = array(
'title' => t('Severity'),
'where' => 'w.severity = %d',
'options' => watchdog_severity_levels(),
);
return $filters;
}
/**
* Formats a log message for display.
*
* @param $dblog
* An object with at least the message and variables properties
*/
function _dblog_format_message($dblog) {
// Legacy messages and user specified text
if ($dblog->variables === 'N;') {
return $dblog->message;
}
// Message to translate with injected variables
else {
return t($dblog->message, unserialize($dblog->variables));
}
}
/**
* Return form for dblog administration filters.
*
* @ingroup forms
* @see dblog_filter_form_submit()
* @see dblog_filter_form_validate()
*/
function dblog_filter_form() {
$session = &$_SESSION['dblog_overview_filter'];
$session = is_array($session) ? $session : array();
$filters = dblog_filters();
$form['filters'] = array(
'#type' => 'fieldset',
'#title' => t('Filter log messages'),
'#theme' => 'dblog_filters',
'#collapsible' => TRUE,
'#collapsed' => empty($session),
);
foreach ($filters as $key => $filter) {
$form['filters']['status'][$key] = array(
'#title' => $filter['title'],
'#type' => 'select',
'#multiple' => TRUE,
'#size' => 8,
'#options' => $filter['options'],
);
if (!empty($session[$key])) {
$form['filters']['status'][$key]['#default_value'] = $session[$key];
}
}
$form['filters']['buttons']['submit'] = array(
'#type' => 'submit',
'#value' => t('Filter'),
);
if (!empty($session)) {
$form['filters']['buttons']['reset'] = array(
'#type' => 'submit',
'#value' => t('Reset')
);
}
return $form;
}
/**
* Validate result from dblog administration filter form.
*/
function dblog_filter_form_validate($form, &$form_state) {
if ($form_state['values']['op'] == t('Filter') && empty($form_state['values']['type']) && empty($form_state['values']['severity'])) {
form_set_error('type', t('You must select something to filter by.'));
}
}
/**
* Process result from dblog administration filter form.
*/
function dblog_filter_form_submit($form, &$form_state) {
$op = $form_state['values']['op'];
$filters = dblog_filters();
switch ($op) {
case t('Filter'):
foreach ($filters as $name => $filter) {
if (isset($form_state['values'][$name])) {
$_SESSION['dblog_overview_filter'][$name] = $form_state['values'][$name];
}
}
break;
case t('Reset'):
$_SESSION['dblog_overview_filter'] = array();
break;
}
return 'admin/reports/dblog';
}

39
modules/dblog/dblog.css Normal file
View file

@ -0,0 +1,39 @@
#edit-type-wrapper, #edit-severity-wrapper {
float: left; /* LTR */
padding-right: .8em; /* LTR */
margin: 0.1em;
/**
* In Opera 9, DOM elements with the property of "overflow: auto"
* will partially hide its contents with unnecessary scrollbars when
* its immediate child is floated without an explicit width set.
*/
width: 15em;
}
#dblog-filter-form .form-item select.form-select {
width: 100%;
}
tr.dblog-user {
background: #ffd;
}
tr.dblog-user .active {
background: #eed;
}
tr.dblog-content {
background: #ddf;
}
tr.dblog-content .active {
background: #cce;
}
tr.dblog-page-not-found, tr.dblog-access-denied {
background: #dfd;
}
tr.dblog-page-not-found .active, tr.dblog-access-denied .active {
background: #cec;
}
tr.dblog-error {
background: #ffc9c9;
}
tr.dblog-error .active {
background: #eeb9b9;
}

11
modules/dblog/dblog.info Normal file
View file

@ -0,0 +1,11 @@
name = Database logging
description = Logs and records system events to the database.
package = Core - optional
version = VERSION
core = 6.x
; Information added by Drupal.org packaging script on 2016-02-24
version = "6.38"
project = "drupal"
datestamp = "1456343372"

119
modules/dblog/dblog.install Normal file
View file

@ -0,0 +1,119 @@
<?php
/**
* Implementation of hook_install().
*/
function dblog_install() {
// Create tables.
drupal_install_schema('dblog');
}
/**
* Implementation of hook_uninstall().
*/
function dblog_uninstall() {
// Remove tables.
drupal_uninstall_schema('dblog');
}
/**
* Implementation of hook_schema().
*/
function dblog_schema() {
$schema['watchdog'] = array(
'description' => 'Table that contains logs of all system events.',
'fields' => array(
'wid' => array(
'type' => 'serial',
'not null' => TRUE,
'description' => 'Primary Key: Unique watchdog event ID.',
),
'uid' => array(
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'description' => 'The {users}.uid of the user who triggered the event.',
),
'type' => array(
'type' => 'varchar',
'length' => 16,
'not null' => TRUE,
'default' => '',
'description' => 'Type of log message, for example "user" or "page not found."',
),
'message' => array(
'type' => 'text',
'not null' => TRUE,
'size' => 'big',
'description' => 'Text of log message to be passed into the t() function.',
),
'variables' => array(
'type' => 'text',
'not null' => TRUE,
'size' => 'big',
'description' => 'Serialized array of variables that match the message string and that is passed into the t() function.',
),
'severity' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
'size' => 'tiny',
'description' => 'The severity level of the event; ranges from 0 (Emergency) to 7 (Debug)',
),
'link' => array(
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
'default' => '',
'description' => 'Link to view the result of the event.',
),
'location' => array(
'type' => 'text',
'not null' => TRUE,
'description' => 'URL of the origin of the event.',
),
'referer' => array(
'type' => 'text',
'not null' => FALSE,
'description' => 'URL of referring page.',
),
'hostname' => array(
'type' => 'varchar',
'length' => 128,
'not null' => TRUE,
'default' => '',
'description' => 'Hostname of the user who triggered the event.',
),
'timestamp' => array(
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'description' => 'Unix timestamp of when event occurred.',
),
),
'primary key' => array('wid'),
'indexes' => array('type' => array('type')),
);
return $schema;
}
/**
* @addtogroup updates-6.x-extra
* @{
*/
/**
* Allow longer referrers.
*/
function dblog_update_6000() {
$ret = array();
db_change_field($ret, 'watchdog', 'referer', 'referer', array('type' => 'text', 'not null' => FALSE));
return $ret;
}
/**
* @} End of "addtogroup updates-6.x-extra".
* The next series of updates should start at 7000.
*/

166
modules/dblog/dblog.module Normal file
View file

@ -0,0 +1,166 @@
<?php
/**
* @file
* System monitoring and logging for administrators.
*
* The dblog module monitors your site and keeps a list of
* recorded events containing usage and performance data, errors,
* warnings, and similar operational information.
*
* @see watchdog()
*/
/**
* Implementation of hook_help().
*/
function dblog_help($path, $arg) {
switch ($path) {
case 'admin/help#dblog':
$output = '<p>'. t('The dblog module monitors your system, capturing system events in a log to be reviewed by an authorized individual at a later time. This is useful for site administrators who want a quick overview of activities on their site. The logs also record the sequence of events, so it can be useful for debugging site errors.') .'</p>';
$output .= '<p>'. t('The dblog log is simply a list of recorded events containing usage data, performance data, errors, warnings and operational information. Administrators should check the dblog report on a regular basis to ensure their site is working properly.') .'</p>';
$output .= '<p>'. t('For more information, see the online handbook entry for <a href="@dblog">Dblog module</a>.', array('@dblog' => 'http://drupal.org/handbook/modules/dblog/')) .'</p>';
return $output;
case 'admin/reports/dblog':
return '<p>'. t('The dblog module monitors your website, capturing system events in a log to be reviewed by an authorized individual at a later time. The dblog log is simply a list of recorded events containing usage data, performance data, errors, warnings and operational information. It is vital to check the dblog report on a regular basis as it is often the only way to tell what is going on.') .'</p>';
}
}
/**
* Implementation of hook_theme()
*/
function dblog_theme() {
return array(
'dblog_filters' => array(
'arguments' => array('form' => NULL),
),
);
}
/**
* Implementation of hook_menu().
*/
function dblog_menu() {
$items['admin/settings/logging/dblog'] = array(
'title' => 'Database logging',
'description' => 'Settings for logging to the Drupal database logs. This is the most common method for small to medium sites on shared hosting. The logs are viewable from the admin pages.',
'page callback' => 'drupal_get_form',
'page arguments' => array('dblog_admin_settings'),
'access arguments' => array('administer site configuration'),
'file' => 'dblog.admin.inc',
);
$items['admin/reports/dblog'] = array(
'title' => 'Recent log entries',
'description' => 'View events that have recently been logged.',
'page callback' => 'dblog_overview',
'access arguments' => array('access site reports'),
'weight' => -1,
'file' => 'dblog.admin.inc',
);
$items['admin/reports/page-not-found'] = array(
'title' => "Top 'page not found' errors",
'description' => "View 'page not found' errors (404s).",
'page callback' => 'dblog_top',
'page arguments' => array('page not found'),
'access arguments' => array('access site reports'),
'file' => 'dblog.admin.inc',
);
$items['admin/reports/access-denied'] = array(
'title' => "Top 'access denied' errors",
'description' => "View 'access denied' errors (403s).",
'page callback' => 'dblog_top',
'page arguments' => array('access denied'),
'access arguments' => array('access site reports'),
'file' => 'dblog.admin.inc',
);
$items['admin/reports/event/%'] = array(
'title' => 'Details',
'page callback' => 'dblog_event',
'page arguments' => array(3),
'access arguments' => array('access site reports'),
'type' => MENU_CALLBACK,
'file' => 'dblog.admin.inc',
);
return $items;
}
function dblog_init() {
if (arg(0) == 'admin' && arg(1) == 'reports') {
// Add the CSS for this module
drupal_add_css(drupal_get_path('module', 'dblog') .'/dblog.css', 'module', 'all', FALSE);
}
}
/**
* Implementation of hook_cron().
*
* Remove expired log messages.
*/
function dblog_cron() {
// Cleanup the watchdog table
$max = db_result(db_query('SELECT MAX(wid) FROM {watchdog}'));
db_query('DELETE FROM {watchdog} WHERE wid <= %d', $max - variable_get('dblog_row_limit', 1000));
}
/**
* Implementation of hook_user().
*/
function dblog_user($op, &$edit, &$user) {
if ($op == 'delete') {
db_query('UPDATE {watchdog} SET uid = 0 WHERE uid = %d', $user->uid);
}
}
function _dblog_get_message_types() {
$types = array();
$result = db_query('SELECT DISTINCT(type) FROM {watchdog} ORDER BY type');
while ($object = db_fetch_object($result)) {
$types[] = $object->type;
}
return $types;
}
/**
* Implementation of hook_watchdog().
*/
function dblog_watchdog($log = array()) {
$current_db = db_set_active();
db_query("INSERT INTO {watchdog}
(uid, type, message, variables, severity, link, location, referer, hostname, timestamp)
VALUES
(%d, '%s', '%s', '%s', %d, '%s', '%s', '%s', '%s', %d)",
$log['user']->uid,
$log['type'],
$log['message'],
serialize($log['variables']),
$log['severity'],
$log['link'],
$log['request_uri'],
$log['referer'],
$log['ip'],
$log['timestamp']);
if ($current_db) {
db_set_active($current_db);
}
}
/**
* Theme dblog administration filter selector.
*
* @ingroup themeable
*/
function theme_dblog_filters($form) {
$output = '';
foreach (element_children($form['status']) as $key) {
$output .= drupal_render($form['status'][$key]);
}
$output .= '<div id="dblog-admin-buttons">'. drupal_render($form['buttons']) .'</div>';
return $output;
}

View file

@ -0,0 +1,393 @@
<?php
/**
* @file
* Admin page callbacks for the filter module.
*/
/**
* Menu callback; Displays a list of all input formats and which
* one is the default.
*
* @ingroup forms
* @see filter_admin_overview_submit()
*/
function filter_admin_overview() {
// Overview of all formats.
$formats = filter_formats();
$error = FALSE;
foreach ($formats as $id => $format) {
$roles = array();
foreach (user_roles() as $rid => $name) {
// Prepare a roles array with roles that may access the filter.
if (strstr($format->roles, ",$rid,")) {
$roles[] = $name;
}
}
$default = ($id == variable_get('filter_default_format', 1));
$options[$id] = '';
$form[$format->name]['id'] = array('#value' => $id);
$form[$format->name]['roles'] = array('#value' => $default ? t('All roles may use default format') : ($roles ? implode(', ', $roles) : t('No roles may use this format')));
$form[$format->name]['configure'] = array('#value' => l(t('configure'), 'admin/settings/filters/'. $id));
$form[$format->name]['delete'] = array('#value' => $default ? '' : l(t('delete'), 'admin/settings/filters/delete/'. $id));
}
$form['default'] = array('#type' => 'radios', '#options' => $options, '#default_value' => variable_get('filter_default_format', 1));
$form['submit'] = array('#type' => 'submit', '#value' => t('Set default format'));
return $form;
}
function filter_admin_overview_submit($form, &$form_state) {
// Process form submission to set the default format.
if (is_numeric($form_state['values']['default'])) {
drupal_set_message(t('Default format updated.'));
variable_set('filter_default_format', $form_state['values']['default']);
}
}
/**
* Theme the admin overview form.
*
* @ingroup themeable
*/
function theme_filter_admin_overview($form) {
$rows = array();
foreach ($form as $name => $element) {
if (isset($element['roles']) && is_array($element['roles'])) {
$rows[] = array(
drupal_render($form['default'][$element['id']['#value']]),
check_plain($name),
drupal_render($element['roles']),
drupal_render($element['configure']),
drupal_render($element['delete'])
);
unset($form[$name]);
}
}
$header = array(t('Default'), t('Name'), t('Roles'), array('data' => t('Operations'), 'colspan' => 2));
$output = theme('table', $header, $rows);
$output .= drupal_render($form);
return $output;
}
/**
* Menu callback; Display a filter format form.
*/
function filter_admin_format_page($format = NULL) {
if (!isset($format->name)) {
drupal_set_title(t("Add input format"));
$format = (object)array('name' => '', 'roles' => '', 'format' => '');
}
return drupal_get_form('filter_admin_format_form', $format);
}
/**
* Generate a filter format form.
*
* @ingroup forms
* @see filter_admin_format_form_validate()
* @see filter_admin_format_form_submit()
*/
function filter_admin_format_form(&$form_state, $format) {
$default = ($format->format == variable_get('filter_default_format', 1));
if ($default) {
$help = t('All roles for the default format must be enabled and cannot be changed.');
$form['default_format'] = array('#type' => 'hidden', '#value' => 1);
}
$form['name'] = array('#type' => 'textfield',
'#title' => t('Name'),
'#default_value' => $format->name,
'#description' => t('Specify a unique name for this filter format.'),
'#required' => TRUE,
);
// Add a row of checkboxes for form group.
$form['roles'] = array('#type' => 'fieldset',
'#title' => t('Roles'),
'#description' => $default ? $help : t('Choose which roles may use this filter format. Note that roles with the "administer filters" permission can always use all the filter formats.'),
'#tree' => TRUE,
);
foreach (user_roles() as $rid => $name) {
$checked = strstr($format->roles, ",$rid,");
$form['roles'][$rid] = array('#type' => 'checkbox',
'#title' => $name,
'#default_value' => ($default || $checked),
);
if ($default) {
$form['roles'][$rid]['#disabled'] = TRUE;
}
}
// Table with filters
$all = filter_list_all();
$enabled = filter_list_format($format->format);
$form['filters'] = array('#type' => 'fieldset',
'#title' => t('Filters'),
'#description' => t('Choose the filters that will be used in this filter format.'),
'#tree' => TRUE,
);
foreach ($all as $id => $filter) {
$form['filters'][$id] = array('#type' => 'checkbox',
'#title' => $filter->name,
'#default_value' => isset($enabled[$id]),
'#description' => module_invoke($filter->module, 'filter', 'description', $filter->delta),
);
}
if (!empty($format->format)) {
$form['format'] = array('#type' => 'hidden', '#value' => $format->format);
// Composition tips (guidelines)
$tips = _filter_tips($format->format, FALSE);
$extra = '<p>'. l(t('More information about formatting options'), 'filter/tips') .'</p>';
$tiplist = theme('filter_tips', $tips, FALSE, $extra);
if (!$tiplist) {
$tiplist = '<p>'. t('No guidelines available.') .'</p>';
}
$group = '<p>'. t('These are the guidelines that users will see for posting in this input format. They are automatically generated from the filter settings.') .'</p>';
$group .= $tiplist;
$form['tips'] = array('#value' => '<h2>'. t('Formatting guidelines') .'</h2>'. $group);
}
$form['submit'] = array('#type' => 'submit', '#value' => t('Save configuration'));
return $form;
}
/**
* Validate filter format form submissions.
*/
function filter_admin_format_form_validate($form, &$form_state) {
if (!isset($form_state['values']['format'])) {
$name = trim($form_state['values']['name']);
$result = db_fetch_object(db_query("SELECT format FROM {filter_formats} WHERE name='%s'", $name));
if ($result) {
form_set_error('name', t('Filter format names need to be unique. A format named %name already exists.', array('%name' => $name)));
}
}
}
/**
* Process filter format form submissions.
*/
function filter_admin_format_form_submit($form, &$form_state) {
$format = isset($form_state['values']['format']) ? $form_state['values']['format'] : NULL;
$current = filter_list_format($format);
$name = trim($form_state['values']['name']);
$cache = TRUE;
// Add a new filter format.
if (!$format) {
$new = TRUE;
db_query("INSERT INTO {filter_formats} (name) VALUES ('%s')", $name);
$format = db_result(db_query("SELECT MAX(format) AS format FROM {filter_formats}"));
drupal_set_message(t('Added input format %format.', array('%format' => $name)));
}
else {
drupal_set_message(t('The input format settings have been updated.'));
}
db_query("DELETE FROM {filters} WHERE format = %d", $format);
foreach ($form_state['values']['filters'] as $id => $checked) {
if ($checked) {
list($module, $delta) = explode('/', $id);
// Add new filters to the bottom.
$weight = isset($current[$id]->weight) ? $current[$id]->weight : 10;
db_query("INSERT INTO {filters} (format, module, delta, weight) VALUES (%d, '%s', %d, %d)", $format, $module, $delta, $weight);
// Check if there are any 'no cache' filters.
$cache &= !module_invoke($module, 'filter', 'no cache', $delta);
}
}
// We store the roles as a string for ease of use.
// We should always set all roles to TRUE when saving a default role.
// We use leading and trailing comma's to allow easy substring matching.
$roles = array();
if (isset($form_state['values']['roles'])) {
foreach ($form_state['values']['roles'] as $id => $checked) {
if ($checked) {
$roles[] = $id;
}
}
}
if (!empty($form_state['values']['default_format'])) {
$roles = ','. implode(',', array_keys(user_roles())) .',';
}
else {
$roles = ','. implode(',', $roles) .',';
}
db_query("UPDATE {filter_formats} SET cache = %d, name='%s', roles = '%s' WHERE format = %d", $cache, $name, $roles, $format);
cache_clear_all($format .':', 'cache_filter', TRUE);
// If a new filter was added, return to the main list of filters. Otherwise, stay on edit filter page to show new changes.
$return = 'admin/settings/filters';
if (!empty($new)) {
$return .= '/'. $format;
}
$form_state['redirect'] = $return;
return;
}
/**
* Menu callback; confirm deletion of a format.
*
* @ingroup forms
* @see filter_admin_delete_submit()
*/
function filter_admin_delete() {
$format = arg(4);
$format = db_fetch_object(db_query('SELECT * FROM {filter_formats} WHERE format = %d', $format));
if ($format) {
if ($format->format != variable_get('filter_default_format', 1)) {
$form['format'] = array('#type' => 'hidden', '#value' => $format->format);
$form['name'] = array('#type' => 'hidden', '#value' => $format->name);
return confirm_form($form, t('Are you sure you want to delete the input format %format?', array('%format' => $format->name)), 'admin/settings/filters', t('If you have any content left in this input format, it will be switched to the default input format. This action cannot be undone.'), t('Delete'), t('Cancel'));
}
else {
drupal_set_message(t('The default format cannot be deleted.'));
drupal_goto('admin/settings/filters');
}
}
else {
drupal_not_found();
}
}
/**
* Process filter delete form submission.
*/
function filter_admin_delete_submit($form, &$form_state) {
db_query("DELETE FROM {filter_formats} WHERE format = %d", $form_state['values']['format']);
db_query("DELETE FROM {filters} WHERE format = %d", $form_state['values']['format']);
$default = variable_get('filter_default_format', 1);
// Replace existing instances of the deleted format with the default format.
db_query("UPDATE {node_revisions} SET format = %d WHERE format = %d", $default, $form_state['values']['format']);
db_query("UPDATE {comments} SET format = %d WHERE format = %d", $default, $form_state['values']['format']);
db_query("UPDATE {boxes} SET format = %d WHERE format = %d", $default, $form_state['values']['format']);
cache_clear_all($form_state['values']['format'] .':', 'cache_filter', TRUE);
drupal_set_message(t('Deleted input format %format.', array('%format' => $form_state['values']['name'])));
$form_state['redirect'] = 'admin/settings/filters';
return;
}
/**
* Menu callback; display settings defined by a format's filters.
*/
function filter_admin_configure_page($format) {
drupal_set_title(t("Configure %format", array('%format' => $format->name)));
return drupal_get_form('filter_admin_configure', $format);
}
/**
* Build a form to change the settings for a format's filters.
*
* @ingroup forms
*/
function filter_admin_configure(&$form_state, $format) {
$list = filter_list_format($format->format);
$form = array();
foreach ($list as $filter) {
$form_module = module_invoke($filter->module, 'filter', 'settings', $filter->delta, $format->format);
if (isset($form_module) && is_array($form_module)) {
$form = array_merge($form, $form_module);
}
}
if (!empty($form)) {
$form = system_settings_form($form);
}
else {
$form['error'] = array('#value' => t('No settings are available.'));
}
$form['format'] = array('#type' => 'hidden', '#value' => $format->format);
$form['#submit'][] = 'filter_admin_configure_submit';
return $form;
}
/**
* Clear the filter's cache when configuration settings are saved.
*/
function filter_admin_configure_submit($form, &$form_state) {
cache_clear_all($form_state['values']['format'] .':', 'cache_filter', TRUE);
}
/**
* Menu callback; display form for ordering filters for a format.
*/
function filter_admin_order_page($format) {
drupal_set_title(t("Rearrange %format", array('%format' => $format->name)));
return drupal_get_form('filter_admin_order', $format);
}
/**
* Build the form for ordering filters for a format.
*
* @ingroup forms
* @see theme_filter_admin_order()
* @see filter_admin_order_submit()
*/
function filter_admin_order(&$form_state, $format = NULL) {
// Get list (with forced refresh).
$filters = filter_list_format($format->format);
$form['weights'] = array('#tree' => TRUE);
foreach ($filters as $id => $filter) {
$form['names'][$id] = array('#value' => $filter->name);
$form['weights'][$id] = array('#type' => 'weight', '#default_value' => $filter->weight);
}
$form['format'] = array('#type' => 'hidden', '#value' => $format->format);
$form['submit'] = array('#type' => 'submit', '#value' => t('Save configuration'));
return $form;
}
/**
* Theme filter order configuration form.
*
* @ingroup themeable
*/
function theme_filter_admin_order($form) {
$header = array(t('Name'), t('Weight'));
$rows = array();
foreach (element_children($form['names']) as $id) {
// Don't take form control structures.
if (is_array($form['names'][$id])) {
$form['weights'][$id]['#attributes']['class'] = 'filter-order-weight';
$rows[] = array(
'data' => array(drupal_render($form['names'][$id]), drupal_render($form['weights'][$id])),
'class' => 'draggable',
);
}
}
$output = theme('table', $header, $rows, array('id' => 'filter-order'));
$output .= drupal_render($form);
drupal_add_tabledrag('filter-order', 'order', 'sibling', 'filter-order-weight', NULL, NULL, FALSE);
return $output;
}
/**
* Process filter order configuration form submission.
*/
function filter_admin_order_submit($form, &$form_state) {
foreach ($form_state['values']['weights'] as $id => $weight) {
list($module, $delta) = explode('/', $id);
db_query("UPDATE {filters} SET weight = %d WHERE format = %d AND module = '%s' AND delta = %d", $weight, $form_state['values']['format'], $module, $delta);
}
drupal_set_message(t('The filter ordering has been saved.'));
cache_clear_all($form_state['values']['format'] .':', 'cache_filter', TRUE);
}

View file

@ -0,0 +1,11 @@
name = Filter
description = Handles the filtering of content in preparation for display.
package = Core - required
version = VERSION
core = 6.x
; Information added by Drupal.org packaging script on 2016-02-24
version = "6.38"
project = "drupal"
datestamp = "1456343372"

View file

@ -0,0 +1,90 @@
<?php
/**
* Implementation of hook_schema().
*/
function filter_schema() {
$schema['filters'] = array(
'description' => 'Table that maps filters (HTML corrector) to input formats (Filtered HTML).',
'fields' => array(
'fid' => array(
'type' => 'serial',
'not null' => TRUE,
'description' => 'Primary Key: Auto-incrementing filter ID.',
),
'format' => array(
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'description' => 'Foreign key: The {filter_formats}.format to which this filter is assigned.',
),
'module' => array(
'type' => 'varchar',
'length' => 64,
'not null' => TRUE,
'default' => '',
'description' => 'The origin module of the filter.',
),
'delta' => array(
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'size' => 'tiny',
'description' => 'ID to identify which filter within module is being referenced.',
),
'weight' => array(
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'size' => 'tiny',
'description' => 'Weight of filter within format.',
)
),
'primary key' => array('fid'),
'unique keys' => array(
'fmd' => array('format', 'module', 'delta'),
),
'indexes' => array(
'list' => array('format', 'weight', 'module', 'delta'),
),
);
$schema['filter_formats'] = array(
'description' => 'Stores input formats: custom groupings of filters, such as Filtered HTML.',
'fields' => array(
'format' => array(
'type' => 'serial',
'not null' => TRUE,
'description' => 'Primary Key: Unique ID for format.',
),
'name' => array(
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
'default' => '',
'description' => 'Name of the input format (Filtered HTML).',
),
'roles' => array(
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
'default' => '',
'description' => 'A comma-separated string of roles; references {role}.rid.', // This is bad since you can't use joins, nor index.
),
'cache' => array(
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'size' => 'tiny',
'description' => 'Flag to indicate whether format is cachable. (1 = cachable, 0 = not cachable)',
),
),
'primary key' => array('format'),
'unique keys' => array('name' => array('name')),
);
$schema['cache_filter'] = drupal_get_schema_unprocessed('system', 'cache');
$schema['cache_filter']['description'] = 'Cache table for the Filter module to store already filtered pieces of text, identified by input format and md5 hash of the text.';
return $schema;
}

1208
modules/filter/filter.module Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,65 @@
<?php
/**
* @file
* User page callbacks for the filter module.
*/
/**
* Menu callback; show a page with long filter tips.
*/
function filter_tips_long() {
$format = arg(2);
if ($format) {
$output = theme('filter_tips', _filter_tips($format, TRUE), TRUE);
}
else {
$output = theme('filter_tips', _filter_tips(-1, TRUE), TRUE);
}
return $output;
}
/**
* Format a set of filter tips.
*
* @ingroup themeable
*/
function theme_filter_tips($tips, $long = FALSE, $extra = '') {
$output = '';
$multiple = count($tips) > 1;
if ($multiple) {
$output = t('input formats') .':';
}
if (count($tips)) {
if ($multiple) {
$output .= '<ul>';
}
foreach ($tips as $name => $tiplist) {
if ($multiple) {
$output .= '<li>';
$output .= '<strong>'. $name .'</strong>:<br />';
}
if (count($tiplist) > 0) {
$output .= '<ul class="tips">';
foreach ($tiplist as $tip) {
$output .= '<li'. ($long ? ' id="filter-'. str_replace("/", "-", $tip['id']) .'">' : '>') . $tip['tip'] .'</li>';
}
$output .= '</ul>';
}
if ($multiple) {
$output .= '</li>';
}
}
if ($multiple) {
$output .= '</ul>';
}
}
return $output;
}

View file

@ -0,0 +1,24 @@
<?php
/**
* @file forum-icon.tpl.php
* Display an appropriate icon for a forum post.
*
* Available variables:
* - $new_posts: Indicates whether or not the topic contains new posts.
* - $icon: The icon to display. May be one of 'hot', 'hot-new', 'new',
* 'default', 'closed', or 'sticky'.
*
* @see template_preprocess_forum_icon()
* @see theme_forum_icon()
*/
?>
<?php if ($new_posts): ?>
<a name="new">
<?php endif; ?>
<?php print theme('image', "misc/forum-$icon.png") ?>
<?php if ($new_posts): ?>
</a>
<?php endif; ?>

View file

@ -0,0 +1,71 @@
<?php
/**
* @file forum-list.tpl.php
* Default theme implementation to display a list of forums and containers.
*
* Available variables:
* - $forums: An array of forums and containers to display. It is keyed to the
* numeric id's of all child forums and containers.
* - $forum_id: Forum id for the current forum. Parent to all items within
* the $forums array.
*
* Each $forum in $forums contains:
* - $forum->is_container: Is TRUE if the forum can contain other forums. Is
* FALSE if the forum can contain only topics.
* - $forum->depth: How deep the forum is in the current hierarchy.
* - $forum->zebra: 'even' or 'odd' string used for row class.
* - $forum->name: The name of the forum.
* - $forum->link: The URL to link to this forum.
* - $forum->description: The description of this forum.
* - $forum->new_topics: True if the forum contains unread posts.
* - $forum->new_url: A URL to the forum's unread posts.
* - $forum->new_text: Text for the above URL which tells how many new posts.
* - $forum->old_topics: A count of posts that have already been read.
* - $forum->num_posts: The total number of posts in the forum.
* - $forum->last_reply: Text representing the last time a forum was posted or
* commented in.
*
* @see template_preprocess_forum_list()
* @see theme_forum_list()
*/
?>
<table id="forum-<?php print $forum_id; ?>">
<thead>
<tr>
<th><?php print t('Forum'); ?></th>
<th><?php print t('Topics');?></th>
<th><?php print t('Posts'); ?></th>
<th><?php print t('Last post'); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($forums as $child_id => $forum): ?>
<tr id="forum-list-<?php print $child_id; ?>" class="<?php print $forum->zebra; ?>">
<td <?php print $forum->is_container ? 'colspan="4" class="container"' : 'class="forum"'; ?>>
<?php /* Enclose the contents of this cell with X divs, where X is the
* depth this forum resides at. This will allow us to use CSS
* left-margin for indenting.
*/ ?>
<?php print str_repeat('<div class="indent">', $forum->depth); ?>
<div class="name"><a href="<?php print $forum->link; ?>"><?php print $forum->name; ?></a></div>
<?php if ($forum->description): ?>
<div class="description"><?php print $forum->description; ?></div>
<?php endif; ?>
<?php print str_repeat('</div>', $forum->depth); ?>
</td>
<?php if (!$forum->is_container): ?>
<td class="topics">
<?php print $forum->num_topics ?>
<?php if ($forum->new_topics): ?>
<br />
<a href="<?php print $forum->new_url; ?>"><?php print $forum->new_text; ?></a>
<?php endif; ?>
</td>
<td class="posts"><?php print $forum->num_posts ?></td>
<td class="last-reply"><?php print $forum->last_reply ?></td>
<?php endif; ?>
</tr>
<?php endforeach; ?>
</tbody>
</table>

View file

@ -0,0 +1,17 @@
#forum tr td.forum {
padding-left: 0.5em;
padding-right: 25px;
background-position: 98% 2px;
}
.forum-topic-navigation {
padding: 1em 3em 0 0;
}
.forum-topic-navigation .topic-previous {
text-align: left;
float: right;
}
.forum-topic-navigation .topic-next {
text-align: right;
float: left;
}

View file

@ -0,0 +1,27 @@
<?php
/**
* @file forum-submitted.tpl.php
* Default theme implementation to format a simple string indicated when and
* by whom a topic was submitted.
*
* Available variables:
*
* - $author: The author of the post.
* - $time: How long ago the post was created.
* - $topic: An object with the raw data of the post. Unsafe, be sure
* to clean this data before printing.
*
* @see template_preprocess_forum_submitted()
* @see theme_forum_submitted()
*/
?>
<?php if ($time): ?>
<?php print t(
'@time ago<br />by !author', array(
'@time' => $time,
'!author' => $author,
)); ?>
<?php else: ?>
<?php print t('n/a'); ?>
<?php endif; ?>

View file

@ -0,0 +1,61 @@
<?php
/**
* @file forum-topic-list.tpl.php
* Theme implementation to display a list of forum topics.
*
* Available variables:
* - $header: The table header. This is pre-generated with click-sorting
* information. If you need to change this, @see template_preprocess_forum_topic_list().
* - $pager: The pager to display beneath the table.
* - $topics: An array of topics to be displayed.
* - $topic_id: Numeric id for the current forum topic.
*
* Each $topic in $topics contains:
* - $topic->icon: The icon to display.
* - $topic->moved: A flag to indicate whether the topic has been moved to
* another forum.
* - $topic->title: The title of the topic. Safe to output.
* - $topic->message: If the topic has been moved, this contains an
* explanation and a link.
* - $topic->zebra: 'even' or 'odd' string used for row class.
* - $topic->num_comments: The number of replies on this topic.
* - $topic->new_replies: A flag to indicate whether there are unread comments.
* - $topic->new_url: If there are unread replies, this is a link to them.
* - $topic->new_text: Text containing the translated, properly pluralized count.
* - $topic->created: An outputtable string represented when the topic was posted.
* - $topic->last_reply: An outputtable string representing when the topic was
* last replied to.
* - $topic->timestamp: The raw timestamp this topic was posted.
*
* @see template_preprocess_forum_topic_list()
* @see theme_forum_topic_list()
*/
?>
<table id="forum-topic-<?php print $topic_id; ?>">
<thead>
<tr><?php print $header; ?></tr>
</thead>
<tbody>
<?php foreach ($topics as $topic): ?>
<tr class="<?php print $topic->zebra;?>">
<td class="icon"><?php print $topic->icon; ?></td>
<td class="title"><?php print $topic->title; ?></td>
<?php if ($topic->moved): ?>
<td colspan="3"><?php print $topic->message; ?></td>
<?php else: ?>
<td class="replies">
<?php print $topic->num_comments; ?>
<?php if ($topic->new_replies): ?>
<br />
<a href="<?php print $topic->new_url; ?>"><?php print $topic->new_text; ?></a>
<?php endif; ?>
</td>
<td class="created"><?php print $topic->created; ?></td>
<td class="last-reply"><?php print $topic->last_reply; ?></td>
<?php endif; ?>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php print $pager; ?>

View file

@ -0,0 +1,34 @@
<?php
/**
* @file forum-topic-navigation.tpl.php
* Default theme implementation to display the topic navigation string at the
* bottom of all forum topics.
*
* Available variables:
*
* - $prev: The node ID of the previous post.
* - $prev_url: The URL of the previous post.
* - $prev_title: The title of the previous post.
*
* - $next: The node ID of the next post.
* - $next_url: The URL of the next post.
* - $next_title: The title of the next post.
*
* - $node: The raw node currently being viewed. Contains unsafe data
* and any data in this must be cleaned before presenting.
*
* @see template_preprocess_forum_topic_navigation()
* @see theme_forum_topic_navigation()
*/
?>
<?php if ($prev || $next): ?>
<div class="forum-topic-navigation clear-block">
<?php if ($prev): ?>
<a href="<?php print $prev_url; ?>" class="topic-previous" title="<?php print t('Go to previous forum topic') ?>"> <?php print $prev_title ?></a>
<?php endif; ?>
<?php if ($next): ?>
<a href="<?php print $next_url; ?>" class="topic-next" title="<?php print t('Go to next forum topic') ?>"><?php print $next_title ?> </a>
<?php endif; ?>
</div>
<?php endif; ?>

View file

@ -0,0 +1,291 @@
<?php
/**
* @file
* Administrative page callbacks for the forum module.
*/
function forum_form_main($type, $edit = array()) {
if ((isset($_POST['op']) && $_POST['op'] == t('Delete')) || !empty($_POST['confirm'])) {
return drupal_get_form('forum_confirm_delete', $edit['tid']);
}
switch ($type) {
case 'forum':
return drupal_get_form('forum_form_forum', $edit);
break;
case 'container':
return drupal_get_form('forum_form_container', $edit);
break;
}
}
/**
* Returns a form for adding a forum to the forum vocabulary
*
* @param $edit Associative array containing a forum term to be added or edited.
* @ingroup forms
* @see forum_form_submit()
*/
function forum_form_forum(&$form_state, $edit = array()) {
$edit += array(
'name' => '',
'description' => '',
'tid' => NULL,
'weight' => 0,
);
$form['name'] = array('#type' => 'textfield',
'#title' => t('Forum name'),
'#default_value' => $edit['name'],
'#maxlength' => 255,
'#description' => t('Short but meaningful name for this collection of threaded discussions.'),
'#required' => TRUE,
);
$form['description'] = array('#type' => 'textarea',
'#title' => t('Description'),
'#default_value' => $edit['description'],
'#description' => t('Description and guidelines for discussions within this forum.'),
);
$form['parent']['#tree'] = TRUE;
$form['parent'][0] = _forum_parent_select($edit['tid'], t('Parent'), 'forum');
$form['weight'] = array('#type' => 'weight',
'#title' => t('Weight'),
'#default_value' => $edit['weight'],
'#description' => t('Forums are displayed in ascending order by weight (forums with equal weights are displayed alphabetically).'),
);
$form['vid'] = array('#type' => 'hidden', '#value' => variable_get('forum_nav_vocabulary', ''));
$form['submit' ] = array('#type' => 'submit', '#value' => t('Save'));
if ($edit['tid']) {
$form['delete'] = array('#type' => 'submit', '#value' => t('Delete'));
$form['tid'] = array('#type' => 'hidden', '#value' => $edit['tid']);
}
$form['#submit'][] = 'forum_form_submit';
$form['#theme'] = 'forum_form';
return $form;
}
/**
* Process forum form and container form submissions.
*/
function forum_form_submit($form, &$form_state) {
if ($form['form_id']['#value'] == 'forum_form_container') {
$container = TRUE;
$type = t('forum container');
}
else {
$container = FALSE;
$type = t('forum');
}
$status = taxonomy_save_term($form_state['values']);
switch ($status) {
case SAVED_NEW:
if ($container) {
$containers = variable_get('forum_containers', array());
$containers[] = $form_state['values']['tid'];
variable_set('forum_containers', $containers);
}
drupal_set_message(t('Created new @type %term.', array('%term' => $form_state['values']['name'], '@type' => $type)));
break;
case SAVED_UPDATED:
drupal_set_message(t('The @type %term has been updated.', array('%term' => $form_state['values']['name'], '@type' => $type)));
break;
}
$form_state['redirect'] = 'admin/content/forum';
return;
}
/**
* Returns a form for adding a container to the forum vocabulary
*
* @param $edit Associative array containing a container term to be added or edited.
* @ingroup forms
* @see forum_form_submit()
*/
function forum_form_container(&$form_state, $edit = array()) {
$edit += array(
'name' => '',
'description' => '',
'tid' => NULL,
'weight' => 0,
);
// Handle a delete operation.
$form['name'] = array(
'#title' => t('Container name'),
'#type' => 'textfield',
'#default_value' => $edit['name'],
'#maxlength' => 255,
'#description' => t('Short but meaningful name for this collection of related forums.'),
'#required' => TRUE
);
$form['description'] = array(
'#type' => 'textarea',
'#title' => t('Description'),
'#default_value' => $edit['description'],
'#description' => t('Description and guidelines for forums within this container.')
);
$form['parent']['#tree'] = TRUE;
$form['parent'][0] = _forum_parent_select($edit['tid'], t('Parent'), 'container');
$form['weight'] = array(
'#type' => 'weight',
'#title' => t('Weight'),
'#default_value' => $edit['weight'],
'#description' => t('Containers are displayed in ascending order by weight (containers with equal weights are displayed alphabetically).')
);
$form['vid'] = array(
'#type' => 'hidden',
'#value' => variable_get('forum_nav_vocabulary', ''),
);
$form['submit'] = array(
'#type' => 'submit',
'#value' => t('Save')
);
if ($edit['tid']) {
$form['delete'] = array('#type' => 'submit', '#value' => t('Delete'));
$form['tid'] = array('#type' => 'value', '#value' => $edit['tid']);
}
$form['#submit'][] = 'forum_form_submit';
$form['#theme'] = 'forum_form';
return $form;
}
/**
* Returns a confirmation page for deleting a forum taxonomy term.
*
* @param $tid ID of the term to be deleted
*/
function forum_confirm_delete(&$form_state, $tid) {
$term = taxonomy_get_term($tid);
$form['tid'] = array('#type' => 'value', '#value' => $tid);
$form['name'] = array('#type' => 'value', '#value' => $term->name);
return confirm_form($form, t('Are you sure you want to delete the forum %name?', array('%name' => $term->name)), 'admin/content/forum', t('Deleting a forum or container will also delete its sub-forums, if any. To delete posts in this forum, visit <a href="@content">content administration</a> first. This action cannot be undone.', array('@content' => url('admin/content/node'))), t('Delete'), t('Cancel'));
}
/**
* Implementation of forms api _submit call. Deletes a forum after confirmation.
*/
function forum_confirm_delete_submit($form, &$form_state) {
taxonomy_del_term($form_state['values']['tid']);
drupal_set_message(t('The forum %term and all sub-forums and associated posts have been deleted.', array('%term' => $form_state['values']['name'])));
watchdog('content', 'forum: deleted %term and all its sub-forums and associated posts.', array('%term' => $form_state['values']['name']));
$form_state['redirect'] = 'admin/content/forum';
return;
}
/**
* Form builder for the forum settings page.
*
* @see system_settings_form()
*/
function forum_admin_settings() {
$form = array();
$number = drupal_map_assoc(array(5, 10, 15, 20, 25, 30, 35, 40, 50, 60, 80, 100, 150, 200, 250, 300, 350, 400, 500));
$form['forum_hot_topic'] = array('#type' => 'select',
'#title' => t('Hot topic threshold'),
'#default_value' => variable_get('forum_hot_topic', 15),
'#options' => $number,
'#description' => t('The number of posts a topic must have to be considered "hot".'),
);
$number = drupal_map_assoc(array(10, 25, 50, 75, 100));
$form['forum_per_page'] = array('#type' => 'select',
'#title' => t('Topics per page'),
'#default_value' => variable_get('forum_per_page', 25),
'#options' => $number,
'#description' => t('Default number of forum topics displayed per page.'),
);
$forder = array(1 => t('Date - newest first'), 2 => t('Date - oldest first'), 3 => t('Posts - most active first'), 4 => t('Posts - least active first'));
$form['forum_order'] = array('#type' => 'radios',
'#title' => t('Default order'),
'#default_value' => variable_get('forum_order', '1'),
'#options' => $forder,
'#description' => t('Default display order for topics.'),
);
return system_settings_form($form);
}
/**
* Returns an overview list of existing forums and containers
*/
function forum_overview(&$form_state) {
module_load_include('inc', 'taxonomy', 'taxonomy.admin');
$vid = variable_get('forum_nav_vocabulary', '');
$vocabulary = taxonomy_vocabulary_load($vid);
$form = taxonomy_overview_terms($form_state, $vocabulary);
foreach (element_children($form) as $key) {
if (isset($form[$key]['#term'])) {
$term = $form[$key]['#term'];
$form[$key]['view']['#value'] = l($term['name'], 'forum/'. $term['tid']);
if (in_array($form[$key]['#term']['tid'], variable_get('forum_containers', array()))) {
$form[$key]['edit']['#value'] = l(t('edit container'), 'admin/content/forum/edit/container/'. $term['tid']);
}
else {
$form[$key]['edit']['#value'] = l(t('edit forum'), 'admin/content/forum/edit/forum/'. $term['tid']);
}
}
}
// Remove the alphabetical reset.
unset($form['reset_alphabetical']);
// The form needs to have submit and validate handlers set explicitly.
$form['#theme'] = 'taxonomy_overview_terms';
$form['#submit'] = array('taxonomy_overview_terms_submit'); // Use the existing taxonomy overview submit handler.
$form['#validate'] = array('taxonomy_overview_terms_validate');
$form['#empty_text'] = '<em>'. t('There are no existing containers or forums. Containers and forums may be added using the <a href="@container">add container</a> and <a href="@forum">add forum</a> pages.', array('@container' => url('admin/content/forum/add/container'), '@forum' => url('admin/content/forum/add/forum'))) .'</em>';
return $form;
}
/**
* Returns a select box for available parent terms
*
* @param $tid ID of the term which is being added or edited
* @param $title Title to display the select box with
* @param $child_type Whether the child is forum or container
*/
function _forum_parent_select($tid, $title, $child_type) {
$parents = taxonomy_get_parents($tid);
if ($parents) {
$parent = array_shift($parents);
$parent = $parent->tid;
}
else {
$parent = 0;
}
$vid = variable_get('forum_nav_vocabulary', '');
$children = taxonomy_get_tree($vid, $tid);
// A term can't be the child of itself, nor of its children.
foreach ($children as $child) {
$exclude[] = $child->tid;
}
$exclude[] = $tid;
$tree = taxonomy_get_tree($vid);
$options[0] = '<'. t('root') .'>';
if ($tree) {
foreach ($tree as $term) {
if (!in_array($term->tid, $exclude)) {
$options[$term->tid] = str_repeat(' -- ', $term->depth) . $term->name;
}
}
}
if ($child_type == 'container') {
$description = t('Containers are usually placed at the top (root) level, but may also be placed inside another container or forum.');
}
else if ($child_type == 'forum') {
$description = t('Forums may be placed at the top (root) level, or inside another container or forum.');
}
return array('#type' => 'select', '#title' => $title, '#default_value' => $parent, '#options' => $options, '#description' => $description, '#required' => TRUE);
}

41
modules/forum/forum.css Normal file
View file

@ -0,0 +1,41 @@
#forum .description {
font-size: 0.9em;
margin: 0.5em;
}
#forum td.created, #forum td.posts, #forum td.topics, #forum td.last-reply, #forum td.replies, #forum td.pager {
white-space: nowrap;
}
#forum td.posts, #forum td.topics, #forum td.replies, #forum td.pager {
text-align: center;
}
#forum tr td.forum {
padding-left: 25px; /* LTR */
background-position: 2px 2px; /* LTR */
background-image: url(../../misc/forum-default.png);
background-repeat: no-repeat;
}
#forum tr.new-topics td.forum {
background-image: url(../../misc/forum-new.png);
}
#forum div.indent {
margin-left: 20px;
}
.forum-topic-navigation {
padding: 1em 0 0 3em; /* LTR */
border-top: 1px solid #888;
border-bottom: 1px solid #888;
text-align: center;
padding: 0.5em;
}
.forum-topic-navigation .topic-previous {
text-align: right; /* LTR */
float: left; /* LTR */
width: 46%;
}
.forum-topic-navigation .topic-next {
text-align: left; /* LTR */
float: right; /* LTR */
width: 46%;
}

13
modules/forum/forum.info Normal file
View file

@ -0,0 +1,13 @@
name = Forum
description = Enables threaded discussions about general topics.
dependencies[] = taxonomy
dependencies[] = comment
package = Core - optional
version = VERSION
core = 6.x
; Information added by Drupal.org packaging script on 2016-02-24
version = "6.38"
project = "drupal"
datestamp = "1456343372"

129
modules/forum/forum.install Normal file
View file

@ -0,0 +1,129 @@
<?php
/**
* Implementation of hook_install().
*/
function forum_install() {
// Create tables.
drupal_install_schema('forum');
// Set the weight of the forum.module to 1 so it is loaded after the taxonomy.module.
db_query("UPDATE {system} SET weight = 1 WHERE name = 'forum'");
}
function forum_enable() {
if ($vocabulary = taxonomy_vocabulary_load(variable_get('forum_nav_vocabulary', 0))) {
// Existing install. Add back forum node type, if the forums
// vocabulary still exists. Keep all other node types intact there.
$vocabulary = (array) $vocabulary;
$vocabulary['nodes']['forum'] = 1;
taxonomy_save_vocabulary($vocabulary);
}
else {
// Create the forum vocabulary if it does not exist. Assign the vocabulary
// a low weight so it will appear first in forum topic create and edit
// forms.
$vocabulary = array(
'name' => t('Forums'),
'multiple' => 0,
'required' => 0,
'hierarchy' => 1,
'relations' => 0,
'module' => 'forum',
'weight' => -10,
'nodes' => array('forum' => 1),
);
taxonomy_save_vocabulary($vocabulary);
variable_set('forum_nav_vocabulary', $vocabulary['vid']);
}
}
/**
* Implementation of hook_uninstall().
*/
function forum_uninstall() {
// Load the dependent Taxonomy module, in case it has been disabled.
drupal_load('module', 'taxonomy');
// Delete the vocabulary.
$vid = variable_get('forum_nav_vocabulary', '');
taxonomy_del_vocabulary($vid);
db_query('DROP TABLE {forum}');
variable_del('forum_containers');
variable_del('forum_nav_vocabulary');
variable_del('forum_hot_topic');
variable_del('forum_per_page');
variable_del('forum_order');
variable_del('forum_block_num_0');
variable_del('forum_block_num_1');
}
/**
* Implementation of hook_schema().
*/
function forum_schema() {
$schema['forum'] = array(
'description' => 'Stores the relationship of nodes to forum terms.',
'fields' => array(
'nid' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
'description' => 'The {node}.nid of the node.',
),
'vid' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
'description' => 'Primary Key: The {node}.vid of the node.',
),
'tid' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
'description' => 'The {term_data}.tid of the forum term assigned to the node.',
),
),
'indexes' => array(
'nid' => array('nid'),
'tid' => array('tid')
),
'primary key' => array('vid'),
);
return $schema;
}
/**
* Create the forum vocabulary if does not exist. Assign the
* vocabulary a low weight so it will appear first in forum topic
* create and edit forms. Do not just call forum_enable() because in
* future versions it might do something different.
*/
function forum_update_6000() {
$ret = array();
$vid = variable_get('forum_nav_vocabulary', 0);
$vocabularies = taxonomy_get_vocabularies();
if (!isset($vocabularies[$vid])) {
$vocabulary = array(
'name' => t('Forums'),
'multiple' => 0,
'required' => 0,
'hierarchy' => 1,
'relations' => 0,
'module' => 'forum',
'weight' => -10,
'nodes' => array('forum' => 1),
);
taxonomy_save_vocabulary($vocabulary);
variable_set('forum_nav_vocabulary', $vocabulary['vid']);
}
return $ret;
}

970
modules/forum/forum.module Normal file
View file

@ -0,0 +1,970 @@
<?php
/**
* @file
* Enable threaded discussions about general topics.
*/
/**
* Implementation of hook_help().
*/
function forum_help($path, $arg) {
switch ($path) {
case 'admin/help#forum':
$output = '<p>'. t('The forum module lets you create threaded discussion forums with functionality similar to other message board systems. Forums are useful because they allow community members to discuss topics with one another while ensuring those conversations are archived for later reference. The <a href="@create-topic">forum topic</a> menu item (under <em>Create content</em> on the Navigation menu) creates the initial post of a new threaded discussion, or thread.', array('@create-topic' => url('node/add/forum'))) .'</p>';
$output .= '<p>'. t('A threaded discussion occurs as people leave comments on a forum topic (or on other comments within that topic). A forum topic is contained within a forum, which may hold many similar or related forum topics. Forums are (optionally) nested within a container, which may hold many similar or related forums. Both containers and forums may be nested within other containers and forums, and provide structure for your message board. By carefully planning this structure, you make it easier for users to find and comment on a specific forum topic.') .'</p>';
$output .= '<p>'. t('When administering a forum, note that:') .'</p>';
$output .= '<ul><li>'. t('a forum topic (and all of its comments) may be moved between forums by selecting a different forum while editing a forum topic.') .'</li>';
$output .= '<li>'. t('when moving a forum topic between forums, the <em>Leave shadow copy</em> option creates a link in the original forum pointing to the new location.') .'</li>';
$output .= '<li>'. t('selecting <em>Read only</em> under <em>Comment settings</em> while editing a forum topic will lock (prevent new comments) on the thread.') .'</li>';
$output .= '<li>'. t('selecting <em>Disabled</em> under <em>Comment settings</em> while editing a forum topic will hide all existing comments on the thread, and prevent new ones.') .'</li></ul>';
$output .= '<p>'. t('For more information, see the online handbook entry for <a href="@forum">Forum module</a>.', array('@forum' => 'http://drupal.org/handbook/modules/forum/')) .'</p>';
return $output;
case 'admin/content/forum':
return '<p>'. t('This page displays a list of existing forums and containers. Containers (optionally) hold forums, and forums hold forum topics (a forum topic is the initial post to a threaded discussion). To provide structure, both containers and forums may be placed inside other containers and forums. To rearrange forums and containers, grab a drag-and-drop handle under the <em>Name</em> column and drag the forum or container to a new location in the list. (Grab a handle by clicking and holding the mouse while hovering over a handle icon.) Remember that your changes will not be saved until you click the <em>Save</em> button at the bottom of the page.') .'</p>';
case 'admin/content/forum/add/container':
return '<p>'. t('By grouping related or similar forums, containers help organize forums. For example, a container named "Food" may hold two forums named "Fruit" and "Vegetables", respectively.') .'</p>';
case 'admin/content/forum/add/forum':
return '<p>'. t('A forum holds related or similar forum topics (a forum topic is the initial post to a threaded discussion). For example, a forum named "Fruit" may contain forum topics titled "Apples" and "Bananas", respectively.') .'</p>';
case 'admin/content/forum/settings':
return '<p>'. t('These settings allow you to adjust the display of your forum topics. The content types available for use within a forum may be selected by editing the <em>Content types</em> on the <a href="@forum-vocabulary">forum vocabulary page</a>.', array('@forum-vocabulary' => url('admin/content/taxonomy/edit/vocabulary/'. variable_get('forum_nav_vocabulary', '')))) .'</p>';
}
}
/**
* Implementation of hook_theme()
*/
function forum_theme() {
return array(
'forums' => array(
'template' => 'forums',
'arguments' => array('forums' => NULL, 'topics' => NULL, 'parents' => NULL, 'tid' => NULL, 'sortby' => NULL, 'forum_per_page' => NULL),
),
'forum_list' => array(
'template' => 'forum-list',
'arguments' => array('forums' => NULL, 'parents' => NULL, 'tid' => NULL),
),
'forum_topic_list' => array(
'template' => 'forum-topic-list',
'arguments' => array('tid' => NULL, 'topics' => NULL, 'sortby' => NULL, 'forum_per_page' => NULL),
),
'forum_icon' => array(
'template' => 'forum-icon',
'arguments' => array('new_posts' => NULL, 'num_posts' => 0, 'comment_mode' => 0, 'sticky' => 0),
),
'forum_topic_navigation' => array(
'template' => 'forum-topic-navigation',
'arguments' => array('node' => NULL),
),
'forum_submitted' => array(
'template' => 'forum-submitted',
'arguments' => array('topic' => NULL),
),
);
}
/**
* Fetch a forum term.
*
* @param $tid
* The ID of the term which should be loaded.
*
* @return
* An associative array containing the term data or FALSE if the term cannot be loaded, or is not part of the forum vocabulary.
*/
function forum_term_load($tid) {
$result = db_query(db_rewrite_sql('SELECT t.tid, t.vid, t.name, t.description, t.weight FROM {term_data} t WHERE t.tid = %d AND t.vid = %d', 't', 'tid'), $tid, variable_get('forum_nav_vocabulary', ''));
return db_fetch_array($result);
}
/**
* Implementation of hook_menu().
*/
function forum_menu() {
$items['forum'] = array(
'title' => 'Forums',
'page callback' => 'forum_page',
'access arguments' => array('access content'),
'type' => MENU_SUGGESTED_ITEM,
'file' => 'forum.pages.inc',
);
$items['admin/content/forum'] = array(
'title' => 'Forums',
'description' => 'Control forums and their hierarchy and change forum settings.',
'page callback' => 'drupal_get_form',
'page arguments' => array('forum_overview'),
'access arguments' => array('administer forums'),
'file' => 'forum.admin.inc',
);
$items['admin/content/forum/list'] = array(
'title' => 'List',
'type' => MENU_DEFAULT_LOCAL_TASK,
'weight' => -10,
);
$items['admin/content/forum/add/container'] = array(
'title' => 'Add container',
'page callback' => 'forum_form_main',
'page arguments' => array('container'),
'access arguments' => array('administer forums'),
'type' => MENU_LOCAL_TASK,
'parent' => 'admin/content/forum',
'file' => 'forum.admin.inc',
);
$items['admin/content/forum/add/forum'] = array(
'title' => 'Add forum',
'page callback' => 'forum_form_main',
'page arguments' => array('forum'),
'access arguments' => array('administer forums'),
'type' => MENU_LOCAL_TASK,
'parent' => 'admin/content/forum',
'file' => 'forum.admin.inc',
);
$items['admin/content/forum/settings'] = array(
'title' => 'Settings',
'page callback' => 'drupal_get_form',
'page arguments' => array('forum_admin_settings'),
'access arguments' => array('administer forums'),
'weight' => 5,
'type' => MENU_LOCAL_TASK,
'parent' => 'admin/content/forum',
'file' => 'forum.admin.inc',
);
$items['admin/content/forum/edit/%forum_term'] = array(
'page callback' => 'forum_form_main',
'access arguments' => array('administer forums'),
'type' => MENU_CALLBACK,
'file' => 'forum.admin.inc',
);
$items['admin/content/forum/edit/container/%forum_term'] = array(
'title' => 'Edit container',
'page callback' => 'forum_form_main',
'page arguments' => array('container', 5),
'access arguments' => array('administer forums'),
'type' => MENU_CALLBACK,
'file' => 'forum.admin.inc',
);
$items['admin/content/forum/edit/forum/%forum_term'] = array(
'title' => 'Edit forum',
'page callback' => 'forum_form_main',
'page arguments' => array('forum', 5),
'access arguments' => array('administer forums'),
'type' => MENU_CALLBACK,
'file' => 'forum.admin.inc',
);
return $items;
}
/**
* Implementation of hook_init().
*/
function forum_init() {
drupal_add_css(drupal_get_path('module', 'forum') .'/forum.css');
}
/**
* Implementation of hook_nodeapi().
*/
function forum_nodeapi(&$node, $op, $teaser = NULL, $page = NULL) {
// We are going to return if $node->type is not one of the node
// types assigned to the forum vocabulary. If forum_nav_vocabulary
// is undefined or the vocabulary does not exist, it clearly cannot
// be assigned to $node->type, so return to avoid E_ALL warnings.
$vid = variable_get('forum_nav_vocabulary', '');
$vocabulary = taxonomy_vocabulary_load($vid);
if (empty($vocabulary)) {
return;
}
// Operate only on node types assigned for the forum vocabulary.
if (!in_array($node->type, $vocabulary->nodes)) {
return;
}
switch ($op) {
case 'view':
if ($page && taxonomy_node_get_terms_by_vocabulary($node, $vid) && $tree = taxonomy_get_tree($vid)) {
// Get the forum terms from the (cached) tree
foreach ($tree as $term) {
$forum_terms[] = $term->tid;
}
foreach ($node->taxonomy as $term_id => $term) {
if (in_array($term_id, $forum_terms)) {
$node->tid = $term_id;
}
}
// Breadcrumb navigation
$breadcrumb[] = l(t('Home'), NULL);
$breadcrumb[] = l($vocabulary->name, 'forum');
if ($parents = taxonomy_get_parents_all($node->tid)) {
$parents = array_reverse($parents);
foreach ($parents as $p) {
$breadcrumb[] = l($p->name, 'forum/'. $p->tid);
}
}
drupal_set_breadcrumb($breadcrumb);
if (!$teaser) {
$node->content['forum_navigation'] = array(
'#value' => theme('forum_topic_navigation', $node),
'#weight' => 100,
);
}
}
break;
case 'prepare':
if (empty($node->nid)) {
// New topic
$node->taxonomy[arg(3)]->vid = $vid;
$node->taxonomy[arg(3)]->tid = arg(3);
}
break;
// Check in particular that only a "leaf" term in the associated taxonomy
// vocabulary is selected, not a "container" term.
case 'validate':
if ($node->taxonomy) {
// Extract the node's proper topic ID.
$vocabulary = $vid;
$containers = variable_get('forum_containers', array());
foreach ($node->taxonomy as $term) {
if (db_result(db_query('SELECT COUNT(*) FROM {term_data} WHERE tid = %d AND vid = %d', $term, $vocabulary))) {
if (in_array($term, $containers)) {
$term = taxonomy_get_term($term);
form_set_error('taxonomy', t('The item %forum is only a container for forums. Please select one of the forums below it.', array('%forum' => $term->name)));
}
}
}
}
break;
// Assign forum taxonomy when adding a topic from within a forum.
case 'presave':
// Make sure all fields are set properly:
$node->icon = !empty($node->icon) ? $node->icon : '';
if ($node->taxonomy && $tree = taxonomy_get_tree($vid)) {
// Get the forum terms from the (cached) tree if we have a taxonomy.
foreach ($tree as $term) {
$forum_terms[] = $term->tid;
}
foreach ($node->taxonomy as $term_id) {
if (in_array($term_id, $forum_terms)) {
$node->tid = $term_id;
}
}
$old_tid = db_result(db_query_range("SELECT f.tid FROM {forum} f INNER JOIN {node} n ON f.vid = n.vid WHERE n.nid = %d ORDER BY f.vid DESC", $node->nid, 0, 1));
if ($old_tid && isset($node->tid) && ($node->tid != $old_tid) && !empty($node->shadow)) {
// A shadow copy needs to be created. Retain new term and add old term.
$node->taxonomy[] = $old_tid;
}
}
break;
case 'update':
if (empty($node->revision) && db_result(db_query('SELECT tid FROM {forum} WHERE nid=%d', $node->nid))) {
if (!empty($node->tid)) {
db_query('UPDATE {forum} SET tid = %d WHERE vid = %d', $node->tid, $node->vid);
}
// The node is removed from the forum.
else {
db_query('DELETE FROM {forum} WHERE nid = %d', $node->nid);
}
break;
}
// Deliberate no break -- for new revisions and for previously unassigned terms we need an insert.
case 'insert':
if (!empty($node->tid)) {
db_query('INSERT INTO {forum} (tid, vid, nid) VALUES (%d, %d, %d)', $node->tid, $node->vid, $node->nid);
}
break;
case 'delete':
db_query('DELETE FROM {forum} WHERE nid = %d', $node->nid);
break;
case 'load':
return db_fetch_array(db_query('SELECT tid AS forum_tid FROM {forum} WHERE vid = %d', $node->vid));
}
return;
}
/**
* Implementation of hook_node_info().
*/
function forum_node_info() {
return array(
'forum' => array(
'name' => t('Forum topic'),
'module' => 'forum',
'description' => t('A <em>forum topic</em> is the initial post to a new discussion thread within a forum.'),
'title_label' => t('Subject'),
)
);
}
/**
* Implementation of hook_access().
*/
function forum_access($op, $node, $account) {
switch ($op) {
case 'create':
return user_access('create forum topics', $account) ? TRUE : NULL;
case 'update':
return user_access('edit any forum topic', $account) || (user_access('edit own forum topics', $account) && ($account->uid == $node->uid)) ? TRUE : NULL;
case 'delete':
return user_access('delete any forum topic', $account) || (user_access('delete own forum topics', $account) && ($account->uid == $node->uid)) ? TRUE : NULL;
}
}
/**
* Implementation of hook_perm().
*/
function forum_perm() {
return array('create forum topics', 'delete own forum topics', 'delete any forum topic', 'edit own forum topics', 'edit any forum topic', 'administer forums');
}
/**
* Implementation of hook_taxonomy().
*/
function forum_taxonomy($op, $type, $term = NULL) {
if ($op == 'delete' && $term['vid'] == variable_get('forum_nav_vocabulary', '')) {
switch ($type) {
case 'term':
$results = db_query('SELECT tn.nid FROM {term_node} tn WHERE tn.tid = %d', $term['tid']);
while ($node = db_fetch_object($results)) {
// node_delete will also remove any association with non-forum vocabularies.
node_delete($node->nid);
}
// For containers, remove the tid from the forum_containers variable.
$containers = variable_get('forum_containers', array());
$key = array_search($term['tid'], $containers);
if ($key !== FALSE) {
unset($containers[$key]);
}
variable_set('forum_containers', $containers);
break;
case 'vocabulary':
variable_del('forum_nav_vocabulary');
}
}
}
/**
* Implementation of hook_form_alter().
*/
function forum_form_alter(&$form, $form_state, $form_id) {
$vid = variable_get('forum_nav_vocabulary', '');
if (isset($form['vid']) && $form['vid']['#value'] == $vid) {
// Hide critical options from forum vocabulary.
if ($form_id == 'taxonomy_form_vocabulary') {
$form['help_forum_vocab'] = array(
'#value' => t('This is the designated forum vocabulary. Some of the normal vocabulary options have been removed.'),
'#weight' => -1,
);
$form['content_types']['nodes']['#required'] = TRUE;
$form['hierarchy'] = array('#type' => 'value', '#value' => 1);
$form['settings']['required'] = array('#type' => 'value', '#value' => FALSE);
$form['settings']['relations'] = array('#type' => 'value', '#value' => FALSE);
$form['settings']['tags'] = array('#type' => 'value', '#value' => FALSE);
$form['settings']['multiple'] = array('#type' => 'value', '#value' => FALSE);
unset($form['delete']);
}
// Hide multiple parents select from forum terms.
elseif ($form_id == 'taxonomy_form_term') {
$form['advanced']['parent']['#access'] = FALSE;
}
}
if ($form_id == 'forum_node_form') {
// Make the vocabulary required for 'real' forum-nodes.
$vid = variable_get('forum_nav_vocabulary', '');
$form['taxonomy'][$vid]['#required'] = TRUE;
$form['taxonomy'][$vid]['#options'][''] = t('- Please choose -');
}
}
/**
* Implementation of hook_load().
*/
function forum_load($node) {
$forum = db_fetch_object(db_query('SELECT * FROM {forum} WHERE vid = %d', $node->vid));
return $forum;
}
/**
* Implementation of hook_block().
*
* Generates a block containing the currently active forum topics and the
* most recently added forum topics.
*/
function forum_block($op = 'list', $delta = 0, $edit = array()) {
switch ($op) {
case 'list':
$blocks[0]['info'] = t('Active forum topics');
$blocks[1]['info'] = t('New forum topics');
return $blocks;
case 'configure':
$form['forum_block_num_'. $delta] = array('#type' => 'select', '#title' => t('Number of topics'), '#default_value' => variable_get('forum_block_num_'. $delta, '5'), '#options' => drupal_map_assoc(array(2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)));
return $form;
case 'save':
variable_set('forum_block_num_'. $delta, $edit['forum_block_num_'. $delta]);
break;
case 'view':
if (user_access('access content')) {
switch ($delta) {
case 0:
$title = t('Active forum topics');
$sql = db_rewrite_sql("SELECT n.nid, n.title, l.comment_count, l.last_comment_timestamp FROM {node} n INNER JOIN {term_node} tn ON tn.vid = n.vid INNER JOIN {term_data} td ON td.tid = tn.tid INNER JOIN {node_comment_statistics} l ON n.nid = l.nid WHERE n.status = 1 AND td.vid = %d ORDER BY l.last_comment_timestamp DESC");
$result = db_query_range($sql, variable_get('forum_nav_vocabulary', ''), 0, variable_get('forum_block_num_0', '5'));
$content = node_title_list($result);
break;
case 1:
$title = t('New forum topics');
$sql = db_rewrite_sql("SELECT n.nid, n.title, l.comment_count FROM {node} n INNER JOIN {term_node} tn ON tn.vid = n.vid INNER JOIN {term_data} td ON td.tid = tn.tid INNER JOIN {node_comment_statistics} l ON n.nid = l.nid WHERE n.status = 1 AND td.vid = %d ORDER BY n.nid DESC");
$result = db_query_range($sql, variable_get('forum_nav_vocabulary', ''), 0, variable_get('forum_block_num_1', '5'));
$content = node_title_list($result);
break;
}
if (!empty($content)) {
$block['subject'] = $title;
$block['content'] = $content . theme('more_link', url('forum'), t('Read the latest forum topics.'));
return $block;
}
}
}
}
/**
* Implementation of hook_form().
*/
function forum_form(&$node, $form_state) {
$type = node_get_types('type', $node);
$form['title'] = array('#type' => 'textfield', '#title' => check_plain($type->title_label), '#default_value' => !empty($node->title) ? $node->title : '', '#required' => TRUE, '#weight' => -5);
if (!empty($node->nid)) {
$vid = variable_get('forum_nav_vocabulary', '');
$forum_terms = taxonomy_node_get_terms_by_vocabulary($node, $vid);
// if editing, give option to leave shadows
$shadow = (count($forum_terms) > 1);
$form['shadow'] = array('#type' => 'checkbox', '#title' => t('Leave shadow copy'), '#default_value' => $shadow, '#description' => t('If you move this topic, you can leave a link in the old forum to the new forum.'));
}
$form['body_field'] = node_body_field($node, $type->body_label, 1);
$form['#submit'][] = 'forum_submit';
// Assign the forum topic submit handler.
return $form;
}
/**
* Implementation of hook_term_path().
*/
function forum_term_path($term) {
return 'forum/'. $term->tid;
}
/**
* Returns a list of all forums for a given taxonomy id
*
* Forum objects contain the following fields
* -num_topics Number of topics in the forum
* -num_posts Total number of posts in all topics
* -last_post Most recent post for the forum
*
* @param $tid
* Taxonomy ID of the vocabulary that holds the forum list.
* @return
* Array of object containing the forum information.
*/
function forum_get_forums($tid = 0) {
$forums = array();
$vid = variable_get('forum_nav_vocabulary', '');
$_forums = taxonomy_get_tree($vid, $tid);
if (count($_forums)) {
$counts = array();
$sql = "
SELECT r.tid AS tid, n.nid AS nid, l.comment_count AS nid_comment_count
FROM {node} n
INNER JOIN {node_comment_statistics} l ON n.nid = l.nid
INNER JOIN {term_node} r ON n.vid = r.vid
WHERE n.status = 1
GROUP BY r.tid, n.nid, l.comment_count";
$sql_rewritten = db_rewrite_sql($sql);
if ($sql_rewritten == $sql) {
$sql = "
SELECT r.tid, COUNT(n.nid) AS topic_count, SUM(l.comment_count) AS comment_count
FROM {node} n
INNER JOIN {node_comment_statistics} l ON n.nid = l.nid
INNER JOIN {term_node} r ON n.vid = r.vid
WHERE n.status = 1
GROUP BY r.tid";
$sql = db_rewrite_sql($sql);
}
else {
$sql = "
SELECT tid, COUNT(nid) AS topic_count, SUM(nid_comment_count) AS comment_count
FROM ($sql_rewritten) AS forum_content_list
GROUP BY tid";
}
$_counts = db_query($sql);
while ($count = db_fetch_object($_counts)) {
$counts[$count->tid] = $count;
}
}
foreach ($_forums as $forum) {
if (in_array($forum->tid, variable_get('forum_containers', array()))) {
$forum->container = 1;
}
if (!empty($counts[$forum->tid])) {
$forum->num_topics = $counts[$forum->tid]->topic_count;
$forum->num_posts = $counts[$forum->tid]->topic_count + $counts[$forum->tid]->comment_count;
}
else {
$forum->num_topics = 0;
$forum->num_posts = 0;
}
// This query does not use full ANSI syntax since MySQL 3.x does not support
// table1 INNER JOIN table2 INNER JOIN table3 ON table2_criteria ON table3_criteria
// used to join node_comment_statistics to users.
$sql = "SELECT ncs.last_comment_timestamp, IF (ncs.last_comment_uid != 0, u2.name, ncs.last_comment_name) AS last_comment_name, ncs.last_comment_uid FROM {node} n INNER JOIN {users} u1 ON n.uid = u1.uid INNER JOIN {term_node} tn ON n.vid = tn.vid INNER JOIN {node_comment_statistics} ncs ON n.nid = ncs.nid INNER JOIN {users} u2 ON ncs.last_comment_uid=u2.uid WHERE n.status = 1 AND tn.tid = %d ORDER BY ncs.last_comment_timestamp DESC";
$sql = db_rewrite_sql($sql);
$topic = db_fetch_object(db_query_range($sql, $forum->tid, 0, 1));
$last_post = new stdClass();
if (!empty($topic->last_comment_timestamp)) {
$last_post->timestamp = $topic->last_comment_timestamp;
$last_post->name = $topic->last_comment_name;
$last_post->uid = $topic->last_comment_uid;
}
$forum->last_post = $last_post;
$forums[$forum->tid] = $forum;
}
return $forums;
}
/**
* Calculate the number of nodes the user has not yet read and are newer
* than NODE_NEW_LIMIT.
*/
function _forum_topics_unread($term, $uid) {
$sql = "SELECT COUNT(DISTINCT n.nid) FROM {node} n INNER JOIN {term_node} tn ON n.vid = tn.vid AND tn.tid = %d LEFT JOIN {history} h ON n.nid = h.nid AND h.uid = %d WHERE n.status = 1 AND n.created > %d AND h.nid IS NULL";
$sql = db_rewrite_sql($sql);
return db_result(db_query($sql, $term, $uid, NODE_NEW_LIMIT));
}
function forum_get_topics($tid, $sortby, $forum_per_page) {
global $user, $forum_topic_list_header;
$forum_topic_list_header = array(
NULL,
array('data' => t('Topic'), 'field' => 'n.title'),
array('data' => t('Replies'), 'field' => 'l.comment_count'),
array('data' => t('Created'), 'field' => 'n.created'),
array('data' => t('Last reply'), 'field' => 'l.last_comment_timestamp'),
);
$order = _forum_get_topic_order($sortby);
for ($i = 0; $i < count($forum_topic_list_header); $i++) {
if ($forum_topic_list_header[$i]['field'] == $order['field']) {
$forum_topic_list_header[$i]['sort'] = $order['sort'];
}
}
$term = taxonomy_get_term($tid);
$sql = db_rewrite_sql("SELECT n.nid, r.tid, n.title, n.type, n.sticky, u.name, u.uid, n.created AS timestamp, n.comment AS comment_mode, l.last_comment_timestamp, IF(l.last_comment_uid != 0, cu.name, l.last_comment_name) AS last_comment_name, l.last_comment_uid, l.comment_count AS num_comments, f.tid AS forum_tid FROM {node_comment_statistics} l INNER JOIN {node} n ON n.nid = l.nid INNER JOIN {users} cu ON l.last_comment_uid = cu.uid INNER JOIN {term_node} r ON n.vid = r.vid INNER JOIN {users} u ON n.uid = u.uid INNER JOIN {forum} f ON n.vid = f.vid WHERE n.status = 1 AND r.tid = %d");
$sql .= tablesort_sql($forum_topic_list_header, 'n.sticky DESC,');
$sql .= ', n.created DESC'; // Always add a secondary sort order so that the news forum topics are on top.
$sql_count = db_rewrite_sql("SELECT COUNT(DISTINCT n.nid) FROM {node} n INNER JOIN {term_node} r ON n.vid = r.vid AND r.tid = %d WHERE n.status = 1");
$result = pager_query($sql, $forum_per_page, 0, $sql_count, $tid);
$topics = array();
while ($topic = db_fetch_object($result)) {
if ($user->uid) {
// folder is new if topic is new or there are new comments since last visit
if ($topic->tid != $tid) {
$topic->new = 0;
}
else {
$history = _forum_user_last_visit($topic->nid);
$topic->new_replies = comment_num_new($topic->nid, $history);
$topic->new = $topic->new_replies || ($topic->timestamp > $history);
}
}
else {
// Do not track "new replies" status for topics if the user is anonymous.
$topic->new_replies = 0;
$topic->new = 0;
}
if ($topic->num_comments > 0) {
$last_reply = new stdClass();
$last_reply->timestamp = $topic->last_comment_timestamp;
$last_reply->name = $topic->last_comment_name;
$last_reply->uid = $topic->last_comment_uid;
$topic->last_reply = $last_reply;
}
$topics[] = $topic;
}
return $topics;
}
/**
* Finds the first unread node for a given forum.
*/
function _forum_new($tid) {
global $user;
$sql = "SELECT n.nid FROM {node} n LEFT JOIN {history} h ON n.nid = h.nid AND h.uid = %d INNER JOIN {term_node} r ON n.nid = r.nid AND r.tid = %d WHERE n.status = 1 AND h.nid IS NULL AND n.created > %d ORDER BY created";
$sql = db_rewrite_sql($sql);
$nid = db_result(db_query_range($sql, $user->uid, $tid, NODE_NEW_LIMIT, 0, 1));
return $nid ? $nid : 0;
}
/**
* Process variables for forums.tpl.php
*
* The $variables array contains the following arguments:
* - $forums
* - $topics
* - $parents
* - $tid
* - $sortby
* - $forum_per_page
*
* @see forums.tpl.php
*/
function template_preprocess_forums(&$variables) {
global $user;
$vid = variable_get('forum_nav_vocabulary', '');
$vocabulary = taxonomy_vocabulary_load($vid);
$title = !empty($vocabulary->name) ? $vocabulary->name : '';
// Breadcrumb navigation:
$breadcrumb[] = l(t('Home'), NULL);
if ($variables['tid']) {
$breadcrumb[] = l($vocabulary->name, 'forum');
}
if ($variables['parents']) {
$variables['parents'] = array_reverse($variables['parents']);
foreach ($variables['parents'] as $p) {
if ($p->tid == $variables['tid']) {
$title = $p->name;
}
else {
$breadcrumb[] = l($p->name, 'forum/'. $p->tid);
}
}
}
drupal_set_breadcrumb($breadcrumb);
drupal_set_title(check_plain($title));
if ($variables['forums_defined'] = count($variables['forums']) || count($variables['parents'])) {
// Format the "post new content" links listing.
$forum_types = array();
// Loop through all node types for forum vocabulary.
foreach ($vocabulary->nodes as $type) {
// Check if the current user has the 'create' permission for this node type.
if (node_access('create', $type)) {
// Fetch the "General" name of the content type;
// Push the link with title and URL to the array.
$forum_types[$type] = array('title' => t('Post new @node_type', array('@node_type' => node_get_types('name', $type))), 'href' => 'node/add/'. str_replace('_', '-', $type) .'/'. $variables['tid']);
}
}
if (empty($forum_types)) {
// The user is logged-in; but denied access to create any new forum content type.
if ($user->uid) {
$forum_types['disallowed'] = array('title' => t('You are not allowed to post new content in the forum.'));
}
// The user is not logged-in; and denied access to create any new forum content type.
else {
$forum_types['login'] = array('title' => t('<a href="@login">Login</a> to post new content in the forum.', array('@login' => url('user/login', array('query' => drupal_get_destination())))), 'html' => TRUE);
}
}
$variables['links'] = $forum_types;
if (!empty($variables['forums'])) {
$variables['forums'] = theme('forum_list', $variables['forums'], $variables['parents'], $variables['tid']);
}
else {
$variables['forums'] = '';
}
if ($variables['tid'] && !in_array($variables['tid'], variable_get('forum_containers', array()))) {
$variables['topics'] = theme('forum_topic_list', $variables['tid'], $variables['topics'], $variables['sortby'], $variables['forum_per_page']);
drupal_add_feed(url('taxonomy/term/'. $variables['tid'] .'/0/feed'), 'RSS - '. $title);
}
else {
$variables['topics'] = '';
}
// Provide separate template suggestions based on what's being output. Topic id is also accounted for.
// Check both variables to be safe then the inverse. Forums with topic ID's take precedence.
if ($variables['forums'] && !$variables['topics']) {
$variables['template_files'][] = 'forums-containers';
$variables['template_files'][] = 'forums-'. $variables['tid'];
$variables['template_files'][] = 'forums-containers-'. $variables['tid'];
}
elseif (!$variables['forums'] && $variables['topics']) {
$variables['template_files'][] = 'forums-topics';
$variables['template_files'][] = 'forums-'. $variables['tid'];
$variables['template_files'][] = 'forums-topics-'. $variables['tid'];
}
else {
$variables['template_files'][] = 'forums-'. $variables['tid'];
}
}
else {
drupal_set_title(t('No forums defined'));
$variables['links'] = array();
$variables['forums'] = '';
$variables['topics'] = '';
}
}
/**
* Process variables to format a forum listing.
*
* $variables contains the following information:
* - $forums
* - $parents
* - $tid
*
* @see forum-list.tpl.php
* @see theme_forum_list()
*/
function template_preprocess_forum_list(&$variables) {
global $user;
$row = 0;
// Sanitize each forum so that the template can safely print the data.
foreach ($variables['forums'] as $id => $forum) {
$variables['forums'][$id]->description = !empty($forum->description) ? filter_xss_admin($forum->description) : '';
$variables['forums'][$id]->link = url("forum/$forum->tid");
$variables['forums'][$id]->name = check_plain($forum->name);
$variables['forums'][$id]->is_container = !empty($forum->container);
$variables['forums'][$id]->zebra = $row % 2 == 0 ? 'odd' : 'even';
$row++;
$variables['forums'][$id]->new_text = '';
$variables['forums'][$id]->new_url = '';
$variables['forums'][$id]->new_topics = 0;
$variables['forums'][$id]->old_topics = $forum->num_topics;
if ($user->uid) {
$variables['forums'][$id]->new_topics = _forum_topics_unread($forum->tid, $user->uid);
if ($variables['forums'][$id]->new_topics) {
$variables['forums'][$id]->new_text = format_plural($variables['forums'][$id]->new_topics, '1 new', '@count new');
$variables['forums'][$id]->new_url = url("forum/$forum->tid", array('fragment' => 'new'));
}
$variables['forums'][$id]->old_topics = $forum->num_topics - $variables['forums'][$id]->new_topics;
}
$variables['forums'][$id]->last_reply = theme('forum_submitted', $forum->last_post);
}
// Give meaning to $tid for themers. $tid actually stands for term id.
$variables['forum_id'] = $variables['tid'];
unset($variables['tid']);
}
/**
* Preprocess variables to format the topic listing.
*
* $variables contains the following data:
* - $tid
* - $topics
* - $sortby
* - $forum_per_page
*
* @see forum-topic-list.tpl.php
* @see theme_forum_topic_list()
*/
function template_preprocess_forum_topic_list(&$variables) {
global $forum_topic_list_header;
// Create the tablesorting header.
$ts = tablesort_init($forum_topic_list_header);
$header = '';
foreach ($forum_topic_list_header as $cell) {
$cell = tablesort_header($cell, $forum_topic_list_header, $ts);
$header .= _theme_table_cell($cell, TRUE);
}
$variables['header'] = $header;
if (!empty($variables['topics'])) {
$row = 0;
foreach ($variables['topics'] as $id => $topic) {
$variables['topics'][$id]->icon = theme('forum_icon', $topic->new, $topic->num_comments, $topic->comment_mode, $topic->sticky);
$variables['topics'][$id]->zebra = $row % 2 == 0 ? 'odd' : 'even';
$row++;
// We keep the actual tid in forum table, if it's different from the
// current tid then it means the topic appears in two forums, one of
// them is a shadow copy.
if ($topic->forum_tid != $variables['tid']) {
$variables['topics'][$id]->moved = TRUE;
$variables['topics'][$id]->title = check_plain($topic->title);
$variables['topics'][$id]->message = l(t('This topic has been moved'), "forum/$topic->forum_tid");
}
else {
$variables['topics'][$id]->moved = FALSE;
$variables['topics'][$id]->title = l($topic->title, "node/$topic->nid");
$variables['topics'][$id]->message = '';
}
$variables['topics'][$id]->created = theme('forum_submitted', $topic);
$variables['topics'][$id]->last_reply = theme('forum_submitted', isset($topic->last_reply) ? $topic->last_reply : NULL);
$variables['topics'][$id]->new_text = '';
$variables['topics'][$id]->new_url = '';
if ($topic->new_replies) {
$variables['topics'][$id]->new_text = format_plural($topic->new_replies, '1 new', '@count new');
$variables['topics'][$id]->new_url = url("node/$topic->nid", array('query' => comment_new_page_count($topic->num_comments, $topic->new_replies, $topic), 'fragment' => 'new'));
}
}
}
else {
// Make this safe for the template
$variables['topics'] = array();
}
// Give meaning to $tid for themers. $tid actually stands for term id.
$variables['topic_id'] = $variables['tid'];
unset($variables['tid']);
$variables['pager'] = theme('pager', NULL, $variables['forum_per_page'], 0);
}
/**
* Process variables to format the icon for each individual topic.
*
* $variables contains the following data:
* - $new_posts
* - $num_posts = 0
* - $comment_mode = 0
* - $sticky = 0
*
* @see forum-icon.tpl.php
* @see theme_forum_icon()
*/
function template_preprocess_forum_icon(&$variables) {
$variables['hot_threshold'] = variable_get('forum_hot_topic', 15);
if ($variables['num_posts'] > $variables['hot_threshold']) {
$variables['icon'] = $variables['new_posts'] ? 'hot-new' : 'hot';
}
else {
$variables['icon'] = $variables['new_posts'] ? 'new' : 'default';
}
if ($variables['comment_mode'] == COMMENT_NODE_READ_ONLY || $variables['comment_mode'] == COMMENT_NODE_DISABLED) {
$variables['icon'] = 'closed';
}
if ($variables['sticky'] == 1) {
$variables['icon'] = 'sticky';
}
}
/**
* Preprocess variables to format the next/previous forum topic navigation links.
*
* $variables contains $node.
*
* @see forum-topic-navigation.tpl.php
* @see theme_forum_topic_navigation()
*/
function template_preprocess_forum_topic_navigation(&$variables) {
$output = '';
// get previous and next topic
$sql = "SELECT n.nid, n.title, n.sticky, l.comment_count, l.last_comment_timestamp FROM {node} n INNER JOIN {node_comment_statistics} l ON n.nid = l.nid INNER JOIN {term_node} r ON n.nid = r.nid AND r.tid = %d WHERE n.status = 1 ORDER BY n.sticky DESC, ". _forum_get_topic_order_sql(variable_get('forum_order', 1));
$result = db_query(db_rewrite_sql($sql), isset($variables['node']->tid) ? $variables['node']->tid : 0);
$stop = $variables['prev'] = $variables['next'] = 0;
while ($topic = db_fetch_object($result)) {
if ($stop == 1) {
$variables['next'] = $topic->nid;
$variables['next_title'] = check_plain($topic->title);
$variables['next_url'] = url("node/$topic->nid");
break;
}
if ($topic->nid == $variables['node']->nid) {
$stop = 1;
}
else {
$variables['prev'] = $topic->nid;
$variables['prev_title'] = check_plain($topic->title);
$variables['prev_url'] = url("node/$topic->nid");
}
}
}
/**
* Process variables to format submission info for display in the forum list and topic list.
*
* $variables will contain: $topic
*
* @see forum-submitted.tpl.php
* @see theme_forum_submitted()
*/
function template_preprocess_forum_submitted(&$variables) {
$variables['author'] = isset($variables['topic']->uid) ? theme('username', $variables['topic']) : '';
$variables['time'] = isset($variables['topic']->timestamp) ? format_interval(time() - $variables['topic']->timestamp) : '';
}
function _forum_user_last_visit($nid) {
global $user;
static $history = array();
if (empty($history)) {
$result = db_query('SELECT nid, timestamp FROM {history} WHERE uid = %d', $user->uid);
while ($t = db_fetch_object($result)) {
$history[$t->nid] = $t->timestamp > NODE_NEW_LIMIT ? $t->timestamp : NODE_NEW_LIMIT;
}
}
return isset($history[$nid]) ? $history[$nid] : NODE_NEW_LIMIT;
}
function _forum_get_topic_order($sortby) {
switch ($sortby) {
case 1:
return array('field' => 'l.last_comment_timestamp', 'sort' => 'desc');
break;
case 2:
return array('field' => 'l.last_comment_timestamp', 'sort' => 'asc');
break;
case 3:
return array('field' => 'l.comment_count', 'sort' => 'desc');
break;
case 4:
return array('field' => 'l.comment_count', 'sort' => 'asc');
break;
}
}
function _forum_get_topic_order_sql($sortby) {
$order = _forum_get_topic_order($sortby);
return $order['field'] .' '. strtoupper($order['sort']);
}

View file

@ -0,0 +1,28 @@
<?php
/**
* @file
* User page callbacks for the forum module.
*/
/**
* Menu callback; prints a forum listing.
*/
function forum_page($tid = 0) {
if (!is_numeric($tid)) {
return MENU_NOT_FOUND;
}
$tid = (int)$tid;
$topics = '';
$forum_per_page = variable_get('forum_per_page', 25);
$sortby = variable_get('forum_order', 1);
$forums = forum_get_forums($tid);
$parents = taxonomy_get_parents_all($tid);
if ($tid && !in_array($tid, variable_get('forum_containers', array()))) {
$topics = forum_get_topics($tid, $sortby, $forum_per_page);
}
return theme('forums', $forums, $topics, $parents, $tid, $sortby, $forum_per_page);
}

View file

@ -0,0 +1,26 @@
<?php
/**
* @file forums.tpl.php
* Default theme implementation to display a forum which may contain forum
* containers as well as forum topics.
*
* Variables available:
* - $links: An array of links that allow a user to post new forum topics.
* It may also contain a string telling a user they must log in in order
* to post.
* - $forums: The forums to display (as processed by forum-list.tpl.php)
* - $topics: The topics to display (as processed by forum-topic-list.tpl.php)
* - $forums_defined: A flag to indicate that the forums are configured.
*
* @see template_preprocess_forums()
* @see theme_forums()
*/
?>
<?php if ($forums_defined): ?>
<div id="forum">
<?php print theme('links', $links); ?>
<?php print $forums; ?>
<?php print $topics; ?>
</div>
<?php endif; ?>

10
modules/help/help-rtl.css Normal file
View file

@ -0,0 +1,10 @@
.help-items {
float: right;
padding-right: 0;
padding-left: 3%;
}
.help-items-last {
padding-right: 0;
padding-left: 0;
}

View file

@ -0,0 +1,75 @@
<?php
/**
* @file
* Admin page callbacks for the help module.
*/
/**
* Menu callback; prints a page listing a glossary of Drupal terminology.
*/
function help_main() {
// Add CSS
drupal_add_css(drupal_get_path('module', 'help') .'/help.css', 'module', 'all', FALSE);
$output = '<h2>'. t('Help topics') .'</h2><p>'. t('Help is available on the following items:') .'</p>'. help_links_as_list();
return $output;
}
/**
* Menu callback; prints a page listing general help for a module.
*/
function help_page($name) {
$output = '';
if (module_hook($name, 'help')) {
$module = drupal_parse_info_file(drupal_get_path('module', $name) .'/'. $name .'.info');
drupal_set_title($module['name']);
$temp = module_invoke($name, 'help', "admin/help#$name", drupal_help_arg());
if (empty($temp)) {
$output .= t("No help is available for module %module.", array('%module' => $module['name']));
}
else {
$output .= $temp;
}
// Only print list of administration pages if the module in question has
// any such pages associated to it.
$admin_tasks = system_get_module_admin_tasks($name);
if (!empty($admin_tasks)) {
ksort($admin_tasks);
$output .= theme('item_list', $admin_tasks, t('@module administration pages', array('@module' => $module['name'])));
}
}
return $output;
}
function help_links_as_list() {
$empty_arg = drupal_help_arg();
$module_info = module_rebuild_cache();
$modules = array();
foreach (module_implements('help', TRUE) as $module) {
if (module_invoke($module, 'help', "admin/help#$module", $empty_arg)) {
$modules[$module] = $module_info[$module]->info['name'];
}
}
asort($modules);
// Output pretty four-column list
$count = count($modules);
$break = ceil($count / 4);
$output = '<div class="clear-block"><div class="help-items"><ul>';
$i = 0;
foreach ($modules as $module => $name) {
$output .= '<li>'. l($name, 'admin/help/'. $module) .'</li>';
if (($i + 1) % $break == 0 && ($i + 1) != $count) {
$output .= '</ul></div><div class="help-items'. ($i + 1 == $break * 3 ? ' help-items-last' : '') .'"><ul>';
}
$i++;
}
$output .= '</ul></div></div>';
return $output;
}

9
modules/help/help.css Normal file
View file

@ -0,0 +1,9 @@
.help-items {
float: left; /* LTR */
width: 22%;
padding-right: 3%; /* LTR */
}
.help-items-last {
padding-right: 0; /* LTR */
}

11
modules/help/help.info Normal file
View file

@ -0,0 +1,11 @@
name = Help
description = Manages the display of online help.
package = Core - optional
version = VERSION
core = 6.x
; Information added by Drupal.org packaging script on 2016-02-24
version = "6.38"
project = "drupal"
datestamp = "1456343372"

47
modules/help/help.module Normal file
View file

@ -0,0 +1,47 @@
<?php
/**
* @file
* Manages displaying online help.
*/
/**
* Implementation of hook_menu().
*/
function help_menu() {
$items['admin/help'] = array(
'title' => 'Help',
'page callback' => 'help_main',
'access arguments' => array('access administration pages'),
'weight' => 9,
'file' => 'help.admin.inc',
);
foreach (module_implements('help', TRUE) as $module) {
$items['admin/help/'. $module] = array(
'title' => $module,
'page callback' => 'help_page',
'page arguments' => array(2),
'access arguments' => array('access administration pages'),
'type' => MENU_CALLBACK,
'file' => 'help.admin.inc',
);
}
return $items;
}
/**
* Implementation of hook_help().
*/
function help_help($path, $arg) {
switch ($path) {
case 'admin/help':
$output = '<p>'. t('This guide provides context sensitive help on the use and configuration of <a href="@drupal">Drupal</a> and its modules, and is a supplement to the more extensive online <a href="@handbook">Drupal handbook</a>. The online handbook may contain more up-to-date information, is annotated with helpful user-contributed comments, and serves as the definitive reference point for all Drupal documentation.', array('@drupal' => 'http://drupal.org', '@handbook' => 'http://drupal.org/handbook')) .'</p>';
return $output;
case 'admin/help#help':
$output = '<p>'. t('The help module provides context sensitive help on the use and configuration of <a href="@drupal">Drupal</a> and its modules, and is a supplement to the more extensive online <a href="@handbook">Drupal handbook</a>. The online handbook may contain more up-to-date information, is annotated with helpful user-contributed comments, and serves as the definitive reference point for all Drupal documentation.', array('@drupal' => 'http://drupal.org', '@handbook' => 'http://drupal.org/handbook')) .'</p>';
$output .= '<p>'. t('For more information, see the online handbook entry for <a href="@help">Help module</a>.', array('@help' => 'http://drupal.org/handbook/modules/help/')) .'</p>';
return $output;
}
}

View file

@ -0,0 +1,5 @@
.locale-untranslated {
font-style: normal;
text-decoration: line-through;
}

View file

@ -0,0 +1,11 @@
name = Locale
description = Adds language handling functionality and enables the translation of the user interface to languages other than English.
package = Core - optional
version = VERSION
core = 6.x
; Information added by Drupal.org packaging script on 2016-02-24
version = "6.38"
project = "drupal"
datestamp = "1456343372"

View file

@ -0,0 +1,455 @@
<?php
/**
* Implementation of hook_install().
*/
function locale_install() {
// locales_source.source and locales_target.target are not used as binary
// fields; non-MySQL database servers need to ensure the field type is text
// and that LIKE produces a case-sensitive comparison.
// Create tables.
drupal_install_schema('locale');
db_query("INSERT INTO {languages} (language, name, native, direction, enabled, weight, javascript) VALUES ('en', 'English', 'English', '0', '1', '0', '')");
}
/**
* @addtogroup updates-5.x-to-6.x
* @{
*/
/**
* {locales_meta} table became {languages}.
*/
function locale_update_6000() {
$ret = array();
$schema['languages'] = array(
'fields' => array(
'language' => array(
'type' => 'varchar',
'length' => 12,
'not null' => TRUE,
'default' => '',
),
'name' => array(
'type' => 'varchar',
'length' => 64,
'not null' => TRUE,
'default' => '',
),
'native' => array(
'type' => 'varchar',
'length' => 64,
'not null' => TRUE,
'default' => '',
),
'direction' => array(
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
'enabled' => array(
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
'plurals' => array(
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
'formula' => array(
'type' => 'varchar',
'length' => 128,
'not null' => TRUE,
'default' => '',
),
'domain' => array(
'type' => 'varchar',
'length' => 128,
'not null' => TRUE,
'default' => '',
),
'prefix' => array(
'type' => 'varchar',
'length' => 128,
'not null' => TRUE,
'default' => '',
),
'weight' => array(
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
'javascript' => array( //Adds a column to store the filename of the JavaScript translation file.
'type' => 'varchar',
'length' => 32,
'not null' => TRUE,
'default' => '',
),
),
'primary key' => array('language'),
'indexes' => array(
'list' => array('weight', 'name'),
),
);
db_create_table($ret, 'languages', $schema['languages']);
// Save the languages
$ret[] = update_sql("INSERT INTO {languages} (language, name, native, direction, enabled, plurals, formula, domain, prefix, weight) SELECT locale, name, name, 0, enabled, plurals, formula, '', locale, 0 FROM {locales_meta}");
// Save the language count in the variable table
$count = db_result(db_query('SELECT COUNT(*) FROM {languages} WHERE enabled = 1'));
variable_set('language_count', $count);
// Save the default language in the variable table
$default = db_fetch_object(db_query('SELECT * FROM {locales_meta} WHERE isdefault = 1'));
variable_set('language_default', (object) array('language' => $default->locale, 'name' => $default->name, 'native' => '', 'direction' => 0, 'enabled' => 1, 'plurals' => $default->plurals, 'formula' => $default->formula, 'domain' => '', 'prefix' => $default->locale, 'weight' => 0));
$ret[] = update_sql("DROP TABLE {locales_meta}");
return $ret;
}
/**
* Change locale column to language. The language column is added by
* update_fix_d6_requirements() in update.php to avoid a large number
* of error messages from update.php. All we need to do here is copy
* locale to language and then drop locale.
*/
function locale_update_6001() {
$ret = array();
$ret[] = update_sql('UPDATE {locales_target} SET language = locale');
db_drop_field($ret, 'locales_target', 'locale');
return $ret;
}
/**
* Remove empty translations, we don't need these anymore.
*/
function locale_update_6002() {
$ret = array();
$ret[] = update_sql("DELETE FROM {locales_target} WHERE translation = ''");
return $ret;
}
/**
* Prune strings with no translations (will be automatically re-registered if still in use)
*/
function locale_update_6003() {
$ret = array();
$ret[] = update_sql("DELETE FROM {locales_source} WHERE lid NOT IN (SELECT lid FROM {locales_target})");
return $ret;
}
/**
* Fix remaining inconsistent indexes.
*/
function locale_update_6004() {
$ret = array();
db_add_index($ret, 'locales_target', 'language', array('language'));
switch ($GLOBALS['db_type']) {
case 'pgsql':
db_drop_index($ret, 'locales_source', 'source');
db_add_index($ret, 'locales_source', 'source', array(array('source', 30)));
break;
}
return $ret;
}
/**
* Change language setting variable of content types.
*
* Use language_content_type_<content_type> instead of language_<content_type>
* so content types such as 'default', 'count' or 'negotiation' will not
* interfere with language variables.
*/
function locale_update_6005() {
foreach (node_get_types() as $type => $content_type) {
// Default to NULL, so we can skip dealing with non-existent settings.
$setting = variable_get('language_'. $type, NULL);
if ($type == 'default' && is_numeric($setting)) {
// language_default was overwritten with the content type setting,
// so reset the default language and save the content type setting.
variable_set('language_content_type_default', $setting);
variable_del('language_default');
drupal_set_message('The default language setting has been reset to its default value. Check the '. l('language configuration page', 'admin/settings/language') .' to configure it correctly.');
}
elseif ($type == 'negotiation') {
// language_content_type_negotiation is an integer either if it is
// the negotiation setting or the content type setting.
// The language_negotiation setting is not reset, but
// the user is alerted that this setting possibly was overwritten
variable_set('language_content_type_negotiation', $setting);
drupal_set_message('The language negotiation setting was possibly overwritten by a content type of the same name. Check the '. l('language configuration page', 'admin/settings/language/configure') .' and the '. l('<em>'. $content_type->name ."</em> content type's multilingual support settings", 'admin/content/types/negotiation', array('html' => TRUE)) .' to configure them correctly.');
}
elseif (!is_null($setting)) {
// Change the language setting variable for any other content type.
// Do not worry about language_count, it will be updated below.
variable_set('language_content_type_'. $type, $setting);
variable_del('language_'. $type);
}
}
// Update language count variable that might be overwritten.
$count = db_result(db_query('SELECT COUNT(*) FROM {languages} WHERE enabled = 1'));
variable_set('language_count', $count);
return array();
}
/**
* Neutralize unsafe language names in the database.
*/
function locale_update_6006() {
$ret = array();
$matches = db_result(db_query("SELECT 1 FROM {languages} WHERE native LIKE '%<%' OR native LIKE '%>%' OR name LIKE '%<%' OR name LIKE '%>%'"));
if ($matches) {
$ret[] = update_sql("UPDATE {languages} SET name = REPLACE(name, '<', ''), native = REPLACE(native, '<', '')");
$ret[] = update_sql("UPDATE {languages} SET name = REPLACE(name, '>', ''), native = REPLACE(native, '>', '')");
drupal_set_message('The language name in English and the native language name values of all the existing custom languages of your site have been sanitized for security purposes. Visit the <a href="'. url('admin/settings/language') .'">Languages</a> page to check these and fix them if necessary.', 'warning');
}
// Check if some langcode values contain potentially dangerous characters and
// warn the user if so. These are not fixed since they are referenced in other
// tables (e.g. {node}).
if (db_result(db_query("SELECT 1 FROM {languages} WHERE language LIKE '%<%' OR language LIKE '%>%' OR language LIKE '%\"%' OR language LIKE '%\\\\\%'"))) {
drupal_set_message('Some of your custom language code values contain invalid characters. You should examine the <a href="'. url('admin/settings/language') .'">Languages</a> page. These must be fixed manually.', 'error');
}
return $ret;
}
/**
* @} End of "addtogroup updates-5.x-to-6.x".
*/
/**
* @addtogroup updates-6.x-extra
* @{
*/
/**
* Fix Drupal.formatPlural().
*/
function locale_update_6007() {
drupal_load('module', 'locale');
locale_inc_callback('_locale_invalidate_js');
return array();
}
/**
* @} End of "addtogroup updates-6.x-extra".
* The next series of updates should start at 7000.
*/
/**
* Implementation of hook_uninstall().
*/
function locale_uninstall() {
// Delete all JavaScript translation files
$files = db_query('SELECT javascript FROM {languages}');
while ($file = db_fetch_object($files)) {
if (!empty($file)) {
file_delete(file_create_path($file->javascript));
}
}
// Clear variables.
variable_del('language_default');
variable_del('language_count');
variable_del('language_content_type_default');
variable_del('language_content_type_negotiation');
variable_del('locale_cache_strings');
variable_del('locale_js_directory');
variable_del('javascript_parsed');
variable_del('language_negotiation');
foreach (node_get_types() as $type => $content_type) {
variable_del("language_content_type_$type");
}
// Switch back to English: with a $language->language value different from
// 'en' successive calls of t() might result in calling locale(), which in
// turn might try to query the unexisting {locales_source} and
// {locales_target} tables.
drupal_init_language();
// Remove tables.
drupal_uninstall_schema('locale');
}
/**
* Implementation of hook_schema().
*/
function locale_schema() {
$schema['languages'] = array(
'description' => 'List of all available languages in the system.',
'fields' => array(
'language' => array(
'type' => 'varchar',
'length' => 12,
'not null' => TRUE,
'default' => '',
'description' => "Language code, e.g. 'de' or 'en-US'.",
),
'name' => array(
'type' => 'varchar',
'length' => 64,
'not null' => TRUE,
'default' => '',
'description' => 'Language name in English.',
),
'native' => array(
'type' => 'varchar',
'length' => 64,
'not null' => TRUE,
'default' => '',
'description' => 'Native language name.',
),
'direction' => array(
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'description' => 'Direction of language (Left-to-Right = 0, Right-to-Left = 1).',
),
'enabled' => array(
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'description' => 'Enabled flag (1 = Enabled, 0 = Disabled).',
),
'plurals' => array(
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'description' => 'Number of plural indexes in this language.',
),
'formula' => array(
'type' => 'varchar',
'length' => 128,
'not null' => TRUE,
'default' => '',
'description' => 'Plural formula in PHP code to evaluate to get plural indexes.',
),
'domain' => array(
'type' => 'varchar',
'length' => 128,
'not null' => TRUE,
'default' => '',
'description' => 'Domain to use for this language.',
),
'prefix' => array(
'type' => 'varchar',
'length' => 128,
'not null' => TRUE,
'default' => '',
'description' => 'Path prefix to use for this language.',
),
'weight' => array(
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'description' => 'Weight, used in lists of languages.',
),
'javascript' => array(
'type' => 'varchar',
'length' => 32,
'not null' => TRUE,
'default' => '',
'description' => 'Location of JavaScript translation file.',
),
),
'primary key' => array('language'),
'indexes' => array(
'list' => array('weight', 'name'),
),
);
$schema['locales_source'] = array(
'description' => 'List of English source strings.',
'fields' => array(
'lid' => array(
'type' => 'serial',
'not null' => TRUE,
'description' => 'Unique identifier of this string.',
),
'location' => array(
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
'default' => '',
'description' => 'Drupal path in case of online discovered translations or file path in case of imported strings.',
),
'textgroup' => array(
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
'default' => 'default',
'description' => 'A module defined group of translations, see hook_locale().',
),
'source' => array(
'type' => 'text',
'mysql_type' => 'blob',
'not null' => TRUE,
'description' => 'The original string in English.',
),
'version' => array(
'type' => 'varchar',
'length' => 20,
'not null' => TRUE,
'default' => 'none',
'description' => 'Version of Drupal, where the string was last used (for locales optimization).',
),
),
'primary key' => array('lid'),
'indexes' => array(
'source' => array(array('source', 30)),
),
);
$schema['locales_target'] = array(
'description' => 'Stores translated versions of strings.',
'fields' => array(
'lid' => array(
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'description' => 'Source string ID. References {locales_source}.lid.',
),
'translation' => array(
'type' => 'text',
'mysql_type' => 'blob',
'not null' => TRUE,
'description' => 'Translation string value in this language.',
),
'language' => array(
'type' => 'varchar',
'length' => 12,
'not null' => TRUE,
'default' => '',
'description' => 'Language code. References {languages}.language.',
),
'plid' => array(
'type' => 'int',
'not null' => TRUE, // This should be NULL for no referenced string, not zero.
'default' => 0,
'description' => 'Parent lid (lid of the previous string in the plural chain) in case of plural strings. References {locales_source}.lid.',
),
'plural' => array(
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'description' => 'Plural index number in case of plural strings.',
),
),
'primary key' => array('language', 'lid', 'plural'),
'indexes' => array(
'lid' => array('lid'),
'plid' => array('plid'),
'plural' => array('plural'),
),
);
return $schema;
}

View file

@ -0,0 +1,595 @@
<?php
/**
* @file
* Add language handling functionality and enables the translation of the
* user interface to languages other than English.
*
* When enabled, multiple languages can be set up. The site interface
* can be displayed in different languages, as well as nodes can have languages
* assigned. The setup of languages and translations is completely web based.
* Gettext portable object files are supported.
*/
// ---------------------------------------------------------------------------------
// Hook implementations
/**
* Implementation of hook_help().
*/
function locale_help($path, $arg) {
switch ($path) {
case 'admin/help#locale':
$output = '<p>'. t('The locale module allows your Drupal site to be presented in languages other than the default English, a defining feature of multi-lingual websites. The locale module works by examining text as it is about to be displayed: when a translation of the text is available in the language to be displayed, the translation is displayed rather than the original text. When a translation is unavailable, the original text is displayed, and then stored for later review by a translator.') .'</p>';
$output .= '<p>'. t('Beyond translation of the Drupal interface, the locale module provides a feature set tailored to the needs of a multi-lingual site. Language negotiation allows your site to automatically change language based on the domain or path used for each request. Users may (optionally) select their preferred language on their <em>My account</em> page, and your site can be configured to honor a web browser\'s preferred language settings. Your site content can be created in (and translated to) any enabled language, and each post may have a language-appropriate alias for each of its translations. The locale module works in concert with the <a href="@content-help">content translation module</a> to manage translated content.', array('@content-help' => url('admin/help/translation'))) .'</p>';
$output .= '<p>'. t('Translations may be provided by:') .'</p>';
$output .= '<ul><li>'. t("translating the original text via the locale module's integrated web interface, or") .'</li>';
$output .= '<li>'. t('importing files from a set of existing translations, known as a translation package. A translation package enables the display of a specific version of Drupal in a specific language, and contain files in the Gettext Portable Object (<em>.po</em>) format. Although not all languages are available for every version of Drupal, translation packages for many languages are available for download from the <a href="@translations">Drupal translation page</a>.', array('@translations' => 'http://localize.drupal.org')) .'</li></ul>';
$output .= '<p>'. t('If an existing translation package does not meet your needs, the Gettext Portable Object (<em>.po</em>) files within a package may be modified, or new <em>.po</em> files may be created, using a desktop Gettext editor. The locale module\'s <a href="@import">import</a> feature allows the translated strings from a new or modified <em>.po</em> file to be added to your site. The locale module\'s <a href="@export">export</a> feature generates files from your site\'s translated strings, that can either be shared with others or edited offline by a Gettext translation editor.', array('@import' => url('admin/build/translate/import'), '@export' => url('admin/build/translate/export'))) .'</p>';
$output .= '<p>'. t('For more information, see the online handbook entry for <a href="@locale">Locale module</a>.', array('@locale' => 'http://drupal.org/handbook/modules/locale/')) .'</p>';
return $output;
case 'admin/settings/language':
$output = '<p>'. t("This page provides an overview of your site's enabled languages. If multiple languages are available and enabled, the text on your site interface may be translated, registered users may select their preferred language on the <em>My account</em> page, and site authors may indicate a specific language when creating posts. The site's default language is used for anonymous visitors and for users who have not selected a preferred language.") .'</p>';
$output .= '<p>'. t('For each language available on the site, use the <em>edit</em> link to configure language details, including name, an optional language-specific path or domain, and whether the language is natively presented either left-to-right or right-to-left. These languages also appear in the <em>Language</em> selection when creating a post of a content type with multilingual support.') .'</p>';
$output .= '<p>'. t('Use the <a href="@add-language">add language page</a> to enable additional languages (and automatically import files from a translation package, if available), the <a href="@search">translate interface page</a> to locate strings for manual translation, or the <a href="@import">import page</a> to add translations from individual <em>.po</em> files. A number of contributed translation packages containing <em>.po</em> files are available on the <a href="@translations">Drupal.org translations page</a>.', array('@add-language' => url('admin/settings/language/add'), '@search' => url('admin/build/translate/search'), '@import' => url('admin/build/translate/import'), '@translations' => 'http://localize.drupal.org')) .'</p>';
return $output;
case 'admin/settings/language/add':
return '<p>'. t('Add all languages to be supported by your site. If your desired language is not available in the <em>Language name</em> drop-down, click <em>Custom language</em> and provide a language code and other details manually. When providing a language code manually, be sure to enter a standardized language code, since this code may be used by browsers to determine an appropriate display language.') .'</p>';
case 'admin/settings/language/configure':
$output = '<p>'. t("Language negotiation settings determine the site's presentation language. Available options include:") .'</p>';
$output .= '<ul><li>'. t('<strong>None.</strong> The default language is used for site presentation, though users may (optionally) select a preferred language on the <em>My Account</em> page. (User language preferences will be used for site e-mails, if available.)') .'</li>';
$output .= '<li>'. t('<strong>Path prefix only.</strong> The presentation language is determined by examining the path for a language code or other custom string that matches the path prefix (if any) specified for each language. If a suitable prefix is not identified, the default language is used. <em>Example: "example.com/de/contact" sets presentation language to German based on the use of "de" within the path.</em>') .'</li>';
$output .= '<li>'. t("<strong>Path prefix with language fallback.</strong> The presentation language is determined by examining the path for a language code or other custom string that matches the path prefix (if any) specified for each language. If a suitable prefix is not identified, the display language is determined by the user's language preferences from the <em>My Account</em> page, or by the browser's language settings. If a presentation language cannot be determined, the default language is used.") .'</li>';
$output .= '<li>'. t('<strong>Domain name only.</strong> The presentation language is determined by examining the domain used to access the site, and comparing it to the language domain (if any) specified for each language. If a match is not identified, the default language is used. <em>Example: "http://de.example.com/contact" sets presentation language to German based on the use of "http://de.example.com" in the domain.</em>') .'</li></ul>';
$output .= '<p>'. t('The path prefix or domain name for a language may be set by editing the <a href="@languages">available languages</a>. In the absence of an appropriate match, the site is displayed in the <a href="@languages">default language</a>.', array('@languages' => url('admin/settings/language'))) .'</p>';
return $output;
case 'admin/build/translate':
$output = '<p>'. t('This page provides an overview of available translatable strings. Drupal displays translatable strings in text groups; modules may define additional text groups containing other translatable strings. Because text groups provide a method of grouping related strings, they are often used to focus translation efforts on specific areas of the Drupal interface.') .'</p>';
$output .= '<p>'. t('Review the <a href="@languages">languages page</a> for more information on adding support for additional languages.', array('@languages' => url('admin/settings/language'))) .'</p>';
return $output;
case 'admin/build/translate/import':
$output = '<p>'. t('This page imports the translated strings contained in an individual Gettext Portable Object (<em>.po</em>) file. Normally distributed as part of a translation package (each translation package may contain several <em>.po</em> files), a <em>.po</em> file may need to be imported after off-line editing in a Gettext translation editor. Importing an individual <em>.po</em> file may be a lengthy process.') .'</p>';
$output .= '<p>'. t('Note that the <em>.po</em> files within a translation package are imported automatically (if available) when new modules or themes are enabled, or as new languages are added. Since this page only allows the import of one <em>.po</em> file at a time, it may be simpler to download and extract a translation package into your Drupal installation directory and <a href="@language-add">add the language</a> (which automatically imports all <em>.po</em> files within the package). Translation packages are available for download on the <a href="@translations">Drupal translation page</a>.', array('@language-add' => url('admin/settings/language/add'), '@translations' => 'http://localize.drupal.org')) .'</p>';
return $output;
case 'admin/build/translate/export':
return '<p>'. t('This page exports the translated strings used by your site. An export file may be in Gettext Portable Object (<em>.po</em>) form, which includes both the original string and the translation (used to share translations with others), or in Gettext Portable Object Template (<em>.pot</em>) form, which includes the original strings only (used to create new translations with a Gettext translation editor).') .'</p>';
case 'admin/build/translate/search':
return '<p>'. t('This page allows a translator to search for specific translated and untranslated strings, and is used when creating or editing translations. (Note: For translation tasks involving many strings, it may be more convenient to <a href="@export">export</a> strings for off-line editing in a desktop Gettext translation editor.) Searches may be limited to strings found within a specific text group or in a specific language.', array('@export' => url('admin/build/translate/export'))) .'</p>';
case 'admin/build/block/configure':
if ($arg[4] == 'locale' && $arg[5] == 0) {
return '<p>'. t('This block is only shown if <a href="@languages">at least two languages are enabled</a> and <a href="@configuration">language negotiation</a> is set to something other than <em>None</em>.', array('@languages' => url('admin/settings/language'), '@configuration' => url('admin/settings/language/configure'))) .'</p>';
}
break;
}
}
/**
* Implementation of hook_menu().
*
* Locale module only provides administrative menu items, so all
* menu items are invoked through locale_inc_callback().
*/
function locale_menu() {
// Manage languages
$items['admin/settings/language'] = array(
'title' => 'Languages',
'description' => 'Configure languages for content and the user interface.',
'page callback' => 'locale_inc_callback',
'page arguments' => array('drupal_get_form', 'locale_languages_overview_form'),
'access arguments' => array('administer languages'),
);
$items['admin/settings/language/overview'] = array(
'title' => 'List',
'weight' => 0,
'type' => MENU_DEFAULT_LOCAL_TASK,
);
$items['admin/settings/language/add'] = array(
'title' => 'Add language',
'page callback' => 'locale_inc_callback',
'page arguments' => array('locale_languages_add_screen'), // two forms concatenated
'access arguments' => array('administer languages'),
'weight' => 5,
'type' => MENU_LOCAL_TASK,
);
$items['admin/settings/language/configure'] = array(
'title' => 'Configure',
'page callback' => 'locale_inc_callback',
'page arguments' => array('drupal_get_form', 'locale_languages_configure_form'),
'access arguments' => array('administer languages'),
'weight' => 10,
'type' => MENU_LOCAL_TASK,
);
$items['admin/settings/language/edit/%'] = array(
'title' => 'Edit language',
'page callback' => 'locale_inc_callback',
'page arguments' => array('drupal_get_form', 'locale_languages_edit_form', 4),
'access arguments' => array('administer languages'),
'type' => MENU_CALLBACK,
);
$items['admin/settings/language/delete/%'] = array(
'title' => 'Confirm',
'page callback' => 'locale_inc_callback',
'page arguments' => array('drupal_get_form', 'locale_languages_delete_form', 4),
'access arguments' => array('administer languages'),
'type' => MENU_CALLBACK,
);
// Translation functionality
$items['admin/build/translate'] = array(
'title' => 'Translate interface',
'description' => 'Translate the built in interface and optionally other text.',
'page callback' => 'locale_inc_callback',
'page arguments' => array('locale_translate_overview_screen'), // not a form, just a table
'access arguments' => array('translate interface'),
);
$items['admin/build/translate/overview'] = array(
'title' => 'Overview',
'weight' => 0,
'type' => MENU_DEFAULT_LOCAL_TASK,
);
$items['admin/build/translate/search'] = array(
'title' => 'Search',
'weight' => 10,
'type' => MENU_LOCAL_TASK,
'page callback' => 'locale_inc_callback',
'page arguments' => array('locale_translate_seek_screen'), // search results and form concatenated
'access arguments' => array('translate interface'),
);
$items['admin/build/translate/import'] = array(
'title' => 'Import',
'page callback' => 'locale_inc_callback',
'page arguments' => array('drupal_get_form', 'locale_translate_import_form'),
'access arguments' => array('translate interface'),
'weight' => 20,
'type' => MENU_LOCAL_TASK,
);
$items['admin/build/translate/export'] = array(
'title' => 'Export',
'page callback' => 'locale_inc_callback',
'page arguments' => array('locale_translate_export_screen'), // possibly multiple forms concatenated
'access arguments' => array('translate interface'),
'weight' => 30,
'type' => MENU_LOCAL_TASK,
);
$items['admin/build/translate/edit/%'] = array(
'title' => 'Edit string',
'page callback' => 'locale_inc_callback',
'page arguments' => array('drupal_get_form', 'locale_translate_edit_form', 4),
'access arguments' => array('translate interface'),
'type' => MENU_CALLBACK,
);
$items['admin/build/translate/delete/%'] = array(
'title' => 'Delete string',
'page callback' => 'locale_inc_callback',
'page arguments' => array('locale_translate_delete_page', 4),
'access arguments' => array('translate interface'),
'type' => MENU_CALLBACK,
);
return $items;
}
/**
* Wrapper function to be able to set callbacks in locale.inc
*/
function locale_inc_callback() {
$args = func_get_args();
$function = array_shift($args);
include_once './includes/locale.inc';
return call_user_func_array($function, $args);
}
/**
* Implementation of hook_perm().
*/
function locale_perm() {
return array('administer languages', 'translate interface');
}
/**
* Implementation of hook_locale().
*/
function locale_locale($op = 'groups') {
switch ($op) {
case 'groups':
return array('default' => t('Built-in interface'));
}
}
/**
* Implementation of hook_user().
*/
function locale_user($type, $edit, &$user, $category = NULL) {
global $language;
// If we have more then one language and either creating a user on the
// admin interface or edit the user, show the language selector.
if (variable_get('language_count', 1) > 1 && ($type == 'register' && user_access('administer users') || $type == 'form' && $category == 'account' )) {
$languages = language_list('enabled');
$languages = $languages[1];
// If the user is being created, we set the user language to the page language.
$user_preferred_language = $user ? user_preferred_language($user) : $language;
$names = array();
foreach ($languages as $langcode => $item) {
$name = t($item->name);
$names[check_plain($langcode)] = check_plain($name . ($item->native != $name ? ' ('. $item->native .')' : ''));
}
$form['locale'] = array(
'#type' => 'fieldset',
'#title' => t('Language settings'),
'#weight' => 1,
);
// Get language negotiation settings.
$mode = variable_get('language_negotiation', LANGUAGE_NEGOTIATION_NONE);
$form['locale']['language'] = array(
'#type' => (count($names) <= 5 ? 'radios' : 'select'),
'#title' => t('Language'),
'#default_value' => check_plain($user_preferred_language->language),
'#options' => $names,
'#description' => ($mode == LANGUAGE_NEGOTIATION_PATH) ? t("This account's default language for e-mails, and preferred language for site presentation.") : t("This account's default language for e-mails."),
);
return $form;
}
}
/**
* Implementation of hook_form_alter(). Adds language fields to forms.
*/
function locale_form_alter(&$form, $form_state, $form_id) {
switch ($form_id) {
// Language field for paths
case 'path_admin_form':
$form['language'] = array(
'#type' => 'select',
'#title' => t('Language'),
'#options' => array('' => t('All languages')) + locale_language_list('name'),
'#default_value' => $form['language']['#value'],
'#weight' => -10,
'#description' => t('A path alias set for a specific language will always be used when displaying this page in that language, and takes precedence over path aliases set for <em>All languages</em>.'),
);
break;
// Language setting for content types
case 'node_type_form':
if (isset($form['identity']['type'])) {
$form['workflow']['language_content_type'] = array(
'#type' => 'radios',
'#title' => t('Multilingual support'),
'#default_value' => variable_get('language_content_type_'. $form['#node_type']->type, 0),
'#options' => array(t('Disabled'), t('Enabled')),
'#description' => t('Enable multilingual support for this content type. If enabled, a language selection field will be added to the editing form, allowing you to select from one of the <a href="!languages">enabled languages</a>. If disabled, new posts are saved with the default language. Existing content will not be affected by changing this option.', array('!languages' => url('admin/settings/language'))),
);
}
break;
// Language field for nodes
default:
if (isset($form['#id']) && $form['#id'] == 'node-form') {
if (isset($form['#node']->type) && variable_get('language_content_type_'. $form['#node']->type, 0)) {
$form['language'] = array(
'#type' => 'select',
'#title' => t('Language'),
'#default_value' => (isset($form['#node']->language) ? $form['#node']->language : ''),
'#options' => array('' => t('Language neutral')) + locale_language_list('name'),
);
}
// Node type without language selector: assign the default for new nodes
elseif (!isset($form['#node']->nid)) {
$default = language_default();
$form['language'] = array(
'#type' => 'value',
'#value' => $default->language
);
}
}
}
}
/**
* Implementation of hook_theme()
*/
function locale_theme() {
return array(
'locale_languages_overview_form' => array(
'arguments' => array('form' => array()),
),
);
}
/**
* Implementation of hook_node_type().
*/
function locale_node_type($op, $info) {
if ($op == 'delete') {
variable_del('language_content_type_'. $info->type);
}
}
// ---------------------------------------------------------------------------------
// Locale core functionality
/**
* Provides interface translation services.
*
* This function is called from t() to translate a string if needed.
*
* @param $string
* A string to look up translation for. If omitted, all the
* cached strings will be returned in all languages already
* used on the page.
* @param $langcode
* Language code to use for the lookup.
* @param $reset
* Set to TRUE to reset the in-memory cache.
*/
function locale($string = NULL, $langcode = NULL, $reset = FALSE) {
global $language;
static $locale_t;
if ($reset) {
// Reset in-memory cache.
$locale_t = NULL;
}
if (!isset($string)) {
// Return all cached strings if no string was specified
return $locale_t;
}
$langcode = isset($langcode) ? $langcode : $language->language;
// Store database cached translations in a static var.
if (!isset($locale_t[$langcode])) {
$locale_t[$langcode] = array();
// Disabling the usage of string caching allows a module to watch for
// the exact list of strings used on a page. From a performance
// perspective that is a really bad idea, so we have no user
// interface for this. Be careful when turning this option off!
if (variable_get('locale_cache_strings', 1) == 1) {
if ($cache = cache_get('locale:'. $langcode, 'cache')) {
$locale_t[$langcode] = $cache->data;
}
elseif (lock_acquire('locale_cache_' . $langcode)) {
// Refresh database stored cache of translations for given language.
// We only store short strings used in current version, to improve
// performance and consume less memory.
$result = db_query("SELECT s.source, t.translation, t.language FROM {locales_source} s LEFT JOIN {locales_target} t ON s.lid = t.lid AND t.language = '%s' WHERE s.textgroup = 'default' AND s.version = '%s' AND LENGTH(s.source) < 75", $langcode, VERSION);
while ($data = db_fetch_object($result)) {
$locale_t[$langcode][$data->source] = (empty($data->translation) ? TRUE : $data->translation);
}
cache_set('locale:'. $langcode, $locale_t[$langcode]);
lock_release('locale_cache_' . $langcode);
}
}
}
// If we have the translation cached, skip checking the database
if (!isset($locale_t[$langcode][$string])) {
// We do not have this translation cached, so get it from the DB.
$translation = db_fetch_object(db_query("SELECT s.lid, t.translation, s.version FROM {locales_source} s LEFT JOIN {locales_target} t ON s.lid = t.lid AND t.language = '%s' WHERE s.source = '%s' AND s.textgroup = 'default'", $langcode, $string));
if ($translation) {
// We have the source string at least.
// Cache translation string or TRUE if no translation exists.
$locale_t[$langcode][$string] = (empty($translation->translation) ? TRUE : $translation->translation);
if ($translation->version != VERSION) {
// This is the first use of this string under current Drupal version. Save version
// and clear cache, to include the string into caching next time. Saved version is
// also a string-history information for later pruning of the tables.
db_query("UPDATE {locales_source} SET version = '%s' WHERE lid = %d", VERSION, $translation->lid);
cache_clear_all('locale:', 'cache', TRUE);
}
}
else {
// We don't have the source string, cache this as untranslated.
db_query("INSERT INTO {locales_source} (location, source, textgroup, version) VALUES ('%s', '%s', 'default', '%s')", request_uri(), $string, VERSION);
$locale_t[$langcode][$string] = TRUE;
// Clear locale cache so this string can be added in a later request.
cache_clear_all('locale:', 'cache', TRUE);
}
}
return ($locale_t[$langcode][$string] === TRUE ? $string : $locale_t[$langcode][$string]);
}
/**
* Returns plural form index for a specific number.
*
* The index is computed from the formula of this language.
*
* @param $count
* Number to return plural for.
* @param $langcode
* Optional language code to translate to a language other than
* what is used to display the page.
*/
function locale_get_plural($count, $langcode = NULL) {
global $language;
static $locale_formula, $plurals = array();
$langcode = $langcode ? $langcode : $language->language;
if (!isset($plurals[$langcode][$count])) {
if (!isset($locale_formula)) {
$language_list = language_list();
$locale_formula[$langcode] = $language_list[$langcode]->formula;
}
if ($locale_formula[$langcode]) {
$n = $count;
$plurals[$langcode][$count] = @eval('return intval('. $locale_formula[$langcode] .');');
return $plurals[$langcode][$count];
}
else {
$plurals[$langcode][$count] = -1;
return -1;
}
}
return $plurals[$langcode][$count];
}
/**
* Returns a language name
*/
function locale_language_name($lang) {
static $list = NULL;
if (!isset($list)) {
$list = locale_language_list();
}
return ($lang && isset($list[$lang])) ? $list[$lang] : t('All');
}
/**
* Returns array of language names
*
* @param $field
* 'name' => names in current language, localized
* 'native' => native names
* @param $all
* Boolean to return all languages or only enabled ones
*/
function locale_language_list($field = 'name', $all = FALSE) {
if ($all) {
$languages = language_list();
}
else {
$languages = language_list('enabled');
$languages = $languages[1];
}
$list = array();
foreach ($languages as $language) {
$list[$language->language] = ($field == 'name') ? t($language->name) : $language->$field;
}
return $list;
}
/**
* Imports translations when new modules or themes are installed or enabled.
*
* This function will either import translation for the component change
* right away, or start a batch if more files need to be imported.
*
* @param $components
* An array of component (theme and/or module) names to import
* translations for.
*/
function locale_system_update($components) {
include_once 'includes/locale.inc';
if ($batch = locale_batch_by_component($components)) {
batch_set($batch);
}
}
/**
* Update JavaScript translation file, if required, and add it to the page.
*
* This function checks all JavaScript files currently added via drupal_add_js()
* and invokes parsing if they have not yet been parsed for Drupal.t()
* and Drupal.formatPlural() calls. Also refreshes the JavaScript translation
* file if necessary, and adds it to the page.
*/
function locale_update_js_files() {
global $language;
$dir = file_create_path(variable_get('locale_js_directory', 'languages'));
$parsed = variable_get('javascript_parsed', array());
// The first three parameters are NULL in order to get an array with all
// scopes. This is necessary to prevent recreation of JS translation files
// when new files are added for example in the footer.
$javascript = drupal_add_js(NULL, NULL, NULL);
$files = $new_files = FALSE;
foreach ($javascript as $scope) {
foreach ($scope as $type => $data) {
if ($type != 'setting' && $type != 'inline') {
foreach ($data as $filepath => $info) {
$files = TRUE;
if (!in_array($filepath, $parsed)) {
// Don't parse our own translations files.
if (substr($filepath, 0, strlen($dir)) != $dir) {
locale_inc_callback('_locale_parse_js_file', $filepath);
$parsed[] = $filepath;
$new_files = TRUE;
}
}
}
}
}
}
// If there are any new source files we parsed, invalidate existing
// JavaScript translation files for all languages, adding the refresh
// flags into the existing array.
if ($new_files) {
$parsed += locale_inc_callback('_locale_invalidate_js');
}
// If necessary, rebuild the translation file for the current language.
if (!empty($parsed['refresh:'. $language->language])) {
// Don't clear the refresh flag on failure, so that another try will
// be performed later.
if (locale_inc_callback('_locale_rebuild_js')) {
unset($parsed['refresh:'. $language->language]);
}
// Store any changes after refresh was attempted.
variable_set('javascript_parsed', $parsed);
}
// If no refresh was attempted, but we have new source files, we need
// to store them too. This occurs if current page is in English.
else if ($new_files) {
variable_set('javascript_parsed', $parsed);
}
// Add the translation JavaScript file to the page.
if ($files && !empty($language->javascript)) {
drupal_add_js($dir .'/'. $language->language .'_'. $language->javascript .'.js', 'core');
}
}
// ---------------------------------------------------------------------------------
// Language switcher block
/**
* Implementation of hook_block().
* Displays a language switcher. Translation links may be provided by other modules.
*/
function locale_block($op = 'list', $delta = 0) {
if ($op == 'list') {
$block[0]['info'] = t('Language switcher');
// Not worth caching.
$block[0]['cache'] = BLOCK_NO_CACHE;
return $block;
}
// Only show if we have at least two languages and language dependent
// web addresses, so we can actually link to other language versions.
elseif ($op == 'view' && variable_get('language_count', 1) > 1 && variable_get('language_negotiation', LANGUAGE_NEGOTIATION_NONE) != LANGUAGE_NEGOTIATION_NONE) {
$path = drupal_is_front_page() ? '<front>' : $_GET['q'];
$languages = language_list('enabled');
$links = array();
foreach ($languages[1] as $language) {
$links[$language->language] = array(
'href' => $path,
'title' => $language->native,
'language' => $language,
'attributes' => array('class' => 'language-link'),
);
}
// Allow modules to provide translations for specific links.
// A translation link may need to point to a different path or use
// a translated link text before going through l(), which will just
// handle the path aliases.
drupal_alter('translation_link', $links, $path);
$block['subject'] = t('Languages');
$block['content'] = theme('links', $links, array());
return $block;
}
}

643
modules/menu/menu.admin.inc Normal file
View file

@ -0,0 +1,643 @@
<?php
/**
* @file
* Administrative page callbacks for menu module.
*/
/**
* Menu callback which shows an overview page of all the custom menus and their descriptions.
*/
function menu_overview_page() {
$result = db_query("SELECT * FROM {menu_custom} ORDER BY title");
$content = array();
while ($menu = db_fetch_array($result)) {
$menu['href'] = 'admin/build/menu-customize/'. $menu['menu_name'];
$menu['localized_options'] = array();
$menu['description'] = filter_xss_admin($menu['description']);
$content[] = $menu;
}
return theme('admin_block_content', $content);
}
/**
* Form for editing an entire menu tree at once.
*
* Shows for one menu the menu items accessible to the current user and
* relevant operations.
*/
function menu_overview_form(&$form_state, $menu) {
global $menu_admin;
$sql = "
SELECT m.load_functions, m.to_arg_functions, m.access_callback, m.access_arguments, m.page_callback, m.page_arguments, m.title, m.title_callback, m.title_arguments, m.type, m.description, ml.*
FROM {menu_links} ml LEFT JOIN {menu_router} m ON m.path = ml.router_path
WHERE ml.menu_name = '%s'
ORDER BY p1 ASC, p2 ASC, p3 ASC, p4 ASC, p5 ASC, p6 ASC, p7 ASC, p8 ASC, p9 ASC";
$result = db_query($sql, $menu['menu_name']);
$tree = menu_tree_data($result);
$node_links = array();
menu_tree_collect_node_links($tree, $node_links);
// We indicate that a menu administrator is running the menu access check.
$menu_admin = TRUE;
menu_tree_check_access($tree, $node_links);
$menu_admin = FALSE;
$form = _menu_overview_tree_form($tree);
$form['#menu'] = $menu;
if (element_children($form)) {
$form['submit'] = array(
'#type' => 'submit',
'#value' => t('Save configuration'),
);
}
else {
$form['empty_menu'] = array('#value' => t('There are no menu items yet.'));
}
return $form;
}
/**
* Recursive helper function for menu_overview_form().
*/
function _menu_overview_tree_form($tree) {
static $form = array('#tree' => TRUE);
foreach ($tree as $data) {
$title = '';
$item = $data['link'];
// Don't show callbacks; these have $item['hidden'] < 0.
if ($item && $item['hidden'] >= 0) {
$mlid = 'mlid:'. $item['mlid'];
$form[$mlid]['#item'] = $item;
$form[$mlid]['#attributes'] = $item['hidden'] ? array('class' => 'menu-disabled') : array('class' => 'menu-enabled');
$form[$mlid]['title']['#value'] = l($item['title'], $item['href'], $item['localized_options']) . ($item['hidden'] ? ' ('. t('disabled') .')' : '');
$form[$mlid]['hidden'] = array(
'#type' => 'checkbox',
'#default_value' => !$item['hidden'],
);
$form[$mlid]['expanded'] = array(
'#type' => 'checkbox',
'#default_value' => $item['expanded'],
);
$form[$mlid]['weight'] = array(
'#type' => 'weight',
'#delta' => 50,
'#default_value' => isset($form_state[$mlid]['weight']) ? $form_state[$mlid]['weight'] : $item['weight'],
);
$form[$mlid]['mlid'] = array(
'#type' => 'hidden',
'#value' => $item['mlid'],
);
$form[$mlid]['plid'] = array(
'#type' => 'textfield',
'#default_value' => isset($form_state[$mlid]['plid']) ? $form_state[$mlid]['plid'] : $item['plid'],
'#size' => 6,
);
// Build a list of operations.
$operations = array();
$operations['edit'] = l(t('edit'), 'admin/build/menu/item/'. $item['mlid'] .'/edit');
// Only items created by the menu module can be deleted.
if ($item['module'] == 'menu' || $item['updated'] == 1) {
$operations['delete'] = l(t('delete'), 'admin/build/menu/item/'. $item['mlid'] .'/delete');
}
// Set the reset column.
elseif ($item['module'] == 'system' && $item['customized']) {
$operations['reset'] = l(t('reset'), 'admin/build/menu/item/'. $item['mlid'] .'/reset');
}
$form[$mlid]['operations'] = array();
foreach ($operations as $op => $value) {
$form[$mlid]['operations'][$op] = array('#value' => $value);
}
}
if ($data['below']) {
_menu_overview_tree_form($data['below']);
}
}
return $form;
}
/**
* Submit handler for the menu overview form.
*
* This function takes great care in saving parent items first, then items
* underneath them. Saving items in the incorrect order can break the menu tree.
*
* @see menu_overview_form()
*/
function menu_overview_form_submit($form, &$form_state) {
// When dealing with saving menu items, the order in which these items are
// saved is critical. If a changed child item is saved before its parent,
// the child item could be saved with an invalid path past its immediate
// parent. To prevent this, save items in the form in the same order they
// are sent by $_POST, ensuring parents are saved first, then their children.
// See http://drupal.org/node/181126#comment-632270
$order = array_flip(array_keys($form['#post'])); // Get the $_POST order.
$form = array_merge($order, $form); // Update our original form with the new order.
$updated_items = array();
$fields = array('expanded', 'weight', 'plid');
foreach (element_children($form) as $mlid) {
if (isset($form[$mlid]['#item'])) {
$element = $form[$mlid];
// Update any fields that have changed in this menu item.
foreach ($fields as $field) {
if ($element[$field]['#value'] != $element[$field]['#default_value']) {
$element['#item'][$field] = $element[$field]['#value'];
$updated_items[$mlid] = $element['#item'];
}
}
// Hidden is a special case, the value needs to be reversed.
if ($element['hidden']['#value'] != $element['hidden']['#default_value']) {
$element['#item']['hidden'] = !$element['hidden']['#value'];
$updated_items[$mlid] = $element['#item'];
}
}
}
// Save all our changed items to the database.
foreach ($updated_items as $item) {
$item['customized'] = 1;
menu_link_save($item);
}
}
/**
* Theme the menu overview form into a table.
*
* @ingroup themeable
*/
function theme_menu_overview_form($form) {
drupal_add_tabledrag('menu-overview', 'match', 'parent', 'menu-plid', 'menu-plid', 'menu-mlid', TRUE, MENU_MAX_DEPTH - 1);
drupal_add_tabledrag('menu-overview', 'order', 'sibling', 'menu-weight');
$header = array(
t('Menu item'),
array('data' => t('Enabled'), 'class' => 'checkbox'),
array('data' => t('Expanded'), 'class' => 'checkbox'),
t('Weight'),
array('data' => t('Operations'), 'colspan' => '3'),
);
$rows = array();
foreach (element_children($form) as $mlid) {
if (isset($form[$mlid]['hidden'])) {
$element = &$form[$mlid];
// Build a list of operations.
$operations = array();
foreach (element_children($element['operations']) as $op) {
$operations[] = drupal_render($element['operations'][$op]);
}
while (count($operations) < 2) {
$operations[] = '';
}
// Add special classes to be used for tabledrag.js.
$element['plid']['#attributes']['class'] = 'menu-plid';
$element['mlid']['#attributes']['class'] = 'menu-mlid';
$element['weight']['#attributes']['class'] = 'menu-weight';
// Change the parent field to a hidden. This allows any value but hides the field.
$element['plid']['#type'] = 'hidden';
$row = array();
$row[] = theme('indentation', $element['#item']['depth'] - 1) . drupal_render($element['title']);
$row[] = array('data' => drupal_render($element['hidden']), 'class' => 'checkbox');
$row[] = array('data' => drupal_render($element['expanded']), 'class' => 'checkbox');
$row[] = drupal_render($element['weight']) . drupal_render($element['plid']) . drupal_render($element['mlid']);
$row = array_merge($row, $operations);
$row = array_merge(array('data' => $row), $element['#attributes']);
$row['class'] = !empty($row['class']) ? $row['class'] .' draggable' : 'draggable';
$rows[] = $row;
}
}
$output = '';
if ($rows) {
$output .= theme('table', $header, $rows, array('id' => 'menu-overview'));
}
$output .= drupal_render($form);
return $output;
}
/**
* Menu callback; Build the menu link editing form.
*/
function menu_edit_item(&$form_state, $type, $item, $menu) {
$form['menu'] = array(
'#type' => 'fieldset',
'#title' => t('Menu settings'),
'#collapsible' => FALSE,
'#tree' => TRUE,
'#weight' => -2,
'#attributes' => array('class' => 'menu-item-form'),
'#item' => $item,
);
if ($type == 'add' || empty($item)) {
// This is an add form, initialize the menu link.
$item = array('link_title' => '', 'mlid' => 0, 'plid' => 0, 'menu_name' => $menu['menu_name'], 'weight' => 0, 'link_path' => '', 'options' => array(), 'module' => 'menu', 'expanded' => 0, 'hidden' => 0, 'has_children' => 0);
}
foreach (array('link_path', 'mlid', 'module', 'has_children', 'options') as $key) {
$form['menu'][$key] = array('#type' => 'value', '#value' => $item[$key]);
}
// Any item created or edited via this interface is considered "customized".
$form['menu']['customized'] = array('#type' => 'value', '#value' => 1);
$form['menu']['original_item'] = array('#type' => 'value', '#value' => $item);
$path = $item['link_path'];
if (isset($item['options']['query'])) {
$path .= '?'. $item['options']['query'];
}
if (isset($item['options']['fragment'])) {
$path .= '#'. $item['options']['fragment'];
}
if ($item['module'] == 'menu') {
$form['menu']['link_path'] = array(
'#type' => 'textfield',
'#title' => t('Path'),
'#default_value' => $path,
'#description' => t('The path this menu item links to. This can be an internal Drupal path such as %add-node or an external URL such as %drupal. Enter %front to link to the front page.', array('%front' => '<front>', '%add-node' => 'node/add', '%drupal' => 'http://drupal.org')),
'#required' => TRUE,
);
$form['delete'] = array(
'#type' => 'submit',
'#value' => t('Delete'),
'#access' => $item['mlid'],
'#submit' => array('menu_item_delete_submit'),
'#weight' => 10,
);
}
else {
$form['menu']['_path'] = array(
'#type' => 'item',
'#title' => t('Path'),
'#description' => l($item['link_title'], $item['href'], $item['options']),
);
}
$form['menu']['link_title'] = array('#type' => 'textfield',
'#title' => t('Menu link title'),
'#default_value' => $item['link_title'],
'#description' => t('The link text corresponding to this item that should appear in the menu.'),
'#required' => TRUE,
);
$form['menu']['description'] = array(
'#type' => 'textarea',
'#title' => t('Description'),
'#default_value' => isset($item['options']['attributes']['title']) ? $item['options']['attributes']['title'] : '',
'#rows' => 1,
'#description' => t('The description displayed when hovering over a menu item.'),
);
$form['menu']['enabled'] = array(
'#type' => 'checkbox',
'#title' => t('Enabled'),
'#default_value' => !$item['hidden'],
'#description' => t('Menu items that are not enabled will not be listed in any menu.'),
);
$form['menu']['expanded'] = array(
'#type' => 'checkbox',
'#title' => t('Expanded'),
'#default_value' => $item['expanded'],
'#description' => t('If selected and this menu item has children, the menu will always appear expanded.'),
);
// Generate a list of possible parents (not including this item or descendants).
$options = menu_parent_options(menu_get_menus(), $item);
$default = $item['menu_name'] .':'. $item['plid'];
if (!isset($options[$default])) {
$default = 'navigation:0';
}
$form['menu']['parent'] = array(
'#type' => 'select',
'#title' => t('Parent item'),
'#default_value' => $default,
'#options' => $options,
'#description' => t('The maximum depth for an item and all its children is fixed at !maxdepth. Some menu items may not be available as parents if selecting them would exceed this limit.', array('!maxdepth' => MENU_MAX_DEPTH)),
'#attributes' => array('class' => 'menu-title-select'),
);
$form['menu']['weight'] = array(
'#type' => 'weight',
'#title' => t('Weight'),
'#delta' => 50,
'#default_value' => $item['weight'],
'#description' => t('Optional. In the menu, the heavier items will sink and the lighter items will be positioned nearer the top.'),
);
$form['submit'] = array('#type' => 'submit', '#value' => t('Save'));
return $form;
}
/**
* Validate form values for a menu link being added or edited.
*/
function menu_edit_item_validate($form, &$form_state) {
$item = &$form_state['values']['menu'];
$normal_path = drupal_get_normal_path($item['link_path']);
if ($item['link_path'] != $normal_path) {
drupal_set_message(t('The menu system stores system paths only, but will use the URL alias for display. %link_path has been stored as %normal_path', array('%link_path' => $item['link_path'], '%normal_path' => $normal_path)));
$item['link_path'] = $normal_path;
}
if (!menu_path_is_external($item['link_path'])) {
$parsed_link = parse_url($item['link_path']);
if (isset($parsed_link['query'])) {
$item['options']['query'] = $parsed_link['query'];
}
else {
// Use unset() rather than setting to empty string
// to avoid redundant serialized data being stored.
unset($item['options']['query']);
}
if (isset($parsed_link['fragment'])) {
$item['options']['fragment'] = $parsed_link['fragment'];
}
else {
unset($item['options']['fragment']);
}
if ($item['link_path'] != $parsed_link['path']) {
$item['link_path'] = $parsed_link['path'];
}
}
if (!trim($item['link_path']) || !menu_valid_path($item)) {
form_set_error('link_path', t("The path '@link_path' is either invalid or you do not have access to it.", array('@link_path' => $item['link_path'])));
}
}
/**
* Submit function for the delete button on the menu item editing form.
*/
function menu_item_delete_submit($form, &$form_state) {
$form_state['redirect'] = 'admin/build/menu/item/'. $form_state['values']['menu']['mlid'] .'/delete';
}
/**
* Process menu and menu item add/edit form submissions.
*/
function menu_edit_item_submit($form, &$form_state) {
$item = &$form_state['values']['menu'];
// The value of "hidden" is the opposite of the value
// supplied by the "enabled" checkbox.
$item['hidden'] = (int) !$item['enabled'];
unset($item['enabled']);
$item['options']['attributes']['title'] = $item['description'];
list($item['menu_name'], $item['plid']) = explode(':', $item['parent']);
if (!menu_link_save($item)) {
drupal_set_message(t('There was an error saving the menu link.'), 'error');
}
$form_state['redirect'] = 'admin/build/menu-customize/'. $item['menu_name'];
}
/**
* Menu callback; Build the form that handles the adding/editing of a custom menu.
*/
function menu_edit_menu(&$form_state, $type, $menu = array()) {
if ($type == 'edit') {
$form['menu_name'] = array('#type' => 'value', '#value' => $menu['menu_name']);
$form['#insert'] = FALSE;
$form['delete'] = array(
'#type' => 'submit',
'#value' => t('Delete'),
'#access' => !in_array($menu['menu_name'], menu_list_system_menus()),
'#submit' => array('menu_custom_delete_submit'),
'#weight' => 10,
);
}
else {
$menu = array('menu_name' => '', 'title' => '', 'description' => '');
$form['menu_name'] = array(
'#type' => 'textfield',
'#title' => t('Menu name'),
'#maxlength' => MENU_MAX_MENU_NAME_LENGTH_UI,
'#description' => t('The machine-readable name of this menu. This text will be used for constructing the URL of the <em>menu overview</em> page for this menu. This name must contain only lowercase letters, numbers, and hyphens, and must be unique.'),
'#required' => TRUE,
);
$form['#insert'] = TRUE;
}
$form['#title'] = $menu['title'];
$form['title'] = array(
'#type' => 'textfield',
'#title' => t('Title'),
'#default_value' => $menu['title'],
'#required' => TRUE,
);
$form['description'] = array(
'#type' => 'textarea',
'#title' => t('Description'),
'#default_value' => $menu['description'],
);
$form['submit'] = array(
'#type' => 'submit',
'#value' => t('Save'),
);
return $form;
}
/**
* Submit function for the 'Delete' button on the menu editing form.
*/
function menu_custom_delete_submit($form, &$form_state) {
$form_state['redirect'] = 'admin/build/menu-customize/'. $form_state['values']['menu_name'] .'/delete';
}
/**
* Menu callback; check access and get a confirm form for deletion of a custom menu.
*/
function menu_delete_menu_page($menu) {
// System-defined menus may not be deleted.
if (in_array($menu['menu_name'], menu_list_system_menus())) {
drupal_access_denied();
return;
}
return drupal_get_form('menu_delete_menu_confirm', $menu);
}
/**
* Build a confirm form for deletion of a custom menu.
*/
function menu_delete_menu_confirm(&$form_state, $menu) {
$form['#menu'] = $menu;
$caption = '';
$num_links = db_result(db_query("SELECT COUNT(*) FROM {menu_links} WHERE menu_name = '%s'", $menu['menu_name']));
if ($num_links) {
$caption .= '<p>'. format_plural($num_links, '<strong>Warning:</strong> There is currently 1 menu item in %title. It will be deleted (system-defined items will be reset).', '<strong>Warning:</strong> There are currently @count menu items in %title. They will be deleted (system-defined items will be reset).', array('%title' => $menu['title'])) .'</p>';
}
$caption .= '<p>'. t('This action cannot be undone.') .'</p>';
return confirm_form($form, t('Are you sure you want to delete the custom menu %title?', array('%title' => $menu['title'])), 'admin/build/menu-customize/'. $menu['menu_name'], $caption, t('Delete'));
}
/**
* Delete a custom menu and all items in it.
*/
function menu_delete_menu_confirm_submit($form, &$form_state) {
$menu = $form['#menu'];
$form_state['redirect'] = 'admin/build/menu';
// System-defined menus may not be deleted - only menus defined by this module.
if (in_array($menu['menu_name'], menu_list_system_menus()) || !db_result(db_query("SELECT COUNT(*) FROM {menu_custom} WHERE menu_name = '%s'", $menu['menu_name']))) {
return;
}
// Reset all the menu links defined by the system via hook_menu.
$result = db_query("SELECT * FROM {menu_links} ml INNER JOIN {menu_router} m ON ml.router_path = m.path WHERE ml.menu_name = '%s' AND ml.module = 'system' ORDER BY m.number_parts ASC", $menu['menu_name']);
while ($item = db_fetch_array($result)) {
menu_reset_item($item);
}
// Delete all links to the overview page for this menu.
$result = db_query("SELECT mlid FROM {menu_links} ml WHERE ml.link_path = '%s'", 'admin/build/menu-customize/'. $menu['menu_name']);
while ($m = db_fetch_array($result)) {
menu_link_delete($m['mlid']);
}
// Delete all the links in the menu and the menu from the list of custom menus.
db_query("DELETE FROM {menu_links} WHERE menu_name = '%s'", $menu['menu_name']);
db_query("DELETE FROM {menu_custom} WHERE menu_name = '%s'", $menu['menu_name']);
// Delete all the blocks for this menu.
db_query("DELETE FROM {blocks} WHERE module = 'menu' AND delta = '%s'", $menu['menu_name']);
db_query("DELETE FROM {blocks_roles} WHERE module = 'menu' AND delta = '%s'", $menu['menu_name']);
menu_cache_clear_all();
cache_clear_all();
$t_args = array('%title' => $menu['title']);
drupal_set_message(t('The custom menu %title has been deleted.', $t_args));
watchdog('menu', 'Deleted custom menu %title and all its menu items.', $t_args, WATCHDOG_NOTICE);
}
/**
* Validates the human and machine-readable names when adding or editing a menu.
*/
function menu_edit_menu_validate($form, &$form_state) {
$item = $form_state['values'];
if (preg_match('/[^a-z0-9-]/', $item['menu_name'])) {
form_set_error('menu_name', t('The menu name may only consist of lowercase letters, numbers, and hyphens.'));
}
if ($form['#insert']) {
// We will add 'menu-' to the menu name to help avoid name-space conflicts.
$item['menu_name'] = 'menu-'. $item['menu_name'];
if (db_result(db_query("SELECT menu_name FROM {menu_custom} WHERE menu_name = '%s'", $item['menu_name'])) ||
db_result(db_query_range("SELECT menu_name FROM {menu_links} WHERE menu_name = '%s'", $item['menu_name'], 0, 1))) {
form_set_error('menu_name', t('The menu already exists.'));
}
}
}
/**
* Submit function for adding or editing a custom menu.
*/
function menu_edit_menu_submit($form, &$form_state) {
$menu = $form_state['values'];
$path = 'admin/build/menu-customize/';
if ($form['#insert']) {
// Add 'menu-' to the menu name to help avoid name-space conflicts.
$menu['menu_name'] = 'menu-'. $menu['menu_name'];
$link['link_title'] = $menu['title'];
$link['link_path'] = $path . $menu['menu_name'];
$link['router_path'] = $path .'%';
$link['module'] = 'menu';
$link['plid'] = db_result(db_query("SELECT mlid FROM {menu_links} WHERE link_path = '%s' AND module = '%s'", 'admin/build/menu', 'system'));
menu_link_save($link);
db_query("INSERT INTO {menu_custom} (menu_name, title, description) VALUES ('%s', '%s', '%s')", $menu['menu_name'], $menu['title'], $menu['description']);
}
else {
db_query("UPDATE {menu_custom} SET title = '%s', description = '%s' WHERE menu_name = '%s'", $menu['title'], $menu['description'], $menu['menu_name']);
$result = db_query("SELECT mlid FROM {menu_links} WHERE link_path = '%s'", $path . $menu['menu_name']);
while ($m = db_fetch_array($result)) {
$link = menu_link_load($m['mlid']);
$link['link_title'] = $menu['title'];
menu_link_save($link);
}
}
$form_state['redirect'] = $path . $menu['menu_name'];
}
/**
* Menu callback; Check access and present a confirm form for deleting a menu link.
*/
function menu_item_delete_page($item) {
// Links defined via hook_menu may not be deleted. Updated items are an
// exception, as they can be broken.
if ($item['module'] == 'system' && !$item['updated']) {
drupal_access_denied();
return;
}
return drupal_get_form('menu_item_delete_form', $item);
}
/**
* Build a confirm form for deletion of a single menu link.
*/
function menu_item_delete_form(&$form_state, $item) {
$form['#item'] = $item;
return confirm_form($form, t('Are you sure you want to delete the custom menu item %item?', array('%item' => $item['link_title'])), 'admin/build/menu-customize/'. $item['menu_name']);
}
/**
* Process menu delete form submissions.
*/
function menu_item_delete_form_submit($form, &$form_state) {
$item = $form['#item'];
menu_link_delete($item['mlid']);
$t_args = array('%title' => $item['link_title']);
drupal_set_message(t('The menu item %title has been deleted.', $t_args));
watchdog('menu', 'Deleted menu item %title.', $t_args, WATCHDOG_NOTICE);
$form_state['redirect'] = 'admin/build/menu-customize/'. $item['menu_name'];
}
/**
* Menu callback; reset a single modified item.
*/
function menu_reset_item_confirm(&$form_state, $item) {
$form['item'] = array('#type' => 'value', '#value' => $item);
return confirm_form($form, t('Are you sure you want to reset the item %item to its default values?', array('%item' => $item['link_title'])), 'admin/build/menu-customize/'. $item['menu_name'], t('Any customizations will be lost. This action cannot be undone.'), t('Reset'));
}
/**
* Process menu reset item form submissions.
*/
function menu_reset_item_confirm_submit($form, &$form_state) {
$item = $form_state['values']['item'];
$new_item = menu_reset_item($item);
drupal_set_message(t('The menu item was reset to its default settings.'));
$form_state['redirect'] = 'admin/build/menu-customize/'. $new_item['menu_name'];
}
/**
* Menu callback; Build the form presenting menu configuration options.
*/
function menu_configure() {
$form['intro'] = array(
'#type' => 'item',
'#value' => t('The menu module allows on-the-fly creation of menu links in the content authoring forms. The following option sets the default menu in which a new link will be added.'),
);
$menu_options = menu_get_menus();
$form['menu_default_node_menu'] = array(
'#type' => 'select',
'#title' => t('Default menu for content'),
'#default_value' => variable_get('menu_default_node_menu', 'primary-links'),
'#options' => $menu_options,
'#description' => t('Choose the menu to be the default in the menu options in the content authoring form.'),
);
$primary = variable_get('menu_primary_links_source', 'primary-links');
$primary_options = array_merge($menu_options, array('' => t('No primary links')));
$form['menu_primary_links_source'] = array(
'#type' => 'select',
'#title' => t('Source for the primary links'),
'#default_value' => $primary,
'#options' => $primary_options,
'#tree' => FALSE,
'#description' => t('Select what should be displayed as the primary links.'),
);
$secondary_options = array_merge($menu_options, array('' => t('No secondary links')));
$form["menu_secondary_links_source"] = array(
'#type' => 'select',
'#title' => t('Source for the secondary links'),
'#default_value' => variable_get('menu_secondary_links_source', 'secondary-links'),
'#options' => $secondary_options,
'#tree' => FALSE,
'#description' => t('Select what should be displayed as the secondary links. You can choose the same menu for secondary links as for primary links (currently %primary). If you do this, the children of the active primary menu link will be displayed as secondary links.', array('%primary' => $primary_options[$primary])),
);
return system_settings_form($form);
}

11
modules/menu/menu.info Normal file
View file

@ -0,0 +1,11 @@
name = Menu
description = Allows administrators to customize the site navigation menu.
package = Core - optional
version = VERSION
core = 6.x
; Information added by Drupal.org packaging script on 2016-02-24
version = "6.38"
project = "drupal"
datestamp = "1456343372"

57
modules/menu/menu.install Normal file
View file

@ -0,0 +1,57 @@
<?php
/**
* Implementation of hook_install().
*/
function menu_install() {
// Create tables.
drupal_install_schema('menu');
$t = get_t();
db_query("INSERT INTO {menu_custom} (menu_name, title, description) VALUES ('%s', '%s', '%s')", 'navigation', $t('Navigation'), $t('The navigation menu is provided by Drupal and is the main interactive menu for any site. It is usually the only menu that contains personalized links for authenticated users, and is often not even visible to anonymous users.'));
db_query("INSERT INTO {menu_custom} (menu_name, title, description) VALUES ('%s', '%s', '%s')", 'primary-links', $t('Primary links'), $t('Primary links are often used at the theme layer to show the major sections of a site. A typical representation for primary links would be tabs along the top.'));
db_query("INSERT INTO {menu_custom} (menu_name, title, description) VALUES ('%s', '%s', '%s')", 'secondary-links', $t('Secondary links'), $t('Secondary links are often used for pages like legal notices, contact details, and other secondary navigation items that play a lesser role than primary links'));
}
/**
* Implementation of hook_uninstall().
*/
function menu_uninstall() {
// Remove tables.
drupal_uninstall_schema('menu');
menu_rebuild();
}
/**
* Implementation of hook_schema().
*/
function menu_schema() {
$schema['menu_custom'] = array(
'description' => 'Holds definitions for top-level custom menus (for example, Primary Links).',
'fields' => array(
'menu_name' => array(
'type' => 'varchar',
'length' => 32,
'not null' => TRUE,
'default' => '',
'description' => 'Primary Key: Unique key for menu. This is used as a block delta so length is 32.',
),
'title' => array(
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
'default' => '',
'description' => 'Menu title; displayed at top of block.',
),
'description' => array(
'type' => 'text',
'not null' => FALSE,
'description' => 'Menu description.',
),
),
'primary key' => array('menu_name'),
);
return $schema;
}

453
modules/menu/menu.module Normal file
View file

@ -0,0 +1,453 @@
<?php
/**
* @file
* Allows administrators to customize the site navigation menu.
*/
/**
* Maximum length of menu name as entered by the user. Database length is 32
* and we add a menu- prefix.
*/
define('MENU_MAX_MENU_NAME_LENGTH_UI', 27);
/**
* Implementation of hook_help().
*/
function menu_help($path, $arg) {
switch ($path) {
case 'admin/help#menu':
$output = '<p>'. t("The menu module provides an interface to control and customize Drupal's powerful menu system. Menus are a hierarchical collection of links, or menu items, used to navigate a website, and are positioned and displayed using Drupal's flexible block system. By default, three menus are created during installation: <em>Navigation</em>, <em>Primary links</em>, and <em>Secondary links</em>. The <em>Navigation</em> menu contains most links necessary for working with and navigating your site, and is often displayed in either the left or right sidebar. Most Drupal themes also provide support for <em>Primary links</em> and <em>Secondary links</em>, by displaying them in either the header or footer of each page. By default, <em>Primary links</em> and <em>Secondary links</em> contain no menu items but may be configured to contain custom menu items specific to your site.") .'</p>';
$output .= '<p>'. t('The <a href="@menu">menus page</a> displays all menus currently available on your site. Select a menu from this list to add or edit a menu item, or to rearrange items within the menu. Create new menus using the <a href="@add-menu">add menu page</a> (the block containing a new menu must also be enabled on the <a href="@blocks">blocks administration page</a>).', array('@menu' => url('admin/build/menu'), '@add-menu' => url('admin/build/menu/add'), '@blocks' => url('admin/build/block'))) .'</p>';
$output .= '<p>'. t('For more information, see the online handbook entry for <a href="@menu">Menu module</a>.', array('@menu' => 'http://drupal.org/handbook/modules/menu/')) .'</p>';
return $output;
case 'admin/build/menu':
return '<p>'. t('Menus are a collection of links (menu items) used to navigate a website. The menus currently available on your site are displayed below. Select a menu from this list to manage its menu items.') .'</p>';
case 'admin/build/menu/add':
return '<p>'. t('Enter the name for your new menu. Remember to enable the newly created block in the <a href="@blocks">blocks administration page</a>.', array('@blocks' => url('admin/build/block'))) .'</p>';
case 'admin/build/menu-customize/%':
return '<p>'. t('To rearrange menu items, grab a drag-and-drop handle under the <em>Menu item</em> column and drag the items (or group of items) to a new location in the list. (Grab a handle by clicking and holding the mouse while hovering over a handle icon.) Remember that your changes will not be saved until you click the <em>Save configuration</em> button at the bottom of the page.') .'</p>';
case 'admin/build/menu/item/add':
return '<p>'. t('Enter the title and path for your new menu item.') .'</p>';
}
}
/**
* Implementation of hook_perm().
*/
function menu_perm() {
return array('administer menu');
}
/**
* Implementation of hook_menu().
*/
function menu_menu() {
$items['admin/build/menu'] = array(
'title' => 'Menus',
'description' => "Control your site's navigation menu, primary links and secondary links, as well as rename and reorganize menu items.",
'page callback' => 'menu_overview_page',
'access callback' => 'user_access',
'access arguments' => array('administer menu'),
'file' => 'menu.admin.inc',
);
$items['admin/build/menu/list'] = array(
'title' => 'List menus',
'type' => MENU_DEFAULT_LOCAL_TASK,
'weight' => -10,
'file' => 'menu.admin.inc',
);
$items['admin/build/menu/add'] = array(
'title' => 'Add menu',
'page callback' => 'drupal_get_form',
'page arguments' => array('menu_edit_menu', 'add'),
'access arguments' => array('administer menu'),
'type' => MENU_LOCAL_TASK,
'file' => 'menu.admin.inc',
);
$items['admin/build/menu/settings'] = array(
'title' => 'Settings',
'page callback' => 'drupal_get_form',
'page arguments' => array('menu_configure'),
'access arguments' => array('administer menu'),
'type' => MENU_LOCAL_TASK,
'weight' => 5,
'file' => 'menu.admin.inc',
);
$items['admin/build/menu-customize/%menu'] = array(
'title' => 'Customize menu',
'page callback' => 'drupal_get_form',
'page arguments' => array('menu_overview_form', 3),
'title callback' => 'menu_overview_title',
'title arguments' => array(3),
'access arguments' => array('administer menu'),
'type' => MENU_CALLBACK,
'file' => 'menu.admin.inc',
);
$items['admin/build/menu-customize/%menu/list'] = array(
'title' => 'List items',
'weight' => -10,
'type' => MENU_DEFAULT_LOCAL_TASK,
'file' => 'menu.admin.inc',
);
$items['admin/build/menu-customize/%menu/add'] = array(
'title' => 'Add item',
'page callback' => 'drupal_get_form',
'page arguments' => array('menu_edit_item', 'add', NULL, 3),
'access arguments' => array('administer menu'),
'type' => MENU_LOCAL_TASK,
'file' => 'menu.admin.inc',
);
$items['admin/build/menu-customize/%menu/edit'] = array(
'title' => 'Edit menu',
'page callback' => 'drupal_get_form',
'page arguments' => array('menu_edit_menu', 'edit', 3),
'access arguments' => array('administer menu'),
'type' => MENU_LOCAL_TASK,
'file' => 'menu.admin.inc',
);
$items['admin/build/menu-customize/%menu/delete'] = array(
'title' => 'Delete menu',
'page callback' => 'menu_delete_menu_page',
'page arguments' => array(3),
'access arguments' => array('administer menu'),
'type' => MENU_CALLBACK,
'file' => 'menu.admin.inc',
);
$items['admin/build/menu/item/%menu_link/edit'] = array(
'title' => 'Edit menu item',
'page callback' => 'drupal_get_form',
'page arguments' => array('menu_edit_item', 'edit', 4, NULL),
'access arguments' => array('administer menu'),
'type' => MENU_CALLBACK,
'file' => 'menu.admin.inc',
);
$items['admin/build/menu/item/%menu_link/reset'] = array(
'title' => 'Reset menu item',
'page callback' => 'drupal_get_form',
'page arguments' => array('menu_reset_item_confirm', 4),
'access arguments' => array('administer menu'),
'type' => MENU_CALLBACK,
'file' => 'menu.admin.inc',
);
$items['admin/build/menu/item/%menu_link/delete'] = array(
'title' => 'Delete menu item',
'page callback' => 'menu_item_delete_page',
'page arguments' => array(4),
'access arguments' => array('administer menu'),
'type' => MENU_CALLBACK,
'file' => 'menu.admin.inc',
);
return $items;
}
/**
* Implemenation of hook_theme().
*/
function menu_theme() {
return array(
'menu_overview_form' => array(
'file' => 'menu.admin.inc',
'arguments' => array('form' => NULL),
),
);
}
/**
* Implementation of hook_enable()
*
* Add a link for each custom menu.
*/
function menu_enable() {
menu_rebuild();
$base_link = db_fetch_array(db_query("SELECT mlid AS plid, menu_name from {menu_links} WHERE link_path = 'admin/build/menu' AND module = 'system'"));
$base_link['router_path'] = 'admin/build/menu-customize/%';
$base_link['module'] = 'menu';
$result = db_query("SELECT * FROM {menu_custom}");
while ($menu = db_fetch_array($result)) {
// $link is passed by reference to menu_link_save(), so we make a copy of $base_link.
$link = $base_link;
$link['mlid'] = 0;
$link['link_title'] = $menu['title'];
$link['link_path'] = 'admin/build/menu-customize/'. $menu['menu_name'];
if (!db_result(db_query("SELECT mlid FROM {menu_links} WHERE link_path = '%s' AND plid = %d", $link['link_path'], $link['plid']))) {
menu_link_save($link);
}
}
menu_cache_clear_all();
}
/**
* Title callback for the menu overview page and links.
*/
function menu_overview_title($menu) {
return $menu['title'];
}
/**
* Load the data for a single custom menu.
*/
function menu_load($menu_name) {
return db_fetch_array(db_query("SELECT * FROM {menu_custom} WHERE menu_name = '%s'", $menu_name));
}
/**
* Return a list of menu items that are valid possible parents for the given menu item.
*
* @param $menus
* An array of menu names and titles, such as from menu_get_menus().
* @param $item
* The menu item for which to generate a list of parents.
* If $item['mlid'] == 0 then the complete tree is returned.
* @return
* An array of menu link titles keyed on the a string containing the menu name
* and mlid. The list excludes the given item and its children.
*/
function menu_parent_options($menus, $item) {
// The menu_links table can be practically any size and we need a way to
// allow contrib modules to provide more scalable pattern choosers.
// hook_form_alter is too late in itself because all the possible parents are
// retrieved here, unless menu_override_parent_selector is set to TRUE.
if (variable_get('menu_override_parent_selector', FALSE)) {
return array();
}
// If the item has children, there is an added limit to the depth of valid parents.
if (isset($item['parent_depth_limit'])) {
$limit = $item['parent_depth_limit'];
}
else {
$limit = _menu_parent_depth_limit($item);
}
foreach ($menus as $menu_name => $title) {
$tree = menu_tree_all_data($menu_name, NULL);
$options[$menu_name .':0'] = '<'. $title .'>';
_menu_parents_recurse($tree, $menu_name, '--', $options, $item['mlid'], $limit);
}
return $options;
}
/**
* Recursive helper function for menu_parent_options().
*/
function _menu_parents_recurse($tree, $menu_name, $indent, &$options, $exclude, $depth_limit) {
foreach ($tree as $data) {
if ($data['link']['depth'] > $depth_limit) {
// Don't iterate through any links on this level.
break;
}
if ($data['link']['mlid'] != $exclude && $data['link']['hidden'] >= 0) {
$title = $indent .' '. truncate_utf8($data['link']['title'], 30, TRUE, FALSE);
if ($data['link']['hidden']) {
$title .= ' ('. t('disabled') .')';
}
$options[$menu_name .':'. $data['link']['mlid']] = $title;
if ($data['below']) {
_menu_parents_recurse($data['below'], $menu_name, $indent .'--', $options, $exclude, $depth_limit);
}
}
}
}
/**
* Reset a system-defined menu item.
*/
function menu_reset_item($item) {
$new_item = _menu_link_build(menu_get_item($item['router_path']));
foreach (array('mlid', 'has_children') as $key) {
$new_item[$key] = $item[$key];
}
menu_link_save($new_item);
return $new_item;
}
/**
* Implementation of hook_block().
*/
function menu_block($op = 'list', $delta = 0) {
$menus = menu_get_menus();
// The Navigation menu is handled by the user module.
unset($menus['navigation']);
if ($op == 'list') {
$blocks = array();
foreach ($menus as $name => $title) {
$blocks[$name]['info'] = check_plain($title);
// Menu blocks can't be cached because each menu item can have
// a custom access callback. menu.inc manages its own caching.
$blocks[$name]['cache'] = BLOCK_NO_CACHE;
}
return $blocks;
}
else if ($op == 'view') {
$data['subject'] = check_plain($menus[$delta]);
$data['content'] = menu_tree($delta);
return $data;
}
}
/**
* Implementation of hook_nodeapi().
*/
function menu_nodeapi(&$node, $op) {
switch ($op) {
case 'insert':
case 'update':
if (isset($node->menu)) {
$item = &$node->menu;
if (!empty($item['delete'])) {
menu_link_delete($item['mlid']);
}
elseif (trim($item['link_title'])) {
$item['link_title'] = trim($item['link_title']);
$item['link_path'] = "node/$node->nid";
if (!$item['customized']) {
$item['options']['attributes']['title'] = trim($node->title);
}
if (!menu_link_save($item)) {
drupal_set_message(t('There was an error saving the menu link.'), 'error');
}
}
}
break;
case 'delete':
// Delete all menu module links that point to this node.
$result = db_query("SELECT mlid FROM {menu_links} WHERE link_path = 'node/%d' AND module = 'menu'", $node->nid);
while ($m = db_fetch_array($result)) {
menu_link_delete($m['mlid']);
}
break;
case 'prepare':
if (empty($node->menu)) {
// Prepare the node for the edit form so that $node->menu always exists.
$menu_name = variable_get('menu_default_node_menu', 'primary-links');
$item = array();
if (isset($node->nid)) {
// Give priority to the default menu
$mlid = db_result(db_query_range("SELECT mlid FROM {menu_links} WHERE link_path = 'node/%d' AND menu_name = '%s' AND module = 'menu' ORDER BY mlid ASC", $node->nid, $menu_name, 0, 1));
// Check all menus if a link does not exist in the default menu.
if (!$mlid) {
$mlid = db_result(db_query_range("SELECT mlid FROM {menu_links} WHERE link_path = 'node/%d' AND module = 'menu' ORDER BY mlid ASC", $node->nid, 0, 1));
}
if ($mlid) {
$item = menu_link_load($mlid);
}
}
// Set default values.
$node->menu = $item + array('link_title' => '', 'mlid' => 0, 'plid' => 0, 'menu_name' => $menu_name, 'weight' => 0, 'options' => array(), 'module' => 'menu', 'expanded' => 0, 'hidden' => 0, 'has_children' => 0, 'customized' => 0);
}
// Find the depth limit for the parent select.
if (!isset($node->menu['parent_depth_limit'])) {
$node->menu['parent_depth_limit'] = _menu_parent_depth_limit($node->menu);
}
break;
}
}
/**
* Find the depth limit for items in the parent select.
*/
function _menu_parent_depth_limit($item) {
return MENU_MAX_DEPTH - 1 - (($item['mlid'] && $item['has_children']) ? menu_link_children_relative_depth($item) : 0);
}
/**
* Implementation of hook_form_alter(). Adds menu item fields to the node form.
*/
function menu_form_alter(&$form, $form_state, $form_id) {
if (isset($form['#node']) && $form['#node']->type .'_node_form' == $form_id) {
// Note - doing this to make sure the delete checkbox stays in the form.
$form['#cache'] = TRUE;
$form['menu'] = array(
'#type' => 'fieldset',
'#title' => t('Menu settings'),
'#access' => user_access('administer menu'),
'#collapsible' => TRUE,
'#collapsed' => FALSE,
'#tree' => TRUE,
'#weight' => -2,
'#attributes' => array('class' => 'menu-item-form'),
);
$item = $form['#node']->menu;
if ($item['mlid']) {
// There is an existing link.
$form['menu']['delete'] = array(
'#type' => 'checkbox',
'#title' => t('Delete this menu item.'),
);
}
if (!$item['link_title']) {
$form['menu']['#collapsed'] = TRUE;
}
foreach (array('mlid', 'module', 'hidden', 'has_children', 'customized', 'options', 'expanded', 'hidden', 'parent_depth_limit') as $key) {
$form['menu'][$key] = array('#type' => 'value', '#value' => $item[$key]);
}
$form['menu']['#item'] = $item;
$form['menu']['link_title'] = array('#type' => 'textfield',
'#title' => t('Menu link title'),
'#default_value' => $item['link_title'],
'#description' => t('The link text corresponding to this item that should appear in the menu. Leave blank if you do not wish to add this post to the menu.'),
'#required' => FALSE,
);
// Generate a list of possible parents (not including this item or descendants).
$options = menu_parent_options(menu_get_menus(), $item);
$default = $item['menu_name'] .':'. $item['plid'];
if (!isset($options[$default])) {
$default = 'primary-links:0';
}
$form['menu']['parent'] = array(
'#type' => 'select',
'#title' => t('Parent item'),
'#default_value' => $default,
'#options' => $options,
'#description' => t('The maximum depth for an item and all its children is fixed at !maxdepth. Some menu items may not be available as parents if selecting them would exceed this limit.', array('!maxdepth' => MENU_MAX_DEPTH)),
'#attributes' => array('class' => 'menu-title-select'),
);
$form['#submit'][] = 'menu_node_form_submit';
$form['menu']['weight'] = array(
'#type' => 'weight',
'#title' => t('Weight'),
'#delta' => 50,
'#default_value' => $item['weight'],
'#description' => t('Optional. In the menu, the heavier items will sink and the lighter items will be positioned nearer the top.'),
);
}
}
/**
* Decompose the selected menu parent option into the menu_name and plid.
*/
function menu_node_form_submit($form, &$form_state) {
list($form_state['values']['menu']['menu_name'], $form_state['values']['menu']['plid']) = explode(':', $form_state['values']['menu']['parent']);
}
/**
* Return an associative array of the custom menus names.
*
* @param $all
* If FALSE return only user-added menus, or if TRUE also include
* the menus defined by the system.
* @return
* An array with the machine-readable names as the keys, and human-readable
* titles as the values.
*/
function menu_get_menus($all = TRUE) {
$system_menus = menu_list_system_menus();
$sql = 'SELECT * FROM {menu_custom}';
if (!$all) {
$sql .= ' WHERE menu_name NOT IN ('. implode(',', array_fill(0, count($system_menus), "'%s'")) .')';
}
$sql .= ' ORDER BY title';
$result = db_query($sql, $system_menus);
$rows = array();
while ($r = db_fetch_array($result)) {
$rows[$r['menu_name']] = $r['title'];
}
return $rows;
}

View file

@ -0,0 +1,410 @@
<?php
/**
* @file
* Content type editing UI.
*/
/**
* Displays the content type admin overview page.
*/
function node_overview_types() {
$types = node_get_types();
$names = node_get_types('names');
$header = array(t('Name'), t('Type'), t('Description'), array('data' => t('Operations'), 'colspan' => '2'));
$rows = array();
foreach ($names as $key => $name) {
$type = $types[$key];
if (node_hook($type, 'form')) {
$type_url_str = str_replace('_', '-', $type->type);
$row = array(
l($name, 'admin/content/node-type/'. $type_url_str),
check_plain($type->type),
filter_xss_admin($type->description),
);
// Set the edit column.
$row[] = array('data' => l(t('edit'), 'admin/content/node-type/'. $type_url_str));
// Set the delete column.
if ($type->custom) {
$row[] = array('data' => l(t('delete'), 'admin/content/node-type/'. $type_url_str .'/delete'));
}
else {
$row[] = array('data' => '');
}
$rows[] = $row;
}
}
if (empty($rows)) {
$rows[] = array(array('data' => t('No content types available.'), 'colspan' => '5', 'class' => 'message'));
}
return theme('table', $header, $rows);
}
/**
* Generates the node type editing form.
*/
function node_type_form(&$form_state, $type = NULL) {
if (!isset($type->type)) {
$type = new stdClass();
$type->type = $type->name = $type->module = $type->description = $type->help = '';
$type->min_word_count = 0;
$type->has_title = TRUE;
$type->has_body = TRUE;
$type->title_label = t('Title');
$type->body_label = t('Body');
$type->custom = TRUE;
$type->modified = FALSE;
$type->locked = FALSE;
}
$form['#node_type'] = $type; // Make the type object available to implementations of hook_form_alter.
$form['identity'] = array(
'#type' => 'fieldset',
'#title' => t('Identification'),
);
$form['identity']['name'] = array(
'#title' => t('Name'),
'#type' => 'textfield',
'#default_value' => $type->name,
'#description' => t('The human-readable name of this content type. This text will be displayed as part of the list on the <em>create content</em> page. It is recommended that this name begin with a capital letter and contain only letters, numbers, and <strong>spaces</strong>. This name must be unique.'),
'#required' => TRUE,
);
if (!$type->locked) {
$form['identity']['type'] = array(
'#title' => t('Type'),
'#type' => 'textfield',
'#default_value' => $type->type,
'#maxlength' => 32,
'#required' => TRUE,
'#description' => t('The machine-readable name of this content type. This text will be used for constructing the URL of the <em>create content</em> page for this content type. This name must contain only lowercase letters, numbers, and underscores. Underscores will be converted into hyphens when constructing the URL of the <em>create content</em> page. This name must be unique.'),
);
}
else {
$form['identity']['type'] = array(
'#type' => 'value',
'#value' => $type->type,
);
$form['identity']['type_display'] = array(
'#title' => t('Type'),
'#type' => 'item',
'#value' => theme('placeholder', $type->type),
'#description' => t('The machine-readable name of this content type. This field cannot be modified for system-defined content types.'),
);
}
$form['identity']['description'] = array(
'#title' => t('Description'),
'#type' => 'textarea',
'#default_value' => $type->description,
'#description' => t('A brief description of this content type. This text will be displayed as part of the list on the <em>create content</em> page.'),
);
$form['submission'] = array(
'#type' => 'fieldset',
'#title' => t('Submission form settings'),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
);
$form['submission']['title_label'] = array(
'#title' => t('Title field label'),
'#type' => 'textfield',
'#default_value' => $type->title_label,
'#required' => TRUE,
);
if (!$type->has_title) {
// Avoid overwriting a content type that intentionally does not have a
// title field.
$form['submission']['title_label']['#attributes'] = array('disabled' => 'disabled');
$form['submission']['title_label']['#description'] = t('This content type does not have a title field.');
$form['submission']['title_label']['#required'] = FALSE;
}
$form['submission']['body_label'] = array(
'#title' => t('Body field label'),
'#type' => 'textfield',
'#default_value' => isset($type->body_label) ? $type->body_label : '',
'#description' => t('To omit the body field for this content type, remove any text and leave this field blank.'),
);
$form['submission']['min_word_count'] = array(
'#type' => 'select',
'#title' => t('Minimum number of words'),
'#default_value' => $type->min_word_count,
'#options' => drupal_map_assoc(array(0, 1, 10, 25, 50, 75, 100, 125, 150, 175, 200)),
'#description' => t('The minimum number of words for the body field to be considered valid for this content type. This can be useful to rule out submissions that do not meet the site\'s standards, such as short test posts.')
);
$form['submission']['help'] = array(
'#type' => 'textarea',
'#title' => t('Explanation or submission guidelines'),
'#default_value' => $type->help,
'#description' => t('This text will be displayed at the top of the submission form for this content type. It is useful for helping or instructing your users.')
);
$form['workflow'] = array(
'#type' => 'fieldset',
'#title' => t('Workflow settings'),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
);
$form['workflow']['node_options'] = array('#type' => 'checkboxes',
'#title' => t('Default options'),
'#default_value' => variable_get('node_options_'. $type->type, array('status', 'promote')),
'#options' => array(
'status' => t('Published'),
'promote' => t('Promoted to front page'),
'sticky' => t('Sticky at top of lists'),
'revision' => t('Create new revision'),
),
'#description' => t('Users with the <em>administer nodes</em> permission will be able to override these options.'),
);
$form['old_type'] = array(
'#type' => 'value',
'#value' => $type->type,
);
$form['orig_type'] = array(
'#type' => 'value',
'#value' => isset($type->orig_type) ? $type->orig_type : '',
);
$form['module'] = array(
'#type' => 'value',
'#value' => $type->module,
);
$form['custom'] = array(
'#type' => 'value',
'#value' => $type->custom,
);
$form['modified'] = array(
'#type' => 'value',
'#value' => $type->modified,
);
$form['locked'] = array(
'#type' => 'value',
'#value' => $type->locked,
);
$form['submit'] = array(
'#type' => 'submit',
'#value' => t('Save content type'),
'#weight' => 40,
);
if ($type->custom) {
if (!empty($type->type)) {
$form['delete'] = array(
'#type' => 'submit',
'#value' => t('Delete content type'),
'#weight' => 45,
);
}
}
else {
$form['reset'] = array(
'#type' => 'submit',
'#value' => t('Reset to defaults'),
'#weight' => 50,
);
}
return $form;
}
/**
* Validates the content type submission form generated by node_type_form().
*/
function node_type_form_validate($form, &$form_state) {
$type = new stdClass();
$type->type = trim($form_state['values']['type']);
$type->name = trim($form_state['values']['name']);
// Work out what the type was before the user submitted this form
$old_type = trim($form_state['values']['old_type']);
$types = node_get_types('names');
if (!$form_state['values']['locked']) {
if (isset($types[$type->type]) && $type->type != $old_type) {
form_set_error('type', t('The machine-readable name %type is already taken.', array('%type' => $type->type)));
}
if (!preg_match('!^[a-z0-9_]+$!', $type->type)) {
form_set_error('type', t('The machine-readable name must contain only lowercase letters, numbers, and underscores.'));
}
// 'theme' conflicts with theme_node_form().
// '0' is invalid, since elsewhere we check it using empty().
if (in_array($type->type, array('0', 'theme'))) {
form_set_error('type', t("Invalid machine-readable name. Please enter a name other than %invalid.", array('%invalid' => $type->type)));
}
}
$names = array_flip($types);
if (isset($names[$type->name]) && $names[$type->name] != $old_type) {
form_set_error('name', t('The human-readable name %name is already taken.', array('%name' => $type->name)));
}
}
/**
* Implementation of hook_form_submit().
*/
function node_type_form_submit($form, &$form_state) {
$op = isset($form_state['values']['op']) ? $form_state['values']['op'] : '';
$type = new stdClass();
$type->type = trim($form_state['values']['type']);
$type->name = trim($form_state['values']['name']);
$type->orig_type = trim($form_state['values']['orig_type']);
$type->old_type = isset($form_state['values']['old_type']) ? $form_state['values']['old_type'] : $type->type;
$type->description = $form_state['values']['description'];
$type->help = $form_state['values']['help'];
$type->min_word_count = $form_state['values']['min_word_count'];
$type->title_label = $form_state['values']['title_label'];
$type->body_label = $form_state['values']['body_label'];
// title_label is required in core; has_title will always be true, unless a
// module alters the title field.
$type->has_title = ($type->title_label != '');
$type->has_body = ($type->body_label != '');
$type->module = !empty($form_state['values']['module']) ? $form_state['values']['module'] : 'node';
$type->custom = $form_state['values']['custom'];
$type->modified = TRUE;
$type->locked = $form_state['values']['locked'];
if ($op == t('Reset to defaults')) {
node_type_reset($type);
}
elseif ($op == t('Delete content type')) {
$form_state['redirect'] = 'admin/content/node-type/'. str_replace('_', '-', $type->old_type) .'/delete';
return;
}
$status = node_type_save($type);
$variables = $form_state['values'];
// Remove everything that's been saved already - whatever's left is assumed
// to be a persistent variable.
foreach ($variables as $key => $value) {
if (isset($type->$key)) {
unset($variables[$key]);
}
}
unset($variables['form_token'], $variables['op'], $variables['submit'], $variables['delete'], $variables['reset'], $variables['form_id']);
// Save or reset persistent variable values.
foreach ($variables as $key => $value) {
$variable_new = $key .'_'. $type->type;
$variable_old = $key .'_'. $type->old_type;
if ($op == t('Reset to defaults')) {
variable_del($variable_old);
}
else {
if (is_array($value)) {
$value = array_keys(array_filter($value));
}
variable_set($variable_new, $value);
if ($variable_new != $variable_old) {
variable_del($variable_old);
}
}
}
node_types_rebuild();
menu_rebuild();
$t_args = array('%name' => $type->name);
if ($op == t('Reset to defaults')) {
drupal_set_message(t('The content type %name has been reset to its default values.', $t_args));
return;
}
if ($status == SAVED_UPDATED) {
drupal_set_message(t('The content type %name has been updated.', $t_args));
}
elseif ($status == SAVED_NEW) {
drupal_set_message(t('The content type %name has been added.', $t_args));
watchdog('node', 'Added content type %name.', $t_args, WATCHDOG_NOTICE, l(t('view'), 'admin/content/types'));
}
$form_state['redirect'] = 'admin/content/types';
return;
}
/**
* Implementation of hook_node_type().
*/
function node_node_type($op, $info) {
if ($op != 'delete' && !empty($info->old_type) && $info->old_type != $info->type) {
$update_count = node_type_update_nodes($info->old_type, $info->type);
if ($update_count) {
drupal_set_message(format_plural($update_count, 'Changed the content type of 1 post from %old-type to %type.', 'Changed the content type of @count posts from %old-type to %type.', array('%old-type' => $info->old_type, '%type' => $info->type)));
}
}
}
/**
* Resets all of the relevant fields of a module-defined node type to their
* default values.
*
* @param &$type
* The node type to reset. The node type is passed back by reference with its
* resetted values. If there is no module-defined info for this node type,
* then nothing happens.
*/
function node_type_reset(&$type) {
$info_array = module_invoke_all('node_info');
if (isset($info_array[$type->orig_type])) {
$info_array[$type->orig_type]['type'] = $type->orig_type;
$info = _node_type_set_defaults($info_array[$type->orig_type]);
foreach ($info as $field => $value) {
$type->$field = $value;
}
}
}
/**
* Menu callback; delete a single content type.
*/
function node_type_delete_confirm(&$form_state, $type) {
$form['type'] = array('#type' => 'value', '#value' => $type->type);
$form['name'] = array('#type' => 'value', '#value' => $type->name);
$message = t('Are you sure you want to delete the content type %type?', array('%type' => $type->name));
$caption = '';
$num_nodes = db_result(db_query("SELECT COUNT(*) FROM {node} WHERE type = '%s'", $type->type));
if ($num_nodes) {
$caption .= '<p>'. format_plural($num_nodes, '<strong>Warning:</strong> there is currently 1 %type post on your site. It may not be able to be displayed or edited correctly, once you have removed this content type.', '<strong>Warning:</strong> there are currently @count %type posts on your site. They may not be able to be displayed or edited correctly, once you have removed this content type.', array('%type' => $type->name)) .'</p>';
}
$caption .= '<p>'. t('This action cannot be undone.') .'</p>';
return confirm_form($form, $message, 'admin/content/types', $caption, t('Delete'));
}
/**
* Process content type delete confirm submissions.
*/
function node_type_delete_confirm_submit($form, &$form_state) {
node_type_delete($form_state['values']['type']);
$t_args = array('%name' => $form_state['values']['name']);
drupal_set_message(t('The content type %name has been deleted.', $t_args));
watchdog('menu', 'Deleted content type %name.', $t_args, WATCHDOG_NOTICE);
node_types_rebuild();
menu_rebuild();
$form_state['redirect'] = 'admin/content/types';
return;
}

Some files were not shown because too many files have changed in this diff Show more