From: Thierry Parmentelat
For more help, see the Installation and upgrading handbook. If you are unsure what these terms mean you should probably contact your hosting provider.
'); + exit; + } + + $url = db_parse_url($url); + + // Decode url-encoded information in the db connection string + $url['user'] = urldecode($url['user']); + $url['pass'] = urldecode($url['pass']); + $url['host'] = urldecode($url['host']); + $url['path'] = urldecode($url['path']); + + // Allow for non-standard MySQL port. + if (isset($url['port'])) { + $url['host'] = $url['host'] .':'. $url['port']; + } + + // - TRUE makes mysql_connect() always open a new link, even if + // mysql_connect() was called before with the same parameters. + // This is important if you are using two databases on the same + // server. + // - 2 means CLIENT_FOUND_ROWS: return the number of found + // (matched) rows, not the number of affected rows. + $connection = @mysql_connect($url['host'], $url['user'], $url['pass'], TRUE, 2); + if (!$connection) { + drupal_maintenance_theme(); + drupal_set_title('Unable to connect to database server'); + print theme('maintenance_page', 'This either means that the username and password information in your settings.php
file is incorrect or we can\'t contact the MySQL database server. This could mean your hosting provider\'s database server is down.
The MySQL error was: '. theme('placeholder', mysql_error()) .'.
+Currently, the username is '. theme('placeholder', $url['user']) .' and the database server is '. theme('placeholder', $url['host']) .'.
+For more help, see the Installation and upgrading handbook. If you are unsure what these terms mean you should probably contact your hosting provider.
'); + exit; + } + + if (!mysql_select_db(substr($url['path'], 1))) { + drupal_maintenance_theme(); + drupal_set_title('Unable to select database'); + print theme('maintenance_page', 'We were able to connect to the MySQL database server (which means your username and password are okay) but not able to select the database.
+The MySQL error was: '. theme('placeholder', mysql_error($connection)) .'.
+Currently, the database is '. theme('placeholder', substr($url['path'], 1)) .'. The username is '. theme('placeholder', $url['user']) .' and the database server is '. theme('placeholder', $url['host']) .'.
+For more help, see the Installation and upgrading handbook. If you are unsure what these terms mean you should probably contact your hosting provider.
'); + exit; + } + + /* On MySQL 4.1 and later, force UTF-8 */ + if (version_compare(mysql_get_server_info(), '4.1.0', '>=')) { + mysql_query('SET NAMES "utf8"', $connection); + } + return $connection; +} + +/** + * Helper function for db_query(). + */ +function _db_query($query, $debug = 0) { + global $active_db, $queries; + + if (variable_get('dev_query', 0)) { + list($usec, $sec) = explode(' ', microtime()); + $timer = (float)$usec + (float)$sec; + } + + $result = mysql_query($query, $active_db); + + if (variable_get('dev_query', 0)) { + $bt = debug_backtrace(); + $query = $bt[2]['function'] . "\n" . $query; + list($usec, $sec) = explode(' ', microtime()); + $stop = (float)$usec + (float)$sec; + $diff = $stop - $timer; + $queries[] = array($query, $diff); + } + + if ($debug) { + print 'query: '. $query .'
error:'. mysql_error($active_db) .'
PHP.ini
to see how you can enable it.
+For more help, see the Installation and upgrading handbook. If you are unsure what these terms mean you should probably contact your hosting provider.
'); + exit; + } + + $url = parse_url($url); + + // Decode url-encoded information in the db connection string + $url['user'] = urldecode($url['user']); + $url['pass'] = urldecode($url['pass']); + $url['host'] = urldecode($url['host']); + $url['path'] = urldecode($url['path']); + + $connection = mysqli_init(); + @mysqli_real_connect($connection, $url['host'], $url['user'], $url['pass'], substr($url['path'], 1), $url['port'], NULL, MYSQLI_CLIENT_FOUND_ROWS); + + // Find all database connection errors and error 1045 for access denied for user account + if (mysqli_connect_errno() >= 2000 || mysqli_connect_errno() == 1045) { + drupal_maintenance_theme(); + drupal_set_title('Unable to connect to database server'); + print theme('maintenance_page', 'This either means that the username and password information in your settings.php
file is incorrect or we can\'t contact the MySQL database server through the mysqli libraries. This could also mean your hosting provider\'s database server is down.
The MySQL error was: '. theme('placeholder', mysqli_error($connection)) .'.
+Currently, the username is '. theme('placeholder', $url['user']) .' and the database server is '. theme('placeholder', $url['host']) .'.
+settings.php
configuration file in Drupal.For more help, see the Installation and upgrading handbook. If you are unsure what these terms mean you should probably contact your hosting provider.
'); + exit; + } + else if (mysqli_connect_errno() > 0) { + drupal_maintenance_theme(); + drupal_set_title('Unable to select database'); + print theme('maintenance_page', 'We were able to connect to the MySQL database server (which means your username and password are okay) but not able to select the database.
+The MySQL error was: '. theme('placeholder', mysqli_error($connection)) .'.
+Currently, the database is '. theme('placeholder', substr($url['path'], 1)) .'. The username is '. theme('placeholder', $url['user']) .' and the database server is '. theme('placeholder', $url['host']) .'.
+For more help, see the Installation and upgrading handbook. If you are unsure what these terms mean you should probably contact your hosting provider.
'); + exit; + } + + /* Force UTF-8 */ + mysqli_query($connection, 'SET NAMES "utf8"'); + + /** + * from: http://bugs.php.net/bug.php?id=33772 + * Write and Close handlers are called after destructing objects since PHP + * 5.0.5. Thus destructors can use sessions but session handler can't use + * objects. In prior versions, they were called in the opposite order. It + * is possible to call session_write_close() from the destructor to solve + * this chicken and egg problem. + */ + register_shutdown_function('session_write_close'); + + return $connection; +} + +/** + * Helper function for db_query(). + */ +function _db_query($query, $debug = 0) { + global $active_db, $queries; + + if (variable_get('dev_query', 0)) { + list($usec, $sec) = explode(' ', microtime()); + $timer = (float)$usec + (float)$sec; + } + + $result = mysqli_query($active_db, $query); + + if (variable_get('dev_query', 0)) { + $bt = debug_backtrace(); + $query = $bt[2]['function'] . "\n" . $query; + list($usec, $sec) = explode(' ', microtime()); + $stop = (float)$usec + (float)$sec; + $diff = $stop - $timer; + $queries[] = array($query, $diff); + } + + if ($debug) { + print 'query: '. $query .'
error:'. mysqli_error($active_db) .'
PHP.ini
to see how you can enable it.
+For more help, see the Installation and upgrading handbook. If you are unsure what these terms mean you should probably contact your hosting provider.
'); + exit; + } + + // Cannot parse passwords with @ in them + $url = db_parse_url($url); + + // Decode url-encoded information in the db connection string + $url['user'] = urldecode($url['user']); + $url['pass'] = urldecode($url['pass']); + $url['host'] = urldecode($url['host']); + $url['path'] = urldecode($url['path']); + + $conn_string = ' user='. $url['user'] .' dbname='. substr($url['path'], 1) .' password='. $url['pass'] . ' host=' . $url['host']; + $conn_string .= isset($url['port']) ? ' port=' . $url['port'] : ''; + + // pg_last_error() does not return a useful error message for database + // connection errors. We must turn on error tracking to get at a good error + // message, which will be stored in $php_errormsg. + $track_errors_previous = ini_get('track_errors'); + ini_set('track_errors', 1); + + $connection = @pg_connect($conn_string); + if (!$connection) { + drupal_maintenance_theme(); + drupal_set_title('Unable to connect to database'); + print theme('maintenance_page', 'This either means that the database information in your settings.php
file is incorrect or we can\'t contact the PostgreSQL database server. This could mean your hosting provider\'s database server is down.
The PostgreSQL error was: '. theme('placeholder', decode_entities($php_errormsg)) .'
+Currently, the database is '. theme('placeholder', substr($url['path'], 1)) .', the username is '. theme('placeholder', $url['user']) .', and the database server is '. theme('placeholder', $url['host']) .'.
+For more help, see the Installation and upgrading handbook. If you are unsure what these terms mean you should probably contact your hosting provider.
'); + exit; + } + + // Restore error tracking setting + ini_set('track_errors', $track_errors_previous); + + return $connection; +} + +/** + * Helper function for db_query(). + */ +function _db_query($query, $debug = 0) { + global $active_db, $last_result, $queries; + + if (variable_get('dev_query', 0)) { + list($usec, $sec) = explode(' ', microtime()); + $timer = (float)$usec + (float)$sec; + } + + $last_result = pg_query($active_db, $query); + + if (variable_get('dev_query', 0)) { + $bt = debug_backtrace(); + $query = $bt[2]['function'] . "\n" . $query; + list($usec, $sec) = explode(' ', microtime()); + $stop = (float)$usec + (float)$sec; + $diff = $stop - $timer; + $queries[] = array($query, $diff); + } + + if ($debug) { + print 'query: '. $query .'
error:'. pg_last_error($active_db) .'
%htaccess
", array('%directory' => theme('placeholder', $directory), '%htaccess' => ''; + + $output .= theme('blocks', 'all'); + $output .= ' | ';
+
+ $output .= theme('breadcrumb', drupal_get_breadcrumb());
+ $output .= '' . drupal_get_title() . ''; + + if ($tabs = theme('menu_local_tasks')) { + $output .= $tabs; + } + + $output .= theme('help'); + + $output .= theme('status_messages'); + + $output .= "\n\n"; + $output .= $content; + $output .= "\n\n"; + + $output .= ' |
'. t('Users can view the latest news chronologically in the main news aggregator display or by source. Administrators can add, edit and delete feeds and choose how often to check for newly updated news for each individual feed. Administrators can also tag individual feeds with categories, offering selective grouping of some feeds into separate displays. Listings of the latest news for individual sources or categorized sources can be enabled as blocks for display in the sidebar through the block administration page. The news aggregator requires cron to check for the latest news from the sites to which you have subscribed. Drupal also provides a machine-readable OPML file of all of your subscribed feeds.', array('%aggregator' => url('aggregator'), '%aggregator-sources' => url('aggregator/sources'), '%admin-block' => url('admin/block'), '%aggregator-opml' => url('aggregator/opml'))) .'
'; + $output .= t('You can
+'. t('For more information please read the configuration and customization handbook Aggregator page.', array('%aggregator' => 'http://drupal.org/handbook/modules/aggregator/')) .'
'; + return $output; + case 'admin/modules#description': + return t('Aggregates syndicated content (RSS, RDF, and Atom feeds).'); + case 'admin/aggregator': + return t('Thousands of sites (particularly news sites and weblogs) publish their latest headlines and/or stories in a machine-readable format so that other sites can easily link to them. This content is usually in the form of an RSS feed (which is an XML-based syndication standard). To display the feed or category in a block you must decide how many items to show by editing the feed or block and turning on the feed\'s block.
', array('%block' => url('admin/block'))); + case 'admin/aggregator/add/feed': + return t('Add a site that has an RSS/RDF/Atom feed. The URL is the full path to the feed file. For the feed to update automatically you must run "cron.php" on a regular basis. If you already have a feed with the URL you are planning to use, the system will not accept another feed with the same URL.
'); + case 'admin/aggregator/add/category': + return t('Categories provide a way to group items from different news feeds together. Each news category has its own feed page and block. For example, you could tag various sport-related feeds as belonging to a category called Sports. News items can be added to a category automatically by setting a feed to automatically place its item into that category, or by using the categorize items link in any listing of news items.
'); + } +} + +/** + * Implementation of hook_menu(). + */ +function aggregator_menu($may_cache) { + $items = array(); + $edit = user_access('administer news feeds'); + $view = user_access('access news feeds'); + + if ($may_cache) { + $items[] = array('path' => 'admin/aggregator', + 'title' => t('aggregator'), + 'callback' => 'aggregator_admin_overview', + 'access' => $edit); + $items[] = array('path' => 'admin/aggregator/add/feed', + 'title' => t('add feed'), + 'callback' => 'aggregator_form_feed', + 'access' => $edit, + 'type' => MENU_LOCAL_TASK); + $items[] = array('path' => 'admin/aggregator/add/category', + 'title' => t('add category'), + 'callback' => 'aggregator_form_category', + 'access' => $edit, + 'type' => MENU_LOCAL_TASK); + $items[] = array('path' => 'admin/aggregator/remove', + 'title' => t('remove items'), + 'callback' => 'aggregator_admin_remove_feed', + 'access' => $edit, + 'type' => MENU_CALLBACK); + $items[] = array('path' => 'admin/aggregator/update', + 'title' => t('update items'), + 'callback' => 'aggregator_admin_refresh_feed', + 'access' => $edit, + 'type' => MENU_CALLBACK); + $items[] = array('path' => 'admin/aggregator/list', + 'title' => t('list'), + 'type' => MENU_DEFAULT_LOCAL_TASK, + 'weight' => -10); + + $items[] = array('path' => 'aggregator', + 'title' => t('news aggregator'), + 'callback' => 'aggregator_page_last', + 'access' => $view, + 'weight' => 5); + $items[] = array('path' => 'aggregator/sources', + 'title' => t('sources'), + 'callback' => 'aggregator_page_sources', + 'access' => $view); + $items[] = array('path' => 'aggregator/categories', + 'title' => t('categories'), + 'callback' => 'aggregator_page_categories', + 'access' => $view, + 'type' => MENU_ITEM_GROUPING); + $items[] = array('path' => 'aggregator/rss', + 'title' => t('RSS feed'), + 'callback' => 'aggregator_page_rss', + 'access' => $view, + 'type' => MENU_CALLBACK); + $items[] = array('path' => 'aggregator/opml', + 'title' => t('OPML feed'), + 'callback' => 'aggregator_page_opml', + 'access' => $view, + 'type' => MENU_CALLBACK); + + $result = db_query('SELECT title, cid FROM {aggregator_category} ORDER BY title'); + while ($category = db_fetch_array($result)) { + $items[] = array('path' => 'aggregator/categories/'. $category['cid'], + 'title' => $category['title'], + 'callback' => 'aggregator_page_category', + 'access' => $view); + } + } + else { + if (arg(0) == 'aggregator' && is_numeric(arg(2))) { + if (arg(1) == 'sources') { + $feed = aggregator_get_feed(arg(2)); + if ($feed) { + $items[] = array('path' => 'aggregator/sources/'. $feed['fid'], + 'title' => $feed['title'], + 'callback' => 'aggregator_page_source', + 'access' => $view, + 'type' => MENU_CALLBACK); + $items[] = array('path' => 'aggregator/sources/'. $feed['fid'] .'/view', + 'title' => t('view'), + 'type' => MENU_DEFAULT_LOCAL_TASK, + 'weight' => -10); + $items[] = array('path' => 'aggregator/sources/'. $feed['fid'] .'/categorize', + 'title' => t('categorize'), + 'callback' => 'aggregator_page_source', + 'access' => $edit, + 'type' => MENU_LOCAL_TASK); + $items[] = array('path' => 'aggregator/sources/'. $feed['fid'] .'/configure', + 'title' => t('configure'), + 'callback' => 'aggregator_form_feed', + 'callback arguments' => array($feed), + 'access' => $edit, + 'type' => MENU_LOCAL_TASK, + 'weight' => 1); + } + } + else if (arg(1) == 'categories') { + $category = aggregator_get_category(arg(2)); + if ($category) { + $items[] = array('path' => 'aggregator/categories/'. $category['cid'] .'/view', + 'title' => t('view'), + 'type' => MENU_DEFAULT_LOCAL_TASK, + 'weight' => -10); + $items[] = array('path' => 'aggregator/categories/'. $category['cid'] .'/categorize', + 'title' => t('categorize'), + 'callback' => 'aggregator_page_category', + 'access' => $edit, + 'type' => MENU_LOCAL_TASK); + $items[] = array('path' => 'aggregator/categories/'. $category['cid'] .'/configure', + 'title' => t('configure'), + 'callback' => 'aggregator_form_category', + 'callback arguments' => array($category), + 'access' => $edit, + 'type' => MENU_LOCAL_TASK, + 'weight' => 1); + } + } + } + else if (arg(1) == 'aggregator' && is_numeric(arg(4))) { + if (arg(3) == 'feed') { + $feed = aggregator_get_feed(arg(4)); + if ($feed) { + $items[] = array('path' => 'admin/aggregator/edit/feed/'. $feed['fid'], + 'title' => t('edit feed'), + 'callback' => 'aggregator_form_feed', + 'callback arguments' => array($feed), + 'access' => $edit, + 'type' => MENU_CALLBACK); + } + } + else { + $category = aggregator_get_category(arg(4)); + if ($category) { + $items[] = array('path' => 'admin/aggregator/edit/category/'. $category['cid'], + 'title' => t('edit category'), + 'callback' => 'aggregator_form_category', + 'callback arguments' => array($category), + 'access' => $edit, + 'type' => MENU_CALLBACK); + } + } + } + } + + return $items; +} + +/** + * Implementation of hook_settings(). + */ +function aggregator_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', ' '. t('To view the archive by date, select the date in the calendar. Administrators can enable the browse archives block in block administration to allow users to browse by calendar. Clicking on a date in the monthly calendar view shows the content for that date. Users can navigate to different months using arrows beside the month\'s name in the calendar display. The current date will be highlighted in the calendar.') .' You can '. t('For more information please read the configuration and customization handbook Archive page.', array('%archive' => 'http://drupal.org/handbook/modules/archive/')) .''),
+ '#description' => t('The list of tags which are allowed in feeds, i.e., which will not be 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('The number of items which will be shown with each feed or category in the feed and category summary pages.')
+ );
+
+ $form['aggregator_clear'] = array(
+ '#type' => 'select', '#title' => t('Discard news items older than'),
+ '#default_value' => variable_get('aggregator_clear', 9676800), '#options' => $period,
+ '#description' => t('Older news items will be automatically discarded. Requires crontab.')
+ );
+
+ $form['aggregator_category_selector'] = array(
+ '#type' => 'radios', '#title' => t('Category selection type'), '#default_value' => variable_get('aggregator_category_selector', 'check'),
+ '#options' => array('checkboxes' => t('checkboxes'), 'select' => t('multiple selector')),
+ '#description' => t('The type of category selection widget which is shown on categorization pages. Checkboxes are easier to use; a multiple selector is good for working with large numbers of categories.')
+ );
+ return $form;
+}
+
+/**
+ * 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, $delta = 0, $edit = array()) {
+ if (user_access('access news feeds')) {
+ 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' => theme('placeholder', $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' => theme('placeholder', $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') {
+ 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);
+ $block['content'] = '
'. t('Feed overview') .'
';
+
+ $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/aggregator/edit/feed/$feed->fid"), l(t('remove items'), "admin/aggregator/remove/$feed->fid"), l(t('update items'), "admin/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 .= ''. t('Category overview') .'
';
+
+ $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/aggregator/edit/category/$category->cid"));
+ }
+ $output .= theme('table', $header, $rows);
+
+ return $output;
+}
+
+/**
+ * Menu callback; removes all items from a feed, then redirects to the overview page.
+ */
+function aggregator_admin_remove_feed($feed) {
+ aggregator_remove(aggregator_get_feed($feed));
+ drupal_goto('admin/aggregator');
+}
+
+/**
+ * Menu callback; refreshes a feed, then redirects to the overview page.
+ */
+function aggregator_admin_refresh_feed($feed) {
+ aggregator_refresh(aggregator_get_feed($feed));
+ drupal_goto('admin/aggregator');
+}
+
+/**
+ * Menu callback; displays the aggregator administration page.
+ */
+function aggregator_admin_overview() {
+ return aggregator_view();
+}
+
+/**
+ * Menu callback; displays the most recent items gathered from any feed.
+ */
+function aggregator_page_last() {
+ return _aggregator_page_list('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', arg(1));
+}
+
+/**
+ * Menu callback; displays all the items captured from a particular feed.
+ */
+function aggregator_page_source() {
+ $feed = db_fetch_object(db_query('SELECT * FROM {aggregator_feed} WHERE fid = %d', arg(2)));
+ $info = theme('aggregator_feed', $feed);
+
+ return _aggregator_page_list('SELECT * FROM {aggregator_item} WHERE fid = '. $feed->fid .' ORDER BY timestamp DESC, iid DESC', arg(3), $info);
+}
+
+/**
+ * Menu callback; displays all the items aggregated in a particular category.
+ */
+function aggregator_page_category() {
+ $category = db_fetch_object(db_query('SELECT cid, title FROM {aggregator_category} WHERE cid = %d', arg(2)));
+
+ return _aggregator_page_list('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, iid DESC', arg(3));
+}
+
+/**
+ * Prints an aggregator page listing a number of feed items. Various
+ * menu callbacks use this function to print their feeds.
+ */
+function _aggregator_page_list($sql, $op, $header = '') {
+ $categorize = (user_access('administer news feeds') && ($op == 'categorize'));
+
+ $output = ''. check_plain($feed->title) ."
\n";
+
+ // Most recent items:
+ $list = 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)) {
+ $list[] = theme('aggregator_summary_item', $item);
+ }
+ }
+ $output .= theme('item_list', $list);
+ $output .= ''. check_plain($category->title) ."
\n";
+ if (variable_get('aggregator_summary_items', 3)) {
+ $list = 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)) {
+ $list[] = theme('aggregator_summary_item', $item);
+ }
+ $output .= theme('item_list', $list);
+ }
+ $output .= ''. check_plain($item->title) ."
\n";
+ $output .= " \n";
+
+ if ($item->description) {
+ $output .= '
'), -1, PREG_SPLIT_NO_EMPTY));
+}
+
+/**
+ * Helper function for drupal_map_assoc.
+ */
+function _aggregator_items($count) {
+ return format_plural($count, '1 item', '%count items');
+}
diff --git a/modules/archive.module b/modules/archive.module
new file mode 100644
index 0000000..d015cb5
--- /dev/null
+++ b/modules/archive.module
@@ -0,0 +1,291 @@
+'. t('The archive page allows content to be viewed by date. It also provides a monthly calendar view that users can use to navigate through content.') .'
+
+', array('%archive' => url('archive'), '%admin-block' => url('admin/block')));
+ $output .= '\n";
+ $output .= '
\n";
+ $days = array(t('Sunday') => t('Su'), t('Monday') => t('Mo'), t('Tuesday') => t('Tu'), t('Wednesday') => t('We'), t('Thursday') => t('Th'), t('Friday') => t('Fr'), t('Saturday') => t('Sa'));
+ if ($weekstart) {
+ $days = array_merge(array_slice($days, $weekstart), array_slice($days, 0, $weekstart));
+ }
+
+ foreach ($days as $fullname => $name) {
+ $output .= ' \n";
+
+ // Initialize temporary variables:
+ $nday = 1;
+ $sday = $first;
+
+ // Loop through all the days of the month:
+ while ($nday <= $last) {
+ // Set up blank days for first week of the month (allowing individual blank day styling):
+ if ($first != $weekstart) {
+ $blankdays = ($first - $weekstart + 7) % 7;
+ $output .= " '. $name . " \n";
+ }
+ $output .= "" . str_repeat(" \n", $blankdays);
+ $first = $weekstart;
+ }
+ // Start every week on a new line:
+ if ($sday == $weekstart) {
+ $output .= " \n";
+ }
+
+ // Print one cell:
+ $date = mktime(0, 0, 0, $month, $nday, $year) + $user->timezone;
+ if (isset($days_with_posts[$nday])) {
+ $daytext = l($nday, "archive/$year/$month/$nday", array("title" => format_plural($days_with_posts[$nday], "1 post", "%count posts")));
+ $dayclass = 'day-link';
+ }
+ else {
+ $daytext = $nday;
+ $dayclass = 'day-normal';
+ }
+ if ($date == $requested) {
+ $output .= " \n";
+ }
+
+ // Update temporary variables:
+ $sday++;
+ $sday = $sday % 7;
+ $nday++;
+ }
+
+ // Complete the calendar (allowing individual blank day styling):
+ if ($sday != $weekstart) {
+ $end = (7 - $sday + $weekstart) % 7;
+ $output .= str_repeat("$daytext \n";
+ }
+ else if ($date == $start_of_today) {
+ $output .= " $daytext \n";
+ }
+ else if ($date > $end_of_today) {
+ $output .= " $daytext \n";
+ }
+ else {
+ $output .= " $daytext \n";
+ }
+
+ // Start every week on a new line:
+ if ($sday == $lastday) {
+ $output .= " \n", $end) . "\n";
+ }
+
+ $output .= "
'. t('The region each block appears in depends on both which theme you are using (some themes allow greater control over block placement than others), and on the settings in the block administration section.') .'
'; + $output .= ''. t('The block administration screen lets you specify the vertical placement of the blocks within a region. You do this by assigning a weight to each block. Lighter blocks (those having a smaller weight) "float up" towards the top of the region; heavier ones "sink".') .'
'; + $output .= t('A block\'s visibility depends on:
+'. t('Some modules generate blocks that become available when the modules are enabled. These blocks can be administered via the blocks administration page.
', array('%admin-block' => url('admin/block'))) .''; + $output .= ''. t('Administrators can also define custom blocks. These blocks consist of a title, a description, and a body which can be as long as you wish. Block content can be in any of the input formats supported for other content.') .'
'; + $output .= t('You can
+'. t('For more information please read the configuration and customization handbook Block page.', array('%block' => 'http://drupal.org/handbook/modules/block/')) .'
'; + return $output; + case 'admin/modules#description': + return t('Controls the boxes that are displayed around the main content.'); + case 'admin/block': + return t(" +Blocks are boxes of content that may be rendered into certain regions of your web pages, for example, into sidebars. They are usually generated automatically by modules, but administrators can create blocks manually.
+Only enabled blocks are shown. You can position blocks by specifying which area of the page they should appear in (e.g., a sidebar). Highlighted labels on this page show the regions into which blocks can be rendered. You can specify where within a region a block will appear by adjusting its weight.
+If you want certain blocks to disable themselves temporarily during high server loads, check the 'Throttle' box. You can configure the auto-throttle on the throttle configuration page after having enabled the throttle module.
+You can configure the behaviour of each block (for example, specifying on which pages and for what users it will appear) by clicking the 'configure' link for each block.
+", array('%throttle' => url('admin/settings/throttle'))); + case 'admin/block/add': + return t('Here you can create a new block. Once you have created this block you must make it active and give it a place on the page using blocks. The title is used when displaying the block. The description is used in the "block" column on the blocks page.
', array('%overview' => url('admin/block'))); + } +} + +/** + * Implementation of hook_perm(). + */ +function block_perm() { + return array('administer blocks', 'use PHP for block visibility'); +} + +/** + * Implementation of hook_menu(). + */ +function block_menu($may_cache) { + $items = array(); + + if ($may_cache) { + $items[] = array('path' => 'admin/block', 'title' => t('blocks'), + 'access' => user_access('administer blocks'), + 'callback' => 'block_admin_display'); + $items[] = array('path' => 'admin/block/list', 'title' => t('list'), + 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -10); + $items[] = array('path' => 'admin/block/configure', 'title' => t('configure block'), + 'access' => user_access('administer blocks'), + 'callback' => 'block_admin_configure', + 'type' => MENU_CALLBACK); + $items[] = array('path' => 'admin/block/delete', 'title' => t('delete block'), + 'access' => user_access('administer blocks'), + 'callback' => 'block_box_delete', + 'type' => MENU_CALLBACK); + $items[] = array('path' => 'admin/block/add', 'title' => t('add block'), + 'access' => user_access('administer blocks'), + 'callback' => 'block_box_add', + 'type' => MENU_LOCAL_TASK); + foreach (list_themes() as $key => $theme) { + if ($theme->status) { + if ($key == variable_get('theme_default', 'bluemarine')) { + $items[] = array('path' => 'admin/block/list/' . $key, 'title' => t('%key settings', array('%key' => $key)), + 'access' => user_access('administer blocks'), 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -10); + } + else { + $items[] = array('path' => 'admin/block/list/' . $key, 'title' => t('%key settings', array('%key' => $key)), + 'access' => user_access('administer blocks'), 'type' => MENU_LOCAL_TASK); + } + } + } + } + + return $items; +} + +/** + * 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, title, info FROM {boxes} ORDER BY title'); + while ($block = db_fetch_object($result)) { + $blocks[$block->bid]['info'] = $block->info ? check_plain($block->info) : check_plain($block->title); + } + return $blocks; + + case 'configure': + $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 * FROM {boxes} WHERE bid = %d', $delta)); + $data['subject'] = check_plain($block->title); + $data['content'] = check_markup($block->body, $block->format, FALSE); + return $data; + } +} + +/** + * Update the 'blocks' DB table with the blocks currently exported by modules. + * + * @return + * Blocks currently exported by modules. + */ +function _block_rehash() { + global $theme_key; + + init_theme(); + + $result = db_query("SELECT * FROM {blocks} WHERE theme = '%s'", $theme_key); + while ($old_block = db_fetch_object($result)) { + $old_blocks[$old_block->module][$old_block->delta] = $old_block; + } + + db_query("DELETE FROM {blocks} WHERE theme = '%s'", $theme_key); + + foreach (module_list() as $module) { + $module_blocks = module_invoke($module, 'block', 'list'); + if ($module_blocks) { + foreach ($module_blocks as $delta => $block) { + $block['module'] = $module; + $block['delta'] = $delta; + // If previously written to database, load values. + if ($old_blocks[$module][$delta]) { + $block['status'] = $old_blocks[$module][$delta]->status; + $block['weight'] = $old_blocks[$module][$delta]->weight; + $block['region'] = $old_blocks[$module][$delta]->region; + $block['visibility'] = $old_blocks[$module][$delta]->visibility; + $block['pages'] = $old_blocks[$module][$delta]->pages; + $block['custom'] = $old_blocks[$module][$delta]->custom; + $block['throttle'] = $old_blocks[$module][$delta]->throttle; + } + // Otherwise, use any set values, or else substitute defaults. + else { + $properties = array('status' => 0, 'weight' => 0, 'region' => 'left', 'pages' => '', 'custom' => 0); + foreach ($properties as $property => $default) { + if (!isset($block[$property])) { + $block[$property] = $default; + } + } + } + + // Reinsert blocks into table + db_query("INSERT INTO {blocks} (module, delta, theme, status, weight, region, visibility, pages, custom, throttle) VALUES ('%s', '%s', '%s', %d, %d, '%s', %d, '%s', %d, %d)", + $block['module'], $block['delta'], $theme_key, $block['status'], $block['weight'], $block['region'], $block['visibility'], $block['pages'], $block['custom'], $block['throttle']); + $blocks[] = $block; + } + } + } + + return $blocks; +} + +/** + * Generate main block administration form. + */ +function block_admin_display() { + global $theme_key, $custom_theme; + + // If non-default theme configuration has been selected, set the custom theme. + if (arg(3)) { + $custom_theme = arg(3); + } + else { + $custom_theme = variable_get('theme_default', 'bluemarine'); + } + init_theme(); + + // Fetch and sort blocks + $blocks = _block_rehash(); + usort($blocks, '_block_compare'); + + $throttle = module_exist('throttle'); + $block_regions = system_region_list($theme_key); + + // Build form tree + $form['#action'] = arg(3) ? url('admin/block/list/' . $theme_key) : url('admin/block'); + $form['#tree'] = TRUE; + foreach ($blocks as $i => $block) { + $form[$i]['module'] = array('#type' => 'value', '#value' => $block['module']); + $form[$i]['delta'] = array('#type' => 'value', '#value' => $block['delta']); + $form[$i]['info'] = array('#value' => $block['info']); + $form[$i]['status'] = array('#type' => 'checkbox', '#default_value' => $block['status']); + $form[$i]['theme'] = array('#type' => 'hidden', '#value' => $theme_key); + $form[$i]['weight'] = array('#type' => 'weight', '#default_value' => $block['weight']); + $form[$i]['region'] = array('#type' => 'select', + '#default_value' => isset($block['region']) ? $block['region'] : system_default_region($theme_key), + '#options' => $block_regions, + ); + + if ($throttle) { + $form[$i]['throttle'] = array('#type' => 'checkbox', '#default_value' => $block['throttle']); + } + $form[$i]['configure'] = array('#value' => l(t('configure'), 'admin/block/configure/'. $block['module'] .'/'. $block['delta'])); + if ($block['module'] == 'block') { + $form[$i]['delete'] = array('#value' => l(t('delete'), 'admin/block/delete/'. $block['delta'])); + } + } + $form['submit'] = array('#type' => 'submit', '#value' => t('Save blocks')); + + return drupal_get_form('block_admin_display', $form); +} + +/** + * Helper function for sorting blocks on admin/block. + * + * Active blocks are sorted by region, then by weight. + * Disabled blocks are sorted by name. + */ +function _block_compare($a, $b) { + $status = $b['status'] - $a['status']; + // Separate enabled from disabled. + if ($status) { + return $status; + } + // Enabled blocks + if ($a['status']) { + $place = strcmp($a['region'], $b['region']); + return $place ? $place : ($a['weight'] - $b['weight']); + } + // Disabled blocks + else { + return strcmp($a['info'], $b['info']); + } +} + +/** + * Process main block administration form submission. + */ +function block_admin_display_submit($form_id, $form_values) { + foreach ($form_values as $block) { + 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'], $block['throttle'], $block['module'], $block['delta'], $block['theme']); + } + drupal_set_message(t('The block settings have been updated.')); + cache_clear_all(); +} + +/** + * Theme main block administration form submission. + * + * Note: the blocks are already sorted in the right order, + * grouped by status, region and weight. + */ +function theme_block_admin_display($form) { + global $theme_key; + + $throttle = module_exist('throttle'); + $block_regions = system_region_list($theme_key); + + // Highlight regions on page to provide visual reference. + foreach ($block_regions as $key => $value) { + drupal_set_content($key, 'TRUE
(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' => theme('placeholder', '')));
+ }
+ $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 drupal_get_form('block_admin_configure', $form);
+}
+
+function block_admin_configure_validate($form_id, $form_values) {
+ if ($form_values['module'] == 'block') {
+ if (empty($form_values['info']) || db_num_rows(db_query("SELECT bid FROM {boxes} WHERE bid != %d AND info = '%s'", $form_values['delta'], $form_values['info']))) {
+ form_set_error('info', t('Please ensure that each block description is unique.'));
+ }
+ }
+}
+
+function block_admin_configure_submit($form_id, $form_values) {
+ if (!form_get_errors()) {
+ db_query("UPDATE {blocks} SET visibility = %d, pages = '%s', custom = %d WHERE module = '%s' AND delta = '%s'", $form_values['visibility'], $form_values['pages'], $form_values['custom'], $form_values['module'], $form_values['delta']);
+ module_invoke($form_values['module'], 'block', 'save', $form_values['delta'], $form_values);
+ drupal_set_message(t('The block configuration has been saved.'));
+ cache_clear_all();
+ return 'admin/block';
+ }
+}
+
+/**
+ * Menu callback; displays the block creation form.
+ */
+function block_box_add() {
+ $form = block_box_form();
+ $form['submit'] = array('#type' => 'submit', '#value' => t('Save block'));
+
+ return drupal_get_form('block_box_add', $form);
+}
+
+function block_box_add_validate($form_id, $form_values) {
+ if (empty($form_values['info']) || db_num_rows(db_query("SELECT info FROM {boxes} WHERE info = '%s'", $form_values['info']))) {
+ form_set_error('info', t('Please ensure that each block description is unique.'));
+ }
+}
+
+function block_box_add_submit($form_id, $form_values) {
+ if (!form_get_errors()) {
+ if (block_box_save($form_values)) {
+ drupal_set_message(t('The block has been created.'));
+ return 'admin/block';
+ }
+ }
+}
+
+/**
+ * Menu callback; confirm deletion of custom blocks.
+ */
+function block_box_delete($bid = 0) {
+ $box = block_box_get($bid);
+ $form['info'] = array('#type' => 'hidden', '#value' => $box['info'] ? $box['info'] : $box['title']);
+ $form['bid'] = array('#type' => 'hidden', '#value' => $bid);
+
+ return confirm_form('block_box_delete_confirm', $form, t('Are you sure you want to delete the block %name?', array('%name' => theme('placeholder', $box['info']))), 'admin/block', '', t('Delete'), t('Cancel'));
+}
+
+/**
+ * Deletion of custom blocks.
+ */
+function block_box_delete_confirm_submit($form_id, $form_values) {
+ db_query('DELETE FROM {boxes} WHERE bid = %d', $form_values['bid']);
+ drupal_set_message(t('The block %name has been removed.', array('%name' => theme('placeholder', $form_values['info']))));
+ cache_clear_all();
+ return 'admin/block';
+};
+
+function block_box_form($edit = array()) {
+ $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 block overview page.', array('%overview' => url('admin/block'))),
+ '#required' => TRUE,
+ '#weight' => -19,
+ );
+ $form['title'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Block title'),
+ '#default_value' => $edit['title'],
+ '#maxlength' => 64,
+ '#description' => t('The title of the block as shown to the user.'),
+ '#weight' => -18,
+ );
+ $form['body_filter']['#weight'] = -17;
+ $form['body_filter']['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,
+ );
+ $form['body_filter']['format'] = filter_form($edit['format'], -16);
+
+ return $form;
+}
+
+function block_box_save($edit, $delta = NULL) {
+ if (!filter_access($edit['format'])) {
+ $edit['format'] = FILTER_FORMAT_DEFAULT;
+ }
+
+ if (isset($delta)) {
+ db_query("UPDATE {boxes} SET title = '%s', body = '%s', info = '%s', format = %d WHERE bid = %d", $edit['title'], $edit['body'], $edit['info'], $edit['format'], $delta);
+ }
+ else {
+ db_query("INSERT INTO {boxes} (title, body, info, format) VALUES ('%s', '%s', '%s', %d)", $edit['title'], $edit['body'], $edit['info'], $edit['format']);
+ }
+ 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, &$user, $category = NULL) {
+ switch ($type) {
+ case 'form':
+ if ($category == 'account') {
+ $result = db_query('SELECT * FROM {blocks} WHERE status = 1 AND custom != 0 ORDER BY weight, module, delta');
+ $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' => $data[$block->delta]['info'], '#default_value' => isset($user->block[$block->module][$block->delta]) ? $user->block[$block->module][$block->delta] : ($block->custom == 1));
+ }
+ }
+
+ if ($return) {
+ return $form;
+ }
+ }
+
+ break;
+ case 'validate':
+ if (!$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_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
+ * Add a proper primary key (bid) to the blocks table so we don't have
+ * to mess around with this module_delta construct.
+ * Currently, the blocks table has no primary key defined!
+ */
+function block_list($region) {
+ global $user, $theme_key;
+
+ static $blocks = array();
+
+ if (!count($blocks)) {
+ $result = db_query("SELECT * FROM {blocks} WHERE theme = '%s' AND status = 1 ORDER BY region, weight, module", $theme_key);
+ 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']);
+ $regexp = '/^('. preg_replace(array('/(\r\n?|\n)/', '/\\\\\*/', '/(^|\|)\\\\'. t('The blog module allows registered users to maintain an online weblog (commonly known as a blog), often referred to as an online journal or diary. Blogs are made up of individual posts that are time stamped and are typically viewed by date as you would a diary. Blogs often contain links to webpages users have read and/or agree/disagree with.') .'
'; + $output .= ''. t('The blog module adds a user blogs navigation link to the site, which takes any visitor to a page that displays the most recent blog entries from all the users on the site. The navigation menu has a create a blog entry link (which takes you to a submission form) and a view personal blog link (which displays your blog entries as other people will see them). The blog module also creates a recent blog posts block that can be enabled.') .'
'; + $output .= ''. t('If a user has the ability to post blogs, then the import module (news aggregator) will display a blog-it link next to each news item in its lists. Clicking on this takes the user to the blog submission form, with the title, a link to the item, and a link to the source into the body text already in the text box, ready for the user to add a comment or explanation. This actively encourages people to add blog entries about things they see and hear elsewhere in the website and from your syndicated partner sites.') .'
'; + $output .= t('You can
+'. t('For more information please read the configuration and customization handbook Blog page.', array('%blog' => 'http://drupal.org/handbook/modules/blog/')) .'
'; + return $output; + case 'admin/modules#description': + return t('Enables keeping an easily and regularly updated web page or a blog.'); + case 'node/add#blog': + return t("A blog is a regularly updated journal or diary made up of individual posts shown in reversed chronological order. A blog is tightly coupled to the author so each user will have his 'own' blog."); + } +} + +/** + * Displays an RSS feed containing recent blog entries of a given user. + */ +function blog_feed_user($uid = 0) { + global $user; + + if ($uid) { + $account = user_load(array('uid' => $uid, 'status' => 1)); + } + else { + $account = $user; + } + + $result = db_query_range(db_rewrite_sql("SELECT n.nid, n.title, r.teaser, n.created, u.name, u.uid FROM {node} n INNER JOIN {node_revisions} r ON n.vid = r.vid INNER JOIN {users} u ON n.uid = u.uid WHERE n.type = 'blog' AND u.uid = %d AND n.status = 1 ORDER BY n.created DESC"), $uid, 0, variable_get('feed_default_items', 10)); + $channel['title'] = $account->name ."'s blog"; + $channel['link'] = url("blog/$uid", NULL, NULL, TRUE); + $channel['description'] = $term->description; + node_feed($result, $channel); +} + +/** + * 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.title, r.teaser, n.created, u.name, u.uid FROM {node} n INNER JOIN {node_revisions} r ON n.vid = r.vid INNER JOIN {users} u ON n.uid = u.uid WHERE n.type = 'blog' AND n.status = 1 ORDER BY n.created DESC"), 0, variable_get('feed_default_items', 10)); + $channel['title'] = variable_get('site_name', 'drupal') .' blogs'; + $channel['link'] = url('blog', NULL, NULL, TRUE); + $channel['description'] = $term->description; + node_feed($result, $channel); +} + +/** + * Menu callback; displays a Drupal page containing recent blog entries. + */ +function blog_page($a = NULL, $b = NULL) { + + if (is_numeric($a)) { // $a is a user ID + if ($b == 'feed') { + return blog_feed_user($a); + } + else { + return blog_page_user($a); + } + } + else if ($a == 'feed') { + return blog_feed_last(); + } + else { + return blog_page_last(); + } +} + +/** + * Displays a Drupal page containing recent blog entries of a given user. + */ +function blog_page_user($uid) { + global $user; + + $account = user_load(array((is_numeric($uid) ? 'uid' : 'name') => $uid, 'status' => 1)); + + if ($account->uid) { + drupal_set_title($title = t("%name's blog", array('%name' => $account->name))); + + if (($account->uid == $user->uid) && user_access('edit own blog')) { + $output = ''. t('When this module is enabled and configured you can use programs like Ecto to create and publish posts from your desktop. Blog API module supports several XML-RPC based blogging APIs such as the Blogger API, MetaWeblog API, and most of the Movable Type API. Any desktop blogging tools or other services (e.g. Flickr\'s "post to blog") that support these APIs should work with this site.', array('%external-http-ecto-kung-foo-tv' => 'http://ecto.kung-foo.tv/', '%-' => url('http://www.blogger.com/developers/api/1_docs/'), '%external-http-www-xmlrpc-com-metaWeblogApi' => 'http://www.xmlrpc.com/metaWeblogApi', '%external-http-www-movabletype-org-docs-mtmanual_programmatic-html' => 'http://www.movabletype.org/docs/mtmanual_programmatic.html', '%external-http-www-flickr-com' => 'http://www.flickr.com')) .'
'; + $output .= ''. t('This module also allows site administrators to configure which content types can be posted via the external applications. So, for instance, users can post forum topics as well as blog posts. Where supported, the external applications will display each content type as a separate "blog".') .'
'; + $output .= t('You can
+'. t('For more information please read the configuration and customization handbook BlogApi page.', array('%blogapi' => 'http://drupal.org/handbook/modules/blogapi/')) .'
'; + return $output; + case 'admin/modules#description': + return t('Allows users to post content using applications that support XML-RPC blog APIs.'); + } +} + +/** + * 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 weblogs 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 weblog.')), + 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.getTrackbackPings', + 'blogapi_mt_get_trackback_pings', + array('array', 'string'), + t('Retrieve the list of TrackBack pings posted to a particular entry. This could be used to programmatically retrieve the list of pings for a particular entry, then iterate through each of those pings doing the same, until one has built up a graph of the web of entries referencing one another on a particular topic.')), + array( + 'mt.publishPost', + 'blogap_mti_publish_post', + array('boolean', 'string', 'string', 'string'), + t('Publish (rebuild) all of the static files related to an entry from your weblog. 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, NULL, NULL, 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, NULL, NULL, 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); + } + + $edit = array(); + $edit['type'] = _blogapi_blogid($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['moderate'] = in_array('moderate', $node_type_default); + $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 the type of post you wanted to create.')); + } + + if (user_access('administer nodes') && !isset($edit['date'])) { + $edit['date'] = format_date(time(), 'custom', 'Y-m-d H:i:s O'); + } + + 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', t('%type: added %title using blog API.', array('%type' => ''. t($node->type) .'', '%title' => theme('placeholder', $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(message_na()); + } + // 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.')); + } + + $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_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', t('%type: updated %title using blog API.', array('%type' => ''. t($node->type) .'', '%title' => theme('placeholder', $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); +} + +/** + * 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 + * + * returns a bandwidth-friendly list. + */ +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); + } + + $type = _blogapi_blogid($blogid); + if ($bodies) { + $result = db_query_range("SELECT n.nid, n.title, r.body, 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", $type, $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", $type, $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); + } + + $name = basename($file['name']); + $data = $file['bits']; + + if (!$data) { + return blogapi_error(t('No file sent.')); + } + + if (!$file = file_save_data($data, $name)) { + return blogapi_error(t('Error storing file.')); + } + + // 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) { + $type = _blogapi_blogid($blogid); + $vocabularies = module_invoke('taxonomy', 'get_vocabularies', $type, '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); +} + +// see above +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); + } + + $terms = module_invoke('taxonomy', 'node_get_terms', $postid, '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']; + } + node_save($node); + 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. Can not be implemented without support from + * trackback module. + */ +function blogapi_mt_get_trackback_pings() { + return blogapi_error(t('Not implemented.')); +} + +/** + * Blogging API callback. Publishes the given node + */ +function blogap_mti_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.')); + } + + $node->status = 1; + if (!node_access('update', $node)) { + return blogapi_error(t('You do not have permission to update this post.')); + } + + 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($username, $password); + + if ($user->uid) { + if (user_access('edit own blog', $user)) { + return $user; + } + else { + return t("You either tried to edit somebody else's blog or you don't have permission to edit your own 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(''. t('There are no orphan pages.') .'
'; + } +} + +function book_admin_edit_submit($form_id, $form_values) { + foreach ($form_values['table'] as $row) { + $node = node_load($row['nid']); + + if ($row['title'] != $node->title || $row['weight'] != $node->weight) { + $node->title = $row['title']; + $node->weight = $row['weight']; + + node_save($node); + watchdog('content', t('%type: updated %title.', array('%type' => theme('placeholder', t('book')), '%title' => theme('placeholder', $node->title))), WATCHDOG_NOTICE, l(t('view'), 'node/'. $node->nid)); + } + } + + if (is_numeric(arg(3))) { + // Updating pages in a single book. + $book = node_load(arg(3)); + drupal_set_message(t('Updated book %title.', array('%title' => theme('placeholder', $book->title)))); + } + else { + // Updating the orphan pages. + drupal_set_message(t('Updated orphan book pages.')); + } +} + +/** + * Menu callback; displays the book administration page. + */ +function book_admin($nid = 0) { + if ($nid) { + return book_admin_edit($nid); + } + else { + return book_admin_overview(); + } +} + +/** + * Returns an administrative overview of all books. + */ +function book_admin_overview() { + $result = db_query(db_rewrite_sql('SELECT n.nid, n.title, b.weight FROM {node} n INNER JOIN {book} b ON n.vid = b.vid WHERE b.parent = 0 ORDER BY b.weight, n.title')); + while ($book = db_fetch_object($result)) { + $rows[] = array(l($book->title, "node/$book->nid"), l(t('outline'), "admin/node/book/$book->nid")); + } + $headers = array(t('Book'), t('Operations')); + + return theme('table', $headers, $rows); +} + +/** + * Implementation of hook_help(). + */ +function book_help($section) { + switch ($section) { + case 'admin/help#book': + $output = ''. t('The book content type is suited for creating structured, multi-page hypertexts such as site resource guides, manuals, and Frequently Asked Questions (FAQs). It permits a document to have chapters, sections, subsections, etc. Authors with suitable permissions can add pages to a collaborative book, placing them into the existing document by adding them to a table of contents menu. ') .'
'; + $output .= ''. t('Books have additional previous, up, and next navigation elements at the bottom of each page for moving through the text. Additional navigation may be provided by enabling the book navigation block on the block administration page.', array('%admin-block' => url('admin/block'))) .'
'; + $output .= ''. t('Users can select the printer-friendly version link visible at the bottom of a book page to generate a printer-friendly display of the page and all of its subsections. ') .'
'; + $output .= ''. t('Administrators can view a book outline, from which is it possible to change the titles of sections, and their weight (thus reordering sections). From this outline, it is also possible to edit and/or delete book pages. Many content types besides pages (for example, blog entries, stories, and polls) can be added to a collaborative book by choosing the outline tab when viewing the post.') .'
'; + $output .= t('You can
+'. t('For more information please read the configuration and customization handbook Book page.', array('%book' => 'http://drupal.org/handbook/modules/book/')) .'
'; + return $output; + case 'admin/modules#description': + return t('Allows users to collaboratively author a book.'); + case 'admin/node/book': + return t('The book module offers a means to organize content, authored by many users, in an online manual, outline or FAQ.
'); + case 'admin/node/book/orphan': + return t('Pages in a book are like a tree. As pages are edited, reorganized and removed, child pages might be left with no link to the rest of the book. Such pages are referred to as "orphan pages". On this page, administrators can review their books for orphans and reattach those pages as desired.
'); + case 'node/add#book': + return t("A book is a collaborative writing effort: users can collaborate writing the pages of the book, positioning the pages in the right order, and reviewing or modifying pages previously written. So when you have some information to share or when you read a page of the book and you didn't like it, or if you think a certain page could have been written better, you can do something about it."); + } + + if (arg(0) == 'node' && is_numeric(arg(1)) && arg(2) == 'outline') { + return t('The outline feature allows you to include posts in the book hierarchy.', array('%book' => url('book'))); + } +} + + diff --git a/modules/comment.module b/modules/comment.module new file mode 100644 index 0000000..d6df750 --- /dev/null +++ b/modules/comment.module @@ -0,0 +1,1787 @@ +'. t('The comment module creates a discussion board for each post. Users can post comments to discuss a forum topic, weblog post, story, collaborative book page, etc. The ability to comment is an important part of involving members in a community dialogue.') .''; + $output .= ''. t('An administrator can give comment permissions to user groups, and users can (optionally) edit their last comment, assuming no others have been posted since. Attached to each comment board is a control panel for customizing the way that comments are displayed. Users can control the chronological ordering of posts (newest or oldest first) and the number of posts to display on each page. Comments behave like other user submissions. Filters, smileys and HTML that work in nodes will also work with comments. The comment module provides specific features to inform site members when new comments have been posted.') .'
'; + $output .= t('You can
+'. t('For more information please read the configuration and customization handbook Comment page.', array('%comment' => 'http://drupal.org/handbook/modules/comment/')) .'
'; + return $output; + case 'admin/modules#description': + return t('Allows users to comment on and discuss published content.'); + case 'admin/comment': + case 'admin/comment/new': + return t("Below is a list of the latest comments posted to your site. Click on a subject to see the comment, the author's name to edit the author's user information , \"edit\" to modify the text, and \"delete\" to remove their submission.
"); + case 'admin/comment/approval': + return t("Below is a list of the comments posted to your site that need approval. To approve a comment, click on \"edit\" and then change its \"moderation status\" to Approved. Click on a subject to see the comment, the author's name to edit the author's user information, \"edit\" to modify the text, and \"delete\" to remove their submission.
"); + case 'admin/comment/configure': + case 'admin/comment/configure/settings': + return t("Comments can be attached to any node, and their settings are below. The display comes in two types: a \"flat list\" where everything is flush to the left side, and comments come in chronological order, and a \"threaded list\" where replies to other comments are placed immediately below and slightly indented, forming an outline. They also come in two styles: \"expanded\", where you see both the title and the contents, and \"collapsed\" where you only see the title. Preview comment forces a user to look at their comment by clicking on a \"Preview\" button before they can actually add the comment.
"); + } +} + +/** + * Implementation of hook_menu(). + */ +function comment_menu($may_cache) { + $items = array(); + + if ($may_cache) { + $access = user_access('administer comments'); + $items[] = array('path' => 'admin/comment', 'title' => t('comments'), + 'callback' => 'comment_admin_overview', 'access' => $access); + + // Tabs: + $items[] = array('path' => 'admin/comment/list', 'title' => t('list'), + 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -10); + $items[] = array('path' => 'admin/comment/configure', 'title' => t('configure'), + 'callback' => 'comment_configure', 'access' => $access, 'type' => MENU_LOCAL_TASK); + + // Subtabs: + $items[] = array('path' => 'admin/comment/list/new', 'title' => t('published comments'), + 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -10); + $items[] = array('path' => 'admin/comment/list/approval', 'title' => t('approval queue'), + 'callback' => 'comment_admin_overview', 'access' => $access, + 'callback arguments' => array('approval'), + 'type' => MENU_LOCAL_TASK); + + $items[] = array('path' => 'admin/comment/configure/settings', 'title' => t('settings'), + 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -10); + + $items[] = array('path' => 'comment/delete', 'title' => t('delete comment'), + 'callback' => 'comment_delete', 'access' => $access, 'type' => MENU_CALLBACK); + + $access = user_access('post comments'); + $items[] = array('path' => 'comment/edit', 'title' => t('edit comment'), + 'callback' => 'comment_edit', 'access' => $access, 'type' => MENU_CALLBACK); + } + else { + if (arg(0) == 'comment' && arg(1) == 'reply' && is_numeric(arg(2))) { + $node = node_load(arg(2)); + if ($node->nid) { + $items[] = array('path' => 'comment/reply', 'title' => t('reply to comment'), + 'callback' => 'comment_reply', 'access' => node_access('view', $node), 'type' => MENU_CALLBACK); + } + } + if ((arg(0) == 'node') && is_numeric(arg(1)) && is_numeric(arg(2))) { + $items[] = array('path' => ('node/'. arg(1) .'/'. arg(2)), 'title' => t('view'), + 'callback' => 'node_page', + 'type' => MENU_CALLBACK); + } + } + + return $items; +} + +/** + * Implementation of hook_perm(). + */ +function comment_perm() { + return array('access comments', 'post comments', 'administer comments', 'post comments without approval'); +} + +/** + * Implementation of hook_block(). + * + * Generates a block with the most recent comments. + */ +function comment_block($op = 'list', $delta = 0) { + if ($op == 'list') { + $blocks[0]['info'] = t('Recent comments'); + return $blocks; + } + else if ($op == 'view' && user_access('access comments')) { + $block['subject'] = t('Recent comments'); + $block['content'] = theme('comment_block'); + return $block; + } +} + +function theme_comment_block() { + $result = db_query_range(db_rewrite_sql('SELECT c.nid, c.subject, c.cid, c.timestamp FROM {comments} c INNER JOIN {node} n ON n.nid = c.nid WHERE n.status = 1 AND c.status = %d ORDER BY c.timestamp DESC', 'c'), COMMENT_PUBLISHED, 0, 10); + $items = array(); + while ($comment = db_fetch_object($result)) { + $items[] = l($comment->subject, 'node/'. $comment->nid, NULL, NULL, 'comment-'. $comment->cid) .'http://example.com/directory
.'));
+ }
+ }
+ }
+ }
+
+ return $edit;
+}
+
+/*
+** Generate the basic commenting form, for appending to a node or display on a separate page.
+** This is rendered by theme_comment_form.
+*/
+
+function comment_form($edit, $title = NULL) {
+ global $user;
+
+ $op = isset($_POST['op']) ? $_POST['op'] : '';
+
+ if ($user->uid) {
+ if ($edit['cid'] && user_access('administer comments')) {
+ if ($edit['author']) {
+ $author = $edit['author'];
+ }
+ elseif ($edit['name']) {
+ $author = $edit['name'];
+ }
+ else {
+ $author = $edit['registered_name'];
+ }
+
+ if ($edit['status']) {
+ $status = $edit['status'];
+ }
+ else {
+ $status = 0;
+ }
+
+ if ($edit['date']) {
+ $date = $edit['date'];
+ }
+ else {
+ $date = format_date($edit['timestamp'], 'custom', 'Y-m-d H:i O');
+ }
+
+ $form['admin'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Administration'),
+ '#collapsible' => TRUE,
+ '#collapsed' => TRUE,
+ '#weight' => -2,
+ );
+
+ if ($edit['registered_name'] != '') {
+ // The comment is by a registered user
+ $form['admin']['author'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Authored by'),
+ '#size' => 30,
+ '#maxlength' => 60,
+ '#autocomplete_path' => 'user/autocomplete',
+ '#default_value' => $author,
+ '#weight' => -1,
+ );
+ }
+ else {
+ // The comment is by an anonymous user
+ $form['is_anonymous'] = array(
+ '#type' => 'value',
+ '#value' => TRUE,
+ );
+ $form['admin']['name'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Authored by'),
+ '#size' => 30,
+ '#maxlength' => 60,
+ '#default_value' => $author,
+ '#weight' => -1,
+ );
+ $form['admin']['mail'] = array(
+ '#type' => 'textfield',
+ '#title' => t('E-mail'),
+ '#maxlength' => 64,
+ '#size' => 30,
+ '#default_value' => $edit['mail'],
+ '#description' => t('The content of this field is kept private and will not be shown publicly.'),
+ );
+
+ $form['admin']['homepage'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Homepage'),
+ '#maxlength' => 255,
+ '#size' => 30,
+ '#default_value' => $edit['homepage'],
+ );
+ }
+
+ $form['admin']['date'] = array('#type' => 'textfield', '#parents' => array('date'), '#title' => t('Authored on'), '#size' => 20, '#maxlength' => 25, '#default_value' => $date, '#weight' => -1);
+
+ $form['admin']['status'] = array('#type' => 'radios', '#parents' => array('status'), '#title' => t('Status'), '#default_value' => $status, '#options' => array(t('Published'), t('Not published')), '#weight' => -1);
+
+ }
+ else {
+ $form['_author'] = array('#type' => 'item', '#title' => t('Your name'), '#value' => theme('username', $user)
+ );
+ $form['author'] = array('#type' => 'value', '#value' => $user->name);
+ }
+ }
+ else if (variable_get('comment_anonymous', COMMENT_ANONYMOUS_MAYNOT_CONTACT) == COMMENT_ANONYMOUS_MAY_CONTACT) {
+ $form['name'] = array('#type' => 'textfield', '#title' => t('Your name'), '#maxlength' => 60, '#size' => 30, '#default_value' => $edit['name'] ? $edit['name'] : variable_get('anonymous', 'Anonymous')
+ );
+
+ $form['mail'] = array('#type' => 'textfield', '#title' => t('E-mail'), '#maxlength' => 64, '#size' => 30, '#default_value' => $edit['mail'], '#description' => t('The content of this field is kept private and will not be shown publicly.')
+ );
+
+ $form['homepage'] = array('#type' => 'textfield', '#title' => t('Homepage'), '#maxlength' => 255, '#size' => 30, '#default_value' => $edit['homepage']);
+ }
+ else if (variable_get('comment_anonymous', COMMENT_ANONYMOUS_MAYNOT_CONTACT) == COMMENT_ANONYMOUS_MUST_CONTACT) {
+ $form['name'] = array('#type' => 'textfield', '#title' => t('Your name'), '#maxlength' => 60, '#size' => 30, '#default_value' => $edit['name'] ? $edit['name'] : variable_get('anonymous', 'Anonymous'), '#required' => TRUE);
+
+ $form['mail'] = array('#type' => 'textfield', '#title' => t('E-mail'), '#maxlength' => 64, '#size' => 30, '#default_value' => $edit['mail'],'#description' => t('The content of this field is kept private and will not be shown publicly.'), '#required' => TRUE);
+
+ $form['homepage'] = array('#type' => 'textfield', '#title' => t('Homepage'), '#maxlength' => 255, '#size' => 30, '#default_value' => $edit['homepage']);
+ }
+
+ if (variable_get('comment_subject_field', 1) == 1) {
+ $form['subject'] = array('#type' => 'textfield', '#title' => t('Subject'), '#maxlength' => 64, '#default_value' => $edit['subject']);
+ }
+
+ $form['comment_filter']['comment'] = array('#type' => 'textarea', '#title' => t('Comment'), '#rows' => 15, '#default_value' => $edit['comment'] ? $edit['comment'] : $user->signature, '#required' => TRUE);
+ $form['comment_filter']['format'] = filter_form($edit['format']);
+
+ $form['cid'] = array('#type' => 'value', '#value' => $edit['cid']);
+ $form['pid'] = array('#type' => 'value', '#value' => $edit['pid']);
+ $form['nid'] = array('#type' => 'value', '#value' => $edit['nid']);
+ $form['uid'] = array('#type' => 'value', '#value' => $edit['uid']);
+
+ $form['preview'] = array('#type' => 'button', '#value' => t('Preview comment'), '#weight' => 19);
+ $form['#token'] = 'comment' . $edit['nid'] . $edit['pid'];
+
+ // Only show post button if preview is optional or if we are in preview mode.
+ // We show the post button in preview mode even if there are form errors so that
+ // optional form elements (e.g., captcha) can be updated in preview mode.
+ if (!form_get_errors() && ((variable_get('comment_preview', COMMENT_PREVIEW_REQUIRED) == COMMENT_PREVIEW_OPTIONAL) || ($op == t('Preview comment')) || ($op == t('Post comment')))) {
+ $form['submit'] = array('#type' => 'submit', '#value' => t('Post comment'), '#weight' => 20);
+ }
+
+ if ($op == t('Preview comment')) {
+ $form['#after_build'] = array('comment_form_add_preview');
+ }
+
+ if ($_REQUEST['destination']) {
+ $form['#attributes']['destination'] = $_REQUEST['destination'];
+ }
+
+ if (empty($edit['cid']) && empty($edit['pid'])) {
+ $form['#action'] = url('comment/reply/'. $edit['nid']);
+ }
+
+ // Graft in extra form additions
+ $form = array_merge($form, comment_invoke_comment($form, 'form'));
+
+ return theme('box', $title, drupal_get_form('comment_form', $form));
+}
+
+function comment_form_add_preview($form, $edit) {
+ global $user;
+
+ drupal_set_title(t('Preview comment'));
+
+ $output = '';
+
+ comment_validate($edit);
+ $comment = (object)_comment_form_submit($edit);
+
+ // Attach the user and time information.
+ if ($edit['author']) {
+ $account = user_load(array('name' => $edit['author']));
+ }
+ elseif ($user->uid && !isset($edit['is_anonymous'])) {
+ $account = $user;
+ }
+ if ($account) {
+ $comment->uid = $account->uid;
+ $comment->name = check_plain($account->name);
+ }
+ $comment->timestamp = $edit['timestamp'] ? $edit['timestamp'] : time();
+
+ // Preview the comment with security check.
+ if (!form_get_errors()) {
+ $output .= theme('comment_view', $comment);
+ }
+ $form['comment_preview'] = array(
+ '#value' => $output,
+ '#weight' => -100,
+ '#prefix' => ''. t("Users can activate/deactivate their personal contact forms in their account settings. Upon activation, a contact tab will appear in their user profiles. Privileged users such as site administrators are able to contact users even if they have chosen not to enable this feature.") .'
'; + $output .= ''. t('If the menu module is enabled, a menu item linking to the site-wide contact page is added to the navigation block. It is disabled by default, but can be enabled via the menu management page. Links to the contact page may also be added to the primary and secondary links using the same page.', array('%menu-module' => url('admin/menu'))) .'
'; + $output .= t('Contact module links:') .''. t('The contact module also adds a menu item (disabled by default) to the navigation block.', array('%menu-settings' => url('admin/menu'))) .' '. $menu_note .'
'; + return($output); + } +} + +/** + * Implementation of hook_menu(). + */ +function contact_menu($may_cache) { + $items = array(); + if ($may_cache) { + $items[] = array('path' => 'admin/contact', + 'title' => t('contact form'), + 'callback' => 'contact_admin_categories', + 'access' => user_access('administer site configuration'), + ); + $items[] = array('path' => 'admin/contact/category', + 'title' => t('categories'), + 'callback' => 'contact_admin_categories', + 'access' => user_access('administer site configuration'), + 'type' => MENU_DEFAULT_LOCAL_TASK, + ); + $items[] = array('path' => 'admin/contact/category/list', + 'title' => t('list'), + 'callback' => 'contact_admin_categories', + 'access' => user_access('administer site configuration'), + 'type' => MENU_DEFAULT_LOCAL_TASK, + ); + $items[] = array('path' => 'admin/contact/category/add', + 'title' => t('add category'), + 'callback' => 'contact_admin_edit', + 'access' => user_access('administer site configuration'), + 'type' => MENU_LOCAL_TASK, + 'weight' => 1, + ); + $items[] = array('path' => 'admin/contact/category/edit', + 'title' => t('edit contact category'), + 'callback' => 'contact_admin_edit', + 'access' => user_access('administer site configuration'), + 'type' => MENU_CALLBACK, + ); + $items[] = array('path' => 'admin/contact/category/delete', + 'title' => t('delete contact'), + 'callback' => 'contact_admin_delete', + 'access' => user_access('administer site configuration'), + 'type' => MENU_CALLBACK, + ); + $items[] = array('path' => 'admin/contact/settings', + 'title' => t('settings'), + 'callback' => 'contact_admin_settings', + 'access' => user_access('administer site configuration'), + 'type' => MENU_LOCAL_TASK, + 'weight' => 1, + ); + $items[] = array('path' => 'contact', + 'title' => t('contact'), + 'callback' => 'contact_mail_page', + 'access' => user_access('access content'), + 'type' => MENU_SUGGESTED_ITEM, + ); + } + else { + if (arg(0) == 'user' && is_numeric(arg(1))) { + $items[] = array('path' => "user/". arg(1) ."/contact", + 'title' => t('contact'), + 'callback' => 'contact_mail_user', + 'type' => MENU_LOCAL_TASK, + 'weight' => 2, + ); + } + } + + return $items; +} + +/** + * 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' => $edit['contact'], + '#description' => t('Allow other users to contact you by e-mail via your personal contact form. 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; + } + if ($type == 'validate') { + return array('contact' => $edit['contact']); + } +} + +/** + * 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($category->category, $category->recipients, ($category->selected ? t('Yes') : t('No')), l(t('edit'), 'admin/contact/category/edit/'. $category->cid), l(t('delete'), 'admin/contact/category/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($cid = NULL) { + if (arg(3) == "edit" && $cid > 0) { + $edit = db_fetch_array(db_query("SELECT * FROM {contact} WHERE cid = %d", $cid)); + } + $form['category'] = array('#type' => 'textfield', + '#title' => t('Category'), + '#maxlength' => 255, + '#default_value' => $edit['category'], + '#description' => t("Example: 'website feedback' or 'product information'."), + '#required' => TRUE, + ); + $form['recipients'] = array('#type' => 'textarea', + '#title' => t('Recipients'), + '#default_value' => $edit['recipients'], + '#description' => t("Example: 'webmaster@yoursite.com' or 'sales@yoursite.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' => $edit['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' => $edit['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' => $edit['selected'], + '#description' => t('Set this to Yes if you would like this category to be selected by default.'), + ); + $form['cid'] = array('#type' => 'value', + '#value' => $edit['cid'], + ); + $form['submit'] = array('#type' => 'submit', + '#value' => t('Submit'), + ); + + return drupal_get_form('contact_admin_edit', $form); +} + +/** + * Validate the contact category edit page form submission. + */ +function contact_admin_edit_validate($form_id, $form_values) { + if (empty($form_values['category'])) { + form_set_error('category', t('You must enter a category.')); + } + if (empty($form_values['recipients'])) { + form_set_error('recipients', t('You must enter one or more recipients.')); + } + else { + $recipients = explode(',', $form_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' => theme('placeholder', $recipient)))); + } + } + } +} + +/** + * Process the contact category edit page form submission. + */ +function contact_admin_edit_submit($form_id, $form_values) { + if ($form_values['selected']) { + // Unselect all other contact categories. + db_query('UPDATE {contact} SET selected = 0'); + } + $recipients = explode(',', $form_values['recipients']); + foreach($recipients as $key=>$recipient) { + // E-mail address validation has already been done in _validate. + $recipients[$key] = trim($recipient); + } + $form_values['recipients'] = implode(',', $recipients); + if (arg(3) == 'add') { + db_query("INSERT INTO {contact} (category, recipients, reply, weight, selected) VALUES ('%s', '%s', '%s', %d, %d)", $form_values['category'], $form_values['recipients'], $form_values['reply'], $form_values['weight'], $form_values['selected']); + drupal_set_message(t('Category %category has been added.', array('%category' => theme('placeholder', $form_values['category'])))); + watchdog('mail', t('Contact form: category %category added.', array('%category' => theme('placeholder', $form_values['category']))), WATCHDOG_NOTICE, l(t('view'), 'admin/contact')); + + } + else { + db_query("UPDATE {contact} SET category = '%s', recipients = '%s', reply = '%s', weight = %d, selected = %d WHERE cid = %d", $form_values['category'], $form_values['recipients'], $form_values['reply'], $form_values['weight'], $form_values['selected'], $form_values['cid']); + drupal_set_message(t('Category %category has been updated.', array('%category' => theme('placeholder', $form_values['category'])))); + watchdog('mail', t('Contact form: category %category updated.', array('%category' => theme('placeholder', $form_values['category']))), WATCHDOG_NOTICE, l(t('view'), 'admin/contact')); + } + + return 'admin/contact'; +} + +/** + * Category delete page. + */ +function contact_admin_delete($cid = NULL) { + if ($info = db_fetch_object(db_query("SELECT category FROM {contact} WHERE cid = %d", $cid))) { + $form['category'] = array('#type' => 'value', + '#value' => $info->category, + ); + + return confirm_form('contact_admin_delete', $form, t('Are you sure you want to delete %category?', array('%category' => theme('placeholder', $info->category))), 'admin/contact', t('This action cannot be undone.'), t('Delete'), t('Cancel')); + } + else { + drupal_set_message(t('Category not found.'), 'error'); + drupal_goto('admin/contact'); + } +} + +/** + * Process category delete form submission. + */ +function contact_admin_delete_submit($form_id, $form_values) { + db_query("DELETE FROM {contact} WHERE cid = %d", arg(4)); + drupal_set_message(t('Category %category has been deleted.', array('%category' => theme('placeholder', $form_values['category'])))); + watchdog('mail', t('Contact form: category %category deleted.', array('%category' => theme('placeholder', $form_values['category']))), WATCHDOG_NOTICE); + + return 'admin/contact'; +} + +/** + * Settings tab. Using a form rather than hook_settings(). + */ +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 contact page. 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['submit'] = array('#type' => 'submit', + '#value' => t('Save configuration'), + ); + $form['reset'] = array('#type' => 'submit', + '#value' => t('Reset to defaults'), + ); + // Use system_settings_form for the callback. + return drupal_get_form('contact_admin_settings', $form, 'system_settings_form'); +} + +/** + * Personal contact page. + */ +function contact_mail_user() { + global $user; + + if ($account = user_load(array('uid' => arg(1)))) { + $admin_access = user_access('administer users'); + if (!$account->status && !$admin_access) { + drupal_access_denied(); + } + else if (!$account->contact && !$admin_access) { + $output = t('%name is not accepting e-mails.', array('%name' => $account->name)); + } + else if (!$user->uid) { + $output = t('Please login or register to send %name a message.', array('%login' => url('user/login'), '%register' => url('user/register'), '%name' => $account->name)); + } + else if (!valid_email_address($user->mail)) { + $output = t('You need to provide a valid e-mail address to contact other users. Please update your user information 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($account->name); + + $form['#token'] = $user->name . $user->mail; + $form['from'] = array('#type' => 'item', + '#title' => t('From'), + '#value' => $user->name .' <'. $user->mail .'>', + ); + $form['to'] = array('#type' => 'item', + '#title' => t('To'), + '#value' => $account->name, + ); + $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 me a copy.'), + ); + $form['submit'] = array('#type' => 'submit', + '#value' => t('Send e-mail'), + ); + $output = drupal_get_form('contact_mail_user', $form); + } + + return $output; + } + else { + drupal_not_found(); + } +} + +/** + * Process the personal contact page form submission. + */ +function contact_mail_user_submit($form_id, $edit) { + global $user; + + $account = user_load(array('uid' => arg(1), 'status' => 1)); + // Compose the body: + $message[] = "$account->name,"; + $message[] = 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", NULL, NULL, TRUE), '%form-url' => url($_GET['q'], NULL, NULL, TRUE), '%site' => variable_get('site_name', 'drupal'))); + $message[] = t("If you don't want to receive such e-mails, you can change your settings at %url.", array('%url' => url("user/$account->uid", NULL, NULL, TRUE))); + $message[] = t('Message:'); + $message[] = $edit['message']; + + // Tidy up the body: + foreach ($message as $key => $value) { + $message[$key] = wordwrap($value); + } + + // Prepare all fields: + $to = $account->mail; + $from = $user->mail; + + // Format the subject: + $subject = '['. variable_get('site_name', 'drupal') .'] '. $edit['subject']; + + // Prepare the body: + $body = implode("\n\n", $message); + + // Send the e-mail: + user_mail($to, $subject, $body, "From: $from\nReply-to: $from\nX-Mailer: Drupal\nReturn-path: $from\nErrors-to: $from"); + + // Send a copy if requested: + if ($edit['copy']) { + user_mail($from, $subject, $body, "From: $from\nReply-to: $from\nX-Mailer: Drupal\nReturn-path: $from\nErrors-to: $from"); + } + + // Log the operation: + flood_register_event('contact'); + watchdog('mail', t('%name-from sent %name-to an e-mail.', array('%name-from' => theme('placeholder', $user->name), '%name-to' => theme('placeholder', $account->name)))); + + // Set a status message: + drupal_set_message(t('The message has been sent.')); + + // Jump to the user's profile page: + return "user/$account->uid"; +} + +/** + * Site-wide contact page + */ +function contact_mail_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 { + if ($user->uid) { + $edit['name'] = $user->name; + $edit['mail'] = $user->mail; + } + + $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->name . $user->mail; + $form['contact_information'] = array('#value' => filter_xss_admin(variable_get('contact_form_information', t('You can leave us a message using the contact form below.')))); + $form['name'] = array('#type' => 'textfield', + '#title' => t('Your name'), + '#maxlength' => 255, + '#default_value' => $edit['name'], + '#required' => TRUE, + ); + $form['mail'] = array('#type' => 'textfield', + '#title' => t('Your e-mail address'), + '#maxlength' => 255, + '#default_value' => $edit['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)) { + $categories = array(t('--')) + $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, + ); + $form['copy'] = array('#type' => 'checkbox', + '#title' => t('Send me a copy.'), + ); + $form['submit'] = array('#type' => 'submit', + '#value' => t('Send e-mail'), + ); + $output = drupal_get_form('contact_mail_page', $form); + } + else { + $output = t('The contact form has not been configured.'); + } + } + + return $output; +} + +/** + * Validate the site-wide contact page form submission. + */ +function contact_mail_page_validate($form_id, $form_values) { + if (!$form_values['cid']) { + form_set_error('category', t('You must select a valid category.')); + } + if (!valid_email_address($form_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_id, $edit) { + + // 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 = $edit['mail']; + + // Compose the body: + $message[] = t("%name sent a message using the contact form at %form.", array('%name' => $edit['name'], '%form' => url($_GET['q'], NULL, NULL, TRUE))); + $message[] = $edit['message']; + + // Tidy up the body: + foreach ($message as $key => $value) { + $message[$key] = wordwrap($value); + } + + // Load the category information: + $contact = db_fetch_object(db_query("SELECT * FROM {contact} WHERE cid = %d", $edit['cid'])); + + // Format the category: + $subject = t('[%category] %subject', array('%category' => $contact->category, '%subject' => $edit['subject'])); + + // Prepare the body: + $body = implode("\n\n", $message); + + // Send the e-mail to the recipients: + user_mail($contact->recipients, $subject, $body, "From: $from\nReply-to: $from\nX-Mailer: Drupal\nReturn-path: $from\nErrors-to: $from"); + + // If the user requests it, send a copy. + if ($edit['copy']) { + user_mail($from, $subject, $body, "From: $from\nReply-to: $from\nX-Mailer: Drupal\nReturn-path: $from\nErrors-to: $from"); + } + + // Send an auto-reply if necessary: + if ($contact->reply) { + user_mail($from, $subject, wordwrap($contact->reply), "From: $contact->recipients\nReply-to: $contact->recipients\nX-Mailer: Drupal\nReturn-path: $contact->recipients\nErrors-to: $contact->recipients"); + } + + // Log the operation: + flood_register_event('contact'); + watchdog('mail', t('%name-from sent an e-mail regarding %category.', array('%name-from' => theme('placeholder', $edit['name'] ." <$from>"), '%category' => theme('placeholder', $contact->category)))); + + // Update user: + 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. + return(''); +} diff --git a/modules/drupal.module b/modules/drupal.module new file mode 100644 index 0000000..d47d004 --- /dev/null +++ b/modules/drupal.module @@ -0,0 +1,364 @@ +'. t('The Drupal module uses the XML-RPC network communication protocol to connect your site with a central server that maintains a directory of client sites.') .''; + $output .= t('Enabling the Drupal module will allow you to:
+'. t('The Drupal module administration page allows you to set the xml-rpc server page and other related options.') .'
'; + $output .= t('You can
+'. t('If you maintain a directory of sites, you can list them on a page using the drupal_client_page()
function. Sample instructions:
+
+<?php +print drupal_client_page(); +?> ++
'. t('For more information please read the configuration and customization handbook Drupal page.', array('%drupal' => 'http://drupal.org/handbook/modules/drupal/')) .'
'; + return $output; + case 'admin/modules#description': + return t('Lets you register your site with a central server and improve ranking of Drupal projects by posting information on your installed modules and themes; also enables users to log in using a Drupal ID.'); + case 'admin/settings/drupal': + return t('Using this your site can "call home" to another Drupal server. By calling home to drupal.org and sending a list of your installed modules and themes, you help rank projects on drupal.org and so assist all Drupal administrators to find the best components for meeting their needs. If you want to register with a different server, you can change the Drupal XML-RPC server setting -- but the server has to be able to handle Drupal XML. Some XML-RPC servers may present directories of all registered sites. To get all your site information listed, go to the settings page and set the site name, the e-mail address, the slogan, and the mission statement.
', array('%site-settings' => url('admin/settings'))); + case 'user/help#drupal': + return variable_get('drupal_authentication_service', 0) ? t("Drupal is the name of the software that powers %this-site. There are Drupal web sites all over the world, and many of them share their registration databases so that users may freely log in to any Drupal site using a single Drupal ID.
+So please feel free to log in to your account here at %this-site with a username from another Drupal site. The format of a Drupal ID is similar to an e-mail address: username@server. An example of a valid Drupal ID is mwlily@drupal.org.
", array('%Drupal' => 'http://drupal.org', '%this-site' => ''. variable_get('site_name', 'this web site') .'')) : ''; + } +} + +/** + * Implementation of hook_settings(). + */ +function drupal_settings() { + // Check if all required fields are present + if ((variable_get('site_name', 'drupal') == 'drupal') || (variable_get('site_name', 'drupal') == '')) { + form_set_error('drupal_directory', t('You must set the name of your site on the administer » settings page.', array('%url' => url('admin/settings')))); + } + else if (variable_get('site_mail', ini_get('sendmail_from')) == '') { + form_set_error('drupal_directory', t('You must set an e-mail address for your site on the administer » settings page.', array('%url' => url('admin/settings')))); + } + else if (variable_get('site_slogan', '') == '') { + form_set_error('drupal_directory', t('You must set your site slogan on the administer » settings page.', array('%url' => url('admin/settings')))); + } + else if (variable_get('site_mission', '') == '') { + form_set_error('drupal_directory', t('You must set your site mission on the administer » settings page.' , array('%url' => url('admin/settings')))); + } + $options = array('1' => t('Enabled'), '0' => t('Disabled')); + + $form['drupal'] = array( + '#type' => 'fieldset', + '#title' => t('Post data to another site'), + '#tree' => FALSE + ); + + $form['drupal']['drupal_register'] = array( + '#type' => 'radios', + '#title' => t('Register with a Drupal server'), + '#default_value' => variable_get('drupal_register', 0), + '#options' => $options, + '#description' => t("If enabled, your Drupal site will register itself with the specified Drupal XML-RPC server. For this to work properly, you must set your site's name, e-mail address, slogan and mission statement. When the \"Drupal XML-RPC server\" field is set to \"%drupal-xml-rpc\", your web site will register itself with drupal.org. Requires the cron feature to be enabled.", array("%drupal-xml-rpc" => "http://drupal.org/xmlrpc.php", "%drupal-sites" => "http://drupal.org/drupal-sites/")) + ); + + $form['drupal']['drupal_server'] = array( + '#type' => 'textfield', + '#title' => t('Drupal XML-RPC server'), + '#default_value' => variable_get('drupal_server', 'http://drupal.org/xmlrpc.php'), + '#description' => t('The URL of the Drupal XML-RPC server you wish to register with.') + ); + + $form['drupal']['drupal_system'] = array( + '#type' => 'radios', + '#title' => t('Send system information'), + '#default_value' => variable_get('drupal_system', 0), + '#options' => $options, + '#description' => t("If enabled, your site will send information on its installed components (modules, themes, and theme engines). This information can help in compiling statistics on usage of Drupal projects.") + ); + + $form['drupal']['drupal_statistics'] = array( + '#type' => 'radios', + '#title' => t('Send statistics'), + '#default_value' => variable_get('drupal_statistics', 0), + '#options' => $options, + '#description' => t("If enabled, your site will send summary statistics on the number of registered users and the total number of posts. No private information will be sent. These data help to improve the ranking statistics of Drupal projects.") + ); + + $form['services'] = array( + '#type' => 'fieldset', + '#title' => t('Receive data from other sites'), + '#tree' => FALSE + ); + + $form['services']['drupal_client_service'] = array( + '#type' => 'radios', + '#title' => t('Allow other Drupal sites to register'), + '#default_value' => variable_get('drupal_client_service', 0), + '#options' => $options, + '#description' => t('If enabled, your Drupal site will allow other sites to register with your site and send information to this site. This functionality can be used to maintain a list of related sites.') + ); + + $form['services']['drupal_authentication_service'] = array( + '#type' => 'radios', + '#title' => t('Authentication service'), + '#default_value' => variable_get('drupal_authentication_service', 0), + '#options' => $options, + '#description' => t('If enabled, your Drupal site will accept logins with the user names of other Drupal sites, and likewise provide authentication for users logging into other Drupal sites, based on their user accounts here.') + ); + + return $form; +} + +/** + * Implementation of hook_cron(); handles pings to and from the site. + */ +function drupal_cron() { + if (time() - variable_get('cron_last', 0) > 21600) { + + // If this site acts as a Drupal XML-RPC server, delete the sites that + // stopped sending "ping" messages. + if (variable_get('drupal_client_service', 0)) { + $result = db_query("SELECT cid FROM {client} WHERE changed < %d", time() - 259200); + while ($client = db_fetch_object($result)) { + db_query("DELETE FROM {client_system} WHERE cid = %d", $client->cid); + db_query("DELETE FROM {client} WHERE cid = %d", $client->cid); + } + } + + // If this site acts as a Drupal XML-RPC client, send a message to the + // Drupal XML-RPC server. + if (variable_get('drupal_register', 0) && variable_get('drupal_server', 0)) { + drupal_notify(variable_get('drupal_server', '')); + } + } +} + +/** + * Callback function from drupal_xmlrpc() called when another site pings this one. + */ +function drupal_client_ping($client, $system) { + /* + ** Parse our parameters: + */ + + foreach (array('link', 'name', 'mail', 'slogan', 'mission') as $key) { + $client[$key] = strip_tags($client[$key]); + } + + /* + ** Update the data in our database and send back a reply: + */ + + if ($client['link'] && $client['name'] && $client['mail'] && $client['slogan'] && $client['mission']) { + $result = db_query(db_rewrite_sql("SELECT cid FROM {client} WHERE link = '%s'"), $client['link']); + if (db_num_rows($result)) { + $record = db_fetch_object($result); + $client['cid'] = $record->cid; + // We have an existing record. + db_query("UPDATE {client} SET link = '%s', name = '%s', mail = '%s', slogan = '%s', mission = '%s', users = %d, nodes = %d, version = '%s', changed = '%s' WHERE cid = %d", $client['uid'], $client['link'], $client['name'], $client['mail'], $client['slogan'], $client['mission'], $client['users'], $client['nodes'], $client['version'], time(), $client['cid']); + } + else { + $client['cid'] = db_next_id('{client}_cid'); + db_query("INSERT INTO {client} (cid, link, name, mail, slogan, mission, users, nodes, version, created, changed) VALUES (%d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s')", $client['cid'], $client['link'], $client['name'], $client['mail'], $client['slogan'], $client['mission'], $client['users'], $client['nodes'], $client['version'], time(), time()); + } + if (is_array($system)) { + db_query("DELETE FROM {client_system} WHERE cid = %d", $client['cid']); + foreach ($system as $item) { + db_query("INSERT INTO {client_system} (cid, name, type) VALUES (%d, '%s', '%s')", $client['cid'], $item['name'], $item['type']); + } + } + watchdog('client ping', t('Ping from %name (%link).', array('%name' => theme('placeholder', $client['name']), '%link' => theme('placeholder', $client['link']))), WATCHDOG_NOTICE, 'view'); + + return TRUE; + } + else { + return 0; + } + +} + +/** + * Formats a list of all clients. + * + * This function may be called from a custom page on sites that are + * Drupal directory servers. + */ +function drupal_client_page($sort = 'name') { + $result = db_query('SELECT * FROM {client} ORDER BY %s', $sort); + + $clients = array(); + while ($client = db_fetch_object($result)) { + $clients[] = $client; + } + + return theme('client_list', $clients); +} + +/** + * Theme a client list. + */ +function theme_client_list($clients) { + // Note: All fields except the mission are treated as plain-text. + // The mission is stripped of any HTML tags to keep the output simple and consistent. + $output = "\n'. t('Users can choose between the available input formats when creating or editing content. Administrators can configure which input formats are available to which user roles, as well as choose a default input format. Administrators can also create new input formats. Each input format can be configured to use a selection of filters.') .'
'; + $output .= t('You can
+'. t('For more information please read the configuration and customization handbook Filter page.', array('%filter' => 'http://drupal.org/handbook/modules/filter/')) .'
'; + return $output; + case 'admin/modules#description': + return t('Handles the filtering of content in preparation for display.'); + + case 'admin/filters': + return t(' +Input formats define a way of processing user-supplied text in Drupal. Every input format has its own settings of which filters to apply. Possible filters include stripping out malicious HTML and making URLs clickable.
+Users can choose between the available input formats when submitting content.
+Below you can configure which input formats are available to which roles, as well as choose a default input format (used for imported content, for example).
+Note that (1) the default format is always available to all roles, and (2) all filter formats can always be used by roles with the "administer filters" permission even if they are not explicitly listed in the Roles column of this table.
'); + + case 'admin/filters/'. arg(2): + return t(' +Every filter performs one particular change on the user input, for example stripping out malicious HTML or making URLs clickable. Choose which filters you want to apply to text in this input format.
+If you notice some filters are causing conflicts in the output, you can rearrange them.
', array('%rearrange' => url('admin/filters/'. arg(2) .'/order'))); + + case 'admin/filters/'. arg(2) .'/configure': + return t(' +If you cannot find the settings for a certain filter, make sure you\'ve enabled it on the view tab first.
', array('%url' => url('admin/filters/'. arg(2)))); + + case 'admin/filters/'. arg(2) .'/order': + return t(' +Because of the flexible filtering system, you might encounter a situation where one filter prevents another from doing its job. For example: a word in an URL gets converted into a glossary term, before the URL can be converted in a clickable link. When this happens, you will need to rearrange the order in which filters get executed.
+Filters are executed from top-to-bottom. You can use the weight column to rearrange them: heavier filters \'sink\' to the bottom.
'); + } +} + +/** + * Implementation of hook_menu(). + */ +function filter_menu($may_cache) { + $items = array(); + + if ($may_cache) { + $items[] = array('path' => 'admin/filters', + 'title' => t('input formats'), + 'callback' => 'filter_admin_overview', + 'access' => user_access('administer filters'), + ); + $items[] = array('path' => 'admin/filters/list', + 'title' => t('list'), + 'callback' => 'filter_admin_overview', + 'type' => MENU_DEFAULT_LOCAL_TASK, + 'access' => user_access('administer filters'), + ); + $items[] = array('path' => 'admin/filters/add', + 'title' => t('add input format'), + 'callback' => 'filter_admin_format_form', + 'type' => MENU_LOCAL_TASK, + 'weight' => 1, + 'access' => user_access('administer filters'), + ); + $items[] = array('path' => 'admin/filters/delete', + 'title' => t('delete input format'), + 'callback' => 'filter_admin_delete', + 'type' => MENU_CALLBACK, + 'access' => user_access('administer filters'), + ); + $items[] = array('path' => 'filter/tips', + 'title' => t('compose tips'), + 'callback' => 'filter_tips_long', + 'access' => TRUE, + 'type' => MENU_SUGGESTED_ITEM, + ); + } + else { + if (arg(0) == 'admin' && arg(1) == 'filters' && is_numeric(arg(2))) { + $formats = filter_formats(); + + if (isset($formats[arg(2)])) { + $items[] = array('path' => 'admin/filters/'. arg(2), + 'title' => t("'%format' input format", array('%format' => $formats[arg(2)]->name)), + 'callback' => 'filter_admin_format_form', + 'callback arguments' => array('format' => $formats[arg(2)]), + 'type' => MENU_CALLBACK, + 'access' => user_access('administer filters'), + ); + $items[] = array('path' => 'admin/filters/'. arg(2) .'/list', + 'title' => t('view'), + 'callback' => 'filter_admin_format_form', + 'callback arguments' => array('format' => $formats[arg(2)]), + 'type' => MENU_DEFAULT_LOCAL_TASK, + 'weight' => 0, + 'access' => user_access('administer filters'), + ); + $items[] = array('path' => 'admin/filters/'. arg(2) .'/configure', + 'title' => t('configure'), + 'callback' => 'filter_admin_configure', + 'type' => MENU_LOCAL_TASK, + 'weight' => 1, + 'access' => user_access('administer filters'), + ); + $items[] = array('path' => 'admin/filters/'. arg(2) .'/order', + 'title' => t('rearrange'), + 'callback' => 'filter_admin_order', + 'callback arguments' => array('format' => $formats[arg(2)]), + 'type' => MENU_LOCAL_TASK, + 'weight' => 2, + 'access' => user_access('administer filters'), + ); + } + } + } + + return $items; +} + +/** + * Implementation of hook_perm(). + */ +function filter_perm() { + return array('administer filters'); +} + +/** + * Implementation of hook_filter_tips(). + */ +function filter_filter_tips($delta, $format, $long = false) { + global $base_url; + switch ($delta) { + case 0: + if (variable_get("filter_html_$format", FILTER_HTML_STRIP) == FILTER_HTML_STRIP) { + if ($allowed_html = variable_get("allowed_html_$format", ' -
-
- ')) {
+ switch ($long) {
+ case 0:
+ return t('Allowed HTML tags') .': '. check_plain($allowed_html);
+ case 1:
+ $output = '
'. t('Allowed HTML tags') .': '. check_plain($allowed_html) .'
';
+ if (!variable_get("filter_html_help_$format", 1)) {
+ return $output;
+ }
+
+ $output .= t('
+This site allows HTML content. While learning all of HTML may feel intimidating, learning how to use a very small number of the most basic HTML "tags" is very easy. This table provides examples for each tag that is enabled on this site.
+For more information see W3C\'s HTML Specifications or use your favorite search engine to find other sites that explain HTML.
');
+ $tips = array(
+ 'a' => array( t('Anchors are used to make links to other pages.'), ''. variable_get('site_name', 'drupal') .''),
+ 'br' => array( t('By default line break tags are automatically added, so use this tag to add additional ones. Use of this tag is different because it is not used with an open/close pair like all the others. Use the extra " /" inside the tag to maintain XHTML 1.0 compatibility'), t('Text with
line break')),
+ 'p' => array( t('By default paragraph tags are automatically added, so use this tag to add additional ones.'), ''. t('Paragraph one.') .'
'. t('Paragraph two.') .'
'),
+ 'strong' => array( t('Strong'), ''. t('Strong'). ''),
+ 'em' => array( t('Emphasized'), ''. t('Emphasized') .''),
+ 'cite' => array( t('Cited'), ''. t('Cited') .''),
+ 'code' => array( t('Coded text used to show programming source code'), ''. t('Coded') .'
'),
+ 'b' => array( t('Bolded'), ''. t('Bolded') .''),
+ 'u' => array( t('Underlined'), ''. t('Underlined') .''),
+ 'i' => array( t('Italicized'), ''. t('Italicized') .''),
+ 'sup' => array( t('Superscripted'), t('Superscripted')),
+ 'sub' => array( t('Subscripted'), t('Subscripted')),
+ 'pre' => array( t('Preformatted'), ''. t('Preformatted') .'
'),
+ 'abbr' => array( t('Abbreviation'), t('Abbrev.')),
+ 'acronym' => array( t('Acronym'), t('TLA')),
+ 'blockquote' => array( t('Block quoted'), ''. t('Block quoted') .'
'),
+ 'q' => array( t('Quoted inline'), ''. t('Quoted inline') .'
'),
+ // Assumes and describes tr, td, th.
+ 'table' => array( t('Table'), ' '. t('Table header') .' '. t('Table cell') .'
'),
+ 'tr' => NULL, 'td' => NULL, 'th' => NULL,
+ 'del' => array( t('Deleted'), ''. t('Deleted') .''),
+ 'ins' => array( t('Inserted'), ''. t('Inserted') .''),
+ // Assumes and describes li.
+ 'ol' => array( t('Ordered list - use the <li> to begin each list item'), ' - '. t('First item') .'
- '. t('Second item') .'
'),
+ 'ul' => array( t('Unordered list - use the <li> to begin each list item'), ' - '. t('First item') .'
- '. t('Second item') .'
'),
+ 'li' => NULL,
+ // Assumes and describes dt and dd.
+ 'dl' => array( t('Definition lists are similar to other HTML lists. <dl> begins the definition list, <dt> begins the definition term and <dd> begins the definition description.'), ' - '. t('First term') .'
- '. t('First definition') .'
- '. t('Second term') .'
- '. t('Second definition') .'
'),
+ 'dt' => NULL, 'dd' => NULL,
+ 'h1' => array( t('Header'), ''. t('Title') .'
'),
+ 'h2' => array( t('Header'), ''. t('Subtitle') .'
'),
+ 'h3' => array( t('Header'), ''. t('Subtitle three') .'
'),
+ 'h4' => array( t('Header'), ''. t('Subtitle four') .'
'),
+ 'h5' => array( t('Header'), ''. t('Subtitle five') .'
'),
+ 'h6' => array( t('Header'), ''. t('Subtitle six') .'
')
+ );
+ $header = array(t('Tag Description'), t('You Type'), t('You Get'));
+ preg_match_all('/<([a-z0-9]+)[^a-z0-9]/i', $allowed_html, $out);
+ foreach ($out[1] as $tag) {
+ if (array_key_exists($tag, $tips)) {
+ if ($tips[$tag]) {
+ $rows[] = array(
+ array('data' => $tips[$tag][0], 'class' => 'description'),
+ array('data' => ''. check_plain($tips[$tag][1]) .'
', 'class' => 'type'),
+ array('data' => $tips[$tag][1], 'class' => 'get')
+ );
+ }
+ }
+ else {
+ $rows[] = array(
+ array('data' => t('No help provided for tag %tag.', array('%tag' => check_plain($tag))), 'class' => 'description', 'colspan' => 3),
+ );
+ }
+ }
+ $output .= theme('table', $header, $rows);
+
+ $output .= t('
+Most unusual characters can be directly entered without any problems.
+If you do encounter problems, try using HTML character entities. A common example looks like & for an ampersand & character. For a full list of entities see HTML\'s entities page. Some of the available characters include:
');
+ $entities = array(
+ array( t('Ampersand'), '&'),
+ array( t('Greater than'), '>'),
+ array( t('Less than'), '<'),
+ array( t('Quotation mark'), '"'),
+ );
+ $header = array(t('Character Description'), t('You Type'), t('You Get'));
+ unset($rows);
+ foreach ($entities as $entity) {
+ $rows[] = array(
+ array('data' => $entity[0], 'class' => 'description'),
+ array('data' => ''. check_plain($entity[1]) .'
', 'class' => 'type'),
+ array('data' => $entity[1], 'class' => 'get')
+ );
+ }
+ $output .= theme('table', $header, $rows);
+ return $output;
+ }
+ }
+ else {
+ return t('No HTML tags allowed');
+ }
+ }
+ break;
+
+ case 1:
+ switch ($long) {
+ case 0:
+ return t('You may post PHP code. You should include <?php ?> tags.');
+ case 1:
+ return t('
+Using custom PHP code
+If you know how to script in PHP, Drupal gives you the power to embed any script you like. It will be executed when the page is viewed and dynamically embedded into the page. This gives you amazing flexibility and power, but of course with that comes danger and insecurity if you don\'t write good code. If you are not familiar with PHP, SQL or with the site engine, avoid experimenting with PHP because you can corrupt your database or render your site insecure or even unusable! If you don\'t plan to do fancy stuff with your content then you\'re probably better off with straight HTML.
+Remember that the code within each PHP item must be valid PHP code - including things like correctly terminating statements with a semicolon. It is highly recommended that you develop your code separately using a simple test script on top of a test database before migrating to your production environment.
+Notes:
- You can use global variables, such as configuration parameters, within the scope of your PHP code but remember that global variables which have been given values in your code will retain these values in the engine afterwards.
- register_globals is now set to off by default. If you need form information you need to get it from the "superglobals" $_POST, $_GET, etc.
- You can either use the
print
or return
statement to output the actual content for your item.
+A basic example:
+You want to have a box with the title "Welcome" that you use to greet your visitors. The content for this box could be created by going:
+
+ print t("Welcome visitor, ... welcome message goes here ...");
+
+If we are however dealing with a registered user, we can customize the message by using:
+
+ global $user;
+ if ($user->uid) {
+ print t("Welcome $user->name, ... welcome message goes here ...");
+ }
+ else {
+ print t("Welcome visitor, ... welcome message goes here ...");
+ }
+
+For more in-depth examples, we recommend that you check the existing Drupal code and use it as a starting point, especially for sidebar boxes.
');
+ }
+
+ case 2:
+ switch ($long) {
+ case 0:
+ return t('Lines and paragraphs break automatically.');
+ case 1:
+ return t('Lines and paragraphs are automatically recognized. The <br /> line break, <p> paragraph and </p> close paragraph tags are inserted automatically. If paragraphs are not recognized simply add a couple blank lines.');
+ }
+ }
+}
+
+/**
+ * Displays a list of all input formats and which one is the default
+ */
+function filter_admin_overview() {
+
+ // Overview of all formats.
+ $formats = filter_formats();
+ $error = false;
+
+ $rows = array();
+ 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;
+ }
+ }
+ $row = array();
+ $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/filters/'. $id));
+ $form[$format->name]['delete'] = array('#value' => $default ? '' : l(t('delete'), 'admin/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 drupal_get_form('filter_admin_overview', $form);
+}
+
+function filter_admin_overview_submit($form_id, $form_values) {
+ // Process form submission to set the default format
+ if (is_numeric($form_values['default'])) {
+ drupal_set_message(t('Default format updated.'));
+ variable_set('filter_default_format', $form_values['default']);
+ }
+}
+
+function theme_filter_admin_overview($form) {
+ foreach ($form as $name => $element) {
+ if (isset($element['roles']) && is_array($element['roles'])) {
+ $rows[] = array(
+ form_render($form['default'][$element['id']['#value']]),
+ check_plain($name),
+ form_render($element['roles']),
+ form_render($element['configure']),
+ form_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 .= form_render($form);
+
+ return $output;
+}
+
+/**
+ * Menu callback; confirm deletion of a format.
+ */
+function filter_admin_delete() {
+ $format = arg(3);
+ $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('filter_admin_delete', $form, t('Are you sure you want to delete the input format %format?', array('%format' => theme('placeholder', $format->name))), 'admin/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/filters');
+ }
+ }
+ else {
+ drupal_not_found();
+ }
+}
+
+/**
+ * Process filter delete form submission.
+ */
+function filter_admin_delete_submit($form_id, $form_values) {
+ db_query("DELETE FROM {filter_formats} WHERE format = %d", $form_values['format']);
+ db_query("DELETE FROM {filters} WHERE format = %d", $form_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_values['format']);
+ db_query("UPDATE {comments} SET format = %d WHERE format = %d", $default, $form_values['format']);
+ db_query("UPDATE {boxes} SET format = %d WHERE format = %d", $default, $form_values['format']);
+
+ cache_clear_all('filter:'. $form_values['format'], true);
+ drupal_set_message(t('Deleted input format %format.', array('%format' => theme('placeholder', $form_values['name']))));
+
+ return 'admin/filters';
+}
+
+/**
+ * Generate a filter format form.
+ */
+function filter_admin_format_form($format = NULL) {
+ $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' => '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]['#attributes'] = array('disabled' => 'disabled');
+ }
+ }
+ // 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),
+ );
+ }
+ $form['submit'] = array('#type' => 'submit', '#value' => t('Save configuration'));
+
+ if (isset($format)) {
+ $form['format'] = array('#type' => 'hidden', '#value' => $format->format);
+
+ // Composition tips (guidelines)
+ $tips = _filter_tips($format->format, false);
+ $extra = l(t('More information about formatting options'), 'filter/tips');
+ $tiplist = theme('filter_tips', $tips, false, $extra);
+ if (!$tiplist) {
+ $tiplist = t('No guidelines available.
');
+ }
+ $group = t('These are the guidelines that users will see for posting in this input format. They are automatically generated from the filter settings.
');
+ $group .= $tiplist;
+ $output = ''. t('Formatting guidelines') .'
'. $group;
+ }
+ $output = drupal_get_form('filter_admin_format_form', $form) . $output;
+
+ return $output;
+}
+
+/**
+ * Validate filter format form submissions.
+ */
+function filter_admin_format_form_validate($form_id, $form_values) {
+ if (!isset($form_values['format'])) {
+ $name = trim($form_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' => theme('placeholder', $name))));
+ }
+ }
+}
+
+/**
+ * Process filter format form submissions.
+ */
+function filter_admin_format_form_submit($form_id, $form_values) {
+ $format = isset($form_values['format']) ? $form_values['format'] : NULL;
+ $current = filter_list_format($format);
+ $name = trim($form_values['name']);
+ $cache = TRUE;
+
+ // Add a new filter format.
+ if (!$format) {
+ $new = TRUE;
+ db_query("INSERT INTO {filter_formats} (name) VALUES ('%s')", $name);
+ $result = db_fetch_object(db_query("SELECT MAX(format) AS format FROM {filter_formats}"));
+ $format = $result->format;
+ drupal_set_message(t('Added input format %format.', array('%format' => theme('placeholder', $name))));
+ }
+ else {
+ drupal_set_message(t('The input format settings have been updated.'));
+ }
+
+ db_query("DELETE FROM {filters} WHERE format = %d", $format);
+ foreach ($form_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_values['roles'])) {
+ foreach ($form_values['roles'] as $id => $checked) {
+ if ($checked) {
+ $roles[] = $id;
+ }
+ }
+ }
+ $roles = ','. implode(',', ($form_values['default_format'] ? user_roles() : $roles)) .',';
+
+ db_query("UPDATE {filter_formats} SET cache = %d, name='%s', roles = '%s' WHERE format = %d", $cache, $name, $roles, $format);
+
+ cache_clear_all('filter:'. $format, true);
+
+ // If a new filter was added, return to the main list of filters. Otherwise, stay on edit filter page to show new changes.
+ if ($new) {
+ return 'admin/filters/';
+ }
+ else {
+ return 'admin/filters/'. $format;
+ }
+}
+
+/**
+ * Menu callback; display form for ordering filters for a format.
+ */
+function filter_admin_order($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 drupal_get_form('filter_admin_order', $form);
+}
+
+/**
+ * Theme filter order configuration form.
+ */
+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])) {
+ $rows[] = array(form_render($form['names'][$id]), form_render($form['weights'][$id]));
+ }
+ }
+
+ $output = theme('table', $header, $rows);
+ $output .= form_render($form);
+
+ return $output;
+}
+
+/**
+ * Process filter order configuration form submission.
+ */
+function filter_admin_order_submit($form_id, $form_values) {
+ foreach ($form_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_values['format'], $module, $delta);
+ }
+ drupal_set_message(t('The filter ordering has been saved.'));
+
+ cache_clear_all('filter:'. $form_values['format'], true);
+}
+
+/**
+ * Menu callback; display settings defined by filters.
+ */
+function filter_admin_configure() {
+ $format = arg(2);
+
+ $list = filter_list_format($format);
+ $form = array();
+ foreach ($list as $filter) {
+ $form_module = module_invoke($filter->module, 'filter', 'settings', $filter->delta, $format);
+ if (isset($form_module) && is_array($form_module)) {
+ $form = array_merge($form, $form_module);
+ }
+ }
+
+ if (!empty($form)) {
+ $output = system_settings_form('filter_admin_configure', $form);
+ }
+ else {
+ $output = t('No settings are available.');
+ }
+
+ return $output;
+}
+
+/**
+ * Retrieve a list of input formats.
+ */
+function filter_formats() {
+ global $user;
+ static $formats;
+
+ // Administrators can always use all input formats.
+ $all = user_access('administer filters');
+
+ if (!isset($formats)) {
+ $formats = array();
+
+ $query = 'SELECT * FROM {filter_formats}';
+
+ // Build query for selecting the format(s) based on the user's roles.
+ if (!$all) {
+ $where = array();
+ foreach ($user->roles as $rid => $role) {
+ $where[] = "roles LIKE '%%,%d,%%'";
+ $args[] = $rid;
+ }
+ $query .= ' WHERE '. implode(' OR ', $where) . ' OR format = %d';
+ $args[] = variable_get('filter_default_format', 1);
+ }
+
+ $result = db_query($query, $args);
+ while ($format = db_fetch_object($result)) {
+ $formats[$format->format] = $format;
+ }
+ }
+ return $formats;
+}
+
+/**
+ * Build a list of all filters.
+ */
+function filter_list_all() {
+ $filters = array();
+
+ foreach (module_list() as $module) {
+ $list = module_invoke($module, 'filter', 'list');
+ if (isset($list) && is_array($list)) {
+ foreach ($list as $delta => $name) {
+ $filters[$module .'/'. $delta] = (object)array('module' => $module, 'delta' => $delta, 'name' => $name);
+ }
+ }
+ }
+
+ uasort($filters, '_filter_list_cmp');
+
+ return $filters;
+}
+
+/**
+ * Helper function for sorting the filter list by filter name.
+ */
+function _filter_list_cmp($a, $b) {
+ return strcmp($a->name, $b->name);
+}
+
+/**
+ * Resolve a format id, including the default format.
+ */
+function filter_resolve_format($format) {
+ return $format == FILTER_FORMAT_DEFAULT ? variable_get('filter_default_format', 1) : $format;
+}
+/**
+ * Check if text in a certain input format is allowed to be cached.
+ */
+function filter_format_allowcache($format) {
+ static $cache = array();
+ $format = filter_resolve_format($format);
+ if (!isset($cache[$format])) {
+ $cache[$format] = db_result(db_query('SELECT cache FROM {filter_formats} WHERE format = %d', $format));
+ }
+ return $cache[$format];
+}
+
+/**
+ * Retrieve a list of filters for a certain format.
+ */
+function filter_list_format($format) {
+ static $filters = array();
+
+ if (!isset($filters[$format])) {
+ $filters[$format] = array();
+ $result = db_query("SELECT * FROM {filters} WHERE format = %d ORDER BY weight ASC", $format);
+ while ($filter = db_fetch_object($result)) {
+ $list = module_invoke($filter->module, 'filter', 'list');
+ if (isset($list) && is_array($list) && isset($list[$filter->delta])) {
+ $filter->name = $list[$filter->delta];
+ $filters[$format][$filter->module .'/'. $filter->delta] = $filter;
+ }
+ }
+ }
+
+ return $filters[$format];
+}
+
+/**
+ * @name Filtering functions
+ * @{
+ * Modules which need to have content filtered can use these functions to
+ * interact with the filter system.
+ *
+ * For more info, see the hook_filter() documentation.
+ *
+ * Note: because filters can inject JavaScript or execute PHP code, security is
+ * vital here. When a user supplies a $format, you should validate it with
+ * filter_access($format) before accepting/using it. This is normally done in
+ * the validation stage of the node system. You should for example never make a
+ * preview of content in a disallowed format.
+ */
+
+/**
+ * Run all the enabled filters on a piece of text.
+ *
+ * @param $text
+ * The text to be filtered.
+ * @param $format
+ * The format of the text to be filtered. Specify FILTER_FORMAT_DEFAULT for
+ * the default format.
+ * @param $check
+ * Whether to check the $format with filter_access() first. Defaults to TRUE.
+ * Note that this will check the permissions of the current user, so you
+ * should specify $check = FALSE when viewing other people's content. When
+ * showing content that is not (yet) stored in the database (eg. upon preview),
+ * set to TRUE so the user's permissions are checked.
+ */
+function check_markup($text, $format = FILTER_FORMAT_DEFAULT, $check = TRUE) {
+ // When $check = true, do an access check on $format.
+ if (isset($text) && (!$check || filter_access($format))) {
+ $format = filter_resolve_format($format);
+
+ // Check for a cached version of this piece of text.
+ $id = 'filter:'. $format .':'. md5($text);
+ if ($cached = cache_get($id)) {
+ return $cached->data;
+ }
+
+ // See if caching is allowed for this format.
+ $cache = filter_format_allowcache($format);
+
+ // Convert all Windows and Mac newlines to a single newline,
+ // so filters only need to deal with one possibility.
+ $text = str_replace(array("\r\n", "\r"), "\n", $text);
+
+ // Get a complete list of filters, ordered properly.
+ $filters = filter_list_format($format);
+
+ // Give filters the chance to escape HTML-like data such as code or formulas.
+ foreach ($filters as $filter) {
+ $text = module_invoke($filter->module, 'filter', 'prepare', $filter->delta, $format, $text);
+ }
+
+ // Perform filtering.
+ foreach ($filters as $filter) {
+ $text = module_invoke($filter->module, 'filter', 'process', $filter->delta, $format, $text);
+ }
+
+ // Store in cache with a minimum expiration time of 1 day.
+ if ($cache) {
+ cache_set($id, $text, time() + (60 * 60 * 24));
+ }
+ }
+ else {
+ $text = message_na();
+ }
+
+ return $text;
+}
+
+/**
+ * Generate a selector for choosing a format in a form.
+ *
+ * @param $value
+ * The ID of the format that is currently selected.
+ * @param $weight
+ * The weight of the input format.
+ * @param $parents
+ * Required when defining multiple input formats on a single node or having a different parent than 'format'.
+ * @return
+ * HTML for the form element.
+ */
+function filter_form($value = FILTER_FORMAT_DEFAULT, $weight = NULL, $parents = array('format')) {
+ $value = filter_resolve_format($value);
+ $formats = filter_formats();
+
+ $extra = l(t('More information about formatting options'), 'filter/tips');
+
+ if (count($formats) > 1) {
+ $form = array(
+ '#type' => 'fieldset',
+ '#title' => t('Input format'),
+ '#collapsible' => TRUE,
+ '#collapsed' => TRUE,
+ '#weight' => $weight,
+ '#validate' => array('filter_form_validate' => array()),
+ );
+ // Multiple formats available: display radio buttons with tips.
+ foreach ($formats as $format) {
+ $form[$format->format] = array(
+ '#type' => 'radio',
+ '#title' => $format->name,
+ '#default_value' => $value,
+ '#return_value' => $format->format,
+ '#parents' => $parents,
+ '#description' => theme('filter_tips', _filter_tips($format->format, false)),
+ );
+ }
+ }
+ else {
+ // Only one format available: use a hidden form item and only show tips.
+ $format = array_shift($formats);
+ $form[$format->format] = array('#type' => 'value', '#value' => $format->format, '#parents' => $parents);
+ $tips = _filter_tips(variable_get('filter_default_format', 1), false);
+ $form['format']['guidelines'] = array(
+ '#title' => t('Formatting guidelines'),
+ '#value' => theme('filter_tips', $tips, false, $extra),
+ );
+ }
+ $form[] = array(
+ '#type' => 'markup',
+ '#value' => $extra,
+ );
+ return $form;
+}
+
+function filter_form_validate($form) {
+ foreach (element_children($form) as $key) {
+ if ($form[$key]['#value'] == $form[$key]['#return_value']) {
+ return;
+ }
+ }
+ form_error($form, t('An illegal choice has been detected. Please contact the site administrator.'));
+ watchdog('form', t('Illegal choice %choice in %name element.', array('%choice' => theme('placeholder', check_plain($v)), '%name' => theme('placeholder', empty($form['#title']) ? $form['#parents'][0] : $form['#title']))), WATCHDOG_ERROR);
+}
+
+/**
+ * Returns true if the user is allowed to access this format.
+ */
+function filter_access($format) {
+ $format = filter_resolve_format($format);
+ if (user_access('administer filters') || ($format == variable_get('filter_default_format', 1))) {
+ return true;
+ }
+ else {
+ $formats = filter_formats();
+ return isset($formats[$format]);
+ }
+}
+/**
+ * @} End of "Filtering functions".
+ */
+
+/**
+ * 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;
+}
+
+/**
+ * Helper function for fetching filter tips.
+ */
+function _filter_tips($format, $long = false) {
+ if ($format == -1) {
+ $formats = filter_formats();
+ }
+ else {
+ $formats = array(db_fetch_object(db_query("SELECT * FROM {filter_formats} WHERE format = %d", $format)));
+ }
+
+ $tips = array();
+
+ foreach ($formats as $format) {
+ $filters = filter_list_format($format->format);
+
+ $tips[$format->name] = array();
+ foreach ($filters as $id => $filter) {
+ if ($tip = module_invoke($filter->module, 'filter_tips', $filter->delta, $format->format, $long)) {
+ $tips[$format->name][] = array('tip' => $tip, 'id' => $id);
+ }
+ }
+ }
+
+ return $tips;
+}
+
+/**
+ * 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 .= '';
+ }
+ foreach ($tips as $name => $tiplist) {
+ if ($multiple) {
+ $output .= '- ';
+ $output .= ''. $name .':
';
+ }
+
+ $tips = '';
+ foreach ($tiplist as $tip) {
+ $tips .= ' - ' : '>') . $tip['tip'] . '
';
+ }
+
+ if ($tips) {
+ $output .= "$tips
";
+ }
+
+ if ($multiple) {
+ $output .= '
';
+ }
+ }
+ if ($multiple) {
+ $output .= '
';
+ }
+ }
+
+ return $output;
+}
+
+/**
+ * @name Standard filters
+ * @{
+ * Filters implemented by the filter.module.
+ */
+
+/**
+ * Implementation of hook_filter(). Contains a basic set of essential filters.
+ * - HTML filter:
+ * Validates user-supplied HTML, transforming it as necessary.
+ * - PHP evaluator:
+ * Executes PHP code.
+ * - Line break converter:
+ * Converts newlines into paragraph and break tags.
+ */
+function filter_filter($op, $delta = 0, $format = -1, $text = '') {
+ switch ($op) {
+ case 'list':
+ return array(0 => t('HTML filter'), 1 => t('PHP evaluator'), 2 => t('Line break converter'));
+
+ case 'no cache':
+ return $delta == 1; // No caching for the PHP evaluator.
+
+ case 'description':
+ switch ($delta) {
+ case 0:
+ return t('Allows you to restrict if users can post HTML and which tags to filter out.');
+ case 1:
+ return t('Runs a piece of PHP code. The usage of this filter should be restricted to administrators only!');
+ case 2:
+ return t('Converts line breaks into HTML (i.e. <br> and <p> tags).');
+ default:
+ return;
+ }
+
+ case 'process':
+ switch ($delta) {
+ case 0:
+ return _filter_html($text, $format);
+ case 1:
+ return drupal_eval($text);
+ case 2:
+ return _filter_autop($text);
+ default:
+ return $text;
+ }
+
+ case 'settings':
+ switch ($delta) {
+ case 0:
+ return _filter_html_settings($format);
+ default:
+ return;
+ }
+
+ default:
+ return $text;
+ }
+}
+
+/**
+ * Settings for the HTML filter.
+ */
+function _filter_html_settings($format) {
+ $form['filter_html'] = array('#type' => 'fieldset', '#title' => t('HTML filter'), '#collapsible' => TRUE, '#collapsed' => TRUE);
+ $form['filter_html']["filter_html_$format"] = array('#type' => 'radios', '#title' => t('Filter HTML tags'), '#default_value' => variable_get("filter_html_$format", FILTER_HTML_STRIP), '#options' => array(FILTER_HTML_STRIP => t('Strip disallowed tags'), FILTER_HTML_ESCAPE => t('Escape all tags')), '#description' => t('How to deal with HTML tags in user-contributed content. If set to "Strip disallowed tags", dangerous tags are removed (see below). If set to "Escape tags", all HTML is escaped and presented as it was typed.'));
+ $form['filter_html']["allowed_html_$format"] = array('#type' => 'textfield', '#title' => t('Allowed HTML tags'), '#default_value' => variable_get("allowed_html_$format", ' -
-
- '), '#size' => 64, '#maxlength' => 255, '#description' => t('If "Strip disallowed tags" is selected, optionally specify tags which should not be stripped. JavaScript event attributes are always stripped.'));
+ $form['filter_html']["filter_html_help_$format"] = array('#type' => 'checkbox', '#title' => t('Display HTML help'), '#default_value' => variable_get("filter_html_help_$format", 1), '#description' => t('If enabled, Drupal will display some basic HTML help in the long filter tips.'));
+ $form['filter_html']["filter_html_nofollow_$format"] = array('#type' => 'checkbox', '#title' => t('Spam link deterrent'), '#default_value' => variable_get("filter_html_nofollow_$format", FALSE), '#description' => t('If enabled, Drupal will add rel="nofollow" to all links, as a measure to reduce the effectiveness of spam links. Note: this will also prevent valid links from being followed by search engines, therefore it is likely most effective when enabled for anonymous users.'));
+ return $form;
+}
+
+/**
+ * HTML filter. Provides filtering of input into accepted HTML.
+ */
+function _filter_html($text, $format) {
+ if (variable_get("filter_html_$format", FILTER_HTML_STRIP) == FILTER_HTML_STRIP) {
+ $allowed_tags = preg_split('/\s+|<|>/', variable_get("allowed_html_$format", '
-
-
- '), -1, PREG_SPLIT_NO_EMPTY);
+ $text = filter_xss($text, $allowed_tags);
+ }
+
+ if (variable_get("filter_html_$format", FILTER_HTML_STRIP) == FILTER_HTML_ESCAPE) {
+ // Escape HTML
+ $text = check_plain($text);
+ }
+
+ if (variable_get("filter_html_nofollow_$format", FALSE)) {
+ $text = preg_replace('/]+)>/i', '', $text);
+ }
+
+ return trim($text);
+}
+
+/**
+ * Convert line breaks into
and
in an intelligent fashion.
+ * Based on: http://photomatt.net/scripts/autop
+ */
+function _filter_autop($text) {
+ // All block level tags
+ $block = '(?:table|thead|tfoot|caption|colgroup|tbody|tr|td|th|div|dl|dd|dt|ul|ol|li|pre|select|form|blockquote|address|p|h[1-6])';
+
+ // Split at
, , tags.
+ // We don't apply any processing to the contents of these tags to avoid messing
+ // up code. We look for matched pairs and allow basic nesting. For example:
+ // "processed ignored ignored
processed"
+ $chunks = preg_split('@(?(?:pre|script|style)[^>]*>)@i', $text, -1, PREG_SPLIT_DELIM_CAPTURE);
+ // Note: PHP ensures the array consists of alternating delimiters and literals
+ // and begins and ends with a literal (inserting NULL as required).
+ $ignore = false;
+ $ignoretag = '';
+ $output = '';
+ foreach ($chunks as $i => $chunk) {
+ if ($i % 2) {
+ // Opening or closing tag?
+ $open = ($chunk[1] != '/');
+ list($tag) = split('[ >]', substr($chunk, 2 - $open), 2);
+ if (!$ignore) {
+ if ($open) {
+ $ignore = true;
+ $ignoretag = $tag;
+ }
+ }
+ // Only allow a matching tag to close it.
+ else if (!$open && $ignoretag == $tag) {
+ $ignore = false;
+ $ignoretag = '';
+ }
+ }
+ else if (!$ignore) {
+ $chunk = preg_replace('|\n*$|', '', $chunk) ."\n\n"; // just to make things a little easier, pad the end
+ $chunk = preg_replace('|
\s*
|', "\n\n", $chunk);
+ $chunk = preg_replace('!(<'. $block .'[^>]*>)!', "\n$1", $chunk); // Space things out a little
+ $chunk = preg_replace('!('. $block .'>)!', "$1\n\n", $chunk); // Space things out a little
+ $chunk = preg_replace("/\n\n+/", "\n\n", $chunk); // take care of duplicates
+ $chunk = preg_replace('/\n?(.+?)(?:\n\s*\n|\z)/s', "$1
\n", $chunk); // make paragraphs, including one at the end
+ $chunk = preg_replace('|\s*?
|', '', $chunk); // under certain strange conditions it could create a P of entirely whitespace
+ $chunk = preg_replace("|(
|", "$1", $chunk); // problem with nested lists
+ $chunk = preg_replace('|]*)>|i', "", $chunk);
+ $chunk = str_replace('
', '
', $chunk);
+ $chunk = preg_replace('!\s*(?'. $block .'[^>]*>)!', "$1", $chunk);
+ $chunk = preg_replace('!(?'. $block .'[^>]*>)\s*
!', "$1", $chunk);
+ $chunk = preg_replace('|(?)\s*\n|', "
\n", $chunk); // make line breaks
+ $chunk = preg_replace('!(?'. $block .'[^>]*>)\s*
!', "$1", $chunk);
+ $chunk = preg_replace('!
(\s*?(?:p|li|div|th|pre|td|ul|ol)>)!', '$1', $chunk);
+ $chunk = preg_replace('/&([^#])(?![A-Za-z0-9]{1,8};)/', '&$1', $chunk);
+ }
+ $output .= $chunk;
+ }
+ return $output;
+}
+
+/**
+ * Very permissive XSS/HTML filter for admin-only use.
+ *
+ * Use only for fields where it is impractical to use the
+ * whole filter system, but where some (mainly inline) mark-up
+ * is desired (so check_plain() is not acceptable).
+ *
+ * Allows all tags that can be used inside an HTML body, save
+ * for scripts and styles.
+ */
+function filter_xss_admin($string) {
+ return filter_xss($string, array('a', 'abbr', 'acronym', 'address', 'b', 'bdo', 'big', 'blockquote', 'br', 'caption', 'cite', 'code', 'col', 'colgroup', 'dd', 'del', 'dfn', 'div', 'dl', 'dt', 'em', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'ins', 'kbd', 'li', 'object', 'ol', 'p', 'param', 'pre', 'q', 'samp', 'small', 'span', 'strong', 'sub', 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr', 'tt', 'ul', 'var'));
+}
+
+/**
+ * Filters XSS. Based on kses by Ulf Harnhammar, see
+ * http://sourceforge.net/projects/kses
+ *
+ * For examples of various XSS attacks, see:
+ * http://ha.ckers.org/xss.html
+ *
+ * This code does four things:
+ * - Removes characters and constructs that can trick browsers
+ * - Makes sure all HTML entities are well-formed
+ * - Makes sure all HTML tags and attributes are well-formed
+ * - Makes sure no HTML tags contain URLs with a disallowed protocol (e.g. javascript:)
+ *
+ * @param $string
+ * The string with raw HTML in it. It will be stripped of everything that can cause
+ * an XSS attack.
+ * @param $allowed_tags
+ * An array of allowed tags.
+ * @param $format
+ * The format to use.
+ */
+function filter_xss($string, $allowed_tags = array('a', 'em', 'strong', 'cite', 'code', 'ul', 'ol', 'li', 'dl', 'dt', 'dd')) {
+ // Store the input format
+ _filter_xss_split($allowed_tags, TRUE);
+ // Remove NUL characters (ignored by some browsers)
+ $string = str_replace(chr(0), '', $string);
+ // Remove Netscape 4 JS entities
+ $string = preg_replace('%&\s*\{[^}]*(\}\s*;?|$)%', '', $string);
+
+ // Defuse all HTML entities
+ $string = str_replace('&', '&', $string);
+ // Change back only well-formed entities in our whitelist
+ // Named entities
+ $string = preg_replace('/&([A-Za-z][A-Za-z0-9]*;)/', '&\1', $string);
+ // Decimal numeric entities
+ $string = preg_replace('/&#([0-9]+;)/', '\1', $string);
+ // Hexadecimal numeric entities
+ $string = preg_replace('/&#[Xx]0*((?:[0-9A-Fa-f]{2})+;)/', '\1', $string);
+
+ return preg_replace_callback('%
+ (
+ <[^>]*.(>|$) # a string that starts with a <, up until the > or the end of the string
+ | # or
+ > # just a >
+ )%x', '_filter_xss_split', $string);
+}
+
+/**
+ * Processes an HTML tag.
+ *
+ * @param @m
+ * An array with various meaning depending on the value of $store.
+ * If $store is TRUE then the array contains the allowed tags.
+ * If $store is FALSE then the array has one element, the HTML tag to process.
+ * @param $store
+ * Whether to store $m.
+ * @return
+ * If the element isn't allowed, an empty string. Otherwise, the cleaned up
+ * version of the HTML element.
+ */
+function _filter_xss_split($m, $store = FALSE) {
+ static $allowed_html;
+
+ if ($store) {
+ $allowed_html = array_flip($m);
+ return;
+ }
+
+ $string = $m[1];
+
+ if (substr($string, 0, 1) != '<') {
+ // We matched a lone ">" character
+ return '>';
+ }
+
+ if (!preg_match('%^<\s*(/\s*)?([a-zA-Z0-9]+)([^>]*)>?$%', $string, $matches)) {
+ // Seriously malformed
+ return '';
+ }
+
+ $slash = trim($matches[1]);
+ $elem = &$matches[2];
+ $attrlist = &$matches[3];
+
+ if (!isset($allowed_html[strtolower($elem)])) {
+ // Disallowed HTML element
+ return '';
+ }
+
+ if ($slash != '') {
+ return "$elem>";
+ }
+
+ // Is there a closing XHTML slash at the end of the attributes?
+ // In PHP 5.1.0+ we could count the changes, currently we need a separate match
+ $xhtml_slash = preg_match('%\s?/\s*$%', $attrlist) ? ' /' : '';
+ $attrlist = preg_replace('%(\s?)/\s*$%', '\1', $attrlist);
+
+ // Clean up attributes
+ $attr2 = implode(' ', _filter_xss_attributes($attrlist));
+ $attr2 = preg_replace('/[<>]/', '', $attr2);
+ $attr2 = strlen($attr2) ? ' '. $attr2 : '';
+
+ return "<$elem$attr2$xhtml_slash>";
+}
+
+/**
+ * Processes a string of HTML attributes.
+ *
+ * @return
+ * Cleaned up version of the HTML attributes.
+ */
+function _filter_xss_attributes($attr) {
+ $attrarr = array();
+ $mode = 0;
+ $attrname = '';
+
+ while (strlen($attr) != 0) {
+ // Was the last operation successful?
+ $working = 0;
+
+ switch ($mode) {
+ case 0:
+ // Attribute name, href for instance
+ if (preg_match('/^([-a-zA-Z]+)/', $attr, $match)) {
+ $attrname = strtolower($match[1]);
+ $skip = ($attrname == 'style' || substr($attrname, 0, 2) == 'on');
+ $working = $mode = 1;
+ $attr = preg_replace('/^[-a-zA-Z]+/', '', $attr);
+ }
+
+ break;
+
+ case 1:
+ // Equals sign or valueless ("selected")
+ if (preg_match('/^\s*=\s*/', $attr)) {
+ $working = 1; $mode = 2;
+ $attr = preg_replace('/^\s*=\s*/', '', $attr);
+ break;
+ }
+
+ if (preg_match('/^\s+/', $attr)) {
+ $working = 1; $mode = 0;
+ if (!$skip) {
+ $attrarr[] = $attrname;
+ }
+ $attr = preg_replace('/^\s+/', '', $attr);
+ }
+
+ break;
+
+ case 2:
+ // Attribute value, a URL after href= for instance
+ if (preg_match('/^"([^"]*)"(\s+|$)/', $attr, $match)) {
+ $thisval = filter_xss_bad_protocol($match[1]);
+
+ if (!$skip) {
+ $attrarr[] = "$attrname=\"$thisval\"";
+ }
+ $working = 1;
+ $mode = 0;
+ $attr = preg_replace('/^"[^"]*"(\s+|$)/', '', $attr);
+ break;
+ }
+
+ if (preg_match("/^'([^']*)'(\s+|$)/", $attr, $match)) {
+ $thisval = filter_xss_bad_protocol($match[1]);
+
+ if (!$skip) {
+ $attrarr[] = "$attrname='$thisval'";;
+ }
+ $working = 1; $mode = 0;
+ $attr = preg_replace("/^'[^']*'(\s+|$)/", '', $attr);
+ break;
+ }
+
+ if (preg_match("%^([^\s\"']+)(\s+|$)%", $attr, $match)) {
+ $thisval = filter_xss_bad_protocol($match[1]);
+
+ if (!$skip) {
+ $attrarr[] = "$attrname=\"$thisval\"";
+ }
+ $working = 1; $mode = 0;
+ $attr = preg_replace("%^[^\s\"']+(\s+|$)%", '', $attr);
+ }
+
+ break;
+ }
+
+ if ($working == 0) {
+ // not well formed, remove and try again
+ $attr = preg_replace('/
+ ^
+ (
+ "[^"]*("|$) # - a string that starts with a double quote, up until the next double quote or the end of the string
+ | # or
+ \'[^\']*(\'|$)| # - a string that starts with a quote, up until the next quote or the end of the string
+ | # or
+ \S # - a non-whitespace character
+ )* # any number of the above three
+ \s* # any number of whitespaces
+ /x', '', $attr);
+ $mode = 0;
+ }
+ }
+
+ // the attribute list ends with a valueless attribute like "selected"
+ if ($mode == 1) {
+ $attrarr[] = $attrname;
+ }
+ return $attrarr;
+}
+
+/**
+ * Processes an HTML attribute value and ensures it does not contain an URL
+ * with a disallowed protocol (e.g. javascript:)
+ *
+ * @param $string
+ * The string with the attribute value.
+ * @param $decode
+ * Whether to decode entities in the $string. Set to FALSE if the $string
+ * is in plain text, TRUE otherwise. Defaults to TRUE.
+ * @return
+ * Cleaned up and HTML-escaped version of $string.
+ */
+function filter_xss_bad_protocol($string, $decode = TRUE) {
+ static $allowed_protocols;
+ if (!isset($allowed_protocols)) {
+ $allowed_protocols = array_flip(variable_get('filter_allowed_protocols', array('http', 'https', 'ftp', 'news', 'nntp', 'telnet', 'mailto', 'irc', 'ssh', 'sftp', 'webcal')));
+ }
+
+ // Get the plain text representation of the attribute value (i.e. its meaning)
+ if ($decode) {
+ $string = decode_entities($string);
+ }
+ // Remove soft hyphen
+ $string = str_replace(chr(194) . chr(173), '', $string);
+ // Strip protocols
+
+ do {
+ $before = $string;
+ $colonpos = strpos($string, ':');
+ if ($colonpos > 0) {
+ $protocol = substr($string, 0, $colonpos);
+ if (!isset($allowed_protocols[$protocol])) {
+ $string = substr($string, $colonpos + 1);
+ }
+ }
+ } while ($before != $string);
+ return check_plain($string);
+}
+
+/**
+ * @} End of "Standard filters".
+ */
+
diff --git a/modules/forum.module b/modules/forum.module
new file mode 100644
index 0000000..6d5fe65
--- /dev/null
+++ b/modules/forum.module
@@ -0,0 +1,1126 @@
+'. t('The forum module lets you create threaded discussion forums for a particular topic on your site. This is similar to a message board system such as phpBB. Forums are very useful because they allow community members to discuss topics with one another, and they are archived for future reference.') .'';
+ $output .= ''. t('Forums can be organized under what are called containers. Containers hold forums and, in turn, forums hold threaded discussions. Both containers and forums can be placed inside other containers and forums. By planning the structure of your containers and forums well, you make it easier for users to find a topic area of interest to them. Forum topics can be moved by selecting a different forum and can be left in the existing forum by selecting leave a shadow copy. Forum topics can also have their own URL.') .'
';
+ $output .= ''. t('Forums module requires Taxonomy and Comments module be enabled.') .'
';
+ $output .= t('You can
+
+- administer forums at administer >> forums.
+- enable the required comment and taxonomy modules at administer >> modules.
+- read about the comment module at administer >> help >> comment.
+- read about the taxonomy module at administer >> help >> taxonomy.
+
+', array('%admin-forum' => url('admin/forum'), '%admin-modules' => url('admin/modules'), '%admin-help-comment' => url('admin/help/comment'), '%admin-help-taxonomy' => url('admin/help/taxonomy')));
+ $output .= ''. t('For more information please read the configuration and customization handbook Forum page.', array('%forum' => 'http://drupal.org/handbook/modules/forum/')) .'
';
+ return $output;
+ case 'admin/modules#description':
+ return t('Enables threaded discussions about general topics.');
+ case 'admin/forum':
+ return t('This is a list of existing containers and forums that you can edit. Containers hold forums and, in turn, forums hold threaded discussions. Both containers and forums can be placed inside other containers and forums. By planning the structure of your containers and forums well, you make it easier for users to find a topic area of interest to them.
');
+ case 'admin/forum/add/container':
+ return t('Containers help you organize your forums. The job of a container is to hold, or contain, other forums that are related. For example, a container named "Food" might hold two forums named "Fruit" and "Vegetables".
');
+ case 'admin/forum/add/forum':
+ return t('A forum holds discussion topics that are related. For example, a forum named "Fruit" might contain topics titled "Apples" and "Bananas".
');
+ case 'admin/forum/configure':
+ return t('This is where you can configure system-wide options for how your forums act and display.');
+ case 'node/add#forum':
+ return t('Create a new topic for discussion in the forums.');
+ }
+}
+
+/**
+ * Implementation of hook_menu().
+ */
+function forum_menu($may_cache) {
+ $items = array();
+
+ if ($may_cache) {
+ $items[] = array('path' => 'node/add/forum',
+ 'title' => t('forum topic'),
+ 'access' => user_access('create forum topics'));
+ $items[] = array('path' => 'forum',
+ 'title' => t('forums'),
+ 'callback' => 'forum_page',
+ 'access' => user_access('access content'),
+ 'type' => MENU_SUGGESTED_ITEM);
+ $items[] = array('path' => 'admin/forum',
+ 'title' => t('forums'),
+ 'callback' => 'forum_overview',
+ 'access' => user_access('administer forums'),
+ 'type' => MENU_NORMAL_ITEM);
+ $items[] = array('path' => 'admin/forum/list',
+ 'title' => t('list'),
+ 'access' => user_access('administer forums'),
+ 'type' => MENU_DEFAULT_LOCAL_TASK,
+ 'weight' => -10);
+ $items[] = array('path' => 'admin/forum/add/container',
+ 'title' => t('add container'),
+ 'callback' => 'forum_form_container',
+ 'access' => user_access('administer forums'),
+ 'type' => MENU_LOCAL_TASK);
+ $items[] = array('path' => 'admin/forum/add/forum',
+ 'title' => t('add forum'),
+ 'callback' => 'forum_form_forum',
+ 'access' => user_access('administer forums'),
+ 'type' => MENU_LOCAL_TASK);
+ $items[] = array('path' => 'admin/forum/configure',
+ 'title' => t('configure'),
+ 'callback' => 'forum_admin_configure',
+ 'access' => user_access('administer forums'),
+ 'type' => MENU_LOCAL_TASK);
+ }
+ elseif (is_numeric(arg(4))) {
+ $term = taxonomy_get_term(arg(4));
+ // Check if this is a valid term.
+ if ($term) {
+ $items[] = array('path' => 'admin/forum/edit/container',
+ 'title' => t('edit container'),
+ 'callback' => 'forum_form_container',
+ 'callback arguments' => array((array)$term),
+ 'access' => user_access('administer forums'),
+ 'type' => MENU_CALLBACK);
+ $items[] = array('path' => 'admin/forum/edit/forum',
+ 'title' => t('edit forum'),
+ 'callback' => 'forum_form_forum',
+ 'callback arguments' => array((array)$term),
+ 'access' => user_access('administer forums'),
+ 'type' => MENU_CALLBACK);
+ }
+ }
+
+ return $items;
+}
+
+/**
+ * Implementation of hook_node_info().
+ */
+function forum_node_info() {
+ return array('forum' => array('name' => t('forum topic'), 'base' => 'forum'));
+}
+
+/**
+ * Implementation of hook_access().
+ */
+function forum_access($op, $node) {
+ global $user;
+
+ if ($op == 'create') {
+ return user_access('create forum topics');
+ }
+
+ if ($op == 'update' || $op == 'delete') {
+ if (user_access('edit own forum topics') && ($user->uid == $node->uid)) {
+ return TRUE;
+ }
+ }
+}
+
+/**
+ * Implementation of hook_perm().
+ */
+function forum_perm() {
+ return array('create forum topics', 'edit own forum topics', 'administer forums');
+}
+
+/**
+ * Implementation of hook_nodeapi().
+ */
+function forum_nodeapi(&$node, $op, $teaser, $page) {
+ switch ($op) {
+ case 'delete revision':
+ db_query('DELETE FROM {forum} WHERE vid = %d', $node->vid);
+ break;
+ }
+}
+
+/**
+ * Implementation of hook_taxonomy().
+ */
+function forum_taxonomy($op, $type, $term = NULL) {
+ if ($op == 'delete' && $term['vid'] == _forum_get_vid()) {
+ switch ($type) {
+ case 'term':
+ $results = db_query('SELECT f.nid FROM {forum} f WHERE f.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_settings
+ */
+function forum_admin_configure() {
+
+ $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('The default number of topics displayed per page; links to browse older messages are automatically being displayed.'),
+ );
+ $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('The default display order for topics.'),
+ );
+
+ return system_settings_form('forum_admin_configure', $form);
+}
+
+/**
+ * Implementation of hook_form_alter().
+ */
+function forum_form_alter($form_id, &$form) {
+ // hide critical options from forum vocabulary
+ if ($form_id == 'taxonomy_form_vocabulary') {
+ if ($form['vid']['#value'] == _forum_get_vid()) {
+ $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['nodes']['forum'] = array('#type' => 'checkbox', '#value' => 1, '#title' => t('forum topic'), '#attributes' => array('disabled' => '' ), '#description' => t('forum topic is affixed to the forum vocabulary.'));
+ $form['hierarchy'] = array('#type' => 'value', '#value' => 1);
+ unset($form['relations']);
+ unset($form['tags']);
+ unset($form['multiple']);
+ $form['required'] = array('#type' => 'value', '#value' => 1);
+ }
+ else {
+ unset($form['nodes']['forum']);
+ }
+ }
+}
+
+/**
+ * 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 FROM {node} n INNER JOIN {node_comment_statistics} l ON n.nid = l.nid WHERE n.status = 1 AND n.type = 'forum' ORDER BY l.last_comment_timestamp DESC");
+ $result = db_query_range($sql, 0, variable_get('forum_block_num_0', '5'));
+ if (db_num_rows($result)) {
+ $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 {node_comment_statistics} l ON n.nid = l.nid WHERE n.type = 'forum' AND n.status = 1 ORDER BY n.nid DESC");
+ $result = db_query_range($sql, 0, variable_get('forum_block_num_1', '5'));
+ if (db_num_rows($result)) {
+ $content = node_title_list($result);
+ }
+ break;
+ }
+
+ if ($content) {
+ $content .= ''. l(t('more'), 'forum', array('title' => t('Read the latest forum topics.'))) .'';
+ }
+
+ $block['subject'] = $title;
+ $block['content'] = $content;
+
+ return $block;
+ }
+ }
+}
+
+/**
+ * Implementation of hook_view().
+ */
+function forum_view(&$node, $teaser = FALSE, $page = FALSE) {
+ if ($page) {
+ $vocabulary = taxonomy_get_vocabulary(variable_get('forum_nav_vocabulary', ''));
+ // Breadcrumb navigation
+ $breadcrumb = array();
+ $breadcrumb[] = array('path' => 'forum', 'title' => $vocabulary->name);
+ if ($parents = taxonomy_get_parents_all($node->tid)) {
+ $parents = array_reverse($parents);
+ foreach ($parents as $p) {
+ $breadcrumb[] = array('path' => 'forum/'. $p->tid, 'title' => $p->name);
+ }
+ }
+ $breadcrumb[] = array('path' => 'node/'. $node->nid);
+ menu_set_location($breadcrumb);
+ }
+
+ $node = node_prepare($node, $teaser);
+
+ $node->body .= theme('forum_topic_navigation', $node);
+}
+
+/**
+ * Implementation of hook_submit().
+ *
+ * Check in particular that only a "leaf" term in the associated taxonomy
+ * vocabulary is selected, not a "container" term.
+ */
+function forum_submit(&$node) {
+ // Make sure all fields are set properly:
+ $node->icon = $node->icon ? $node->icon : '';
+
+ if ($node->taxonomy) {
+ // Extract the node's proper topic ID.
+ $vocabulary = variable_get('forum_nav_vocabulary', '');
+ foreach ($node->taxonomy as $term) {
+ if (db_result(db_query('SELECT COUNT(*) FROM {term_data} WHERE tid = %d AND vid = %d', $term, $vocabulary))) {
+ $node->tid = $term;
+ }
+ }
+ if ($node->tid && $node->shadow) {
+ // A shadow copy needs to be created. Retain existing term and add new term.
+ $terms = array_keys(taxonomy_node_get_terms($node->nid));
+ if (!in_array($node->tid, $terms)) {
+ $terms[] = $node->tid;
+ }
+ $node->taxonomy = $terms;
+ }
+ }
+}
+
+/**
+ * Implementation of hook_validate().
+ *
+ * Check in particular that only a "leaf" term in the associated taxonomy
+ * vocabulary is selected, not a "container" term.
+ */
+function forum_validate($node) {
+ if ($node->taxonomy) {
+ // Extract the node's proper topic ID.
+ $vocabulary = variable_get('forum_nav_vocabulary', '');
+ $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' => theme('placeholder', $term->name))));
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Implementation of hook_update().
+ */
+function forum_update($node) {
+ if ($node->revision) {
+ db_query("INSERT INTO {forum} (nid, vid, tid) VALUES (%d, %d, %d)", $node->nid, $node->vid, $node->tid);
+ }
+ else {
+ db_query('UPDATE {forum} SET tid = %d WHERE vid = %d', $node->tid, $node->vid);
+ }
+}
+
+/**
+ * Implementation of hook_form().
+ */
+function forum_form(&$node) {
+ $form['title'] = array('#type' => 'textfield', '#title' => t('Subject'), '#default_value' => $node->title, '#required' => TRUE, '#weight' => -5);
+
+ if ($node->nid) {
+ $forum_terms = taxonomy_node_get_terms_by_vocabulary(_forum_get_vid(), $node->nid);
+ // 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_filter']['body'] = array('#type' => 'textarea', '#title' => t('Body'), '#default_value' => $node->body, '#rows' => 20, '#required' => TRUE);
+ $form['body_filter']['format'] = filter_form($node->format);
+
+ return $form;
+}
+
+/**
+ * Implementation of hook_prepare; assign forum taxonomy when adding a topic from within a forum.
+ */
+function forum_prepare(&$node) {
+ if (!$node->nid) {
+ // new topic
+ $node->taxonomy[arg(3)]->vid = _forum_get_vid();
+ $node->taxonomy[arg(3)]->tid = arg(3);
+ }
+}
+
+/**
+ * Implementation of hook_insert().
+ */
+function forum_insert($node) {
+ db_query('INSERT INTO {forum} (nid, vid, tid) VALUES (%d, %d, %d)', $node->nid, $node->vid, $node->tid);
+}
+
+/**
+ * Implementation of hook_delete().
+ */
+function forum_delete(&$node) {
+ db_query('DELETE FROM {forum} WHERE nid = %d', $node->nid);
+}
+
+/**
+ * Returns a form for adding a container to the forum vocabulary
+ *
+ * @param $edit Associative array containing a container term to be added or edited.
+ */
+function forum_form_container($edit = array()) {
+ // Handle a delete operation.
+ if ($_POST['op'] == t('Delete') || $_POST['edit']['confirm']) {
+ return _forum_confirm_delete($edit['tid']);
+ }
+
+ $form['name'] = array(
+ '#title' => t('Container name'),
+ '#type' => 'textfield',
+ '#default_value' => $edit['name'],
+ '#maxlength' => 64,
+ '#description' => t('The container name is used to identify related forums.'),
+ '#required' => TRUE
+ );
+
+ $form['description'] = array(
+ '#type' => 'textarea',
+ '#title' => t('Description'),
+ '#default_value' => $edit['description'],
+ '#description' => t('The container description can give users more information about the forums it contains.')
+ );
+ $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('When listing containers, those with with light (small) weights get listed before containers with heavier (larger) weights. Containers with equal weights are sorted alphabetically.')
+ );
+
+ $form['vid'] = array('#type' => 'hidden',
+ '#value' => _forum_get_vid());
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Submit')
+ );
+ if ($edit['tid']) {
+ $form['delete'] = array('#type' => 'submit', '#value' => t('Delete'));
+ $form['tid'] = array('#type' => 'value', '#value' => $edit['tid']);
+ }
+
+ return drupal_get_form('forum_form_container', $form, 'forum_form');
+}
+
+/**
+ * Returns a form for adding a forum to the forum vocabulary
+ *
+ * @param $edit Associative array containing a forum term to be added or edited.
+ */
+function forum_form_forum($edit = array()) {
+ // Handle a delete operation.
+ if ($_POST['op'] == t('Delete') || $_POST['edit']['confirm']) {
+ return _forum_confirm_delete($edit['tid']);
+ }
+
+ $form['name'] = array('#type' => 'textfield',
+ '#title' => t('Forum name'),
+ '#default_value' => $edit['name'],
+ '#maxlength' => 64,
+ '#description' => t('The forum name is used to identify related discussions.'),
+ '#required' => TRUE,
+ );
+ $form['description'] = array('#type' => 'textarea',
+ '#title' => t('Description'),
+ '#default_value' => $edit['description'],
+ '#description' => t('The forum description can give users more information about the discussion topics it contains.'),
+ );
+ $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('When listing forums, those with lighter (smaller) weights get listed before containers with heavier (larger) weights. Forums with equal weights are sorted alphabetically.'),
+ );
+
+ $form['vid'] = array('#type' => 'hidden', '#value' => _forum_get_vid());
+ $form['submit' ] = array('#type' => 'submit', '#value' => t('Submit'));
+ if ($edit['tid']) {
+ $form['delete'] = array('#type' => 'submit', '#value' => t('Delete'));
+ $form['tid'] = array('#type' => 'hidden', '#value' => $edit['tid']);
+ }
+
+ return drupal_get_form('forum_form_forum', $form, 'forum_form');
+}
+
+/**
+ * Process forum form and container form submissions.
+ */
+function forum_form_submit($form_id, $form_values) {
+ if ($form_id == 'forum_form_container') {
+ $container = TRUE;
+ $type = t('forum container');
+ }
+ else {
+ $container = false;
+ $type = t('forum');
+ }
+
+ $status = taxonomy_save_term($form_values);
+ switch ($status) {
+ case SAVED_NEW:
+ if ($container) {
+ $containers = variable_get('forum_containers', array());
+ $containers[] = $form_values['tid'];
+ variable_set('forum_containers', $containers);
+ }
+ drupal_set_message(t('Created new %type %term.', array('%term' => theme('placeholder', $form_values['name']), '%type' => $type)));
+ break;
+ case SAVED_UPDATED:
+ drupal_set_message(t('The %type %term has been updated.', array('%term' => theme('placeholder', $form_values['name']), '%type' => $type)));
+ break;
+ }
+ return 'admin/forum';
+}
+
+/**
+ * Returns a confirmation page for deleting a forum taxonomy term.
+ *
+ * @param $tid ID of the term to be deleted
+ */
+function _forum_confirm_delete($tid) {
+ $term = taxonomy_get_term($tid);
+
+ $form['tid'] = array('#type' => 'value', '#value' => $tid);
+ $form['name'] = array('#type' => 'value', '#value' => $term->name);
+
+ return confirm_form('forum_confirm_delete', $form, t('Are you sure you want to delete the forum %name?', array('%name' => theme('placeholder', $term->name))), 'admin/forums', t('Deleting a forum or container will delete all sub-forums and associated posts as well. This action cannot be undone.'), t('Delete'), t('Cancel'));
+}
+
+/**
+ * Implementation of forms api _submit call. Deletes a forum after confirmation.
+ */
+function forum_confirm_delete_submit($form_id, $form_values) {
+ taxonomy_del_term($form_values['tid']);
+ drupal_set_message(t('The forum %term and all sub-forums and associated posts have been deleted.', array('%term' => theme('placeholder', $form_values['name']))));
+ watchdog('content', t('forum: deleted %term and all its sub-forums and associated posts.', array('%term' => theme('placeholder', $form_values['name']))));
+
+ return 'admin/forum';
+}
+
+/**
+ * Returns an overview list of existing forums and containers
+ */
+function forum_overview() {
+ $header = array(t('Name'), t('Operations'));
+
+ $tree = taxonomy_get_tree(_forum_get_vid());
+ if ($tree) {
+ foreach ($tree as $term) {
+ if (in_array($term->tid, variable_get('forum_containers', array()))) {
+ $rows[] = array(_taxonomy_depth($term->depth) .' '. check_plain($term->name), l(t('edit container'), "admin/forum/edit/container/$term->tid"));
+ }
+ else {
+ $rows[] = array(_taxonomy_depth($term->depth) .' '. check_plain($term->name), l(t('edit forum'), "admin/forum/edit/forum/$term->tid"));
+ }
+
+ }
+ }
+ else {
+ $rows[] = array(array('data' => '' . t('There are no existing containers or forums. You may add some on the add container or add forum pages.', array('%container' => url('admin/forum/add/container'), '%forum' => url('admin/forum/add/forum'))) . '', 'colspan' => 2));
+ }
+ return theme('table', $header, $rows);
+}
+
+/**
+ * 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;
+ }
+
+ $children = taxonomy_get_tree(_forum_get_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(_forum_get_vid());
+ $options[0] = '<'. t('root') .'>';
+ if ($tree) {
+ foreach ($tree as $term) {
+ if (!in_array($term->tid, $exclude)) {
+ $options[$term->tid] = _taxonomy_depth($term->depth) . $term->name;
+ }
+ }
+ }
+ if ($child_type == 'container') {
+ $description = t('Containers are usually placed at the top (root) level of your forum but you can also place a container inside a parent container or forum.');
+ }
+ else if ($child_type == 'forum') {
+ $description = t('You may place your forum inside a parent container or forum, or at the top (root) level of your forum.');
+ }
+
+ return array('#type' => 'select', '#title' => $title, '#default_value' => $parent, '#options' => $options, '#description' => $description, '#required' => TRUE);
+}
+
+function forum_term_path($term) {
+ return 'forum/'. $term->tid;
+}
+
+/**
+ * Returns the vocabulary id for forum navigation.
+ */
+function _forum_get_vid() {
+ $vid = variable_get('forum_nav_vocabulary', '');
+ if (empty($vid)) {
+ // Check to see if a forum vocabulary exists
+ $vid = db_result(db_query("SELECT vid FROM {vocabulary} WHERE module = '%s'", 'forum'));
+ if (!$vid) {
+ $edit = array('name' => 'Forums', 'multiple' => 0, 'required' => 1, 'hierarchy' => 1, 'relations' => 0, 'module' => 'forum', 'nodes' => array('forum' => 1));
+ taxonomy_save_vocabulary($edit);
+ $vid = $edit['vid'];
+ }
+ variable_set('forum_nav_vocabulary', $vid);
+ }
+
+ return $vid;
+}
+
+/**
+ * Formats a topic for display
+ *
+ * @TODO Give a better description. Not sure where this function is used yet.
+ */
+function _forum_format($topic) {
+ if ($topic && $topic->timestamp) {
+ return t('%time ago
by %author', array('%time' => format_interval(time() - $topic->timestamp), '%author' => theme('username', $topic)));
+ }
+ else {
+ return message_na();
+ }
+}
+
+/**
+ * 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();
+ $_forums = taxonomy_get_tree(variable_get('forum_nav_vocabulary', ''), $tid);
+
+ if (count($_forums)) {
+
+ $counts = array();
+
+ $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.nid = r.nid WHERE n.status = 1 AND n.type = 'forum' GROUP BY r.tid";
+ $sql = db_rewrite_sql($sql);
+ $_counts = db_query($sql, $forum->tid);
+ 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 ($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.nid = tn.nid 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();
+ $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(n.nid) FROM {node} n INNER JOIN {term_node} tn ON n.nid = tn.nid AND tn.tid = %d LEFT JOIN {history} h ON n.nid = h.nid AND h.uid = %d WHERE n.status = 1 AND n.type = 'forum' 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(
+ array('data' => ' '),
+ 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, f.tid, n.title, 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 FROM {node_comment_statistics} l, {users} cu, {term_node} r, {users} u, {forum} f, {node} n WHERE n.status = 1 AND l.last_comment_uid = cu.uid AND n.nid = l.nid AND n.nid = r.nid AND r.tid = %d AND n.uid = u.uid AND n.vid = f.vid");
+ $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(n.nid) FROM {node} n INNER JOIN {term_node} r ON n.nid = r.nid AND r.tid = %d WHERE n.status = 1 AND n.type = 'forum'");
+
+ $result = pager_query($sql, $forum_per_page, 0, $sql_count, $tid);
+
+ 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 n.type = 'forum' 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;
+}
+
+/**
+ * Menu callback; prints a forum listing.
+ */
+function forum_page($tid = 0) {
+ if (module_exist('taxonomy') && module_exist('comment')) {
+ $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('forum_display', $forums, $topics, $parents, $tid, $sortby, $forum_per_page);
+ }
+ else {
+ drupal_set_message(t('The forum module requires both the taxonomy module and the comment module to be enabled and configured.'), 'error');
+ return ' ';
+ }
+}
+
+/**
+ * Format the forum body.
+ *
+ * @ingroup themeable
+ */
+function theme_forum_display($forums, $topics, $parents, $tid, $sortby, $forum_per_page) {
+ global $user;
+ // forum list, topics list, topic browser and 'add new topic' link
+
+ $vocabulary = taxonomy_get_vocabulary(variable_get('forum_nav_vocabulary', ''));
+ $title = $vocabulary->name;
+
+ // Breadcrumb navigation:
+ $breadcrumb = array();
+ if ($tid) {
+ $breadcrumb[] = array('path' => 'forum', 'title' => $title);
+ }
+
+ if ($parents) {
+ $parents = array_reverse($parents);
+ foreach ($parents as $p) {
+ if ($p->tid == $tid) {
+ $title = $p->name;
+ }
+ else {
+ $breadcrumb[] = array('path' => 'forum/'. $p->tid, 'title' => $p->name);
+ }
+ }
+ }
+
+ drupal_set_title($title);
+
+ $breadcrumb[] = array('path' => $_GET['q']);
+ menu_set_location($breadcrumb);
+
+ if (count($forums) || count($parents)) {
+ $output = '';
+ $output .= '';
+
+ if (module_exist('tracker')) {
+ if ($user->uid) {
+ $output .= ' - '. l(t('My discussions.'), "tracker/$user->uid") .'
';
+ }
+
+ $output .= ' - '. l(t('Active discussions.'), 'tracker') .'
';
+ }
+
+ if (user_access('create forum topics')) {
+ $output .= '- '. l(t('Post new forum topic.'), "node/add/forum/$tid") .'
';
+ }
+ else if ($user->uid) {
+ $output .= '- '. t('You are not allowed to post a new forum topic.') .'
';
+ }
+ else {
+ $output .= '- '. t('Login to post a new forum topic.', array('%login' => url('user/login'))) .'
';
+ }
+ $output .= '
';
+
+ $output .= theme('forum_list', $forums, $parents, $tid);
+
+ if ($tid && !in_array($tid, variable_get('forum_containers', array()))) {
+ drupal_add_link(array('rel' => 'alternate',
+ 'type' => 'application/rss+xml',
+ 'title' => 'RSS - '. $title,
+ 'href' => url('taxonomy/term/'. $tid .'/0/feed')));
+
+ $output .= theme('forum_topic_list', $tid, $topics, $sortby, $forum_per_page);
+ $output .= theme('feed_icon', url("taxonomy/term/$tid/0/feed"));
+ }
+ $output .= '';
+ }
+ else {
+ drupal_set_title(t('No forums defined'));
+ $output = '';
+ }
+
+ return $output;
+}
+
+/**
+ * Format the forum listing.
+ *
+ * @ingroup themeable
+ */
+function theme_forum_list($forums, $parents, $tid) {
+ global $user;
+
+ if ($forums) {
+
+ $header = array(t('Forum'), t('Topics'), t('Posts'), t('Last post'));
+
+ foreach ($forums as $forum) {
+ if ($forum->container) {
+ $description = '\n";
+ $description .= ' '. l($forum->name, "forum/$forum->tid") ."\n";
+
+ if ($forum->description) {
+ $description .= ' '. filter_xss_admin($forum->description) ."\n";
+ }
+ $description .= "\n";
+
+ $rows[] = array(array('data' => $description, 'class' => 'container', 'colspan' => '4'));
+ }
+ else {
+ $new_topics = _forum_topics_unread($forum->tid, $user->uid);
+ $forum->old_topics = $forum->num_topics - $new_topics;
+ if (!$user->uid) {
+ $new_topics = 0;
+ }
+
+ $description = '\n";
+ $description .= ' '. l($forum->name, "forum/$forum->tid") ."\n";
+
+ if ($forum->description) {
+ $description .= ' '. filter_xss_admin($forum->description) ."\n";
+ }
+ $description .= "\n";
+
+ $rows[] = array(
+ array('data' => $description, 'class' => 'forum'),
+ array('data' => $forum->num_topics . ($new_topics ? '
'. l(format_plural($new_topics, '1 new', '%count new'), "forum/$forum->tid", NULL, NULL, 'new') : ''), 'class' => 'topics'),
+ array('data' => $forum->num_posts, 'class' => 'posts'),
+ array('data' => _forum_format($forum->last_post), 'class' => 'last-reply'));
+ }
+ }
+
+ return theme('table', $header, $rows);
+
+ }
+
+}
+
+/**
+ * Format the topic listing.
+ *
+ * @ingroup themeable
+ */
+function theme_forum_topic_list($tid, $topics, $sortby, $forum_per_page) {
+ global $forum_topic_list_header;
+
+ if ($topics) {
+
+ foreach ($topics as $topic) {
+ // folder is new if topic is new or there are new comments since last visit
+ if ($topic->tid != $tid) {
+ $rows[] = array(
+ array('data' => theme('forum_icon', $topic->new, $topic->num_comments, $topic->comment_mode, $topic->sticky), 'class' => 'icon'),
+ array('data' => check_plain($topic->title), 'class' => 'title'),
+ array('data' => l(t('This topic has been moved'), "forum/$topic->tid"), 'colspan' => '3')
+ );
+ }
+ else {
+ $rows[] = array(
+ array('data' => theme('forum_icon', $topic->new, $topic->num_comments, $topic->comment_mode, $topic->sticky), 'class' => 'icon'),
+ array('data' => l($topic->title, "node/$topic->nid"), 'class' => 'topic'),
+ array('data' => $topic->num_comments . ($topic->new_replies ? '
'. l(format_plural($topic->new_replies, '1 new', '%count new'), "node/$topic->nid", NULL, NULL, 'new') : ''), 'class' => 'replies'),
+ array('data' => _forum_format($topic), 'class' => 'created'),
+ array('data' => _forum_format($topic->last_reply), 'class' => 'last-reply')
+ );
+ }
+ }
+ }
+
+ $output .= theme('table', $forum_topic_list_header, $rows);
+ $output .= theme('pager', NULL, $forum_per_page, 0);
+
+ return $output;
+}
+
+/**
+ * Format the icon for each individual topic.
+ *
+ * @ingroup themeable
+ */
+function theme_forum_icon($new_posts, $num_posts = 0, $comment_mode = 0, $sticky = 0) {
+
+ if ($num_posts > variable_get('forum_hot_topic', 15)) {
+ $icon = $new_posts ? 'hot-new' : 'hot';
+ }
+ else {
+ $icon = $new_posts ? 'new' : 'default';
+ }
+
+ if ($comment_mode == COMMENT_NODE_READ_ONLY || $comment_mode == COMMENT_NODE_DISABLED) {
+ $icon = 'closed';
+ }
+
+ if ($sticky == 1) {
+ $icon = 'sticky';
+ }
+
+ $output = theme('image', "misc/forum-$icon.png");
+
+ if ($new_posts) {
+ $output = "$output";
+ }
+
+ return $output;
+}
+
+/**
+ * Format the next/previous forum topic navigation links.
+ *
+ * @ingroup themeable
+ */
+function theme_forum_topic_navigation($node) {
+ $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 AND n.type = 'forum' ORDER BY n.sticky DESC, ". _forum_get_topic_order_sql(variable_get('forum_order', 1));
+ $result = db_query(db_rewrite_sql($sql), $node->tid);
+
+ while ($topic = db_fetch_object($result)) {
+ if ($stop == 1) {
+ $next = new StdClass();
+ $next->nid = $topic->nid;
+ $next->title = $topic->title;
+ break;
+ }
+ if ($topic->nid == $node->nid) {
+ $stop = 1;
+ }
+ else {
+ $prev = new StdClass();
+ $prev->nid = $topic->nid;
+ $prev->title = $topic->title;
+ }
+ }
+
+ if ($prev || $next) {
+ $output .= ' ';
+ }
+
+ return $output;
+}
+
+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 $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'] .' '. $order['sort'];
+}
+
+
diff --git a/modules/help.module b/modules/help.module
new file mode 100644
index 0000000..5a8f789
--- /dev/null
+++ b/modules/help.module
@@ -0,0 +1,135 @@
+ 'admin/help', 'title' => t('help'),
+ 'callback' => 'help_main',
+ 'access' => $admin_access,
+ 'weight' => 9);
+
+ foreach (module_implements('help', TRUE) as $module) {
+ $items[] = array('path' => 'admin/help/' . $module,
+ 'title' => t($module),
+ 'callback' => 'help_page',
+ 'type' => MENU_CALLBACK,
+ 'access' => $admin_access);
+ }
+ }
+
+ return $items;
+}
+
+/**
+ * Menu callback; prints a page listing a glossary of Drupal terminology.
+ */
+function help_main() {
+ $output = t("
+ Help topics
+ Help is available on the following items:
+ %help_pages
+ Glossary of Drupal terminology
+
+ - Block
- A small box containing information or content placed in the left-hand or right-hand sidebar of a web page.
+ - Comment
- A note attached to a node. Usually intended to clarify, explain, criticize, or express an opinion on the original material.
+ - Moderation
+ - The activity of making sure a post to a Drupal site fits in with what is expected for that Drupal site.
+
+ - Approved
- A moderated post which has been accepted by the moderators for publication. (See published).
+ - Waiting
- A moderated post which is still being voted on to be accepted for publication. (See published.)
+
+
+ - Node
- The basic data unit in Drupal. Everything is a node or an extension of a node.
+ - Public
- See published.
+ - Published
- A node that is viewable by everyone. (See unpublished.)
+ - Role
- A classification users are placed into for the purpose of setting users' permissions.
+ - Taxonomy
- A division of a collection of things into ordered, classified groups. (See taxonomy help.)
+ - Unpublished
- A node that is only viewable by administrators and moderators.
+ - User
- A person who has an account at your Drupal site, and is logged in with that account.
+ - Visitor
- A person who does not have an account at your Drupal site or a person who has an account at your Drupal site but is not logged in with that account. Also termed \"anonymous user\".
+
", array('%help_pages' => help_links_as_list(), '%taxonomy' => url('admin/help/taxonomy')));
+
+ return $output;
+}
+
+function help_links_as_list() {
+ $modules = array();
+ foreach (module_implements('help', TRUE) as $module) {
+ if (module_invoke($module, 'help', "admin/help#$module")) {
+ $modules[] = $module;
+ }
+ }
+ sort($modules);
+
+ // Output pretty four-column list
+ $break = ceil(count($modules) / 4);
+ $output = '';
+ foreach ($modules as $i => $module) {
+ $output .= '- '. l(t($module), 'admin/help/'. $module) .'
';
+ if (($i + 1) % $break == 0) {
+ $output .= '
';
+ }
+ }
+ $output .= '
';
+
+ return $output;
+}
+
+/**
+ * Implementation of hook_help().
+ */
+function help_help($section) {
+ switch ($section) {
+ case 'admin/help':
+ $output = t('This guide explains what the various modules in Drupal do and how to configure them.
+It is not a substitute for the Drupal handbook available online and should be used in conjunction with it. The online reference handbook might be more up-to-date and has helpful user-contributed comments. It is your definitive reference point for all Drupal documentation.
+', array('%Drupal' => 'http://drupal.org', '%handbook' => 'http://drupal.org/handbook'));
+ return $output;
+ case 'admin/help#help':
+ $output = ''. t('The help module displays context sensitive help information. Users can learn how to use modules and accomplish tasks quicker with less errors by clicking on links in provided by the help module.') .'
';
+ $output .= t('Modules can make documentation available to other modules with this module. All user help should be presented using this module. Some examples of help:
+
+- The name of a module (unused, but there).
+- The description found on the admin/system/modules page.
+- The module\'s help text, displayed on the admin/help page and through the module\'s individual help link.
+- The help for a distributed authorization module (if applicable).
+- The description of a post type (if applicable).
+
+');
+ $output .= ''. t('You can not administer the help system.') .'
';
+ $output .= ''. t('For more information please read the configuration and customization handbook Help page.', array('%help' => 'http://drupal.org/handbook/modules/help/')) .'
';
+ return $output;
+ case 'admin/modules#description':
+ return t('Manages the display of online help.');
+ }
+}
+
+/**
+ * Menu callback; prints a page listing general help for all modules.
+ */
+function help_page() {
+ $name = arg(2);
+ $output = '';
+ if (module_hook($name, 'help')) {
+ $temp = module_invoke($name, 'help', "admin/help#$name");
+ if (empty($temp)) {
+ $output .= t("No help is available for module %module.", array('%module' => $name));
+ }
+ else {
+ $output .= $temp;
+ }
+ }
+ return $output;
+}
diff --git a/modules/legacy.module b/modules/legacy.module
new file mode 100644
index 0000000..91a51db
--- /dev/null
+++ b/modules/legacy.module
@@ -0,0 +1,202 @@
+'. t('The legacy module provides legacy handlers for upgrades from older installations. These handlers help automatically redirect references to pages from old installations and prevent page not found errors for your site.') .'';
+ $output .= ''. t('The legacy module handles legacy style taxonomy page, taxonomy feed, and blog feed paths. It also handles URL upgrades from Drupal 4.1. It rewrites old-style URLs to new-style URLs (clean URLs). ') .'
';
+ $output .= t('Example Mappings:
+
+- taxonomy/page/or/52,97 to taxonomy/term/52+97.
+- taxonomy/feed/or/52,97 to taxonomy/term/52+97/0/feed.
+- blog/feed/52 to blog/52/feed.
+- node/view/52 to node/52.
+- book/view/52 to node/52.
+- user/view/52 to user/52.
+
+');
+ $output .= ''. t('Legacy module has no configurable options.') .'
';
+ $output .= ''. t('For more information please read the configuration and customization handbook Legacy page.', array('%legacy' => 'http://drupal.org/handbook/modules/legacy/')) .'
';
+ return $output;
+ case 'admin/modules#description':
+ return t('Provides legacy handlers for upgrades from older Drupal installations.');
+ }
+}
+
+/**
+ * Implementation of hook_menu().
+ *
+ * Registers menu paths used in earlier Drupal versions.
+ */
+function legacy_menu($may_cache) {
+ $items = array();
+
+ if ($may_cache) {
+ // Map "taxonomy/page/or/52,97" to "taxonomy/term/52+97".
+ $items[] = array('path' => 'taxonomy/page', 'title' => t('taxonomy'),
+ 'callback' => 'legacy_taxonomy_page',
+ 'access' => TRUE, 'type' => MENU_CALLBACK);
+
+ // Map "taxonomy/feed/or/52,97" to "taxonomy/term/52+97/0/feed".
+ $items[] = array('path' => 'taxonomy/feed', 'title' => t('taxonomy'),
+ 'callback' => 'legacy_taxonomy_feed',
+ 'access' => TRUE, 'type' => MENU_CALLBACK);
+
+ // Map "blog/feed/52" to "blog/52/feed".
+ $items[] = array('path' => 'blog/feed', 'title' => t('blog'),
+ 'callback' => 'legacy_blog_feed',
+ 'access' => TRUE, 'type' => MENU_CALLBACK);
+ }
+ else {
+ // Map "node/view/52" to "node/52".
+ $items[] = array('path' => 'node/view', 'title' => t('view'),
+ 'callback' => 'drupal_goto',
+ 'callback arguments' => array('node/'. arg(2), NULL, NULL),
+ 'access' => TRUE, 'type' => MENU_CALLBACK);
+
+ // Map "book/view/52" to "node/52".
+ $items[] = array('path' => 'book/view', 'title' => t('view'),
+ 'callback' => 'drupal_goto',
+ 'callback arguments' => array('node/'. arg(2), NULL, NULL),
+ 'access' => TRUE, 'type' => MENU_CALLBACK);
+
+ // Map "user/view/52" to "user/52".
+ $items[] = array('path' => 'user/view', 'title' => t('view'),
+ 'callback' => 'drupal_goto',
+ 'callback arguments' => array('user/'. arg(2), NULL, NULL),
+ 'access' => TRUE, 'type' => MENU_CALLBACK);
+ }
+
+ return $items;
+}
+
+/**
+ * Menu callback; redirects users to new taxonomy page paths.
+ */
+function legacy_taxonomy_page($operation = 'or', $str_tids = '') {
+ if ($operation == 'or') {
+ $str_tids = str_replace(',', '+', $str_tids);
+ }
+ drupal_goto('taxonomy/term/'. $str_tids);
+}
+
+/**
+ * Menu callback; redirects users to new taxonomy feed paths.
+ */
+function legacy_taxonomy_feed($operation = 'or', $str_tids = '') {
+ if ($operation == 'or') {
+ $str_tids = str_replace(',', '+', $str_tids);
+ }
+ drupal_goto('taxonomy/term/'. $str_tids .'/0/feed');
+}
+
+/**
+ * Menu callback; redirects users to new blog feed paths.
+ */
+function legacy_blog_feed($str_uid = '') {
+ // if URL is of form blog/feed/52 redirect
+ // if URL is of form blog/feed we have to call blog_feed_last().
+ if (is_numeric($str_uid)) {
+ drupal_goto('blog/'. $str_uid .'/feed');
+ }
+ else {
+ module_invoke('blog', 'feed_last');
+ }
+}
+
+/**
+ * Implementation of hook_filter(). Handles URL upgrades from Drupal 4.1.
+ */
+function legacy_filter($op, $delta = 0, $format = -1, $text = '') {
+ switch ($op) {
+ case 'list':
+ return array(t('Legacy filter'));
+
+ case 'description':
+ return t('Replaces URLs from Drupal 4.1 (and lower) with updated equivalents.');
+
+ case 'process':
+ return _legacy_filter_old_urls($text);
+
+ case 'settings':
+ return;
+
+ default:
+ return $text;
+ }
+}
+
+/**
+ * Rewrite legacy URLs.
+ *
+ * This is a *temporary* filter to rewrite old-style URLs to new-style
+ * URLs (clean URLs). Currently, URLs are being rewritten dynamically
+ * (ie. "on output"), however when these rewrite rules have been tested
+ * enough, we will use them to permanently rewrite the links in node
+ * and comment bodies.
+ */
+function _legacy_filter_old_urls($text) {
+ if (!variable_get('rewrite_old_urls', 0)) {
+ return $text;
+ }
+
+ global $base_url;
+
+ $end = substr($base_url, 12);
+
+ if (variable_get('clean_url', '0') == '0') {
+ // Relative URLs:
+
+ // rewrite 'node.php?id=[&cid=]' style URLs:
+ $text = eregi_replace("\"(node)\.php\?id=([[:digit:]]+)(&cid=)?([[:digit:]]*)", "\"?q=\\1/view/\\2/\\4", $text);
+
+ // rewrite 'module.php?mod={&=}' style URLs:
+ $text = ereg_replace("\"module\.php\?(&?[[:alpha:]]+=([[:alnum:]]+))(&?[[:alpha:]]+=([[:alnum:]]+))(&?[[:alpha:]]+=([[:alnum:]]+))", "\"?q=\\2/\\4/\\6" , $text);
+ $text = ereg_replace("\"module\.php\?(&?[[:alpha:]]+=([[:alnum:]]+))(&?[[:alpha:]]+=([[:alnum:]]+))", "\"?q=\\2/\\4", $text);
+ $text = ereg_replace("\"module\.php\?(&?[[:alpha:]]+=([[:alnum:]]+))", "\"?q=\\2", $text);
+
+ // Absolute URLs:
+
+ // rewrite 'node.php?id=[&cid=]' style URLs:
+ $text = eregi_replace("$end/(node)\.php\?id=([[:digit:]]+)(&cid=)?([[:digit:]]*)", "$end/?q=\\1/view/\\2/\\4", $text);
+
+ // rewrite 'module.php?mod={&=}' style URLs:
+ $text = ereg_replace("$end/module\.php\?(&?[[:alpha:]]+=([[:alnum:]]+))(&?[[:alpha:]]+=([[:alnum:]]+))(&?[[:alpha:]]+=([[:alnum:]]+))", "$end/?q=\\2/\\4/\\6" , $text);
+ $text = ereg_replace("$end/module\.php\?(&?[[:alpha:]]+=([[:alnum:]]+))(&?[[:alpha:]]+=([[:alnum:]]+))", "$end/?q=\\2/\\4", $text);
+ $text = ereg_replace("$end/module\.php\?(&?[[:alpha:]]+=([[:alnum:]]+))", "\"$end/?q=\\2", $text);
+ }
+ else {
+ // Relative URLs:
+
+ // Rewrite 'node.php?id=[&cid=]' style URLs:
+ $text = eregi_replace("\"(node)\.php\?id=([[:digit:]]+)(&cid=)?([[:digit:]]*)", "\"\\1/view/\\2/\\4", $text);
+
+ // Rewrite 'module.php?mod={&=}' style URLs:
+ $text = ereg_replace("\"module\.php\?(&?[[:alpha:]]+=([[:alnum:]]+))(&?[[:alpha:]]+=([[:alnum:]]+))(&?[[:alpha:]]+=([[:alnum:]]+))", "\"\\2/\\4/\\6", $text);
+ $text = ereg_replace("\"module\.php\?(&?[[:alpha:]]+=([[:alnum:]]+))(&?[[:alpha:]]+=([[:alnum:]]+))", "\"\\2/\\4", $text);
+ $text = ereg_replace("\"module\.php\?(&?[[:alpha:]]+=([[:alnum:]]+))", "\"\\2", $text);
+
+ // Absolute URLs:
+
+ // Rewrite 'node.php?id=[&cid=]' style URLs:
+ $text = eregi_replace("$end/(node)\.php\?id=([[:digit:]]+)(&cid=)?([[:digit:]]*)", "$end/\\1/view/\\2/\\4", $text);
+
+ // Rewrite 'module.php?mod={&=}' style URLs:
+ $text = ereg_replace("$end/module\.php\?(&?[[:alpha:]]+=([[:alnum:]]+))(&?[[:alpha:]]+=([[:alnum:]]+))(&?[[:alpha:]]+=([[:alnum:]]+))", "$end/\\2/\\4/\\6", $text);
+ $text = ereg_replace("$end/module\.php\?(&?[[:alpha:]]+=([[:alnum:]]+))(&?[[:alpha:]]+=([[:alnum:]]+))", "$end/\\2/\\4", $text);
+ $text = ereg_replace("$end/module\.php\?(&?[[:alpha:]]+=([[:alnum:]]+))", "$end/\\2", $text);
+ }
+
+ return $text;
+}
+
+
diff --git a/modules/locale.module b/modules/locale.module
new file mode 100644
index 0000000..87f7aaf
--- /dev/null
+++ b/modules/locale.module
@@ -0,0 +1,417 @@
+'. t('The locale module allows you to present your Drupal site in a language other than the default English. You can use it to set up a multi-lingual web site or replace given built-in text with text which has been customized for your site. Whenever the locale module encounters text which needs to be displayed, it tries to translate it into the currently selected language. If a translation is not available, then the string is remembered, so you can look up untranslated strings easily.') .'';
+ $output .= ''. t('The locale module provides two options for providing translations. The first is the integrated web interface, via which you can search for untranslated strings, and specify their translations. An easier and less time-consuming method is to import existing translations for your language. These translations are available as GNU gettext Portable Object files (.po files for short). Translations for many languages are available for download from the translation page.') .'
';
+ $output .= ''. t('If an existing translation does not meet your needs, the .po files are easily edited with special editing tools. The locale module\'s import feature allows you to add strings from such files into your site\'s database. The export functionality enables you to share your translations with others, generating Portable Object files from your site strings.') .'
';
+ $output .= t('You can
+
+- administer localization at administer >> localization.
+- manage strings for the localization: administer >> localization >> manage strings.
+- add a locale language: administer >> localization >> add language.
+- download translation files from the Drupal translations page.
+
+
+', array('%admin-locale' => url('admin/locale'), '%admin-locale-string-search' => url('admin/locale/string/search'), '%admin-locale-language-add' => url('admin/locale/language/add'), '%external-http-drupal-org-project-Translations' => 'http://drupal.org/project/Translations'));
+ $output .= ''. t('For more information please read the configuration and customization handbook Locale page.', array('%locale' => 'http://drupal.org/handbook/modules/locale/')) .'
';
+ return $output;
+ case 'admin/modules#description':
+ return t('Enables the translation of the user interface to languages other than English.');
+ case 'admin/locale':
+ case 'admin/locale/language/overview':
+ return t("Drupal provides support for the translation of its interface text into different languages. This page provides an overview of the installed languages. You can add a language on the add language page, or directly by importing a translation. If multiple languages are enabled, registered users will be able to set their preferred language. The site default will be used for anonymous visitors and for users without their own settings.
Drupal interface translations may be added or extended by several courses: by importing an existing translation, by translating everything from scratch, or by a combination of these approaches.
", array("%search" => url("admin/locale/string/search"), "%import" => url("admin/locale/language/import"), "%add-language" => url("admin/locale/language/add")));
+ case 'admin/locale/language/add':
+ return t("You need to add all languages in which you would like to display the site interface. If you can't find the desired language in the quick-add dropdown, then you will need to provide the proper language code yourself. The language code may be used to negotiate with browsers and to present flags, etc., so it is important to pick a code that is standardised for the desired language. You can also add a language by importing a translation.
", array("%import" => url("admin/locale/language/import")));
+ case 'admin/locale/language/import':
+ return t("This page allows you to import a translation provided in the gettext Portable Object (.po) format. The easiest way to get your site translated is to obtain an existing Drupal translation and to import it. You can find existing translations on the Drupal translation page. Note that importing a translation file might take a while.
", array('%url' => 'http://drupal.org/project/translations'));
+ case 'admin/locale/language/export':
+ return t("This page allows you to export Drupal strings. The first option is to export a translation so it can be shared. The second option generates a translation template, which contains all Drupal strings, but without their translations. You can use this template to start a new translation using various software packages designed for this task.
");
+ case 'admin/locale/string/search':
+ return t("It is often convenient to get the strings from your setup on the export page, and use a desktop Gettext translation editor to edit the translations. On this page you can search in the translated and untranslated strings, and the default English texts provided by Drupal.
", array("%export" => url("admin/locale/language/export")));
+ }
+}
+
+/**
+ * Implementation of hook_menu().
+ */
+function locale_menu($may_cache) {
+ $items = array();
+
+ if ($may_cache) {
+ $access = user_access('administer locales');
+
+ // Main admin menu item
+ $items[] = array('path' => 'admin/locale',
+ 'title' => t('localization'),
+ 'callback' => 'locale_admin_manage',
+ 'access' => $access);
+
+ // Top level tabs
+ $items[] = array('path' => 'admin/locale/language',
+ 'title' => t('manage languages'),
+ 'access' => $access,
+ 'weight' => -10,
+ 'type' => MENU_DEFAULT_LOCAL_TASK);
+ $items[] = array('path' => 'admin/locale/string/search',
+ 'title' => t('manage strings'),
+ 'callback' => 'locale_string_search',
+ 'access' => $access,
+ 'weight' => 10,
+ 'type' => MENU_LOCAL_TASK);
+
+ // Manage languages subtabs
+ $items[] = array('path' => 'admin/locale/language/overview',
+ 'title' => t('list'),
+ 'callback' => 'locale_admin_manage',
+ 'access' => $access,
+ 'weight' => 0,
+ 'type' => MENU_DEFAULT_LOCAL_TASK);
+ $items[] = array('path' => 'admin/locale/language/add',
+ 'title' => t('add language'),
+ 'callback' => 'locale_admin_manage_add',
+ 'access' => $access,
+ 'weight' => 5,
+ 'type' => MENU_LOCAL_TASK);
+ $items[] = array('path' => 'admin/locale/language/import',
+ 'title' => t('import'),
+ 'callback' => 'locale_admin_import',
+ 'access' => $access,
+ 'weight' => 10,
+ 'type' => MENU_LOCAL_TASK);
+ $items[] = array('path' => 'admin/locale/language/export',
+ 'title' => t('export'),
+ 'callback' => 'locale_admin_export',
+ 'access' => $access,
+ 'weight' => 20,
+ 'type' => MENU_LOCAL_TASK);
+
+ // Language related callbacks
+ $items[] = array('path' => 'admin/locale/language/delete',
+ 'title' => t('confirm'),
+ 'callback' => 'locale_admin_manage_delete_form',
+ 'access' => $access,
+ 'type' => MENU_CALLBACK);
+ }
+ else {
+ if (is_numeric(arg(4))) {
+ // String related callbacks
+ $items[] = array('path' => 'admin/locale/string/edit/'. arg(4),
+ 'title' => t('edit string'),
+ 'callback' => 'locale_admin_string_edit',
+ 'callback arguments' => arg(4),
+ 'access' => $access,
+ 'type' => MENU_CALLBACK);
+ $items[] = array('path' => 'admin/locale/string/delete/'. arg(4),
+ 'title' => t('delete string'),
+ 'callback' => 'locale_admin_string_delete',
+ 'callback arguments' => arg(4),
+ 'access' => $access,
+ 'type' => MENU_CALLBACK);
+ }
+ }
+
+ return $items;
+}
+
+/**
+ * Implementation of hook_perm().
+ */
+function locale_perm() {
+ return array('administer locales');
+}
+
+/**
+ * Implementation of hook_user().
+ */
+function locale_user($type, $edit, &$user, $category = NULL) {
+ $languages = locale_supported_languages();
+ if ($type == 'form' && $category == 'account' && count($languages['name']) > 1) {
+ if ($user->language == '') {
+ $user->language = key($languages['name']);
+ }
+ $languages['name'] = array_map('check_plain', $languages['name']);
+ $form['locale'] = array('#type' => 'fieldset',
+ '#title' => t('Interface language settings'),
+ '#weight' => 1,
+ );
+ $form['locale']['language'] = array('#type' => 'radios',
+ '#title' => t('Language'),
+ '#default_value' => $user->language,
+ '#options' => $languages['name'],
+ '#description' => t('Selecting a different locale will change the interface language of the site.'),
+ );
+ return $form;
+ }
+}
+
+// ---------------------------------------------------------------------------------
+// Locale core functionality (needed on all page loads)
+
+/**
+ * Provides interface translation services.
+ *
+ * This function is called from t() to translate a string if needed.
+ */
+function locale($string) {
+ global $locale;
+ static $locale_t;
+
+ // Store database cached translations in a static var.
+ if (!isset($locale_t)) {
+ $cache = cache_get("locale:$locale");
+
+ if ($cache == 0) {
+ locale_refresh_cache();
+ $cache = cache_get("locale:$locale");
+ }
+ $locale_t = unserialize($cache->data);
+ }
+
+ // We have the translation cached (if it is TRUE, then there is no
+ // translation, so there is no point in checking the database)
+ if (isset($locale_t[$string])) {
+ $string = ($locale_t[$string] === TRUE ? $string : $locale_t[$string]);
+ }
+
+ // We do not have this translation cached, so get it from the DB.
+ else {
+ $result = db_query("SELECT s.lid, t.translation FROM {locales_source} s INNER JOIN {locales_target} t ON s.lid = t.lid WHERE s.source = '%s' AND t.locale = '%s'", $string, $locale);
+ // Translation found
+ if ($trans = db_fetch_object($result)) {
+ if (!empty($trans->translation)) {
+ $locale_t[$string] = $trans->translation;
+ $string = $trans->translation;
+ }
+ }
+
+ // Either we have no such source string, or no translation
+ else {
+ $result = db_query("SELECT lid, source FROM {locales_source} WHERE source = '%s'", $string);
+ // We have no such translation
+ if ($obj = db_fetch_object($result)) {
+ if ($locale) {
+ db_query("INSERT INTO {locales_target} (lid, locale, translation) VALUES (%d, '%s', '')", $obj->lid, $locale);
+ }
+ }
+ // We have no such source string
+ else {
+ db_query("INSERT INTO {locales_source} (location, source) VALUES ('%s', '%s')", request_uri(), $string);
+ if ($locale) {
+ $lid = db_fetch_object(db_query("SELECT lid FROM {locales_source} WHERE source = '%s'", $string));
+ db_query("INSERT INTO {locales_target} (lid, locale, translation) VALUES (%d, '%s', '')", $lid->lid, $locale);
+ }
+ }
+ // Clear locale cache in DB
+ cache_clear_all("locale:$locale");
+ }
+ }
+
+ return $string;
+}
+
+/**
+ * Refreshes database stored cache of translations.
+ *
+ * We only store short strings to improve performance and consume less memory.
+ */
+function locale_refresh_cache() {
+ $languages = locale_supported_languages();
+
+ foreach (array_keys($languages['name']) as $locale) {
+ $result = db_query("SELECT s.source, t.translation, t.locale FROM {locales_source} s INNER JOIN {locales_target} t ON s.lid = t.lid WHERE t.locale = '%s' AND LENGTH(s.source) < 75", $locale);
+ $t = array();
+ while ($data = db_fetch_object($result)) {
+ $t[$data->source] = (empty($data->translation) ? TRUE : $data->translation);
+ }
+ cache_set("locale:$locale", serialize($t));
+ }
+}
+
+/**
+ * Returns list of languages supported on this site.
+ *
+ * @param $reset Refresh cached language list.
+ * @param $getall Return all languages (even disabled ones)
+ */
+function locale_supported_languages($reset = FALSE, $getall = FALSE) {
+ static $enabled = NULL;
+ static $all = NULL;
+
+ if ($reset) {
+ unset($enabled); unset($all);
+ }
+
+ if (is_null($enabled)) {
+ $enabled = $all = array();
+ $all['name'] = $all['formula'] = $enabled['name'] = $enabled['formula'] = array();
+ $result = db_query('SELECT locale, name, formula, enabled FROM {locales_meta} ORDER BY isdefault DESC, enabled DESC, name ASC');
+ while ($row = db_fetch_object($result)) {
+ $all['name'][$row->locale] = $row->name;
+ $all['formula'][$row->locale] = $row->formula;
+ if ($row->enabled) {
+ $enabled['name'][$row->locale] = $row->name;
+ $enabled['formula'][$row->locale] = $row->formula;
+ }
+ }
+ }
+ return $getall ? $all : $enabled;
+}
+
+/**
+ * Returns plural form index for a specific number.
+ *
+ * The index is computed from the formula of this language.
+ */
+function locale_get_plural($count) {
+ global $locale;
+ static $locale_formula, $plurals = array();
+
+ if (!isset($plurals[$count])) {
+ if (!isset($locale_formula)) {
+ $languages = locale_supported_languages();
+ $locale_formula = $languages['formula'][$locale];
+ }
+ if ($locale_formula) {
+ $n = $count;
+ $plurals[$count] = @eval("return intval($locale_formula);");
+ return $plurals[$count];
+ }
+ else {
+ $plurals[$count] = -1;
+ return -1;
+ }
+ }
+ return $plurals[$count];
+}
+
+// ---------------------------------------------------------------------------------
+// Language management functionality (administration only)
+
+/**
+ * Page handler for the language management screen.
+ */
+function locale_admin_manage() {
+ include_once './includes/locale.inc';
+ return _locale_admin_manage_screen();
+}
+
+/**
+ * User interface for the language deletion confirmation screen.
+ */
+function locale_admin_manage_delete_form() {
+ include_once './includes/locale.inc';
+ $langcode = arg(4);
+
+ // Do not allow deletion of English locale.
+ if ($langcode == 'en') {
+ drupal_set_message(t('The English locale cannot be deleted.'));
+ drupal_goto('admin/locale/language/overview');
+ }
+
+ // For other locales, warn user that data loss is ahead.
+ $languages = locale_supported_languages(FALSE, TRUE);
+
+ if (!isset($languages['name'][$langcode])) {
+ drupal_not_found();
+ }
+ else {
+ $form['langcode'] = array('#type' => 'value', '#value' => $langcode);
+ return confirm_form('locale_admin_manage_delete_form', $form, t('Are you sure you want to delete the language %name?', array('%name' => theme('placeholder', t($languages['name'][$langcode])))), 'admin/locale/language/overview', t('Deleting a language will remove all data associated with it. This action cannot be undone.'), t('Delete'), t('Cancel'));
+ }
+}
+
+/**
+ * Process language deletion submissions.
+ */
+function locale_admin_manage_delete_form_submit($form_id, $form_values) {
+ $languages = locale_supported_languages(FALSE, TRUE);
+ if (isset($languages['name'][$form_values['langcode']])) {
+ db_query("DELETE FROM {locales_meta} WHERE locale = '%s'", $form_values['langcode']);
+ db_query("DELETE FROM {locales_target} WHERE locale = '%s'", $form_values['langcode']);
+ $message = t('The language %locale has been removed.', array('%locale' => theme('placeholder', t($languages['name'][$form_values['langcode']]))));
+ drupal_set_message($message);
+ watchdog('locale', $message);
+ }
+
+ // Changing the locale settings impacts the interface:
+ cache_clear_all();
+
+ return 'admin/locale/language/overview';
+}
+
+/**
+ * Page handler for the language addition screen
+ */
+function locale_admin_manage_add() {
+ include_once './includes/locale.inc';
+ return _locale_admin_manage_add_screen();
+}
+
+// ---------------------------------------------------------------------------------
+// Gettext Portable Object import functionality (administration only)
+
+/**
+ * Page handler for the translation import screen
+ */
+function locale_admin_import() {
+ include_once './includes/locale.inc';
+ return _locale_admin_import_screen();
+}
+
+// ---------------------------------------------------------------------------------
+// Gettext Portable Object export functionality (administration only)
+
+/**
+ * Page handler for the translation export screen
+ */
+function locale_admin_export() {
+ include_once './includes/locale.inc';
+ return _locale_admin_export_screen();
+}
+
+// ---------------------------------------------------------------------------------
+// String search and editing functionality (administration only)
+
+/**
+ * Page handler for the string search.
+ */
+function locale_string_search() {
+ include_once './includes/locale.inc';
+ $output = _locale_string_seek();
+ $output .= _locale_string_seek_form();
+ return $output;
+}
+
+/**
+ * Display the string edit form.
+ */
+function locale_admin_string_edit($lid) {
+ include_once './includes/locale.inc';
+ return _locale_string_edit($lid);
+}
+
+/**
+ * Delete a string.
+ */
+function locale_admin_string_delete($lid) {
+ include_once './includes/locale.inc';
+ _locale_string_delete($lid);
+}
diff --git a/modules/menu.module b/modules/menu.module
new file mode 100644
index 0000000..e950144
--- /dev/null
+++ b/modules/menu.module
@@ -0,0 +1,764 @@
+Menus are a collection of links (menu items) used to navigate a website. The menu module provides an interface to control and customize the powerful menu system that comes with Drupal. Menus are primarily displayed as a hierarchical list of links using Drupal\'s highly flexible blocks feature. Each menu automatically creates a block of the same name. By default, new menu items are placed inside a built-in menu labelled %navigation, but administrators can also create custom menus.
+Drupal themes generally provide out-of-the-box support for two menus commonly labelled %primary-links and %secondary-links. These are sets of links which are usually displayed in the header or footer of each page (depending on the currently active theme). Any menu can be designated as the primary or secondary links menu via the menu settings page.
+Menu administration tabs:
+
+ - On the administer menu page, administrators can "edit" to change the title, description, parent or weight of a menu item. Under the "operations" column, click on "enable/disable" to toggle a menu item on or off. Only menu items which are enabled are displayed in the corresponding menu block. Note that the default menu items generated by the menu module cannot be deleted, only disabled.
+ - Use the "add menu" tab to submit a title for a new custom menu. Once submitted, the menu will appear in a list toward the bottom of the administer menu page underneath the main navigation menu. Under the menu name there will be links to edit or delete the menu, and a link to add new items to the menu.
+ - Use the "add menu item" tab to create new links in either the navigation or a custom menu (such as a primary/secondary links menu). Select the parent item to place the new link within an existing menu structure. For top level menu items, choose the name of the menu in which the link is to be added.
+
', array('%navigation' => theme('placeholder', 'Navigation'), '%primary-links' => theme('placeholder', 'primary links'), '%secondary-links' => theme('placeholder', 'secondary links'), '%admin-block' => url('admin/block'), '%menu-settings' => url('admin/settings/menu')));
+ $output .= t('You can
+
+ - administer menus at administer >> menus.
+ - add a menu at administer >> menus >> add menu.
+ - add a menu item at administer >> menus >> add menu item.
+ - modify menu settings (in particular, to specify a menu to use for primary or secondary links) at administer >> settings >> menus.
+ - manage menu blocks at administer >> blocks.
+
+', array('%admin-menu' => url('admin/menu'), '%admin-block' => url('admin/block'), '%admin-menu-menu-add' => url('admin/menu/menu/add'), '%admin-menu-item-add' => url('admin/menu/item/add'), '%admin-settings-menus' => url('admin/settings/menu')));
+ $output .= ''. t('For more information please read the configuration and customization handbook Menu page.', array('%menu' => 'http://drupal.org/handbook/modules/menu/')) .'
';
+ return $output;
+ case 'admin/modules#description':
+ return t('Allows administrators to customize the site navigation menu.');
+ case 'admin/menu':
+ return ''. t('Menus are a collection of links (menu items) used to navigate a website. The list(s) below display the currently available menus along with their menu items. Select an operation from the list to manage each menu or menu item.', array('%admin-settings-menus' => url('admin/settings/menu'), '%admin-block'=>url('admin/block'))) .'
';
+ case 'admin/menu/menu/add':
+ return ''. t('Enter the name for your new menu. Remember to enable the newly created block in the blocks administration page.', array('%blocks' => url('admin/block'))) .'
';
+ case 'admin/menu/item/add':
+ return ''. t('Enter the title, path, position and the weight for your new menu item.') .'
';
+ }
+}
+
+/**
+ * Implementation of hook_menu().
+ */
+function menu_menu($may_cache) {
+ $items = array();
+
+ if ($may_cache) {
+ $items[] = array('path' => 'admin/menu',
+ 'title' => t('menus'),
+ 'callback' => 'menu_overview',
+ 'access' => user_access('administer menu'));
+ $items[] = array('path' => 'admin/menu/list',
+ 'title' => t('list'),
+ 'type' => MENU_DEFAULT_LOCAL_TASK,
+ 'weight' => -10);
+
+ $items[] = array('path' => 'admin/menu/item/add',
+ 'title' => t('add menu item'),
+ 'callback' => 'menu_edit_item_form',
+ 'access' => user_access('administer menu'),
+ 'type' => MENU_LOCAL_TASK);
+ $items[] = array('path' => 'admin/menu/item/edit',
+ 'title' => t('edit menu item'),
+ 'callback' => 'menu_edit_item_form',
+ 'access' => user_access('administer menu'),
+ 'type' => MENU_CALLBACK);
+ $items[] = array('path' => 'admin/menu/item/reset',
+ 'title' => t('reset menu item'),
+ 'callback' => 'menu_reset_item',
+ 'access' => user_access('administer menu'),
+ 'type' => MENU_CALLBACK);
+ $items[] = array('path' => 'admin/menu/item/disable',
+ 'title' => t('disable menu item'),
+ 'callback' => 'menu_disable_item',
+ 'access' => user_access('administer menu'),
+ 'type' => MENU_CALLBACK);
+ $items[] = array('path' => 'admin/menu/item/delete',
+ 'title' => t('delete menu item'),
+ 'callback' => 'menu_item_delete_form',
+ 'access' => user_access('administer menu'),
+ 'type' => MENU_CALLBACK);
+
+ $items[] = array('path' => 'admin/menu/menu/add',
+ 'title' => t('add menu'),
+ 'callback' => 'menu_edit_menu_form',
+ 'access' => user_access('administer menu'),
+ 'type' => MENU_LOCAL_TASK);
+ $items[] = array('path' => 'admin/menu/menu/edit',
+ 'title' => t('edit menu'),
+ 'callback' => 'menu_edit_menu_form',
+ 'access' => user_access('administer menu'),
+ 'type' => MENU_CALLBACK);
+ $items[] = array('path' => 'admin/menu/menu/delete',
+ 'title' => t('delete menu'),
+ 'callback' => 'menu_item_delete_form',
+ 'access' => user_access('administer menu'),
+ 'type' => MENU_CALLBACK);
+
+ $items[] = array('path' => 'admin/settings/menu',
+ 'title' => t('menus'),
+ 'callback' => 'menu_configure');
+ }
+
+ return $items;
+}
+
+/**
+ * Implementation of hook_block().
+ */
+function menu_block($op = 'list', $delta = 0) {
+ if ($op == 'list') {
+ $blocks = array();
+ $root_menus = menu_get_root_menus();
+ foreach ($root_menus as $mid => $title) {
+ // Default "Navigation" block is handled by user.module.
+ if ($mid != 1) {
+ $blocks[$mid]['info'] = check_plain($title);
+ }
+ }
+ return $blocks;
+ }
+ else if ($op == 'view') {
+ $item = menu_get_item($delta);
+ $data['subject'] = check_plain($item['title']);
+ $data['content'] = theme('menu_tree', $delta);
+ return $data;
+ }
+}
+
+/**
+ * Implementation of hook_nodeapi().
+ */
+function menu_nodeapi(&$node, $op) {
+
+ if (user_access('administer menu')) {
+ switch ($op) {
+ case 'insert':
+ case 'update':
+ if ($node->menu['delete']) {
+ menu_node_form_delete($node);
+ menu_rebuild();
+ }
+ elseif ($node->menu['title']) {
+ $node->menu['path'] = ($node->menu['path']) ? $node->menu['path'] : "node/$node->nid";
+ menu_edit_item_save($node->menu);
+ menu_rebuild();
+ }
+ break;
+
+ case 'delete':
+ menu_node_form_delete($node);
+ menu_rebuild();
+ break;
+ }
+ }
+}
+
+/**
+ * Implementation of hook_perm().
+ */
+function menu_perm() {
+ return array('administer menu');
+}
+
+/**
+ * Implementation of hook_form_alter().
+ * Add menu item fields to the node form.
+ */
+function menu_form_alter($form_id, &$form) {
+ if (user_access('administer menu') && isset($form['type']) && $form['type']['#value'] .'_node_form' == $form_id) {
+ $edit = isset($_POST['edit']) ? $_POST['edit'] : '';
+ $edit['nid'] = $form['nid']['#value'];
+
+ $item = array();
+ if ($edit['nid'] > 0) {
+ $item = db_fetch_array(db_query("SELECT * FROM {menu} WHERE path = 'node/%d'", $edit['nid']));
+ if (is_array($edit['menu'])) {
+ $item = !is_array($item) ? $edit['menu'] : (($_POST['op'] == t('Preview')) ? array_merge($item, $edit['menu']) : array_merge($edit['menu'], $item));
+ }
+ }
+
+ $form['menu'] = array('#type' => 'fieldset',
+ '#title' => t('Menu settings'),
+ '#collapsible' => TRUE,
+ '#collapsed' => empty($item['title']),
+ '#tree' => TRUE,
+ '#weight' => 30,
+ );
+
+ $form['menu']['title'] = array('#type' => 'textfield',
+ '#title' => t('Title'),
+ '#default_value' => $item['title'],
+ '#description' => t('The name to display for this link.'),
+ );
+
+ $form['menu']['description'] = array('#type' => 'textfield',
+ '#title' => t('Description'),
+ '#default_value' => $item['description'],
+ '#description' => t('The description displayed when hovering over a menu item.'),
+ );
+
+ // Generate a list of possible parents.
+ $options = menu_parent_options($item['mid'], variable_get('menu_parent_items', 0));
+
+ $form['menu']['pid'] = array('#type' => 'select',
+ '#title' => t('Parent item'),
+ '#default_value' => $item['pid'],
+ '#options' => $options,
+ );
+
+ $form['menu']['path'] = array('#type' => 'hidden',
+ '#value' => $item['path'],
+ );
+
+ $form['menu']['weight'] = array('#type' => 'weight',
+ '#title' => t('Weight'),
+ '#default_value' => $item['weight'],
+ '#delta' => 10,
+ '#description' => t('Optional. In the menu, the heavier items will sink and the lighter items will be positioned nearer the top.'),
+ );
+
+ $form['menu']['mid'] = array('#type' => 'hidden',
+ '#value' => $item['mid'] ? $item['mid'] : 0,
+ );
+
+ $form['menu']['type'] = array('#type' => 'hidden',
+ '#value' => $item['type'] ? $item['type'] : MENU_CUSTOM_ITEM,
+ );
+
+ if ($item['mid'] > 0) {
+ $form['menu']['delete'] = array('#type' => 'checkbox',
+ '#title' => t('Check to delete this menu item.'),
+ '#default_value' => $item['delete'],
+ );
+
+ $form['menu']['advanced'] = array('#type' => 'item',
+ '#value' => t('You may also edit the advanced settings for this menu item.', array('%edit' => url("admin/menu/item/edit/{$item['mid']}"))),
+ );
+ }
+ }
+}
+
+/**
+ * Menu callback; presents menu configuration options.
+ */
+function menu_configure() {
+ $menu = menu_get_menu();
+ $root_menus = menu_get_root_menus();
+
+ $primary_options = $root_menus;
+ $primary_options[0] = t('No primary links');
+
+ $form['settings_links'] = array('#type' => 'fieldset',
+ '#title' => t('Primary and secondary links settings'),
+ );
+
+ $form['settings_links']['intro'] = array('#type' => 'item',
+ '#value' => t('Primary and secondary links provide a navigational menu system which usually (depending on your theme) appears at the top-right of the browser window. The links displayed can be generated either from a custom list created via the menu administration page or from a built-in list of menu items such as the navigation menu links.', array('%menu' => url('admin/menu'))),
+ );
+
+ $form['settings_links']['menu_primary_menu'] = array('#type' => 'select',
+ '#title' => t('Menu containing primary links'),
+ '#default_value' => variable_get('menu_primary_menu', 0),
+ '#options' => $primary_options,
+ );
+
+ $secondary_options = $root_menus;
+ $secondary_options[0] = t('No secondary links');
+
+ $form['settings_links']['menu_secondary_menu'] = array('#type' => 'select',
+ '#title' => t('Menu containing secondary links'),
+ '#default_value' => variable_get('menu_secondary_menu', 0),
+ '#options' => $secondary_options,
+ '#description' => t('If you select the same menu as primary links then secondary links will display the appropriate second level of your navigation hierarchy.'),
+ );
+
+ $form['settings_authoring'] = array('#type' => 'fieldset',
+ '#title' => t('Post authoring form settings'),
+ );
+
+ $form['settings_authoring']['intro'] = array('#type' => 'item',
+ '#value' => t('The menu module allows on-the-fly creation of menu links in the post authoring forms. The following option limits the menus in which a new link may be added. For e.g. this can be used to force new menu items to be created in the primary links menu or to hide admin menu items.'),
+ );
+
+ $authoring_options = $root_menus;
+ $authoring_options[0] = t('Show all menus');
+
+ $form['settings_authoring']['menu_parent_items'] = array('#type' => 'select',
+ '#title' => t('Restrict parent items to'),
+ '#default_value' => variable_get('menu_parent_items', 0),
+ '#options' => $authoring_options,
+ '#description' => t('Choose the menu to be made available in the post authoring form. Only this menu item and its children will be shown.'),
+ );
+
+ return system_settings_form('menu_configure', $form);
+}
+
+/**
+ * Menu callback; handle the adding/editing of a new menu.
+ */
+function menu_edit_menu_form($mid = 0) {
+ if (arg(3) == 'edit') {
+ if (!($item = db_fetch_array(db_query('SELECT * FROM {menu} WHERE mid = %d', $mid)))) {
+ drupal_not_found();
+ return;
+ }
+ }
+ else {
+ $item = array('mid' => 0, 'pid' => 0, 'path' => '', 'weight' => 0, 'type' => MENU_CUSTOM_MENU);
+ }
+ $form['title'] = array('#type' => 'textfield',
+ '#title' => t('Title'),
+ '#default_value' => $item['title'],
+ '#description' => t('The name of the menu.'),
+ '#required' => TRUE,
+ );
+ $form['mid'] = array('#type' => 'value', '#value' => $item['mid']);
+ $form['pid'] = array('#type' => 'value', '#value' => $item['pid']);
+ $form['path'] = array('#type' => 'value', '#value' => $item['path']);
+ $form['weight'] = array('#type' => 'value', '#value' => $item['weight']);
+ $form['type'] = array('#type' => 'value', '#value' => $item['type']);
+ $form['submit'] = array('#type' => 'submit', '#value' => t('Submit'));
+
+ // Reuse the submit function of menu_edit_item_form.
+ return drupal_get_form('menu_edit_menu_form', $form, 'menu_edit_item_form');
+}
+
+/**
+ * Present the menu item editing form.
+ */
+function menu_edit_item_form($mid = 0) {
+ if (arg(3) == 'edit') {
+ if (!($item = db_fetch_array(db_query('SELECT * FROM {menu} WHERE mid = %d', $mid)))) {
+ drupal_not_found();
+ return;
+ }
+ }
+ else {
+ // This is an add form.
+ // The mid argument (if set) will be the default pid to use.
+ // Otherwise, we default to the "Navigation" menu (pid #1).
+ $default_pid = $mid ? $mid : 1;
+ $item = array('mid' => 0, 'pid' => $default_pid, 'weight' => 0, 'type' => MENU_CUSTOM_ITEM);
+ }
+
+ $form['title'] = array('#type' => 'textfield',
+ '#title' => t('Title'),
+ '#default_value' => $item['title'],
+ '#description' => t('The name of the menu item.'),
+ '#required' => TRUE,
+ );
+ $form['description'] = array('#type' => 'textfield',
+ '#title' => t('Description'),
+ '#default_value' => $item['description'],
+ '#description' => t('The description displayed when hovering over a menu item.'),
+ );
+
+ if ($item['type'] & MENU_CREATED_BY_ADMIN) {
+ $form['path'] = array('#type' => 'textfield',
+ '#title' => t('Path'),
+ '#default_value' => $item['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' => theme('placeholder', ''), '%add-node' => theme('placeholder', 'node/add'), '%drupal' => theme('placeholder', 'http://drupal.org'))),
+ '#required' => TRUE,
+ );
+ }
+ else {
+ $form['_path'] = array('#type' => 'item',
+ '#title' => t('Path'),
+ '#description' => l($item['path'], $item['path']),
+ );
+ $form['path'] = array('#type' => 'value', '#value' => $item['path']);
+ }
+
+ $expanded = $item['type'] & MENU_EXPANDED ? 1 : 0;
+ $form['expanded'] = array('#type' => 'checkbox',
+ '#title' => t('Expanded'),
+ '#default_value' => $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($item['mid']);
+ $form['pid'] = array('#type' => 'select',
+ '#title' => t('Parent item'),
+ '#default_value' => $item['pid'],
+ '#options' => $options,
+ );
+ $form['weight'] = array('#type' => 'weight',
+ '#title' => t('Weight'),
+ '#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.'),
+ );
+
+ // Always enable menu items (but not menus) when editing them.
+ if (!($item['type'] & MENU_IS_ROOT)) {
+ $item['type'] |= MENU_VISIBLE_IN_TREE | MENU_VISIBLE_IN_BREADCRUMB;
+ }
+
+ $form['type'] = array('#type' => 'value', '#value' => $item['type']);
+ $form['mid'] = array('#type' => 'value', '#value' => $item['mid']);
+ $form['submit'] = array('#type' => 'submit', '#value' => t('Submit'));
+
+ return drupal_get_form('menu_edit_item_form', $form);
+}
+
+/**
+ * Process menu and menu item add/edit form submissions.
+ */
+function menu_edit_item_form_submit($form_id, $form_values) {
+ menu_edit_item_save($form_values);
+ return 'admin/menu';
+}
+
+/**
+ * Menu callback; delete a single custom item.
+ */
+function menu_item_delete_form($mid) {
+ if (!($menu = db_fetch_object(db_query('SELECT type, title FROM {menu} WHERE mid = %d', $mid)))) {
+ drupal_not_found();
+ return;
+ }
+
+ $form['mid'] = array('#type' => 'value', '#value' => $mid);
+ $form['type'] = array('#type' => 'value', '#value' => $menu->type);
+ $form['title'] = array('#type' => 'value', '#value' => $menu->title);
+
+ if ($menu->type & MENU_IS_ROOT) {
+ $message = t('Are you sure you want to delete the menu %item?', array('%item' => theme('placeholder', $menu->title)));
+ }
+ else {
+ $message = t('Are you sure you want to delete the custom menu item %item?', array('%item' => theme('placeholder', $menu->title)));
+ }
+
+ return confirm_form('menu_confirm_delete_form', $form, $message, 'admin/menu', t('This action cannot be undone.'), t('Delete'));
+}
+
+/**
+ * Process menu delete form submissions.
+ */
+function menu_confirm_delete_form_submit($form_id, $form_values) {
+ menu_delete_item($form_values['mid']);
+
+ $t_args = array('%title' => theme('placeholder', $form_values['title']));
+ if ($form_values['type'] & MENU_IS_ROOT) {
+ drupal_set_message(t('The menu %title has been deleted.', $t_args));
+ watchdog('menu', t('Deleted menu %title.', $t_args), WATCHDOG_NOTICE);
+ }
+ else {
+ drupal_set_message(t('The menu item %title has been deleted.', $t_args));
+ watchdog('menu', t('Deleted menu item %title.', $t_args), WATCHDOG_NOTICE);
+ }
+
+ return 'admin/menu';
+}
+
+/**
+ * Menu callback; reset a single modified item.
+ */
+function menu_reset_item($mid) {
+ if (isset($mid) && $title = db_result(db_query('SELECT title FROM {menu} WHERE mid = %d', $mid))) {
+ $form['mid'] = array('#type' => 'value', '#value' => $mid);
+ return confirm_form('menu_reset_item_form', $form, t('Are you sure you want to reset the item %item to its default values?', array('%item' => theme('placeholder', $title))), 'admin/menu', t('Any customizations will be lost. This action cannot be undone.'), t('Reset'));
+ }
+ else {
+ drupal_not_found();
+ }
+}
+
+/**
+ * Process menu reset item form submissions.
+ */
+function menu_reset_item_form_submit($form_id, $form_values) {
+ menu_delete_item($form_values['mid']);
+ drupal_set_message(t('The menu item was reset to its default settings.'));
+
+ return 'admin/menu';
+}
+
+/**
+ * Menu callback; hide a menu item.
+ */
+function menu_disable_item($mid) {
+ $item = menu_get_item($mid);
+ $type = $item['type'];
+ $type &= ~MENU_VISIBLE_IN_TREE;
+ $type &= ~MENU_VISIBLE_IN_BREADCRUMB;
+ $type |= MENU_MODIFIED_BY_ADMIN;
+ db_query('UPDATE {menu} SET type = %d WHERE mid = %d', $type, $mid);
+ drupal_set_message(t('The menu item has been disabled.'));
+ drupal_goto('admin/menu');
+}
+
+/**
+ * Menu callback; present the main menu management page.
+ */
+function menu_overview() {
+ menu_rebuild();
+
+ return menu_overview_tree();
+}
+
+/**
+ * Save changes to a menu item into the database.
+ *
+ * @return mid
+ */
+function menu_edit_item_save($edit) {
+ if ($edit['expanded']) {
+ $edit['type'] |= MENU_EXPANDED;
+ }
+ else {
+ $edit['type'] &= ~MENU_EXPANDED;
+ }
+
+ $edit['type'] = $edit['type'] | MENU_MODIFIED_BY_ADMIN;
+
+ $status = menu_save_item($edit);
+
+ $t_args = array('%title' => theme('placeholder', $edit['title']));
+ if ($status == SAVED_UPDATED) {
+ drupal_set_message(t('The menu item %title has been updated.', $t_args));
+ }
+ elseif ($status == SAVED_NEW) {
+ drupal_set_message(t('The menu item %title has been added.', $t_args));
+ watchdog('menu', t('Added menu item %title.', $t_args), WATCHDOG_NOTICE, l(t('view'), 'admin/menu'));
+ }
+ return $edit['mid'];
+}
+
+/**
+ * Save a menu item to the database.
+ *
+ * @param $item
+ * The menu item to be saved. This is passed by reference, so that the newly
+ * generated $item['mid'] can be accessed after an insert takes place.
+ *
+ * @return $status
+ * The operation that was performed in saving. Either SAVED_NEW (if a new
+ * menu item was created), or SAVED_UPDATED (if an existing menu item was
+ * updated).
+ */
+function menu_save_item(&$item) {
+ $existing_item = NULL;
+
+ // Check that the item already exists in the menu tree, if $item['mid'] is
+ // specified.
+ if (isset($item['mid'])) {
+ $existing_item = menu_get_item($item['mid']);
+ }
+
+ if ($item['mid'] && !empty($existing_item)) {
+ db_query("UPDATE {menu} SET pid = %d, path = '%s', title = '%s', description = '%s', weight = %d, type = %d WHERE mid = %d", $item['pid'], $item['path'], $item['title'], $item['description'], $item['weight'], $item['type'], $item['mid']);
+ return SAVED_UPDATED;
+ }
+ else {
+ $item['mid'] = db_next_id('{menu}_mid');
+ // Check explicitly for mid <= 2. If the database was improperly prefixed,
+ // this would cause a nasty infinite loop or duplicate mid errors.
+ // TODO: have automatic prefixing through an installer to prevent this.
+ while ($item['mid'] <= 2) {
+ $item['mid'] = db_next_id('{menu}_mid');
+ }
+ db_query("INSERT INTO {menu} (mid, pid, path, title, description, weight, type) VALUES (%d, %d, '%s', '%s', '%s', %d, %d)", $item['mid'], $item['pid'], $item['path'], $item['title'], $item['description'], $item['weight'], $item['type']);
+ return SAVED_NEW;
+ }
+}
+
+/**
+ * Delete a menu item from the database. If $item['mid'] is specified, then
+ * this is used to find the existing item; otherwise, $item['path'] is used.
+ *
+ * @param $item
+ * The menu item to be deleted.
+ */
+function menu_delete_item($item) {
+ if (!is_array($item)) {
+ $item = array('mid' => $item);
+ }
+
+ if ($item['mid']) {
+ db_query('DELETE FROM {menu} WHERE mid = %d', $item['mid']);
+ }
+ elseif ($item['path']) {
+ db_query("DELETE FROM {menu} WHERE path = '%s'", $item['path']);
+ }
+}
+
+/**
+ * Present the menu tree, rendered along with links to edit menu items.
+ */
+function menu_overview_tree() {
+ $menu = menu_get_menu();
+ $root_menus = menu_get_root_menus();
+ $header = array(t('Menu item'), t('Expanded'), array('data' => t('Operations'), 'colspan' => '3'));
+ $output = '';
+
+ foreach ($root_menus as $mid => $title) {
+ $operations = array();
+ if ($menu['items'][$mid]['type'] & MENU_MODIFIABLE_BY_ADMIN) {
+ $operations[] = l(t('edit'), 'admin/menu/menu/edit/'. $mid);
+ }
+ if ($menu['items'][$mid]['type'] & MENU_CREATED_BY_ADMIN) {
+ $operations[] = l(t('delete'), 'admin/menu/menu/delete/'. $mid);
+ }
+ $operations[] = l(t('add item'), 'admin/menu/item/add/'. $mid);
+ $table = theme('item_list', $operations);
+ $table .= theme('table', $header, menu_overview_tree_rows($mid));
+ $output .= theme('box', check_plain($title), $table);
+ }
+ return $output;
+}
+
+function menu_overview_tree_rows($pid = 0, $depth = 0) {
+ $parent_item = menu_get_item($pid);
+ $rows = array();
+
+ if (isset($parent_item) && isset($parent_item['children'])) {
+ usort($parent_item['children'], '_menu_sort');
+ foreach ($parent_item['children'] as $mid) {
+ $item = menu_get_item($mid);
+ // Populate the title field.
+ $title = '';
+ if ($pid == 0) {
+ // Top-level items are menu names, and don't have an associated path.
+ $title .= check_plain($item['title']);
+ }
+ else {
+ $title .= l($item['title'], $item['path']);
+ }
+ if ($depth > 0) {
+ $title = '- '. $title;
+ }
+ for ($i = 1; $i < $depth; $i++) {
+ $title = ' '. $title;
+ }
+
+ // Populate the operations field.
+ $operations = array();
+ if (!($item['type'] & MENU_MODIFIABLE_BY_ADMIN)) {
+ $operations[] = array('data' => t('locked'), 'colspan' => '3', 'align' => 'center');
+ }
+ else {
+ // Set the edit column.
+ if ($item['type'] & (MENU_VISIBLE_IN_TREE | MENU_VISIBLE_IF_HAS_CHILDREN)) {
+ $operations[] = array('data' => l(t('edit'), 'admin/menu/item/edit/'. $mid));
+ }
+ else {
+ $operations[] = array('data' => '');
+ }
+
+ // Set the disable column.
+ if ($item['type'] & (MENU_IS_ROOT | MENU_VISIBLE_IF_HAS_CHILDREN)) {
+ // Disabling entire menus is done from block admin page.
+ // MENU_VISIBLE_IF_HAS_CHILDREN menus are always enabled so hide this operation.
+ $operations[] = array('data' => '');
+ }
+ else if ($item['type'] & MENU_VISIBLE_IN_TREE) {
+ $operations[] = array('data' => l(t('disable'), 'admin/menu/item/disable/'. $mid));
+ }
+ else {
+ $operations[] = array('data' => l(t('enable'), 'admin/menu/item/edit/'. $mid));
+ }
+
+ // Set the reset column.
+ if ($item['type'] & MENU_CREATED_BY_ADMIN) {
+ $operations[] = array('data' => l(t('delete'), 'admin/menu/item/delete/'. $mid));
+ }
+ else if ($item['type'] & MENU_MODIFIED_BY_ADMIN) {
+ $operations[] = array('data' => l(t('reset'), 'admin/menu/item/reset/'. $mid));
+ }
+ else {
+ $operations[] = array('data' => '');
+ }
+ }
+
+ // Call out disabled items.
+ if ($item['type'] & (MENU_VISIBLE_IN_TREE | MENU_VISIBLE_IF_HAS_CHILDREN)) {
+ $class = 'menu-enabled';
+ }
+ else {
+ $title .= ' ('. t('disabled') .')';
+ $class = 'menu-disabled';
+ }
+
+ if ($item['type'] & (MENU_MODIFIABLE_BY_ADMIN | MENU_VISIBLE_IN_TREE)) {
+ $row = array(array('data' => $title, 'class' => $class), array('data' => ($item['children'] ? (($item['type'] & MENU_EXPANDED) ? t('Yes') : t('No')) : ''), 'class' => $class));
+ foreach ($operations as $operation) {
+ $operation['class'] = $class;
+ $row[] = $operation;
+ }
+ $rows[] = $row;
+ $rows = array_merge($rows, menu_overview_tree_rows($mid, $depth + 1));
+ }
+ else {
+ // Skip items that are hidden and locked; admins will never care about them.
+ $rows = array_merge($rows, menu_overview_tree_rows($mid, $depth));
+ }
+ }
+ }
+
+ return $rows;
+}
+
+/**
+ * Return a list of menu items that are valid possible parents for the
+ * given menu item. The list excludes the given item and its children.
+ *
+ * @param $mid
+ * The menu item id for which to generate a list of parents.
+ * If $mid == 0 then the complete tree is returned.
+ * @param $pid
+ * The menu item id of the menu item at which to start the tree.
+ * If $pid > 0 then this item will be included in the tree.
+ * @param $depth
+ * The current depth in the tree - used when recursing to indent the tree.
+ * @return
+ * An array of menu titles keyed on the mid.
+ */
+function menu_parent_options($mid, $pid = 0, $depth = 0) {
+ $options = array();
+
+ if (!($parent_item = menu_get_item($pid))) {
+ return $options;
+ }
+
+ // Exclude $mid and its children from the list unless $mid is 0.
+ if ($mid && $mid == $pid) {
+ return $options;
+ }
+
+ // Add the current $pid to the list.
+ if ($pid > 0 && ($parent_item['type'] & (MENU_MODIFIABLE_BY_ADMIN | MENU_IS_ROOT))) {
+ $title = ' '. $parent_item['title'];
+ for ($i = 0; $i < $depth; $i++) {
+ $title = '--'. $title;
+ }
+ if (!($parent_item['type'] & MENU_VISIBLE_IN_TREE)) {
+ $title .= ' ('. t('disabled') .')';
+ }
+ $options[$pid] = $title;
+ $depth ++;
+ }
+
+ // Add children of $pid to the list recursively.
+ if ($parent_item['children']) {
+ usort($parent_item['children'], '_menu_sort');
+ foreach ($parent_item['children'] as $child) {
+ $options += menu_parent_options($mid, $child, $depth);
+ }
+ }
+
+ return $options;
+}
+
+/**
+ * Remove the menu item.
+ */
+function menu_node_form_delete($node) {
+ menu_delete_item(array('path' => 'node/'. $node->nid));
+}
diff --git a/modules/node.module b/modules/node.module
new file mode 100644
index 0000000..7ebfb11
--- /dev/null
+++ b/modules/node.module
@@ -0,0 +1,2481 @@
+'. t('All content in a website is stored and treated as nodes. Therefore nodes are any postings such as blogs, stories, polls and forums. The node module manages these content types and is one of the strengths of Drupal over other content management systems.') .'';
+ $output .= ''. t('Treating all content as nodes allows the flexibility of creating new types of content. It also allows you to painlessly apply new features or changes to all content. Comments are not stored as nodes but are always associated with a node.') .'
';
+ $output .= t('Node module features
+
+- The list tab provides an interface to search and sort all content on your site.
+- The configure settings tab has basic settings for content on your site.
+- The configure content types tab lists all content types for your site and lets you configure their default workflow.
+- The search tab lets you search all content on your site
+
+');
+ $output .= t('You can
+
+- search for content at search.
+- administer nodes at administer >> settings >> content types.
+
+', array('%search' => url('search'), '%admin-settings-content-types' => url('admin/settings/content-types')));
+ $output .= ''. t('For more information please read the configuration and customization handbook Node page.', array('%node' => 'http://drupal.org/handbook/modules/node/')) .'
';
+ return $output;
+ case 'admin/modules#description':
+ return t('Allows content to be submitted to the site and displayed on pages.');
+ case 'admin/node/configure':
+ case 'admin/node/configure/settings':
+ return t('Settings for the core of Drupal. Almost everything is a node so these settings will affect most of the site.
');
+ case 'admin/node':
+ return t('Below is a list of all of the posts on your site. Other forms of content are listed elsewhere (e.g. comments).
Clicking a title views the post, while clicking an author\'s name views their user information.
', array('%comments' => url('admin/comment')));
+ case 'admin/node/search':
+ return t('Enter a simple pattern to search for a post. This can include the wildcard character *.
For example, a search for "br*" might return "bread bakers", "our daily bread" and "brenda".
');
+ }
+
+ if (arg(0) == 'node' && is_numeric(arg(1)) && arg(2) == 'revisions') {
+ return t('The revisions let you track differences between multiple versions of a post.');
+ }
+
+ if (arg(0) == 'node' && arg(1) == 'add' && $type = arg(2)) {
+ return filter_xss_admin(variable_get($type .'_help', ''));
+ }
+}
+
+/**
+ * Implementation of hook_cron().
+ */
+function node_cron() {
+ db_query('DELETE FROM {history} WHERE timestamp < %d', NODE_NEW_LIMIT);
+}
+
+/**
+ * Gather a listing of links to nodes.
+ *
+ * @param $result
+ * A DB result object from a query to fetch node objects. If your query joins the node_comment_statistics
table so that the comment_count
field is available, a title attribute will be added to show the number of comments.
+ * @param $title
+ * A heading for the resulting list.
+ *
+ * @return
+ * An HTML list suitable as content for a block.
+ */
+function node_title_list($result, $title = NULL) {
+ while ($node = db_fetch_object($result)) {
+ $items[] = l($node->title, 'node/'. $node->nid, $node->comment_count ? array('title' => format_plural($node->comment_count, '1 comment', '%count comments')) : '');
+ }
+
+ return theme('node_list', $items, $title);
+}
+
+/**
+ * Format a listing of links to nodes.
+ */
+function theme_node_list($items, $title = NULL) {
+ return theme('item_list', $items, $title);
+}
+
+/**
+ * Update the 'last viewed' timestamp of the specified node for current user.
+ */
+function node_tag_new($nid) {
+ global $user;
+
+ if ($user->uid) {
+ if (node_last_viewed($nid)) {
+ db_query('UPDATE {history} SET timestamp = %d WHERE uid = %d AND nid = %d', time(), $user->uid, $nid);
+ }
+ else {
+ @db_query('INSERT INTO {history} (uid, nid, timestamp) VALUES (%d, %d, %d)', $user->uid, $nid, time());
+ }
+ }
+}
+
+/**
+ * Retrieves the timestamp at which the current user last viewed the
+ * specified node.
+ */
+function node_last_viewed($nid) {
+ global $user;
+ static $history;
+
+ if (!isset($history[$nid])) {
+ $history[$nid] = db_fetch_object(db_query("SELECT timestamp FROM {history} WHERE uid = '$user->uid' AND nid = %d", $nid));
+ }
+
+ return (isset($history[$nid]->timestamp) ? $history[$nid]->timestamp : 0);
+}
+
+/**
+ * Decide on the type of marker to be displayed for a given node.
+ *
+ * @param $nid
+ * Node ID whose history supplies the "last viewed" timestamp.
+ * @param $timestamp
+ * Time which is compared against node's "last viewed" timestamp.
+ * @return
+ * One of the MARK constants.
+ */
+function node_mark($nid, $timestamp) {
+ global $user;
+ static $cache;
+
+ if (!$user->uid) {
+ return MARK_READ;
+ }
+ if (!isset($cache[$nid])) {
+ $cache[$nid] = node_last_viewed($nid);
+ }
+ if ($cache[$nid] == 0 && $timestamp > NODE_NEW_LIMIT) {
+ return MARK_NEW;
+ }
+ elseif ($timestamp > $cache[$nid] && $timestamp > NODE_NEW_LIMIT) {
+ return MARK_UPDATED;
+ }
+ return MARK_READ;
+}
+
+/**
+ * Automatically generate a teaser for a node body in a given format.
+ */
+function node_teaser($body, $format = NULL) {
+
+ $size = variable_get('teaser_length', 600);
+
+ // find where the delimiter is in the body
+ $delimiter = strpos($body, '');
+
+ // If the size is zero, and there is no delimiter, the entire body is the teaser.
+ if ($size == 0 && $delimiter === FALSE) {
+ return $body;
+ }
+
+ // We check for the presence of the PHP evaluator filter in the current
+ // format. If the body contains PHP code, we do not split it up to prevent
+ // parse errors.
+ if (isset($format)) {
+ $filters = filter_list_format($format);
+ if (isset($filters['filter/1']) && strpos($body, '') !== FALSE) {
+ return $body;
+ }
+ }
+
+ // If a valid delimiter has been specified, use it to chop of the teaser.
+ if ($delimiter !== FALSE) {
+ return substr($body, 0, $delimiter);
+ }
+
+ // If we have a short body, the entire body is the teaser.
+ if (strlen($body) < $size) {
+ return $body;
+ }
+
+ // In some cases, no delimiter has been specified (e.g. when posting using
+ // the Blogger API). In this case, we try to split at paragraph boundaries.
+ // When even the first paragraph is too long, we try to split at the end of
+ // the next sentence.
+ $breakpoints = array('' => 4, '
' => 0, '
' => 0, "\n" => 0, '. ' => 1, '! ' => 1, '? ' => 1, 'ã' => 3, 'Ø ' => 1);
+ foreach ($breakpoints as $point => $charnum) {
+ if ($length = strpos($body, $point, $size)) {
+ return substr($body, 0, $length + $charnum);
+ }
+ }
+
+ // If all else fails, we simply truncate the string.
+ return truncate_utf8($body, $size);
+}
+
+function _node_names($op = '', $node = NULL) {
+ static $node_names = array();
+ static $node_list = array();
+
+ if (empty($node_names)) {
+ $node_names = module_invoke_all('node_info');
+ foreach ($node_names as $type => $value) {
+ $node_list[$type] = $value['name'];
+ }
+ }
+ if ($node) {
+ if (is_array($node)) {
+ $type = $node['type'];
+ }
+ elseif (is_object($node)) {
+ $type = $node->type;
+ }
+ elseif (is_string($node)) {
+ $type = $node;
+ }
+ if (!isset($node_names[$type])) {
+ return FALSE;
+ }
+ }
+ switch ($op) {
+ case 'base':
+ return $node_names[$type]['base'];
+ case 'list':
+ return $node_list;
+ case 'name':
+ return $node_list[$type];
+ }
+}
+
+/**
+ * Determine the basename for hook_load etc.
+ *
+ * @param $node
+ * Either a node object, a node array, or a string containing the node type.
+ * @return
+ * The basename for hook_load, hook_nodeapi etc.
+ */
+function node_get_base($node) {
+ return _node_names('base', $node);
+}
+
+/**
+ * Determine the human readable name for a given type.
+ *
+ * @param $node
+ * Either a node object, a node array, or a string containing the node type.
+ * @return
+ * The human readable name of the node type.
+ */
+function node_get_name($node) {
+ return _node_names('name', $node);
+}
+
+/**
+ * Return the list of available node types.
+ *
+ * @param $node
+ * Either a node object, a node array, or a string containing the node type.
+ * @return
+ * An array consisting ('#type' => name) pairs.
+ */
+function node_get_types() {
+ return _node_names('list');
+}
+
+/**
+ * Determine whether a node hook exists.
+ *
+ * @param &$node
+ * Either a node object, node array, or a string containing the node type.
+ * @param $hook
+ * A string containing the name of the hook.
+ * @return
+ * TRUE iff the $hook exists in the node type of $node.
+ */
+function node_hook(&$node, $hook) {
+ return module_hook(node_get_base($node), $hook);
+}
+
+/**
+ * Invoke a node hook.
+ *
+ * @param &$node
+ * Either a node object, node array, or a string containing the node type.
+ * @param $hook
+ * A string containing the name of the hook.
+ * @param $a2, $a3, $a4
+ * Arguments to pass on to the hook, after the $node argument.
+ * @return
+ * The returned value of the invoked hook.
+ */
+function node_invoke(&$node, $hook, $a2 = NULL, $a3 = NULL, $a4 = NULL) {
+ if (node_hook($node, $hook)) {
+ $function = node_get_base($node) ."_$hook";
+ return ($function($node, $a2, $a3, $a4));
+ }
+}
+
+/**
+ * Invoke a hook_nodeapi() operation in all modules.
+ *
+ * @param &$node
+ * A node object.
+ * @param $op
+ * A string containing the name of the nodeapi operation.
+ * @param $a3, $a4
+ * Arguments to pass on to the hook, after the $node and $op arguments.
+ * @return
+ * The returned value of the invoked hooks.
+ */
+function node_invoke_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
+ $return = array();
+ foreach (module_implements('nodeapi') as $name) {
+ $function = $name .'_nodeapi';
+ $result = $function($node, $op, $a3, $a4);
+ if (isset($result) && is_array($result)) {
+ $return = array_merge($return, $result);
+ }
+ else if (isset($result)) {
+ $return[] = $result;
+ }
+ }
+ return $return;
+}
+
+/**
+ * Load a node object from the database.
+ *
+ * @param $param
+ * Either the nid of the node or an array of conditions to match against in the database query
+ * @param $revision
+ * Which numbered revision to load. Defaults to the current version.
+ * @param $reset
+ * Whether to reset the internal node_load cache.
+ *
+ * @return
+ * A fully-populated node object.
+ */
+function node_load($param = array(), $revision = NULL, $reset = NULL) {
+ static $nodes = array();
+
+ if ($reset) {
+ $nodes = array();
+ }
+
+ $arguments = array();
+ if (is_numeric($param)) {
+ $cachable = $revision == NULL;
+ if ($cachable && isset($nodes[$param])) {
+ return $nodes[$param];
+ }
+ $cond = 'n.nid = %d';
+ $arguments[] = $param;
+ }
+ else {
+ // Turn the conditions into a query.
+ foreach ($param as $key => $value) {
+ $cond[] = 'n.'. db_escape_string($key) ." = '%s'";
+ $arguments[] = $value;
+ }
+ $cond = implode(' AND ', $cond);
+ }
+
+ // Retrieve the node.
+ // No db_rewrite_sql is applied so as to get complete indexing for search.
+ if ($revision) {
+ array_unshift($arguments, $revision);
+ $node = db_fetch_object(db_query('SELECT n.nid, r.vid, n.type, n.status, n.created, n.changed, n.comment, n.promote, n.moderate, n.sticky, r.timestamp AS revision_timestamp, r.title, r.body, r.teaser, r.log, r.format, u.uid, u.name, u.picture, u.data FROM {node} n INNER JOIN {users} u ON u.uid = n.uid INNER JOIN {node_revisions} r ON r.nid = n.nid AND r.vid = %d WHERE '. $cond, $arguments));
+ }
+ else {
+ $node = db_fetch_object(db_query('SELECT n.nid, n.vid, n.type, n.status, n.created, n.changed, n.comment, n.promote, n.moderate, n.sticky, r.timestamp AS revision_timestamp, r.title, r.body, r.teaser, r.log, r.format, u.uid, u.name, u.picture, u.data FROM {node} n INNER JOIN {users} u ON u.uid = n.uid INNER JOIN {node_revisions} r ON r.vid = n.vid WHERE '. $cond, $arguments));
+ }
+
+ if ($node->nid) {
+ // Call the node specific callback (if any) and piggy-back the
+ // results to the node or overwrite some values.
+ if ($extra = node_invoke($node, 'load')) {
+ foreach ($extra as $key => $value) {
+ $node->$key = $value;
+ }
+ }
+
+ if ($extra = node_invoke_nodeapi($node, 'load')) {
+ foreach ($extra as $key => $value) {
+ $node->$key = $value;
+ }
+ }
+ }
+
+ if ($cachable) {
+ $nodes[$param] = $node;
+ }
+
+ return $node;
+}
+
+/**
+ * Save a node object into the database.
+ */
+function node_save(&$node) {
+ global $user;
+
+ $node->is_new = false;
+
+ // Apply filters to some default node fields:
+ if (empty($node->nid)) {
+ // Insert a new node.
+ $node->is_new = true;
+
+ $node->nid = db_next_id('{node}_nid');
+ $node->vid = db_next_id('{node_revisions}_vid');;
+ }
+ else {
+ // We need to ensure that all node fields are filled.
+ $node_current = node_load($node->nid);
+ foreach ($node as $field => $data) {
+ $node_current->$field = $data;
+ }
+ $node = $node_current;
+
+ if ($node->revision) {
+ $node->old_vid = $node->vid;
+ $node->vid = db_next_id('{node_revisions}_vid');
+ }
+ }
+
+ // Set some required fields:
+ if (empty($node->created)) {
+ $node->created = time();
+ }
+ // The changed timestamp is always updated for bookkeeping purposes (revisions, searching, ...)
+ $node->changed = time();
+
+ // Split off revisions data to another structure
+ $revisions_table_values = array('nid' => $node->nid, 'vid' => $node->vid,
+ 'title' => $node->title, 'body' => $node->body,
+ 'teaser' => $node->teaser, 'log' => $node->log, 'timestamp' => $node->changed,
+ 'uid' => $user->uid, 'format' => $node->format);
+ $revisions_table_types = array('nid' => '%d', 'vid' => '%d',
+ 'title' => "'%s'", 'body' => "'%s'",
+ 'teaser' => "'%s'", 'log' => "'%s'", 'timestamp' => '%d',
+ 'uid' => '%d', 'format' => '%d');
+ $node_table_values = array('nid' => $node->nid, 'vid' => $node->vid,
+ 'title' => $node->title, 'type' => $node->type, 'uid' => $node->uid,
+ 'status' => $node->status, 'created' => $node->created,
+ 'changed' => $node->changed, 'comment' => $node->comment,
+ 'promote' => $node->promote, 'moderate' => $node->moderate,
+ 'sticky' => $node->sticky);
+ $node_table_types = array('nid' => '%d', 'vid' => '%d',
+ 'title' => "'%s'", 'type' => "'%s'", 'uid' => '%d',
+ 'status' => '%d', 'created' => '%d',
+ 'changed' => '%d', 'comment' => '%d',
+ 'promote' => '%d', 'moderate' => '%d',
+ 'sticky' => '%d');
+
+ //Generate the node table query and the
+ //the node_revisions table query
+ if ($node->is_new) {
+ $node_query = 'INSERT INTO {node} ('. implode(', ', array_keys($node_table_types)) .') VALUES ('. implode(', ', $node_table_types) .')';
+ $revisions_query = 'INSERT INTO {node_revisions} ('. implode(', ', array_keys($revisions_table_types)) .') VALUES ('. implode(', ', $revisions_table_types) .')';
+ }
+ else {
+ $arr = array();
+ foreach ($node_table_types as $key => $value) {
+ $arr[] = $key .' = '. $value;
+ }
+ $node_table_values[] = $node->nid;
+ $node_query = 'UPDATE {node} SET '. implode(', ', $arr) .' WHERE nid = %d';
+ if ($node->revision) {
+ $revisions_query = 'INSERT INTO {node_revisions} ('. implode(', ', array_keys($revisions_table_types)) .') VALUES ('. implode(', ', $revisions_table_types) .')';
+ }
+ else {
+ $arr = array();
+ foreach ($revisions_table_types as $key => $value) {
+ $arr[] = $key .' = '. $value;
+ }
+ $revisions_table_values[] = $node->vid;
+ $revisions_query = 'UPDATE {node_revisions} SET '. implode(', ', $arr) .' WHERE vid = %d';
+ }
+ }
+
+ // Insert the node into the database:
+ db_query($node_query, $node_table_values);
+ db_query($revisions_query, $revisions_table_values);
+
+ // Call the node specific callback (if any):
+ if ($node->is_new) {
+ node_invoke($node, 'insert');
+ node_invoke_nodeapi($node, 'insert');
+ }
+ else {
+ node_invoke($node, 'update');
+ node_invoke_nodeapi($node, 'update');
+ }
+
+ // Clear the cache so an anonymous poster can see the node being added or updated.
+ cache_clear_all();
+}
+
+/**
+ * Generate a display of the given node.
+ *
+ * @param $node
+ * A node array or node object.
+ * @param $teaser
+ * Whether to display the teaser only, as on the main page.
+ * @param $page
+ * Whether the node is being displayed by itself as a page.
+ * @param $links
+ * Whether or not to display node links. Links are omitted for node previews.
+ *
+ * @return
+ * An HTML representation of the themed node.
+ */
+function node_view($node, $teaser = FALSE, $page = FALSE, $links = TRUE) {
+ $node = (object)$node;
+
+ // Remove the delimiter (if any) that separates the teaser from the body.
+ // TODO: this strips legitimate uses of '' also.
+ $node->body = str_replace('', '', $node->body);
+
+ if ($node->log != '' && !$teaser && $node->moderate) {
+ $node->body .= ''. t('Log') .':'. filter_xss($node->log) .'';
+ }
+
+ // The 'view' hook can be implemented to overwrite the default function
+ // to display nodes.
+ if (node_hook($node, 'view')) {
+ node_invoke($node, 'view', $teaser, $page);
+ }
+ else {
+ $node = node_prepare($node, $teaser);
+ }
+ // Allow modules to change $node->body before viewing.
+ node_invoke_nodeapi($node, 'view', $teaser, $page);
+ if ($links) {
+ $node->links = module_invoke_all('link', 'node', $node, !$page);
+ }
+ // unset unused $node part so that a bad theme can not open a security hole
+ if ($teaser) {
+ unset($node->body);
+ }
+ else {
+ unset($node->teaser);
+ }
+
+ return theme('node', $node, $teaser, $page);
+}
+
+/**
+ * Apply filters to a node in preparation for theming.
+ */
+function node_prepare($node, $teaser = FALSE) {
+ $node->readmore = (strlen($node->teaser) < strlen($node->body));
+ if ($teaser == FALSE) {
+ $node->body = check_markup($node->body, $node->format, FALSE);
+ }
+ else {
+ $node->teaser = check_markup($node->teaser, $node->format, FALSE);
+ }
+ return $node;
+}
+
+/**
+ * Generate a page displaying a single node, along with its comments.
+ */
+function node_show($node, $cid) {
+ $output = node_view($node, FALSE, TRUE);
+
+ if (function_exists('comment_render') && $node->comment) {
+ $output .= comment_render($node, $cid);
+ }
+
+ // Update the history table, stating that this user viewed this node.
+ node_tag_new($node->nid);
+
+ return $output;
+}
+
+/**
+ * Implementation of hook_perm().
+ */
+function node_perm() {
+ return array('administer nodes', 'access content', 'view revisions', 'revert revisions');
+}
+
+/**
+ * Implementation of hook_search().
+ */
+function node_search($op = 'search', $keys = null) {
+ switch ($op) {
+ case 'name':
+ return t('content');
+
+ case 'reset':
+ variable_del('node_cron_last');
+ variable_del('node_cron_last_nid');
+ return;
+
+ case 'status':
+ $last = variable_get('node_cron_last', 0);
+ $last_nid = variable_get('node_cron_last_nid', 0);
+ $total = db_result(db_query('SELECT COUNT(*) FROM {node} WHERE status = 1'));
+ $remaining = db_result(db_query('SELECT COUNT(*) FROM {node} n LEFT JOIN {node_comment_statistics} c ON n.nid = c.nid WHERE n.status = 1 AND ((GREATEST(n.created, n.changed, c.last_comment_timestamp) = %d AND n.nid > %d ) OR (n.created > %d OR n.changed > %d OR c.last_comment_timestamp > %d))', $last, $last_nid, $last, $last, $last));
+ return array('remaining' => $remaining, 'total' => $total);
+
+ case 'admin':
+ $form = array();
+ // Output form for defining rank factor weights.
+ $form['content_ranking'] = array('#type' => 'fieldset', '#title' => t('Content ranking'));
+ $form['content_ranking']['#theme'] = 'node_search_admin';
+ $form['content_ranking']['info'] = array('#type' => 'markup', '#value' => ''. t('The following numbers control which properties the content search should favor when ordering the results. Higher numbers mean more influence, zero means the property is ignored. Changing these numbers does not require the search index to be rebuilt. Changes take effect immediately.') .'');
+
+ $ranking = array('node_rank_relevance' => t('Keyword relevance'),
+ 'node_rank_recent' => t('Recently posted'));
+ if (module_exist('comment')) {
+ $ranking['node_rank_comments'] = t('Number of comments');
+ }
+ if (module_exist('statistics') && variable_get('statistics_count_content_views', 0)) {
+ $ranking['node_rank_views'] = t('Number of views');
+ }
+
+ // Note: reversed to reflect that higher number = higher ranking.
+ $options = drupal_map_assoc(range(0, 10));
+ foreach ($ranking as $var => $title) {
+ $form['content_ranking']['factors'][$var] = array('#title' => $title, '#type' => 'select', '#options' => $options, '#default_value' => variable_get($var, 5));
+ }
+ return $form;
+
+ case 'search':
+ // Build matching conditions
+ list($join1, $where1) = _db_rewrite_sql();
+ $arguments1 = array();
+ $conditions1 = 'n.status = 1';
+
+ if ($type = search_query_extract($keys, 'type')) {
+ $types = array();
+ foreach (explode(',', $type) as $t) {
+ $types[] = "n.type = '%s'";
+ $arguments1[] = $t;
+ }
+ $conditions1 .= ' AND ('. implode(' OR ', $types) .')';
+ $keys = search_query_insert($keys, 'type');
+ }
+
+ if ($category = search_query_extract($keys, 'category')) {
+ $categories = array();
+ foreach (explode(',', $category) as $c) {
+ $categories[] = "tn.tid = %d";
+ $arguments1[] = $c;
+ }
+ $conditions1 .= ' AND ('. implode(' OR ', $categories) .')';
+ $join1 .= ' INNER JOIN {term_node} tn ON n.nid = tn.nid';
+ $keys = search_query_insert($keys, 'category');
+ }
+
+ // Build ranking expression (we try to map each parameter to a
+ // uniform distribution in the range 0..1).
+ $ranking = array();
+ $arguments2 = array();
+ $join2 = '';
+ // Used to avoid joining on node_comment_statistics twice
+ $stats_join = false;
+ if ($weight = (int)variable_get('node_rank_relevance', 5)) {
+ // Average relevance values hover around 0.15
+ $ranking[] = '%d * i.relevance';
+ $arguments2[] = $weight;
+ }
+ if ($weight = (int)variable_get('node_rank_recent', 5)) {
+ // Exponential decay with half-life of 6 months, starting at last indexed node
+ $ranking[] = '%d * POW(2, (GREATEST(n.created, n.changed, c.last_comment_timestamp) - %d) * 6.43e-8)';
+ $arguments2[] = $weight;
+ $arguments2[] = (int)variable_get('node_cron_last', 0);
+ $join2 .= ' INNER JOIN {node} n ON n.nid = i.sid LEFT JOIN {node_comment_statistics} c ON c.nid = i.sid';
+ $stats_join = true;
+ }
+ if (module_exist('comment') && $weight = (int)variable_get('node_rank_comments', 5)) {
+ // Inverse law that maps the highest reply count on the site to 1 and 0 to 0.
+ $scale = variable_get('node_cron_comments_scale', 0.0);
+ $ranking[] = '%d * (2.0 - 2.0 / (1.0 + c.comment_count * %f))';
+ $arguments2[] = $weight;
+ $arguments2[] = $scale;
+ if (!$stats_join) {
+ $join2 .= ' LEFT JOIN {node_comment_statistics} c ON c.nid = i.sid';
+ }
+ }
+ if (module_exist('statistics') && variable_get('statistics_count_content_views', 0) &&
+ $weight = (int)variable_get('node_rank_views', 5)) {
+ // Inverse law that maps the highest view count on the site to 1 and 0 to 0.
+ $scale = variable_get('node_cron_views_scale', 0.0);
+ $ranking[] = '%d * (2.0 - 2.0 / (1.0 + nc.totalcount * %f))';
+ $arguments2[] = $weight;
+ $arguments2[] = $scale;
+ $join2 .= ' LEFT JOIN {node_counter} nc ON nc.nid = i.sid';
+ }
+ $select2 = (count($ranking) ? implode(' + ', $ranking) : 'i.relevance') . ' AS score';
+
+ // Do search
+ $find = do_search($keys, 'node', 'INNER JOIN {node} n ON n.nid = i.sid '. $join1 .' INNER JOIN {users} u ON n.uid = u.uid', $conditions1 . (empty($where1) ? '' : ' AND '. $where1), $arguments1, $select2, $join2, $arguments2);
+
+ // Load results
+ $results = array();
+ foreach ($find as $item) {
+ $node = node_load($item->sid);
+
+ // Get node output (filtered and with module-specific fields).
+ if (node_hook($node, 'view')) {
+ node_invoke($node, 'view', false, false);
+ }
+ else {
+ $node = node_prepare($node, false);
+ }
+ // Allow modules to change $node->body before viewing.
+ node_invoke_nodeapi($node, 'view', false, false);
+
+ // Fetch comments for snippet
+ $node->body .= module_invoke('comment', 'nodeapi', $node, 'update index');
+ // Fetch terms for snippet
+ $node->body .= module_invoke('taxonomy', 'nodeapi', $node, 'update index');
+
+ $extra = node_invoke_nodeapi($node, 'search result');
+ $results[] = array('link' => url('node/'. $item->sid),
+ 'type' => node_get_name($node),
+ 'title' => $node->title,
+ 'user' => theme('username', $node),
+ 'date' => $node->changed,
+ 'node' => $node,
+ 'extra' => $extra,
+ 'snippet' => search_excerpt($keys, $node->body));
+ }
+ return $results;
+ }
+}
+
+/**
+ * Implementation of hook_user().
+ */
+function node_user($op, &$edit, &$user) {
+ if ($op == 'delete') {
+ db_query('UPDATE {node} SET uid = 0 WHERE uid = %d', $user->uid);
+ db_query('UPDATE {node_revisions} SET uid = 0 WHERE uid = %d', $user->uid);
+ }
+}
+
+function theme_node_search_admin($form) {
+ $output = form_render($form['info']);
+
+ $header = array(t('Factor'), t('Weight'));
+ foreach (element_children($form['factors']) as $key) {
+ $row = array();
+ $row[] = $form['factors'][$key]['#title'];
+ unset($form['factors'][$key]['#title']);
+ $row[] = form_render($form['factors'][$key]);
+ $rows[] = $row;
+ }
+ $output .= theme('table', $header, $rows);
+
+ $output .= form_render($form);
+ return $output;
+}
+
+/**
+ * Menu callback; presents general node configuration options.
+ */
+function node_configure() {
+
+ $form['default_nodes_main'] = array(
+ '#type' => 'select', '#title' => t('Number of posts on main page'), '#default_value' => variable_get('default_nodes_main', 10),
+ '#options' => drupal_map_assoc(array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 25, 30)),
+ '#description' => t('The default maximum number of posts to display per page on overview pages such as the main page.')
+ );
+
+ $form['teaser_length'] = array(
+ '#type' => 'select', '#title' => t('Length of trimmed posts'), '#default_value' => variable_get('teaser_length', 600),
+ '#options' => array(0 => t('Unlimited'), 200 => t('200 characters'), 400 => t('400 characters'), 600 => t('600 characters'),
+ 800 => t('800 characters'), 1000 => t('1000 characters'), 1200 => t('1200 characters'), 1400 => t('1400 characters'),
+ 1600 => t('1600 characters'), 1800 => t('1800 characters'), 2000 => t('2000 characters')),
+ '#description' => t("The maximum number of characters used in the trimmed version of a post. Drupal will use this setting to determine at which offset long posts should be trimmed. The trimmed version of a post is typically used as a teaser when displaying the post on the main page, in XML feeds, etc. To disable teasers, set to 'Unlimited'. Note that this setting will only affect new or updated content and will not affect existing teasers.")
+ );
+
+ $form['node_preview'] = array(
+ '#type' => 'radios', '#title' => t('Preview post'), '#default_value' => variable_get('node_preview', 0),
+ '#options' => array(t('Optional'), t('Required')), '#description' => t('Must users preview posts before submitting?')
+ );
+
+ return system_settings_form('node_configure', $form);
+}
+
+/**
+ * Retrieve the comment mode for the given node ID (none, read, or read/write).
+ */
+function node_comment_mode($nid) {
+ static $comment_mode;
+ if (!isset($comment_mode[$nid])) {
+ $comment_mode[$nid] = db_result(db_query('SELECT comment FROM {node} WHERE nid = %d', $nid));
+ }
+ return $comment_mode[$nid];
+}
+
+/**
+ * Implementation of hook_link().
+ */
+function node_link($type, $node = 0, $main = 0) {
+ $links = array();
+
+ if ($type == 'node') {
+ if (array_key_exists('links', $node)) {
+ $links = $node->links;
+ }
+
+ if ($main == 1 && $node->teaser && $node->readmore) {
+ $links[] = l(t('read more'), "node/$node->nid", array('title' => t('Read the rest of this posting.'), 'class' => 'read-more'));
+ }
+ }
+
+ return $links;
+}
+
+/**
+ * Implementation of hook_menu().
+ */
+function node_menu($may_cache) {
+ $items = array();
+
+ if ($may_cache) {
+ $items[] = array('path' => 'admin/node', 'title' => t('content'),
+ 'callback' => 'node_admin_nodes',
+ 'access' => user_access('administer nodes'));
+ $items[] = array('path' => 'admin/node/overview', 'title' => t('list'),
+ 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -10);
+
+ if (module_exist('search')) {
+ $items[] = array('path' => 'admin/node/search', 'title' => t('search'),
+ 'callback' => 'node_admin_search',
+ 'access' => user_access('administer nodes'),
+ 'type' => MENU_LOCAL_TASK);
+ }
+
+ $items[] = array('path' => 'admin/settings/node', 'title' => t('posts'),
+ 'callback' => 'node_configure',
+ 'access' => user_access('administer nodes'));
+ $items[] = array('path' => 'admin/settings/content-types', 'title' => t('content types'),
+ 'callback' => 'node_types_configure',
+ 'access' => user_access('administer nodes'));
+
+ $items[] = array('path' => 'node', 'title' => t('content'),
+ 'callback' => 'node_page',
+ 'access' => user_access('access content'),
+ 'type' => MENU_MODIFIABLE_BY_ADMIN);
+ $items[] = array('path' => 'node/add', 'title' => t('create content'),
+ 'callback' => 'node_page',
+ 'access' => user_access('access content'),
+ 'type' => MENU_ITEM_GROUPING,
+ 'weight' => 1);
+ $items[] = array('path' => 'rss.xml', 'title' => t('rss feed'),
+ 'callback' => 'node_feed',
+ 'access' => user_access('access content'),
+ 'type' => MENU_CALLBACK);
+ }
+ else {
+ if (arg(0) == 'node' && is_numeric(arg(1))) {
+ $node = node_load(arg(1));
+ if ($node->nid) {
+ $items[] = array('path' => 'node/'. arg(1), 'title' => t('view'),
+ 'callback' => 'node_page',
+ 'access' => node_access('view', $node),
+ 'type' => MENU_CALLBACK);
+ $items[] = array('path' => 'node/'. arg(1) .'/view', 'title' => t('view'),
+ 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -10);
+ $items[] = array('path' => 'node/'. arg(1) .'/edit', 'title' => t('edit'),
+ 'callback' => 'node_page',
+ 'access' => node_access('update', $node),
+ 'weight' => 1,
+ 'type' => MENU_LOCAL_TASK);
+ $items[] = array('path' => 'node/'. arg(1) .'/delete', 'title' => t('delete'),
+ 'callback' => 'node_delete_confirm',
+ 'access' => node_access('delete', $node),
+ 'weight' => 1,
+ 'type' => MENU_CALLBACK);
+ $revisions_access = ((user_access('view revisions') || user_access('administer nodes')) && node_access('view', $node) && db_result(db_query('SELECT COUNT(vid) FROM {node_revisions} WHERE nid = %d', arg(1))) > 1);
+ $items[] = array('path' => 'node/'. arg(1) .'/revisions', 'title' => t('revisions'),
+ 'callback' => 'node_revisions',
+ 'access' => $revisions_access,
+ 'weight' => 2,
+ 'type' => MENU_LOCAL_TASK);
+ }
+ }
+ else if (arg(0) == 'admin' && arg(1) == 'settings' && arg(2) == 'content-types' && is_string(arg(3))) {
+ $items[] = array('path' => 'admin/settings/content-types/'. arg(3),
+ 'title' => t("'%name' content type", array('%name' => node_get_name(arg(3)))),
+ 'type' => MENU_CALLBACK);
+ }
+ }
+
+ return $items;
+}
+
+function node_last_changed($nid) {
+ $node = db_fetch_object(db_query('SELECT changed FROM {node} WHERE nid = %d', $nid));
+ return ($node->changed);
+}
+
+/**
+ * List node administration operations that can be performed.
+ */
+function node_operations() {
+ $operations = array(
+ 'approve' => array(t('Approve the selected posts'), 'UPDATE {node} SET status = 1, moderate = 0 WHERE nid = %d'),
+ 'promote' => array(t('Promote the selected posts'), 'UPDATE {node} SET status = 1, promote = 1 WHERE nid = %d'),
+ 'sticky' => array(t('Make the selected posts sticky'), 'UPDATE {node} SET status = 1, sticky = 1 WHERE nid = %d'),
+ 'demote' => array(t('Demote the selected posts'), 'UPDATE {node} SET promote = 0 WHERE nid = %d'),
+ 'unpublish' => array(t('Unpublish the selected posts'), 'UPDATE {node} SET status = 0 WHERE nid = %d'),
+ 'delete' => array(t('Delete the selected posts'), '')
+ );
+ return $operations;
+}
+
+/**
+ * List node administration filters that can be applied.
+ */
+function node_filters() {
+ // Regular filters
+ $filters['status'] = array('title' => t('status'),
+ 'options' => array('status-1' => t('published'), 'status-0' => t('not published'),
+ 'moderate-1' => t('in moderation'), 'moderate-0' => t('not in moderation'),
+ 'promote-1' => t('promoted'), 'promote-0' => t('not promoted'),
+ 'sticky-1' => t('sticky'), 'sticky-0' => t('not sticky')));
+ $filters['type'] = array('title' => t('type'), 'options' => node_get_types());
+ // The taxonomy filter
+ if ($taxonomy = module_invoke('taxonomy', 'form_all', 1)) {
+ $filters['category'] = array('title' => t('category'), 'options' => $taxonomy);
+ }
+
+ return $filters;
+}
+
+/**
+ * Build query for node administration filters based on session.
+ */
+function node_build_filter_query() {
+ $filters = node_filters();
+
+ // Build query
+ $where = $args = array();
+ $join = '';
+ foreach ($_SESSION['node_overview_filter'] as $index => $filter) {
+ list($key, $value) = $filter;
+ switch($key) {
+ case 'status':
+ // Note: no exploitable hole as $key/$value have already been checked when submitted
+ list($key, $value) = explode('-', $value, 2);
+ $where[] = 'n.'. $key .' = %d';
+ break;
+ case 'category':
+ $table = "tn$index";
+ $where[] = "$table.tid = %d";
+ $join .= "INNER JOIN {term_node} $table ON n.nid = $table.nid ";
+ break;
+ case 'type':
+ $where[] = "n.type = '%s'";
+ }
+ $args[] = $value;
+ }
+ $where = count($where) ? 'WHERE '. implode(' AND ', $where) : '';
+
+ return array('where' => $where, 'join' => $join, 'args' => $args);
+}
+
+/**
+ * Return form for node administration filters.
+ */
+function node_filter_form() {
+ $session = &$_SESSION['node_overview_filter'];
+ $session = is_array($session) ? $session : array();
+ $filters = node_filters();
+
+ $i = 0;
+ $form['filters'] = array('#type' => 'fieldset',
+ '#title' => t('Show only items where'),
+ '#theme' => 'node_filters',
+ );
+ foreach ($session as $filter) {
+ list($type, $value) = $filter;
+ if ($type == 'category') {
+ // Load term name from DB rather than search and parse options array.
+ $value = module_invoke('taxonomy', 'get_term', $value);
+ $value = $value->name;
+ }
+ else {
+ $value = $filters[$type]['options'][$value];
+ }
+ $string = ($i++ ? 'and where %a is %b' : '%a is %b');
+ $form['filters']['current'][] = array('#value' => t($string, array('%a' => $filters[$type]['title'] , '%b' => $value)));
+ }
+
+ foreach ($filters as $key => $filter) {
+ $names[$key] = $filter['title'];
+ $form['filters']['status'][$key] = array('#type' => 'select', '#options' => $filter['options']);
+ }
+
+ $form['filters']['filter'] = array('#type' => 'radios', '#options' => $names, '#default_value' => 'status');
+ $form['filters']['buttons']['submit'] = array('#type' => 'submit', '#value' => (count($session) ? t('Refine') : t('Filter')));
+ if (count($session)) {
+ $form['filters']['buttons']['undo'] = array('#type' => 'submit', '#value' => t('Undo'));
+ $form['filters']['buttons']['reset'] = array('#type' => 'submit', '#value' => t('Reset'));
+ }
+
+ return drupal_get_form('node_filter_form', $form);
+}
+
+/**
+ * Theme node administration filter form.
+ */
+function theme_node_filter_form(&$form) {
+ $output .= '';
+ $output .= form_render($form['filters']);
+ $output .= '';
+ $output .= form_render($form);
+ return $output;
+}
+
+/**
+ * Theme node administraton filter selector.
+ */
+function theme_node_filters(&$form) {
+ $output .= '';
+ if (sizeof($form['current'])) {
+ foreach (element_children($form['current']) as $key) {
+ $output .= '- ' . form_render($form['current'][$key]) . '
';
+ }
+ }
+
+ $output .= '' . (sizeof($form['current']) ? '- '. t('and') .' '. t('where') .'
' : '') . '- ';
+ foreach (element_children($form['filter']) as $key) {
+ $output .= form_render($form['filter'][$key]);
+ }
+ $output .= '
';
+
+ $output .= '- '. t('is') .'
' . '- ';
+
+ foreach (element_children($form['status']) as $key) {
+ $output .= form_render($form['status'][$key]);
+ }
+ $output .= '
';
+
+ $output .= '
';
+ $output .= ''. form_render($form['buttons']) .'';
+ $output .= '
';
+
+ return $output;
+}
+
+/**
+ * Process result from node administration filter form.
+ */
+function node_filter_form_submit() {
+ global $form_values;
+ $op = $_POST['op'];
+ $filters = node_filters();
+ switch ($op) {
+ case t('Filter'):
+ case t('Refine'):
+ if (isset($form_values['filter'])) {
+ $filter = $form_values['filter'];
+
+ // Flatten the options array to accommodate hierarchical/nested options.
+ $flat_options = form_options_flatten($filters[$filter]['options']);
+
+ if (isset($flat_options[$form_values[$filter]])) {
+ $_SESSION['node_overview_filter'][] = array($filter, $form_values[$filter]);
+ }
+ }
+ break;
+ case t('Undo'):
+ array_pop($_SESSION['node_overview_filter']);
+ break;
+ case t('Reset'):
+ $_SESSION['node_overview_filter'] = array();
+ break;
+ }
+}
+
+/**
+ * Generate the content administration overview.
+ */
+function node_admin_nodes_submit($form_id, $edit) {
+ $operations = node_operations();
+ if ($operations[$edit['operation']][1]) {
+ // Flag changes
+ $operation = $operations[$edit['operation']][1];
+ foreach ($edit['nodes'] as $nid => $value) {
+ if ($value) {
+ db_query($operation, $nid);
+ }
+ }
+ cache_clear_all();
+ drupal_set_message(t('The update has been performed.'));
+ }
+}
+
+function node_admin_nodes_validate($form_id, $edit) {
+ $edit['nodes'] = array_diff($edit['nodes'], array(0));
+ if (count($edit['nodes']) == 0) {
+ if ($edit['operation'] == 'delete') {
+ form_set_error('', t('Please select some items to perform the delete operation.'));
+ }
+ else {
+ form_set_error('', t('Please select some items to perform the update on.'));
+ }
+ }
+}
+
+/**
+ * Menu callback: content administration.
+ */
+function node_admin_nodes() {
+ global $form_values;
+ $output = node_filter_form();
+
+ if ($_POST['edit']['operation'] == 'delete' && $_POST['edit']['nodes']) {
+ return node_multiple_delete_confirm();
+ }
+
+ $filter = node_build_filter_query();
+
+ $result = pager_query('SELECT n.*, u.name, u.uid FROM {node} n '. $filter['join'] .' INNER JOIN {users} u ON n.uid = u.uid '. $filter['where'] .' ORDER BY n.changed DESC', 50, 0, NULL, $filter['args']);
+
+ $form['options'] = array('#type' => 'fieldset',
+ '#title' => t('Update options'),
+ '#prefix' => '',
+ '#suffix' => '',
+ );
+ $options = array();
+ foreach (node_operations() as $key => $value) {
+ $options[$key] = $value[0];
+ }
+ $form['options']['operation'] = array('#type' => 'select', '#options' => $options, '#default_value' => 'approve');
+ $form['options']['submit'] = array('#type' => 'submit', '#value' => t('Update'));
+
+ $destination = drupal_get_destination();
+ while ($node = db_fetch_object($result)) {
+ $nodes[$node->nid] = '';
+ $form['title'][$node->nid] = array('#value' => l($node->title, 'node/'. $node->nid) .' '. theme('mark', node_mark($node->nid, $node->changed)));
+ $form['name'][$node->nid] = array('#value' => node_get_name($node));
+ $form['username'][$node->nid] = array('#value' => theme('username', $node));
+ $form['status'][$node->nid] = array('#value' => ($node->status ? t('published') : t('not published')));
+ $form['operations'][$node->nid] = array('#value' => l(t('edit'), 'node/'. $node->nid .'/edit', array(), $destination));
+ }
+ $form['nodes'] = array('#type' => 'checkboxes', '#options' => $nodes);
+ $form['pager'] = array('#value' => theme('pager', NULL, 50, 0));
+
+ // Call the form first, to allow for the form_values array to be populated.
+ $output .= drupal_get_form('node_admin_nodes', $form);
+
+ return $output;
+}
+
+/**
+ * Theme node administration overview.
+ */
+function theme_node_admin_nodes($form) {
+ // Overview table:
+ $header = array(NULL, t('Title'), t('Type'), t('Author'), t('Status'), t('Operations'));
+
+ $output .= form_render($form['options']);
+ if (isset($form['title']) && is_array($form['title'])) {
+ foreach (element_children($form['title']) as $key) {
+ $row = array();
+ $row[] = form_render($form['nodes'][$key]);
+ $row[] = form_render($form['title'][$key]);
+ $row[] = form_render($form['name'][$key]);
+ $row[] = form_render($form['username'][$key]);
+ $row[] = form_render($form['status'][$key]);
+ $row[] = form_render($form['operations'][$key]);
+ $rows[] = $row;
+ }
+
+ }
+ else {
+ $rows[] = array(array('data' => t('No posts available.'), 'colspan' => '6'));
+ }
+
+ $output .= theme('table', $header, $rows);
+ if ($form['pager']['#value']) {
+ $output .= form_render($form['pager']);
+ }
+
+ $output .= form_render($form);
+
+ return $output;
+}
+
+function node_multiple_delete_confirm() {
+ $edit = $_POST['edit'];
+
+ $form['nodes'] = array('#prefix' => '', '#suffix' => '
', '#tree' => TRUE);
+ // array_filter returns only elements with true values
+ foreach (array_filter($edit['nodes']) as $nid => $value) {
+ $title = db_result(db_query('SELECT title FROM {node} WHERE nid = %d', $nid));
+ $form['nodes'][$nid] = array('#type' => 'hidden', '#value' => $nid, '#prefix' => '- ', '#suffix' => check_plain($title) ."
\n");
+ }
+ $form['operation'] = array('#type' => 'hidden', '#value' => 'delete');
+
+ return confirm_form('node_multiple_delete_confirm', $form,
+ t('Are you sure you want to delete these items?'),
+ 'admin/node', t('This action cannot be undone.'),
+ t('Delete all'), t('Cancel'));
+}
+
+function node_multiple_delete_confirm_submit($form_id, $edit) {
+ if ($edit['confirm']) {
+ foreach ($edit['nodes'] as $nid => $value) {
+ node_delete($nid);
+ }
+ drupal_set_message(t('The items have been deleted.'));
+ }
+ return 'admin/node';
+}
+
+/**
+ * Menu callback; presents each node type configuration page.
+ */
+function node_types_configure($type = NULL) {
+ if (isset($type)) {
+ $node = new stdClass();
+ $node->type = $type;
+ $form['submission'] = array('#type' => 'fieldset', '#title' =>t('Submission form') );
+ $form['submission'][$type . '_help'] = array(
+ '#type' => 'textarea', '#title' => t('Explanation or submission guidelines'), '#default_value' => variable_get($type .'_help', ''),
+ '#description' => t('This text will be displayed at the top of the %type submission form. It is useful for helping or instructing your users.', array('%type' => node_get_name($type)))
+ );
+ $form['submission']['minimum_'. $type .'_size'] = array(
+ '#type' => 'select', '#title' => t('Minimum number of words'), '#default_value' => variable_get('minimum_'. $type .'_size', 0), '#options' => drupal_map_assoc(array(0, 10, 25, 50, 75, 100, 125, 150, 175, 200)),
+ '#description' => t('The minimum number of words a %type must be to be considered valid. This can be useful to rule out submissions that do not meet the site\'s standards, such as short test posts.', array('%type' => node_get_name($type)))
+ );
+ $form['workflow'] = array('#type' => 'fieldset', '#title' =>t('Workflow'));
+ $form['type'] = array('#type' => 'value', '#value' => $type);
+
+ $form['array_filter'] = array('#type' => 'value', '#value' => TRUE);
+ return system_settings_form($type .'_node_settings', $form);
+ }
+ else {
+ $header = array(t('Type'), t('Operations'));
+
+ $rows = array();
+ foreach (node_get_types() as $type => $name) {
+ $rows[] = array($name, l(t('configure'), 'admin/settings/content-types/'. $type));
+ }
+
+ return theme('table', $header, $rows);
+ }
+}
+
+/**
+ * Generate an overview table of older revisions of a node.
+ */
+function node_revision_overview($node) {
+ drupal_set_title(t('Revisions for %title', array('%title' => check_plain($node->title))));
+
+ $header = array(t('Revision'), array('data' => t('Operations'), 'colspan' => 2));
+
+ $revisions = node_revision_list($node);
+
+ $rows = array();
+ $revert_permission = FALSE;
+ if ((user_access('revert revisions') || user_access('administer nodes')) && node_access('update', $node)) {
+ $revert_permission = TRUE;
+ }
+ $delete_permission = FALSE;
+ if (user_access('administer nodes')) {
+ $delete_permission = TRUE;
+ }
+ foreach ($revisions as $revision) {
+ $row = array();
+ $operations = array();
+
+ if ($revision->current_vid > 0) {
+ $row[] = array('data' => t('%date by %username', array('%date' => l(format_date($revision->timestamp, 'small'), "node/$node->nid"), '%username' => theme('username', $revision)))
+ . (($revision->log != '') ? ''. filter_xss($revision->log) .'
' : ''),
+ 'class' => 'revision-current');
+ $operations[] = array('data' => theme('placeholder', t('current revision')), 'class' => 'revision-current', 'colspan' => 2);
+ }
+ else {
+ $row[] = t('%date by %username', array('%date' => l(format_date($revision->timestamp, 'small'), "node/$node->nid/revisions/$revision->vid/view"), '%username' => theme('username', $revision)))
+ . (($revision->log != '') ? ''. filter_xss($revision->log) .'
' : '');
+ if ($revert_permission) {
+ $operations[] = l(t('revert'), "node/$node->nid/revisions/$revision->vid/revert");
+ }
+ if ($delete_permission) {
+ $operations[] = l(t('delete'), "node/$node->nid/revisions/$revision->vid/delete");
+ }
+ }
+ $rows[] = array_merge($row, $operations);
+ }
+ $output .= theme('table', $header, $rows);
+
+ return $output;
+}
+
+/**
+ * Revert to the revision with the specified revision number. A node and nodeapi "update" event is triggered
+ * (via the node_save() call) when a revision is reverted.
+ */
+function node_revision_revert($nid, $revision) {
+ global $user;
+
+ $node = node_load($nid, $revision);
+ if ((user_access('revert revisions') || user_access('administer nodes')) && node_access('update', $node)) {
+ if ($node->vid) {
+ $node->revision = 1;
+ $node->log = t('Copy of the revision from %date.', array('%date' => theme('placeholder', format_date($node->revision_timestamp))));
+ $node->taxonomy = array_keys($node->taxonomy);
+
+ node_save($node);
+
+ drupal_set_message(t('%title has been reverted back to the revision from %revision-date', array('%revision-date' => theme('placeholder', format_date($node->revision_timestamp)), '%title' => theme('placeholder', check_plain($node->title)))));
+ watchdog('content', t('%type: reverted %title revision %revision.', array('%type' => theme('placeholder', t($node->type)), '%title' => theme('placeholder', $node->title), '%revision' => theme('placeholder', $revision))));
+ }
+ else {
+ drupal_set_message(t('You tried to revert to an invalid revision.'), 'error');
+ }
+ drupal_goto('node/'. $nid .'/revisions');
+ }
+ drupal_access_denied();
+}
+
+/**
+ * Delete the revision with specified revision number. A "delete revision" nodeapi event is invoked when a
+ * revision is deleted.
+ */
+function node_revision_delete($nid, $revision) {
+ if (user_access('administer nodes')) {
+ $node = node_load($nid);
+ if (node_access('delete', $node)) {
+ // Don't delete the current revision
+ if ($revision != $node->vid) {
+ $node = node_load($nid, $revision);
+
+ db_query("DELETE FROM {node_revisions} WHERE nid = %d AND vid = %d", $nid, $revision);
+ node_invoke_nodeapi($node, 'delete revision');
+ drupal_set_message(t('Deleted %title revision %revision.', array('%title' => theme('placeholder', $node->title), '%revision' => theme('placeholder', $revision))));
+ watchdog('content', t('%type: deleted %title revision %revision.', array('%type' => theme('placeholder', t($node->type)), '%title' => theme('placeholder', $node->title), '%revision' => theme('placeholder', $revision))));
+ }
+
+ else {
+ drupal_set_message(t('Deletion failed. You tried to delete the current revision.'));
+ }
+ if (db_result(db_query('SELECT COUNT(vid) FROM {node_revisions} WHERE nid = %d', $nid)) > 1) {
+ drupal_goto("node/$nid/revisions");
+ }
+ else {
+ drupal_goto("node/$nid");
+ }
+ }
+ }
+
+ drupal_access_denied();
+}
+
+/**
+ * Return a list of all the existing revision numbers.
+ */
+function node_revision_list($node) {
+ $revisions = array();
+ $result = db_query('SELECT r.vid, r.title, r.log, r.uid, n.vid AS current_vid, r.timestamp, u.name FROM {node_revisions} r LEFT JOIN {node} n ON n.vid = r.vid INNER JOIN {users} u ON u.uid = r.uid WHERE r.nid = %d ORDER BY r.timestamp DESC', $node->nid);
+ while ($revision = db_fetch_object($result)) {
+ $revisions[] = $revision;
+ }
+
+ return $revisions;
+}
+
+function node_admin_search() {
+ $output = search_form(url('admin/node/search'), $_POST['edit']['keys'], 'node') . search_data($_POST['edit']['keys'], 'node');
+ return $output;
+}
+
+/**
+ * Implementation of hook_block().
+ */
+function node_block($op = 'list', $delta = 0) {
+ if ($op == 'list') {
+ $blocks[0]['info'] = t('Syndicate');
+ return $blocks;
+ }
+ else if ($op == 'view') {
+ $block['subject'] = t('Syndicate');
+ $block['content'] = theme('feed_icon', url('rss.xml'));
+
+ return $block;
+ }
+}
+
+/**
+ * A generic function for generating RSS feeds from a set of nodes.
+ *
+ * @param $nodes
+ * An object as returned by db_query() which contains the nid field.
+ * @param $channel
+ * An associative array containing title, link, description and other keys.
+ * The link should be an absolute URL.
+ */
+function node_feed($nodes = 0, $channel = array()) {
+ global $base_url, $locale;
+
+ if (!$nodes) {
+ $nodes = db_query_range(db_rewrite_sql('SELECT n.nid, n.created FROM {node} n WHERE n.promote = 1 AND n.status = 1 ORDER BY n.created DESC'), 0, variable_get('feed_default_items', 10));
+ }
+
+ $item_length = variable_get('feed_item_length', 'teaser');
+ $namespaces = array('xmlns:dc="http://purl.org/dc/elements/1.1/"');
+
+ while ($node = db_fetch_object($nodes)) {
+ // Load the specified node:
+ $item = node_load($node->nid);
+ $link = url("node/$node->nid", NULL, NULL, 1);
+
+ if ($item_length != 'title') {
+ $teaser = ($item_length == 'teaser') ? TRUE : FALSE;
+
+ // Filter and prepare node teaser
+ if (node_hook($item, 'view')) {
+ node_invoke($item, 'view', $teaser, FALSE);
+ }
+ else {
+ $item = node_prepare($item, $teaser);
+ }
+
+ // Allow modules to change $node->teaser before viewing.
+ node_invoke_nodeapi($item, 'view', $teaser, FALSE);
+ }
+
+ // Prepare the item description
+ switch ($item_length) {
+ case 'fulltext':
+ $item_text = $item->body;
+ break;
+ case 'teaser':
+ $item_text = $item->teaser;
+ if ($item->readmore) {
+ $item_text .= ''. l(t('read more'), 'node/'. $item->nid, NULL, NULL, NULL, TRUE) .'
';
+ }
+ break;
+ case 'title':
+ $item_text = '';
+ break;
+ }
+
+ // Allow modules to add additional item fields
+ $extra = node_invoke_nodeapi($item, 'rss item');
+ $extra = array_merge($extra, array(array('key' => 'pubDate', 'value' => date('r', $item->created)), array('key' => 'dc:creator', 'value' => $item->name), array('key' => 'guid', 'value' => $item->nid . ' at ' . $base_url, 'attributes' => array('isPermaLink' => 'false'))));
+ foreach ($extra as $element) {
+ if ($element['namespace']) {
+ $namespaces = array_merge($namespaces, $element['namespace']);
+ }
+ }
+ $items .= format_rss_item($item->title, $link, $item_text, $extra);
+ }
+
+ $channel_defaults = array(
+ 'version' => '2.0',
+ 'title' => variable_get('site_name', 'drupal') .' - '. variable_get('site_slogan', ''),
+ 'link' => $base_url,
+ 'description' => variable_get('site_mission', ''),
+ 'language' => $locale
+ );
+ $channel = array_merge($channel_defaults, $channel);
+
+ $output = "\n";
+ $output .= "\n";
+ $output .= format_rss_channel($channel['title'], $channel['link'], $channel['description'], $items, $channel['language']);
+ $output .= " \n";
+
+ drupal_set_header('Content-Type: application/rss+xml; charset=utf-8');
+ print $output;
+}
+
+/**
+ * Prepare node for save and allow modules to make changes.
+ */
+function node_submit($node) {
+ global $user;
+
+ // Convert the node to an object, if necessary.
+ $node = (object)$node;
+
+ // Auto-generate the teaser, but only if it hasn't been set (e.g. by a
+ // module-provided 'teaser' form item).
+ if (!isset($node->teaser)) {
+ $node->teaser = isset($node->body) ? node_teaser($node->body, isset($node->format) ? $node->format : NULL) : '';
+ }
+
+ $access = user_access('administer nodes');
+ if ($access) {
+ // Populate the "authored by" field.
+ if ($account = user_load(array('name' => $node->name))) {
+ $node->uid = $account->uid;
+ }
+ else {
+ $node->uid = 0;
+ }
+
+ $node->created = $node->date ? strtotime($node->date) : NULL;
+ }
+ // Force defaults in case people modify the form:
+ $node_options = variable_get('node_options_'. $node->type, array('status', 'promote'));
+ foreach (array('status', 'moderate', 'promote', 'sticky', 'revision') as $key) {
+ if (!$access || !isset($node->$key)) {
+ $node->$key = in_array($key, $node_options);
+ }
+ }
+
+ // Do node-type-specific validation checks.
+ node_invoke($node, 'submit');
+ node_invoke_nodeapi($node, 'submit');
+
+ $node->validated = TRUE;
+
+ return $node;
+}
+
+/**
+ * Perform validation checks on the given node.
+ */
+function node_validate($node, $form = array()) {
+ // Convert the node to an object, if necessary.
+ $node = (object)$node;
+
+ // Make sure the body has the minimum number of words.
+ // todo use a better word counting algorithm that will work in other languages
+ if (isset($node->body) && count(explode(' ', $node->body)) < variable_get('minimum_'. $node->type .'_size', 0)) {
+ form_set_error('body', t('The body of your %type is too short. You need at least %words words.', array('%words' => variable_get('minimum_'. $node->type .'_size', 0), '%type' => node_get_name($node))));
+ }
+
+ if (isset($node->nid) && (node_last_changed($node->nid) > $_POST['edit']['changed'])) {
+ form_set_error('changed', t('This content has been modified by another user, changes cannot be saved.'));
+ }
+
+ if (user_access('administer nodes')) {
+ // Validate the "authored by" field.
+ if (!empty($node->name) && !($account = user_load(array('name' => $node->name)))) {
+ // The use of empty() is mandatory in the context of usernames
+ // as the empty string denotes the anonymous user. In case we
+ // are dealing with an anonymous user we set the user ID to 0.
+ form_set_error('name', t('The username %name does not exist.', array ('%name' => theme('placeholder', $node->name))));
+ }
+
+ // Validate the "authored on" field. As of PHP 5.1.0, strtotime returns FALSE instead of -1 upon failure.
+ if (!empty($node->date) && strtotime($node->date) <= 0) {
+ form_set_error('date', t('You have to specify a valid date.'));
+ }
+ }
+
+ // Do node-type-specific validation checks.
+ node_invoke($node, 'validate', $form);
+ node_invoke_nodeapi($node, 'validate', $form);
+}
+
+function node_form_validate($form_id, $form_values, $form) {
+ node_validate($form_values, $form);
+}
+
+function node_object_prepare(&$node) {
+ if (user_access('administer nodes')) {
+ // Set up default values, if required.
+ if (!isset($node->created)) {
+ $node->created = time();
+ }
+
+ if (!isset($node->date)) {
+ $node->date = format_date($node->created, 'custom', 'Y-m-d H:i:s O');
+ }
+ }
+ node_invoke($node, 'prepare');
+ node_invoke_nodeapi($node, 'prepare');
+}
+
+/**
+ * Generate the node editing form.
+ */
+function node_form($node) {
+ $node = (object)$node;
+ $form = node_form_array($node);
+ return drupal_get_form($node->type .'_node_form', $form, 'node_form');
+}
+
+/**
+* Generate the node editing form array.
+*/
+function node_form_array($node) {
+ node_object_prepare($node);
+
+ // Set the id of the top-level form tag
+ $form['#id'] = 'node-form';
+
+ /**
+ * Basic node information.
+ * These elements are just values so they are not even sent to the client.
+ */
+ foreach (array('nid', 'vid', 'uid', 'created', 'type') as $key) {
+ $form[$key] = array('#type' => 'value', '#value' => $node->$key);
+ }
+
+ // Changed must be sent to the client, for later overwrite error checking.
+ $form['changed'] = array('#type' => 'hidden', '#value' => $node->changed);
+
+ // Get the node-specific bits.
+ $form = array_merge_recursive($form, node_invoke($node, 'form'));
+ if (!isset($form['title']['#weight'])) {
+ $form['title']['#weight'] = -5;
+ }
+
+ $node_options = variable_get('node_options_'. $node->type, array('status', 'promote'));
+ // If this is a new node, fill in the default values.
+ if (!isset($node->nid)) {
+ foreach (array('status', 'moderate', 'promote', 'sticky', 'revision') as $key) {
+ $node->$key = in_array($key, $node_options);
+ }
+ global $user;
+ $node->uid = $user->uid;
+ }
+ else {
+ // Nodes being edited should always be preset with the default revision setting.
+ $node->revision = in_array('revision', $node_options);
+ }
+ $form['#node'] = $node;
+
+ if (user_access('administer nodes')) {
+ // Node author information
+ $form['author'] = array('#type' => 'fieldset', '#title' => t('Authoring information'), '#collapsible' => TRUE, '#collapsed' => TRUE, '#weight' => 20);
+ $form['author']['name'] = array('#type' => 'textfield', '#title' => t('Authored by'), '#maxlength' => 60, '#autocomplete_path' => 'user/autocomplete', '#default_value' => $node->name ? $node->name : '', '#weight' => -1, '#description' => t('Leave blank for %anonymous.', array('%anonymous' => theme('placeholder', variable_get('anonymous', 'Anonymous')))));
+ $form['author']['date'] = array('#type' => 'textfield', '#title' => t('Authored on'), '#maxlength' => 25, '#description' => t('Format: %time. Leave blank to use the time of form submission.', array('%time' => $node->date)));
+
+ if (isset($node->nid)) {
+ $form['author']['date']['#default_value'] = $node->date;
+ }
+
+ // Node options for administrators
+ $form['options'] = array('#type' => 'fieldset', '#title' => t('Publishing options'), '#collapsible' => TRUE, '#collapsed' => TRUE, '#weight' => 25);
+ $form['options']['status'] = array('#type' => 'checkbox', '#title' => t('Published'), '#default_value' => $node->status);
+ $form['options']['moderate'] = array('#type' => 'checkbox', '#title' => t('In moderation queue'), '#default_value' => $node->moderate);
+ $form['options']['promote'] = array('#type' => 'checkbox', '#title' => t('Promoted to front page'), '#default_value' => $node->promote);
+ $form['options']['sticky'] = array('#type' => 'checkbox', '#title' => t('Sticky at top of lists'), '#default_value' => $node->sticky);
+ $form['options']['revision'] = array('#type' => 'checkbox', '#title' => t('Create new revision'), '#default_value' => $node->revision);
+ }
+ else {
+ // Put all of these through as values if the user doesn't have access to them.
+ foreach (array('uid', 'created') as $key) {
+ $form[$key] = array('#type' => 'value', '#value' => $node->$key);
+ }
+ }
+
+ // Add the buttons.
+ $form['preview'] = array('#type' => 'button', '#value' => t('Preview'), '#weight' => 40);
+ $form['submit'] = array('#type' => 'submit', '#value' => t('Submit'), '#weight' => 45);
+ if ($node->nid && node_access('delete', $node)) {
+ $form['delete'] = array('#type' => 'button', '#value' => t('Delete'), '#weight' => 50);
+ }
+
+ $form['#after_build'] = array('node_form_add_preview');
+
+ return $form;
+}
+
+function node_form_add_preview($form) {
+ global $form_values;
+
+ $op = isset($_POST['op']) ? $_POST['op'] : '';
+ if ($op == t('Preview')) {
+ drupal_validate_form($form['form_id']['#value'], $form);
+ if (!form_get_errors()) {
+ // We pass the global $form_values here to preserve changes made during form validation
+ $form['node_preview'] = array('#value' => node_preview((object)$form_values), '#weight' => -100);
+ }
+ }
+ if (variable_get('node_preview', 0) && (form_get_errors() || $op != t('Preview'))) {
+ unset($form['submit']);
+ }
+ return $form;
+}
+
+function theme_node_form($form) {
+ $output = "\n\n";
+ if (isset($form['node_preview'])) {
+ $output .= form_render($form['node_preview']);
+ }
+
+ // Admin form fields and submit buttons must be rendered first, because
+ // they need to go to the bottom of the form, and so should not be part of
+ // the catch-all call to form_render().
+ $admin = '';
+ if (isset($form['author'])) {
+ $admin .= " \n";
+ $output .= form_render($form);
+ $output .= " \n";
+
+ if (!empty($admin)) {
+ $output .= " \n";
+ $output .= $admin;
+ $output .= " \n";
+ }
+ $output .= $buttons;
+ $output .= "\n";
+
+ return $output;
+}
+
+/**
+ * Present a node submission form or a set of links to such forms.
+ */
+function node_add($type) {
+ global $user;
+
+ // If a node type has been specified, validate its existence.
+ if (array_key_exists($type, node_get_types()) && node_access('create', $type)) {
+ // Initialize settings:
+ $node = array('uid' => $user->uid, 'name' => $user->name, 'type' => $type);
+
+ $output = node_form($node);
+ drupal_set_title(t('Submit %name', array('%name' => node_get_name($node))));
+ }
+ else {
+ // If no (valid) node type has been provided, display a node type overview.
+ foreach (node_get_types() as $type => $name) {
+ if (node_access('create', $type)) {
+ $out = ' \n";
+ }
+ if (isset($form['options'])) {
+ $admin .= " \n";
+ }
+ $buttons = form_render($form['preview']);
+ $buttons .= form_render($form['submit']);
+ $buttons .= isset($form['delete']) ? form_render($form['delete']) : '';
+
+ // Everything else gets rendered here, and is displayed before the admin form
+ // field and the submit buttons.
+ $output .= " - '. l($name, "node/add/$type", array('title' => t('Add a new %s.', array('%s' => $name)))) .'
';
+ $out .= '- '. implode("\n", module_invoke_all('help', 'node/add#'. $type)) .'
';
+ $item[$name] = $out;
+ }
+ }
+
+ if (isset($item)) {
+ uksort($item, 'strnatcasecmp');
+ $output = t('Choose the appropriate item from the list:') .''. implode('', $item) .'
';
+ }
+ else {
+ $output = t('You are not allowed to create content.');
+ }
+ }
+
+ return $output;
+}
+
+/**
+ * Generate a node preview.
+ */
+function node_preview($node) {
+ if (node_access('create', $node) || node_access('update', $node)) {
+ // Load the user's name when needed:
+ if (isset($node->name)) {
+ // The use of isset() is mandatory in the context of user IDs, because
+ // user ID 0 denotes the anonymous user.
+ if ($user = user_load(array('name' => $node->name))) {
+ $node->uid = $user->uid;
+ }
+ else {
+ $node->uid = 0; // anonymous user
+ }
+ }
+ else if ($node->uid) {
+ $user = user_load(array('uid' => $node->uid));
+ $node->name = $user->name;
+ }
+
+ // Set the timestamps when needed:
+ if ($node->date) {
+ $node->created = strtotime($node->date);
+ }
+ $node->changed = time();
+
+ // Extract a teaser, if it hasn't been set (e.g. by a module-provided
+ // 'teaser' form item).
+ if (!isset($node->teaser)) {
+ $node->teaser = node_teaser($node->body, $node->format);
+ }
+
+ // Display a preview of the node:
+ // Previewing alters $node so it needs to be cloned.
+ if (!form_get_errors()) {
+ $cloned_node = drupal_clone($node);
+ $cloned_node->in_preview = TRUE;
+ $output = theme('node_preview', $cloned_node);
+ }
+ drupal_set_title(t('Preview'));
+ drupal_set_breadcrumb(array(l(t('Home'), NULL), l(t('create content'), 'node/add'), l(t('Submit %name', array('%name' => node_get_name($node))), 'node/add/'. $node->type)));
+
+ return $output;
+ }
+}
+
+/**
+ * Display a node preview for display during node creation and editing.
+ *
+ * @param $node
+ * The node object which is being previewed.
+ */
+function theme_node_preview($node) {
+ $output = '';
+ if ($node->teaser && $node->teaser != $node->body) {
+ drupal_set_message(t('The trimmed version of your post shows what your post looks like when promoted to the main page or when exported for syndication. You can insert the delimiter "<!--break-->" (without the quotes) to fine-tune where your post gets split.'));
+ $output .= ''. t('Preview trimmed version') .'
';
+ $output .= node_view(drupal_clone($node), 1, FALSE, 0);
+ $output .= ''. t('Preview full version') .'
';
+ $output .= node_view($node, 0, FALSE, 0);
+ }
+ else {
+ $output .= node_view($node, 0, FALSE, 0);
+ }
+ $output .= "\n";
+
+ return $output;
+}
+
+function node_form_submit($form_id, $edit) {
+ global $user;
+
+ // Fix up the node when required:
+ $node = node_submit($edit);
+
+ // Prepare the node's body:
+ if ($node->nid) {
+ // Check whether the current user has the proper access rights to
+ // perform this operation:
+ if (node_access('update', $node)) {
+ node_save($node);
+ watchdog('content', t('%type: updated %title.', array('%type' => theme('placeholder', t($node->type)), '%title' => theme('placeholder', $node->title))), WATCHDOG_NOTICE, l(t('view'), 'node/'. $node->nid));
+ drupal_set_message(t('The %post was updated.', array ('%post' => node_get_name($node))));
+ }
+ }
+ else {
+ // Check whether the current user has the proper access rights to
+ // perform this operation:
+ if (node_access('create', $node)) {
+ node_save($node);
+ watchdog('content', t('%type: added %title.', array('%type' => theme('placeholder', t($node->type)), '%title' => theme('placeholder', $node->title))), WATCHDOG_NOTICE, l(t('view'), "node/$node->nid"));
+ drupal_set_message(t('Your %post was created.', array ('%post' => node_get_name($node))));
+ }
+ }
+ if ($node->nid) {
+ if (node_access('view', $node)) {
+ return 'node/'. $node->nid;
+ }
+ else {
+ return '';
+ }
+ }
+ // it is very unlikely we get here
+ return FALSE;
+}
+
+/**
+ * Menu callback -- ask for confirmation of node deletion
+ */
+function node_delete_confirm() {
+ $edit = $_POST['edit'];
+ $edit['nid'] = $edit['nid'] ? $edit['nid'] : arg(1);
+ $node = node_load($edit['nid']);
+
+ if (node_access('delete', $node)) {
+ $form['nid'] = array('#type' => 'value', '#value' => $node->nid);
+ $output = confirm_form('node_delete_confirm', $form,
+ t('Are you sure you want to delete %title?', array('%title' => theme('placeholder', $node->title))),
+ $_GET['destination'] ? $_GET['destination'] : 'node/'. $node->nid, t('This action cannot be undone.'),
+ t('Delete'), t('Cancel') );
+ }
+
+ return $output;
+}
+
+/**
+ * Execute node deletion
+ */
+function node_delete_confirm_submit($form_id, $form_values) {
+ if ($form_values['confirm']) {
+ node_delete($form_values['nid']);
+ }
+
+ return '';
+}
+
+/**
+ * Delete a node.
+ */
+function node_delete($nid) {
+
+ $node = node_load($nid);
+
+ if (node_access('delete', $node)) {
+ db_query('DELETE FROM {node} WHERE nid = %d', $node->nid);
+ db_query('DELETE FROM {node_revisions} WHERE nid = %d', $node->nid);
+
+ // Call the node-specific callback (if any):
+ node_invoke($node, 'delete');
+ node_invoke_nodeapi($node, 'delete');
+
+ // Clear the cache so an anonymous poster can see the node being deleted.
+ cache_clear_all();
+
+ // Remove this node from the search index if needed.
+ if (function_exists('search_wipe')) {
+ search_wipe($node->nid, 'node');
+ }
+ drupal_set_message(t('%title has been deleted.', array('%title' => theme('placeholder', $node->title))));
+ watchdog('content', t('%type: deleted %title.', array('%type' => theme('placeholder', t($node->type)), '%title' => theme('placeholder', $node->title))));
+ }
+}
+
+/**
+ * Menu callback for revisions related activities.
+ */
+function node_revisions() {
+ if (is_numeric(arg(1)) && arg(2) == 'revisions') {
+ $op = arg(4) ? arg(4) : 'overview';
+ switch ($op) {
+ case 'overview':
+ $node = node_load(arg(1));
+ if ((user_access('view revisions') || user_access('administer nodes')) && node_access('view', $node)) {
+ return node_revision_overview($node);
+ }
+ drupal_access_denied();
+ return;
+ case 'view':
+ if (is_numeric(arg(3))) {
+ $node = node_load(arg(1), arg(3));
+ if ($node->nid) {
+ if ((user_access('view revisions') || user_access('administer nodes')) && node_access('view', $node)) {
+ drupal_set_title(t('Revision of %title from %date', array('%title' => theme('placeholder', $node->title), '%date' => format_date($node->revision_timestamp))));
+ return node_show($node, arg(2));
+ }
+ drupal_access_denied();
+ return;
+ }
+ }
+ break;
+ case 'revert':
+ node_revision_revert(arg(1), arg(3));
+ break;
+ case 'delete':
+ node_revision_delete(arg(1), arg(3));
+ break;
+ }
+ }
+ drupal_not_found();
+}
+
+/**
+ * Generate a listing of promoted nodes.
+ */
+function node_page_default() {
+ $result = pager_query(db_rewrite_sql('SELECT n.nid, n.sticky, n.created FROM {node} n WHERE n.promote = 1 AND n.status = 1 ORDER BY n.sticky DESC, n.created DESC'), variable_get('default_nodes_main', 10));
+
+ if (db_num_rows($result)) {
+ drupal_add_link(array('rel' => 'alternate',
+ 'type' => 'application/rss+xml',
+ 'title' => t('RSS'),
+ 'href' => url('rss.xml', NULL, NULL, TRUE)));
+
+ $output = '';
+ while ($node = db_fetch_object($result)) {
+ $output .= node_view(node_load($node->nid), 1);
+ }
+ $output .= theme('pager', NULL, variable_get('default_nodes_main', 10));
+ }
+ else {
+ $output = t('
+ Welcome to your new PlanetLab website!
+ Please follow these steps to set up and start using your website:
+
+ -
+ Login
+ To begin, login using a PlanetLab administrative account (e.g., %plc_root_user). This account will have full administration rights and will allow you to configure both the PlanetLab and Drupal aspects of the website.
+
+ -
+ Configure your website
+ Once logged in, visit the Drupal administration section, where you can customize and configure all Drupal aspects of the website. Visit the PlanetLab administration section to configure your PlanetLab installation.
+
+ -
+ Enable additional functionality
+ Next, visit the module list and enable features which suit your specific needs. You can find additional modules in the Drupal modules download section.
+
+ -
+ Customize your website design
+ To change the "look and feel" of your website, visit the themes section. You may choose from one of the included themes or download additional themes from the Drupal themes download section.
+
+ -
+ Start posting content
+ Finally, you can create content for your website. This message will disappear once you have published your first post.
+
+
+ For more information about administering the PlanetLab portions of the website, please refer to the MyPLC documentation.
+ For more information about Drupal, please refer to the Help section, or the online Drupal handbooks. You may also post at the Drupal forum, or view the wide range of other support options available.
',
+ array('%drupal' => 'http://drupal.org/', '%register' => url('user/register'), '%admin' => url('admin'), '%config' => url('admin/settings'), '%modules' => url('admin/modules'), '%download_modules' => 'http://drupal.org/project/modules', '%themes' => url('admin/themes'), '%download_themes' => 'http://drupal.org/project/themes', '%content' => url('node/add'), '%help' => url('admin/help'), '%handbook' => 'http://drupal.org/handbooks', '%forum' => 'http://drupal.org/forum', '%support' => 'http://drupal.org/support', '%plc_root_user' => PLC_ROOT_USER, '%db_adm' => '/db/adm/index.php', '%myplc' => '/doc/myplc/myplc.php')
+ );
+ $output = ''. $output .'';
+ }
+
+ return $output;
+}
+
+/**
+ * Menu callback; dispatches control to the appropriate operation handler.
+ */
+function node_page() {
+ $op = arg(1);
+
+ if (is_numeric($op)) {
+ $op = (arg(2) && !is_numeric(arg(2))) ? arg(2) : 'view';
+ }
+
+ switch ($op) {
+ case 'view':
+ if (is_numeric(arg(1))) {
+ $node = node_load(arg(1));
+ if ($node->nid) {
+ drupal_set_title(check_plain($node->title));
+ return node_show($node, arg(2));
+ }
+ else if (db_result(db_query('SELECT nid FROM {node} WHERE nid = %d', arg(1)))) {
+ drupal_access_denied();
+ }
+ else {
+ drupal_not_found();
+ }
+ }
+ break;
+ case 'add':
+ return node_add(arg(2));
+ break;
+ case 'edit':
+ if ($_POST['op'] == t('Delete')) {
+ // Note: we redirect from node/uid/edit to node/uid/delete to make the tabs disappear.
+ if ($_REQUEST['destination']) {
+ $destination = drupal_get_destination();
+ unset($_REQUEST['destination']);
+ }
+ drupal_goto('node/'. arg(1) .'/delete', $destination);
+ }
+
+ if (is_numeric(arg(1))) {
+ $node = node_load(arg(1));
+ if ($node->nid) {
+ drupal_set_title(check_plain($node->title));
+ return node_form($node);
+ }
+ else if (db_result(db_query('SELECT nid FROM {node} WHERE nid = %d', arg(1)))) {
+ drupal_access_denied();
+ }
+ else {
+ drupal_not_found();
+ }
+ }
+ break;
+ default:
+ drupal_set_title('');
+ return node_page_default();
+ }
+}
+
+/**
+ * shutdown function to make sure we always mark the last node processed.
+ */
+function node_update_shutdown() {
+ global $last_change, $last_nid;
+
+ if ($last_change && $last_nid) {
+ variable_set('node_cron_last', $last_change);
+ variable_set('node_cron_last_nid', $last_nid);
+ }
+}
+
+/**
+ * Implementation of hook_update_index().
+ */
+function node_update_index() {
+ global $last_change, $last_nid;
+
+ register_shutdown_function('node_update_shutdown');
+
+ $last = variable_get('node_cron_last', 0);
+ $last_nid = variable_get('node_cron_last_nid', 0);
+ $limit = (int)variable_get('search_cron_limit', 100);
+
+ // Store the maximum possible comments per thread (used for ranking by reply count)
+ variable_set('node_cron_comments_scale', 1.0 / max(1, db_result(db_query('SELECT MAX(comment_count) FROM {node_comment_statistics}'))));
+ variable_set('node_cron_views_scale', 1.0 / max(1, db_result(db_query('SELECT MAX(totalcount) FROM {node_counter}'))));
+
+ $result = db_query_range('SELECT GREATEST(c.last_comment_timestamp, n.changed) as last_change, n.nid FROM {node} n LEFT JOIN {node_comment_statistics} c ON n.nid = c.nid WHERE n.status = 1 AND ((GREATEST(n.changed, c.last_comment_timestamp) = %d AND n.nid > %d) OR (n.changed > %d OR c.last_comment_timestamp > %d)) ORDER BY GREATEST(n.changed, c.last_comment_timestamp) ASC, n.nid ASC', $last, $last_nid, $last, $last, $last, 0, $limit);
+
+ while ($node = db_fetch_object($result)) {
+ $last_change = $node->last_change;
+ $last_nid = $node->nid;
+ $node = node_load($node->nid);
+
+ // Get node output (filtered and with module-specific fields).
+ if (node_hook($node, 'view')) {
+ node_invoke($node, 'view', false, false);
+ }
+ else {
+ $node = node_prepare($node, false);
+ }
+ // Allow modules to change $node->body before viewing.
+ node_invoke_nodeapi($node, 'view', false, false);
+
+ $text = ''. check_plain($node->title) .'
'. $node->body;
+
+ // Fetch extra data normally not visible
+ $extra = node_invoke_nodeapi($node, 'update index');
+ foreach ($extra as $t) {
+ $text .= $t;
+ }
+
+ // Update index
+ search_index($node->nid, 'node', $text);
+ }
+}
+
+/**
+ * Implementation of hook_form_alter().
+ */
+function node_form_alter($form_id, &$form) {
+ // Node publishing options
+ if (isset($form['type']) && $form['type']['#value'] .'_node_settings' == $form_id) {
+ $form['workflow']['node_options_'. $form['type']['#value']] = array('#type' => 'checkboxes',
+ '#title' => t('Default options'),
+ '#default_value' => variable_get('node_options_'. $form['type']['#value'], array('status', 'promote')),
+ '#options' => array(
+ 'status' => t('Published'),
+ 'moderate' => t('In moderation queue'),
+ 'promote' => t('Promoted to front page'),
+ 'sticky' => t('Sticky at top of lists'),
+ 'revision' => t('Create new revision'),
+ ),
+ '#description' => t('Users with the administer nodes permission will be able to override these options.'),
+ );
+ }
+
+ // Advanced node search form
+ elseif ($form_id == 'search_form' && arg(1) == 'node') {
+ // Keyword boxes:
+ $form['advanced'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Advanced search'),
+ '#collapsible' => TRUE,
+ '#collapsed' => TRUE,
+ '#attributes' => array('class' => 'search-advanced'),
+ );
+ $form['advanced']['keywords'] = array(
+ '#prefix' => '',
+ '#suffix' => '',
+ );
+ $form['advanced']['keywords']['or'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Containing any of the words'),
+ '#size' => 30,
+ '#maxlength' => 255,
+ );
+ $form['advanced']['keywords']['phrase'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Containing the phrase'),
+ '#size' => 30,
+ '#maxlength' => 255,
+ );
+ $form['advanced']['keywords']['negative'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Containing none of the words'),
+ '#size' => 30,
+ '#maxlength' => 255,
+ );
+
+ // Taxonomy box:
+ if ($taxonomy = module_invoke('taxonomy', 'form_all', 1)) {
+ $form['advanced']['category'] = array(
+ '#type' => 'select',
+ '#title' => t('Only in the category(s)'),
+ '#prefix' => '',
+ '#size' => 10,
+ '#suffix' => '',
+ '#options' => $taxonomy,
+ '#multiple' => TRUE,
+ );
+ }
+
+ // Node types:
+ $types = node_get_types();
+ $form['advanced']['type'] = array(
+ '#type' => 'checkboxes',
+ '#title' => t('Only of the type(s)'),
+ '#prefix' => '',
+ '#suffix' => '',
+ '#options' => $types,
+ );
+ $form['advanced']['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Advanced search'),
+ '#prefix' => '',
+ '#suffix' => '
',
+ );
+
+ $form['#validate']['node_search_validate'] = array();
+ }
+}
+
+/**
+ * Form API callback for the search form. Registered in node_form_alter().
+ */
+function node_search_validate($form_id, $form_values, $form) {
+ // Initialise using any existing basic search keywords.
+ $keys = $form_values['processed_keys'];
+
+ // Insert extra restrictions into the search keywords string.
+ if (isset($form_values['type']) && is_array($form_values['type'])) {
+ // Retrieve selected types - Forms API sets the value of unselected checkboxes to 0.
+ $form_values['type'] = array_filter($form_values['type']);
+ if (count($form_values['type'])) {
+ $keys = search_query_insert($keys, 'type', implode(',', array_keys($form_values['type'])));
+ }
+ }
+
+ if (isset($form_values['category']) && is_array($form_values['category'])) {
+ $keys = search_query_insert($keys, 'category', implode(',', $form_values['category']));
+ }
+ if ($form_values['or'] != '') {
+ if (preg_match_all('/ ("[^"]+"|[^" ]+)/i', ' '. $form_values['or'], $matches)) {
+ $keys .= ' '. implode(' OR ', $matches[1]);
+ }
+ }
+ if ($form_values['negative'] != '') {
+ if (preg_match_all('/ ("[^"]+"|[^" ]+)/i', ' '. $form_values['negative'], $matches)) {
+ $keys .= ' -'. implode(' -', $matches[1]);
+ }
+ }
+ if ($form_values['phrase'] != '') {
+ $keys .= ' "'. str_replace('"', ' ', $form_values['phrase']) .'"';
+ }
+ if (!empty($keys)) {
+ form_set_value($form['basic']['inline']['processed_keys'], trim($keys));
+ }
+}
+
+/**
+ * @defgroup node_access Node access rights
+ * @{
+ * The node access system determines who can do what to which nodes.
+ *
+ * In determining access rights for a node, node_access() first checks
+ * whether the user has the "administer nodes" permission. Such users have
+ * unrestricted access to all nodes. Then the node module's hook_access()
+ * is called, and a TRUE or FALSE return value will grant or deny access.
+ * This allows, for example, the blog module to always grant access to the
+ * blog author, and for the book module to always deny editing access to
+ * PHP pages.
+ *
+ * If node module does not intervene (returns NULL), then the
+ * node_access table is used to determine access. All node access
+ * modules are queried using hook_node_grants() to assemble a list of
+ * "grant IDs" for the user. This list is compared against the table.
+ * If any row contains the node ID in question (or 0, which stands for "all
+ * nodes"), one of the grant IDs returned, and a value of TRUE for the
+ * operation in question, then access is granted. Note that this table is a
+ * list of grants; any matching row is sufficient to grant access to the
+ * node.
+ *
+ * In node listings, the process above is followed except that
+ * hook_access() is not called on each node for performance reasons and for
+ * proper functioning of the pager system. When adding a node listing to your
+ * module, be sure to use db_rewrite_sql() to add
+ * the appropriate clauses to your query for access checks.
+ *
+ * To see how to write a node access module of your own, see
+ * node_access_example.module.
+ */
+
+/**
+ * Determine whether the current user may perform the given operation on the
+ * specified node.
+ *
+ * @param $op
+ * The operation to be performed on the node. Possible values are:
+ * - "view"
+ * - "update"
+ * - "delete"
+ * - "create"
+ * @param $node
+ * The node object (or node array) on which the operation is to be performed,
+ * or node type (e.g. 'forum') for "create" operation.
+ * @param $uid
+ * The user ID on which the operation is to be performed.
+ * @return
+ * TRUE if the operation may be performed.
+ */
+function node_access($op, $node = NULL, $uid = NULL) {
+ // Convert the node to an object if necessary:
+ if ($op != 'create') {
+ $node = (object)$node;
+ }
+ // If the node is in a restricted format, disallow editing.
+ if ($op == 'update' && !filter_access($node->format)) {
+ return FALSE;
+ }
+
+ if (user_access('administer nodes')) {
+ return TRUE;
+ }
+
+ if (!user_access('access content')) {
+ return FALSE;
+ }
+
+ // Can't use node_invoke(), because the access hook takes the $op parameter
+ // before the $node parameter.
+ $access = module_invoke(node_get_base($node), 'access', $op, $node);
+ if (!is_null($access)) {
+ return $access;
+ }
+
+ // If the module did not override the access rights, use those set in the
+ // node_access table.
+ if ($op != 'create' && $node->nid && $node->status) {
+ $grants = array();
+ foreach (node_access_grants($op, $uid) as $realm => $gids) {
+ foreach ($gids as $gid) {
+ $grants[] = "(gid = $gid AND realm = '$realm')";
+ }
+ }
+
+ $grants_sql = '';
+ if (count($grants)) {
+ $grants_sql = 'AND ('. implode(' OR ', $grants) .')';
+ }
+
+ $sql = "SELECT COUNT(*) FROM {node_access} WHERE (nid = 0 OR nid = %d) $grants_sql AND grant_$op >= 1";
+ $result = db_query($sql, $node->nid);
+ return (db_result($result));
+ }
+ return FALSE;
+}
+
+/**
+ * Generate an SQL join clause for use in fetching a node listing.
+ *
+ * @param $node_alias
+ * If the node table has been given an SQL alias other than the default
+ * "n", that must be passed here.
+ * @param $node_access_alias
+ * If the node_access table has been given an SQL alias other than the default
+ * "na", that must be passed here.
+ * @return
+ * An SQL join clause.
+ */
+function _node_access_join_sql($node_alias = 'n', $node_access_alias = 'na') {
+ if (user_access('administer nodes')) {
+ return '';
+ }
+
+ return 'INNER JOIN {node_access} '. $node_access_alias .' ON '. $node_access_alias .'.nid = '. $node_alias .'.nid';
+}
+
+/**
+ * Generate an SQL where clause for use in fetching a node listing.
+ *
+ * @param $op
+ * The operation that must be allowed to return a node.
+ * @param $node_access_alias
+ * If the node_access table has been given an SQL alias other than the default
+ * "na", that must be passed here.
+ * @return
+ * An SQL where clause.
+ */
+function _node_access_where_sql($op = 'view', $node_access_alias = 'na', $uid = NULL) {
+ if (user_access('administer nodes')) {
+ return;
+ }
+
+ $grants = array();
+ foreach (node_access_grants($op, $uid) as $realm => $gids) {
+ foreach ($gids as $gid) {
+ $grants[] = "($node_access_alias.gid = $gid AND $node_access_alias.realm = '$realm')";
+ }
+ }
+
+ $grants_sql = '';
+ if (count($grants)) {
+ $grants_sql = 'AND ('. implode(' OR ', $grants) .')';
+ }
+
+ $sql = "$node_access_alias.grant_$op >= 1 $grants_sql";
+ return $sql;
+}
+
+/**
+ * Fetch an array of permission IDs granted to the given user ID.
+ *
+ * The implementation here provides only the universal "all" grant. A node
+ * access module should implement hook_node_grants() to provide a grant
+ * list for the user.
+ *
+ * @param $op
+ * The operation that the user is trying to perform.
+ * @param $uid
+ * The user ID performing the operation. If omitted, the current user is used.
+ * @return
+ * An associative array in which the keys are realms, and the values are
+ * arrays of grants for those realms.
+ */
+function node_access_grants($op, $uid = NULL) {
+ global $user;
+
+ if (isset($uid)) {
+ $user_object = user_load(array('uid' => $uid));
+ }
+ else {
+ $user_object = $user;
+ }
+
+ return array_merge(array('all' => array(0)), module_invoke_all('node_grants', $user_object, $op));
+}
+
+/**
+ * Determine whether the user has a global viewing grant for all nodes.
+ */
+function node_access_view_all_nodes() {
+ static $access;
+
+ if (!isset($access)) {
+ $grants = array();
+ foreach (node_access_grants('view') as $realm => $gids) {
+ foreach ($gids as $gid) {
+ $grants[] = "(gid = $gid AND realm = '$realm')";
+ }
+ }
+
+ $grants_sql = '';
+ if (count($grants)) {
+ $grants_sql = 'AND ('. implode(' OR ', $grants) .')';
+ }
+
+ $sql = "SELECT COUNT(*) FROM {node_access} WHERE nid = 0 $grants_sql AND grant_view >= 1";
+ $result = db_query($sql);
+ $access = db_result($result);
+ }
+
+ return $access;
+}
+
+/**
+ * Implementation of hook_db_rewrite_sql
+ */
+function node_db_rewrite_sql($query, $primary_table, $primary_field) {
+ if ($primary_field == 'nid' && !node_access_view_all_nodes()) {
+ $return['join'] = _node_access_join_sql($primary_table);
+ $return['where'] = _node_access_where_sql();
+ $return['distinct'] = 1;
+ return $return;
+ }
+}
+
+/**
+ * @} End of "defgroup node_access".
+ */
+
+
diff --git a/modules/page.module b/modules/page.module
new file mode 100644
index 0000000..361ce9f
--- /dev/null
+++ b/modules/page.module
@@ -0,0 +1,100 @@
+'. t('The page module allows users to create static pages, which are the most basic type of content. Pages are commonly collected in books via the book module. Users should create a page if the information on the page is static. An example would be an "about" page. ') .'';
+ $output .= ''. t('When a page is created, a user can set authoring information, configure publishing options, whether readers will be able to post comments. They can also select the content type of the page (e.g., full HTML, filtered HTML). ') .'
';
+ $output .= ''. t('As an administrator, you can set the publishing default for a page (in its workflow): you can specify whether a page is by default published, sent to moderation, promoted to the front page, sticky at the top of lists, and whether revisions are enabled by default. You can set the permissions that different user roles have to view, create, and edit pages.') .'
';
+ $output .= ''. t('If the location module is enabled, then location specific information can be added. If the trackback module is enabled trackbacks can be configured.') .'
';
+ $output .= t('You can
+
+- read the node administration help at administer >> help >> node.
+- read the page administration help at administer >> help >> page.
+- read the story administration help at administer >> help >> story.
+- create a page at create content >> page.
+- administer page content type at administer >> settings >> content types >> configure page.
+
+', array('%admin-help-node' => url('admin/help/node'), '%admin-help-page' => url('admin/help/page'), '%admin-help-story' => url('admin/help/story'), '%node-add-page' => url('node/add/page'), '%admin-settings-content-types-page' => url('admin/settings/content-types/page')));
+ $output .= ''. t('For more information please read the configuration and customization handbook Page page.', array('%page' => 'http://drupal.org/handbook/modules/page/')) .'
';
+ return $output;
+ case 'admin/modules#description':
+ return t('Enables the creation of pages that can be added to the navigation system.');
+ case 'node/add#page':
+ return t('If you want to add a static page, like a contact page or an about page, use a page.');
+ }
+}
+
+/**
+ * Implementation of hook_perm().
+ */
+function page_perm() {
+ return array('create pages', 'edit own pages');
+}
+
+/**
+ * Implementation of hook_node_info().
+ */
+function page_node_info() {
+ return array('page' => array('name' => t('page'), 'base' => 'page'));
+}
+
+/**
+ * Implementation of hook_access().
+ */
+function page_access($op, $node) {
+ global $user;
+
+ if ($op == 'create') {
+ return user_access('create pages');
+ }
+
+ if ($op == 'update' || $op == 'delete') {
+ if (user_access('edit own pages') && ($user->uid == $node->uid)) {
+ return TRUE;
+ }
+ }
+}
+
+/**
+ * Implementation of hook_menu().
+ */
+function page_menu($may_cache) {
+ $items = array();
+
+ if ($may_cache) {
+ $items[] = array('path' => 'node/add/page', 'title' => t('page'),
+ 'access' => user_access('create pages'));
+ }
+
+ return $items;
+}
+
+/**
+ * Implementation of hook_form().
+ */
+function page_form(&$node) {
+
+ $form['title'] = array('#type' => 'textfield', '#title' => t('Title'), '#required' => TRUE, '#default_value' => $node->title, '#weight' => -5);
+
+ $form['body_filter']['body'] = array('#type' => 'textarea', '#title' => t('Body'), '#default_value' => $node->body, '#rows' => 20, '#required' => TRUE);
+ $form['body_filter']['format'] = filter_form($node->format);
+
+ $form['log'] = array(
+ '#type' => 'textarea', '#title' => t('Log message'), '#default_value' => $node->log, '#weight' => 5,
+ '#description' => t('An explanation of the additions or updates being made to help other authors understand your motivations.')
+ );
+
+ return $form;
+}
+
+
diff --git a/modules/path.module b/modules/path.module
new file mode 100644
index 0000000..5c93e32
--- /dev/null
+++ b/modules/path.module
@@ -0,0 +1,353 @@
+'. t('The path module allows you to specify aliases for Drupal URLs. Such aliases improve readability of URLs for your users and may help internet search engines to index your content more effectively. More than one alias may be created for a given page.') .'';
+ $output .= t('Some examples of URL aliases are:
+
+- user/login => login
+- image/tid/16 => store
+- taxonomy/term/7+19+20+21 => store/products/whirlygigs
+- node/3 => contact
+
+');
+ $output .= ''. t('The path module enables an extra field for aliases in all node input and editing forms (when users have the appropriate permissions). It also provides an interface to view and edit all URL aliases. The two permissions are related to URL aliasing are "administer a list of URL aliases" and "add url aliases". ') .'
';
+ $output .= ''. t('This module also comes with user-defined mass URL aliasing capabilities, which is useful if you wish to uniformly use URLs different from the default. For example, you may want to have your URLs presented in a different language. Access to the Drupal source code on the web server is required to set up these kinds of aliases. ') .'
';
+ $output .= t('You can
+
+- set the path for a post with the path module.
+- add a URL alias: administer >> url aliases >> add alias.
+- administer the list of URL aliases: administer >> url aliases.
+- read how to configure clean URLs for your webserver.
+
- enable clean url\'s to remove the =? at administer >> settings.
+
+', array('%admin-path-add' => url('admin/path/add'), '%admin-path' => url('admin/path'), '%external-http-drupal-org-node-15365' => 'http://drupal.org/node/15365', '%admin-settings' => url('admin/settings')));
+ $output .= ''. t('For more information please read the configuration and customization handbook Path page.', array('%path' => 'http://drupal.org/handbook/modules/path/')) .'
';
+ return $output;
+ case 'admin/modules#description':
+ return t('Allows users to rename URLs.');
+ case 'admin/path':
+ return t("Drupal provides users complete control over URLs through aliasing. This feature is typically used to make URLs human-readable or easy to remember. For example, one could map the relative URL 'node/1' onto 'about'. Each system path can have multiple aliases.
");
+ case 'admin/path/add':
+ return t('Enter the path you wish to create the alias for, followed by the name of the new alias.
');
+ }
+}
+
+/**
+ * Implementation of hook_menu().
+ */
+function path_menu($may_cache) {
+ $items = array();
+
+ if ($may_cache) {
+ $items[] = array('path' => 'admin/path', 'title' => t('url aliases'),
+ 'callback' => 'path_admin',
+ 'access' => user_access('administer url aliases'));
+ $items[] = array('path' => 'admin/path/edit', 'title' => t('edit alias'),
+ 'callback' => 'path_admin_edit',
+ 'access' => user_access('administer url aliases'),
+ 'type' => MENU_CALLBACK);
+ $items[] = array('path' => 'admin/path/delete', 'title' => t('delete alias'),
+ 'callback' => 'path_admin_delete_confirm',
+ 'access' => user_access('administer url aliases'),
+ 'type' => MENU_CALLBACK);
+ $items[] = array('path' => 'admin/path/list', 'title' => t('list'),
+ 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -10);
+ $items[] = array('path' => 'admin/path/add', 'title' => t('add alias'),
+ 'callback' => 'path_admin_edit',
+ 'access' => user_access('administer url aliases'),
+ 'type' => MENU_LOCAL_TASK);
+ }
+
+ return $items;
+}
+
+/**
+ * Menu callback; presents an overview of all URL aliases.
+ */
+function path_admin() {
+ return path_overview();
+}
+
+/**
+ * Menu callback; handles pages for creating and editing URL aliases.
+ */
+function path_admin_edit($pid = 0) {
+ if ($pid) {
+ $alias = path_load($pid);
+ drupal_set_title($alias['dst']);
+ $output = path_form(path_load($pid));
+ }
+ else {
+ $output = path_form();
+ }
+
+ return $output;
+}
+
+/**
+ * Menu callback; confirms deleting an URL alias
+ **/
+function path_admin_delete_confirm($pid) {
+ $path = path_load($pid);
+ if (user_access('administer url aliases')) {
+ $form['pid'] = array('#type' => 'value', '#value' => $pid);
+ $output = confirm_form('path_admin_delete_confirm', $form,
+ t('Are you sure you want to delete path alias %title?', array('%title' => theme('placeholder', $path['dst']))),
+ $_GET['destination'] ? $_GET['destination'] : 'admin/path', t('This action cannot be undone.'),
+ t('Delete'), t('Cancel') );
+ }
+
+ return $output;
+}
+
+/**
+ * Execute URL alias deletion
+ **/
+function path_admin_delete_confirm_submit($form_id, $form_values) {
+ if ($form_values['confirm']) {
+ path_admin_delete($form_values['pid']);
+ return 'admin/path';
+ }
+}
+
+/**
+ * Post-confirmation; delete an URL alias.
+ */
+function path_admin_delete($pid = 0) {
+ db_query('DELETE FROM {url_alias} WHERE pid = %d', $pid);
+ drupal_set_message(t('The alias has been deleted.'));
+}
+
+
+
+/**
+ * Set an aliased path for a given Drupal path, preventing duplicates.
+ */
+function path_set_alias($path = NULL, $alias = NULL, $pid = NULL) {
+ if ($path && !$alias) {
+ db_query("DELETE FROM {url_alias} WHERE src = '%s'", $path);
+ drupal_clear_path_cache();
+ }
+ else if (!$path && $alias) {
+ db_query("DELETE FROM {url_alias} WHERE dst = '%s'", $alias);
+ drupal_clear_path_cache();
+ }
+ else if ($path && $alias) {
+ $path = urldecode($path);
+ $path_count = db_result(db_query("SELECT COUNT(src) FROM {url_alias} WHERE src = '%s'", $path));
+ $alias = urldecode($alias);
+ $alias_count = db_result(db_query("SELECT COUNT(dst) FROM {url_alias} WHERE dst = '%s'", $alias));
+
+ // We have an insert:
+ if ($path_count == 0 && $alias_count == 0) {
+ db_query("INSERT INTO {url_alias} (src, dst) VALUES ('%s', '%s')", $path, $alias);
+ drupal_clear_path_cache();
+ }
+ else if ($path_count >= 1 && $alias_count == 0) {
+ if ($pid) {
+ db_query("UPDATE {url_alias} SET dst = '%s', src = '%s' WHERE pid = %d", $alias, $path, $pid);
+ }
+ else {
+ db_query("INSERT INTO {url_alias} (src, dst) VALUES ('%s', '%s')", $path, $alias);
+ }
+ drupal_clear_path_cache();
+ }
+ else if ($path_count == 0 && $alias_count == 1) {
+ db_query("UPDATE {url_alias} SET src = '%s' WHERE dst = '%s'", $path, $alias);
+ drupal_clear_path_cache();
+ }
+ else if ($path_count == 1 && $alias_count == 1) {
+ // This will delete the path that alias was originally pointing to:
+ path_set_alias(NULL, $alias);
+ path_set_alias($path);
+ path_set_alias($path, $alias);
+ }
+ }
+}
+
+/**
+ * Return a form for editing or creating an individual URL alias.
+ */
+function path_form($edit = '') {
+
+ $form['src'] = array('#type' => 'textfield', '#title' => t('Existing system path'), '#default_value' => $edit['src'], '#maxlength' => 64, '#description' => t('Specify the existing path you wish to alias. For example: node/28, forum/1, taxonomy/term/1+2.'));
+ $form['dst'] = array('#type' => 'textfield', '#default_value' => $edit['dst'], '#maxlength' => 64, '#description' => t('Specify an alternative path by which this data can be accessed. For example, type "about" when writing an about page. Use a relative path and don\'t add a trailing slash or the URL alias won\'t work.'));
+
+ if ($edit['pid']) {
+ $form['pid'] = array('#type' => 'hidden', '#value' => $edit['pid']);
+ $form['submit'] = array('#type' => 'submit', '#value' => t('Update alias'));
+ }
+ else {
+ $form['submit'] = array('#type' => 'submit', '#value' => t('Create new alias'));
+ }
+
+ return drupal_get_form('path_form', $form);
+}
+
+/**
+ * Implementation of hook_nodeapi().
+ *
+ * Allows URL aliases for nodes to be specified at node edit time rather
+ * than through the administrative interface.
+ */
+function path_nodeapi(&$node, $op, $arg) {
+ if (user_access('create url aliases') || user_access('administer url aliases')) {
+ switch ($op) {
+ case 'validate':
+ $node->path = trim($node->path);
+ if ($node->path && !valid_url($node->path)) {
+ form_set_error('path', t('The path is invalid.'));
+ }
+ else if (db_result(db_query("SELECT COUNT(dst) FROM {url_alias} WHERE dst = '%s' AND src != '%s'", $node->path, "node/$node->nid"))) {
+ form_set_error('path', t('The path is already in use.'));
+ }
+ break;
+
+ case 'load':
+ $path = "node/$node->nid";
+ // We don't use drupal_get_path_alias() to avoid custom rewrite functions.
+ // We only care about exact aliases.
+ $result = db_query("SELECT dst FROM {url_alias} WHERE src = '%s'", $path);
+ if (db_num_rows($result)) {
+ $node->path = db_result($result);
+ }
+ break;
+
+ case 'insert':
+ // Don't try to insert if path is NULL. We may have already set
+ // the alias ahead of time.
+ if ($node->path) {
+ path_set_alias("node/$node->nid", $node->path);
+ }
+ break;
+
+ case 'update':
+ path_set_alias("node/$node->nid", $node->path, $node->pid);
+ break;
+
+ case 'delete':
+ $path = "node/$node->nid";
+ if (drupal_get_path_alias($path) != $path) {
+ path_set_alias($path);
+ }
+ break;
+ }
+ }
+}
+
+/**
+ * Implementation of hook_form_alter().
+ */
+function path_form_alter($form_id, &$form) {
+ if (user_access('create url aliases') && isset($form['type']) && $form['type']['#value'] .'_node_form' == $form_id) {
+ $path = $form['#node']->path;
+ $form['path'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('URL path settings'),
+ '#collapsible' => TRUE,
+ '#collapsed' => empty($path),
+ '#weight' => 30,
+ );
+ $form['path']['path'] = array(
+ '#type' => 'textfield',
+ '#default_value' => $path,
+ '#maxlength' => 250,
+ '#collapsible' => TRUE,
+ '#collapsed' => TRUE,
+ '#description' => t('Optionally specify an alternative URL by which this node can be accessed. For example, type "about" when writing an about page. Use a relative path and don\'t add a trailing slash or the URL alias won\'t work.'),
+ );
+ if ($path) {
+ $form['path']['pid'] = array(
+ '#type' => 'value',
+ '#value' => db_result(db_query("SELECT pid FROM {url_alias} WHERE dst = '%s'", $path))
+ );
+ }
+ }
+}
+
+
+/**
+ * Implementation of hook_perm().
+ */
+function path_perm() {
+ return array('create url aliases', 'administer url aliases');
+}
+
+/**
+ * Return a listing of all defined URL aliases.
+ */
+function path_overview() {
+ $sql = 'SELECT * FROM {url_alias}';
+ $header = array(
+ array('data' => t('Alias'), 'field' => 'dst', 'sort' => 'asc'),
+ array('data' => t('System'), 'field' => 'src'),
+ array('data' => t('Operations'), 'colspan' => '2')
+ );
+ $sql .= tablesort_sql($header);
+ $result = pager_query($sql, 50);
+
+ $destination = drupal_get_destination();
+ while ($data = db_fetch_object($result)) {
+ $rows[] = array($data->dst, $data->src, l(t('edit'), "admin/path/edit/$data->pid", array(), $destination), l(t('delete'), "admin/path/delete/$data->pid", array(), $destination));
+ }
+
+ if (!$rows) {
+ $rows[] = array(array('data' => t('No URL aliases available.'), 'colspan' => '4'));
+ }
+
+ $output = theme('table', $header, $rows);
+ $output .= theme('pager', NULL, 50, 0);
+ return $output;
+}
+
+/**
+ * Fetch a specific URL alias from the database.
+ */
+function path_load($pid) {
+ return db_fetch_array(db_query('SELECT * FROM {url_alias} WHERE pid = %d', $pid));
+}
+
+/**
+ * Verify that a new URL alias is valid, and save it to the database.
+ */
+function path_form_submit() {
+ $edit = $GLOBALS['form_values'];
+ $src = $edit['src'];
+ $dst = $edit['dst'];
+ $pid = $edit['pid'];
+
+ if (!valid_url($src)) {
+ form_set_error('src', t('The system path %path is invalid.', array('%path' => theme('placeholder', $src))));
+ }
+
+ if (!valid_url($dst)) {
+ form_set_error('dst', t('The alias %alias is invalid.', array('%alias' => theme('placeholder', $dst))));
+ }
+
+ if (db_result(db_query("SELECT COUNT(dst) FROM {url_alias} WHERE pid != %d AND dst = '%s'", $pid, $dst))) {
+ form_set_error('dst', t('The alias %alias is already in use.', array('%alias' => theme('placeholder', $dst))));
+ }
+
+ if (form_get_errors()) {
+ return path_form($edit);
+ }
+ else {
+ path_set_alias($src, $dst, $pid);
+
+ drupal_set_message(t('The alias has been saved.'));
+ return 'admin/path';
+ }
+}
+
+
diff --git a/modules/ping.module b/modules/ping.module
new file mode 100644
index 0000000..fdf45e9
--- /dev/null
+++ b/modules/ping.module
@@ -0,0 +1,68 @@
+'. t('The ping module is useful for notifying interested sites that your site has changed. It automatically sends notifications (called "pings") to the pingomatic service to tell it that your site has changed. In turn pingomatic will ping other services such as weblogs.com, Technorati, blo.gs, BlogRolling, Feedster.com, Moreover, etc.', array('%external-http-pingomatic-com' => 'http://pingomatic.com/')) .'';
+ $output .= ''. t('The ping module requires cron
or a similar periodic job scheduler to be enabled.') .'
';
+ $output .= t('You can:
+
+- enable or disable the ping module at administer >> modules.
+- run your cron job at your sites cron page.
+- read about configuring cron jobs.
+
+', array('%admin-modules' => url('admin/modules'), '%file-cron' => 'cron.php', '%external-http-drupal-org-node-23714' => 'http://drupal.org/node/23714'));
+ $output .= ''. t('For more information please read the configuration and customization handbook Ping page.', array('%ping' => 'http://drupal.org/handbook/modules/ping/')) .'
';
+ return $output;
+ case 'admin/modules#description':
+ return t('Alerts other sites when your site has been updated.');
+ }
+}
+
+/**
+ * Implementation of hook_cron().
+ *
+ * Fire off notifications of updates to remote sites.
+ */
+function ping_cron() {
+ global $base_url;
+
+ if (variable_get('site_name', 0) && variable_get('site_slogan', 0)) {
+ if (db_num_rows(db_query("SELECT nid FROM {node} WHERE status = 1 AND moderate = 0 AND (created > '". variable_get('cron_last', time()) ."' OR changed > '". variable_get('cron_last', time()) ."')"))) {
+ _ping_notify(variable_get('site_name', '') .' - '. variable_get('site_slogan', ''), $base_url);
+ }
+ }
+}
+
+/**
+ * Call hook_ping() in all modules to notify remote sites that there is
+ * new content at this one.
+ */
+function _ping_notify($name, $url) {
+ module_invoke_all('ping', $name, $url);
+}
+
+/**
+ * Implementation of hook_ping().
+ *
+ * Notifies pingomatic.com, blo.gs, and technorati.com of changes at this site.
+ */
+function ping_ping($name = '', $url = '') {
+
+ $result = xmlrpc('http://rpc.pingomatic.com', 'weblogUpdates.ping', $name, $url);
+
+ if ($result === FALSE) {
+ watchdog('directory ping', t('Failed to notify pingomatic.com (site).'), WATCHDOG_WARNING);
+ }
+}
+
+
diff --git a/modules/planetlab.module b/modules/planetlab.module
new file mode 100644
index 0000000..735049a
--- /dev/null
+++ b/modules/planetlab.module
@@ -0,0 +1,319 @@
+
+ // Copyright (C) 2006 The Trustees of Princeton University
+ //
+ // $Id: planetlab.module 809 2007-09-03 09:47:40Z thierry $
+ //
+
+require_once 'plc_config.php';
+require_once 'plc_session.php';
+
+function planetlab_help($section)
+{
+ switch ($section) {
+ case 'admin/modules#description':
+ return t('Enables authenticated login via a PlanetLab API server.');
+ }
+}
+
+function planetlab_info($field = 0)
+{
+ $info['name'] = 'PlanetLab';
+
+ if ($field) {
+ return $info[$field];
+ }
+ else {
+ return $info;
+ }
+}
+
+function planetlab_menu($may_cache)
+{
+ $items = array();
+
+ if ($may_cache) {
+ $items[] = array(
+ 'path' => 'planetlab/logout',
+ 'title' => t('Log out of %s', array('%s' => variable_get('site_name', 'local'))),
+ 'callback' => 'planetlab_logout',
+ 'access' => TRUE,
+ 'type' => MENU_CALLBACK
+ );
+ $items[] = array(
+ 'path' => 'db',
+ 'title' => variable_get('site_name', 'local'),
+ 'callback' => 'planetlab_page',
+ 'access' => TRUE,
+ 'type' => MENU_CALLBACK
+ );
+ }
+
+ return $items;
+}
+
+function planetlab_block($op = 'list', $delta = 0, $edit = array())
+{
+ global $user, $plc;
+
+ if ($op == 'list') {
+ $blocks[0]['info'] = t('PlanetLab login');
+
+ return $blocks;
+ }
+ else if ($op == 'view') {
+ $block = array();
+
+ if (!$plc->person) {
+ // Force login via HTTPS
+ unset($_GET['time']);
+ $form['#action'] = "https://" . $_SERVER['HTTP_HOST'] . url($_GET['q'], drupal_get_destination());
+ $form['#id'] = 'planetlab-login-form';
+ $form['name'] = array('#type' => 'textfield',
+ '#title' => t('E-mail'),
+ '#maxlength' => 60,
+ '#size' => 25,
+ '#required' => TRUE,
+ );
+ $form['pass'] = array('#type' => 'password',
+ '#title' => t('Password'),
+ '#size' => 25,
+ '#required' => TRUE,
+ );
+ $form['submit'] = array('#type' => 'submit',
+ '#value' => t('Log in'),
+ );
+
+ $block['subject'] = t('%s login', array('%s' => variable_get('site_name', 'local')));
+ $block['content'] = drupal_get_form('planetlab_login_block', $form, 'planetlab_login');
+ $block['content'] .= '';
+ $block['content'] .= '';
+ $block['content'] .= '';
+ } else {
+ $block['subject'] = $plc->person['email'];
+ $is_admin = in_array(10,$plc->person['role_ids']);
+ $is_pi = in_array(20,$plc->person['role_ids']);
+ $is_user = in_array(30,$plc->person['role_ids']);
+ $is_tech = in_array(40,$plc->person['role_ids']);
+
+ $site_item = '';
+ $site_item .= 'Sites';
+ $site_item .= '';
+ $site_item .= '- My Site
';
+ if ( $is_admin )
+ $site_item .= "- Join Requests
";
+ if( $is_admin )
+ $site_item .= "- Migration Status
";
+ $site_item .= "
";
+ $items[] = $site_item;
+
+ $user_item = '';
+ $user_item .= 'Users';
+ $user_item .= '';
+ $user_item .= '- My account
';
+ if (is_pi)
+ $user_item .= '- My users
';
+ if ( $plc->alt_person && $plc->alt_auth) {
+ $email = $plc->person['email'];
+ $user_item .= "- Log out of $email
";
+ }
+
+
+ $user_item .= '
';
+ $items [] = $user_item;
+
+ $node_item = '';
+ $node_item .= 'Nodes';
+ $node_item .= '';
+ $node_item .= '- My Site Nodes
';
+ if( $is_admin || $is_pi || $is_user )
+ $node_item .= "- Add Node
";
+ if ( $is_admin)
+ $node_item .= "- Setting Types
";
+ $node_item .= '
';
+ $items [] = $node_item;
+
+ $slice_item = '';
+ //if( !( $is_tech && ! $is_user && ! $is_pi && ! $is_admin ) )
+ $slice_item .= 'Slices';
+ $slice_item .= '';
+ if( $is_admin || $is_pi ) {
+ $slice_item .= "- Create Slice
";
+ $slice_item .= "- Attribute Types
";
+ }
+ if( !( $is_tech && ! $is_user && ! $is_pi && ! $is_admin ) )
+ $slice_item .= '- Sirius
';
+ $slice_item .= '
';
+ $items [] = $slice_item;
+
+ if ( $is_admin )
+ $items[] = l(t('Peers'),'db/peers/');
+
+ if ( $is_admin )
+ $items[] = l(t('Events'),'db/events/');
+
+ $items[] = l(t('About'),'db/about.php');
+
+ if ($user->uid) {
+ // Drupal logout (destroys the session and cleans up $user)
+ $items[] = l(t('Log out of %s', array('%s' => variable_get('site_name', 'local'))), 'logout');
+ } else {
+ // PlanetLab logout (just destroy the session)
+ $items[] = l(t('Log out'), 'planetlab/logout');
+ }
+
+ $block['content'] = theme('item_list', $items);
+ }
+
+ /*
+ ob_start();
+ print '';
+ print_r($_SESSION);
+ print '
';
+ $block['content'] .= ob_get_contents();
+ ob_end_clean();
+ */
+
+ return $block;
+ }
+}
+
+function planetlab_login_validate($form_id, $form_values)
+{
+ global $user, $plc;
+
+ if ($form_values['name'] && $form_values['pass']) {
+ // Drupal login succeeded
+ if (($user = user_authenticate($form_values['name'], trim($form_values['pass']))) &&
+ $user->uid) {
+ return;
+ }
+
+ $plc = new PLCSession($form_values['name'], $form_values['pass']);
+
+ // PlanetLab login failed
+ if (!$plc->person) {
+ form_set_error('login', t('Sorry. Unrecognized username or password.'));
+ watchdog('planetlab', t('Login attempt failed for %user.', array('%user' => theme('placeholder', $form_values['name']))));
+ }
+
+ // PlanetLab login succeeded
+ else {
+ // Login admins to Drupal as the superuser
+ if (in_array('admin', $plc->person['roles'])) {
+ $user = user_load(array('uid' => 1));
+ }
+ }
+ }
+}
+
+function planetlab_login_submit($form_id, $form_values)
+{
+ global $plc;
+
+ // Our referring page is encased in a query string of the form
+ // "destination=referrer".
+ parse_str(drupal_get_destination()); // => $destination
+
+ // The referrer itself is a URL path with the original query string,
+ // e.g. "referer.php?query".
+ extract(parse_url($destination)); // => $query
+
+ // Which we then have to parse again as a query string.
+ parse_str($query); // => $url
+
+ if ($plc->person) {
+ // To handle the edge case where this function is called during a
+ // bootstrap, check for the existence of t().
+ if (function_exists('t')) {
+ $message = t('Session opened for %name.', array('%name' => theme('placeholder', $plc->person['email'])));
+ }
+ else {
+ $message = "Session opened for ". check_plain($person['email']);
+ }
+ watchdog('planetlab', $message);
+
+ if (empty($url)) {
+ // Create a timestamped final URL so that browsers don't return the user to
+ // a cached page (where it would appear as if they never logged in or out).
+ return array('time='. time());
+ } else {
+ // Make sure that redirections are always local
+ $url = urldecode($url);
+ if ($url[0] != "/") {
+ $url = "/$url";
+ }
+ Header("Location: $url");
+ exit();
+ }
+ }
+}
+
+function planetlab_logout()
+{
+ global $plc;
+
+ if ($plc->person) {
+ // Invalidate PlanetLab session
+ $plc->logout();
+ watchdog('planetlab', t('Session closed for %name.', array('%name' => theme('placeholder', $plc->person['email']))));
+ }
+
+ // Destroy the current session:
+ session_destroy();
+
+ // The time prevents caching.
+ drupal_goto(NULL, 'time='. time());
+}
+
+function planetlab_user($type, &$edit, &$user, $category = NULL)
+{
+ switch ($type) {
+ case 'logout':
+ if ($plc->person) {
+ $plc->logout();
+ watchdog('planetlab', t('Session closed for %name.', array('%name' => theme('placeholder', $plc->person['email']))));
+ }
+ break;
+ }
+}
+
+function planetlab_page()
+{
+ $path = $_SERVER['DOCUMENT_ROOT'] . preg_replace('/^db\//', '/planetlab/', $_GET['q']);
+
+ // error_log("Requested " . $_GET['q'] . " -> $path");
+
+ if (is_dir($path)) {
+ foreach (array('index.php', 'index.html', 'index.htm') as $index) {
+ if (is_file($path . "/$index")) {
+ $path .= "/$index";
+ break;
+ }
+ }
+ }
+
+ if (is_file($path)) {
+ if (preg_match('/.php$/', $path)) {
+ ob_start();
+ include $path;
+ $output = ob_get_contents();
+ ob_end_clean();
+ } else {
+ $output = file_get_contents($path);
+ }
+ return $output;
+ }
+
+ drupal_not_found();
+}
+
+function theme_planetlab($content)
+{
+ return $content;
+}
+
+?>
diff --git a/modules/poll.module b/modules/poll.module
new file mode 100644
index 0000000..14a85a8
--- /dev/null
+++ b/modules/poll.module
@@ -0,0 +1,501 @@
+'. t('The poll module can be used to create simple polls for site users. A poll is a simple multiple choice questionnaire which displays the cumulative results of the answers to the poll. Having polls on the site is a good way to get instant feedback from community members.') .'';
+ $output .= ''. t('Users can create a poll. The title of the poll should be the question, then enter the answers and the "base" vote counts. You can also choose the time period over which the vote will run.The poll item in the navigation menu will take you to a page where you can see all the current polls, vote on them (if you haven\'t already) and view the results.', array('%poll' => url('poll'))) .'
';
+ $output .= t('You can
+
+', array('%poll' => url('poll'), '%admin-node-configure-types-poll' => url('admin/settings/content-types/poll')));
+ $output .= ''. t('For more information please read the configuration and customization handbook Poll page.', array('%poll' => 'http://drupal.org/handbook/modules/poll/')) .'
';
+ return $output;
+ case 'admin/modules#description':
+ return t("Allows your site to capture votes on different topics in the form of multiple choice questions.");
+ case 'node/add#poll':
+ return t("A poll is a multiple-choice question which visitors can vote on.");
+ }
+}
+
+/**
+ * Implementation of hook_access().
+ */
+function poll_access($op, $node) {
+ if ($op == 'create') {
+ return user_access('create polls');
+ }
+}
+
+/**
+ * Implementation of hook_block().
+ *
+ * Generates a block containing the latest poll.
+ */
+function poll_block($op = 'list', $delta = 0) {
+ if (user_access('access content')) {
+ if ($op == 'list') {
+ $blocks[0]['info'] = t('Most recent poll');
+ return $blocks;
+ }
+ else if ($op == 'view') {
+ // Retrieve the latest poll.
+ $sql = db_rewrite_sql("SELECT MAX(n.created) FROM {node} n INNER JOIN {poll} p ON p.nid = n.nid WHERE n.status = 1 AND p.active = 1 AND n.moderate = 0");
+ $timestamp = db_result(db_query($sql));
+ if ($timestamp) {
+ $poll = node_load(array('type' => 'poll', 'created' => $timestamp, 'moderate' => 0, 'status' => 1));
+
+ if ($poll->nid) {
+ // poll_view() dumps the output into $poll->body.
+ poll_view($poll, 1, 0, 1);
+ }
+ }
+ $block['subject'] = t('Poll');
+ $block['content'] = $poll->body;
+ return $block;
+ }
+ }
+}
+
+/**
+ * Implementation of hook_cron().
+ *
+ * Closes polls that have exceeded their allowed runtime.
+ */
+function poll_cron() {
+ $result = db_query('SELECT p.nid FROM {poll} p INNER JOIN {node} n ON p.nid = n.nid WHERE (n.created + p.runtime) < '. time() .' AND p.active = 1 AND p.runtime != 0');
+ while ($poll = db_fetch_object($result)) {
+ db_query("UPDATE {poll} SET active = 0 WHERE nid = %d", $poll->nid);
+ }
+}
+
+/**
+ * Implementation of hook_delete().
+ */
+function poll_delete($node) {
+ db_query("DELETE FROM {poll} WHERE nid = %d", $node->nid);
+ db_query("DELETE FROM {poll_choices} WHERE nid = %d", $node->nid);
+ db_query("DELETE FROM {poll_votes} WHERE nid = %d", $node->nid);
+}
+
+/**
+ * Implementation of hook_submit().
+ */
+function poll_submit(&$node) {
+ // Renumber fields
+ $node->choice = array_values($node->choice);
+ $node->teaser = poll_teaser($node);
+}
+
+/**
+ * Implementation of hook_validate().
+ */
+function poll_validate($node) {
+ if (isset($node->title)) {
+ // Check for at least two options and validate amount of votes:
+ $realchoices = 0;
+ // Renumber fields
+ $node->choice = array_values($node->choice);
+ foreach ($node->choice as $i => $choice) {
+ if ($choice['chtext'] != '') {
+ $realchoices++;
+ }
+ if ($choice['chvotes'] < 0) {
+ form_set_error("choice][$i][chvotes", t('Negative values are not allowed.'));
+ }
+ }
+
+ if ($realchoices < 2) {
+ form_set_error("choice][$realchoices][chtext", t('You must fill in at least two choices.'));
+ }
+ }
+}
+
+/**
+ * Implementation of hook_form().
+ */
+function poll_form(&$node) {
+ $admin = user_access('administer nodes');
+
+ $form['title'] = array('#type' => 'textfield', '#title' => t('Question'), '#required' => TRUE, '#default_value' => $node->title, '#weight' => -1);
+
+ $form['choice']['choices'] = array('#type' => 'hidden', '#default_value' => max(2, count($node->choice) ? count($node->choice) : 5));
+ $form['choice']['morechoices'] = array('#type' => 'checkbox', '#title' => t('Need more choices'), '#default_value' => 0, '#description' => t("If the amount of boxes above isn't enough, check this box and click the Preview button below to add some more."), '#weight' => 1);
+ $form['choice'] = form_builder('poll_node_form', $form['choice']);
+ if ($form['choice']['morechoices']['#value']) {
+ $form['choice']['morechoices']['#value'] = 0;
+ $form['choice']['choices']['#value'] *= 2;
+ }
+
+ // if the value was changed in a previous iteration, retain it.
+ $node->choices = $form['choice']['choices']['#value'];
+
+ // Poll choices
+ $form['choice'] += array('#type' => 'fieldset', '#title' => t('Choices'), '#prefix' => '', '#suffix' => '', '#tree' => TRUE);
+ for ($a = 0; $a < $node->choices; $a++) {
+ $form['choice'][$a]['chtext'] = array('#type' => 'textfield', '#title' => t('Choice %n', array('%n' => ($a + 1))), '#default_value' => $node->choice[$a]['chtext']);
+ if ($admin) {
+ $form['choice'][$a]['chvotes'] = array('#type' => 'textfield', '#title' => t('Votes for choice %n', array('%n' => ($a + 1))), '#default_value' => (int)$node->choice[$a]['chvotes'], '#size' => 5, '#maxlength' => 7);
+ }
+ }
+
+ // Poll attributes
+ $_duration = array(0 => t('Unlimited')) + drupal_map_assoc(array(86400, 172800, 345600, 604800, 1209600, 2419200, 4838400, 9676800, 31536000), "format_interval");
+ $_active = array(0 => t('Closed'), 1 => t('Active'));
+
+ if ($admin) {
+ $form['settings'] = array('#type' => 'fieldset', '#title' => t('Settings'));
+ $form['settings']['active'] = array('#type' => 'radios', '#title' => t('Poll status'), '#default_value' => isset($node->active) ? $node->active : 1, '#options' => $_active, '#description' => t('When a poll is closed, visitors can no longer vote for it.'));
+ }
+ $form['settings']['runtime'] = array('#type' => 'select', '#title' => t('Poll duration'), '#default_value' => $node->runtime ? $node->runtime : 0, '#options' => $_duration, '#description' => t('After this period, the poll will be closed automatically.'));
+
+ return $form;
+}
+
+function poll_insert($node) {
+ if (!user_access('administer nodes')) {
+ // Make sure all votes are 0 initially
+ foreach ($node->choice as $i => $choice) {
+ $node->choice[$i]['chvotes'] = 0;
+ }
+ $node->active = 1;
+ }
+
+ db_query("INSERT INTO {poll} (nid, runtime, active) VALUES (%d, %d, %d)", $node->nid, $node->runtime, $node->active);
+
+ foreach ($node->choice as $choice) {
+ if ($choice['chtext'] != '') {
+ db_query("INSERT INTO {poll_choices} (nid, chtext, chvotes, chorder) VALUES (%d, '%s', %d, %d)", $node->nid, $choice['chtext'], $choice['chvotes'], $i++);
+ }
+ }
+}
+
+/**
+ * Implementation of hook_menu().
+ */
+function poll_menu($may_cache) {
+ $items = array();
+
+ if ($may_cache) {
+ $items[] = array('path' => 'node/add/poll', 'title' => t('poll'),
+ 'access' => user_access('create polls'));
+ $items[] = array('path' => 'poll', 'title' => t('polls'),
+ 'callback' => 'poll_page',
+ 'access' => user_access('access content'),
+ 'type' => MENU_SUGGESTED_ITEM);
+
+ $items[] = array('path' => 'poll/vote',
+ 'title' => t('vote'),
+ 'callback' => 'poll_vote',
+ 'access' => user_access('vote on polls'),
+ 'type' => MENU_CALLBACK);
+ }
+ else {
+ if (arg(0) == 'node' && is_numeric(arg(1))) {
+ $node = node_load(arg(1));
+
+ if ($node->type == 'poll' && $node->allowvotes) {
+ $items[] = array('path' => 'node/'. arg(1) .'/results',
+ 'title' => t('results'),
+ 'callback' => 'poll_results',
+ 'access' => user_access('access content'),
+ 'weight' => 3,
+ 'type' => MENU_LOCAL_TASK);
+ }
+ }
+ }
+
+ return $items;
+}
+
+/**
+ * Implementation of hook_load().
+ */
+function poll_load($node) {
+ global $user;
+
+ // Load the appropriate choices into the $node object
+ $poll = db_fetch_object(db_query("SELECT runtime, active FROM {poll} WHERE nid = %d", $node->nid));
+
+ $result = db_query("SELECT chtext, chvotes, chorder FROM {poll_choices} WHERE nid = %d ORDER BY chorder", $node->nid);
+ while ($choice = db_fetch_array($result)) {
+ $poll->choice[$choice['chorder']] = $choice;
+ }
+
+ // Determine whether or not this user is allowed to vote
+ $poll->allowvotes = FALSE;
+ if (user_access('vote on polls') && $poll->active) {
+ if ($user->uid && db_num_rows(db_query('SELECT uid FROM {poll_votes} WHERE nid = %d AND uid = %d', $node->nid, $user->uid)) == 0) {
+ $poll->allowvotes = TRUE;
+ }
+ else if ($user->uid == 0 && db_num_rows(db_query("SELECT hostname FROM {poll_votes} WHERE nid = %d AND hostname = '%s'", $node->nid, $_SERVER['REMOTE_ADDR'])) == 0) {
+ $poll->allowvotes = TRUE;
+ }
+ }
+ return $poll;
+}
+
+/**
+ * Implementation of hook_node_info().
+ */
+function poll_node_info() {
+ return array('poll' => array('name' => t("poll"), 'base' => 'poll'));
+}
+
+function poll_page() {
+ // List all polls
+ $sql = "SELECT n.nid, n.title, p.active, n.created, SUM(c.chvotes) AS votes FROM {node} n INNER JOIN {poll} p ON n.nid = p.nid INNER JOIN {poll_choices} c ON n.nid = c.nid WHERE n.status = 1 AND n.moderate = 0 GROUP BY n.nid, n.title, p.active, n.created ORDER BY n.created DESC";
+ $sql = db_rewrite_sql($sql);
+ $result = pager_query($sql, 15);
+ $output = '';
+ while ($node = db_fetch_object($result)) {
+ $output .= '- '. l($node->title, "node/$node->nid") .' - '. format_plural($node->votes, '1 vote', '%count votes') .' - '. ($node->active ? t('open') : t('closed')) .'
';
+ }
+ $output .= '
';
+ $output .= theme("pager", NULL, 15);
+ return $output;
+}
+
+/**
+ * Implementation of hook_perm().
+ */
+function poll_perm() {
+ return array('create polls', 'vote on polls');
+}
+
+/**
+ * Creates a simple teaser that lists all the choices.
+ */
+function poll_teaser($node) {
+ $teaser = NULL;
+ if (is_array($node->choice)) {
+ foreach ($node->choice as $k => $choice) {
+ $teaser .= '* '. $choice['chtext'] .'\n';
+ }
+ }
+ return $teaser;
+}
+
+/**
+ * Generates the voting form for a poll.
+ */
+function poll_view_voting(&$node, $teaser, $page, $block) {
+ if ($_POST['op'] == t('Vote')) {
+ poll_vote($node);
+ }
+
+ if ($node->choice) {
+ $list = array();
+ foreach ($node->choice as $i => $choice) {
+ $list[$i] = check_plain($choice['chtext']);
+ }
+ $form['choice'] = array('#type' => 'radios', '#title' => $page ? '' : check_plain($node->title), '#default_value' => -1, '#options' => $list);
+ }
+ $form['nid'] = array('#type' => 'hidden', '#value' => $node->nid);
+ $form['vote'] = array('#type' => 'submit', '#value' => t('Vote'));
+ $form['#action'] = url('node/'. $node->nid);
+ return drupal_get_form('poll_view_voting', $form);
+}
+
+/**
+ * Themes the voting form for a poll.
+ */
+function theme_poll_view_voting($form) {
+ $output .= '';
+ $output .= ' ';
+ $output .= ' ';
+ $output .= form_render($form['choice']);
+ $output .= ' ';
+ $output .= form_render($form['nid']);
+ $output .= form_render($form['vote']);
+ $output .= ' ';
+ $output .= form_render($form);
+ $output .= '';
+ return $output;
+}
+
+/**
+ * Generates a graphical representation of the results of a poll.
+ */
+function poll_view_results(&$node, $teaser, $page, $block) {
+ // Count the votes and find the maximum
+ foreach ($node->choice as $choice) {
+ $total_votes += $choice['chvotes'];
+ $max_votes = max($max_votes, $choice['chvotes']);
+ }
+
+ foreach ($node->choice as $i => $choice) {
+ if ($choice['chtext'] != '') {
+ $poll_results .= theme('poll_bar', check_plain($choice['chtext']), round($choice['chvotes'] * 100 / max($total_votes, 1)), format_plural($choice['chvotes'], '1 vote', '%count votes'), $block);
+ }
+ }
+
+ $output .= theme('poll_results', check_plain($node->title), $poll_results, $total_votes, $node->links, $block);
+
+ return $output;
+}
+
+function theme_poll_results($title, $results, $votes, $links, $block) {
+ if ($block) {
+ $output .= '';
+ $output .= ''. $title .'';
+ $output .= $results;
+ $output .= ''. t('Total votes: %votes', array('%votes' => $votes)) .'';
+ $output .= '';
+ $output .= ''. theme('links', $links) .'';
+ }
+ else {
+ $output .= '';
+ $output .= $results;
+ $output .= ''. t('Total votes: %votes', array('%votes' => $votes)) .'';
+ $output .= '';
+ }
+
+ return $output;
+}
+
+function theme_poll_bar($title, $percentage, $votes, $block) {
+ if ($block) {
+ $output = ''. $title .'';
+ $output .= ' ';
+ $output .= ''. $percentage .'%';
+ }
+ else {
+ $output = ''. $title .'';
+ $output .= ' ';
+ $output .= ''. $percentage .'% ('. $votes .')';
+ }
+
+ return $output;
+}
+
+/**
+ * Callback for the 'results' tab for polls you can vote on
+ */
+function poll_results() {
+ if ($node = node_load(arg(1))) {
+ drupal_set_title(check_plain($node->title));
+ return node_show($node, 0);
+ }
+ else {
+ drupal_not_found();
+ }
+}
+
+/**
+ * Callback for processing a vote
+ */
+function poll_vote(&$node) {
+ global $user;
+ $nid = arg(1);
+
+ if ($node = node_load($nid)) {
+ $edit = $_POST['edit'];
+ $choice = $edit['choice'];
+ $vote = $_POST['vote'];
+
+ if (isset($choice) && isset($node->choice[$choice])) {
+ if ($node->allowvotes) {
+ // Mark the user or host as having voted.
+ if ($user->uid) {
+ db_query('INSERT INTO {poll_votes} (nid, uid) VALUES (%d, %d)', $node->nid, $user->uid);
+ }
+ else {
+ db_query("INSERT INTO {poll_votes} (nid, hostname) VALUES (%d, '%s')", $node->nid, $_SERVER['REMOTE_ADDR']);
+ }
+
+ // Add one to the votes.
+ db_query("UPDATE {poll_choices} SET chvotes = chvotes + 1 WHERE nid = %d AND chorder = %d", $node->nid, $choice);
+
+ $node->allowvotes = FALSE;
+ $node->choice[$choice]['chvotes']++;
+ drupal_set_message(t('Your vote was recorded.'));
+ }
+ else {
+ drupal_set_message(t("You're not allowed to vote on this poll."), 'error');
+ }
+ }
+ else {
+ drupal_set_message(t("You didn't specify a valid poll choice."), 'error');
+ }
+
+ drupal_goto('node/'. $nid);
+ }
+ else {
+ drupal_not_found();
+ }
+}
+
+/**
+ * Implementation of hook_view().
+ *
+ * @param $block
+ * An extra parameter that adapts the hook to display a block-ready
+ * rendering of the poll.
+ */
+function poll_view(&$node, $teaser = FALSE, $page = FALSE, $block = FALSE) {
+ global $user;
+ $output = '';
+
+ // Special display for side-block
+ if ($block) {
+ // No 'read more' link
+ $node->body = $node->teaser = '';
+
+ $links = module_invoke_all('link', 'node', $node, 1);
+ $links[] = l(t('older polls'), 'poll', array('title' => t('View the list of polls on this site.')));
+ if ($node->allowvotes && $block) {
+ $links[] = l(t('results'), 'node/'. $node->nid .'/results', array('title' => t('View the current poll results.')));
+ }
+
+ $node->links = $links;
+ }
+
+ if ($node->allowvotes && ($block || arg(2) != 'results')) {
+ $output .= poll_view_voting($node, $teaser, $page, $block);
+ }
+ else {
+ $output .= poll_view_results($node, $teaser, $page, $block);
+ }
+
+ $node->body = $node->teaser = $output;
+}
+
+/**
+ * Implementation of hook_update().
+ */
+function poll_update($node) {
+ db_query('UPDATE {poll} SET runtime = %d, active = %d WHERE nid = %d', $node->runtime, $node->active, $node->nid);
+
+ db_query('DELETE FROM {poll_choices} WHERE nid = %d', $node->nid);
+ db_query('DELETE FROM {poll_votes} WHERE nid = %d', $node->nid);
+ foreach ($node->choice as $choice) {
+ $chvotes = (int)$choice['chvotes'];
+ $chtext = $choice['chtext'];
+
+ if ($chtext != '') {
+ db_query("INSERT INTO {poll_choices} (nid, chtext, chvotes, chorder) VALUES (%d, '%s', %d, %d)", $node->nid, $chtext, $chvotes, $i++);
+ }
+ }
+}
+
+/**
+ * Implementation of hook_user().
+ */
+function poll_user($op, &$edit, &$user) {
+ if ($op == 'delete') {
+ db_query('UPDATE {poll_votes} SET uid = 0 WHERE uid = %d', $user->uid);
+ }
+}
diff --git a/modules/profile.module b/modules/profile.module
new file mode 100644
index 0000000..30bb492
--- /dev/null
+++ b/modules/profile.module
@@ -0,0 +1,788 @@
+'. t('The profile module allows you to define custom fields (such as country, real name, age, ...) in the user profile. This permits users of a site to share more information about themselves, and can help community-based sites to organize users around profile fields.') .'';
+ $output .= t('The following types of fields can be added to the user profile:
+
+- single-line textfield
+- multi-line textfield
+- checkbox
+- list selection
+- freeform list
+- URL
+- date
+
+');
+ $output .= t('You can
+
+- view user profiles.
+- administer profile settings: administer >> settings >> profiles.
+
+', array('%profile' => url('profile'), '%admin-settings-profile' => url('admin/settings/profile')));
+ $output .= ''. t('For more information please read the configuration and customization handbook Profile page.', array('%profile' => 'http://drupal.org/handbook/modules/profile/')) .'
';
+ return $output;
+ case 'admin/modules#description':
+ return t('Supports configurable user profiles.');
+ case 'admin/settings/profile':
+ return t('Here you can define custom fields that users can fill in in their user profile (such as country, real name, age, ...).
');
+ }
+}
+
+/**
+ * Implementation of hook_menu().
+ */
+function profile_menu($may_cache) {
+ $items = array();
+
+ if ($may_cache) {
+ $items[] = array('path' => 'profile',
+ 'title' => t('user list'),
+ 'callback' => 'profile_browse',
+ 'access' => user_access('access user profiles'),
+ 'type' => MENU_SUGGESTED_ITEM);
+ $items[] = array('path' => 'admin/settings/profile',
+ 'title' => t('profiles'),
+ 'callback' => 'profile_admin_overview');
+ $items[] = array('path' => 'admin/settings/profile/add',
+ 'title' => t('add field'),
+ 'callback' => 'profile_field_form',
+ 'type' => MENU_CALLBACK);
+ $items[] = array('path' => 'admin/settings/profile/edit',
+ 'title' => t('edit field'),
+ 'callback' => 'profile_field_form',
+ 'type' => MENU_CALLBACK);
+ $items[] = array('path' => 'admin/settings/profile/delete',
+ 'title' => t('delete field'),
+ 'callback' => 'profile_field_delete',
+ 'type' => MENU_CALLBACK);
+ }
+
+ return $items;
+}
+
+/**
+ * Implementation of hook_block().
+ */
+function profile_block($op = 'list', $delta = 0, $edit = array()) {
+
+ if ($op == 'list') {
+ $blocks[0]['info'] = t('Author information');
+
+ return $blocks;
+ }
+ else if ($op == 'configure' && $delta == 0) {
+ // Compile a list of fields to show
+ $fields = array();
+ $result = db_query('SELECT name, title, weight, visibility FROM {profile_fields} WHERE visibility IN (%d, %d) ORDER BY weight', PROFILE_PUBLIC, PROFILE_PUBLIC_LISTINGS);
+ while ($record = db_fetch_object($result)) {
+ $fields[$record->name] = $record->title;
+ }
+ $fields['user_profile'] = t('Link to full user profile');
+ $form['profile_block_author_fields'] = array('#type' => 'checkboxes',
+ '#title' => t('Profile fields to display'),
+ '#default_value' => variable_get('profile_block_author_fields', NULL),
+ '#options' => $fields,
+ '#description' => t('Select which profile fields you wish to display in the block. Only fields designated as public in the profile field configuration are available.', array('%profile-admin' => url('admin/settings/profile'))),
+ );
+ return $form;
+ }
+ else if ($op == 'save' && $delta == 0) {
+ variable_set('profile_block_author_fields', $edit['profile_block_author_fields']);
+ }
+ else if ($op == 'view') {
+ if (user_access('access user profiles')) {
+ if ((arg(0) == 'node') && is_numeric(arg(1)) && (arg(2) == NULL)) {
+ $node = node_load(arg(1));
+ $account = user_load(array('uid' => $node->uid));
+
+ if ($use_fields = variable_get('profile_block_author_fields', array())) {
+ // Compile a list of fields to show.
+ $fields = array();
+ $result = db_query('SELECT name, title, type, visibility, weight FROM {profile_fields} WHERE visibility IN (%d, %d) ORDER BY weight', PROFILE_PUBLIC, PROFILE_PUBLIC_LISTINGS);
+ while ($record = db_fetch_object($result)) {
+ // Ensure that field is displayed only if it is among the defined block fields and, if it is private, the user has appropriate permissions.
+ if (isset($use_fields[$record->name]) && $use_fields[$record->name]) {
+ $fields[] = $record;
+ }
+ }
+ }
+
+ if ($fields) {
+ $fields = _profile_update_user_fields($fields, $account);
+ $output .= theme('profile_block', $account, $fields, true);
+ }
+
+ if (isset($use_fields['user_profile']) && $use_fields['user_profile']) {
+ $output .= '' . l(t('View full user profile'), 'user/' . $account->uid) . '';
+ }
+ }
+
+ if ($output) {
+ $block['subject'] = t('About %name', array('%name' => $account->name));
+ $block['content'] = $output;
+ return $block;
+ }
+ }
+ }
+}
+
+/**
+ * Implementation of hook_user().
+ */
+function profile_user($type, &$edit, &$user, $category = NULL) {
+ switch ($type) {
+ case 'load':
+ return profile_load_profile($user);
+ case 'register':
+ return profile_form_profile($edit, $user, $category);
+ case 'update':
+ case 'insert':
+ return profile_save_profile($edit, $user, $category);
+ case 'view':
+ return profile_view_profile($user);
+ case 'form':
+ return profile_form_profile($edit, $user, $category);
+ case 'validate':
+ return profile_validate_profile($edit, $category);
+ case 'categories':
+ return profile_categories();
+ case 'delete':
+ db_query('DELETE FROM {profile_values} WHERE uid = %d', $user->uid);
+ }
+}
+
+/**
+ * Menu callback: Generate a form to add/edit a user profile field.
+ */
+function profile_field_form($arg = NULL) {
+ if (arg(3) == 'edit') {
+ if (is_numeric($arg)) {
+ $fid = $arg;
+
+ $edit = db_fetch_array(db_query('SELECT * FROM {profile_fields} WHERE fid = %d', $fid));
+
+ if (!$edit) {
+ drupal_not_found();
+ return;
+ }
+ drupal_set_title(t('edit %title', array('%title' => $edit['title'])));
+ $form['fid'] = array('#type' => 'value',
+ '#value' => $fid,
+ );
+ $type = $edit['type'];
+ }
+ else {
+ drupal_not_found();
+ return;
+ }
+ }
+ else {
+ $types = _profile_field_types();
+ if (!isset($types[$arg])) {
+ drupal_not_found();
+ return;
+ }
+ $type = $arg;
+ drupal_set_title(t('add new %type', array('%type' => $types[$type])));
+ $edit = array('name' => 'profile_');
+ $form['type'] = array('#type' => 'value', '#value' => $type);
+ }
+ $form['fields'] = array('#type' => 'fieldset',
+ '#title' => t('Field settings'),
+ );
+ $form['fields']['category'] = array('#type' => 'textfield',
+ '#title' => t('Category'),
+ '#default_value' => $edit['category'],
+ '#description' => t('The category the new field should be part of. Categories are used to group fields logically. An example category is "Personal information".'),
+ '#required' => TRUE,
+ );
+ $form['fields']['title'] = array('#type' => 'textfield',
+ '#title' => t('Title'),
+ '#default_value' => $edit['title'],
+ '#description' => t('The title of the new field. The title will be shown to the user. An example title is "Favorite color".'),
+ '#required' => TRUE,
+ );
+ $form['fields']['name'] = array('#type' => 'textfield',
+ '#title' => t('Form name'),
+ '#default_value' => $edit['name'],
+ '#description' => t('The name of the field. The form name is not shown to the user but used internally in the HTML code and URLs.
+Unless you know what you are doing, it is highly recommended that you prefix the form name with profile_
to avoid name clashes with other fields. Spaces or any other special characters except dash (-) and underscore (_) are not allowed. An example name is "profile_favorite_color" or perhaps just "profile_color".'),
+ '#required' => TRUE,
+ );
+ $form['fields']['explanation'] = array('#type' => 'textarea',
+ '#title' => t('Explanation'),
+ '#default_value' => $edit['explanation'],
+ '#description' => t('An optional explanation to go with the new field. The explanation will be shown to the user.'),
+ );
+ if ($type == 'selection') {
+ $form['fields']['options'] = array('#type' => 'textarea',
+ '#title' => t('Selection options'),
+ '#default_value' => $edit['options'],
+ '#description' => t('A list of all options. Put each option on a separate line. Example options are "red", "blue", "green", etc.'),
+ );
+ }
+ $form['fields']['weight'] = array('#type' => 'weight',
+ '#title' => t('Weight'),
+ '#default_value' => $edit['weight'],
+ '#delta' => 5,
+ '#description' => t('The weights define the order in which the form fields are shown. Lighter fields "float up" towards the top of the category.'),
+ );
+ $form['fields']['visibility'] = array('#type' => 'radios',
+ '#title' => t('Visibility'),
+ '#default_value' => isset($edit['visibility']) ? $edit['visibility'] : PROFILE_PUBLIC,
+ '#options' => array(PROFILE_HIDDEN => t('Hidden profile field, only accessible by administrators, modules and themes.'), PROFILE_PRIVATE => t('Private field, content only available to privileged users.'), PROFILE_PUBLIC => t('Public field, content shown on profile page but not used on member list pages.'), PROFILE_PUBLIC_LISTINGS => t('Public field, content shown on profile page and on member list pages.')),
+ );
+ if ($type == 'selection' || $type == 'list' || $type == 'textfield') {
+ $form['fields']['page'] = array('#type' => 'textfield',
+ '#title' => t('Page title'),
+ '#default_value' => $edit['page'],
+ '#description' => t('To enable browsing this field by value, enter a title for the resulting page. The word %value
will be substituted with the corresponding value. An example page title is "People whose favorite color is %value". This is only applicable for a public field.'),
+ );
+ }
+ else if ($type == 'checkbox') {
+ $form['fields']['page'] = array('#type' => 'textfield',
+ '#title' => t('Page title'),
+ '#default_value' => $edit['page'],
+ '#description' => t('To enable browsing this field by value, enter a title for the resulting page. An example page title is "People who are employed". This is only applicable for a public field.'),
+ );
+ }
+ $form['fields']['required'] = array('#type' => 'checkbox',
+ '#title' => t('The user must enter a value.'),
+ '#default_value' => $edit['required'],
+ );
+ $form['fields']['register'] = array('#type' => 'checkbox',
+ '#title' => t('Visible in user registration form.'),
+ '#default_value' => $edit['register'],
+ );
+ $form['submit'] = array('#type' => 'submit',
+ '#value' => t('Save field'),
+ );
+ return drupal_get_form('profile_field_form', $form);
+}
+
+/**
+ * Validate profile_field_form submissions.
+ */
+function profile_field_form_validate($form_id, $form_values) {
+ // Validate the 'field name':
+ if (eregi('[^a-z0-9_-]', $form_values['name'])) {
+ form_set_error('name', t('The specified form name contains one or more illegal characters. Spaces or any other special characters expect dash (-) and underscore (_) are not allowed.'));
+ }
+
+ if (in_array($form_values['name'], user_fields())) {
+ form_set_error('name', t('The specified form name is reserved for use by Drupal.'));
+ }
+ // Validate the category:
+ if (!$form_values['category']) {
+ form_set_error('category', t('You must enter a category.'));
+ }
+ if ($form_values['category'] == 'account') {
+ form_set_error('category', t('The specified category name is reserved for use by Drupal.'));
+ }
+ $args1 = array($form_values['title'], $form_values['category']);
+ $args2 = array($form_values['name']);
+ $query_suffix = '';
+
+ if (isset($form_values['fid'])) {
+ $args1[] = $args2[] = $form_values['fid'];
+ $query_suffix = ' AND fid != %d';
+ }
+
+ if (db_result(db_query("SELECT fid FROM {profile_fields} WHERE title = '%s' AND category = '%s'". $query_suffix, $args1))) {
+ form_set_error('title', t('The specified title is already in use.'));
+ }
+ if (db_result(db_query("SELECT fid FROM {profile_fields} WHERE name = '%s'". $query_suffix, $args2))) {
+ form_set_error('name', t('The specified name is already in use.'));
+ }
+}
+
+/**
+ * Process profile_field_form submissions.
+ */
+function profile_field_form_submit($form_id, $form_values) {
+ if (!isset($form_values['fid'])) {
+ db_query("INSERT INTO {profile_fields} (title, name, explanation, category, type, weight, required, register, visibility, options, page) VALUES ('%s', '%s', '%s', '%s', '%s', %d, %d, %d, %d, '%s', '%s')", $form_values['title'], $form_values['name'], $form_values['explanation'], $form_values['category'], $form_values['type'], $form_values['weight'], $form_values['required'], $form_values['register'], $form_values['visibility'], $form_values['options'], $form_values['page']);
+
+ drupal_set_message(t('The field has been created.'));
+ watchdog('profile', t('Profile field %field added under category %category.', array('%field' => theme('placeholder', $form_values['title']), '%category' => theme('placeholder', $form_values['category']))), WATCHDOG_NOTICE, l(t('view'), 'admin/settings/profile'));
+ }
+ else {
+ db_query("UPDATE {profile_fields} SET title = '%s', name = '%s', explanation = '%s', category = '%s', weight = %d, required = %d, register = %d, visibility = %d, options = '%s', page = '%s' WHERE fid = %d", $form_values['title'], $form_values['name'], $form_values['explanation'], $form_values['category'], $form_values['weight'], $form_values['required'], $form_values['register'], $form_values['visibility'], $form_values['options'], $form_values['page'], $form_values['fid']);
+
+ drupal_set_message(t('The field has been updated.'));
+ }
+ cache_clear_all();
+
+ return 'admin/settings/profile';
+}
+
+/**
+ * Menu callback; deletes a field from all user profiles.
+ */
+function profile_field_delete($fid) {
+ $field = db_fetch_object(db_query("SELECT title FROM {profile_fields} WHERE fid = %d", $fid));
+ if (!$field) {
+ drupal_not_found();
+ return;
+ }
+ $form['fid'] = array('#type' => 'value', '#value' => $fid);
+ $form['title'] = array('#type' => 'value', '#value' => $field->title);
+
+ return confirm_form('profile_field_delete', $form, t('Are you sure you want to delete the field %field?', array('%field' => theme('placeholder', $field->title))), 'admin/settings/profile', t('This action cannot be undone. If users have entered values into this field in their profile, these entries will also be deleted. If you want to keep the user-entered data, instead of deleting the field you may wish to edit this field and change it to a %hidden-field so that it may only be accessed by administrators.', array('%edit-field' => url('admin/settings/profile/edit/' . $fid), '%hidden-field' => theme('placeholder', t('hidden profile field')))), t('Delete'), t('Cancel'));
+}
+
+/**
+ * Process a field delete form submission.
+ */
+function profile_field_delete_submit($form_id, $form_values) {
+ db_query('DELETE FROM {profile_fields} WHERE fid = %d', $form_values['fid']);
+ db_query('DELETE FROM {profile_values} WHERE fid = %d', $form_values['fid']);
+
+ cache_clear_all();
+
+ drupal_set_message(t('The field %field has been deleted.', array('%field' => theme('placeholder', $form_values['title']))));
+ watchdog('profile', t('Profile field %field deleted.', array('%field' => theme('placeholder', $form_values['title']))), WATCHDOG_NOTICE, l(t('view'), 'admin/settings/profile'));
+
+ return 'admin/settings/profile';
+}
+
+/**
+ * Menu callback; display a listing of all editable profile fields.
+ */
+function profile_admin_overview() {
+
+ $result = db_query('SELECT * FROM {profile_fields} ORDER BY category, weight');
+ $rows = array();
+ while ($field = db_fetch_object($result)) {
+ $rows[] = array(check_plain($field->title), $field->name, _profile_field_types($field->type), $field->category, l(t('edit'), "admin/settings/profile/edit/$field->fid"), l(t('delete'), "admin/settings/profile/delete/$field->fid"));
+ }
+ if (count($rows) == 0) {
+ $rows[] = array(array('data' => t('No fields defined.'), 'colspan' => '6'));
+ }
+
+ $header = array(t('Title'), t('Name'), t('Type'), t('Category'), array('data' => t('Operations'), 'colspan' => '2'));
+
+ $output = theme('table', $header, $rows);
+ $output .= ''. t('Add new field') .'
';
+ $output .= '';
+ foreach (_profile_field_types() as $key => $value) {
+ $output .= '- '. l($value, "admin/settings/profile/add/$key") .'
';
+ }
+ $output .= '
';
+
+ return $output;
+}
+
+/**
+ * Menu callback; display a list of user information.
+ */
+function profile_browse() {
+
+ $name = arg(1);
+ list(,,$value) = explode('/', $_GET['q'], 3);
+
+ $field = db_fetch_object(db_query("SELECT DISTINCT(fid), type, title, page, visibility FROM {profile_fields} WHERE name = '%s'", $name));
+
+ if ($name && $field->fid) {
+ // Only allow browsing of fields that have a page title set.
+ if (empty($field->page)) {
+ drupal_not_found();
+ return;
+ }
+ // Do not allow browsing of private fields by non-admins.
+ if (!user_access('administer users') && $field->visibility == PROFILE_PRIVATE) {
+ drupal_access_denied();
+ return;
+ }
+
+ // Compile a list of fields to show.
+ $fields = array();
+ $result = db_query('SELECT name, title, type, weight FROM {profile_fields} WHERE fid != %d AND visibility = %d ORDER BY weight', $field->fid, PROFILE_PUBLIC_LISTINGS);
+ while ($record = db_fetch_object($result)) {
+ $fields[] = $record;
+ }
+
+ // Determine what query to use:
+ $arguments = array($field->fid);
+ switch ($field->type) {
+ case 'checkbox':
+ $query = 'v.value = 1';
+ break;
+ case 'textfield':
+ case 'selection':
+ $query = "v.value = '%s'";
+ $arguments[] = $value;
+ break;
+ case 'list':
+ $query = "v.value LIKE '%%%s%%'";
+ $arguments[] = $value;
+ break;
+ default:
+ drupal_not_found();
+ return;
+ }
+
+ // Extract the affected users:
+ $result = pager_query("SELECT u.uid, u.access FROM {users} u INNER JOIN {profile_values} v ON u.uid = v.uid WHERE v.fid = %d AND $query ORDER BY u.access DESC", 20, 0, NULL, $arguments);
+
+ $output = '';
+ while ($account = db_fetch_object($result)) {
+ $account = user_load(array('uid' => $account->uid));
+ $profile = _profile_update_user_fields($fields, $account);
+ $output .= theme('profile_listing', $account, $profile);
+ }
+ $output .= theme('pager', NULL, 20);
+
+ if ($field->type == 'selection' || $field->type == 'list' || $field->type == 'textfield') {
+ $title = strtr($field->page, array('%value' => theme('placeholder', $value)));
+ }
+ else {
+ $title = $field->page;
+ }
+ $output .= '';
+
+ drupal_set_title($title);
+ return $output;
+ }
+ else if ($name && !$field->fid) {
+ drupal_not_found();
+ }
+ else {
+ // Compile a list of fields to show.
+ $fields = array();
+ $result = db_query('SELECT name, title, type, weight FROM {profile_fields} WHERE visibility = %d ORDER BY category, weight', PROFILE_PUBLIC_LISTINGS);
+ while ($record = db_fetch_object($result)) {
+ $fields[] = $record;
+ }
+
+ // Extract the affected users:
+ $result = pager_query("SELECT uid, access FROM {users} WHERE uid > 0 AND status != 0 ORDER BY access DESC", 20, 0, NULL);
+
+ $output = '';
+ while ($account = db_fetch_object($result)) {
+ $account = user_load(array('uid' => $account->uid));
+ $profile = _profile_update_user_fields($fields, $account);
+ $output .= theme('profile_listing', $account, $profile);
+ }
+ $output .= '';
+ $output .= theme('pager', NULL, 20);
+
+ drupal_set_title(t('user list'));
+ return $output;
+ }
+}
+
+function profile_load_profile(&$user) {
+ $result = db_query('SELECT f.name, f.type, v.value FROM {profile_fields} f INNER JOIN {profile_values} v ON f.fid = v.fid WHERE uid = %d', $user->uid);
+ while ($field = db_fetch_object($result)) {
+ if (empty($user->{$field->name})) {
+ $user->{$field->name} = _profile_field_serialize($field->type) ? unserialize($field->value) : $field->value;
+ }
+ }
+}
+
+function profile_save_profile(&$edit, &$user, $category) {
+ if ((arg(0) == 'user' && arg(1) == 'register') || (arg(0) == 'admin' && arg(1) == 'user' && arg(2) == 'create')) {
+ $result = db_query('SELECT fid, name, type FROM {profile_fields} WHERE register = 1 AND visibility != %d ORDER BY category, weight', PROFILE_HIDDEN);
+ }
+ else {
+ $result = db_query("SELECT fid, name, type FROM {profile_fields} WHERE LOWER(category) = LOWER('%s') AND visibility != %d", $category, PROFILE_HIDDEN);
+ // Use LOWER('%s') instead of PHP's strtolower() to avoid UTF-8 conversion issues.
+ }
+ while ($field = db_fetch_object($result)) {
+ if (_profile_field_serialize($field->type)) {
+ $edit[$field->name] = serialize($edit[$field->name]);
+ }
+ db_query("DELETE FROM {profile_values} WHERE fid = %d AND uid = %d", $field->fid, $user->uid);
+ db_query("INSERT INTO {profile_values} (fid, uid, value) VALUES (%d, %d, '%s')", $field->fid, $user->uid, $edit[$field->name]);
+ // Mark field as handled (prevents saving to user->data).
+ $edit[$field->name] = NULL;
+ }
+}
+
+function profile_view_field($user, $field) {
+ // Only allow browsing of private fields for admins, if browsing is enabled,
+ // and if a user has permission to view profiles. Note that this check is
+ // necessary because a user may always see their own profile.
+ $browse = user_access('access user profiles')
+ && (user_access('administer users') || $field->visibility != PROFILE_PRIVATE)
+ && !empty($field->page);
+
+ if ($value = $user->{$field->name}) {
+ switch ($field->type) {
+ case 'textarea':
+ return check_markup($value);
+ case 'textfield':
+ case 'selection':
+ return $browse ? l($value, 'profile/'. $field->name .'/'. $value) : check_plain($value);
+ case 'checkbox':
+ return $browse ? l($field->title, 'profile/'. $field->name) : check_plain($field->title);
+ case 'url':
+ return ''. check_plain($value) .'';
+ case 'date':
+ $format = substr(variable_get('date_format_short', 'm/d/Y - H:i'), 0, 5);
+ // Note: Avoid PHP's date() because it does not handle dates before
+ // 1970 on Windows. This would make the date field useless for e.g.
+ // birthdays.
+ $replace = array('d' => sprintf('%02d', $value['day']),
+ 'j' => $value['day'],
+ 'm' => sprintf('%02d', $value['month']),
+ 'M' => map_month($value['month']),
+ 'Y' => $value['year'],
+ 'H:i' => null,
+ 'g:ia' => null);
+ return strtr($format, $replace);
+ case 'list':
+ $values = split("[,\n\r]", $value);
+ $fields = array();
+ foreach ($values as $value) {
+ if ($value = trim($value)) {
+ $fields[] = $browse ? l($value, 'profile/'. $field->name .'/'. $value) : check_plain($value);
+ }
+ }
+ return implode(', ', $fields);
+ }
+ }
+}
+
+function profile_view_profile($user) {
+
+ profile_load_profile($user);
+
+ // Show private fields to administrators and people viewing their own account.
+ if (user_access('administer users') || $GLOBALS['user']->uid == $user->uid) {
+ $result = db_query('SELECT * FROM {profile_fields} WHERE visibility != %d ORDER BY category, weight', PROFILE_HIDDEN);
+ }
+ else {
+ $result = db_query('SELECT * FROM {profile_fields} WHERE visibility != %d AND visibility != %d ORDER BY category, weight', PROFILE_PRIVATE, PROFILE_HIDDEN);
+ }
+
+ while ($field = db_fetch_object($result)) {
+ if ($value = profile_view_field($user, $field)) {
+ $description = ($field->visibility == PROFILE_PRIVATE) ? t('The content of this field is private and only visible to yourself.') : '';
+ $title = ($field->type != 'checkbox') ? check_plain($field->title) : NULL;
+ $item = array('title' => $title,
+ 'value' => $value,
+ 'class' => $field->name,
+ );
+ $fields[$field->category][] = $item;
+ }
+ }
+ return $fields;
+}
+
+function _profile_form_explanation($field) {
+ $output = $field->explanation;
+
+ if ($field->type == 'list') {
+ $output .= ' '. t('Put each item on a separate line or separate them by commas. No HTML allowed.');
+ }
+
+ if ($field->visibility == PROFILE_PRIVATE) {
+ $output .= ' '. t('The content of this field is kept private and will not be shown publicly.');
+ }
+
+ return $output;
+}
+
+function profile_form_profile($edit, $user, $category) {
+ if (arg(0) == 'user' && arg(1) == 'register') {
+ $result = db_query('SELECT * FROM {profile_fields} WHERE visibility != %d AND register = 1 ORDER BY category, weight', PROFILE_HIDDEN);
+ }
+ elseif (arg(0) == 'admin' && arg(1) == 'user' && arg(2) == 'create') {
+ $result = db_query('SELECT * FROM {profile_fields} WHERE register = 1 ORDER BY category, weight');
+ }
+ elseif (user_access('administer users')) {
+ $result = db_query("SELECT * FROM {profile_fields} WHERE LOWER(category) = LOWER('%s') ORDER BY weight", $category);
+ }
+ else {
+ $result = db_query("SELECT * FROM {profile_fields} WHERE visibility != %d AND LOWER(category) = LOWER('%s') ORDER BY weight", PROFILE_HIDDEN, $category);
+ // Use LOWER('%s') instead of PHP's strtolower() to avoid UTF-8 conversion issues.
+ }
+
+ while ($field = db_fetch_object($result)) {
+ $category = $field->category;
+ if (!isset($fields[$category])) {
+ $fields[$category] = array('#type' => 'fieldset', '#title' => $category, '#weight' => $w++);
+ }
+ switch ($field->type) {
+ case 'textfield':
+ case 'url':
+ $fields[$category][$field->name] = array('#type' => 'textfield',
+ '#title' => check_plain($field->title),
+ '#default_value' => $edit[$field->name],
+ '#maxlength' => 255,
+ '#description' => _profile_form_explanation($field),
+ '#required' => $field->required,
+ );
+ break;
+ case 'textarea':
+ $fields[$category][$field->name] = array('#type' => 'textarea',
+ '#title' => check_plain($field->title),
+ '#default_value' => $edit[$field->name],
+ '#description' => _profile_form_explanation($field),
+ '#required' => $field->required,
+ );
+ break;
+ case 'list':
+ $fields[$category][$field->name] = array('#type' => 'textarea',
+ '#title' => check_plain($field->title),
+ '#default_value' => $edit[$field->name],
+ '#description' => _profile_form_explanation($field),
+ '#required' => $field->required,
+ );
+ break;
+ case 'checkbox':
+ $fields[$category][$field->name] = array('#type' => 'checkbox',
+ '#title' => check_plain($field->title),
+ '#default_value' => $edit[$field->name],
+ '#description' => _profile_form_explanation($field),
+ '#required' => $field->required,
+ );
+ break;
+ case 'selection':
+ $options = $field->required ? array() : array('--');
+ $lines = split("[,\n\r]", $field->options);
+ foreach ($lines as $line) {
+ if ($line = trim($line)) {
+ $options[$line] = $line;
+ }
+ }
+ $fields[$category][$field->name] = array('#type' => 'select',
+ '#title' => check_plain($field->title),
+ '#default_value' => $edit[$field->name],
+ '#options' => $options,
+ '#description' => _profile_form_explanation($field),
+ '#required' => $field->required,
+ );
+ break;
+ case 'date':
+ $fields[$category][$field->name] = array('#type' => 'date',
+ '#title' => check_plain($field->title),
+ '#default_value' => $edit[$field->name],
+ '#description' => _profile_form_explanation($field),
+ '#required' => $field->required,
+ );
+ break;
+ }
+ }
+ return $fields;
+}
+
+/**
+ * Helper function: update an array of user fields by calling profile_view_field
+ */
+function _profile_update_user_fields($fields, $account) {
+ foreach ($fields as $key => $field) {
+ $fields[$key]->value = profile_view_field($account, $field);
+ }
+ return $fields;
+}
+
+function profile_validate_profile($edit, $category) {
+ if (arg(0) == 'user' && arg(1) == 'register') {
+ $result = db_query('SELECT * FROM {profile_fields} WHERE visibility != %d AND register = 1 ORDER BY category, weight', PROFILE_HIDDEN);
+ }
+ elseif (arg(0) == 'admin' && arg(1) == 'user' && arg(2) == 'create') {
+ $result = db_query('SELECT * FROM {profile_fields} WHERE register = 1 ORDER BY category, weight');
+ }
+ elseif (user_access('administer users')) {
+ $result = db_query("SELECT * FROM {profile_fields} WHERE LOWER(category) = LOWER('%s') ORDER BY weight", $category);
+ }
+ else {
+ $result = db_query("SELECT * FROM {profile_fields} WHERE visibility != %d AND LOWER(category) = LOWER('%s') ORDER BY weight", PROFILE_HIDDEN, $category);
+ // Use LOWER('%s') instead of PHP's strtolower() to avoid UTF-8 conversion issues.
+ }
+
+ while ($field = db_fetch_object($result)) {
+ if ($edit[$field->name]) {
+ if ($field->type == 'url') {
+ if (!valid_url($edit[$field->name], true)) {
+ form_set_error($field->name, t('The value provided for %field is not a valid URL.', array('%field' => theme('placeholder', $field->title))));
+ }
+ }
+ }
+ else if ($field->required && !user_access('administer users')) {
+ form_set_error($field->name, t('The field %field is required.', array('%field' => theme('placeholder', $field->title))));
+ }
+ }
+
+ return $edit;
+}
+
+function profile_categories() {
+ $result = db_query("SELECT DISTINCT(category) FROM {profile_fields}");
+ while ($category = db_fetch_object($result)) {
+ $data[] = array('name' => check_plain($category->category), 'title' => $category->category, 'weight' => 3);
+ }
+ return $data;
+}
+
+function theme_profile_block($account, $fields = array()) {
+
+ $output .= theme('user_picture', $account);
+
+ foreach ($fields as $field) {
+ if ($field->value) {
+ if ($field->type == 'checkbox') {
+ $output .= "$field->value
\n";
+ }
+ else {
+ $output .= "$field->title
$field->value
\n";
+ }
+ }
+ }
+
+ return $output;
+}
+
+function theme_profile_listing($account, $fields = array()) {
+
+ $output = "\n";
+ $output .= theme('user_picture', $account);
+ $output .= ' '. theme('username', $account) ."\n";
+
+ foreach ($fields as $field) {
+ if ($field->value) {
+ $output .= " $field->value\n";
+ }
+ }
+
+ $output .= "\n";
+
+ return $output;
+}
+
+function _profile_field_types($type = NULL) {
+ $types = array('textfield' => t('single-line textfield'),
+ 'textarea' => t('multi-line textfield'),
+ 'checkbox' => t('checkbox'),
+ 'selection' => t('list selection'),
+ 'list' => t('freeform list'),
+ 'url' => t('URL'),
+ 'date' => t('date'));
+ return isset($type) ? $types[$type] : $types;
+}
+
+function _profile_field_serialize($type = NULL) {
+ return $type == 'date';
+}
diff --git a/modules/search.module b/modules/search.module
new file mode 100644
index 0000000..5c4c1e9
--- /dev/null
+++ b/modules/search.module
@@ -0,0 +1,1262 @@
+'. t('The search module adds the ability to search for content by keywords. Search is often the only practical way to find content on a large site. Search is useful for finding users and posts by searching on keywords.') .'';
+ $output .= ''. t('The search engine works by maintaining an index of the words in your site\'s content. It indexes the posts and users. You can adjust the settings to tweak the indexing behaviour. Note that the search requires cron to be set up correctly. The index percentage sets the maximum amount of items that will be indexed in one cron run. Set this number lower if your cron is timing out or if PHP is running out of memory.') .'
';
+ $output .= t('You can
+
+- read about how your site uses cron in the administer >> help >> system.
+- run your cron.php.
+- read about configuring cron jobs.
+- administer >> settings >> search.
+', array('%admin-help-system' => url('admin/help/system'), '%file-cron' => 'cron.php', '%external-http-drupal-org-node-23714' => 'http://drupal.org/node/23714', '%admin-settings-search' => url('admin/settings/search')));
+ $output .= ''. t('For more information please read the configuration and customization handbook Search page.', array('%search' => 'http://drupal.org/handbook/modules/search/')) .'
';
+ return $output;
+ case 'admin/modules#description':
+ return t('Enables site-wide keyword searching.');
+ case 'admin/settings/search':
+ return t('
+The search engine works by maintaining an index of the words in your site\'s content. You can adjust the settings below to tweak the indexing behaviour. Note that the search requires cron to be set up correctly.
+');
+ case 'search#noresults':
+ return t('
+- Check if your spelling is correct.
+- Remove quotes around phrases to match each word individually: "blue smurf" will match less than blue smurf.
+- Consider loosening your query with OR: blue smurf will match less than blue OR smurf.
+
');
+ }
+}
+
+/**
+ * Implementation of hook_perm().
+ */
+function search_perm() {
+ return array('search content', 'administer search');
+}
+
+/**
+ * Implementation of hook_block().
+ */
+function search_block($op = 'list', $delta = 0) {
+ if ($op == 'list') {
+ $blocks[0]['info'] = t('Search form');
+ return $blocks;
+ }
+ else if ($op == 'view' && user_access('search content')) {
+ $block['content'] = search_box('search_block_form');
+ $block['subject'] = t('Search');
+ return $block;
+ }
+}
+
+/**
+ * Implementation of hook_menu().
+ */
+function search_menu($may_cache) {
+ $items = array();
+
+ if ($may_cache) {
+ $items[] = array('path' => 'search', 'title' => t('search'),
+ 'callback' => 'search_view',
+ 'access' => user_access('search content'),
+ 'type' => MENU_SUGGESTED_ITEM);
+ $items[] = array('path' => 'admin/settings/search/wipe', 'title' => t('Clear index'),
+ 'callback' => 'search_wipe_confirm',
+ 'access' => user_access('administer search'),
+ 'type' => MENU_CALLBACK);
+ }
+ else if (arg(0) == 'search') {
+ // To remember the user's search keywords when switching across tabs,
+ // we dynamically add the keywords to the search tabs' paths.
+ $keys = search_get_keys();
+ $keys = strlen($keys) ? '/'. $keys : '';
+ foreach (module_list() as $name) {
+ if (module_hook($name, 'search') && $title = module_invoke($name, 'search', 'name')) {
+ $items[] = array('path' => 'search/'. $name . $keys, 'title' => $title,
+ 'callback' => 'search_view',
+ 'access' => user_access('search content'),
+ 'type' => MENU_LOCAL_TASK);
+ }
+ }
+ }
+
+ return $items;
+}
+
+/**
+ * Implementation of hook_validate().
+ */
+function search_settings_form_validate($form_id, &$form) {
+ if ($_POST['op'] == t('Re-index site')) {
+ drupal_goto('admin/settings/search/wipe');
+ }
+ // If these settings change, the index needs to be rebuilt.
+ if ((variable_get('minimum_word_size', 3) != $form['minimum_word_size']) ||
+ (variable_get('overlap_cjk', true) != $form['overlap_cjk'])) {
+ drupal_set_message(t('The index will be rebuilt.'));
+ search_wipe();
+ }
+}
+
+/**
+ * Menu callback; displays the search module settings page.
+ */
+function search_settings() {
+ // Collect some stats
+ $remaining = 0;
+ $total = 0;
+ foreach (module_list() as $module) {
+ if (module_hook($module, 'search')) {
+ $status = module_invoke($module, 'search', 'status');
+ $remaining += $status['remaining'];
+ $total += $status['total'];
+ }
+ }
+ $count = format_plural($remaining, 'There is 1 item left to index.', 'There are %count items left to index.');
+ $percentage = ((int)min(100, 100 * ($total - $remaining) / max(1, $total))) . '%';
+ $status = ''. t('%percentage of the site has been indexed.', array('%percentage' => $percentage)) .' '. $count .'
';
+ $form['status'] = array('#type' => 'fieldset', '#title' => t('Indexing status'));
+ $form['status']['status'] = array('#type' => 'markup', '#value' => $status);
+ $form['status']['wipe'] = array('#type' => 'submit', '#value' => t('Re-index site'));
+
+ $items = drupal_map_assoc(array(10, 20, 50, 100, 200, 500));
+
+ // Indexing throttle:
+ $form['indexing_throttle'] = array('#type' => 'fieldset', '#title' => t('Indexing throttle'));
+ $form['indexing_throttle']['search_cron_limit'] = array('#type' => 'select', '#title' => t('Items to index per cron run'), '#default_value' => variable_get('search_cron_limit', 100), '#options' => $items, '#description' => t('The maximum amount of items that will be indexed in one cron run. Set this number lower if your cron is timing out or if PHP is running out of memory.'));
+ // Indexing settings:
+ $form['indexing_settings'] = array('#type' => 'fieldset', '#title' => t('Indexing settings'));
+ $form['indexing_settings']['info'] = array('#type' => 'markup', '#value' => ''. t('Changing the settings below will cause the site index to be rebuilt. The search index is not cleared but systematically updated to reflect the new settings. Searching will continue to work but new content won\'t be indexed until all existing content has been re-indexed.
The default settings should be appropriate for the majority of sites.
') .'');
+ $form['indexing_settings']['minimum_word_size'] = array('#type' => 'textfield', '#title' => t('Minimum word length to index'), '#default_value' => variable_get('minimum_word_size', 3), '#size' => 5, '#maxlength' => 3, '#description' => t('The number of characters a word has to be to be indexed. A lower setting means better search result ranking, but also a larger database. Each search query must contain at least one keyword that is this size (or longer).'));
+ $form['indexing_settings']['overlap_cjk'] = array('#type' => 'checkbox', '#title' => t('Simple CJK handling'), '#default_value' => variable_get('overlap_cjk', true), '#description' => t('Whether to apply a simple Chinese/Japanese/Korean tokenizer based on overlapping sequences. Turn this off if you want to use an external preprocessor for this instead. Does not affect other languages.'));
+
+ // Per module settings
+ $form = array_merge($form, module_invoke_all('search', 'admin'));
+ return $form;
+}
+
+/**
+ * Menu callback: confirm wiping of the index.
+ */
+function search_wipe_confirm() {
+ return confirm_form('search_wipe_confirm', $form, t('Are you sure you want to re-index the site?'),
+ 'admin/settings/search', t(' The search index is not cleared but systematically updated to reflect the new settings. Searching will continue to work but new content won\'t be indexed until all existing content has been re-indexed. This action cannot be undone.'), t('Re-index site'), t('Cancel'));
+}
+
+/**
+ * Handler for wipe confirmation
+ */
+function search_wipe_confirm_submit($form_id, &$form) {
+ if ($form['confirm']) {
+ search_wipe();
+ drupal_set_message(t('The index will be rebuilt.'));
+ return 'admin/settings/search';
+ }
+}
+
+/**
+ * Wipes a part of or the entire search index.
+ *
+ * @param $sid
+ * (optional) The SID of the item to wipe. If specified, $type must be passed
+ * too.
+ * @param $type
+ * (optional) The type of item to wipe.
+ */
+function search_wipe($sid = NULL, $type = NULL, $reindex = FALSE) {
+ if ($type == NULL && $sid == NULL) {
+ module_invoke_all('search', 'reset');
+ }
+ else {
+ db_query("DELETE FROM {search_dataset} WHERE sid = %d AND type = '%s'", $sid, $type);
+ db_query("DELETE FROM {search_index} WHERE fromsid = %d AND fromtype = '%s'", $sid, $type);
+ // When re-indexing, keep link references
+ db_query("DELETE FROM {search_index} WHERE sid = %d AND type = '%s'". ($reindex ? " AND fromsid = 0" : ''), $sid, $type);
+ }
+}
+
+/**
+ * Marks a word as dirty (or retrieves the list of dirty words). This is used
+ * during indexing (cron). Words which are dirty have outdated total counts in
+ * the search_total table, and need to be recounted.
+ */
+function search_dirty($word = null) {
+ static $dirty = array();
+ if ($word !== null) {
+ $dirty[$word] = true;
+ }
+ else {
+ return $dirty;
+ }
+}
+
+/**
+ * Implementation of hook_cron().
+ *
+ * Fires hook_update_index() in all modules and cleans up dirty words (see
+ * search_dirty).
+ */
+function search_cron() {
+ // We register a shutdown function to ensure that search_total is always up
+ // to date.
+ register_shutdown_function('search_update_totals');
+
+ // Update word index
+ foreach (module_list() as $module) {
+ module_invoke($module, 'update_index');
+ }
+}
+
+/**
+ * This function is called on shutdown to ensure that search_total is always
+ * up to date (even if cron times out or otherwise fails).
+ */
+function search_update_totals() {
+ // Update word IDF (Inverse Document Frequency) counts for new/changed words
+ foreach (search_dirty() as $word => $dummy) {
+ // Get total count
+ $total = db_result(db_query("SELECT SUM(score) FROM {search_index} WHERE word = '%s'", $word));
+ // Apply Zipf's law to equalize the probability distribution
+ $total = log10(1 + 1/(max(1, $total)));
+ db_query("UPDATE {search_total} SET count = %f WHERE word = '%s'", $total, $word);
+ if (!db_affected_rows()) {
+ db_query("INSERT INTO {search_total} (word, count) VALUES ('%s', %f)", $word, $total);
+ }
+ }
+ // Find words that were deleted from search_index, but are still in
+ // search_total. We use a LEFT JOIN between the two tables and keep only the
+ // rows which fail to join.
+ $result = db_query("SELECT t.word AS realword, i.word FROM {search_total} t LEFT JOIN {search_index} i ON t.word = i.word WHERE i.word IS NULL");
+ while ($word = db_fetch_object($result)) {
+ db_query("DELETE FROM {search_total} WHERE word = '%s'", $word->realword);
+ }
+}
+
+/**
+ * Simplifies a string according to indexing rules.
+ */
+function search_simplify($text) {
+ // Decode entities to UTF-8
+ $text = decode_entities($text);
+
+ // Lowercase
+ $text = drupal_strtolower($text);
+
+ // Call an external processor for word handling.
+ search_preprocess($text);
+
+ // Simple CJK handling
+ if (variable_get('overlap_cjk', true)) {
+ $text = preg_replace_callback('/['. PREG_CLASS_CJK .']+/u', 'search_expand_cjk', $text);
+ }
+
+ // To improve searching for numerical data such as dates, IP addresses
+ // or version numbers, we consider a group of numerical characters
+ // separated only by punctuation characters to be one piece.
+ // This also means that searching for e.g. '20/03/1984' also returns
+ // results with '20-03-1984' in them.
+ // Readable regexp: ([number]+)[punctuation]+(?=[number])
+ $text = preg_replace('/(['. PREG_CLASS_NUMBERS .']+)['. PREG_CLASS_PUNCTUATION .']+(?=['. PREG_CLASS_NUMBERS .'])/u', '\1', $text);
+
+ // The dot, underscore and dash are simply removed. This allows meaningful
+ // search behaviour with acronyms and URLs.
+ $text = preg_replace('/[._-]+/', '', $text);
+
+ // With the exception of the rules above, we consider all punctuation,
+ // marks, spacers, etc, to be a word boundary.
+ $text = preg_replace('/['. PREG_CLASS_SEARCH_EXCLUDE . ']+/u', ' ', $text);
+
+ return $text;
+}
+
+/**
+ * Basic CJK tokenizer. Simply splits a string into consecutive, overlapping
+ * sequences of characters ('minimum_word_size' long).
+ */
+function search_expand_cjk($matches) {
+ $min = variable_get('minimum_word_size', 3);
+ $str = $matches[0];
+ $l = drupal_strlen($str);
+ // Passthrough short words
+ if ($l <= $min) {
+ return ' '. $str .' ';
+ }
+ $tokens = ' ';
+ // FIFO queue of characters
+ $chars = array();
+ // Begin loop
+ for ($i = 0; $i < $l; ++$i) {
+ // Grab next character
+ $current = drupal_substr($str, 0, 1);
+ $str = substr($str, strlen($current));
+ $chars[] = $current;
+ if ($i >= $min - 1) {
+ $tokens .= implode('', $chars) .' ';
+ array_shift($chars);
+ }
+ }
+ return $tokens;
+}
+
+/**
+ * Splits a string into tokens for indexing.
+ */
+function search_index_split($text) {
+ static $last = null;
+ static $lastsplit = null;
+
+ if ($last == $text) {
+ return $lastsplit;
+ }
+ // Process words
+ $text = search_simplify($text);
+ $words = explode(' ', $text);
+ array_walk($words, '_search_index_truncate');
+
+ // Save last keyword result
+ $last = $text;
+ $lastsplit = $words;
+
+ return $words;
+}
+
+/**
+ * Helper function for array_walk in search_index_split.
+ */
+function _search_index_truncate(&$text) {
+ $text = truncate_utf8($text, 50);
+}
+
+/**
+ * Invokes hook_search_preprocess() in modules.
+ */
+function search_preprocess(&$text) {
+ foreach (module_implements('search_preprocess') as $module) {
+ $text = module_invoke($module, 'search_preprocess', $text);
+ }
+}
+
+/**
+ * Update the full-text search index for a particular item.
+ *
+ * @param $sid
+ * A number identifying this particular item (e.g. node id).
+ *
+ * @param $type
+ * A string defining this type of item (e.g. 'node')
+ *
+ * @param $text
+ * The content of this item. Must be a piece of HTML text.
+ *
+ * @ingroup search
+ */
+function search_index($sid, $type, $text) {
+ $minimum_word_size = variable_get('minimum_word_size', 3);
+
+ // Link matching
+ global $base_url;
+ $node_regexp = '@href=[\'"]?(?:'. preg_quote($base_url, '@') .'/|'. preg_quote(base_path(), '@') .')(?:\?q=)?/?((?![a-z]+:)[^\'">]+)[\'">]@i';
+
+ // Multipliers for scores of words inside certain HTML tags.
+ // Note: 'a' must be included for link ranking to work.
+ $tags = array('h1' => 25,
+ 'h2' => 18,
+ 'h3' => 15,
+ 'h4' => 12,
+ 'h5' => 9,
+ 'h6' => 6,
+ 'u' => 3,
+ 'b' => 3,
+ 'i' => 3,
+ 'strong' => 3,
+ 'em' => 3,
+ 'a' => 10);
+
+ // Strip off all ignored tags to speed up processing, but insert space before/after
+ // them to keep word boundaries.
+ $text = str_replace(array('<', '>'), array(' <', '> '), $text);
+ $text = strip_tags($text, '<'. implode('><', array_keys($tags)) .'>');
+
+ // Split HTML tags from plain text.
+ $split = preg_split('/\s*<([^>]+?)>\s*/', $text, -1, PREG_SPLIT_DELIM_CAPTURE);
+ // Note: PHP ensures the array consists of alternating delimiters and literals
+ // and begins and ends with a literal (inserting $null as required).
+
+ $tag = false; // Odd/even counter. Tag or no tag.
+ $link = false; // State variable for link analyser
+ $score = 1; // Starting score per word
+ $accum = ' '; // Accumulator for cleaned up data
+ $tagstack = array(); // Stack with open tags
+ $tagwords = 0; // Counter for consecutive words
+ $focus = 1; // Focus state
+
+ $results = array(0 => array()); // Accumulator for words for index
+
+ foreach ($split as $value) {
+ if ($tag) {
+ // Increase or decrease score per word based on tag
+ list($tagname) = explode(' ', $value, 2);
+ $tagname = drupal_strtolower($tagname);
+ // Closing or opening tag?
+ if ($tagname[0] == '/') {
+ $tagname = substr($tagname, 1);
+ // If we encounter unexpected tags, reset score to avoid incorrect boosting.
+ if (!count($tagstack) || $tagstack[0] != $tagname) {
+ $tagstack = array();
+ $score = 1;
+ }
+ else {
+ // Remove from tag stack and decrement score
+ $score = max(1, $score - $tags[array_shift($tagstack)]);
+ }
+ if ($tagname == 'a') {
+ $link = false;
+ }
+ }
+ else {
+ if ($tagstack[0] == $tagname) {
+ // None of the tags we look for make sense when nested identically.
+ // If they are, it's probably broken HTML.
+ $tagstack = array();
+ $score = 1;
+ }
+ else {
+ // Add to open tag stack and increment score
+ array_unshift($tagstack, $tagname);
+ $score += $tags[$tagname];
+ }
+ if ($tagname == 'a') {
+ // Check if link points to a node on this site
+ if (preg_match($node_regexp, $value, $match)) {
+ $path = drupal_get_normal_path($match[1]);
+ if (preg_match('!(?:node|book)/(?:view/)?([0-9]+)!i', $path, $match)) {
+ $linknid = $match[1];
+ if ($linknid > 0) {
+ // Note: ignore links to uncachable nodes to avoid redirect bugs.
+ $node = db_fetch_object(db_query('SELECT n.title, n.nid, n.vid, r.format FROM {node} n INNER JOIN {node_revisions} r ON n.vid = r.vid WHERE n.nid = %d', $linknid));
+ if (filter_format_allowcache($node->format)) {
+ $link = true;
+ $linktitle = $node->title;
+ }
+ }
+ }
+ }
+ }
+ }
+ // A tag change occurred, reset counter.
+ $tagwords = 0;
+ }
+ else {
+ // Note: use of PREG_SPLIT_DELIM_CAPTURE above will introduce empty values
+ if ($value != '') {
+ if ($link) {
+ // Check to see if the node link text is its URL. If so, we use the target node title instead.
+ if (preg_match('!^https?://!i', $value)) {
+ $value = $linktitle;
+ }
+ }
+ $words = search_index_split($value);
+ foreach ($words as $word) {
+ // Add word to accumulator
+ $accum .= $word .' ';
+ $num = is_numeric($word);
+ // Check wordlength
+ if ($num || drupal_strlen($word) >= $minimum_word_size) {
+ // Normalize numbers
+ if ($num) {
+ $word = (int)ltrim($word, '-0');
+ }
+
+ if ($link) {
+ if (!isset($results[$linknid])) {
+ $results[$linknid] = array();
+ }
+ $results[$linknid][$word] += $score * $focus;
+ }
+ else {
+ $results[0][$word] += $score * $focus;
+ // Focus is a decaying value in terms of the amount of unique words up to this point.
+ // From 100 words and more, it decays, to e.g. 0.5 at 500 words and 0.3 at 1000 words.
+ $focus = min(1, .01 + 3.5 / (2 + count($results[0]) * .015));
+ }
+ }
+ $tagwords++;
+ // Too many words inside a single tag probably mean a tag was accidentally left open.
+ if (count($tagstack) && $tagwords >= 15) {
+ $tagstack = array();
+ $score = 1;
+ }
+ }
+ }
+ }
+ $tag = !$tag;
+ }
+
+ search_wipe($sid, $type, TRUE);
+
+ // Insert cleaned up data into dataset
+ db_query("INSERT INTO {search_dataset} (sid, type, data) VALUES (%d, '%s', '%s')", $sid, $type, $accum);
+
+ // Insert results into search index
+ foreach ($results[0] as $word => $score) {
+ db_query("INSERT INTO {search_index} (word, sid, type, score) VALUES ('%s', %d, '%s', %f)", $word, $sid, $type, $score);
+ search_dirty($word);
+ }
+ unset($results[0]);
+
+ // Now insert links to nodes
+ foreach ($results as $nid => $words) {
+ foreach ($words as $word => $score) {
+ db_query("INSERT INTO {search_index} (word, sid, type, fromsid, fromtype, score) VALUES ('%s', %d, '%s', %d, '%s', %f)", $word, $nid, 'node', $sid, $type, $score);
+ search_dirty($word);
+ }
+ }
+}
+
+/**
+ * Extract a module-specific search option from a search query. e.g. 'type:book'
+ */
+function search_query_extract($keys, $option) {
+ if (preg_match('/(^| )'. $option .':([^ ]*)( |$)/i', $keys, $matches)) {
+ return $matches[2];
+ }
+}
+
+/**
+ * Return a query with the given module-specific search option inserted in.
+ * e.g. 'type:book'.
+ */
+function search_query_insert($keys, $option, $value = '') {
+ if (search_query_extract($keys, $option)) {
+ $keys = trim(preg_replace('/(^| )'. $option .':[^ ]*/i', '', $keys));
+ }
+ if ($value != '') {
+ $keys .= ' '. $option .':'. $value;
+ }
+ return $keys;
+}
+
+/**
+ * Parse a search query into SQL conditions.
+ *
+ * We build a query that matches the dataset bodies.
+ */
+function search_parse_query($text) {
+ $keys = array('positive' => array(), 'negative' => array());
+
+ // Tokenize query string
+ preg_match_all('/ (-?)("[^"]+"|[^" ]+)/i', ' '. $text, $matches, PREG_SET_ORDER);
+
+ if (count($matches) < 1) {
+ return NULL;
+ }
+
+ // Classify tokens
+ $or = false;
+ foreach ($matches as $match) {
+ $phrase = false;
+ // Strip off phrase quotes
+ if ($match[2]{0} == '"') {
+ $match[2] = substr($match[2], 1, -1);
+ $phrase = true;
+ }
+ // Simplify keyword according to indexing rules and external preprocessors
+ $words = search_simplify($match[2]);
+ // Re-explode in case simplification added more words, except when matching a phrase
+ $words = $phrase ? array($words) : preg_split('/ /', $words, -1, PREG_SPLIT_NO_EMPTY);
+ // Negative matches
+ if ($match[1] == '-') {
+ $keys['negative'] = array_merge($keys['negative'], $words);
+ }
+ // OR operator: instead of a single keyword, we store an array of all
+ // OR'd keywords.
+ elseif ($match[2] == 'OR' && count($keys['positive'])) {
+ $last = array_pop($keys['positive']);
+ // Starting a new OR?
+ if (!is_array($last)) {
+ $last = array($last);
+ }
+ $keys['positive'][] = $last;
+ $or = true;
+ continue;
+ }
+ // Plain keyword
+ else {
+ if ($or) {
+ // Add to last element (which is an array)
+ $keys['positive'][count($keys['positive']) - 1] = array_merge($keys['positive'][count($keys['positive']) - 1], $words);
+ }
+ else {
+ $keys['positive'] = array_merge($keys['positive'], $words);
+ }
+ }
+ $or = false;
+ }
+
+ // Convert keywords into SQL statements.
+ $query = array();
+ $query2 = array();
+ $arguments = array();
+ $arguments2 = array();
+ $matches = 0;
+ // Positive matches
+ foreach ($keys['positive'] as $key) {
+ // Group of ORed terms
+ if (is_array($key) && count($key)) {
+ $queryor = array();
+ $any = false;
+ foreach ($key as $or) {
+ list($q, $count) = _search_parse_query($or, $arguments2);
+ $any |= $count;
+ if ($q) {
+ $queryor[] = $q;
+ $arguments[] = $or;
+ }
+ }
+ if (count($queryor)) {
+ $query[] = '('. implode(' OR ', $queryor) .')';
+ // A group of OR keywords only needs to match once
+ $matches += ($any > 0);
+ }
+ }
+ // Single ANDed term
+ else {
+ list($q, $count) = _search_parse_query($key, $arguments2);
+ if ($q) {
+ $query[] = $q;
+ $arguments[] = $key;
+ // Each AND keyword needs to match at least once
+ $matches += $count;
+ }
+ }
+ }
+ // Negative matches
+ foreach ($keys['negative'] as $key) {
+ list($q) = _search_parse_query($key, $arguments2, true);
+ if ($q) {
+ $query[] = $q;
+ $arguments[] = $key;
+ }
+ }
+ $query = implode(' AND ', $query);
+
+ // Build word-index conditions for the first pass
+ $query2 = substr(str_repeat("i.word = '%s' OR ", count($arguments2)), 0, -4);
+
+ return array($query, $arguments, $query2, $arguments2, $matches);
+}
+
+/**
+ * Helper function for search_parse_query();
+ */
+function _search_parse_query(&$word, &$scores, $not = false) {
+ $count = 0;
+ // Determine the scorewords of this word/phrase
+ if (!$not) {
+ $split = explode(' ', $word);
+ foreach ($split as $s) {
+ $num = is_numeric($s);
+ if ($num || drupal_strlen($s) >= variable_get('minimum_word_size', 3)) {
+ $s = $num ? ((int)ltrim($s, '-0')) : $s;
+ if (!isset($scores[$s])) {
+ $scores[$s] = $s;
+ $count++;
+ }
+ }
+ }
+ }
+ // Return matching snippet and number of added words
+ return array("d.data ". ($not ? 'NOT ' : '') ."LIKE '%% %s %%'", $count);
+}
+
+/**
+ * Do a query on the full-text search index for a word or words.
+ *
+ * This function is normally only called by each module that support the
+ * indexed search (and thus, implements hook_update_index()).
+ *
+ * Two queries are performed which can be extended by the caller.
+ *
+ * The first query selects a set of possible matches based on the search index
+ * and any extra given restrictions. This is the classic "OR" search.
+ *
+ * SELECT i.type, i.sid, SUM(i.score*t.count) AS relevance
+ * FROM {search_index} i
+ * INNER JOIN {search_total} t ON i.word = t.word
+ * $join1
+ * WHERE $where1 AND (...)
+ * GROUP BY i.type, i.sid
+ *
+ * The second query further refines this set by verifying advanced text
+ * conditions (such as AND, negative or phrase matches), and orders the results
+ * on a the column or expression 'score':
+ *
+ * SELECT i.type, i.sid, $select2
+ * FROM temp_search_sids i
+ * INNER JOIN {search_dataset} d ON i.sid = d.sid AND i.type = d.type
+ * $join2
+ * WHERE (...)
+ * ORDER BY score DESC
+ *
+ * @param $keywords
+ * A search string as entered by the user.
+ *
+ * @param $type
+ * A string identifying the calling module.
+ *
+ * @param $join1
+ * (optional) Inserted into the JOIN part of the first SQL query.
+ * For example "INNER JOIN {node} n ON n.nid = i.sid".
+ *
+ * @param $where1
+ * (optional) Inserted into the WHERE part of the first SQL query.
+ * For example "(n.status > %d)".
+ *
+ * @param $arguments1
+ * (optional) Extra SQL arguments belonging to the first query.
+ *
+ * @param $select2
+ * (optional) Inserted into the SELECT pat of the second query. Must contain
+ * a column selected as 'score'.
+ * defaults to 'i.relevance AS score'
+ *
+ * @param $join2
+ * (optional) Inserted into the JOIN par of the second SQL query.
+ * For example "INNER JOIN {node_comment_statistics} n ON n.nid = i.sid"
+ *
+ * @param $arguments2
+ * (optional) Extra SQL arguments belonging to the second query parameter.
+ *
+ * @param $sort_parameters
+ * (optional) SQL arguments for sorting the final results.
+ * Default: 'ORDER BY score DESC'
+ *
+ * @return
+ * An array of SIDs for the search results.
+ *
+ * @ingroup search
+ */
+function do_search($keywords, $type, $join1 = '', $where1 = '1', $arguments1 = array(), $select2 = 'i.relevance AS score', $join2 = '', $arguments2 = array(), $sort_parameters = 'ORDER BY score DESC') {
+ $query = search_parse_query($keywords);
+
+ if ($query[2] == '') {
+ form_set_error('keys', t('You must include at least one positive keyword with %count characters or more.', array('%count' => variable_get('minimum_word_size', 3))));
+ }
+ if ($query === NULL || $query[0] == '' || $query[2] == '') {
+ return array();
+ }
+
+ // First pass: select all possible matching sids, doing a simple index-based OR matching on the keywords.
+ // 'matches' is used to reject those items that cannot possibly match the query.
+ $conditions = $where1 .' AND ('. $query[2] .") AND i.type = '%s'";
+ $arguments = array_merge($arguments1, $query[3], array($type, $query[4]));
+ $result = db_query_temporary("SELECT i.type, i.sid, SUM(i.score * t.count) AS relevance, COUNT(*) AS matches FROM {search_index} i INNER JOIN {search_total} t ON i.word = t.word $join1 WHERE $conditions GROUP BY i.type, i.sid HAVING COUNT(*) >= %d", $arguments, 'temp_search_sids');
+
+ // Calculate maximum relevance, to normalize it
+ $normalize = db_result(db_query('SELECT MAX(relevance) FROM temp_search_sids'));
+ if (!$normalize) {
+ return array();
+ }
+ $select2 = str_replace('i.relevance', '('. (1.0 / $normalize) .' * i.relevance)', $select2);
+
+ // Second pass: only keep items that match the complicated keywords conditions (phrase search, negative keywords, ...)
+ $conditions = '('. $query[0] .')';
+ $arguments = array_merge($arguments2, $query[1]);
+ $result = db_query_temporary("SELECT i.type, i.sid, $select2 FROM temp_search_sids i INNER JOIN {search_dataset} d ON i.sid = d.sid AND i.type = d.type $join2 WHERE $conditions $sort_parameters", $arguments, 'temp_search_results');
+ if (($count = db_result(db_query('SELECT COUNT(*) FROM temp_search_results'))) == 0) {
+ return array();
+ }
+ $count_query = "SELECT $count";
+
+ // Do actual search query
+ $result = pager_query("SELECT * FROM temp_search_results", 10, 0, $count_query);
+ $results = array();
+ while ($item = db_fetch_object($result)) {
+ $results[] = $item;
+ }
+ return $results;
+}
+
+/**
+ * Helper function for grabbing search keys.
+ */
+function search_get_keys() {
+ // Extract keys as remainder of path
+ // Note: support old GET format of searches for existing links.
+ $path = explode('/', $_GET['q'], 3);
+ return count($path) == 3 ? $path[2] : $_REQUEST['keys'];
+}
+
+/**
+ * Menu callback; presents the search form and/or search results.
+ */
+function search_view() {
+ $type = arg(1);
+
+ // Search form submits with POST but redirects to GET. This way we can keep
+ // the search query URL clean as a whistle:
+ // search/type/keyword+keyword
+ if (!isset($_POST['edit']['form_id'])) {
+ if ($type == '') {
+ // Note: search/node can not be a default tab because it would take on the
+ // path of its parent (search). It would prevent remembering keywords when
+ // switching tabs. This is why we drupal_goto to it from the parent instead.
+ drupal_goto('search/node');
+ }
+
+ $keys = search_get_keys();
+ // Only perform search if there is non-whitespace search term:
+ if (trim($keys)) {
+ // Log the search keys:
+ watchdog('search', t('Search: %keys (%type).', array('%keys' => theme('placeholder', $keys), '%type' => module_invoke($type, 'search', 'name'))), WATCHDOG_NOTICE, l(t('results'), 'search/'. $type .'/'. $keys));
+
+ // Collect the search results:
+ $results = search_data($keys, $type);
+
+ if ($results) {
+ $results = theme('box', t('Search results'), $results);
+ }
+ else {
+ $results = theme('box', t('Your search yielded no results'), search_help('search#noresults'));
+ }
+ }
+
+ // Construct the search form.
+ $output = search_form(NULL, $keys, $type);
+ $output .= $results;
+
+ return $output;
+ }
+
+ return search_form(NULL, $keys, $type);
+}
+
+/**
+ * @defgroup search Search interface
+ * @{
+ * The Drupal search interface manages a global search mechanism.
+ *
+ * Modules may plug into this system to provide searches of different types of
+ * data. Most of the system is handled by search.module, so this must be enabled
+ * for all of the search features to work.
+ *
+ * There are three ways to interact with the search system:
+ * - Specifically for searching nodes, you can implement nodeapi('update index')
+ * and nodeapi('search result'). However, note that the search system already
+ * indexes all visible output of a node, i.e. everything displayed normally
+ * by hook_view() and hook_nodeapi('view'). This is usually sufficient.
+ * You should only use this mechanism if you want additional, non-visible data
+ * to be indexed.
+ * - Implement hook_search(). This will create a search tab for your module on
+ * the /search page with a simple keyword search form. You may optionally
+ * implement hook_search_item() to customize the display of your results.
+ * - Implement hook_update_index(). This allows your module to use Drupal's
+ * HTML indexing mechanism for searching full text efficiently.
+ *
+ * If your module needs to provide a more complicated search form, then you need
+ * to implement it yourself without hook_search(). In that case, you should
+ * define it as a local task (tab) under the /search page (e.g. /search/mymodule)
+ * so that users can easily find it.
+ */
+
+/**
+ * Render a search form.
+ *
+ * @param $action
+ * Form action. Defaults to "search".
+ * @param $keys
+ * The search string entered by the user, containing keywords for the search.
+ * @param $type
+ * The type of search to render the node for. Must be the name of module
+ * which implements hook_search(). Defaults to 'node'.
+ * @param $prompt
+ * A piece of text to put before the form (e.g. "Enter your keywords")
+ * @return
+ * An HTML string containing the search form.
+ */
+function search_form($action = '', $keys = '', $type = NULL, $prompt = NULL) {
+ if (!$action) {
+ $action = url('search/'. $type);
+ }
+ if (is_null($prompt)) {
+ $prompt = t('Enter your keywords');
+ }
+
+ $form = array(
+ '#action' => $action,
+ '#attributes' => array('class' => 'search-form'),
+ );
+ $form['module'] = array('#type' => 'value', '#value' => $type);
+ $form['basic'] = array('#type' => 'item', '#title' => $prompt);
+ $form['basic']['inline'] = array('#prefix' => '', '#suffix' => '');
+ $form['basic']['inline']['keys'] = array(
+ '#type' => 'textfield',
+ '#title' => '',
+ '#default_value' => $keys,
+ '#size' => $prompt ? 40 : 20,
+ '#maxlength' => 255,
+ );
+ // processed_keys is used to coordinate keyword passing between other forms
+ // that hook into the basic search form.
+ $form['basic']['inline']['processed_keys'] = array('#type' => 'value', '#value' => array());
+ $form['basic']['inline']['submit'] = array('#type' => 'submit', '#value' => t('Search'));
+
+ return drupal_get_form('search_form', $form);
+}
+
+/**
+ * As the search form collates keys from other modules hooked in via
+ * hook_form_alter, the validation takes place in _submit.
+ * search_form_validate() is used solely to set the 'processed_keys' form
+ * value for the basic search form.
+ */
+function search_form_validate($form_id, $form_values, $form) {
+ form_set_value($form['basic']['inline']['processed_keys'], trim($form_values['keys']));
+}
+
+/**
+ * Process a search form submission.
+ */
+function search_form_submit($form_id, $form_values) {
+ $keys = $form_values['processed_keys'];
+ if ($keys == '') {
+ form_set_error('keys', t('Please enter some keywords.'));
+ // Fall through to the drupal_goto() call.
+ }
+
+ $type = $form_values['module'] ? $form_values['module'] : 'node';
+ return 'search/'. $type .'/'. $keys;
+}
+
+/**
+ * Output a search form for the search block and the theme's search box.
+ */
+function search_box($form_id = 'search_theme_form') {
+ // Use search_keys instead of keys to avoid ID conflicts with the search block.
+ $form[$form_id .'_keys'] = array(
+ '#type' => 'textfield',
+ '#size' => 15,
+ '#default_value' => '',
+ '#attributes' => array('title' => t('Enter the terms you wish to search for.')),
+ );
+ $form['submit'] = array('#type' => 'submit', '#value' => t('Search'));
+
+ return drupal_get_form($form_id, $form, 'search_box_form');
+}
+
+/**
+ * Process a block search form submission.
+ */
+function search_box_form_submit($form_id, $form_values) {
+ return 'search/node/'. trim($form_values[$form_id .'_keys']);
+}
+
+/**
+ * Theme the theme search form.
+ */
+function theme_search_theme_form($form) {
+ return ''. form_render($form) .'';
+}
+
+/**
+ * Theme the block search form.
+ */
+function theme_search_block_form($form) {
+ return ''. form_render($form) .'';
+}
+
+/**
+ * Perform a standard search on the given keys, and return the formatted results.
+ */
+function search_data($keys = NULL, $type = 'node') {
+ if (isset($keys)) {
+ if (module_hook($type, 'search')) {
+ $results = module_invoke($type, 'search', 'search', $keys);
+ if (isset($results) && is_array($results) && count($results)) {
+ if (module_hook($type, 'search_page')) {
+ return module_invoke($type, 'search_page', $results);
+ }
+ else {
+ return theme('search_page', $results, $type);
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Returns snippets from a piece of text, with certain keywords highlighted.
+ * Used for formatting search results.
+ *
+ * @param $keys
+ * A string containing a search query.
+ *
+ * @param $text
+ * The text to extract fragments from.
+ *
+ * @return
+ * A string containing HTML for the excerpt.
+ */
+function search_excerpt($keys, $text) {
+ // We highlight around non-indexable or CJK characters.
+ $boundary = '(?:(?<=['. PREG_CLASS_SEARCH_EXCLUDE . PREG_CLASS_CJK .'])|(?=['. PREG_CLASS_SEARCH_EXCLUDE . PREG_CLASS_CJK .']))';
+
+ // Extract positive keywords and phrases
+ preg_match_all('/ ("([^"]+)"|(?!OR)([^" ]+))/', ' '. $keys, $matches);
+ $keys = array_merge($matches[2], $matches[3]);
+
+ // Prepare text
+ $text = ' '. strip_tags(str_replace(array('<', '>'), array(' <', '> '), $text)) .' ';
+ array_walk($keys, '_search_excerpt_replace');
+ $workkeys = $keys;
+
+ // Extract a fragment per keyword for at most 4 keywords.
+ // First we collect ranges of text around each keyword, starting/ending
+ // at spaces.
+ // If the sum of all fragments is too short, we look for second occurrences.
+ $ranges = array();
+ $included = array();
+ $length = 0;
+ while ($length < 256 && count($workkeys)) {
+ foreach ($workkeys as $k => $key) {
+ if (strlen($key) == 0) {
+ unset($workkeys[$k]);
+ unset($keys[$k]);
+ continue;
+ }
+ if ($length >= 256) {
+ break;
+ }
+ // Remember occurrence of key so we can skip over it if more occurrences
+ // are desired.
+ if (!isset($included[$key])) {
+ $included[$key] = 0;
+ }
+ // Locate a keyword (position $p), then locate a space in front (position
+ // $q) and behind it (position $s)
+ if (preg_match('/'. $boundary . $key . $boundary .'/iu', $text, $match, PREG_OFFSET_CAPTURE, $included[$key])) {
+ $p = $match[0][1];
+ if (($q = strpos($text, ' ', max(0, $p - 60))) !== false) {
+ $end = substr($text, $p, 80);
+ if (($s = strrpos($end, ' ')) !== false) {
+ $ranges[$q] = $p + $s;
+ $length += $p + $s - $q;
+ $included[$key] = $p + 1;
+ }
+ else {
+ unset($workkeys[$k]);
+ }
+ }
+ else {
+ unset($workkeys[$k]);
+ }
+ }
+ else {
+ unset($workkeys[$k]);
+ }
+ }
+ }
+
+ // If we didn't find anything, return the beginning.
+ if (count($ranges) == 0) {
+ return truncate_utf8($text, 256) . ' ...';
+ }
+
+ // Sort the text ranges by starting position.
+ ksort($ranges);
+
+ // Now we collapse overlapping text ranges into one. The sorting makes it O(n).
+ $newranges = array();
+ foreach ($ranges as $from2 => $to2) {
+ if (!isset($from1)) {
+ $from1 = $from2;
+ $to1 = $to2;
+ continue;
+ }
+ if ($from2 <= $to1) {
+ $to1 = max($to1, $to2);
+ }
+ else {
+ $newranges[$from1] = $to1;
+ $from1 = $from2;
+ $to1 = $to2;
+ }
+ }
+ $newranges[$from1] = $to1;
+
+ // Fetch text
+ $out = array();
+ foreach ($newranges as $from => $to) {
+ $out[] = substr($text, $from, $to - $from);
+ }
+ $text = (isset($newranges[0]) ? '' : '... '). implode(' ... ', $out) .' ...';
+
+ // Highlight keywords. Must be done at once to prevent conflicts ('strong' and '').
+ $text = preg_replace('/'. $boundary .'('. implode('|', $keys) .')'. $boundary .'/iu', '\0', $text);
+ return $text;
+}
+
+/**
+ * @} End of "defgroup search".
+ */
+
+/**
+ * Helper function for array_walk in search_except.
+ */
+function _search_excerpt_replace(&$text) {
+ $text = preg_quote($text, '/');
+}
+
+/**
+ * Format a single result entry of a search query. This function is normally
+ * called by theme_search_page() or hook_search_page().
+ *
+ * @param $item
+ * A single search result as returned by hook_search(). The result should be
+ * an array with keys "link", "title", "type", "user", "date", and "snippet".
+ * Optionally, "extra" can be an array of extra info to show along with the
+ * result.
+ * @param $type
+ * The type of item found, such as "user" or "node".
+ *
+ * @ingroup themeable
+ */
+function theme_search_item($item, $type) {
+ $output = ' - '. check_plain($item['title']) .'
';
+ $info = array();
+ if ($item['type']) {
+ $info[] = $item['type'];
+ }
+ if ($item['user']) {
+ $info[] = $item['user'];
+ }
+ if ($item['date']) {
+ $info[] = format_date($item['date'], 'small');
+ }
+ if (is_array($item['extra'])) {
+ $info = array_merge($info, $item['extra']);
+ }
+ $output .= ' - '. ($item['snippet'] ? '
'. $item['snippet'] . '
' : '') . '' . implode(' - ', $info) .'
';
+ return $output;
+}
+
+/**
+ * Format the result page of a search query.
+ *
+ * Modules may implement hook_search_page() in order to override this default
+ * function to display search results. In that case it is expected they provide
+ * their own themeable functions.
+ *
+ * @param $results
+ * All search result as returned by hook_search().
+ * @param $type
+ * The type of item found, such as "user" or "node".
+ *
+ * @ingroup themeable
+ */
+function theme_search_page($results, $type) {
+ $output = '';
+
+ foreach ($results as $entry) {
+ $output .= theme('search_item', $entry, $type);
+ }
+ $output .= '
';
+ $output .= theme('pager', NULL, 10, 0);
+
+ return $output;
+}
diff --git a/modules/statistics.module b/modules/statistics.module
new file mode 100644
index 0000000..e5a594a
--- /dev/null
+++ b/modules/statistics.module
@@ -0,0 +1,495 @@
+'. t('The statistics module keeps track of numerous statistics of site usage. It counts how many times, and from where each of your posts is viewed. The statistics module can be used to learn many useful things about how users are interacting with each other and with your site.') .'';
+ $output .= t('Statistics module features
+
+- Logs show statistics for how many times your site and specific content on your site has been accessed.
+- Referrers tells you from where visitors came from (referrer URL).
+- Top pages shows you what\'s hot, what is the most popular content on your site.
+- Top users shows you the most active users for your site.
+- Recent hits displays information about the latest activity on your site.
+- Node count displays the number of times a node has been accessed in the node\'s link section next to # comments.
+- Popular content block creates a block that can display the day\'s top viewed content, the all time top viewed content, and the last content viewed.
+
+');
+ $output .= t('Configuring the statistics module
+
+- Enable access log allows you to turn the access log on and off. This log is used to store data about every page accessed, such as the remote host\'s IP address, where they came from (referrer), what node they\'ve viewed, and their user name. Enabling the log adds one database call per page displayed by Drupal.
+- Discard access logs older than allows you to configure how long an access log entry is saved, after which time it is deleted from the database table. To use this you need to run cron.php
+- Enable node view counter allows you to turn on and off the node-counting functionality of this module. If it is turned on, an extra database query is added for each node displayed, which increments a counter.
+
+');
+ $output .= t('You can
+
+- administer statistics administer >> settings >> statistics.
+- access statistics logs administer >> logs.
+- view recent hits administer >> logs >> recent hits.
+- enable \'popular content\' block in block administration administer >> blocks but only after you have enabled \'Count content views\' in settings.
+
+', array('%admin-settings-statistics' => url('admin/settings/statistics'), '%admin-logs' => url('admin/logs'), '%admin-logs-hits' => url('admin/logs/hits'), '%admin-block' => url('admin/block')));
+ $output .= ''. t('For more information please read the configuration and customization handbook Statistics page.', array('%statistics' => 'http://drupal.org/handbook/modules/statistics/')) .'
';
+ return $output;
+ case 'admin/modules#description':
+ return t('Logs access statistics for your site.');
+ case 'admin/settings/statistics':
+ return t('Settings for the statistical information that Drupal will keep about the site. See site statistics for the actual information.
', array('%statistics' => url('admin/logs/hits')));
+ case 'admin/logs/hits':
+ return t('This page shows you the most recent hits.
');
+ case 'admin/logs/referrers':
+ return t('This page shows you all external referrers. These are links pointing to your web site from outside your web site.
');
+ case 'admin/logs/visitors':
+ return t('When you ban a visitor, you prevent his IP address from accessing your site. Unlike blocking a user, banning a visitor works even for anonymous users. The most common use for this is to block bots/web crawlers that are consuming too many resources.
');
+ }
+}
+
+/**
+ * Implementation of hook_exit().
+ *
+ * This is where statistics are gathered on page accesses.
+ */
+function statistics_exit() {
+ global $user, $recent_activity;
+
+ drupal_bootstrap(DRUPAL_BOOTSTRAP_PATH);
+
+ if (variable_get('statistics_count_content_views', 0)) {
+ // We are counting content views.
+ if ((arg(0) == 'node') && is_numeric(arg(1)) && arg(2) == '') {
+ // A node has been viewed, so update the node's counters.
+ db_query('UPDATE {node_counter} SET daycount = daycount + 1, totalcount = totalcount + 1, timestamp = %d WHERE nid = %d', time(), arg(1));
+ // If we affected 0 rows, this is the first time viewing the node.
+ if (!db_affected_rows()) {
+ // We must create a new row to store counters for the new node.
+ db_query('INSERT INTO {node_counter} (nid, daycount, totalcount, timestamp) VALUES (%d, 1, 1, %d)', arg(1), time());
+ }
+ }
+ }
+ if ((variable_get('statistics_enable_access_log', 0)) && (module_invoke('throttle', 'status') == 0)) {
+ // Log this page access.
+ db_query("INSERT INTO {accesslog} (title, path, url, hostname, uid, sid, timer, timestamp) values('%s', '%s', '%s', '%s', %d, '%s', %d, %d)", strip_tags(drupal_get_title()), $_GET['q'], referer_uri(), $_SERVER['REMOTE_ADDR'], $user->uid, session_id(), timer_read('page'), time());
+ }
+}
+
+/**
+ * Implementation of hook_perm().
+ */
+function statistics_perm() {
+ return array('access statistics', 'view post access counter');
+}
+
+/**
+ * Implementation of hook_link().
+ */
+function statistics_link($type, $node = 0, $main = 0) {
+ global $id;
+ $links = array();
+
+ if ($type != 'comment' && user_access('view post access counter')) {
+ $statistics = statistics_get($node->nid);
+ if ($statistics) {
+ $links[] = format_plural($statistics['totalcount'], '1 read', '%count reads');
+ }
+ }
+
+ return $links;
+}
+
+/**
+ * Implementation of hook_menu().
+ */
+function statistics_menu($may_cache) {
+ $items = array();
+
+ $access = user_access('access statistics');
+ if ($may_cache) {
+ $items[] = array('path' => 'admin/logs/hits', 'title' => t('recent hits'),
+ 'callback' => 'statistics_recent_hits', 'access' => $access,
+ 'weight' => 3);
+ $items[] = array('path' => 'admin/logs/pages', 'title' => t('top pages'),
+ 'callback' => 'statistics_top_pages', 'access' => $access,
+ 'weight' => 1);
+ $items[] = array('path' => 'admin/logs/visitors', 'title' => t('top visitors'),
+ 'callback' => 'statistics_top_visitors', 'access' => $access,
+ 'weight' => 2);
+ $items[] = array('path' => 'admin/logs/referrers', 'title' => t('referrers'),
+ 'callback' => 'statistics_top_referrers', 'access' => $access);
+ $items[] = array('path' => 'admin/logs/access', 'title' => t('details'),
+ 'callback' => 'statistics_access_log', 'access' => $access,
+ 'type' => MENU_CALLBACK);
+ }
+ else {
+ if (arg(0) == 'user' && is_numeric(arg(1)) && variable_get('statistics_enable_access_log', 0)) {
+ $items[] = array('path' => 'user/'. arg(1) .'/track/navigation', 'title' => t('track page visits'),
+ 'callback' => 'statistics_user_tracker', 'access' => $access,
+ 'type' => MENU_LOCAL_TASK, 'weight' => 2);
+ }
+ if (arg(0) == 'node' && is_numeric(arg(1)) && variable_get('statistics_enable_access_log', 0)) {
+ $items[] = array('path' => 'node/'. arg(1) .'/track', 'title' => t('track'),
+ 'callback' => 'statistics_node_tracker', 'access' => $access,
+ 'type' => MENU_LOCAL_TASK, 'weight' => 2);
+ }
+ }
+
+ return $items;
+}
+
+/**
+ * Implementation of hook_user().
+ */
+function statistics_user($op, &$edit, &$user) {
+ if ($op == 'delete') {
+ db_query('UPDATE {accesslog} SET uid = 0 WHERE uid = %d', $user->uid);
+ }
+}
+
+function statistics_access_log($aid) {
+ $result = db_query('SELECT a.*, u.name FROM {accesslog} a LEFT JOIN {users} u ON a.uid = u.uid WHERE aid = %d', $aid);
+ if ($access = db_fetch_object($result)) {
+ $output = '';
+ $output .= ' '. t('URL') ." ". l(url($access->path, NULL, NULL, TRUE), $access->path) ." ";
+ $output .= ' '. t('Title') .' '. $access->title .' '; // safe because it comes from drupal_get_title()
+ $output .= ' '. t('Referrer') ." ". ($access->url ? l($access->url, $access->url) : '') ." ";
+ $output .= ' '. t('Date') .' '. format_date($access->timestamp, 'large') .' ';
+ $output .= ' '. t('User') .' '. theme('username', $access) .' ';
+ $output .= ' '. t('Hostname') .' '. check_plain($access->hostname) .' ';
+ $output .= '
';
+ return $output;
+ }
+ else {
+ drupal_not_found();
+ }
+}
+
+function statistics_node_tracker() {
+ if ($node = node_load(arg(1))) {
+
+ $header = array(
+ array('data' => t('Time'), 'field' => 'a.timestamp', 'sort' => 'desc'),
+ array('data' => t('Referrer'), 'field' => 'a.url'),
+ array('data' => t('User'), 'field' => 'u.name'),
+ array('data' => t('Operations')));
+
+ $result = pager_query('SELECT a.aid, a.timestamp, a.url, a.uid, u.name FROM {accesslog} a LEFT JOIN {users} u ON a.uid = u.uid WHERE a.path LIKE \'node/%d%%\'' . tablesort_sql($header), 30, 0, NULL, $node->nid);
+ while ($log = db_fetch_object($result)) {
+ $rows[] = array(
+ array('data' => format_date($log->timestamp, 'small'), 'class' => 'nowrap'),
+ _statistics_link($log->url),
+ theme('username', $log),
+ l(t('details'), "admin/logs/access/$log->aid"));
+ }
+
+ drupal_set_title(check_plain($node->title));
+ $output = theme('table', $header, $rows);
+ $output .= theme('pager', NULL, 30, 0);
+ return $output;
+ }
+ else {
+ drupal_not_found();
+ }
+}
+
+function statistics_user_tracker() {
+ if ($account = user_load(array('uid' => arg(1)))) {
+
+ $header = array(
+ array('data' => t('Timestamp'), 'field' => 'timestamp', 'sort' => 'desc'),
+ array('data' => t('Page'), 'field' => 'path'),
+ array('data' => t('Operations')));
+
+ $result = pager_query('SELECT aid, timestamp, path, title FROM {accesslog} WHERE uid = %d' . tablesort_sql($header), 30, 0, NULL, $account->uid);
+ while ($log = db_fetch_object($result)) {
+ $rows[] = array(
+ array('data' => format_date($log->timestamp, 'small'), 'class' => 'nowrap'),
+ _statistics_format_item($log->title, $log->path),
+ l(t('details'), "admin/logs/access/$log->aid"));
+ }
+
+ drupal_set_title($account->name);
+ $output = theme('table', $header, $rows);
+ $output .= theme('pager', NULL, 30, 0);
+ return $output;
+ }
+ else {
+ drupal_not_found();
+ }
+}
+
+/**
+ * Menu callback; presents the "recent hits" page.
+ */
+function statistics_recent_hits() {
+ $header = array(
+ array('data' => t('Timestamp'), 'field' => 'a.timestamp', 'sort' => 'desc'),
+ array('data' => t('Page'), 'field' => 'a.path'),
+ array('data' => t('User'), 'field' => 'u.name'),
+ array('data' => t('Operations'))
+ );
+
+ $sql = 'SELECT a.aid, a.path, a.title, a.uid, u.name, a.timestamp FROM {accesslog} a LEFT JOIN {users} u ON u.uid = a.uid' . tablesort_sql($header);
+
+ $result = pager_query($sql, 30);
+ while ($log = db_fetch_object($result)) {
+ $rows[] = array(
+ array('data' => format_date($log->timestamp, 'small'), 'class' => 'nowrap'),
+ _statistics_format_item($log->title, $log->path),
+ theme('username', $log),
+ l(t('details'), "admin/logs/access/$log->aid"));
+ }
+
+ $output = theme('table', $header, $rows);
+ $output .= theme('pager', NULL, 30, 0);
+ return $output;
+}
+
+/**
+ * Menu callback; presents the "top pages" page.
+ */
+function statistics_top_pages() {
+ $sql = "SELECT COUNT(path) AS hits, path, title, AVG(timer) AS average_time, SUM(timer) AS total_time FROM {accesslog} GROUP BY path, title";
+ $sql_cnt = "SELECT COUNT(DISTINCT(path)) FROM {accesslog}";
+
+ $header = array(
+ array('data' => t('Hits'), 'field' => 'hits', 'sort' => 'desc'),
+ array('data' => t('Page'), 'field' => 'path'),
+ array('data' => t('Average page generation time'), 'field' => 'average_time'),
+ array('data' => t('Total page generation time'), 'field' => 'total_time')
+ );
+ $sql .= tablesort_sql($header);
+ $result = pager_query($sql, 30, 0, $sql_cnt);
+
+ while ($page = db_fetch_object($result)) {
+ $rows[] = array($page->hits, _statistics_format_item($page->title, $page->path), t('%time ms', array('%time' => round($page->average_time))), format_interval(round($page->total_time / 1000)));
+ }
+
+ drupal_set_title(t('Top pages in the past %interval', array('%interval' => format_interval(variable_get('statistics_flush_accesslog_timer', 259200)))));
+ $output = theme('table', $header, $rows);
+ $output .= theme('pager', NULL, 30, 0);
+ return $output;
+}
+
+/**
+ * Menu callback; presents the "top visitors" page.
+ */
+function statistics_top_visitors() {
+
+ $header = array(
+ array('data' => t('Hits'), 'field' => 'hits', 'sort' => 'desc'),
+ array('data' => t('Visitor'), 'field' => 'u.name'),
+ array('data' => t('Total page generation time'), 'field' => 'total'),
+ array('data' => t('Operations'))
+ );
+
+ $sql = "SELECT COUNT(a.uid) AS hits, a.uid, u.name, a.hostname, SUM(a.timer) AS total, ac.aid FROM {accesslog} a LEFT JOIN {access} ac ON ac.type = 'host' AND LOWER(a.hostname) LIKE (ac.mask) LEFT JOIN {users} u ON a.uid = u.uid GROUP BY a.hostname, a.uid, u.name, ac.aid". tablesort_sql($header);
+ $sql_cnt = "SELECT COUNT(DISTINCT(uid)) FROM {accesslog}";
+ $result = pager_query($sql, 30, 0, $sql_cnt);
+
+ while ($account = db_fetch_object($result)) {
+ $qs = drupal_get_destination();
+ $ban_link = $account->aid ? l(t('unban'), "admin/access/rules/delete/$account->aid", array(), $qs) : l(t('ban'), "admin/access/rules/add/$account->hostname/host", array(), $qs);
+ $rows[] = array($account->hits, ($account->uid ? theme('username', $account) : $account->hostname), format_interval(round($account->total / 1000)), $ban_link);
+ }
+
+ drupal_set_title(t('Top visitors in the past %interval', array('%interval' => format_interval(variable_get('statistics_flush_accesslog_timer', 259200)))));
+ $output = theme('table', $header, $rows);
+ $output .= theme('pager', NULL, 30, 0);
+ return $output;
+}
+
+/**
+ * Menu callback; presents the "referrer" page.
+ */
+function statistics_top_referrers() {
+ $query = "SELECT url, COUNT(url) AS hits, MAX(timestamp) AS last FROM {accesslog} WHERE url NOT LIKE '%%%s%%' AND url <> '' GROUP BY url";
+ $query_cnt = "SELECT COUNT(DISTINCT(url)) FROM {accesslog} WHERE url <> '' AND url NOT LIKE '%%%s%%'";
+ drupal_set_title(t('Top referrers in the past %interval', array('%interval' => format_interval(variable_get('statistics_flush_accesslog_timer', 259200)))));
+
+ $header = array(
+ array('data' => t('Hits'), 'field' => 'hits', 'sort' => 'desc'),
+ array('data' => t('Url'), 'field' => 'url'),
+ array('data' => t('Last visit'), 'field' => 'last'),
+ );
+
+ $query .= tablesort_sql($header);
+ $result = pager_query($query, 30, 0, $query_cnt, $_SERVER['HTTP_HOST']);
+
+ while ($referrer = db_fetch_object($result)) {
+ $rows[] = array($referrer->hits, _statistics_link($referrer->url), t('%time ago', array('%time' => format_interval(time() - $referrer->last))));
+ }
+
+ $output = theme('table', $header, $rows);
+ $output .= theme('pager', NULL, 30, 0);
+ return $output;
+}
+
+/**
+ * Implementation of hook_settings().
+ */
+function statistics_settings() {
+ // access log settings:
+ $options = array('1' => t('Enabled'), '0' => t('Disabled'));
+ $form['access'] = array('#type' => 'fieldset', '#title' => t('Access log settings'));
+ $form['access']['statistics_enable_access_log'] = array('#type' => 'radios', '#title' => t('Enable access log'), '#default_value' => variable_get('statistics_enable_access_log', 0), '#options' => $options, '#description' => t('Log each page access. Required for referrer statistics.'));
+ $period = drupal_map_assoc(array(3600, 10800, 21600, 32400, 43200, 86400, 172800, 259200, 604800, 1209600, 2419200, 4838400, 9676800), 'format_interval');
+ $form['access']['statistics_flush_accesslog_timer'] = array('#type' => 'select', '#title' => t('Discard access logs older than'), '#default_value' => variable_get('statistics_flush_accesslog_timer', 259200), '#options' => $period, '#description' => t('Older access log entries (including referrer statistics) will be automatically discarded. Requires crontab.'));
+
+ // count content views settings
+ $form['content'] = array('#type' => 'fieldset', '#title' => t('Content viewing counter settings'));
+ $form['content']['statistics_count_content_views'] = array('#type' => 'radios', '#title' => t('Count content views'), '#default_value' => variable_get('statistics_count_content_views', 0), '#options' => $options, '#description' => t('Increment a counter each time content is viewed.'));
+
+ return $form;
+}
+
+/**
+ * Implementation of hook_cron().
+ */
+function statistics_cron() {
+ $statistics_timestamp = variable_get('statistics_day_timestamp', '');
+
+ if ((time() - $statistics_timestamp) >= 86400) {
+ /* reset day counts */
+ db_query('UPDATE {node_counter} SET daycount = 0');
+ variable_set('statistics_day_timestamp', time());
+ }
+
+ /* clean expired access logs */
+ db_query('DELETE FROM {accesslog} WHERE timestamp < %d', time() - variable_get('statistics_flush_accesslog_timer', 259200));
+}
+
+/**
+ * Returns all time or today top or last viewed node(s).
+ *
+ * @param $dbfield
+ * one of
+ * - 'totalcount': top viewed content of all time.
+ * - 'daycount': top viewed content for today.
+ * - 'timestamp': last viewed node.
+ *
+ * @param $dbrows
+ * number of rows to be returned.
+ *
+ * @return
+ * A query result containing n.nid, n.title, u.uid, u.name of the selected node(s)
+ * or FALSE if the query could not be executed correctly.
+ */
+function statistics_title_list($dbfield, $dbrows) {
+ return db_query_range(db_rewrite_sql("SELECT n.nid, n.title, u.uid, u.name FROM {node} n INNER JOIN {node_counter} s ON n.nid = s.nid INNER JOIN {users} u ON n.uid = u.uid WHERE %s <> '0' AND n.status = 1 ORDER BY %s DESC"), 's.'. $dbfield, 's.'. $dbfield, 0, $dbrows);
+}
+
+
+/**
+ * Retrieves a node's "view statistics".
+ *
+ * @param $nid
+ * node ID
+ *
+ * @return
+ * An array with three entries: [0]=totalcount, [1]=daycount, [2]=timestamp
+ * - totalcount: count of the total number of times that node has been viewed.
+ * - daycount: count of the total number of times that node has been viewed "today".
+ * For the daycount to be reset, cron must be enabled.
+ * - timestamp: timestamp of when that node was last viewed.
+ */
+function statistics_get($nid) {
+
+ if ($nid > 0) {
+ /* retrieves an array with both totalcount and daycount */
+ $statistics = db_fetch_array(db_query('SELECT totalcount, daycount, timestamp FROM {node_counter} WHERE nid = %d', $nid));
+ }
+
+ return $statistics;
+}
+
+/**
+ * Implementation of hook_block().
+ */
+function statistics_block($op = 'list', $delta = 0, $edit = array()) {
+ switch ($op) {
+ case 'list':
+ if (variable_get('statistics_count_content_views', 0)) {
+ $blocks[0]['info'] = t('Popular content');
+ }
+ return $blocks;
+
+ case 'configure':
+ // Popular content block settings
+ $numbers = array('0' => t('Disabled')) + drupal_map_assoc(array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 25, 30, 40));
+ $form['statistics_block_top_day_num'] = array('#type' => 'select', '#title' => t("Number of day's top views to display"), '#default_value' => variable_get('statistics_block_top_day_num', 0), '#options' => $numbers, '#description' => t('How many content items to display in "day" list.'));
+ $form['statistics_block_top_all_num'] = array('#type' => 'select', '#title' => t('Number of all time views to display'), '#default_value' => variable_get('statistics_block_top_all_num', 0), '#options' => $numbers, '#description' => t('How many content items to display in "all time" list.'));
+ $form['statistics_block_top_last_num'] = array('#type' => 'select', '#title' => t('Number of most recent views to display'), '#default_value' => variable_get('statistics_block_top_last_num', 0), '#options' => $numbers, '#description' => t('How many content items to display in "recently viewed" list.'));
+ return $form;
+
+ case 'save':
+ variable_set('statistics_block_top_day_num', $edit['statistics_block_top_day_num']);
+ variable_set('statistics_block_top_all_num', $edit['statistics_block_top_all_num']);
+ variable_set('statistics_block_top_last_num', $edit['statistics_block_top_last_num']);
+ break;
+
+ case 'view':
+ if (user_access('access content')) {
+ $content = array();
+
+ $daytop = variable_get('statistics_block_top_day_num', 0);
+ if ($daytop && ($result = statistics_title_list('daycount', $daytop)) && db_num_rows($result)) {
+ $content[] = node_title_list($result, t("Today's:"));
+ }
+
+ $alltimetop = variable_get('statistics_block_top_all_num', 0);
+ if ($alltimetop && ($result = statistics_title_list('totalcount', $alltimetop)) && db_num_rows($result)) {
+ $content[] = node_title_list($result, t('All time:'));
+ }
+
+ $lasttop = variable_get('statistics_block_top_last_num', 0);
+ if ($lasttop && ($result = statistics_title_list('timestamp', $lasttop)) && db_num_rows($result)) {
+ $content[] = node_title_list($result, t('Last viewed:'));
+ }
+
+ if (count($content)) {
+ $block['content'] = implode('
', $content);
+ $block['subject'] = t('Popular content');
+ return $block;
+ }
+ }
+ }
+}
+
+/**
+ * It is possible to adjust the width of columns generated by the
+ * statistics module.
+ */
+function _statistics_link($path, $width = 35) {
+ $title = drupal_get_path_alias($path);
+ $title = truncate_utf8($title, $width, FALSE, TRUE);
+ return l($title, $path);
+}
+
+function _statistics_format_item($title, $path) {
+ $path = ($path ? $path : '/');
+ $output = ($title ? "$title
" : '');
+ $output .= _statistics_link($path);
+ return $output;
+}
+
+/**
+ * Implementation of hook_nodeapi().
+ */
+function statistics_nodeapi(&$node, $op, $arg = 0) {
+ switch ($op) {
+ case 'delete':
+ // clean up statistics table when node is deleted
+ db_query('DELETE FROM {node_counter} WHERE nid = %d', $node->nid);
+ }
+}
+
+
diff --git a/modules/story.module b/modules/story.module
new file mode 100644
index 0000000..6c4fb18
--- /dev/null
+++ b/modules/story.module
@@ -0,0 +1,86 @@
+'. t('The story module is used to create a content post type called stories. Stories are articles in their simplest form: they have a title, a teaser and a body. Stories are typically used to post news articles or as a group blog. ') .'';
+ $output .= ''. t('The story administration interface allows for complex configuration. It provides a submission form, workflow, default view permission, default edit permission, permissions for permission, and attachments. Trackbacks can also be enabled.') .'
';
+ $output .= t('You can
+
+- post a story at create content >> story.
+- configure story at administer >> settings >> content types >> configure story.
+
+', array('%node-add-story' => url('node/add/story'), '%admin-settings-content-types-story' => url('admin/settings/content-types/story')));
+ $output .= ''. t('For more information please read the configuration and customization handbook Story page.', array('%story' => 'http://drupal.org/handbook/modules/story/')) .'
';
+ return $output;
+ case 'admin/modules#description':
+ return t('Allows users to submit stories, articles or similar content.');
+ case 'node/add#story':
+ return t('Stories are articles in their simplest form: they have a title, a teaser and a body, but can be extended by other modules. The teaser is part of the body too. Stories may be used as a personal blog or for news articles.');
+ }
+}
+
+/**
+ * Implementation of hook_node_info().
+ */
+function story_node_info() {
+ return array('story' => array('name' => t('story'), 'base' => 'story'));
+}
+
+/**
+ * Implementation of hook_perm().
+ */
+function story_perm() {
+ return array('create stories', 'edit own stories');
+}
+
+/**
+ * Implementation of hook_access().
+ */
+function story_access($op, $node) {
+ global $user;
+
+ if ($op == 'create') {
+ return user_access('create stories');
+ }
+
+ if ($op == 'update' || $op == 'delete') {
+ if (user_access('edit own stories') && ($user->uid == $node->uid)) {
+ return TRUE;
+ }
+ }
+}
+
+/**
+ * Implementation of hook_menu().
+ */
+function story_menu($may_cache) {
+ $items = array();
+
+ if ($may_cache) {
+ $items[] = array('path' => 'node/add/story', 'title' => t('story'),
+ 'access' => user_access('create stories'));
+ }
+
+ return $items;
+}
+
+/**
+ * Implementation of hook_form().
+ */
+function story_form(&$node) {
+ $form['title'] = array('#type' => 'textfield', '#title' => t('Title'), '#required' => TRUE, '#default_value' => $node->title, '#weight' => -5);
+ $form['body_filter']['body'] = array('#type' => 'textarea', '#title' => t('Body'), '#default_value' => $node->body, '#rows' => 20, '#required' => TRUE);
+ $form['body_filter']['format'] = filter_form($node->format);
+ return $form;
+}
+
diff --git a/modules/system.module b/modules/system.module
new file mode 100644
index 0000000..c68bba4
--- /dev/null
+++ b/modules/system.module
@@ -0,0 +1,1272 @@
+'. t('The system module provides system-wide defaults such as running jobs at a particular time, and storing web pages to improve efficiency. The ability to run scheduled jobs makes administering the web site more usable, as administrators do not have to manually start jobs. The storing of web pages, or caching, allows the site to efficiently re-use web pages and improve web site performance. The settings module provides control over preferences, behaviours including visual and operational settings.') .'';
+ $output .= ''. t('Some modules require regularly scheduled actions, such as cleaning up logfiles. Cron, which stands for chronograph, is a periodic command scheduler executing commands at intervals specified in seconds. It can be used to control the execution of daily, weekly and monthly jobs (or anything with a period measured in seconds). The aggregator module periodically updates feeds using cron. Ping periodically notifies services of new content on your site. Search periodically indexes the content on your site. Automating tasks is one of the best ways to keep a system running smoothly, and if most of your administration does not require your direct involvement, cron is an ideal solution.') .'
';
+ $output .= ''. t('There is a caching mechanism which stores dynamically generated web pages in a database. By caching a web page, the system module does not have to create the page each time someone wants to view it, instead it takes only one SQL query to display it, reducing response time and the server\'s load. Only pages requested by anonymous users are cached. In order to reduce server load and save bandwidth, the system module stores and sends cached pages compressed.') .'
';
+ $output .= t('You can
+
+- activate your cron job on the cron page cron.php.
+- read how to configure cron jobs.
+- administer cache settings in administer >> settings.
+
+', array('%file-cron' => 'cron.php', '%external-http-drupal-org-cron' => 'http://drupal.org/cron', '%admin-settings' => url('admin/settings')));
+ $output .= ''. t('For more information please read the configuration and customization handbook System page.', array('%system' => 'http://drupal.org/handbook/modules/system/')) .'
';
+ return $output;
+ case 'admin/modules#description':
+ return t('Handles general site configuration for administrators.');
+ case 'admin':
+ return t('Welcome to the administration section. Below are the most recent system events.
');
+ case 'admin/settings':
+ return t('General configuration options for your site. Set up the name of the site, e-mail address used in mail-outs, clean URL options, caching, etc.
');
+ case 'admin/themes':
+ return t('Select which themes are available to your users and specify the default theme. To configure site-wide display settings, click the "configure" task above. Alternately, to override these settings in a specific theme, click the "configure" link for the corresponding theme. Note that different themes may have different regions available for rendering content like blocks. If you want consistency in what your users see, you may wish to enable only one theme.
');
+ case 'admin/themes/settings':
+ return t('These options control the default display settings for your entire site, across all themes. Unless they have been overridden by a specific theme, these settings will be used.
');
+ case 'admin/themes/settings/'. arg(3):
+ $reference = explode('.', arg(3), 2);
+ $theme = array_pop($reference);
+ return t('These options control the display settings for the %template
theme. When your site is displayed using this theme, these settings will be used. By clicking "Reset to defaults," you can choose to use the global settings for this theme.
', array('%template' => $theme, '%global' => url('admin/themes/settings')));
+ case 'admin/modules':
+ return t('Modules are plugins for Drupal that extend its core functionality. Here you can select which modules are enabled. Click on the name of the module in the navigation menu for their individual configuration pages. Once a module is enabled, new permissions might be made available. Modules can automatically be temporarily disabled to reduce server load when your site becomes extremely busy by enabling the throttle.module and checking throttle. The auto-throttle functionality must be enabled on the throttle configuration page after having enabled the throttle module.
+It is important that update.php is run every time a module is updated to a newer version.
', array('%permissions' => url('admin/access/permissions'), '%throttle' => url('admin/settings/throttle'), '%update-php' => $base_url .'/update.php'));
+ }
+}
+
+/**
+ * Implementation of hook_perm().
+ */
+function system_perm() {
+ return array('administer site configuration', 'access administration pages', 'select different theme');
+}
+
+/**
+ * Implementation of hook_elements().
+ */
+function system_elements() {
+ // Top level form
+ $type['form'] = array('#method' => 'post', '#action' => request_uri());
+
+ // Inputs
+ $type['checkbox'] = array('#input' => TRUE, '#return_value' => 1);
+ $type['submit'] = array('#input' => TRUE, '#name' => 'op', '#button_type' => 'submit', '#executes_submit_callback' => TRUE);
+ $type['button'] = array('#input' => TRUE, '#name' => 'op', '#button_type' => 'submit', '#executes_submit_callback' => FALSE);
+ $type['textfield'] = array('#input' => TRUE, '#size' => 60, '#maxlength' => 128, '#autocomplete_path' => FALSE);
+ $type['password'] = array('#input' => TRUE, '#size' => 30);
+ $type['password_confirm'] = array('#input' => TRUE, '#process' => array('expand_password_confirm' => array()));
+ $type['textarea'] = array('#input' => TRUE, '#cols' => 60, '#rows' => 5);
+ $type['radios'] = array('#input' => TRUE, '#process' => array('expand_radios' => array()));
+ $type['radio'] = array('#input' => TRUE);
+ $type['checkboxes'] = array('#input' => TRUE, '#process' => array('expand_checkboxes' => array()), '#tree' => TRUE);
+ $type['select'] = array('#input' => TRUE);
+ $type['weight'] = array('#input' => TRUE, '#delta' => 10, '#default_value' => 0);
+ $type['date'] = array('#input' => TRUE, '#process' => array('expand_date' => array()), '#validate' => array('date_validate' => array()));
+ $type['file'] = array('#input' => TRUE, '#size' => 60);
+
+ // Form structure
+ $type['item'] = array();
+ $type['hidden'] = array('#input' => TRUE);
+ $type['value'] = array('#input' => TRUE);
+ $type['markup'] = array('#prefix' => '', '#suffix' => '');
+ $type['fieldset'] = array('#collapsible' => FALSE, '#collapsed' => FALSE);
+ return $type;
+}
+
+/**
+ * Implementation of hook_menu().
+ */
+function system_menu($may_cache) {
+ $items = array();
+
+ if ($may_cache) {
+ $items[] = array('path' => 'system/files', 'title' => t('file download'),
+ 'callback' => 'file_download',
+ 'access' => TRUE,
+ 'type' => MENU_CALLBACK);
+
+ $access = user_access('administer site configuration');
+
+ $items[] = array('path' => 'admin', 'title' => t('administer'),
+ 'access' => user_access('access administration pages'),
+ 'callback' => 'watchdog_overview',
+ 'weight' => 9);
+
+ // Themes:
+ $items[] = array('path' => 'admin/themes', 'title' => t('themes'),
+ 'callback' => 'system_themes', 'access' => $access);
+
+ $items[] = array('path' => 'admin/themes/select', 'title' => t('list'),
+ 'callback' => 'system_themes', 'access' => $access,
+ 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -1);
+
+ $items[] = array('path' => 'admin/themes/settings', 'title' => t('configure'),
+ 'callback' => 'system_theme_settings', 'access' => $access,
+ 'type' => MENU_LOCAL_TASK);
+
+ // Theme configuration subtabs
+ $items[] = array('path' => 'admin/themes/settings/global', 'title' => t('global settings'),
+ 'callback' => 'system_theme_settings', 'access' => $access,
+ 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -1);
+
+ foreach (list_themes() as $theme) {
+ if ($theme->status) {
+ $items[] = array('path' => 'admin/themes/settings/'. $theme->name, 'title' => $theme->name,
+ 'callback' => 'system_theme_settings', 'callback arguments' => array($theme->name), 'access' => $access,
+ 'type' => MENU_LOCAL_TASK);
+ }
+ }
+
+ // Modules:
+ $items[] = array('path' => 'admin/settings', 'title' => t('settings'),
+ 'callback' => 'system_site_settings', 'access' => $access);
+ foreach (module_list() as $name) {
+ if (module_hook($name, 'settings')) {
+ $items[] = array('path' => 'admin/settings/'. $name, 'title' => t($name));
+ }
+ }
+ $items[] = array('path' => 'admin/modules', 'title' => t('modules'),
+ 'callback' => 'system_modules', 'access' => $access);
+ }
+
+ return $items;
+}
+
+/**
+ * Implementation of hook_user().
+ *
+ * Allows users to individually set their theme and time zone.
+ */
+function system_user($type, $edit, &$user, $category = NULL) {
+ if ($type == 'form' && $category == 'account') {
+ $form['theme_select'] = system_theme_select_form(t('Selecting a different theme will change the look and feel of the site.'), $edit['theme'], 2);
+
+ if (variable_get('configurable_timezones', 1)) {
+ $zones = _system_zonelist();
+ $form['timezone'] = array('#type'=>'fieldset', '#title' => t('Locale settings'), '#weight' => 6);
+ $form['timezone']['timezone'] = array(
+ '#type' => 'select', '#title' => t('Time zone'), '#default_value' => strlen($edit['timezone']) ? $edit['timezone'] : variable_get('date_default_timezone', 0),
+ '#options' => $zones, '#description' => t('Select your current local time. Dates and times throughout this site will be displayed using this time zone.')
+ );
+ }
+
+ return $form;
+ }
+}
+
+/*
+ * Returns a fieldset containing the theme select form.
+ *
+ * @param $description
+ * description of the fieldset
+ * @param $default_value
+ * default value of theme radios
+ * @param $weight
+ * weight of the fieldset
+ * @return
+ * a form array
+ */
+function system_theme_select_form($description = '', $default_value = '', $weight = 0) {
+ if (user_access('select different theme')) {
+ foreach (list_themes() as $theme) {
+ if ($theme->status) {
+ $enabled[] = $theme;
+ }
+ }
+
+ if (count($enabled) > 1) {
+ ksort($enabled);
+
+ $form['themes'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Theme configuration'),
+ '#description' => $description,
+ '#collapsible' => TRUE,
+ '#theme' => 'system_theme_select_form'
+ );
+
+ foreach ($enabled as $info) {
+ // For the default theme, revert to an empty string so the user's theme updates when the site theme is changed.
+ $info->key = $info->name == variable_get('theme_default', 'bluemarine') ? '' : $info->name;
+
+ $info->screenshot = dirname($info->filename) . '/screenshot.png';
+ $screenshot = file_exists($info->screenshot) ? theme('image', $info->screenshot, t('Screenshot for %theme theme', array('%theme' => $info->name)), '', array('class' => 'screenshot'), false) : t('no screenshot');
+
+ $form['themes'][$info->key]['screenshot'] = array('#type' => 'markup', '#value' => $screenshot);
+ $form['themes'][$info->key]['description'] = array('#type' => 'item', '#title' => $info->name, '#value' => dirname($info->filename) . ($info->name == variable_get('theme_default', 'bluemarine') ? t('
(site default theme)') : ''));
+ $options[$info->key] = '';
+ }
+
+ $form['themes']['theme'] = array('#type' => 'radios', '#options' => $options, '#default_value' => $default_value ? $default_value : '');
+ $form['#weight'] = $weight;
+ return $form;
+ }
+ }
+}
+
+function theme_system_theme_select_form($form) {
+ foreach (element_children($form) as $key) {
+ $row = array();
+ if (is_array($form[$key]['description'])) {
+ $row[] = form_render($form[$key]['screenshot']);
+ $row[] = form_render($form[$key]['description']);
+ $row[] = form_render($form['theme'][$key]);
+ }
+ $rows[] = $row;
+ }
+
+ $header = array(t('Screenshot'), t('Name'), t('Selected'));
+ $output = theme('table', $header, $rows);
+ return $output;
+}
+
+function _system_zonelist() {
+ $timestamp = time();
+ $zonelist = array(-11, -10, -9.5, -9, -8, -7, -6, -5, -4, -3.5, -3, -2, -1, 0, 1, 2, 3, 3.5, 4, 5, 5.5, 5.75, 6, 6.5, 7, 8, 9, 9.5, 10, 10.5, 11, 11.5, 12, 12.75, 13, 14);
+ $zones = array();
+ foreach ($zonelist as $offset) {
+ $zone = $offset * 3600;
+ $zones[$zone] = format_date($timestamp, 'custom', variable_get('date_format_long', 'l, F j, Y - H:i') . ' O', $zone);
+ }
+ return $zones;
+}
+
+function system_view_general() {
+ // General settings:
+ $form['general'] = array(
+ '#type' => 'fieldset', '#title' => t('General settings'),
+ '#collapsible' => TRUE, '#collapsed' => TRUE
+ );
+ $form['general']['site_name'] = array(
+ '#type' => 'textfield', '#title' => t('Name'), '#default_value' => variable_get('site_name', 'drupal'),
+ '#description' => t('The name of this web site.'), '#required' => TRUE
+ );
+ $form['general']['site_mail'] = array(
+ '#type' => 'textfield', '#title' => t('E-mail address'), '#default_value' => variable_get('site_mail', ini_get('sendmail_from')),
+ '#description' => t('A valid e-mail address for this website, used by the auto-mailer during registration, new password requests, notifications, etc.')
+ );
+ $form['general']['site_slogan'] = array(
+ '#type' => 'textfield', '#title' => t('Slogan'), '#default_value' => variable_get('site_slogan', ''),
+ '#description' => t('The slogan of this website. Some themes display a slogan when available.')
+ );
+
+ $form['general']['site_mission'] = array(
+ '#type' => 'textarea', '#title' => t('Mission'), '#default_value' => variable_get('site_mission', ''),
+ '#description' => t('Your site\'s mission statement or focus.')
+ );
+ $form['general']['site_footer'] = array(
+ '#type' => 'textarea', '#title' => t('Footer message'), '#default_value' => variable_get('site_footer', ''),
+ '#description' => t('This text will be displayed at the bottom of each page. Useful for adding a copyright notice to your pages.')
+ );
+ $form['general']['anonymous'] = array(
+ '#type' => 'textfield', '#title' => t('Anonymous user'), '#default_value' => variable_get('anonymous', 'Anonymous'),
+ '#description' => t('The name used to indicate anonymous users.')
+ );
+ $form['general']['site_frontpage'] = array(
+ '#type' => 'textfield', '#title' => t('Default front page'), '#default_value' => variable_get('site_frontpage', 'node'),
+ '#description' => t('The home page displays content from this relative URL. If you are not using clean URLs, specify the part after "?q=". If unsure, specify "node".')
+ );
+
+ // We check for clean URL support using an image on the client side.
+ $form['general']['clean_url'] = array(
+ '#type' => 'radios',
+ '#title' => t('Clean URLs'),
+ '#default_value' => variable_get('clean_url', 0),
+ '#options' => array(t('Disabled'), t('Enabled')),
+ '#description' => t('This option makes Drupal emit "clean" URLs (i.e. without ?q=
in the URL.)'),
+ );
+
+ if (!variable_get('clean_url', 0)) {
+ if (strpos(request_uri(), '?q=') !== FALSE) {
+ $form['general']['clean_url']['#description'] .= t(' Before enabling clean URLs, you must perform a test to determine if your server is properly configured. If you are able to see this page again after clicking the "Run the clean URL test" link, the test has succeeded and the radio buttons above will be available. If instead you are directed to a "Page not found" error, you will need to change the configuration of your server. The handbook page on Clean URLs has additional troubleshooting information. %run-test', array('%handbook' => 'http://drupal.org/node/15365', '%run-test' => ''. t('Run the clean URL test') .''));
+ $form['general']['clean_url']['#attributes'] = array('disabled' => 'disabled');
+ }
+ else {
+ $form['general']['clean_url']['#description'] .= t(' You have successfully demonstrated that clean URLs work on your server. You are welcome to enable/disable them as you wish.');
+ $form['general']['#collapsed'] = FALSE;
+ }
+ }
+
+ // Error handling:
+
+ $form['errors'] = array( '#type' => 'fieldset', '#title' =>t('Error handling'), '#collapsible' => TRUE, '#collapsed' => TRUE );
+ $form['errors']['site_403'] = array(
+ '#type' => 'textfield', '#title' => t('Default 403 (access denied) page'), '#default_value' => variable_get('site_403', ''),
+ '#description' => t('This page is displayed when the requested document is denied to the current user. If you are not using clean URLs, specify the part after "?q=". If unsure, specify nothing.')
+ );
+
+ $form['errors']['site_404'] = array(
+ '#type' => 'textfield', '#title' => t('Default 404 (not found) page'), '#default_value' => variable_get('site_404', ''),
+ '#description' => t('This page is displayed when no other content matches the requested document. If you are not using clean URLs, specify the part after "?q=". If unsure, specify nothing.')
+ );
+
+ $form['errors']['error_level'] = array(
+ '#type' => 'select', '#title' => t('Error reporting'), '#default_value' => variable_get('error_level', 1),
+ '#options' => array(t('Write errors to the log'), t('Write errors to the log and to the screen')),
+ '#description' => t('Where Drupal, PHP and SQL errors are logged. On a production server it is recommended that errors are only written to the error log. On a test server it can be helpful to write logs to the screen.')
+ );
+
+ $period = drupal_map_assoc(array(3600, 10800, 21600, 32400, 43200, 86400, 172800, 259200, 604800, 1209600, 2419200), 'format_interval');
+ $period['1000000000'] = t('Never');
+ $form['errors']['watchdog_clear'] = array(
+ '#type' => 'select', '#title' => t('Discard log entries older than'), '#default_value' => variable_get('watchdog_clear', 604800), '#options' => $period,
+ '#description' => t('The time log entries should be kept. Older entries will be automatically discarded. Requires crontab.')
+ );
+
+
+ // Caching:
+ $form['cache'] = array('#type' => 'fieldset', '#title' => t('Cache settings'), '#collapsible' => TRUE, '#collapsed' => TRUE);
+
+ $form['cache']['cache'] = array(
+ '#type' => 'radios', '#title' => t('Page cache'), '#default_value' => variable_get('cache', CACHE_DISABLED),
+ '#options' => array(CACHE_DISABLED => t('Disabled'), CACHE_ENABLED => t('Enabled')),
+ '#description' => t("Drupal has a caching mechanism which stores dynamically generated web pages in a database. By caching a web page, Drupal does not have to create the page each time someone wants to view it, instead it takes only one SQL query to display it, reducing response time and the server's load. Only pages requested by \"anonymous\" users are cached. In order to reduce server load and save bandwidth, Drupal stores and sends compressed cached pages.")
+ );
+
+ $period = drupal_map_assoc(array(0, 60, 180, 300, 600, 900, 1800, 2700, 3600, 10800, 21600, 32400, 43200, 86400), 'format_interval');
+ $period[0] = t('none');
+ $form['cache']['cache_lifetime'] = array(
+ '#type' => 'select', '#title' => t('Minimum cache lifetime'), '#default_value' => variable_get('cache_lifetime', 0), '#options' => $period,
+ '#description' => t('Enabling the cache will offer a sufficient performance boost for most low-traffic and medium-traffic sites. On high-traffic sites it can become necessary to enforce a minimum cache lifetime. The minimum cache lifetime is the minimum amount of time that will go by before the cache is emptied and recreated. A larger minimum cache lifetime offers better performance, but users will not see new content for a longer period of time.')
+ );
+
+
+ // File system:
+ $form['files'] = array('#type' => 'fieldset', '#title' => t('File system settings'), '#collapsible' => TRUE, '#collapsed' => TRUE);
+
+ $form['files']['file_directory_path'] = array(
+ '#type' => 'textfield',
+ '#title' => t('File system path'),
+ '#default_value' => file_directory_path(),
+ '#maxlength' => 255,
+ '#description' => t('A file system path where the files will be stored. This directory has to exist and be writable by Drupal. If the download method is set to public this directory has to be relative to Drupal installation directory, and be accessible over the web. When download method is set to private this directory should not be accessible over the web. Changing this location after the site has been in use will cause problems so only change this setting on an existing site if you know what you are doing.'),
+ '#after_build' => array('system_check_directory'),
+ );
+
+ $form['files']['file_directory_temp'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Temporary directory'),
+ '#default_value' => file_directory_temp(),
+ '#maxlength' => 255,
+ '#description' => t('Location where uploaded files will be kept during previews. Relative paths will be resolved relative to the Drupal installation directory.'),
+ '#after_build' => array('system_check_directory'),
+ );
+
+ $form['files']['file_downloads'] = array(
+ '#type' => 'radios', '#title' => t('Download method'), '#default_value' => variable_get('file_downloads', FILE_DOWNLOADS_PUBLIC),
+ '#options' => array(FILE_DOWNLOADS_PUBLIC => t('Public - files are available using http directly.'), FILE_DOWNLOADS_PRIVATE => t('Private - files are transferred by Drupal.')),
+ '#description' => t('If you want any sort of access control on the downloading of files, this needs to be set to private. You can change this at any time, however all download URLs will change and there may be unexpected problems so it is not recommended.')
+ );
+
+ // Image handling:
+ $toolkits_available = image_get_available_toolkits();
+ if (count($toolkits_available) > 1) {
+ $form['image'] = array('#type' => 'fieldset', '#title' => t('Image handling'), '#collapsible' => TRUE, '#collapsed' => true);
+ $form['image']['image_toolkit'] = array(
+ '#type' => 'radios', '#title' => t('Select an image processing toolkit'),
+ '#default_value' => variable_get('image_toolkit', image_get_toolkit()), '#options' => $toolkits_available
+ );
+ }
+
+ // Feed settings
+ $form['feed'] = array('#type' => 'fieldset', '#title' => t('RSS feed settings'), '#collapsible' => TRUE, '#collapsed' => TRUE);
+ $form['feed']['feed_default_items'] = array(
+ '#type' => 'select', '#title' => t('Number of items per feed'), '#default_value' => variable_get('feed_default_items', 10),
+ '#options' => drupal_map_assoc(array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 25, 30)),
+ '#description' => t('The default number of items to include in a feed.')
+ );
+ $form['feed']['feed_item_length'] = array(
+ '#type' => 'select', '#title' => t('Display of XML feed items'), '#default_value' => variable_get('feed_item_length','teaser'),
+ '#options' => array('title' => t('Titles only'), 'teaser' => t('Titles plus teaser'), 'fulltext' => t('Full text')),
+ '#description' => t('Global setting for the length of XML feed items that are output by default.')
+ );
+
+ // Date settings:
+ $zones = _system_zonelist();
+
+ // Date settings: possible date formats
+ $dateshort = array('Y-m-d H:i','m/d/Y - H:i', 'd/m/Y - H:i', 'Y/m/d - H:i',
+ 'd.m.Y - H:i', 'm/d/Y - g:ia', 'd/m/Y - g:ia', 'Y/m/d - g:ia',
+ 'M j Y - H:i', 'j M Y - H:i', 'Y M j - H:i',
+ 'M j Y - g:ia', 'j M Y - g:ia', 'Y M j - g:ia');
+ $datemedium = array('D, Y-m-d H:i', 'D, m/d/Y - H:i', 'D, d/m/Y - H:i',
+ 'D, Y/m/d - H:i', 'F j, Y - H:i', 'j F, Y - H:i', 'Y, F j - H:i',
+ 'D, m/d/Y - g:ia', 'D, d/m/Y - g:ia', 'D, Y/m/d - g:ia',
+ 'F j, Y - g:ia', 'j F Y - g:ia', 'Y, F j - g:ia', 'j. F Y - G:i');
+ $datelong = array('l, F j, Y - H:i', 'l, j F, Y - H:i', 'l, Y, F j - H:i',
+ 'l, F j, Y - g:ia', 'l, j F Y - g:ia', 'l, Y, F j - g:ia', 'l, j. F Y - G:i');
+
+ // Date settings: construct choices for user
+ foreach ($dateshort as $f) {
+ $dateshortchoices[$f] = format_date(time(), 'custom', $f);
+ }
+ foreach ($datemedium as $f) {
+ $datemediumchoices[$f] = format_date(time(), 'custom', $f);
+ }
+ foreach ($datelong as $f) {
+ $datelongchoices[$f] = format_date(time(), 'custom', $f);
+ }
+
+ $form['dates'] = array('#type' => 'fieldset', '#title' => t('Date settings'), '#collapsible' => TRUE, '#collapsed' => TRUE);
+ $form['dates']['date_default_timezone'] = array(
+ '#type' => 'select', '#title' => t('Default time zone'), '#default_value' => variable_get('date_default_timezone', 0),
+ '#options' => $zones, '#description' => t('Select the default site time zone.')
+ );
+
+ $form['dates']['configurable_timezones'] = array(
+ '#type' => 'radios', '#title' => t('Configurable time zones'), '#default_value' => variable_get('configurable_timezones', 1), '#options' => array(t('Disabled'), t('Enabled')),
+ '#description' => t('Enable or disable user-configurable time zones. When enabled, users can set their own time zone and dates will be updated accordingly.')
+ );
+
+ $form['dates']['date_format_short'] = array(
+ '#type' => 'select', '#title' => t('Short date format'), '#default_value' => variable_get('date_format_short', $dateshort[0]),
+ '#options' => $dateshortchoices, '#description' => t('The short format of date display.')
+ );
+
+ $form['dates']['date_format_medium'] = array(
+ '#type' => 'select', '#title' => t('Medium date format'), '#default_value' => variable_get('date_format_medium', $datemedium[0]),
+ '#options' => $datemediumchoices, '#description' => t('The medium sized date display.')
+ );
+
+ $form['dates']['date_format_long'] = array(
+ '#type' => 'select', '#title' => t('Long date format'), '#default_value' => variable_get('date_format_long', $datelong[0]),
+ '#options' => $datelongchoices, '#description' => t('Longer date format used for detailed display.')
+ );
+
+ $form['dates']['date_first_day'] = array(
+ '#type' => 'select', '#title' => t('First day of week'), '#default_value' => variable_get('date_first_day', 0),
+ '#options' => array(0 => t('Sunday'), 1 => t('Monday'), 2 => t('Tuesday'), 3 => t('Wednesday'), 4 => t('Thursday'), 5 => t('Friday'), 6 => t('Saturday')),
+ '#description' => t('The first day of the week for calendar views.')
+ );
+
+
+ // Site off-line/maintenance settings
+ $form['site_status'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Site maintenance'),
+ '#collapsible' => TRUE,
+ '#collapsed' => TRUE);
+
+ $form['site_status']['site_offline'] = array(
+ '#type' => 'radios',
+ '#title' => t('Site status'),
+ '#default_value' => variable_get('site_offline', 0),
+ '#options' => array(t('Online'), t('Off-line')),
+ '#description' => t('When set to "Online", all visitors will be able to browse your site normally. When set to "Off-line", only users with the "administer site configuration" permission will be able to access your site to perform maintenance; all other visitors will see the site off-line message configured below. Authorized users can log in during "Off-line" mode directly via the user login page.', array('%user-login' => url('user'))),
+ );
+
+ $form['site_status']['site_offline_message'] = array(
+ '#type' => 'textarea',
+ '#title' => t('Site off-line message'),
+ '#default_value' => variable_get('site_offline_message', t('%site is currently under maintenance. We should be back shortly. Thank you for your patience.', array('%site' => variable_get('site_name', t('This Drupal site'))))),
+ '#description' => t('Message to show visitors when the site is in off-line mode.')
+ );
+
+ // String handling: report status and errors.
+ $form['strings'] = array('#type' => 'fieldset', '#title' => t('String handling'), '#collapsible' => TRUE, '#collapsed' => TRUE);
+ $form['strings'] = array_merge($form['strings'], unicode_settings());
+
+ // Cron: report status and errors.
+ $form['cron'] = array('#type' => 'fieldset', '#title' => t('Cron jobs'), '#collapsible' => TRUE, '#collapsed' => TRUE);
+ $form['cron'] = array_merge($form['cron'], system_cron_settings());
+
+ // Check database setup if necessary
+ if (function_exists('db_check_setup') && empty($_POST)) {
+ db_check_setup();
+ }
+ return $form;
+}
+
+/**
+ * Checks the existence of the directory specified in $form_element. This
+ * function is called from the system_view_general form to check both the
+ * file_directory_path and file_directory_temp directories. If validation
+ * fails, the form element is flagged with an error from within the
+ * file_check_directory function.
+ *
+ * @param $form_element
+ * The form element containing the name of the directory to check.
+ */
+function system_check_directory($form_element) {
+ file_check_directory($form_element['#value'], FILE_CREATE_DIRECTORY, $form_element['#parents'][0]);
+ return $form_element;
+}
+
+/**
+ * Return the cron status and errors for admin/settings.
+ */
+function system_cron_settings() {
+ $cron_last = variable_get('cron_last', NULL);
+
+ if (is_numeric($cron_last)) {
+ $status = t('Cron is running. The last cron job ran %time ago.', array('%time' => format_interval(time() - $cron_last)));
+ }
+ else {
+ $status = t('Cron has not run. It appears cron jobs have not been setup on your system. Please check the help pages for configuring cron jobs.', array('%url' => 'http://drupal.org/cron'));
+ }
+
+ $form['settings'] = array('#type' => 'item', '#value' => $status);
+ return $form;
+}
+
+/**
+ * Retrieves the current status of an array of files in the system table.
+ */
+function system_get_files_database(&$files, $type) {
+ // Extract current files from database.
+ $result = db_query("SELECT filename, name, type, status, throttle, schema_version FROM {system} WHERE type = '%s'", $type);
+ while ($file = db_fetch_object($result)) {
+ if (isset($files[$file->name]) && is_object($files[$file->name])) {
+ $file->old_filename = $file->filename;
+ foreach ($file as $key => $value) {
+ if (!isset($files[$file->name]) || !isset($files[$file->name]->$key)) {
+ $files[$file->name]->$key = $value;
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Collect data about all currently available themes
+ */
+function system_theme_data() {
+ // Find themes
+ $themes = system_listing('\.theme$', 'themes');
+
+ // Find theme engines
+ $engines = system_listing('\.engine$', 'themes/engines');
+
+ // can't iterate over array itself as it uses a copy of the array items
+ foreach (array_keys($themes) as $key) {
+ drupal_get_filename('theme', $themes[$key]->name, $themes[$key]->filename);
+ drupal_load('theme', $themes[$key]->name);
+ $themes[$key]->owner = $themes[$key]->filename;
+ $themes[$key]->prefix = $key;
+ }
+
+ // Remove all theme engines from the system table
+ db_query("DELETE FROM {system} WHERE type = 'theme_engine'");
+
+ foreach ($engines as $engine) {
+ // Insert theme engine into system table
+ drupal_get_filename('theme_engine', $engine->name, $engine->filename);
+ drupal_load('theme_engine', $engine->name);
+ db_query("INSERT INTO {system} (name, type, filename, status, throttle, bootstrap) VALUES ('%s', '%s', '%s', %d, %d, %d)", $engine->name, 'theme_engine', $engine->filename, 1, 0, 0);
+
+ // Add templates to the site listing
+ foreach (call_user_func($engine->name . '_templates') as $template) {
+ // Do not double-insert templates with theme files in their directory,
+ // but do register their engine data.
+ if (array_key_exists($template->name, $themes)) {
+ $themes[$template->name]->template = TRUE;
+ $themes[$template->name]->owner = $engine->filename;
+ $themes[$template->name]->prefix = $engine->name;
+ }
+ else {
+ $template->template = TRUE;
+ $template->name = basename(dirname($template->filename));
+ $template->owner = $engine->filename;
+ $template->prefix = $engine->name;
+
+ $themes[$template->name] = $template;
+ }
+ }
+ }
+
+ // Find styles in each theme's directory.
+ foreach ($themes as $theme) {
+ foreach (file_scan_directory(dirname($theme->filename), 'style.css$') as $style) {
+ $style->style = TRUE;
+ $style->template = isset($theme->template) ? $theme->template : FALSE;
+ $style->name = basename(dirname($style->filename));
+ $style->owner = $theme->filename;
+ $style->prefix = $theme->template ? $theme->prefix : $theme->name;
+ // do not double-insert styles with theme files in their directory
+ if (array_key_exists($style->name, $themes)) {
+ continue;
+ }
+ $themes[$style->name] = $style;
+ }
+ }
+
+ // Extract current files from database.
+ system_get_files_database($themes, 'theme');
+
+ db_query("DELETE FROM {system} WHERE type = 'theme'");
+
+ foreach ($themes as $theme) {
+ db_query("INSERT INTO {system} (name, description, type, filename, status, throttle, bootstrap) VALUES ('%s', '%s', '%s', '%s', %d, %d, %d)", $theme->name, $theme->owner, 'theme', $theme->filename, $theme->status, 0, 0);
+ }
+
+ return $themes;
+}
+
+/**
+ * Get a list of available regions from a specified theme.
+ *
+ * @param $theme_key
+ * The name of a theme.
+ * @return
+ * An array of regions in the form $region['name'] = 'description'.
+ */
+function system_region_list($theme_key) {
+ static $list = array();
+
+ if (!array_key_exists($theme_key, $list)) {
+ $theme = db_fetch_object(db_query("SELECT * FROM {system} WHERE type = 'theme' AND name = '%s'", $theme_key));
+
+ // Stylesheets can't have regions; use its theme.
+ if (strpos($theme->filename, '.css')) {
+ return system_region_list(basename(dirname($theme->description)));
+ }
+
+ // If this is a custom theme, load it in before moving on.
+ if (file_exists($file = dirname($theme->filename) .'/' . $theme_key . '.theme')) {
+ include_once "./$file";
+ }
+
+ $regions = array();
+
+ // This theme has defined its own regions.
+ if (function_exists($theme_key . '_regions')) {
+ $regions = call_user_func($theme_key . '_regions');
+ }
+ // File is an engine; include its regions.
+ else if (strpos($theme->description, '.engine')) {
+ include_once './' . $theme->description;
+ $theme_engine = basename($theme->description, '.engine');
+ $regions = function_exists($theme_engine . '_regions') ? call_user_func($theme_engine . '_regions') : array();
+ }
+
+ $list[$theme_key] = $regions;
+ }
+
+ return $list[$theme_key];
+}
+
+/**
+ * Get the name of the default region for a given theme.
+ *
+ * @param $theme
+ * The name of a theme.
+ * @return
+ * A string that is the region name.
+ */
+function system_default_region($theme) {
+ $regions = array_keys(system_region_list($theme));
+ return $regions[0];
+}
+
+/**
+ * Returns an array of files objects of the given type from both the
+ * site-wide directory (i.e. modules/) and site-specific directory
+ * (i.e. sites/somesite/modules/). The returned array will be keyed
+ * using the key specified (name, basename, filename). Using name or
+ * basename will cause site-specific files to shadow files in the
+ * default directories. That is, if a file with the same name appears
+ * in both location, only the site-specific version will be included.
+ *
+ * @param $mask
+ * The regular expression of the files to find.
+ * @param $directory
+ * The subdirectory name in which the files are found. For example,
+ * 'modules' will search in both modules/ and
+ * sites/somesite/modules/.
+ * @param $key
+ * The key to be passed to file_scan_directory().
+ * @param $min_depth
+ * Minimum depth of directories to return files from.
+ *
+ * @return
+ * An array of file objects of the specified type.
+ */
+function system_listing($mask, $directory, $key = 'name', $min_depth = 1) {
+ $config = conf_path();
+ $searchdir = array($directory);
+ $files = array();
+
+ if (file_exists("$config/$directory")) {
+ $searchdir[] = "$config/$directory";
+ }
+
+ // Get current list of items
+ foreach ($searchdir as $dir) {
+ $files = array_merge($files, file_scan_directory($dir, $mask, array('.', '..', 'CVS'), 0, TRUE, $key, $min_depth));
+ }
+
+ return $files;
+}
+
+/**
+ * Assign an initial, default set of blocks for a theme.
+ *
+ * This function is called the first time a new theme is enabled. The new theme
+ * gets a copy of the default theme's blocks, with the difference that if a
+ * particular region isn't available in the new theme, the block is assigned
+ * to the new theme's default region.
+ *
+ * @param $theme
+ * The name of a theme.
+ */
+function system_initialize_theme_blocks($theme) {
+ // Initialize theme's blocks if none already registered.
+ if (!(db_num_rows(db_query("SELECT module FROM {blocks} WHERE theme = '%s'", $theme)))) {
+ $default_theme = variable_get('theme_default', 'bluemarine');
+ $regions = system_region_list($theme);
+ $result = db_query("SELECT * FROM {blocks} WHERE theme = '%s'", $default_theme);
+ while($block = db_fetch_array($result)) {
+ // If the region isn't supported by the theme, assign the block to the theme's default region.
+ if (!array_key_exists($block['region'], $regions)) {
+ $block['region'] = system_default_region($theme);
+ }
+ db_query("INSERT INTO {blocks} (module, delta, theme, status, weight, region, visibility, pages, custom, throttle) VALUES ('%s', '%s', '%s', %d, %d, '%s', %d, '%s', %d, %d)",
+ $block['module'], $block['delta'], $theme, $block['status'], $block['weight'], $block['region'], $block['visibility'], $block['pages'], $block['custom'], $block['throttle']);
+ }
+ }
+}
+
+// Add the submit / reset buttons and run drupal_get_form()
+function system_settings_form($form_id, $form) {
+ $form['buttons']['submit'] = array('#type' => 'submit', '#value' => t('Save configuration') );
+ $form['buttons']['reset'] = array('#type' => 'submit', '#value' => t('Reset to defaults') );
+
+ if (!empty($_POST) && form_get_errors()) {
+ drupal_set_message(t('The settings have not been saved because of the errors.'), 'error');
+ }
+
+ return drupal_get_form($form_id, $form, 'system_settings_form');
+}
+
+function system_theme_settings_submit($form_id, $values) {
+ $op = isset($_POST['op']) ? $_POST['op'] : '';
+ $key = $values['var'];
+
+ // Exclude unnecessary elements.
+ unset($values['var'], $values['submit'], $values['reset'], $values['form_id']);
+
+ if ($op == t('Reset to defaults')) {
+ variable_del($key);
+ drupal_set_message(t('The configuration options have been reset to their default values.'));
+ }
+ else {
+ variable_set($key, $values);
+ drupal_set_message(t('The configuration options have been saved.'));
+ }
+}
+
+/**
+ * Execute the system_settings_form.
+ *
+ * If you want node type configure style handling of your checkboxes,
+ * add an array_filter value to your form.
+ *
+ */
+function system_settings_form_submit($form_id, $values) {
+ $op = isset($_POST['op']) ? $_POST['op'] : '';
+
+ // Exclude unnecessary elements.
+ unset($values['submit'], $values['reset'], $values['form_id']);
+
+ foreach ($values as $key => $value) {
+ if ($op == t('Reset to defaults')) {
+ variable_del($key);
+ }
+ else {
+ if (is_array($value) && isset($values['array_filter'])) {
+ $value = array_keys(array_filter($value));
+ }
+ variable_set($key, $value);
+ }
+ }
+ if ($op == t('Reset to defaults')) {
+ drupal_set_message(t('The configuration options have been reset to their default values.'));
+ }
+ else {
+ drupal_set_message(t('The configuration options have been saved.'));
+ }
+ menu_rebuild();
+}
+
+/**
+ * Menu callback; displays a listing of all themes.
+ */
+function system_themes() {
+ $themes = system_theme_data();
+ ksort($themes);
+
+ foreach ($themes as $info) {
+ $info->screenshot = dirname($info->filename) . '/screenshot.png';
+ $screenshot = file_exists($info->screenshot) ? theme('image', $info->screenshot, t('Screenshot for %theme theme', array('%theme' => $info->name)), '', array('class' => 'screenshot'), false) : t('no screenshot');
+
+ $form[$info->name]['screenshot'] = array('#type' => 'markup', '#value' => $screenshot);
+ $form[$info->name]['description'] = array('#type' => 'item', '#title' => $info->name, '#value' => dirname($info->filename));
+ $options[$info->name] = '';
+ if ($info->status) {
+ $status[] = $info->name;
+ }
+ if ($info->status && (function_exists($info->prefix . '_settings') || function_exists($info->prefix . '_features'))) {
+ $form[$info->name]['operations'] = array('#type' => 'markup', '#value' => l(t('configure'), 'admin/themes/settings/' . $info->name) );
+ }
+ else {
+ // Dummy element for form_render. Cleaner than adding a check in the theme function.
+ $form[$info->name]['operations'] = array();
+ }
+ }
+
+ $form['status'] = array('#type' => 'checkboxes', '#options' => $options, '#default_value' => $status);
+ $form['theme_default'] = array('#type' => 'radios', '#options' => $options, '#default_value' => variable_get('theme_default', 'bluemarine'));
+ $form['buttons']['submit'] = array('#type' => 'submit', '#value' => t('Save configuration') );
+ $form['buttons']['reset'] = array('#type' => 'submit', '#value' => t('Reset to defaults') );
+
+ return drupal_get_form('system_themes', $form);
+}
+
+function theme_system_themes($form) {
+ foreach (element_children($form) as $key) {
+ $row = array();
+ if (is_array($form[$key]['description'])) {
+ $row[] = form_render($form[$key]['screenshot']);
+ $row[] = form_render($form[$key]['description']);
+ $row[] = array('data' => form_render($form['status'][$key]), 'align' => 'center');
+ if ($form['theme_default']) {
+ $row[] = array('data' => form_render($form['theme_default'][$key]), 'align' => 'center');
+ $row[] = array('data' => form_render($form[$key]['operations']), 'align' => 'center');
+ }
+ }
+ $rows[] = $row;
+ }
+
+ $header = array(t('Screenshot'), t('Name'), t('Enabled'), t('Default'), t('Operations'));
+ $output = theme('table', $header, $rows);
+ $output .= form_render($form);
+ return $output;
+}
+
+
+function system_themes_submit($form_id, $values) {
+
+ db_query("UPDATE {system} SET status = 0 WHERE type = 'theme'");
+
+ if ($_POST['op'] == t('Save configuration')) {
+ if (is_array($values['status'])) {
+ foreach ($values['status'] as $key => $choice) {
+ // Always enable the default theme, despite its status checkbox being checked:
+ if ($choice || $values['theme_default'] == $key) {
+ // If theme status is being set to 1 from 0, initialize block data for this theme if necessary.
+ if (db_num_rows(db_query("SELECT status FROM {system} WHERE type = 'theme' AND name = '%s' AND status = 0", $key))) {
+ system_initialize_theme_blocks($key);
+ }
+ db_query("UPDATE {system} SET status = 1 WHERE type = 'theme' and name = '%s'", $key);
+ }
+ }
+ }
+ variable_set('theme_default', $values['theme_default']);
+ }
+ else {
+ variable_del('theme_default');
+ db_query("UPDATE {system} SET status = 1 WHERE type = 'theme' AND name = 'bluemarine'");
+ }
+
+ menu_rebuild();
+ drupal_set_message(t('The configuration options have been saved.'));
+ return 'admin/themes';
+}
+
+/**
+ * Menu callback; displays a listing of all modules.
+ */
+function system_modules() {
+ // Get current list of modules
+ $files = system_listing('\.module$', 'modules', 'name', 0);
+
+ // Extract current files from database.
+ system_get_files_database($files, 'module');
+
+ ksort($files);
+
+ foreach ($files as $filename => $file) {
+ drupal_get_filename('module', $file->name, $file->filename);
+ drupal_load('module', $file->name);
+
+ $file->description = module_invoke($file->name, 'help', 'admin/modules#description');
+
+ $form['name'][$file->name] = array('#value' => $file->name);
+ $form['description'][$file->name] = array('#value' => $file->description);
+ $options[$file->name] = '';
+ if ($file->status) {
+ $status[] = $file->name;
+ }
+ if ($file->throttle) {
+ $throttle[] = $file->name;
+ }
+
+ // log the critical hooks implemented by this module
+ $bootstrap = 0;
+ foreach (bootstrap_hooks() as $hook) {
+ if (module_hook($file->name, $hook)) {
+ $bootstrap = 1;
+ break;
+ }
+ }
+
+ // Update the contents of the system table:
+ if (isset($file->status) || (isset($file->old_filename) && $file->old_filename != $file->filename)) {
+ db_query("UPDATE {system} SET description = '%s', name = '%s', bootstrap = %d, filename = '%s' WHERE filename = '%s'", $file->description, $file->name, $bootstrap, $file->filename, $file->old_filename);
+ }
+ else {
+ // This is a new module.
+ db_query("INSERT INTO {system} (name, description, type, filename, status, throttle, bootstrap) VALUES ('%s', '%s', '%s', '%s', %d, %d, %d)", $file->name, $file->description, 'module', $file->filename, $file->status, $file->throttle, $bootstrap);
+ }
+ }
+
+
+ // Handle status checkboxes, including overriding the generated
+ // checkboxes for required modules.
+ $form['status'] = array('#type' => 'checkboxes', '#default_value' => $status, '#options' => $options);
+ $required = array('block', 'filter', 'node', 'system', 'user', 'watchdog');
+ foreach ($required as $require) {
+ $form['status'][$require] = array('#type' => 'hidden', '#value' => 1, '#suffix' => t('required'));
+ }
+
+ /**
+ * Handle throttle checkboxes, including overriding the generated checkboxes for required modules.
+ */
+ if (module_exist('throttle')) {
+ $form['throttle'] = array('#type' => 'checkboxes', '#default_value' => $throttle, '#options' => $options);
+ $throttle_required = array_merge($required, array('throttle'));
+ foreach ($throttle_required as $require) {
+ $form['throttle'][$require] = array('#type' => 'hidden', '#value' => 0, '#suffix' => t('required'));
+ }
+ }
+
+ $form['buttons']['submit'] = array('#type' => 'submit', '#value' => t('Save configuration'));
+
+ return drupal_get_form('system_modules', $form);
+}
+
+function theme_system_modules($form) {
+ foreach (element_children($form['name']) as $key) {
+ $row = array();
+ $row[] = form_render($form['name'][$key]);
+ $row[] = form_render($form['description'][$key]);
+ $row[] = array('data' => form_render($form['status'][$key]), 'align' => 'center');
+
+ if (module_exist('throttle')) {
+ $row[] = array('data' => form_render($form['throttle'][$key]), 'align' => 'center');
+ }
+ $rows[] = $row;
+ }
+
+ $header = array(t('Name'), t('Description'), t('Enabled'));
+ if (module_exist('throttle')) {
+ $header[] = t('Throttle');
+ }
+
+ $output = theme('table', $header, $rows);
+ $output .= form_render($form);
+ return $output;
+}
+
+
+function system_modules_submit($form_id, $edit) {
+ db_query("UPDATE {system} SET status = 0, throttle = 0 WHERE type = 'module'");
+
+ $new_modules = array();
+ foreach ($edit['status'] as $key => $choice) {
+ if ($choice) {
+ db_query("UPDATE {system} SET status = 1 WHERE type = 'module' AND name = '%s'", $key);
+ if (!module_exist($key)) {
+ $new_modules[] = $key;
+ }
+ }
+ }
+
+ if (is_array($edit['throttle'])) {
+ foreach ($edit['throttle'] as $key => $choice) {
+ if ($choice) {
+ db_query("UPDATE {system} SET throttle = 1 WHERE type = 'module' and name = '%s'", $key);
+ }
+ }
+ }
+
+ module_list(TRUE, FALSE);
+
+ include './includes/install.inc';
+ foreach ($new_modules as $module) {
+ // Set the installed schema version for newly-enabled modules
+ $versions = drupal_get_schema_versions($module);
+ if (drupal_get_installed_schema_version($module) == SCHEMA_UNINSTALLED) {
+ drupal_set_installed_schema_version($module, $versions ? max($versions) : SCHEMA_INSTALLED);
+ module_invoke($module, 'install');
+ }
+ }
+
+ menu_rebuild();
+
+ drupal_set_message(t('The configuration options have been saved.'));
+ return 'admin/modules';
+}
+
+
+/**
+ * Menu callback; displays a module's settings page.
+ */
+function system_site_settings($module = NULL) {
+
+ if ($module) {
+ $form = module_invoke($module, 'settings');
+ }
+ else {
+ $form = system_view_general();
+ $module = 'system';
+ }
+
+ return system_settings_form($module . '_settings_form', $form);
+}
+
+/**
+ * Menu callback; display theme configuration for entire site and individual themes.
+ */
+function system_theme_settings($key = '') {
+ $directory_path = file_directory_path();
+ file_check_directory($directory_path, FILE_CREATE_DIRECTORY, 'file_directory_path');
+
+ // Default settings are defined in theme_get_settings() in includes/theme.inc
+ if ($key) {
+ $settings = theme_get_settings($key);
+ $var = str_replace('/', '_', 'theme_'. $key .'_settings');
+ $themes = system_theme_data();
+ $features = function_exists($themes[$key]->prefix . '_features') ? call_user_func($themes[$key]->prefix . '_features') : array();
+ }
+ else {
+ $settings = theme_get_settings('');
+ $var = 'theme_settings';
+ }
+
+ $form['var'] = array('#type' => 'hidden', '#value' => $var);
+
+ // Check for a new uploaded logo, and use that instead.
+ if ($file = file_check_upload('logo_upload')) {
+ if ($info = image_get_info($file->filepath)) {
+ $parts = pathinfo($file->filename);
+ $filename = ($key) ? str_replace('/', '_', $key) . '_logo.' . $parts['extension'] : 'logo.' . $parts['extension'];
+
+ if ($file = file_save_upload('logo_upload', $filename, 1)) {
+ $_POST['edit']['default_logo'] = 0;
+ $_POST['edit']['logo_path'] = $file->filepath;
+ $_POST['edit']['toggle_logo'] = 1;
+ }
+ }
+ else {
+ form_set_error('file_upload', t('Only JPEG, PNG and GIF images are allowed to be used as logos.'));
+ }
+ }
+
+ // Check for a new uploaded favicon, and use that instead.
+ if ($file = file_check_upload('favicon_upload')) {
+ $parts = pathinfo($file->filename);
+ $filename = ($key) ? str_replace('/', '_', $key) . '_favicon.' . $parts['extension'] : 'favicon.' . $parts['extension'];
+
+ if ($file = file_save_upload('favicon_upload', $filename, 1)) {
+ $_POST['edit']['default_favicon'] = 0;
+ $_POST['edit']['favicon_path'] = $file->filepath;
+ $_POST['edit']['toggle_favicon'] = 1;
+ }
+ }
+
+ // Toggle settings
+ $toggles = array(
+ 'toggle_logo' => t('Logo'),
+ 'toggle_name' => t('Site name'),
+ 'toggle_slogan' => t('Site slogan'),
+ 'toggle_mission' => t('Mission statement'),
+ 'toggle_node_user_picture' => t('User pictures in posts'),
+ 'toggle_comment_user_picture' => t('User pictures in comments'),
+ 'toggle_search' => t('Search box'),
+ 'toggle_favicon' => t('Shortcut icon')
+ );
+
+ // Some features are not always available
+ $disabled = array();
+ if (!variable_get('user_pictures', 0)) {
+ $disabled['toggle_node_user_picture'] = true;
+ $disabled['toggle_comment_user_picture'] = true;
+ }
+ if (!module_exist('search')) {
+ $disabled['toggle_search'] = true;
+ }
+
+ $form['theme_settings'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Toggle display'),
+ '#description' => t('Enable or disable the display of certain page elements.'),
+ );
+ foreach ($toggles as $name => $title) {
+ if ((!$key) || in_array($name, $features)) {
+ // disable search box if search.module is disabled
+ $form['theme_settings'][$name] = array('#type' => 'checkbox', '#title' => $title, '#default_value' => $settings[$name]);
+ if (isset($disabled[$name])) {
+ $form['theme_settings'][$name]['#attributes'] = array('disabled' => 'disabled');
+ }
+ }
+ }
+
+ // System wide only settings.
+ if (!$key) {
+ // Create neat 2-column layout for the toggles
+ $form['theme_settings'] += array(
+ '#prefix' => '',
+ '#suffix' => '',
+ );
+
+ // Toggle node display.
+ $node_types = module_invoke('node', 'get_types');
+ if ($node_types) {
+ $form['node_info'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Display post information on'),
+ '#description' => t('Enable or disable the submitted by Username on date text when displaying posts of the following type.'),
+ '#prefix' => '',
+ '#suffix' => '',
+ );
+ foreach ($node_types as $type => $name) {
+ $form['node_info']["toggle_node_info_$type"] = array('#type' => 'checkbox', '#title' => $name, '#default_value' => $settings["toggle_node_info_$type"]);
+ }
+ }
+ }
+
+ // Logo settings
+ if ((!$key) || in_array('toggle_logo', $features)) {
+ $form['logo'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Logo image settings'),
+ '#description' => t('If toggled on, the following logo will be displayed.'),
+ '#attributes' => array('class' => 'theme-settings-bottom'),
+ );
+ $form['logo']["default_logo"] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Use the default logo'),
+ '#default_value' => $settings['default_logo'],
+ '#tree' => FALSE,
+ '#description' => t('Check here if you want the theme to use the logo supplied with it.')
+ );
+ $form['logo']['logo_path'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Path to custom logo'),
+ '#default_value' => $settings['logo_path'],
+ '#description' => t('The path to the file you would like to use as your logo file instead of the default logo.'));
+
+ $form['logo']['logo_upload'] = array(
+ '#type' => 'file',
+ '#title' => t('Upload logo image'),
+ '#maxlength' => 40,
+ '#description' => t("If you don't have direct file access to the server, use this field to upload your logo.")
+ );
+ }
+
+ // Icon settings
+ if ((!$key) || in_array('toggle_favicon', $features)) {
+ $form['favicon'] = array('#type' => 'fieldset', '#title' => t('Shortcut icon settings'));
+ $form['favicon']['text'] = array('#value' => t('Your shortcut icon or \'favicon\' is displayed in the address bar and bookmarks of most browsers.'));
+ $form['favicon']['default_favicon'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Use the default shortcut icon.'),
+ '#default_value' => $settings['default_favicon'],
+ '#description' => t('Check here if you want the theme to use the default shortcut icon.')
+ );
+ $form['favicon']['favicon_path'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Path to custom icon'),
+ '#default_value' => $settings['favicon_path'],
+ '#description' => t('The path to the image file you would like to use as your custom shortcut icon.')
+ );
+
+ $form['favicon']['favicon_upload'] = array(
+ '#type' => 'file',
+ '#title' => t('Upload icon image'),
+ '#description' => t("If you don't have direct file access to the server, use this field to upload your shortcut icon.")
+ );
+ }
+
+ if ($key) {
+ // Template-specific settings
+ $function = $themes[$key]->prefix .'_settings';
+ if (function_exists($function)) {
+ if ($themes[$key]->template) {
+ // file is a template or a style of a template
+ $form['specific'] = array('#type' => 'fieldset', '#title' => t('Engine-specific settings'), '#description' => t('These settings only exist for all the templates and styles based on the %engine theme engine.', array('%engine' => $themes[$key]->prefix)));
+ }
+ else {
+ // file is a theme or a style of a theme
+ $form['specific'] = array('#type' => 'fieldset', '#title' => t('Theme-specific settings'), '#description' => t('These settings only exist for the %theme theme and all the styles based on it.', array('%theme' => $themes[$key]->prefix)));
+ }
+ $group = $function();
+ $form['specific'] = array_merge($form['specific'], (is_array($group) ? $group : array()));
+ }
+ }
+ $form['#attributes'] = array('enctype' => 'multipart/form-data');
+
+ return system_settings_form('system_theme_settings', $form);
+
+}
+
+/**
+ * Output a confirmation form
+ *
+ * This function outputs a complete form for confirming an action. A link is
+ * offered to go back to the item that is being changed in case the user changes
+ * his/her mind.
+ *
+ * You should use $GLOBALS['values']['edit'][$name] (where $name is usually 'confirm') to
+ * check if the confirmation was successful.
+ *
+ * @param $form_id
+ * The unique form identifier. Used by the form API to construct the theme.
+ * @param $form
+ * Additional elements to inject into the form, for example hidden elements.
+ * @param $question
+ * The question to ask the user (e.g. "Are you sure you want to delete the
+ * block foo?").
+ * @param $path
+ * The page to go to if the user denies the action.
+ * @param $description
+ * Additional text to display (defaults to "This action cannot be undone.").
+ * @param $yes
+ * A caption for the button which confirms the action (e.g. "Delete",
+ * "Replace", ...).
+ * @param $no
+ * A caption for the link which denies the action (e.g. "Cancel").
+ * @param $name
+ * The internal name used to refer to the confirmation item.
+ * @return
+ * A themed HTML string representing the form.
+ */
+
+function confirm_form($form_id, $form, $question, $path, $description = NULL, $yes = NULL, $no = NULL, $name = 'confirm') {
+
+ $description = ($description) ? $description : t('This action cannot be undone.');
+ drupal_set_title($question);
+ $form['#attributes'] = array('class' => 'confirmation');
+ $form['description'] = array('#value' => $description);
+ $form[$name] = array('#type' => 'hidden', '#value' => 1);
+
+ $form['actions'] = array('#prefix' => '', '#suffix' => '');
+ $form['actions']['submit'] = array('#type' => 'submit', '#value' => $yes ? $yes : t('Confirm'));
+ $form['actions']['cancel'] = array('#value' => l($no ? $no : t('Cancel'), $path));
+ return drupal_get_form($form_id, $form, 'confirm_form');
+}
diff --git a/modules/taxonomy.module b/modules/taxonomy.module
new file mode 100644
index 0000000..1bb18d5
--- /dev/null
+++ b/modules/taxonomy.module
@@ -0,0 +1,1365 @@
+links(taxonomy_link('taxonomy terms', $node));
+ * }
+ */
+function taxonomy_link($type, $node = NULL) {
+ if ($type == 'taxonomy terms' && $node != NULL) {
+ $links = array();
+ if (array_key_exists('taxonomy', $node)) {
+ foreach ($node->taxonomy as $term) {
+ $links[] = l($term->name, taxonomy_term_path($term), array('rel' => 'tag', 'title' => strip_tags($term->description)));
+ }
+ }
+ return $links;
+ }
+}
+
+function taxonomy_term_path($term) {
+ $vocabulary = taxonomy_get_vocabulary($term->vid);
+ if ($vocabulary->module != 'taxonomy' && $path = module_invoke($vocabulary->module, 'term_path', $term)) {
+ return $path;
+ }
+ return 'taxonomy/term/'. $term->tid;
+}
+
+/**
+ * Implementation of hook_menu().
+ */
+function taxonomy_menu($may_cache) {
+ $items = array();
+
+ if ($may_cache) {
+ $items[] = array('path' => 'admin/taxonomy',
+ 'title' => t('categories'),
+ 'callback' => 'taxonomy_overview_vocabularies',
+ 'access' => user_access('administer taxonomy'));
+
+ $items[] = array('path' => 'admin/taxonomy/list',
+ 'title' => t('list'),
+ 'type' => MENU_DEFAULT_LOCAL_TASK,
+ 'weight' => -10);
+
+ $items[] = array('path' => 'admin/taxonomy/add/vocabulary',
+ 'title' => t('add vocabulary'),
+ 'callback' => 'taxonomy_admin_vocabulary_edit',
+ 'access' => user_access('administer taxonomy'),
+ 'type' => MENU_LOCAL_TASK);
+
+ $items[] = array('path' => 'admin/taxonomy/edit/vocabulary',
+ 'title' => t('edit vocabulary'),
+ 'callback' => 'taxonomy_admin_vocabulary_edit',
+ 'access' => user_access('administer taxonomy'),
+ 'type' => MENU_CALLBACK);
+
+ $items[] = array('path' => 'admin/taxonomy/edit/term',
+ 'title' => t('edit term'),
+ 'callback' => 'taxonomy_admin_term_edit',
+ 'access' => user_access('administer taxonomy'),
+ 'type' => MENU_CALLBACK);
+
+ $items[] = array('path' => 'taxonomy/term',
+ 'title' => t('taxonomy term'),
+ 'callback' => 'taxonomy_term_page',
+ 'access' => user_access('access content'),
+ 'type' => MENU_CALLBACK);
+
+ $items[] = array('path' => 'taxonomy/autocomplete',
+ 'title' => t('autocomplete taxonomy'),
+ 'callback' => 'taxonomy_autocomplete',
+ 'access' => user_access('access content'),
+ 'type' => MENU_CALLBACK);
+ }
+ else {
+ if (is_numeric(arg(2))) {
+ $items[] = array('path' => 'admin/taxonomy/' . arg(2),
+ 'title' => t('list terms'),
+ 'callback' => 'taxonomy_overview_terms',
+ 'callback arguments' => array(arg(2)),
+ 'access' => user_access('administer taxonomy'),
+ 'type' => MENU_CALLBACK);
+
+ $items[] = array('path' => 'admin/taxonomy/' . arg(2) . '/list',
+ 'title' => t('list'),
+ 'type' => MENU_DEFAULT_LOCAL_TASK,
+ 'weight' => -10);
+
+ $items[] = array('path' => 'admin/taxonomy/' . arg(2) . '/add/term',
+ 'title' => t('add term'),
+ 'callback' => 'taxonomy_form_term',
+ 'callback arguments' => array(array('vid' => arg(2))),
+ 'access' => user_access('administer taxonomy'),
+ 'type' => MENU_LOCAL_TASK);
+ }
+ }
+
+ return $items;
+}
+
+/**
+ * List and manage vocabularies.
+ */
+function taxonomy_overview_vocabularies() {
+ $vocabularies = taxonomy_get_vocabularies();
+ $rows = array();
+ foreach ($vocabularies as $vocabulary) {
+ $types = array();
+ foreach ($vocabulary->nodes as $type) {
+ $node_type = node_get_name($type);
+ $types[] = $node_type ? $node_type : $type;
+ }
+ $rows[] = array('name' => check_plain($vocabulary->name),
+ 'type' => implode(', ', $types),
+ 'edit' => l(t('edit vocabulary'), "admin/taxonomy/edit/vocabulary/$vocabulary->vid"),
+ 'list' => l(t('list terms'), "admin/taxonomy/$vocabulary->vid"),
+ 'add' => l(t('add terms'), "admin/taxonomy/$vocabulary->vid/add/term")
+ );
+ }
+ if (empty($rows)) {
+ $rows[] = array(array('data' => t('No categories available.'), 'colspan' => '5', 'class' => 'message'));
+ }
+ $header = array(t('Name'), t('Type'), array('data' => t('Operations'), 'colspan' => '3'));
+
+ return theme('table', $header, $rows, array('id' => 'taxonomy'));
+}
+
+/**
+ * Display a tree of all the terms in a vocabulary, with options to edit
+ * each one.
+ */
+function taxonomy_overview_terms($vid) {
+ $destination = drupal_get_destination();
+
+ $header = array(t('Name'), t('Operations'));
+ $vocabulary = taxonomy_get_vocabulary($vid);
+
+ drupal_set_title(check_plain($vocabulary->name));
+ $start_from = $_GET['page'] ? $_GET['page'] : 0;
+ $total_entries = 0; // total count for pager
+ $page_increment = 25; // number of tids per page
+ $displayed_count = 0; // number of tids shown
+
+ $tree = taxonomy_get_tree($vocabulary->vid);
+ foreach ($tree as $term) {
+ $total_entries++; // we're counting all-totals, not displayed
+ if (($start_from && ($start_from * $page_increment) >= $total_entries) || ($displayed_count == $page_increment)) { continue; }
+ $rows[] = array(_taxonomy_depth($term->depth) . ' ' . l($term->name, "taxonomy/term/$term->tid"), l(t('edit'), "admin/taxonomy/edit/term/$term->tid", array(), $destination));
+ $displayed_count++; // we're counting tids displayed
+ }
+
+ if (!$rows) {
+ $rows[] = array(array('data' => t('No terms available.'), 'colspan' => '2'));
+ }
+
+ $GLOBALS['pager_page_array'][] = $start_from; // FIXME
+ $GLOBALS['pager_total'][] = intval($total_entries / $page_increment) + 1; // FIXME
+
+ if ($total_entries >= $page_increment) {
+ $rows[] = array(array('data' => theme('pager', NULL, $page_increment), 'colspan' => '2'));
+ }
+
+ return theme('table', $header, $rows, array('id' => 'taxonomy'));
+}
+
+/**
+ * Display form for adding and editing vocabularies.
+ */
+function taxonomy_form_vocabulary($edit = array()) {
+ $form['name'] = array('#type' => 'textfield',
+ '#title' => t('Vocabulary name'),
+ '#default_value' => $edit['name'],
+ '#maxlength' => 64,
+ '#description' => t('The name for this vocabulary. Example: "Topic".'),
+ '#required' => TRUE,
+ );
+ $form['description'] = array('#type' => 'textarea',
+ '#title' => t('Description'),
+ '#default_value' => $edit['description'],
+ '#description' => t('Description of the vocabulary; can be used by modules.'),
+ );
+ $form['help'] = array('#type' => 'textfield',
+ '#title' => t('Help text'),
+ '#default_value' => $edit['help'],
+ '#description' => t('Instructions to present to the user when choosing a term.'),
+ );
+ $form['nodes'] = array('#type' => 'checkboxes',
+ '#title' => t('Types'),
+ '#default_value' => $edit['nodes'],
+ '#options' => node_get_types(),
+ '#description' => t('A list of node types you want to associate with this vocabulary.'),
+ '#required' => TRUE,
+ );
+ $form['hierarchy'] = array('#type' => 'radios',
+ '#title' => t('Hierarchy'),
+ '#default_value' => $edit['hierarchy'],
+ '#options' => array(t('Disabled'), t('Single'), t('Multiple')),
+ '#description' => t('Allows a tree-like hierarchy between terms of this vocabulary.', array('%help-url' => url('admin/help/taxonomy', NULL, NULL, 'hierarchy'))),
+ );
+ $form['relations'] = array('#type' => 'checkbox',
+ '#title' => t('Related terms'),
+ '#default_value' => $edit['relations'],
+ '#description' => t('Allows related terms in this vocabulary.', array('%help-url' => url('admin/help/taxonomy', NULL, NULL, 'related-terms'))),
+ );
+ $form['tags'] = array('#type' => 'checkbox',
+ '#title' => t('Free tagging'),
+ '#default_value' => $edit['tags'],
+ '#description' => t('Content is categorized by typing terms instead of choosing from a list.'),
+ );
+ $form['multiple'] = array('#type' => 'checkbox',
+ '#title' => t('Multiple select'),
+ '#default_value' => $edit['multiple'],
+ '#description' => t('Allows nodes to have more than one term from this vocabulary (always true for free tagging).'),
+ );
+ $form['required'] = array('#type' => 'checkbox',
+ '#title' => t('Required'),
+ '#default_value' => $edit['required'],
+ '#description' => t('If enabled, every node must have at least one term in this vocabulary.'),
+ );
+ $form['weight'] = array('#type' => 'weight',
+ '#title' => t('Weight'),
+ '#default_value' => $edit['weight'],
+ '#description' => t('In listings, the heavier vocabularies will sink and the lighter vocabularies will be positioned nearer the top.'),
+ );
+
+ // Add extra vocabulary form elements.
+ $extra = module_invoke_all('taxonomy', 'form', 'vocabulary');
+ if (is_array($extra)) {
+ foreach ($extra as $key => $element) {
+ $extra[$key]['#weight'] = isset($extra[$key]['#weight']) ? $extra[$key]['#weight'] : -18;
+ }
+ $form = array_merge($form, $extra);
+ }
+
+ $form['submit'] = array('#type' => 'submit', '#value' => t('Submit'));
+ if ($edit['vid']) {
+ $form['delete'] = array('#type' => 'submit', '#value' => t('Delete'));
+ $form['vid'] = array('#type' => 'value', '#value' => $edit['vid']);
+ $form['module'] = array('#type' => 'value', '#value' => $edit['module']);
+ }
+ return drupal_get_form('taxonomy_form_vocabulary', $form);
+}
+
+/**
+ * Accept the form submission for a vocabulary and save the results.
+ */
+function taxonomy_form_vocabulary_submit($form_id, $form_values) {
+ // Fix up the nodes array to remove unchecked nodes.
+ $form_values['nodes'] = array_filter($form_values['nodes']);
+ switch (taxonomy_save_vocabulary($form_values)) {
+ case SAVED_NEW:
+ drupal_set_message(t('Created new vocabulary %name.', array('%name' => theme('placeholder', $form_values['name']))));
+ break;
+ case SAVED_UPDATED:
+ drupal_set_message(t('Updated vocabulary %name.', array('%name' => theme('placeholder', $form_values['name']))));
+ break;
+ }
+ return 'admin/taxonomy';
+}
+
+function taxonomy_save_vocabulary(&$edit) {
+ $edit['nodes'] = empty($edit['nodes']) ? array() : $edit['nodes'];
+
+ if ($edit['vid'] && $edit['name']) {
+ db_query("UPDATE {vocabulary} SET name = '%s', description = '%s', help = '%s', multiple = %d, required = %d, hierarchy = %d, relations = %d, tags = %d, weight = %d, module = '%s' WHERE vid = %d", $edit['name'], $edit['description'], $edit['help'], $edit['multiple'], $edit['required'], $edit['hierarchy'], $edit['relations'], $edit['tags'], $edit['weight'], isset($edit['module']) ? $edit['module'] : 'taxonomy', $edit['vid']);
+ db_query("DELETE FROM {vocabulary_node_types} WHERE vid = %d", $edit['vid']);
+ foreach ($edit['nodes'] as $type => $selected) {
+ db_query("INSERT INTO {vocabulary_node_types} (vid, type) VALUES (%d, '%s')", $edit['vid'], $type);
+ }
+ module_invoke_all('taxonomy', 'update', 'vocabulary', $edit);
+ $status = SAVED_UPDATED;
+ }
+ else if ($edit['vid']) {
+ $status = taxonomy_del_vocabulary($edit['vid']);
+ }
+ else {
+ $edit['vid'] = db_next_id('{vocabulary}_vid');
+ db_query("INSERT INTO {vocabulary} (vid, name, description, help, multiple, required, hierarchy, relations, tags, weight, module) VALUES (%d, '%s', '%s', '%s', %d, %d, %d, %d, %d, %d, '%s')", $edit['vid'], $edit['name'], $edit['description'], $edit['help'], $edit['multiple'], $edit['required'], $edit['hierarchy'], $edit['relations'], $edit['tags'], $edit['weight'], isset($edit['module']) ? $edit['module'] : 'taxonomy');
+ foreach ($edit['nodes'] as $type => $selected) {
+ db_query("INSERT INTO {vocabulary_node_types} (vid, type) VALUES (%d, '%s')", $edit['vid'], $type);
+ }
+ module_invoke_all('taxonomy', 'insert', 'vocabulary', $edit);
+ $status = SAVED_NEW;
+ }
+
+ cache_clear_all();
+
+ return $status;
+}
+
+function taxonomy_del_vocabulary($vid) {
+ $vocabulary = (array) taxonomy_get_vocabulary($vid);
+
+ db_query('DELETE FROM {vocabulary} WHERE vid = %d', $vid);
+ db_query('DELETE FROM {vocabulary_node_types} WHERE vid = %d', $vid);
+ $result = db_query('SELECT tid FROM {term_data} WHERE vid = %d', $vid);
+ while ($term = db_fetch_object($result)) {
+ taxonomy_del_term($term->tid);
+ }
+
+ module_invoke_all('taxonomy', 'delete', 'vocabulary', $vocabulary);
+
+ cache_clear_all();
+
+ return SAVED_DELETED;
+}
+
+function _taxonomy_confirm_del_vocabulary($vid) {
+ $vocabulary = taxonomy_get_vocabulary($vid);
+
+ $form['type'] = array('#type' => 'value', '#value' => 'vocabulary');
+ $form['vid'] = array('#type' => 'value', '#value' => $vid);
+ $form['name'] = array('#type' => 'value', '#value' => $vocabulary->name);
+ return confirm_form('taxonomy_vocabulary_confirm_delete', $form,
+ t('Are you sure you want to delete the vocabulary %title?',
+ array('%title' => theme('placeholder', $vocabulary->name))),
+ 'admin/taxonomy', t('Deleting a vocabulary will delete all the terms in it. This action cannot be undone.'),
+ t('Delete'),
+ t('Cancel'));
+}
+
+function taxonomy_vocabulary_confirm_delete_submit($form_id, $form_values) {
+ $status = taxonomy_del_vocabulary($form_values['vid']);
+ drupal_set_message(t('Deleted vocabulary %name.', array('%name' => theme('placeholder', $form_values['name']))));
+ return 'admin/taxonomy';
+}
+
+function taxonomy_form_term($edit = array()) {
+ $vocabulary_id = isset($edit['vid']) ? $edit['vid'] : arg(4);
+ $vocabulary = taxonomy_get_vocabulary($vocabulary_id);
+
+ $form['name'] = array('#type' => 'textfield', '#title' => t('Term name'), '#default_value' => $edit['name'], '#maxlength' => 64, '#description' => t('The name for this term. Example: "Linux".'), '#required' => TRUE);
+
+ $form['description'] = array('#type' => 'textarea', '#title' => t('Description'), '#default_value' => $edit['description'], '#description' => t('A description of the term.'));
+
+ if ($vocabulary->hierarchy) {
+ $parent = array_keys(taxonomy_get_parents($edit['tid']));
+ $children = taxonomy_get_tree($vocabulary_id, $edit['tid']);
+
+ // A term can't be the child of itself, nor of its children.
+ foreach ($children as $child) {
+ $exclude[] = $child->tid;
+ }
+ $exclude[] = $edit['tid'];
+
+ if ($vocabulary->hierarchy == 1) {
+ $form['parent'] = _taxonomy_term_select(t('Parent'), 'parent', $parent, $vocabulary_id, l(t('Parent term'), 'admin/help/taxonomy', NULL, NULL, 'parent') .'.', 0, '<'. t('root') .'>', $exclude);
+ }
+ elseif ($vocabulary->hierarchy == 2) {
+ $form['parent'] = _taxonomy_term_select(t('Parents'), 'parent', $parent, $vocabulary_id, l(t('Parent terms'), 'admin/help/taxonomy', NULL, NULL, 'parent') .'.', 1, '<'. t('root') .'>', $exclude);
+ }
+ }
+
+ if ($vocabulary->relations) {
+ $form['relations'] = _taxonomy_term_select(t('Related terms'), 'relations', array_keys(taxonomy_get_related($edit['tid'])), $vocabulary_id, NULL, 1, '<'. t('none') .'>', array($edit['tid']));
+ }
+
+ $form['synonyms'] = array('#type' => 'textarea', '#title' => t('Synonyms'), '#default_value' => implode("\n", taxonomy_get_synonyms($edit['tid'])), '#description' => t('Synonyms of this term, one synonym per line.', array('%help-url' => url('admin/help/taxonomy', NULL, NULL, 'synonyms'))));
+ $form['weight'] = array('#type' => 'weight', '#title' => t('Weight'), '#default_value' => $edit['weight'], '#description' => t('In listings, the heavier terms will sink and the lighter terms will be positioned nearer the top.'));
+
+ // Add extra term form elements.
+ $extra = module_invoke_all('taxonomy', 'form', 'term');
+ if (is_array($extra)) {
+ foreach ($extra as $key => $element) {
+ $extra[$key]['#weight'] = isset($extra[$key]['#weight']) ? $extra[$key]['#weight'] : -18;
+ }
+ $form = array_merge($form, $extra);
+ }
+
+
+ $form['vid'] = array('#type' => 'value', '#value' => $vocabulary->vid);
+ $form['submit'] = array('#type' => 'submit', '#value' => t('Submit'));
+
+ if ($edit['tid']) {
+ $form['delete'] = array('#type' => 'submit', '#value' => t('Delete'));
+ $form['tid'] = array('#type' => 'value', '#value' => $edit['tid']);
+ }
+ else {
+ $form['destination'] = array('#type' => 'hidden', '#value' => $_GET['q']);
+ }
+
+ return drupal_get_form('taxonomy_form_term', $form);
+}
+
+/**
+ * Accept the form submission for a taxonomy term and save the result.
+ */
+function taxonomy_form_term_submit($form_id, $form_values) {
+ switch (taxonomy_save_term($form_values)) {
+ case SAVED_NEW:
+ drupal_set_message(t('Created new term %term.', array('%term' => theme('placeholder', $form_values['name']))));
+ break;
+ case SAVED_UPDATED:
+ drupal_set_message(t('The term %term has been updated.', array('%term' => theme('placeholder', $form_values['name']))));
+ break;
+ }
+ return 'admin/taxonomy';
+}
+
+function taxonomy_save_term(&$edit) {
+ if ($edit['tid'] && $edit['name']) {
+ db_query("UPDATE {term_data} SET name = '%s', description = '%s', weight = %d WHERE tid = %d", $edit['name'], $edit['description'], $edit['weight'], $edit['tid']);
+ module_invoke_all('taxonomy', 'update', 'term', $edit);
+ $status = SAVED_UPDATED;
+ }
+ else if ($edit['tid']) {
+ return taxonomy_del_term($edit['tid']);
+ }
+ else {
+ $edit['tid'] = db_next_id('{term_data}_tid');
+ db_query("INSERT INTO {term_data} (tid, name, description, vid, weight) VALUES (%d, '%s', '%s', %d, %d)", $edit['tid'], $edit['name'], $edit['description'], $edit['vid'], $edit['weight']);
+ module_invoke_all('taxonomy', 'insert', 'term', $edit);
+ $status = SAVED_NEW;
+ }
+
+ db_query('DELETE FROM {term_relation} WHERE tid1 = %d OR tid2 = %d', $edit['tid'], $edit['tid']);
+ if ($edit['relations']) {
+ foreach ($edit['relations'] as $related_id) {
+ if ($related_id != 0) {
+ db_query('INSERT INTO {term_relation} (tid1, tid2) VALUES (%d, %d)', $edit['tid'], $related_id);
+ }
+ }
+ }
+
+ db_query('DELETE FROM {term_hierarchy} WHERE tid = %d', $edit['tid']);
+ if (!isset($edit['parent']) || empty($edit['parent'])) {
+ $edit['parent'] = array(0);
+ }
+ if (is_array($edit['parent'])) {
+ foreach ($edit['parent'] as $parent) {
+ if (is_array($parent)) {
+ foreach ($parent as $tid) {
+ db_query('INSERT INTO {term_hierarchy} (tid, parent) VALUES (%d, %d)', $edit['tid'], $tid);
+ }
+ }
+ else {
+ db_query('INSERT INTO {term_hierarchy} (tid, parent) VALUES (%d, %d)', $edit['tid'], $parent);
+ }
+ }
+ }
+ else {
+ db_query('INSERT INTO {term_hierarchy} (tid, parent) VALUES (%d, %d)', $edit['tid'], $edit['parent']);
+ }
+
+ db_query('DELETE FROM {term_synonym} WHERE tid = %d', $edit['tid']);
+ if ($edit['synonyms']) {
+ foreach (explode ("\n", str_replace("\r", '', $edit['synonyms'])) as $synonym) {
+ if ($synonym) {
+ db_query("INSERT INTO {term_synonym} (tid, name) VALUES (%d, '%s')", $edit['tid'], chop($synonym));
+ }
+ }
+ }
+
+ cache_clear_all();
+
+ return $status;
+}
+
+function taxonomy_del_term($tid) {
+ $tids = array($tid);
+ while ($tids) {
+ $children_tids = $orphans = array();
+ foreach ($tids as $tid) {
+ // See if any of the term's children are about to be become orphans:
+ if ($children = taxonomy_get_children($tid)) {
+ foreach ($children as $child) {
+ // If the term has multiple parents, we don't delete it.
+ $parents = taxonomy_get_parents($child->tid);
+ if (count($parents) == 1) {
+ $orphans[] = $child->tid;
+ }
+ }
+ }
+
+ $term = (array) taxonomy_get_term($tid);
+
+ db_query('DELETE FROM {term_data} WHERE tid = %d', $tid);
+ db_query('DELETE FROM {term_hierarchy} WHERE tid = %d', $tid);
+ db_query('DELETE FROM {term_relation} WHERE tid1 = %d OR tid2 = %d', $tid, $tid);
+ db_query('DELETE FROM {term_synonym} WHERE tid = %d', $tid);
+ db_query('DELETE FROM {term_node} WHERE tid = %d', $tid);
+
+ module_invoke_all('taxonomy', 'delete', 'term', $term);
+ }
+
+ $tids = $orphans;
+ }
+
+ cache_clear_all();
+
+ return SAVED_DELETED;
+}
+
+function _taxonomy_confirm_del_term($tid) {
+ $term = taxonomy_get_term($tid);
+
+ $form['type'] = array('#type' => 'value', '#value' => 'term');
+ $form['name'] = array('#type' => 'value', '#value' => $term->name);
+ $form['tid'] = array('#type' => 'value', '#value' => $tid);
+ return confirm_form('taxonomy_term_confirm_delete', $form,
+ t('Are you sure you want to delete the term %title?',
+ array('%title' => theme('placeholder', $term->name))),
+ 'admin/taxonomy',
+ t('Deleting a term will delete all its children if there are any. This action cannot be undone.'),
+ t('Delete'),
+ t('Cancel'));
+}
+
+function taxonomy_term_confirm_delete_submit($form_id, $form_values) {
+ taxonomy_del_term($form_values['tid']);
+ drupal_set_message(t('Deleted term %name.', array('%name' => theme('placeholder', $form_values['name']))));
+ return 'admin/taxonomy';
+}
+
+/**
+ * Generate a form element for selecting terms from a vocabulary.
+ */
+function taxonomy_form($vid, $value = 0, $help = NULL, $name = 'taxonomy') {
+ $vocabulary = taxonomy_get_vocabulary($vid);
+ $help = ($help) ? $help : $vocabulary->help;
+ if ($vocabulary->required) {
+ $blank = 0;
+ }
+ else {
+ $blank = '<'. t('none') .'>';
+ }
+
+ return _taxonomy_term_select(check_plain($vocabulary->name), $name, $value, $vid, $help, intval($vocabulary->multiple), $blank);
+}
+
+/**
+ * Generate a set of options for selecting a term from all vocabularies. Can be
+ * passed to form_select.
+ */
+function taxonomy_form_all($free_tags = 0) {
+ $vocabularies = taxonomy_get_vocabularies();
+ $options = array();
+ foreach ($vocabularies as $vid => $vocabulary) {
+ if ($vocabulary->tags && !$free_tags) { continue; }
+ $tree = taxonomy_get_tree($vid);
+ $options[$vocabulary->name] = array();
+ if ($tree) {
+ foreach ($tree as $term) {
+ $options[$vocabulary->name][$term->tid] = _taxonomy_depth($term->depth, '-') . $term->name;
+ }
+ }
+ }
+ return $options;
+}
+
+/**
+ * Return an array of all vocabulary objects.
+ *
+ * @param $type
+ * If set, return only those vocabularies associated with this node type.
+ */
+function taxonomy_get_vocabularies($type = NULL) {
+ if ($type) {
+ $result = db_query(db_rewrite_sql("SELECT v.vid, v.*, n.type FROM {vocabulary} v LEFT JOIN {vocabulary_node_types} n ON v.vid = n.vid WHERE n.type = '%s' ORDER BY v.weight, v.name", 'v', 'vid'), $type);
+ }
+ else {
+ $result = db_query(db_rewrite_sql('SELECT v.*, n.type FROM {vocabulary} v LEFT JOIN {vocabulary_node_types} n ON v.vid = n.vid ORDER BY v.weight, v.name', 'v', 'vid'));
+ }
+
+ $vocabularies = array();
+ $node_types = array();
+ while ($voc = db_fetch_object($result)) {
+ $node_types[$voc->vid][] = $voc->type;
+ unset($voc->type);
+ $voc->nodes = $node_types[$voc->vid];
+ $vocabularies[$voc->vid] = $voc;
+ }
+
+ return $vocabularies;
+}
+
+/**
+ * Generate a form for selecting terms to associate with a node.
+ */
+function taxonomy_form_alter($form_id, &$form) {
+ if (isset($form['type']) && $form['type']['#value'] .'_node_form' == $form_id) {
+ $node = $form['#node'];
+
+ if (!isset($node->taxonomy)) {
+ if ($node->nid) {
+ $terms = taxonomy_node_get_terms($node->nid);
+ }
+ else {
+ $terms = array();
+ }
+ }
+ else {
+ $terms = $node->taxonomy;
+ }
+
+ $c = db_query(db_rewrite_sql("SELECT v.* FROM {vocabulary} v INNER JOIN {vocabulary_node_types} n ON v.vid = n.vid WHERE n.type = '%s' ORDER BY v.weight, v.name", 'v', 'vid'), $node->type);
+
+ while ($vocabulary = db_fetch_object($c)) {
+ if ($vocabulary->tags) {
+ $typed_terms = array();
+ foreach ($terms as $term) {
+ // Extract terms belonging to the vocabulary in question.
+ if ($term->vid == $vocabulary->vid) {
+
+ // Commas and quotes in terms are special cases, so encode 'em.
+ if (preg_match('/,/', $term->name) || preg_match('/"/', $term->name)) {
+ $term->name = '"'.preg_replace('/"/', '""', $term->name).'"';
+ }
+
+ $typed_terms[] = $term->name;
+ }
+ }
+ $typed_string = implode(', ', $typed_terms) . (array_key_exists('tags', $terms) ? $terms['tags'][$vocabulary->vid] : NULL);
+
+ if ($vocabulary->help) {
+ $help = $vocabulary->help;
+ }
+ else {
+ $help = t('A comma-separated list of terms describing this content. Example: funny, bungee jumping, "Company, Inc.".');
+ }
+ $form['taxonomy']['tags'][$vocabulary->vid] = array('#type' => 'textfield',
+ '#title' => $vocabulary->name,
+ '#description' => $help,
+ '#required' => $vocabulary->required,
+ '#default_value' => $typed_string,
+ '#autocomplete_path' => 'taxonomy/autocomplete/'. $vocabulary->vid,
+ '#weight' => $vocabulary->weight,
+ '#maxlength' => 100,
+ );
+ }
+ else {
+ // Extract terms belonging to the vocabulary in question.
+ $default_terms = array();
+ foreach ($terms as $term) {
+ if ($term->vid == $vocabulary->vid) {
+ $default_terms[$term->tid] = $term;
+ }
+ }
+ $form['taxonomy'][$vocabulary->vid] = taxonomy_form($vocabulary->vid, array_keys($default_terms), $vocabulary->help);
+ $form['taxonomy'][$vocabulary->vid]['#weight'] = $vocabulary->weight;
+ $form['taxonomy'][$vocabulary->vid]['#required'] = $vocabulary->required;
+ }
+ }
+ if (isset($form['taxonomy'])) {
+ $form['taxonomy'] += array('#type' => 'fieldset', '#title' => t('Categories'), '#collapsible' => TRUE, '#collapsed' => FALSE, '#tree' => TRUE, '#weight' => -3);
+ }
+ }
+}
+
+/**
+ * Find all terms associated to the given node, within one vocabulary.
+ */
+function taxonomy_node_get_terms_by_vocabulary($nid, $vid, $key = 'tid') {
+ $result = db_query(db_rewrite_sql('SELECT t.tid, t.* FROM {term_data} t INNER JOIN {term_node} r ON r.tid = t.tid WHERE t.vid = %d AND r.nid = %d ORDER BY weight', 't', 'tid'), $vid, $nid);
+ $terms = array();
+ while ($term = db_fetch_object($result)) {
+ $terms[$term->$key] = $term;
+ }
+ return $terms;
+}
+
+/**
+ * Find all terms associated to the given node, ordered by vocabulary and term weight.
+ */
+function taxonomy_node_get_terms($nid, $key = 'tid') {
+ static $terms;
+
+ if (!isset($terms[$nid])) {
+ $result = db_query(db_rewrite_sql('SELECT t.* FROM {term_node} r INNER JOIN {term_data} t ON r.tid = t.tid INNER JOIN {vocabulary} v ON t.vid = v.vid WHERE r.nid = %d ORDER BY v.weight, t.weight, t.name', 't', 'tid'), $nid);
+ $terms[$nid] = array();
+ while ($term = db_fetch_object($result)) {
+ $terms[$nid][$term->$key] = $term;
+ }
+ }
+ return $terms[$nid];
+}
+
+/**
+ * Make sure incoming vids are free tagging enabled.
+ */
+function taxonomy_node_validate(&$node) {
+ if ($node->taxonomy) {
+ $terms = $node->taxonomy;
+ if ($terms['tags']) {
+ foreach ($terms['tags'] as $vid => $vid_value) {
+ $vocabulary = taxonomy_get_vocabulary($vid);
+ if (!$vocabulary->tags) {
+ // see form_get_error $key = implode('][', $element['#parents']);
+ // on why this is the key
+ form_set_error("taxonomy][tags][$vid", t('The %name vocabulary can not be modified in this way.', array('%name' => theme('placeholder', $vocabulary->name))));
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Save term associations for a given node.
+ */
+function taxonomy_node_save($nid, $terms) {
+ taxonomy_node_delete($nid);
+
+ // Free tagging vocabularies do not send their tids in the form,
+ // so we'll detect them here and process them independently.
+ if (isset($terms['tags'])) {
+ $typed_input = $terms['tags'];
+ unset($terms['tags']);
+
+ foreach ($typed_input as $vid => $vid_value) {
+ // This regexp allows the following types of user input:
+ // this, "somecmpany, llc", "and ""this"" w,o.rks", foo bar
+ $regexp = '%(?:^|,\ *)("(?>[^"]*)(?>""[^"]* )*"|(?: [^",]*))%x';
+ preg_match_all($regexp, $vid_value, $matches);
+ $typed_terms = array_unique($matches[1]);
+
+ $inserted = array();
+ foreach ($typed_terms as $typed_term) {
+ // If a user has escaped a term (to demonstrate that it is a group,
+ // or includes a comma or quote character), we remove the escape
+ // formatting so to save the term into the DB as the user intends.
+ $typed_term = str_replace('""', '"', preg_replace('/^"(.*)"$/', '\1', $typed_term));
+ $typed_term = trim($typed_term);
+ if ($typed_term == "") { continue; }
+
+ // See if the term exists in the chosen vocabulary
+ // and return the tid, otherwise, add a new record.
+ $possibilities = taxonomy_get_term_by_name($typed_term);
+ $typed_term_tid = NULL; // tid match if any.
+ foreach ($possibilities as $possibility) {
+ if ($possibility->vid == $vid) {
+ $typed_term_tid = $possibility->tid;
+ }
+ }
+
+ if (!$typed_term_tid) {
+ $edit = array('vid' => $vid, 'name' => $typed_term);
+ $status = taxonomy_save_term($edit);
+ $typed_term_tid = $edit['tid'];
+ }
+
+ // defend against duplicate, different cased tags
+ if (!isset($inserted[$typed_term_tid])) {
+ db_query('INSERT INTO {term_node} (nid, tid) VALUES (%d, %d)', $nid, $typed_term_tid);
+ $inserted[$typed_term_tid] = TRUE;
+ }
+ }
+ }
+ }
+
+ if (is_array($terms)) {
+ foreach ($terms as $term) {
+ if (is_array($term)) {
+ foreach ($term as $tid) {
+ if ($tid) {
+ db_query('INSERT INTO {term_node} (nid, tid) VALUES (%d, %d)', $nid, $tid);
+ }
+ }
+ }
+ else if (is_object($term)) {
+ db_query('INSERT INTO {term_node} (nid, tid) VALUES (%d, %d)', $nid, $term->tid);
+ }
+ else if ($term) {
+ db_query('INSERT INTO {term_node} (nid, tid) VALUES (%d, %d)', $nid, $term);
+ }
+ }
+ }
+}
+
+/**
+ * Remove associations of a node to its terms.
+ */
+function taxonomy_node_delete($nid) {
+ db_query('DELETE FROM {term_node} WHERE nid = %d', $nid);
+}
+
+/**
+ * Find all term objects related to a given term ID.
+ */
+function taxonomy_get_related($tid, $key = 'tid') {
+ if ($tid) {
+ $result = db_query('SELECT t.*, tid1, tid2 FROM {term_relation}, {term_data} t WHERE (t.tid = tid1 OR t.tid = tid2) AND (tid1 = %d OR tid2 = %d) AND t.tid != %d ORDER BY weight, name', $tid, $tid, $tid);
+ $related = array();
+ while ($term = db_fetch_object($result)) {
+ $related[$term->$key] = $term;
+ }
+ return $related;
+ }
+ else {
+ return array();
+ }
+}
+
+/**
+ * Find all parents of a given term ID.
+ */
+function taxonomy_get_parents($tid, $key = 'tid') {
+ if ($tid) {
+ $result = db_query(db_rewrite_sql('SELECT t.tid, t.* FROM {term_data} t INNER JOIN {term_hierarchy} h ON h.parent = t.tid WHERE h.tid = %d ORDER BY weight, name', 't', 'tid'), $tid);
+ $parents = array();
+ while ($parent = db_fetch_object($result)) {
+ $parents[$parent->$key] = $parent;
+ }
+ return $parents;
+ }
+ else {
+ return array();
+ }
+}
+
+/**
+ * Find all ancestors of a given term ID.
+ */
+function taxonomy_get_parents_all($tid) {
+ $parents = array();
+ if ($tid) {
+ $parents[] = taxonomy_get_term($tid);
+ $n = 0;
+ while ($parent = taxonomy_get_parents($parents[$n]->tid)) {
+ $parents = array_merge($parents, $parent);
+ $n++;
+ }
+ }
+ return $parents;
+}
+
+/**
+ * Find all children of a term ID.
+ */
+function taxonomy_get_children($tid, $vid = 0, $key = 'tid') {
+ if ($vid) {
+ $result = db_query(db_rewrite_sql('SELECT t.* FROM {term_data} t INNER JOIN {term_hierarchy} h ON h.tid = t.tid WHERE t.vid = %d AND h.parent = %d ORDER BY weight, name', 't', 'tid'), $vid, $tid);
+ }
+ else {
+ $result = db_query(db_rewrite_sql('SELECT t.* FROM {term_data} t INNER JOIN {term_hierarchy} h ON h.tid = t.tid WHERE parent = %d ORDER BY weight, name', 't', 'tid'), $tid);
+ }
+ $children = array();
+ while ($term = db_fetch_object($result)) {
+ $children[$term->$key] = $term;
+ }
+ return $children;
+}
+
+/**
+ * Create a hierarchical representation of a vocabulary.
+ *
+ * @param $vid
+ * Which vocabulary to generate the tree for.
+ *
+ * @param $parent
+ * The term ID under which to generate the tree. If 0, generate the tree
+ * for the entire vocabulary.
+ *
+ * @param $depth
+ * Internal use only.
+ *
+ * @param $max_depth
+ * The number of levels of the tree to return. Leave NULL to return all levels.
+ *
+ * @return
+ * An array of all term objects in the tree. Each term object is extended
+ * to have "depth" and "parents" attributes in addition to its normal ones.
+ */
+function taxonomy_get_tree($vid, $parent = 0, $depth = -1, $max_depth = NULL) {
+ static $children, $parents, $terms;
+
+ $depth++;
+
+ // We cache trees, so it's not CPU-intensive to call get_tree() on a term
+ // and its children, too.
+ if (!isset($children[$vid])) {
+ $children[$vid] = array();
+
+ $result = db_query(db_rewrite_sql('SELECT t.tid, t.*, parent FROM {term_data} t INNER JOIN {term_hierarchy} h ON t.tid = h.tid WHERE t.vid = %d ORDER BY weight, name', 't', 'tid'), $vid);
+ while ($term = db_fetch_object($result)) {
+ $children[$vid][$term->parent][] = $term->tid;
+ $parents[$vid][$term->tid][] = $term->parent;
+ $terms[$vid][$term->tid] = $term;
+ }
+ }
+
+ $max_depth = (is_null($max_depth)) ? count($children[$vid]) : $max_depth;
+ if ($children[$vid][$parent]) {
+ foreach ($children[$vid][$parent] as $child) {
+ if ($max_depth > $depth) {
+ $terms[$vid][$child]->depth = $depth;
+ // The "parent" attribute is not useful, as it would show one parent only.
+ unset($terms[$vid][$child]->parent);
+ $terms[$vid][$child]->parents = $parents[$vid][$child];
+ $tree[] = $terms[$vid][$child];
+
+ if ($children[$vid][$child]) {
+ $tree = array_merge($tree, taxonomy_get_tree($vid, $child, $depth, $max_depth));
+ }
+ }
+ }
+ }
+
+ return $tree ? $tree : array();
+}
+
+/**
+ * Return an array of synonyms of the given term ID.
+ */
+function taxonomy_get_synonyms($tid) {
+ if ($tid) {
+ $result = db_query('SELECT name FROM {term_synonym} WHERE tid = %d', $tid);
+ while ($synonym = db_fetch_array($result)) {
+ $synonyms[] = $synonym['name'];
+ }
+ return $synonyms ? $synonyms : array();
+ }
+ else {
+ return array();
+ }
+}
+
+/**
+ * Return the term object that has the given string as a synonym.
+ */
+function taxonomy_get_synonym_root($synonym) {
+ return db_fetch_object(db_query("SELECT * FROM {term_synonym} s, {term_data} t WHERE t.tid = s.tid AND s.name = '%s'", $synonym));
+}
+
+/**
+ * Given a term id, count the number of published nodes in it.
+ */
+function taxonomy_term_count_nodes($tid, $type = 0) {
+ static $count;
+
+ if (!isset($count[$type])) {
+ // $type == 0 always evaluates true is $type is a string
+ if (is_numeric($type)) {
+ $result = db_query(db_rewrite_sql('SELECT t.tid, COUNT(n.nid) AS c FROM {term_node} t INNER JOIN {node} n ON t.nid = n.nid WHERE n.status = 1 GROUP BY t.tid'));
+ }
+ else {
+ $result = db_query(db_rewrite_sql("SELECT t.tid, COUNT(n.nid) AS c FROM {term_node} t INNER JOIN {node} n ON t.nid = n.nid WHERE n.status = 1 AND n.type = '%s' GROUP BY t.tid"), $type);
+ }
+ while ($term = db_fetch_object($result)) {
+ $count[$type][$term->tid] = $term->c;
+ }
+ }
+
+ foreach (_taxonomy_term_children($tid) as $c) {
+ $children_count += taxonomy_term_count_nodes($c, $type);
+ }
+ return $count[$type][$tid] + $children_count;
+}
+
+/**
+ * Helper for taxonomy_term_count_nodes().
+ */
+function _taxonomy_term_children($tid) {
+ static $children;
+
+ if (!isset($children)) {
+ $result = db_query('SELECT tid, parent FROM {term_hierarchy}');
+ while ($term = db_fetch_object($result)) {
+ $children[$term->parent][] = $term->tid;
+ }
+ }
+ return $children[$tid] ? $children[$tid] : array();
+}
+
+/**
+ * Try to map a string to an existing term, as for glossary use.
+ *
+ * Provides a case-insensitive and trimmed mapping, to maximize the
+ * likelihood of a successful match.
+ *
+ * @param name
+ * Name of the term to search for.
+ *
+ * @return
+ * An array of matching term objects.
+ */
+function taxonomy_get_term_by_name($name) {
+ $db_result = db_query(db_rewrite_sql("SELECT t.tid, t.* FROM {term_data} t WHERE LOWER('%s') LIKE LOWER(t.name)", 't', 'tid'), trim($name));
+ $result = array();
+ while ($term = db_fetch_object($db_result)) {
+ $result[] = $term;
+ }
+
+ return $result;
+}
+
+/**
+ * Return the vocabulary object matching a vocabulary ID.
+ */
+function taxonomy_get_vocabulary($vid) {
+ static $vocabularies = array();
+
+ if (!array_key_exists($vid, $vocabularies)) {
+ $result = db_query('SELECT v.*, n.type FROM {vocabulary} v LEFT JOIN {vocabulary_node_types} n ON v.vid = n.vid WHERE v.vid = %d ORDER BY v.weight, v.name', $vid);
+ $node_types = array();
+ while ($voc = db_fetch_object($result)) {
+ $node_types[] = $voc->type;
+ unset($voc->type);
+ $voc->nodes = $node_types;
+ $vocabularies[$vid] = $voc;
+ }
+ }
+
+ return $vocabularies[$vid];
+}
+
+/**
+ * Return the term object matching a term ID.
+ */
+function taxonomy_get_term($tid) {
+ // simple cache using a static var?
+ return db_fetch_object(db_query('SELECT * FROM {term_data} WHERE tid = %d', $tid));
+}
+
+function _taxonomy_term_select($title, $name, $value, $vocabulary_id, $description, $multiple, $blank, $exclude = array()) {
+ $tree = taxonomy_get_tree($vocabulary_id);
+ $options = array();
+
+ if ($blank) {
+ $options[0] = $blank;
+ }
+ if ($tree) {
+ foreach ($tree as $term) {
+ if (!in_array($term->tid, $exclude)) {
+ $options[$term->tid] = _taxonomy_depth($term->depth, '-') . $term->name;
+ }
+ }
+ if (!$blank && !$value) {
+ // required but without a predefined value, so set first as predefined
+ $value = $tree[0]->tid;
+ }
+ }
+
+ return array('#type' => 'select',
+ '#title' => $title,
+ '#default_value' => $value,
+ '#options' => $options,
+ '#description' => $description,
+ '#multiple' => $multiple,
+ '#size' => $multiple ? min(9, count($options)) : 0,
+ '#weight' => -15,
+ '#theme' => 'taxonomy_term_select',
+ );
+}
+
+function theme_taxonomy_term_select($element) {
+ return theme('select', $element);
+}
+
+function _taxonomy_depth($depth, $graphic = '--') {
+ for ($n = 0; $n < $depth; $n++) {
+ $result .= $graphic;
+ }
+ return $result;
+}
+
+/**
+ * Finds all nodes that match selected taxonomy conditions.
+ *
+ * @param $tids
+ * An array of term IDs to match.
+ * @param $operator
+ * How to interpret multiple IDs in the array. Can be "or" or "and".
+ * @param $depth
+ * How many levels deep to traverse the taxonomy tree. Can be a nonnegative
+ * integer or "all".
+ * @param $pager
+ * Whether the nodes are to be used with a pager (the case on most Drupal
+ * pages) or not (in an XML feed, for example).
+ * @param $order
+ * The order clause for the query that retrieve the nodes.
+ * @return
+ * A resource identifier pointing to the query results.
+ */
+function taxonomy_select_nodes($tids = array(), $operator = 'or', $depth = 0, $pager = TRUE, $order = 'n.sticky DESC, n.created DESC') {
+ if (count($tids) > 0) {
+ // For each term ID, generate an array of descendant term IDs to the right depth.
+ $descendant_tids = array();
+ if ($depth === 'all') {
+ $depth = NULL;
+ }
+ foreach ($tids as $index => $tid) {
+ $term = taxonomy_get_term($tid);
+ $tree = taxonomy_get_tree($term->vid, $tid, -1, $depth);
+ $descendant_tids[] = array_merge(array($tid), array_map('_taxonomy_get_tid_from_term', $tree));
+ }
+
+ if ($operator == 'or') {
+ $str_tids = implode(',', call_user_func_array('array_merge', $descendant_tids));
+ $sql = 'SELECT DISTINCT(n.nid), n.sticky, n.title, n.created FROM {node} n INNER JOIN {term_node} tn ON n.nid = tn.nid WHERE tn.tid IN ('. $str_tids .') AND n.status = 1 ORDER BY '. $order;
+ $sql_count = 'SELECT COUNT(DISTINCT(n.nid)) FROM {node} n INNER JOIN {term_node} tn ON n.nid = tn.nid WHERE tn.tid IN ('. $str_tids .') AND n.status = 1';
+ }
+ else {
+ $joins = '';
+ $wheres = '';
+ foreach ($descendant_tids as $index => $tids) {
+ $joins .= ' INNER JOIN {term_node} tn'. $index .' ON n.nid = tn'. $index .'.nid';
+ $wheres .= ' AND tn'. $index .'.tid IN ('. implode(',', $tids) .')';
+ }
+ $sql = 'SELECT DISTINCT(n.nid), n.sticky, n.title, n.created FROM {node} n '. $joins .' WHERE n.status = 1 '. $wheres .' ORDER BY '. $order;
+ $sql_count = 'SELECT COUNT(DISTINCT(n.nid)) FROM {node} n '. $joins .' WHERE n.status = 1 ' . $wheres;
+ }
+ $sql = db_rewrite_sql($sql);
+ $sql_count = db_rewrite_sql($sql_count);
+ if ($pager) {
+ $result = pager_query($sql, variable_get('default_nodes_main', 10), 0, $sql_count);
+ }
+ else {
+ $result = db_query_range($sql, 0, variable_get('feed_default_items', 10));
+ }
+ }
+
+ return $result;
+}
+
+/**
+ * Accepts the result of a pager_query() call, such as that performed by
+ * taxonomy_select_nodes(), and formats each node along with a pager.
+*/
+function taxonomy_render_nodes($result) {
+ if (db_num_rows($result) > 0) {
+ while ($node = db_fetch_object($result)) {
+ $output .= node_view(node_load($node->nid), 1);
+ }
+ $output .= theme('pager', NULL, variable_get('default_nodes_main', 10), 0);
+ }
+ else {
+ $output .= t('There are currently no posts in this category.');
+ }
+ return $output;
+}
+
+/**
+ * Implementation of hook_nodeapi().
+ */
+function taxonomy_nodeapi($node, $op, $arg = 0) {
+ switch ($op) {
+ case 'load':
+ $output['taxonomy'] = taxonomy_node_get_terms($node->nid);
+ return $output;
+ case 'insert':
+ taxonomy_node_save($node->nid, $node->taxonomy);
+ break;
+ case 'update':
+ taxonomy_node_save($node->nid, $node->taxonomy);
+ break;
+ case 'delete':
+ taxonomy_node_delete($node->nid);
+ break;
+ case 'validate':
+ taxonomy_node_validate($node);
+ break;
+ case 'rss item':
+ return taxonomy_rss_item($node);
+ case 'update index':
+ return taxonomy_node_update_index($node);
+ }
+}
+
+/**
+ * Implementation of hook_nodeapi('update_index').
+ */
+function taxonomy_node_update_index(&$node) {
+ $output = array();
+ foreach ($node->taxonomy as $term) {
+ $output[] = $term->name;
+ }
+ if (count($output)) {
+ return '('. implode(', ', $output) .')';
+ }
+}
+
+/**
+ * Menu callback; displays all nodes associated with a term.
+ */
+function taxonomy_term_page($str_tids = '', $depth = 0, $op = 'page') {
+ if (preg_match('/^([0-9]+[+ ])+[0-9]+$/', $str_tids)) {
+ $operator = 'or';
+ // The '+' character in a query string may be parsed as ' '.
+ $tids = preg_split('/[+ ]/', $str_tids);
+ }
+ else if (preg_match('/^([0-9]+,)*[0-9]+$/', $str_tids)) {
+ $operator = 'and';
+ $tids = explode(',', $str_tids);
+ }
+ else {
+ drupal_not_found();
+ }
+
+ if ($tids) {
+ $result = db_query(db_rewrite_sql('SELECT t.tid, t.name FROM {term_data} t WHERE t.tid IN (%s)', 't', 'tid'), implode(',', $tids));
+ $tids = array(); // we rebuild the $tids-array so it only contains terms the user has access to.
+ $names = array();
+ while ($term = db_fetch_object($result)) {
+ $tids[] = $term->tid;
+ $names[] = $term->name;
+ }
+
+ if ($names) {
+ drupal_set_title($title = check_plain(implode(', ', $names)));
+
+ switch ($op) {
+ case 'page':
+ // Build breadcrumb based on first hierarchy of first term:
+ $current->tid = $tids[0];
+ $breadcrumbs = array(array('path' => $_GET['q']));
+ while ($parents = taxonomy_get_parents($current->tid)) {
+ $current = array_shift($parents);
+ $breadcrumbs[] = array('path' => 'taxonomy/term/'. $current->tid, 'title' => $current->name);
+ }
+ $breadcrumbs = array_reverse($breadcrumbs);
+ menu_set_location($breadcrumbs);
+
+ drupal_add_link(array('rel' => 'alternate',
+ 'type' => 'application/rss+xml',
+ 'title' => 'RSS - '. $title,
+ 'href' => url('taxonomy/term/'. $str_tids .'/'. $depth .'/feed')));
+
+ $output = taxonomy_render_nodes(taxonomy_select_nodes($tids, $operator, $depth, TRUE));
+ $output .= theme('feed_icon', url('taxonomy/term/'. $str_tids .'/'. $depth .'/feed'));
+ return $output;
+ break;
+
+ case 'feed':
+ $term = taxonomy_get_term($tids[0]);
+ $channel['link'] = url('taxonomy/term/'. $str_tids .'/'. $depth, NULL, NULL, TRUE);
+ $channel['title'] = variable_get('site_name', 'drupal') .' - '. $title;
+ $channel['description'] = $term->description;
+
+ $result = taxonomy_select_nodes($tids, $operator, $depth, FALSE);
+ node_feed($result, $channel);
+ break;
+ default:
+ drupal_not_found();
+ }
+ }
+ else {
+ drupal_not_found();
+ }
+ }
+}
+
+/**
+ * Page to add or edit a vocabulary
+ */
+function taxonomy_admin_vocabulary_edit($vid = NULL) {
+ if ($_POST['op'] == t('Delete') || $_POST['edit']['confirm']) {
+ return _taxonomy_confirm_del_vocabulary($vid);
+ }
+ elseif ($vid) {
+ $vocabulary = (array)taxonomy_get_vocabulary($vid);
+ }
+ return taxonomy_form_vocabulary($vocabulary);
+}
+
+/**
+ * Page to list terms for a vocabulary
+ */
+function taxonomy_admin_term_edit($tid = NULL) {
+ if ($_POST['op'] == t('Delete') || $_POST['edit']['confirm']) {
+ return _taxonomy_confirm_del_term($tid);
+ }
+ elseif ($tid) {
+ $term = (array)taxonomy_get_term($tid);
+ }
+ return taxonomy_form_term($term);
+}
+
+/**
+ * Provides category information for rss feeds
+ */
+function taxonomy_rss_item($node) {
+ $output = array();
+ foreach ($node->taxonomy as $term) {
+ $output[] = array('key' => 'category',
+ 'value' => check_plain($term->name),
+ 'attributes' => array('domain' => url('taxonomy/term/'. $term->tid, NULL, NULL, TRUE)));
+ }
+ return $output;
+}
+
+/**
+ * Implementation of hook_help().
+ */
+function taxonomy_help($section) {
+ switch ($section) {
+ case 'admin/help#taxonomy':
+ $output = ''. t('The taxonomy module is one of the most popular features because users often want to create categories to organize content by type. It can automatically classify new content, which is very useful for organizing content on-the-fly. A simple example would be organizing a list of music reviews by musical genre.') .'
';
+ $output .= ''. t('Taxonomy is also the study of classification. The taxonomy module allows you to define vocabularies (sets of categories) which are used to classify content. The module supports hierarchical classification and association between terms, allowing for truly flexible information retrieval and classification. The taxonomy module allows multiple lists of categories for classification (controlled vocabularies) and offers the possibility of creating thesauri (controlled vocabularies that indicate the relationship of terms) and taxonomies (controlled vocabularies where relationships are indicated hierarchically). To view and manage the terms of each vocabulary, click on the associated list terms link. To delete a vocabulary and all its terms, choose edit vocabulary.') .'
';
+ $output .= ''. t('A controlled vocabulary is a set of terms to use for describing content (known as descriptors in indexing lingo). Drupal allows you to describe each piece of content (blog, story, etc.) using one or many of these terms. For simple implementations, you might create a set of categories without subcategories, similar to Slashdot\'s sections. For more complex implementations, you might create a hierarchical list of categories.') .'
';
+ $output .= t('You can
+
+- add a vocabulary at administer >> categories >> add vocabulary.
+- administer taxonomy at administer >> categories.
+- restrict content access by category for specific users roles using the taxonomy access module.
+- build a custom view of your categories using the taxonomy browser.
+
+', array('%admin-taxonomy-add-vocabulary' => url('admin/taxonomy/add/vocabulary'), '%admin-taxonomy' => url('admin/taxonomy'), '%external-http-drupal-org-project-taxonomy_access' => 'http://drupal.org/project/taxonomy_access', '%external-http-drupal-org-project-taxonomy_browser' => 'http://drupal.org/project/taxonomy_browser'));
+ $output .= ''. t('For more information please read the configuration and customization handbook Taxonomy page.', array('%taxonomy' => 'http://drupal.org/handbook/modules/taxonomy/')) .'
';
+ return $output;
+ case 'admin/modules#description':
+ return t('Enables the categorization of content.');
+ case 'admin/taxonomy':
+ return t('The taxonomy module allows you to classify content into categories and subcategories; it allows multiple lists of categories for classification (controlled vocabularies) and offers the possibility of creating thesauri (controlled vocabularies that indicate the relationship of terms), taxonomies (controlled vocabularies where relationships are indicated hierarchically), and free vocabularies where terms, or tags, are defined during content creation. To view and manage the terms of each vocabulary, click on the associated list terms link. To delete a vocabulary and all its terms, choose "edit vocabulary".
');
+ case 'admin/taxonomy/add/vocabulary':
+ return t("When you create a controlled vocabulary you are creating a set of terms to use for describing content (known as descriptors in indexing lingo). Drupal allows you to describe each piece of content (blog, story, etc.) using one or many of these terms. For simple implementations, you might create a set of categories without subcategories, similar to Slashdot.org's or Kuro5hin.org's sections. For more complex implementations, you might create a hierarchical list of categories.
");
+ }
+}
+
+/**
+ * Helper function for array_map purposes.
+ */
+function _taxonomy_get_tid_from_term($term) {
+ return $term->tid;
+}
+
+/**
+ * Helper function for autocompletion
+ */
+function taxonomy_autocomplete($vid, $string = '') {
+ // The user enters a comma-separated list of tags. We only autocomplete the last tag.
+ // This regexp allows the following types of user input:
+ // this, "somecmpany, llc", "and ""this"" w,o.rks", foo bar
+ $regexp = '%(?:^|,\ *)("(?>[^"]*)(?>""[^"]* )*"|(?: [^",]*))%x';
+ preg_match_all($regexp, $string, $matches);
+ $array = $matches[1];
+
+ // Fetch last tag
+ $last_string = trim(array_pop($array));
+ if ($last_string != '') {
+ $result = db_query_range(db_rewrite_sql("SELECT t.tid, t.name FROM {term_data} t WHERE t.vid = %d AND LOWER(t.name) LIKE LOWER('%%%s%%')", 't', 'tid'), $vid, $last_string, 0, 10);
+
+ $prefix = count($array) ? implode(', ', $array) .', ' : '';
+
+ $matches = array();
+ while ($tag = db_fetch_object($result)) {
+ $n = $tag->name;
+ // Commas and quotes in terms are special cases, so encode 'em.
+ if (preg_match('/,/', $tag->name) || preg_match('/"/', $tag->name)) {
+ $n = '"'. preg_replace('/"/', '""', $tag->name) .'"';
+ }
+ $matches[$prefix . $n] = check_plain($tag->name);
+ }
+ print drupal_to_js($matches);
+ exit();
+ }
+}
diff --git a/modules/taxonomy_block/LICENSE.txt b/modules/taxonomy_block/LICENSE.txt
new file mode 100644
index 0000000..2c095c8
--- /dev/null
+++ b/modules/taxonomy_block/LICENSE.txt
@@ -0,0 +1,274 @@
+GNU GENERAL PUBLIC LICENSE
+
+ Version 2, June 1991
+
+Copyright (C) 1989, 1991 Free Software Foundation, Inc. 675 Mass Ave,
+Cambridge, MA 02139, USA. Everyone is permitted to copy and distribute
+verbatim copies of this license document, but changing it is not allowed.
+
+ Preamble
+
+The licenses for most software are designed to take away your freedom to
+share and change it. By contrast, the GNU General Public License is
+intended to guarantee your freedom to share and change free software--to
+make sure the software is free for all its users. This General Public License
+applies to most of the Free Software Foundation's software and to any other
+program whose authors commit to using it. (Some other Free Software
+Foundation software is covered by the GNU Library General Public License
+instead.) You can apply it to your programs, too.
+
+When we speak of free software, we are referring to freedom, not price. Our
+General Public Licenses are designed to make sure that you have the
+freedom to distribute copies of free software (and charge for this service if
+you wish), that you receive source code or can get it if you want it, that you
+can change the software or use pieces of it in new free programs; and that
+you know you can do these things.
+
+To protect your rights, we need to make restrictions that forbid anyone to
+deny you these rights or to ask you to surrender the rights. These restrictions
+translate to certain responsibilities for you if you distribute copies of the
+software, or if you modify it.
+
+For example, if you distribute copies of such a program, whether gratis or for
+a fee, you must give the recipients all the rights that you have. You must make
+sure that they, too, receive or can get the source code. And you must show
+them these terms so they know their rights.
+
+We protect your rights with two steps: (1) copyright the software, and (2)
+offer you this license which gives you legal permission to copy, distribute
+and/or modify the software.
+
+Also, for each author's protection and ours, we want to make certain that
+everyone understands that there is no warranty for this free software. If the
+software is modified by someone else and passed on, we want its recipients
+to know that what they have is not the original, so that any problems
+introduced by others will not reflect on the original authors' reputations.
+
+Finally, any free program is threatened constantly by software patents. We
+wish to avoid the danger that redistributors of a free program will individually
+obtain patent licenses, in effect making the program proprietary. To prevent
+this, we have made it clear that any patent must be licensed for everyone's
+free use or not licensed at all.
+
+The precise terms and conditions for copying, distribution and modification
+follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND
+ MODIFICATION
+
+0. This License applies to any program or other work which contains a notice
+placed by the copyright holder saying it may be distributed under the terms
+of this General Public License. The "Program", below, refers to any such
+program or work, and a "work based on the Program" means either the
+Program or any derivative work under copyright law: that is to say, a work
+containing the Program or a portion of it, either verbatim or with
+modifications and/or translated into another language. (Hereinafter, translation
+is included without limitation in the term "modification".) Each licensee is
+addressed as "you".
+
+Activities other than copying, distribution and modification are not covered
+by this License; they are outside its scope. The act of running the Program is
+not restricted, and the output from the Program is covered only if its contents
+constitute a work based on the Program (independent of having been made
+by running the Program). Whether that is true depends on what the Program
+does.
+
+1. You may copy and distribute verbatim copies of the Program's source
+code as you receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice and
+disclaimer of warranty; keep intact all the notices that refer to this License
+and to the absence of any warranty; and give any other recipients of the
+Program a copy of this License along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and you
+may at your option offer warranty protection in exchange for a fee.
+
+2. You may modify your copy or copies of the Program or any portion of it,
+thus forming a work based on the Program, and copy and distribute such
+modifications or work under the terms of Section 1 above, provided that you
+also meet all of these conditions:
+
+a) You must cause the modified files to carry prominent notices stating that
+you changed the files and the date of any change.
+
+b) You must cause any work that you distribute or publish, that in whole or in
+part contains or is derived from the Program or any part thereof, to be
+licensed as a whole at no charge to all third parties under the terms of this
+License.
+
+c) If the modified program normally reads commands interactively when run,
+you must cause it, when started running for such interactive use in the most
+ordinary way, to print or display an announcement including an appropriate
+copyright notice and a notice that there is no warranty (or else, saying that
+you provide a warranty) and that users may redistribute the program under
+these conditions, and telling the user how to view a copy of this License.
+(Exception: if the Program itself is interactive but does not normally print such
+an announcement, your work based on the Program is not required to print
+an announcement.)
+
+These requirements apply to the modified work as a whole. If identifiable
+sections of that work are not derived from the Program, and can be
+reasonably considered independent and separate works in themselves, then
+this License, and its terms, do not apply to those sections when you distribute
+them as separate works. But when you distribute the same sections as part
+of a whole which is a work based on the Program, the distribution of the
+whole must be on the terms of this License, whose permissions for other
+licensees extend to the entire whole, and thus to each and every part
+regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest your rights to
+work written entirely by you; rather, the intent is to exercise the right to
+control the distribution of derivative or collective works based on the
+Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of a
+storage or distribution medium does not bring the other work under the scope
+of this License.
+
+3. You may copy and distribute the Program (or a work based on it, under
+Section 2) in object code or executable form under the terms of Sections 1
+and 2 above provided that you also do one of the following:
+
+a) Accompany it with the complete corresponding machine-readable source
+code, which must be distributed under the terms of Sections 1 and 2 above
+on a medium customarily used for software interchange; or,
+
+b) Accompany it with a written offer, valid for at least three years, to give
+any third party, for a charge no more than your cost of physically performing
+source distribution, a complete machine-readable copy of the corresponding
+source code, to be distributed under the terms of Sections 1 and 2 above on
+a medium customarily used for software interchange; or,
+
+c) Accompany it with the information you received as to the offer to distribute
+corresponding source code. (This alternative is allowed only for
+noncommercial distribution and only if you received the program in object
+code or executable form with such an offer, in accord with Subsection b
+above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source code
+means all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation and
+installation of the executable. However, as a special exception, the source
+code distributed need not include anything that is normally distributed (in
+either source or binary form) with the major components (compiler, kernel,
+and so on) of the operating system on which the executable runs, unless that
+component itself accompanies the executable.
+
+If distribution of executable or object code is made by offering access to
+copy from a designated place, then offering equivalent access to copy the
+source code from the same place counts as distribution of the source code,
+even though third parties are not compelled to copy the source along with the
+object code.
+
+4. You may not copy, modify, sublicense, or distribute the Program except as
+expressly provided under this License. Any attempt otherwise to copy,
+modify, sublicense or distribute the Program is void, and will automatically
+terminate your rights under this License. However, parties who have received
+copies, or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+5. You are not required to accept this License, since you have not signed it.
+However, nothing else grants you permission to modify or distribute the
+Program or its derivative works. These actions are prohibited by law if you
+do not accept this License. Therefore, by modifying or distributing the
+Program (or any work based on the Program), you indicate your acceptance
+of this License to do so, and all its terms and conditions for copying,
+distributing or modifying the Program or works based on it.
+
+6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the original
+licensor to copy, distribute or modify the Program subject to these terms and
+conditions. You may not impose any further restrictions on the recipients'
+exercise of the rights granted herein. You are not responsible for enforcing
+compliance by third parties to this License.
+
+7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues), conditions
+are imposed on you (whether by court order, agreement or otherwise) that
+contradict the conditions of this License, they do not excuse you from the
+conditions of this License. If you cannot distribute so as to satisfy
+simultaneously your obligations under this License and any other pertinent
+obligations, then as a consequence you may not distribute the Program at all.
+For example, if a patent license would not permit royalty-free redistribution
+of the Program by all those who receive copies directly or indirectly through
+you, then the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply and
+the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any patents or
+other property right claims or to contest validity of any such claims; this
+section has the sole purpose of protecting the integrity of the free software
+distribution system, which is implemented by public license practices. Many
+people have made generous contributions to the wide range of software
+distributed through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing to
+distribute software through any other system and a licensee cannot impose
+that choice.
+
+This section is intended to make thoroughly clear what is believed to be a
+consequence of the rest of this License.
+
+8. If the distribution and/or use of the Program is restricted in certain
+countries either by patents or by copyrighted interfaces, the original copyright
+holder who places the Program under this License may add an explicit
+geographical distribution limitation excluding those countries, so that
+distribution is permitted only in or among countries not thus excluded. In such
+case, this License incorporates the limitation as if written in the body of this
+License.
+
+9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will be
+similar in spirit to the present version, but may differ in detail to address new
+problems or concerns.
+
+Each version is given a distinguishing version number. If the Program specifies
+a version number of this License which applies to it and "any later version",
+you have the option of following the terms and conditions either of that
+version or of any later version published by the Free Software Foundation. If
+the Program does not specify a version number of this License, you may
+choose any version ever published by the Free Software Foundation.
+
+10. If you wish to incorporate parts of the Program into other free programs
+whose distribution conditions are different, write to the author to ask for
+permission. For software which is copyrighted by the Free Software
+Foundation, write to the Free Software Foundation; we sometimes make
+exceptions for this. Our decision will be guided by the two goals of
+preserving the free status of all derivatives of our free software and of
+promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE,
+THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT
+PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE
+STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT
+WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED,
+INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND
+PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
+NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR
+AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR
+ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE
+LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL,
+SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES
+ARISING OUT OF THE USE OR INABILITY TO USE THE
+PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA
+OR DATA BEING RENDERED INACCURATE OR LOSSES
+SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE
+PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN
+IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF
+THE POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
diff --git a/modules/taxonomy_block/README.txt b/modules/taxonomy_block/README.txt
new file mode 100644
index 0000000..39ac160
--- /dev/null
+++ b/modules/taxonomy_block/README.txt
@@ -0,0 +1,5 @@
+This is a simple module to create blocks based on taxonomy. It displays listings of recently posted nodes based on taxonomy definitions. You can create as many blocks as you like.
+
+Send comments to welch@advomatic.com
+
+Originally developed for stagespace.com by Aaron Welch (crunchywelch) at Advomatic LLC
diff --git a/modules/taxonomy_block/taxonomy_block.install b/modules/taxonomy_block/taxonomy_block.install
new file mode 100644
index 0000000..e112759
--- /dev/null
+++ b/modules/taxonomy_block/taxonomy_block.install
@@ -0,0 +1,46 @@
+ 'admin/block/taxonomy_block', 'title' => t('taxonomy block'), 'callback' => 'taxonomy_block_admin', 'access' => user_access('administer taxonomy blocks'));
+ $items[] = array('path' => 'admin/block/taxonomy_block/edit', 'title' => t('edit taxonomy block'), 'callback' => 'taxonomy_block_form', 'access' => user_access('administer taxonomy blocks'), 'type' => MENU_CALLBACK);
+ }
+
+ return $items;
+}
+
+/**
+ * Implementation of hook_block.
+ */
+function taxonomy_block_block($op = 'list', $delta = 0) {
+ switch ($op) {
+ case 'list':
+ return taxonomy_block_get_blocks();
+ break;
+ case 'view':
+ return taxonomy_block_get_block($delta);
+ break;
+ }
+}
+
+/**
+ * Displays the administrator page to this module.
+ */
+function taxonomy_block_admin($op = NULL, $bid = NULL) {
+ switch ($op) {
+ case 'delete' :
+ $form['bid'] = array(
+ '#type' => 'hidden',
+ '#value' => $bid,
+ );
+ $output = confirm_form('taxonomy_block_delete_form', $form, t('Are you sure you want to delete the taxonomy block?'), 'admin/block/taxonomy_block');
+ break;
+ default:
+ $output = taxonomy_block_blocks();
+ $output .= taxonomy_block_form();
+ break;
+ }
+
+ drupal_set_title(t('Taxonomy Block Administration'));
+ return $output;
+}
+
+/**
+ * Inserts a block into the database.
+ */
+function taxonomy_block_insert($edit) {
+ $sql = 'INSERT INTO {taxonomy_block} (tid, name, description, length, type, teaser) VALUES (%d, \'%s\', \'%s\', %d, \'%s\', %d)';
+ db_query($sql, $edit['tid'], $edit['name'], $edit['description'], $edit['length'], $edit['type'], $edit['teaser']);
+ drupal_set_message(t('Created taxonomy block'));
+}
+
+/**
+ * Updates a block in the database.
+ */
+function taxonomy_block_update($edit) {
+ $sql = 'UPDATE {taxonomy_block} SET tid = %d, name = \'%s\', description = \'%s\', length = %d, type = \'%s\', teaser = %d WHERE bid = %d';
+ db_query($sql, $edit['tid'], $edit['name'], $edit['description'], $edit['length'], $edit['type'], $edit['teaser'], $edit['bid']);
+ drupal_set_message(t('Updated taxonomy block'));
+}
+
+/**
+ * Deletes a block from the database.
+ */
+function taxonomy_block_delete($bid) {
+ db_query('DELETE FROM {taxonomy_block} WHERE bid = %d', $bid);
+}
+
+/**
+* Displays the block creation form.
+*/
+function taxonomy_block_form($bid = NULL) {
+ if ($bid) {
+ $block = db_fetch_object(db_query('SELECT * FROM {taxonomy_block} WHERE bid = %d', $bid));
+ }
+
+ $form['create_taxonomy_block'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Create Block'),
+ );
+ $form['create_taxonomy_block']['bid'] = array(
+ '#type' => 'hidden',
+ '#value' => $bid,
+ );
+ $form['create_taxonomy_block']['name'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Block Name'),
+ '#default_value' => $block->name,
+ '#description' => t('This is the name of your block.'),
+ );
+ $form['create_taxonomy_block']['description'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Block Description'),
+ '#default_value' => $block->description,
+ '#description' => t('This is the description of your block. It is not displayed to users.'),
+ '#required' => TRUE,
+ );
+ $form['create_taxonomy_block']['teaser'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Teaser Length'),
+ '#default_value' => $block->teaser,
+ '#description' => t('This is the length of node body content to display under each title in characters. Leave blank for none.'),
+ );
+ $form['create_taxonomy_block']['length'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Node Count'),
+ '#default_value' => $block->length,
+ '#description' => t('This is the number of nodes to display.'),
+ '#required' => TRUE,
+ );
+
+ $form['create_taxonomy_block']['tid'] = _taxonomy_block_get_taxonomy_dropdown($block->type == 'vocabulary' ? 'v'. $block->tid : $block->tid);
+
+ if($bid) {
+ $form['create_taxonomy_block'][] = array(
+ '#type' => 'submit',
+ '#value' => t('Edit Block'),
+ );
+ $form['create_taxonomy_block'][] = array(
+ '#type' => 'submit',
+ '#value' => t('Cancel'),
+ );
+ }
+ else {
+ $form['create_taxonomy_block'][] = array(
+ '#type' => 'submit',
+ '#value' => t('Create Block'),
+ );
+ }
+
+ return drupal_get_form('taxonomy_block_form', $form);
+}
+
+/**
+ * taxonomy_block_form form validate callback function.
+ */
+function taxonomy_block_form_validate($form_id, $edit) {
+ if(!$edit['description']) {
+ form_set_error('description', t('Please provide a description for your block. It will be used in administration screens only.'));
+ }
+ if($edit['length'] <=0) {
+ form_set_error('length', t('Please provide a node count greater than 0.'));
+ }
+ if(!$edit['tid']) {
+ form_set_error('tid', t('Please select a category for your block to display.'));
+ }
+}
+/**
+ * taxonomy_block_form form execute callback function.
+ */
+function taxonomy_block_form_submit($form_id, $edit) {
+ $op = $_POST['op'];
+
+ if(substr($edit['tid'], 0, 1)=='v') {
+ $edit['type'] = 'vocabulary';
+ $edit['tid'] = substr($edit['tid'], 1);
+ }
+ else {
+ $edit['type'] = 'term';
+ }
+
+ if($op == t('Create Block')) {
+ taxonomy_block_insert($edit);
+ }
+ elseif($op == t('Edit Block')) {
+ taxonomy_block_update($edit);
+ }
+ drupal_goto('admin/block/taxonomy_block');
+}
+
+function taxonomy_block_delete_form_submit($form_id, $edit) {
+ taxonomy_block_delete($edit['bid']);
+ drupal_set_message(t('The block has been deleted.'));
+ drupal_goto('admin/block/taxonomy_block');
+}
+
+/**
+* Displays a list of the blocks.
+*/
+function taxonomy_block_blocks() {
+ $result = db_query('SELECT * FROM {taxonomy_block}');
+ while ($block = db_fetch_object($result)) {
+ $links = array();
+ $links[] = l(t('edit'), 'admin/block/taxonomy_block/edit/'. $block->bid, array('title'=>t('edit block')));;
+ $links[] = l(t('delete'), 'admin/block/taxonomy_block/delete/'. $block->bid, array('title'=>t('delete block')));;
+ $blocks[] = $block->description .' - '. theme('links', $links);
+ }
+
+ $form['show_taxonomy_block'][] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Current Blocks'),
+ '#value' => theme('item_list', $blocks),
+ );
+
+ return drupal_get_form('taxonomy_block_admin', $form);
+}
+
+/**
+* Returns the requested block by $bid.
+*/
+function taxonomy_block_get_block($bid) {
+ $tids = array();
+ $result = db_fetch_object(db_query('SELECT * FROM {taxonomy_block} WHERE bid = %d', $bid));
+ if ($result) {
+ if ($result->type == 'term') {
+ $tids = taxonomy_get_children($result->tid);
+ $tids[$result->tid] = $result->tid;
+ }
+ else {
+ $tids = taxonomy_get_children(0, $result->tid);
+ }
+
+ $nodes = db_query(db_rewrite_sql('SELECT DISTINCT(n.nid), n.title, r.body, n.sticky, n.created FROM {term_node} t INNER JOIN {node} n ON t.nid = n.nid INNER JOIN {node_revisions} r ON r.vid = n.vid WHERE t.tid IN (%s) AND n.status = 1 ORDER BY n.sticky DESC, n.created DESC LIMIT %d'), implode(array_keys($tids), ','), $result->length);
+
+ $block['subject'] = $result->name;
+
+ while ($node = db_fetch_object($nodes)) {
+ if($result->teaser) {
+ $teaser = strip_tags(substr($node->body, 0, $result->teaser) . (strlen($node->body) > $result->teaser ? '...' : ''));
+ }
+ $items .= theme('taxonomy_block_list_item', $node, $teaser);
+ }
+ $content = theme('taxonomy_block_list', $items);
+
+ $content .= '';
+ $content .= l(t("more"), 'taxonomy/term/'. implode(array_keys($tids), '+'), array("title" => t("View all."))) .'';
+ $block['content'] = $content;
+ return $block;
+ }
+ else {
+ return null;
+ }
+}
+
+/**
+* Returns an array of block descriptions for the block config page.
+*/
+function taxonomy_block_get_blocks() {
+ $results = db_query('SELECT * FROM {taxonomy_block}');
+ while ($block = db_fetch_object($results)) {
+ $blocks[$block->bid]['info'] = $block->description;
+ }
+ return $blocks;
+}
+
+/**
+* Returns a dropdown event taxonomy term input control.
+*/
+function _taxonomy_block_get_taxonomy_dropdown($tid = NULL) {
+ $vocabs = taxonomy_get_vocabularies();
+
+ $links[] = '';
+ foreach ($vocabs as $vocab) {
+ $links['v'.$vocab->vid] = $vocab->name;
+ $tree = taxonomy_get_tree($vocab->vid);
+ foreach ($tree as $term) {
+ $links[$term->tid] = $vocab->name .' - '. $term->name;
+ }
+ }
+
+ return array(
+ '#type' => 'select',
+ '#title' => t('Category'),
+ '#default_value' => $tid,
+ '#options' => $links,
+ '#description' => t('Select taxonomy type to display'),
+ '#required' => TRUE,
+ );
+}
+
+/**
+ * Format a single item for a list.
+ *
+ * @ingroup themeable
+ */
+function theme_taxonomy_block_list_item($node, $teaser) {
+ $output = '- ' . l($node->title, 'node/'. $node->nid, array('title'=>t("view {$node->title} in full") ));
+ if ($teaser) {
+ $output .= '
' . $teaser;
+ }
+ $output .= ' ';
+
+ return $output;
+}
+
+/**
+ * Format the item list.
+ *
+ * @ingroup themeable
+ */
+function theme_taxonomy_block_list($items) {
+ $output = '' . $items . '
';
+
+ return $output;
+}
+?>
diff --git a/modules/throttle.module b/modules/throttle.module
new file mode 100644
index 0000000..8ee565e
--- /dev/null
+++ b/modules/throttle.module
@@ -0,0 +1,158 @@
+= %d AND uid = 0', time() - $time_period));
+ }
+ else {
+ $guests = 0;
+ }
+ if ($max_users = variable_get('throttle_user', 0)) {
+ $users = db_result(db_query('SELECT COUNT(DISTINCT(uid)) AS count FROM {sessions} WHERE timestamp >= %d AND uid != 0', time() - $time_period));
+ }
+ else {
+ $users = 0;
+ }
+
+ // update the throttle status
+ $message = '';
+ if ($max_users && $users > $max_users) {
+ if (!$throttle) {
+ variable_set('throttle_level', 1);
+ $message = format_plural($users,
+ '1 user accessing site; throttle enabled.',
+ '%count users accessing site; throttle enabled.');
+ }
+ }
+ elseif ($max_guests && $guests > $max_guests) {
+ if (!$throttle) {
+ variable_set('throttle_level', 1);
+ $message = format_plural($guests,
+ '1 guest accessing site; throttle enabled.',
+ '%count guests accessing site; throttle enabled.');
+ }
+ }
+ else {
+ if ($throttle) {
+ variable_set('throttle_level', 0);
+ // Note: unorthodox format_plural() usage due to Gettext plural limitations.
+ $message = format_plural($users, '1 user', '%count users') .', ';
+ $message .= format_plural($guests, '1 guest accessing site; throttle disabled', '%count guests accessing site; throttle disabled');
+ }
+ }
+ if ($message) {
+ cache_clear_all();
+ watchdog('throttle', t('Throttle') .': '. $message);
+ }
+ }
+}
+
+function _throttle_validate($value, $form) {
+ if ($value != NULL) {
+ if (!is_numeric($value) || $value < 0) {
+ form_set_error($form, t("'%value' is not a valid auto-throttle setting. Please enter a positive numeric value.", array('%value' => theme('placeholder', $value))));
+ }
+ }
+}
+
+/**
+ * Implementation of hook_help().
+ */
+function throttle_help($section) {
+ switch ($section) {
+ case 'admin/help#throttle':
+ $output = ''. t('The throttle module provides a congestion control throttling mechanism for automatically detecting a surge in incoming traffic. If the site gets linked to by a popular website, or otherwise comes under a "Denial of Service" (DoS) attack, your webserver might become overwhelmed. This mechanism is utilized by other modules to automatically optimize their performance by temporarily disabling CPU-intensive functionality. For example, in the site theme, you might choose to disable pictures when the site is too busy (reducing bandwidth), or in modules, you might choose to disable some complicated logic (reducing CPU utilization).') .'
';
+ $output .= ''. t('The congestion control throttle can be automatically enabled when the number of anonymous or authenticated users currently visiting the site exceeds the specified threshold. ') .'
';
+ $output .= t('You can
+
+- enable throttle for modules at administer >> module.
+- enable throttle for blocks at administer >> block.
+- administer throttle at administer >> settings >> throttle.
+
+', array('%admin-modules' => url('admin/modules'), '%admin-block' => url('admin/block'), '%admin-settings-throttle' => url('admin/settings/throttle')));
+ $output .= ''. t('For more information please read the configuration and customization handbook Throttle page.', array('%throttle' => 'http://drupal.org/handbook/modules/throttle/')) .'
';
+ return $output;
+ case 'admin/modules#description':
+ return t('Handles the auto-throttling mechanism, to control site congestion.');
+ case 'admin/settings/throttle':
+ return t('If your site gets linked to by a popular website, or otherwise comes under a "Denial of Service" (DoS) attack, your webserver might become overwhelmed. This module provides a congestion control throttling mechanism for automatically detecting a surge in incoming traffic. This mechanism is utilized by other Drupal modules to automatically optimize their performance by temporarily disabling CPU-intensive functionality.');
+ }
+}
+
+/**
+ * Implementation of hook_settings().
+ */
+function throttle_settings() {
+ _throttle_validate(variable_get('throttle_anonymous', ''), 'throttle_anonymous');
+ _throttle_validate(variable_get('throttle_user', ''), 'throttle_user');
+ $probabilities = array(0 => '100%', 1 => '50%', 2 => '33.3%', 3 => '25%', 4 => '20%', 5 => '16.6%', 7 => '12.5%', 9 => '10%', 19 => '5%', 99 => '1%', 199 => '.5%', 399 => '.25%', 989 => '.1%');
+
+ $form['throttle_anonymous'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Auto-throttle on anonymous users'),
+ '#default_value' => variable_get('throttle_anonymous', 0),
+ '#size' => 5,
+ '#maxlength' => 6,
+ '#description' => t('The congestion control throttle can be automatically enabled when the number of anonymous users currently visiting your site exceeds the specified threshold. For example, to start the throttle when your site has 250 anonymous users online at once, enter \'250\' in this field. Leave this value blank or set to "0" if you do not wish to auto-throttle on anonymous users. You can inspect the current number of anonymous users using the "Who\'s online" block.')
+ );
+ $form['throttle_user'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Auto-throttle on authenticated users'),
+ '#default_value' => variable_get('throttle_user', 0),
+ '#size' => 5,
+ '#maxlength' => 6,
+ '#description' => t('The congestion control throttle can be automatically enabled when the number of authenticated users currently visiting your site exceeds the specified threshold. For example, to start the throttle when your site has 50 registered users online at once, enter \'50\' in this field. Leave this value blank or set to "0" if you do not wish to auto-throttle on authenticated users. You can inspect the current number of authenticated users using the "Who\'s online" block.')
+ );
+ $form['throttle_probability_limiter'] = array(
+ '#type' => 'select',
+ '#title' => t('Auto-throttle probability limiter'),
+ '#default_value' => variable_get('throttle_probability_limiter', 9),
+ '#options' => $probabilities,
+ '#description' => t('The auto-throttle probability limiter is an efficiency mechanism to statistically reduce the overhead of the auto-throttle. The limiter is expressed as a percentage of page views, so for example if set to the default of 10% we only perform the extra database queries to update the throttle status 1 out of every 10 page views. The busier your site, the lower you should set the limiter value.')
+ );
+
+ return $form;
+}
diff --git a/modules/tracker.module b/modules/tracker.module
new file mode 100644
index 0000000..96bc8fd
--- /dev/null
+++ b/modules/tracker.module
@@ -0,0 +1,129 @@
+'. t('The tracker module displays the most recently added or updated content to the website allowing users to see the most recent contributions. The tracker module provides user level tracking for those who like to follow the contributions of particular authors.') .'';
+ $output .= ''. t('The "recent posts" page is available via a link in the navigation menu block and contains a reverse chronological list of new and recently-updated content. The table displays the content type, the title, the author\'s name, how many comments that item has received, and when it was last updated. Updates include any changes to the text, either by the original author or someone else, as well as any new comments added to an item. To use the tracker module to watch for a user\'s updated content, click on that user\'s profile, then the track tab.') .'
';
+ $output .= t('You can
+
+- view the most recent posts.
+- view user profiles and select the track tab.
+- not administer this module.
+
+', array('%tracker' => url('tracker'), '%profile' => url('profile')));
+ $output .= ''. t('For more information please read the configuration and customization handbook Tracker page.', array('%tracker' => 'http://drupal.org/handbook/modules/tracker/')) .'
';
+ return $output;
+ case 'admin/modules#description':
+ return t('Enables tracking of recent posts for users.');
+ }
+}
+
+/**
+ * Implementation of hook_menu().
+ */
+function tracker_menu($may_cache) {
+ global $user;
+ $items = array();
+
+ if ($may_cache) {
+ $items[] = array('path' => 'tracker', 'title' => t('recent posts'),
+ 'callback' => 'tracker_page', 'access' => user_access('access content'),
+ 'weight' => 1);
+
+ if ($user->uid) {
+ $items[] = array('path' => 'tracker/all', 'title' => t('all recent posts'),
+ 'type' => MENU_DEFAULT_LOCAL_TASK);
+ $items[] = array('path' => 'tracker/'. $user->uid, 'title' => t('my recent posts'),
+ 'type' => MENU_LOCAL_TASK);
+ }
+ }
+ else {
+ if (arg(0) == 'user' && is_numeric(arg(1))) {
+ $items[] = array('path' => 'user/'. arg(1) .'/track', 'title' => t('track'),
+ 'callback' => 'tracker_track_user', 'access' => user_access('access content'),
+ 'type' => MENU_IS_LOCAL_TASK);
+ $items[] = array('path' => 'user/'. arg(1) .'/track/posts', 'title' => t('track posts'),
+ 'type' => MENU_DEFAULT_LOCAL_TASK);
+ }
+ }
+
+ return $items;
+}
+
+/**
+ * Menu callback. Prints a listing of active nodes on the site.
+ */
+function tracker_track_user() {
+ if ($account = user_load(array('uid' => arg(1)))) {
+ if ($account->status || user_access('administer users')) {
+ drupal_set_title($account->name);
+ return tracker_page($account->uid);
+ }
+ else {
+ drupal_access_denied();
+ }
+ }
+ else {
+ drupal_not_found();
+ }
+}
+
+/**
+ * Menu callback. Prints a listing of active nodes on the site.
+ */
+function tracker_page($uid = 0) {
+ if ($uid) {
+ $sql = 'SELECT DISTINCT(n.nid), n.title, n.type, n.changed, n.uid, u.name, l.last_comment_timestamp AS last_post, l.comment_count FROM {node} n INNER JOIN {node_comment_statistics} l ON n.nid = l.nid INNER JOIN {users} u ON n.uid = u.uid LEFT JOIN {comments} c ON n.nid = c.nid AND (c.status = %d OR c.status IS NULL) WHERE n.status = 1 AND (n.uid = %d OR c.uid = %d) ORDER BY last_post DESC';
+ $sql = db_rewrite_sql($sql);
+ $sql_count = 'SELECT COUNT(DISTINCT(n.nid)) FROM {node} n LEFT JOIN {comments} c ON n.nid = c.nid AND (c.status = %d OR c.status IS NULL) WHERE n.status = 1 AND (n.uid = %d OR c.uid = %d)';
+ $sql_count = db_rewrite_sql($sql_count);
+ $result = pager_query($sql, 25, 0, $sql_count, COMMENT_PUBLISHED, $uid, $uid);
+ }
+ else {
+ $sql = 'SELECT DISTINCT(n.nid), n.title, n.type, n.changed, n.uid, u.name, l.last_comment_timestamp AS last_post, l.comment_count FROM {node} n INNER JOIN {users} u ON n.uid = u.uid INNER JOIN {node_comment_statistics} l ON n.nid = l.nid WHERE n.status = 1 ORDER BY last_post DESC';
+ $sql = db_rewrite_sql($sql);
+ $sql_count = 'SELECT COUNT(n.nid) FROM {node} n WHERE n.status = 1';
+ $sql_count = db_rewrite_sql($sql_count);
+ $result = pager_query($sql, 25, 0, $sql_count);
+ }
+
+ while ($node = db_fetch_object($result)) {
+ // Determine the number of comments:
+ $comments = 0;
+ if (module_exist('comment') && $node->comment_count) {
+ $comments = $node->comment_count;
+
+ if ($new = comment_num_new($node->nid)) {
+ $comments .= '
';
+ $comments .= l(format_plural($new, '1 new', '%count new'), "node/$node->nid", NULL, NULL, 'new');
+ }
+ }
+
+ $rows[] = array(
+ node_get_name($node->type),
+ l($node->title, "node/$node->nid") .' '. theme('mark', node_mark($node->nid, $node->changed)),
+ theme('username', $node),
+ array('class' => 'replies', 'data' => $comments),
+ t('%time ago', array('%time' => format_interval(time() - $node->last_post)))
+ );
+ }
+
+ $header = array(t('Type'), t('Post'), t('Author'), t('Replies'), t('Last post'));
+
+ $output = '';
+ $output .= theme('table', $header, $rows);
+ $output .= theme('pager', NULL, 25, 0);
+ $output .= '';
+
+ return $output;
+}
diff --git a/modules/upload.module b/modules/upload.module
new file mode 100644
index 0000000..f03d5f3
--- /dev/null
+++ b/modules/upload.module
@@ -0,0 +1,756 @@
+'. t('The upload module allows users to upload files to the site. The ability to upload files to a site is important for members of a community who want to share work. It is also useful to administrators who want to keep uploaded files connected to a node or page.') .'';
+ $output .= ''. t('Users with the upload files permission can upload attachments. You can choose which post types can take attachments on the content types settings page. Each user role can be customized for the file size of uploads, and the dimension of image files.') .'
';
+ $output .= t('You can
+
+- administer user permissions at administer >> access control.
+- administer content at administer >> settings >> content types.
+- administer upload at administer >> settings >> upload.
+
+', array('%admin-access' => url('admin/access'), '%admin-content-types' => url('admin/settings/content-types'), '%admin-upload' => url('admin/settings/upload')));
+ $output .= ''. t('For more information please read the configuration and customization handbook Upload page.', array('%upload' => 'http://drupal.org/handbook/modules/upload/')) .'
';
+ return $output;
+ case 'admin/modules#description':
+ return t('Allows users to upload and attach files to content.');
+ case 'admin/settings/upload':
+ return t('Users with the upload files permission can upload attachments. Users with the view uploaded files permission can view uploaded attachments. You can choose which post types can take attachments on the content types settings page.
', array('%permissions' => url('admin/access'), '%types' => url('admin/settings/content-types')));
+ }
+}
+
+/**
+ * Implementation of hook_perm().
+ */
+function upload_perm() {
+ return array('upload files', 'view uploaded files');
+}
+
+/**
+ * Implementation of hook_link().
+ */
+function upload_link($type, $node = 0, $main = 0) {
+ $links = array();
+
+ // Display a link with the number of attachments
+ if ($main && $type == 'node' && isset($node->files) && user_access('view uploaded files')) {
+ $num_files = 0;
+ foreach ($node->files as $file) {
+ if ($file->list) {
+ $num_files++;
+ }
+ }
+ if ($num_files) {
+ $links[] = l(format_plural($num_files, '1 attachment', '%count attachments'), "node/$node->nid", array('title' => t('Read full article to view attachments.')), NULL, 'attachments');
+ }
+ }
+
+ return $links;
+}
+
+/**
+ * Implementation of hook_menu().
+ */
+function upload_menu($may_cache) {
+ $items = array();
+
+ if ($may_cache) {
+ $items[] = array(
+ 'path' => 'upload/js',
+ 'callback' => 'upload_js',
+ 'access' => user_access('upload files'),
+ 'type' => MENU_CALLBACK
+ );
+ }
+ else {
+ // Add handlers for previewing new uploads.
+ if (isset($_SESSION['file_previews'])) {
+ foreach ($_SESSION['file_previews'] as $fid => $file) {
+ $filename = file_create_filename($file->filename, file_create_path());
+ if (variable_get('file_downloads', FILE_DOWNLOADS_PUBLIC) == FILE_DOWNLOADS_PRIVATE) {
+ // strip file_directory_path() from filename. @see file_create_url
+ if (strpos($filename, file_directory_path()) !== false) {
+ $filename = trim(substr($filename, strlen(file_directory_path())), '\\/');
+ }
+ $filename = 'system/files/' . $filename;
+ }
+
+ $items[] = array(
+ 'path' => $filename, 'title' => t('file download'),
+ 'callback' => 'upload_download',
+ 'access' => user_access('view uploaded files'),
+ 'type' => MENU_CALLBACK
+ );
+ $_SESSION['file_previews'][$fid]->_filename = $filename;
+ }
+ }
+ }
+
+ return $items;
+}
+
+function upload_settings() {
+ $form['settings_general'] = array('#type' => 'fieldset', '#title' => t('General settings'));
+ $form['settings_general']['upload_max_resolution'] = array(
+ '#type' => 'textfield', '#title' => t('Maximum resolution for uploaded images'), '#default_value' => variable_get('upload_max_resolution', 0),
+ '#size' => 15, '#maxlength' => 10, '#description' => t('The maximum allowed image size expressed as WIDTHxHEIGHT (e.g. 640x480). Set to 0 for no restriction.')
+ );
+
+ $form['settings_general']['upload_list_default'] = array('#type' => 'select', '#title' => t('List files by default'),
+ '#default_value' => variable_get('upload_list_default',1),
+ '#options' => array( 0 => t('No'), 1 => t('Yes') ),
+ '#description' => t('Set whether files attached to nodes are listed or not in the node view by default.'),
+ );
+
+ $roles = user_roles(0, 'upload files');
+
+ foreach ($roles as $rid => $role) {
+ $form["settings_role_$rid"] = array('#type' => 'fieldset', '#title' => t('Settings for %role', array('%role' => theme('placeholder', $role))), '#collapsible' => TRUE, '#collapsed' => TRUE);
+ $form["settings_role_$rid"]["upload_extensions_$rid"] = array(
+ '#type' => 'textfield', '#title' => t('Permitted file extensions'), '#default_value' => variable_get("upload_extensions_$rid", "jpg jpeg gif png txt html doc xls pdf ppt pps"),
+ '#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"]["upload_uploadsize_$rid"] = array(
+ '#type' => 'textfield', '#title' => t('Maximum file size per upload'), '#default_value' => variable_get("upload_uploadsize_$rid", 1),
+ '#size' => 5, '#maxlength' => 5, '#description' => t('The maximum size of a file a user can upload (in megabytes).')
+ );
+ $form["settings_role_$rid"]["upload_usersize_$rid"] = array(
+ '#type' => 'textfield', '#title' => t('Total file size per user'), '#default_value' => variable_get("upload_usersize_$rid", 10),
+ '#size' => 5, '#maxlength' => 5, '#description' => t('The maximum size of all files a user can have on the site (in megabytes).')
+ );
+ }
+
+ return $form;
+}
+
+function upload_download() {
+ foreach ($_SESSION['file_previews'] as $file) {
+ if ($file->_filename == $_GET['q']) {
+ file_transfer($file->filepath, array('Content-Type: '. mime_header_encode($file->filemime), 'Content-Length: '. $file->filesize));
+ }
+ }
+}
+
+function upload_file_download($file) {
+ $file = file_create_path($file);
+ $result = db_query("SELECT f.* FROM {files} f WHERE filepath = '%s'", $file);
+ if ($file = db_fetch_object($result)) {
+ if (user_access('view uploaded files')) {
+ $node = node_load($file->nid);
+ if (node_access('view', $node)) {
+ $name = mime_header_encode($file->filename);
+ $type = mime_header_encode($file->filemime);
+ // Serve images and text inline for the browser to display rather than download.
+ $disposition = ereg('^(text/|image/)', $file->filemime) ? 'inline' : 'attachment';
+ return array(
+ 'Content-Type: '. $type .'; name='. $name,
+ 'Content-Length: '. $file->filesize,
+ 'Content-Disposition: '. $disposition .'; filename='. $name
+ );
+ }
+ else {
+ return -1;
+ }
+ }
+ else {
+ return -1;
+ }
+ }
+}
+
+/**
+ * Save new uploads and attach them to the node object.
+ * append file_previews to the node object as well.
+ */
+function _upload_prepare(&$node) {
+
+ // Clean up old file previews if a post didn't get the user to this page.
+ // i.e. the user left the edit page, because they didn't want to upload anything.
+ if(count($_POST) == 0) {
+ if (is_array($_SESSION['file_previews']) && count($_SESSION['file_previews'])) {
+ foreach($_SESSION['file_previews'] as $fid => $file) {
+ file_delete($file->filepath);
+ }
+ unset($_SESSION['file_previews']);
+ }
+ }
+
+ // $_SESSION['file_current_upload'] tracks the fid of the file submitted this page request.
+ // form_builder sets the value of file->list to 0 for checkboxes added to a form after
+ // it has been submitted. Since unchecked checkboxes have no return value and do not
+ // get a key in _POST form_builder has no way of knowing the difference between a check
+ // box that wasn't present on the last form build, and a checkbox that is unchecked.
+
+ unset($_SESSION['file_current_upload']);
+
+ global $user;
+
+ // Save new file uploads to tmp dir.
+ if (($file = file_check_upload()) && user_access('upload files')) {
+
+ // Scale image uploads.
+ $file = _upload_image($file);
+
+ $key = 'upload_'. count($_SESSION['file_previews']);
+ $file->fid = $key;
+ $file->source = $key;
+ $file->list = variable_get('upload_list_default',1);
+ $_SESSION['file_previews'][$key] = $file;
+
+ // Store the uploaded fid for this page request in case of submit without
+ // preview or attach. See earlier notes.
+ $_SESSION['file_current_upload'] = $key;
+ }
+
+ // Attach file previews to node object.
+ if (is_array($_SESSION['file_previews']) && count($_SESSION['file_previews'])) {
+ foreach($_SESSION['file_previews'] as $fid => $file) {
+ if ($user->uid != 1) {
+ // Here something.php.pps becomes something.php_.pps
+ $file->filename = upload_munge_filename($file->filename, NULL, 0);
+ $file->description = $file->filename;
+ }
+ $node->files[$fid] = $file;
+ }
+ }
+}
+
+function upload_form_alter($form_id, &$form) {
+ if (isset($form['type'])) {
+ if ($form['type']['#value'] .'_node_settings' == $form_id) {
+ $form['workflow']['upload_'. $form['type']['#value']] = array(
+ '#type' => 'radios', '#title' => t('Attachments'), '#default_value' => variable_get('upload_'. $form['type']['#value'], 1),
+ '#options' => array(t('Disabled'), t('Enabled')),
+ );
+ }
+
+ $node = $form['#node'];
+ if ($form['type']['#value'] .'_node_form' == $form_id && variable_get("upload_$node->type", TRUE) && user_access('upload files')) {
+ drupal_add_js('misc/progress.js');
+ drupal_add_js('misc/upload.js');
+
+ // Attachments fieldset
+ $form['attachments'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('File attachments'),
+ '#collapsible' => TRUE,
+ '#collapsed' => empty($node->files),
+ '#description' => t('Changes made to the attachments are not permanent until you save this post. The first "listed" file will be included in RSS feeds.'),
+ '#prefix' => ' ',
+ '#weight' => 30,
+ );
+
+ // Wrapper for fieldset contents (used by upload JS).
+ $form['attachments']['wrapper'] = array(
+ '#prefix' => '',
+ '#suffix' => '',
+ );
+ $form['attachments']['wrapper'] += _upload_form($node);
+ $form['#attributes']['enctype'] = 'multipart/form-data';
+ }
+ }
+}
+
+function _upload_validate(&$node) {
+ // Accumulator for disk space quotas.
+ $filesize = 0;
+
+ // Check if node->files exists, and if it contains something.
+ if (is_array($node->files)) {
+ // Update existing files with form data.
+ foreach($node->files as $fid => $file) {
+ // Convert file to object for compatibility
+ $file = (object)$file;
+
+ // Validate new uploads.
+ if (strpos($fid, 'upload') !== false && !$file->remove) {
+ global $user;
+
+ // Bypass validation for uid = 1.
+ if ($user->uid != 1) {
+ // Update filesize accumulator.
+ $filesize += $file->filesize;
+
+ // Validate file against all users roles.
+ // Only denies an upload when all roles prevent it.
+
+ $total_usersize = upload_space_used($user->uid) + $filesize;
+ $error = array();
+ foreach ($user->roles as $rid => $name) {
+ $extensions = variable_get("upload_extensions_$rid", 'jpg jpeg gif png txt html doc xls pdf ppt pps');
+ $uploadsize = variable_get("upload_uploadsize_$rid", 1) * 1024 * 1024;
+ $usersize = variable_get("upload_usersize_$rid", 1) * 1024 * 1024;
+
+ $regex = '/\.('. ereg_replace(' +', '|', preg_quote($extensions)) .')$/i';
+
+ if (!preg_match($regex, $file->filename)) {
+ $error['extension']++;
+ }
+
+ if ($uploadsize && $file->filesize > $uploadsize) {
+ $error['uploadsize']++;
+ }
+
+ if ($usersize && $total_usersize + $file->filesize > $usersize) {
+ $error['usersize']++;
+ }
+ }
+
+ $user_roles = count($user->roles);
+ $valid = TRUE;
+ if ($error['extension'] == $user_roles) {
+ form_set_error('upload', t('The selected file %name can not be attached to this post, because it is only possible to attach files with the following extensions: %files-allowed.', array('%name' => theme('placeholder', $file->filename), '%files-allowed' => theme('placeholder', $extensions))));
+ $valid = FALSE;
+ }
+ elseif ($error['uploadsize'] == $user_roles) {
+ form_set_error('upload', t('The selected file %name can not be attached to this post, because it exceeded the maximum filesize of %maxsize.', array('%name' => theme('placeholder', $file->filename), '%maxsize' => theme('placeholder', format_size($uploadsize)))));
+ $valid = FALSE;
+ }
+ elseif ($error['usersize'] == $user_roles) {
+ form_set_error('upload', t('The selected file %name can not be attached to this post, because the disk quota of %quota has been reached.', array('%name' => theme('placeholder', $file->filename), '%quota' => theme('placeholder', format_size($usersize)))));
+ $valid = FALSE;
+ }
+ elseif (strlen($file->filename) > 255) {
+ form_set_error('upload', t('The selected file %name can not be attached to this post, because the filename is too long.', array('%name' => theme('placeholder', $file->filename))));
+ $valid = FALSE;
+ }
+
+ if (!$valid) {
+ unset($node->files[$fid], $_SESSION['file_previews'][$fid]);
+ file_delete($file->filepath);
+ }
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Implementation of hook_nodeapi().
+ */
+function upload_nodeapi(&$node, $op, $teaser) {
+ switch ($op) {
+
+ case 'load':
+ $output = '';
+ if (variable_get("upload_$node->type", 1) == 1) {
+ $output['files'] = upload_load($node);
+ return $output;
+ }
+ break;
+
+ case 'prepare':
+ _upload_prepare($node);
+ break;
+
+ case 'validate':
+ _upload_validate($node);
+ break;
+
+ case 'view':
+ if (isset($node->files) && user_access('view uploaded files')) {
+ // Manipulate so that inline references work in preview
+ if (!variable_get('clean_url', 0)) {
+ $previews = array();
+ foreach ($node->files as $file) {
+ if (strpos($file->fid, 'upload') !== false) {
+ $previews[] = $file;
+ }
+ }
+
+ // URLs to files being previewed are actually Drupal paths. When Clean
+ // URLs are disabled, the two do not match. We perform an automatic
+ // replacement from temporary to permanent URLs. That way, the author
+ // can use the final URL in the body before having actually saved (to
+ // place inline images for example).
+ foreach ($previews as $file) {
+ $old = file_create_filename($file->filename, file_create_path());
+ $new = url($old);
+ $node->body = str_replace($old, $new, $node->body);
+ $node->teaser = str_replace($old, $new, $node->teaser);
+ }
+ }
+
+ // Add the attachments list to node body
+ if (count($node->files) && !$teaser) {
+ $node->body .= theme('upload_attachments', $node->files);
+ }
+ }
+ break;
+
+ case 'insert':
+ case 'update':
+ if (user_access('upload files')) {
+ upload_save($node);
+ }
+ break;
+
+ case 'delete':
+ upload_delete($node);
+ break;
+
+ case 'delete revision':
+ upload_delete_revision($node);
+ break;
+
+ case 'search result':
+ return is_array($node->files) ? format_plural(count($node->files), '1 attachment', '%count attachments') : null;
+
+ case 'rss item':
+ if (is_array($node->files)) {
+ $files = array();
+ foreach ($node->files as $file) {
+ if ($file->list) {
+ $files[] = $file;
+ }
+ }
+ if (count($files) > 0) {
+ // RSS only allows one enclosure per item
+ $file = array_shift($files);
+ return array(
+ array(
+ 'key' => 'enclosure',
+ 'attributes' => array(
+ 'url' => file_create_url($file->filepath),
+ 'length' => $file->filesize,
+ 'type' => $file->filemime
+ )
+ )
+ );
+ }
+ }
+ return array();
+ }
+}
+
+/**
+ * Displays file attachments in table
+ */
+function theme_upload_attachments($files) {
+ $header = array(t('Attachment'), t('Size'));
+ $rows = array();
+ foreach ($files as $file) {
+ if ($file->list) {
+ $href = check_url(($file->fid ? file_create_url($file->filepath) : url(file_create_filename($file->filename, file_create_path()))));
+ $text = check_plain($file->description ? $file->description : $file->filename);
+ $rows[] = array(l($text, $href), format_size($file->filesize));
+ }
+ }
+ if (count($rows)) {
+ return theme('table', $header, $rows, array('id' => 'attachments'));
+ }
+}
+
+/**
+ * Determine how much disk space is occupied by a user's uploaded files.
+ *
+ * @param $uid
+ * The integer user id of a user.
+ * @return
+ * The amount of disk space used by the user in bytes.
+ */
+function upload_space_used($uid) {
+ return db_result(db_query('SELECT SUM(filesize) FROM {files} f INNER JOIN {node} n ON f.nid = n.nid WHERE n.uid = %d', $uid));
+}
+
+/**
+ * Determine how much disk space is occupied by uploaded files.
+ *
+ * @return
+ * The amount of disk space used by uploaded files in bytes.
+ */
+function upload_total_space_used() {
+ return db_result(db_query('SELECT SUM(filesize) FROM {files}'));
+}
+
+/**
+ * Munge the filename as needed for security purposes.
+ *
+ * @param $filename
+ * The name of a file to modify.
+ * @param $extensions
+ * A space separated list of valid extensions. If this is blank, we'll use
+ * the admin-defined defaults for the user role from upload_extensions_$rid.
+ * @param $alerts
+ * Whether alerts (watchdog, drupal_set_message()) should be displayed.
+ * @return $filename
+ * The potentially modified $filename.
+ */
+function upload_munge_filename($filename, $extensions = NULL, $alerts = 1) {
+ global $user;
+
+ $original = $filename;
+
+ // Allow potentially insecure uploads for very savvy users and admin
+ if (!variable_get('allow_insecure_uploads', 0)) {
+
+ if (!isset($extensions)) {
+ $extensions = '';
+ foreach ($user->roles as $rid => $name) {
+ $extensions .= ' '. variable_get("upload_extensions_$rid", variable_get('upload_extensions_default', 'jpg jpeg gif png txt html doc xls pdf ppt pps'));
+ }
+
+ }
+
+ $whitelist = array_unique(explode(' ', trim($extensions)));
+
+ $filename_parts = explode('.', $filename);
+
+ $new_filename = array_shift($filename_parts); // Remove file basename.
+ $final_extension = array_pop($filename_parts); // Remove final extension.
+
+ foreach($filename_parts as $filename_part) {
+ $new_filename .= ".$filename_part";
+ if (!in_array($filename_part, $whitelist) && preg_match("/^[a-zA-Z]{2,5}\d?$/", $filename_part)) {
+ $new_filename .= '_';
+ }
+ }
+ $filename = "$new_filename.$final_extension";
+ }
+
+ if ($alerts && $original != $filename) {
+ $message = t('Your filename has been renamed to conform to site policy.');
+ drupal_set_message($message);
+ }
+
+ return $filename;
+}
+
+/**
+ * Undo the effect of upload_munge_filename().
+ */
+function upload_unmunge_filename($filename) {
+ return str_replace('_.', '.', $filename);
+}
+
+function upload_save($node) {
+ if (!is_array($node->files)) {
+ return;
+ }
+
+ foreach ($node->files as $fid => $file) {
+ // Convert file to object for compatibility
+ $file = (object)$file;
+
+ // Remove file. Process removals first since no further processing
+ // will be required.
+ if ($file->remove) {
+ // Remove file previews...
+ if (strpos($file->fid, 'upload') !== false) {
+ file_delete($file->filepath);
+ }
+
+ // Remove managed files.
+ else {
+ db_query('DELETE FROM {file_revisions} WHERE fid = %d AND vid = %d', $fid, $node->vid);
+ // Only delete a file if it isn't used by any revision
+ $count = db_result(db_query('SELECT COUNT(fid) FROM {file_revisions} WHERE fid = %d', $fid));
+ if ($count < 1) {
+ db_query('DELETE FROM {files} WHERE fid = %d', $fid);
+ file_delete($file->filepath);
+ }
+ }
+ }
+
+ // New file upload
+ elseif (strpos($file->fid, 'upload') !== false) {
+ if ($file = file_save_upload($file, $file->filename)) {
+ $file->fid = db_next_id('{files}_fid');
+ db_query("INSERT INTO {files} (fid, nid, filename, filepath, filemime, filesize) VALUES (%d, %d, '%s', '%s', '%s', %d)", $file->fid, $node->nid, $file->filename, $file->filepath, $file->filemime, $file->filesize);
+ db_query("INSERT INTO {file_revisions} (fid, vid, list, description) VALUES (%d, %d, %d, '%s')", $file->fid, $node->vid, $file->list, $file->description);
+ }
+ unset($_SESSION['file_previews'][$fid]);
+ }
+
+ // Create a new revision, as needed
+ elseif ($node->old_vid && is_numeric($fid)) {
+ db_query("INSERT INTO {file_revisions} (fid, vid, list, description) VALUES (%d, %d, %d, '%s')", $file->fid, $node->vid, $file->list, $file->description);
+ }
+
+ // Update existing revision
+ else {
+ db_query("UPDATE {file_revisions} SET list = %d, description = '%s' WHERE fid = %d AND vid = %d", $file->list, $file->description, $file->fid, $node->vid);
+ }
+ }
+}
+
+function upload_delete($node) {
+ $files = array();
+ $result = db_query('SELECT * FROM {files} WHERE nid = %d', $node->nid);
+ while ($file = db_fetch_object($result)) {
+ $files[$file->fid] = $file;
+ }
+
+ foreach ($files as $fid => $file) {
+ // Delete all file revision information associated with the node
+ db_query('DELETE FROM {file_revisions} WHERE fid = %d', $fid);
+ file_delete($file->filepath);
+ }
+
+ // Delete all files associated with the node
+ db_query('DELETE FROM {files} WHERE nid = %d', $node->nid);
+}
+
+function upload_delete_revision($node) {
+ if (is_array($node->files)) {
+ foreach ($node->files as $file) {
+ // Check if the file will be used after this revision is deleted
+ $count = db_result(db_query('SELECT COUNT(fid) FROM {file_revisions} WHERE fid = %d', $file->fid));
+
+ // if the file won't be used, delete it
+ if ($count < 2) {
+ db_query('DELETE FROM {files} WHERE fid = %d', $file->fid);
+ file_delete($file->filepath);
+ }
+ }
+ }
+
+ // delete the revision
+ db_query('DELETE FROM {file_revisions} WHERE vid = %d', $node->vid);
+}
+
+function _upload_form($node) {
+
+ $form['#theme'] = 'upload_form_new';
+
+ if (is_array($node->files) && count($node->files)) {
+ $form['files']['#theme'] = 'upload_form_current';
+ $form['files']['#tree'] = TRUE;
+ foreach ($node->files as $key => $file) {
+ $description = file_create_url((strpos($file->fid, 'upload') === false ? $file->filepath : file_create_filename($file->filename, file_create_path())));
+ $description = "". check_plain($description) ."";
+ $form['files'][$key]['description'] = array('#type' => 'textfield', '#default_value' => (strlen($file->description)) ? $file->description : $file->filename, '#maxlength' => 256, '#description' => $description );
+
+ $form['files'][$key]['size'] = array('#type' => 'markup', '#value' => format_size($file->filesize));
+ $form['files'][$key]['remove'] = array('#type' => 'checkbox', '#default_value' => $file->remove);
+ $form['files'][$key]['list'] = array('#type' => 'checkbox', '#default_value' => $file->list);
+ // if the file was uploaded this page request, set value. this fixes the problem
+ // formapi has recognizing new checkboxes. see comments in _upload_prepare.
+ if ($_SESSION['file_current_upload'] == $file->fid) {
+ $form['files'][$key]['list']['#value'] = variable_get('upload_list_default',1);
+ }
+ $form['files'][$key]['filename'] = array('#type' => 'value', '#value' => $file->filename);
+ $form['files'][$key]['filepath'] = array('#type' => 'value', '#value' => $file->filepath);
+ $form['files'][$key]['filemime'] = array('#type' => 'value', '#value' => $file->filemime);
+ $form['files'][$key]['filesize'] = array('#type' => 'value', '#value' => $file->filesize);
+ $form['files'][$key]['fid'] = array('#type' => 'value', '#value' => $file->fid);
+ }
+ }
+
+ if (user_access('upload files')) {
+ // This div is hidden when the user uploads through JS.
+ $form['new'] = array(
+ '#prefix' => '',
+ '#suffix' => '',
+ );
+ $form['new']['upload'] = array('#type' => 'file', '#title' => t('Attach new file'), '#size' => 40);
+ $form['new']['attach'] = array('#type' => 'button', '#value' => t('Attach'), '#name'=> 'attach', '#attributes' => array('id' => 'attach'));
+ // The class triggers the js upload behaviour.
+ $form['attach'] = array('#type' => 'hidden', '#value' => url('upload/js', NULL, NULL, TRUE), '#attributes' => array('class' => 'upload'));
+ }
+
+ // Needed for JS
+ $form['current']['vid'] = array('#type' => 'hidden', '#value' => $node->vid);
+ return $form;
+}
+
+/**
+ * Theme the attachments list.
+ */
+function theme_upload_form_current(&$form) {
+ $header = array(t('Delete'), t('List'), t('Description'), t('Size'));
+
+ foreach (element_children($form) as $key) {
+ $row = array();
+ $row[] = form_render($form[$key]['remove']);
+ $row[] = form_render($form[$key]['list']);
+ $row[] = form_render($form[$key]['description']);
+ $row[] = form_render($form[$key]['size']);
+ $rows[] = $row;
+ }
+ $output = theme('table', $header, $rows);
+ $output .= form_render($form);
+ return $output;
+}
+
+/**
+ * Theme the attachment form.
+ * Note: required to output prefix/suffix.
+ */
+function theme_upload_form_new($form) {
+ $output = form_render($form);
+ return $output;
+}
+
+function upload_load($node) {
+ $files = array();
+
+ if ($node->vid) {
+ $result = db_query('SELECT * FROM {files} f INNER JOIN {file_revisions} r ON f.fid = r.fid WHERE r.vid = %d ORDER BY f.fid', $node->vid);
+ while ($file = db_fetch_object($result)) {
+ $files[$file->fid] = $file;
+ }
+ }
+
+ return $files;
+}
+
+/**
+ * Check an upload, if it is an image, make sure it fits within the
+ * maximum dimensions allowed.
+ */
+function _upload_image($file) {
+ $info = image_get_info($file->filepath);
+
+ if ($info) {
+ list($width, $height) = explode('x', variable_get('upload_max_resolution', 0));
+ if ($width && $height) {
+ $result = image_scale($file->filepath, $file->filepath, $width, $height);
+ if ($result) {
+ $file->filesize = filesize($file->filepath);
+ drupal_set_message(t('The image was resized to fit within the maximum allowed resolution of %resolution pixels.', array('%resolution' => theme('placeholder', variable_get('upload_max_resolution', 0)))));
+ }
+ }
+ }
+
+ return $file;
+}
+
+/**
+ * Menu-callback for JavaScript-based uploads.
+ */
+function upload_js() {
+ // We only do the upload.module part of the node validation process.
+ $node = (object)$_POST['edit'];
+
+ // Load existing node files.
+ $node->files = upload_load($node);
+
+ // Handle new uploads, and merge tmp files into node-files.
+ _upload_prepare($node);
+ _upload_validate($node);
+
+ $form = _upload_form($node);
+ foreach (module_implements('form_alter') as $module) {
+ $function = $module .'_form_alter';
+ $function('upload_js', $form);
+ }
+ $form = form_builder('upload_js', $form);
+ $output = theme('status_messages') . form_render($form);
+ // We send the updated file attachments form.
+ print drupal_to_js(array('status' => TRUE, 'data' => $output));
+ exit;
+}
diff --git a/modules/user.module b/modules/user.module
new file mode 100644
index 0000000..fe3a7a3
--- /dev/null
+++ b/modules/user.module
@@ -0,0 +1,2134 @@
+ $value) {
+ if ($key == 'uid' || $key == 'status') {
+ $query[] = "$key = %d";
+ $params[] = $value;
+ }
+ else if ($key == 'pass') {
+ $query[] = "pass = '%s'";
+ $params[] = md5($value);
+ }
+ else {
+ $query[]= "LOWER($key) = LOWER('%s')";
+ $params[] = $value;
+ }
+ }
+ $result = db_query('SELECT * FROM {users} u WHERE ' . implode(' AND ', $query), $params);
+
+ if (db_num_rows($result)) {
+ $user = db_fetch_object($result);
+ $user = drupal_unpack($user);
+
+ $user->roles = array();
+ if ($user->uid) {
+ $user->roles[DRUPAL_AUTHENTICATED_RID] = 'authenticated user';
+ }
+ else {
+ $user->roles[DRUPAL_ANONYMOUS_RID] = 'anonymous user';
+ }
+ $result = db_query('SELECT r.rid, r.name FROM {role} r INNER JOIN {users_roles} ur ON ur.rid = r.rid WHERE ur.uid = %d', $user->uid);
+ while ($role = db_fetch_object($result)) {
+ $user->roles[$role->rid] = $role->name;
+ }
+ user_module_invoke('load', $array, $user);
+ }
+ else {
+ $user = FALSE;
+ }
+
+ return $user;
+}
+
+/**
+ * Save changes to a user account or add a new user.
+ *
+ * @param $account
+ * The $user object for the user to modify or add. If $user->uid is
+ * omitted, a new user will be added.
+ *
+ * @param $array
+ * An array of fields and values to save. For example array('name' => 'My name');
+ * Setting a field to null deletes it from the data column.
+ *
+ * @param $category
+ * (optional) The category for storing profile information in.
+ */
+function user_save($account, $array = array(), $category = 'account') {
+ // Dynamically compose a SQL query:
+ $user_fields = user_fields();
+ if ($account->uid) {
+ user_module_invoke('update', $array, $account, $category);
+
+ $data = unserialize(db_result(db_query('SELECT data FROM {users} WHERE uid = %d', $account->uid)));
+ foreach ($array as $key => $value) {
+ if ($key == 'pass' && !empty($value)) {
+ $query .= "$key = '%s', ";
+ $v[] = md5($value);
+ }
+ else if ((substr($key, 0, 4) !== 'auth') && ($key != 'pass')) {
+ if (in_array($key, $user_fields)) {
+ // Save standard fields
+ $query .= "$key = '%s', ";
+ $v[] = $value;
+ }
+ else if ($key != 'roles') {
+ // Roles is a special case: it used below.
+ if ($value === null) {
+ unset($data[$key]);
+ }
+ else {
+ $data[$key] = $value;
+ }
+ }
+ }
+ }
+ $query .= "data = '%s' ";
+ $v[] = serialize($data);
+
+ db_query("UPDATE {users} SET $query WHERE uid = %d", array_merge($v, array($account->uid)));
+
+ // Reload user roles if provided
+ if (is_array($array['roles'])) {
+ db_query('DELETE FROM {users_roles} WHERE uid = %d', $account->uid);
+
+ foreach (array_keys($array['roles']) as $rid) {
+ if (!in_array($rid, array(DRUPAL_ANONYMOUS_RID, DRUPAL_AUTHENTICATED_RID))) {
+ db_query('INSERT INTO {users_roles} (uid, rid) VALUES (%d, %d)', $account->uid, $rid);
+ }
+ }
+ }
+
+ // Delete a blocked user's sessions to kick them if they are online.
+ if (isset($array['status']) && $array['status'] == 0) {
+ db_query('DELETE FROM {sessions} WHERE uid = %d', $account->uid);
+ }
+
+ // Refresh user object
+ $user = user_load(array('uid' => $account->uid));
+ }
+ else {
+ $array['created'] = time();
+ $array['uid'] = db_next_id('{users}_uid');
+
+ // Note, we wait with saving the data column to prevent module-handled
+ // fields from being saved there. We cannot invoke hook_user('insert') here
+ // because we don't have a fully initialized user object yet.
+ foreach ($array as $key => $value) {
+ switch($key) {
+ case 'pass':
+ $fields[] = $key;
+ $values[] = md5($value);
+ $s[] = "'%s'";
+ break;
+ case 'uid': case 'mode': case 'sort':
+ case 'threshold': case 'created': case 'access':
+ case 'login': case 'status':
+ $fields[] = $key;
+ $values[] = $value;
+ $s[] = "%d";
+ break;
+ default:
+ if (substr($key, 0, 4) !== 'auth' && in_array($key, $user_fields)) {
+ $fields[] = $key;
+ $values[] = $value;
+ $s[] = "'%s'";
+ }
+ break;
+ }
+ }
+ db_query('INSERT INTO {users} ('. implode(', ', $fields) .') VALUES ('. implode(', ', $s) .')', $values);
+
+ // Reload user roles (delete just to be safe).
+ db_query('DELETE FROM {users_roles} WHERE uid = %d', $array['uid']);
+ foreach ((array)$array['roles'] as $rid) {
+ if (!in_array($rid, array(DRUPAL_ANONYMOUS_RID, DRUPAL_AUTHENTICATED_RID))) {
+ db_query('INSERT INTO {users_roles} (uid, rid) VALUES (%d, %d)', $array['uid'], $rid);
+ }
+ }
+
+ // Build the initial user object.
+ $user = user_load(array('uid' => $array['uid']));
+
+ user_module_invoke('insert', $array, $user, $category);
+
+ // Build and save the serialized data field now
+ $data = array();
+ foreach ($array as $key => $value) {
+ if ((substr($key, 0, 4) !== 'auth') && ($key != 'roles') && (!in_array($key, $user_fields)) && ($value !== null)) {
+ $data[$key] = $value;
+ }
+ }
+ db_query("UPDATE {users} SET data = '%s' WHERE uid = %d", serialize($data), $user->uid);
+
+ // Build the finished user object.
+ $user = user_load(array('uid' => $array['uid']));
+ }
+
+ // Save distributed authentication mappings
+ $authmaps = array();
+ foreach ($array as $key => $value) {
+ if (substr($key, 0, 4) == 'auth') {
+ $authmaps[$key] = $value;
+ }
+ }
+ if (sizeof($authmaps) > 0) {
+ user_set_authmaps($user, $authmaps);
+ }
+
+ return $user;
+}
+
+/**
+ * Verify the syntax of the given name.
+ */
+function user_validate_name($name) {
+ if (!strlen($name)) return t('You must enter a username.');
+ if (substr($name, 0, 1) == ' ') return t('The username cannot begin with a space.');
+ if (substr($name, -1) == ' ') return t('The username cannot end with a space.');
+ if (ereg(' ', $name)) return t('The username cannot contain multiple spaces in a row.');
+ if (ereg("[^\x80-\xF7 [:alnum:]@_.-]", $name)) return t('The username contains an illegal character.');
+ if (preg_match('/[\x{80}-\x{A0}'. // Non-printable ISO-8859-1 + NBSP
+ '\x{AD}'. // Soft-hyphen
+ '\x{2000}-\x{200F}'. // Various space characters
+ '\x{2028}-\x{202F}'. // Bidirectional text overrides
+ '\x{205F}-\x{206F}'. // Various text hinting characters
+ '\x{FEFF}'. // Byte order mark
+ '\x{FF01}-\x{FF60}'. // Full-width latin
+ '\x{FFF9}-\x{FFFD}]/u', // Replacement characters
+ $name)) {
+ return t('The username contains an illegal character.');
+ }
+ if (ereg('@', $name) && !eregi('@([0-9a-z](-?[0-9a-z])*.)+[a-z]{2}([zmuvtg]|fo|me)?$', $name)) return t('The username is not a valid authentication ID.');
+ if (strlen($name) > 56) return t('The username %name is too long: it must be less than 56 characters.', array('%name' => theme('placeholder', $name)));
+}
+
+function user_validate_mail($mail) {
+ if (!$mail) return t('You must enter an e-mail address.');
+ if (!valid_email_address($mail)) {
+ return t('The e-mail address %mail is not valid.', array('%mail' => theme('placeholder', $mail)));
+ }
+}
+
+function user_validate_picture($file, &$edit, $user) {
+ global $form_values;
+ // Initialize the picture:
+ $form_values['picture'] = $user->picture;
+
+ // Check that uploaded file is an image, with a maximum file size
+ // and maximum height/width.
+ $info = image_get_info($file->filepath);
+ list($maxwidth, $maxheight) = explode('x', variable_get('user_picture_dimensions', '85x85'));
+
+ if (!$info || !$info['extension']) {
+ form_set_error('picture_upload', t('The uploaded file was not an image.'));
+ }
+ else if (image_get_toolkit()) {
+ image_scale($file->filepath, $file->filepath, $maxwidth, $maxheight);
+ }
+ else if (filesize($file->filepath) > (variable_get('user_picture_file_size', '30') * 1000)) {
+ form_set_error('picture_upload', t('The uploaded image is too large; the maximum file size is %size kB.', array('%size' => variable_get('user_picture_file_size', '30'))));
+ }
+ else if ($info['width'] > $maxwidth || $info['height'] > $maxheight) {
+ form_set_error('picture_upload', t('The uploaded image is too large; the maximum dimensions are %dimensions pixels.', array('%dimensions' => variable_get('user_picture_dimensions', '85x85'))));
+ }
+
+ if (!form_get_errors()) {
+ if ($file = file_save_upload('picture_upload', variable_get('user_picture_path', 'pictures') .'/picture-'. $user->uid . '.' . $info['extension'], 1)) {
+ $form_values['picture'] = $file->filepath;
+ }
+ else {
+ form_set_error('picture_upload', t("Failed to upload the picture image; the %directory directory doesn't exist.", array('%directory' => ''. variable_get('user_picture_path', 'pictures') .'')));
+ }
+ }
+}
+
+/**
+ * Generate a random alphanumeric password.
+ */
+function user_password($length = 10) {
+ // This variable contains the list of allowable characters for the
+ // password. Note that the number 0 and the letter 'O' have been
+ // removed to avoid confusion between the two. The same is true
+ // of 'I', 1, and l.
+ $allowable_characters = 'abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789';
+
+ // Zero-based count of characters in the allowable list:
+ $len = strlen($allowable_characters) - 1;
+
+ // Declare the password as a blank string.
+ $pass = '';
+
+ // Loop the number of times specified by $length.
+ for ($i = 0; $i < $length; $i++) {
+
+ // Each iteration, pick a random character from the
+ // allowable string and append it to the password:
+ $pass .= $allowable_characters[mt_rand(0, $len)];
+ }
+
+ return $pass;
+}
+
+/**
+ * Determine whether the user has a given privilege.
+ *
+ * @param $string
+ * The permission, such as "administer nodes", being checked for.
+ * @param $account
+ * (optional) The account to check, if not given use currently logged in user.
+ *
+ * @return
+ * boolean TRUE if the current user has the requested permission.
+ *
+ * All permission checks in Drupal should go through this function. This
+ * way, we guarantee consistent behavior, and ensure that the superuser
+ * can perform all actions.
+ */
+function user_access($string, $account = NULL) {
+ global $user;
+ static $perm = array();
+
+ if (is_null($account)) {
+ $account = $user;
+ }
+
+ // User #1 has all privileges:
+ if ($account->uid == 1) {
+ return TRUE;
+ }
+
+ // To reduce the number of SQL queries, we cache the user's permissions
+ // in a static variable.
+ if (!isset($perm[$account->uid])) {
+ $result = db_query("SELECT DISTINCT(p.perm) FROM {role} r INNER JOIN {permission} p ON p.rid = r.rid WHERE r.rid IN (%s)", implode(',', array_keys($account->roles)));
+
+ $perm[$account->uid] = '';
+ while ($row = db_fetch_object($result)) {
+ $perm[$account->uid] .= "$row->perm, ";
+ }
+ }
+
+ if (isset($perm[$account->uid])) {
+ return strpos($perm[$account->uid], "$string, ") !== FALSE;
+ }
+
+ return FALSE;
+}
+
+/**
+ * Checks for usernames blocked by user administration
+ *
+ * @return boolean true for blocked users, false for active
+ */
+function user_is_blocked($name) {
+ $allow = db_fetch_object(db_query("SELECT * FROM {users} WHERE status = 1 AND name = LOWER('%s')", $name));
+ $deny = db_fetch_object(db_query("SELECT * FROM {users} WHERE status = 0 AND name = LOWER('%s')", $name));
+
+ return $deny && !$allow;
+}
+
+/**
+ * Send an e-mail message, using Drupal variables and default settings.
+ * More information in the PHP function reference for mail()
+ * @param $mail
+ * The mail adres or addresses where the message will be send to. The
+ * formatting of this string must comply with RFC 2822. Some examples are:
+ * user@example.com
+ * user@example.com, anotheruser@example.com
+ * User
+ * User , Another User
+ * @param $subject
+ * Subject of the email to be sent. This must not contain any newline characters, or the mail may not be sent properly.
+ * @param $message
+ * Message to be sent. Drupal will format the correct line endings for you.
+ * @param $header
+ * String to be inserted at the end of the email header. This is typically
+ * used to add extra headers (From, Cc, and Bcc). Multiple extra headers
+ * should be separated with a CRLF (\r\n).
+ * When sending mail, the mail must contain a From header.
+ * @return Returns TRUE if the mail was successfully accepted for delivery, FALSE otherwise.
+ */
+function user_mail($mail, $subject, $message, $header) {
+ if (variable_get('smtp_library', '') && file_exists(variable_get('smtp_library', ''))) {
+ include_once './' . variable_get('smtp_library', '');
+ return user_mail_wrapper($mail, $subject, $message, $header);
+ }
+ else {
+ /*
+ ** Note: if you are having problems with sending mail, or mails look wrong
+ ** when they are received you may have to modify the str_replace to suit
+ ** your systems.
+ ** - \r\n will work under dos and windows.
+ ** - \n will work for linux, unix and BSDs.
+ ** - \r will work for macs.
+ **
+ ** According to RFC 2646, it's quite rude to not wrap your e-mails:
+ **
+ ** "The Text/Plain media type is the lowest common denominator of
+ ** Internet e-mail, with lines of no more than 997 characters (by
+ ** convention usually no more than 80), and where the CRLF sequence
+ ** represents a line break [MIME-IMT]."
+ **
+ ** CRLF === \r\n
+ **
+ ** http://www.rfc-editor.org/rfc/rfc2646.txt
+ **
+ */
+ return mail(
+ $mail,
+ mime_header_encode($subject),
+ str_replace("\r", '', $message),
+ "MIME-Version: 1.0\nContent-Type: text/plain; charset=UTF-8; format=flowed\nContent-transfer-encoding: 8Bit\n" . $header
+ );
+ }
+}
+
+function user_fields() {
+ static $fields;
+
+ if (!$fields) {
+ $result = db_query('SELECT * FROM {users} WHERE uid = 1');
+ if (db_num_rows($result)) {
+ $fields = array_keys(db_fetch_array($result));
+ }
+ else {
+ // Make sure we return the default fields at least
+ $fields = array('uid', 'name', 'pass', 'mail', 'picture', 'mode', 'sort', 'threshold', 'theme', 'signature', 'created', 'access', 'login', 'status', 'timezone', 'language', 'init', 'data');
+ }
+ }
+
+ return $fields;
+}
+
+/**
+ * Implementation of hook_perm().
+ */
+function user_perm() {
+ return array('administer access control', 'administer users', 'access user profiles', 'change own username');
+}
+
+/**
+ * Implementation of hook_file_download().
+ *
+ * Ensure that user pictures (avatars) are always downloadable.
+ */
+function user_file_download($file) {
+ if (strpos($file, variable_get('user_picture_path', 'pictures') .'/picture-') === 0) {
+ $info = image_get_info(file_create_path($file));
+ return array('Content-type: '. $info['mime_type']);
+ }
+}
+
+/**
+ * Implementation of hook_search().
+ */
+function user_search($op = 'search', $keys = null) {
+ switch ($op) {
+ case 'name':
+ if (user_access('access user profiles')) {
+ return t('users');
+ }
+ case 'search':
+ if (user_access('access user profiles')) {
+ $find = array();
+ // Replace wildcards with MySQL/PostgreSQL wildcards.
+ $keys = preg_replace('!\*+!', '%', $keys);
+ $result = pager_query("SELECT * FROM {users} WHERE LOWER(name) LIKE LOWER('%%%s%%')", 15, 0, NULL, $keys);
+ while ($account = db_fetch_object($result)) {
+ $find[] = array('title' => $account->name, 'link' => url('user/'. $account->uid));
+ }
+ return $find;
+ }
+ }
+}
+
+/**
+ * Implementation of hook_user().
+ */
+function user_user($type, &$edit, &$user, $category = NULL) {
+ if ($type == 'view') {
+ $items[] = array('title' => t('Member for'),
+ 'value' => format_interval(time() - $user->created),
+ 'class' => 'member',
+ );
+
+ return array(t('History') => $items);
+ }
+ if ($type == 'form' && $category == 'account') {
+ return user_edit_form(arg(1), $edit);
+ }
+
+ if ($type == 'validate' && $category == 'account') {
+ return _user_edit_validate(arg(1), $edit);
+ }
+
+ if ($type == 'submit' && $category == 'account') {
+ return _user_edit_submit(arg(1), $edit);
+ }
+
+ if ($type == 'categories') {
+ return array(array('name' => 'account', 'title' => t('account settings'), 'weight' => 1));
+ }
+}
+
+/**
+ * Implementation of hook_block().
+ */
+function user_block($op = 'list', $delta = 0, $edit = array()) {
+ global $user;
+
+ if ($op == 'list') {
+ $blocks[0]['info'] = t('User login');
+ $blocks[1]['info'] = t('Navigation');
+ $blocks[2]['info'] = t('Who\'s new');
+ $blocks[3]['info'] = t('Who\'s online');
+
+ return $blocks;
+ }
+ else if ($op == 'configure' && $delta == 3) {
+ $period = drupal_map_assoc(array(30, 60, 120, 180, 300, 600, 900, 1800, 2700, 3600, 5400, 7200, 10800, 21600, 43200, 86400), 'format_interval');
+ $form['user_block_seconds_online'] = array('#type' => 'select', '#title' => t('User activity'), '#default_value' => variable_get('user_block_seconds_online', 900), '#options' => $period, '#description' => t('A user is considered online for this long after they have last viewed a page.'));
+ $form['user_block_max_list_count'] = array('#type' => 'select', '#title' => t('User list length'), '#default_value' => variable_get('user_block_max_list_count', 10), '#options' => drupal_map_assoc(array(0, 5, 10, 15, 20, 25, 30, 40, 50, 75, 100)), '#description' => t('Maximum number of currently online users to display.'));
+
+ return $form;
+ }
+ else if ($op == 'save' && $delta == 3) {
+ variable_set('user_block_seconds_online', $edit['user_block_seconds_online']);
+ variable_set('user_block_max_list_count', $edit['user_block_max_list_count']);
+ }
+ else if ($op == 'view') {
+ $block = array();
+
+ switch ($delta) {
+ case 0:
+ // For usability's sake, avoid showing two login forms on one page.
+ if (!$user->uid && !(arg(0) == 'user' && !is_numeric(arg(1)))) {
+ // Unset the time, if any. This prevents a 404 if a user relogs in
+ // on the page they were sent to when they logged out. See
+ // user_login_submit() for explanation of the time parameter.
+ unset($_GET['time']);
+ $form['#action'] = url($_GET['q'], drupal_get_destination());
+ $form['#id'] = 'user-login-form';
+ $form['name'] = array('#type' => 'textfield',
+ '#title' => t('Username'),
+ '#maxlength' => 60,
+ '#size' => 15,
+ '#required' => TRUE,
+ );
+ $form['pass'] = array('#type' => 'password',
+ '#title' => t('Password'),
+ '#size' => 15,
+ '#required' => TRUE,
+ );
+ $form['submit'] = array('#type' => 'submit',
+ '#value' => t('Log in'),
+ );
+
+ if (variable_get('user_register', 1)) {
+ $items[] = l(t('Create new account'), 'user/register', array('title' => t('Create a new user account.')));
+ }
+ $items[] = l(t('Request new password'), 'user/password', array('title' => t('Request new password via e-mail.')));
+ $form['links'] = array('#value' => theme('item_list', $items));
+
+ $block['subject'] = t('User login');
+ $block['content'] = drupal_get_form('user_login_block', $form, 'user_login');
+ }
+ return $block;
+
+ case 1:
+ if ($menu = theme('menu_tree')) {
+ $block['subject'] = $user->uid ? $user->name : t('Navigation');
+ $block['content'] = $menu;
+ }
+ return $block;
+
+ case 2:
+ if (user_access('access content')) {
+ // Retrieve a list of new users who have subsequently accessed the site successfully.
+ $result = db_query_range('SELECT uid, name FROM {users} WHERE status != 0 AND access != 0 ORDER BY created DESC', 0, 5);
+ while ($account = db_fetch_object($result)) {
+ $items[] = $account;
+ }
+ $output = theme('user_list', $items);
+
+ $block['subject'] = t('Who\'s new');
+ $block['content'] = $output;
+ }
+ return $block;
+
+ case 3:
+ if (user_access('access content')) {
+ // Count users with activity in the past defined period.
+ $time_period = variable_get('user_block_seconds_online', 900);
+
+ // Perform database queries to gather online user lists.
+ $guests = db_fetch_object(db_query('SELECT COUNT(sid) AS count FROM {sessions} WHERE timestamp >= %d AND uid = 0', time() - $time_period));
+ $users = db_query('SELECT uid, name, access FROM {users} WHERE access >= %d AND uid != 0 ORDER BY access DESC', time() - $time_period);
+ $total_users = db_num_rows($users);
+
+ // Format the output with proper grammar.
+ if ($total_users == 1 && $guests->count == 1) {
+ $output = t('There is currently %members and %visitors online.', array('%members' => format_plural($total_users, '1 user', '%count users'), '%visitors' => format_plural($guests->count, '1 guest', '%count guests')));
+ }
+ else {
+ $output = t('There are currently %members and %visitors online.', array('%members' => format_plural($total_users, '1 user', '%count users'), '%visitors' => format_plural($guests->count, '1 guest', '%count guests')));
+ }
+
+ // Display a list of currently online users.
+ $max_users = variable_get('user_block_max_list_count', 10);
+ if ($total_users && $max_users) {
+ $items = array();
+
+ while ($max_users-- && $account = db_fetch_object($users)) {
+ $items[] = $account;
+ }
+
+ $output .= theme('user_list', $items, t('Online users'));
+ }
+
+ $block['subject'] = t('Who\'s online');
+ $block['content'] = $output;
+ }
+ return $block;
+ }
+ }
+}
+
+function theme_user_picture($account) {
+ if (variable_get('user_pictures', 0)) {
+ if ($account->picture && file_exists($account->picture)) {
+ $picture = file_create_url($account->picture);
+ }
+ else if (variable_get('user_picture_default', '')) {
+ $picture = variable_get('user_picture_default', '');
+ }
+
+ if (isset($picture)) {
+ $alt = t('%user\'s picture', array('%user' => $account->name ? $account->name : variable_get('anonymous', 'Anonymous')));
+ $picture = theme('image', $picture, $alt, $alt, '', false);
+ if (!empty($account->uid) && user_access('access user profiles')) {
+ $picture = l($picture, "user/$account->uid", array('title' => t('View user profile.')), NULL, NULL, FALSE, TRUE);
+ }
+
+ return "$picture";
+ }
+ }
+}
+
+/**
+ * Theme a user page
+ * @param $account the user object
+ * @param $fields a multidimensional array for the fields, in the form of array (
+ * 'category1' => array(item_array1, item_array2), 'category2' => array(item_array3,
+ * .. etc.). Item arrays are formatted as array(array('title' => 'item title',
+ * 'value' => 'item value', 'class' => 'class-name'), ... etc.). Module names are incorporated
+ * into the CSS class.
+ *
+ * @ingroup themeable
+ */
+function theme_user_profile($account, $fields) {
+ $output = '';
+ $output .= theme('user_picture', $account);
+ foreach ($fields as $category => $items) {
+ if (strlen($category) > 0) {
+ $output .= ''. $category .'
';
+ }
+ $output .= '';
+ foreach ($items as $item) {
+ if (isset($item['title'])) {
+ $output .= '- '. $item['title'] .'
';
+ }
+ $output .= '- '. $item['value'] .'
';
+ }
+ $output .= '
';
+ }
+ $output .= '';
+
+ return $output;
+}
+
+/**
+ * Make a list of users.
+ * @param $items an array with user objects. Should contain at least the name and uid
+ *
+ * @ingroup themeable
+ */
+function theme_user_list($users, $title = NULL) {
+ if (!empty($users)) {
+ foreach ($users as $user) {
+ $items[] = theme('username', $user);
+ }
+ }
+ return theme('item_list', $items, $title);
+}
+
+/**
+ * Implementation of hook_menu().
+ */
+function user_menu($may_cache) {
+ global $user;
+
+ $items = array();
+
+ $admin_access = user_access('administer users');
+ $access_access = user_access('administer access control');
+ $view_access = user_access('access user profiles');
+
+ if ($may_cache) {
+ $items[] = array('path' => 'user', 'title' => t('user account'),
+ 'callback' => 'user_login', 'access' => TRUE, 'type' => MENU_CALLBACK);
+
+ $items[] = array('path' => 'user/autocomplete', 'title' => t('user autocomplete'),
+ 'callback' => 'user_autocomplete', 'access' => $view_access, 'type' => MENU_CALLBACK);
+
+ // Registration and login pages.
+ $items[] = array('path' => 'user/login', 'title' => t('log in'),
+ 'callback' => 'user_login', 'type' => MENU_DEFAULT_LOCAL_TASK);
+ $items[] = array('path' => 'user/register', 'title' => t('register'),
+ 'callback' => 'user_register', 'access' => $user->uid == 0 && variable_get('user_register', 1), 'type' => MENU_LOCAL_TASK);
+ $items[] = array('path' => 'user/password', 'title' => t('request new password'),
+ 'callback' => 'user_pass', 'access' => $user->uid == 0, 'type' => MENU_LOCAL_TASK);
+ $items[] = array('path' => 'user/reset', 'title' => t('reset password'),
+ 'callback' => 'user_pass_reset', 'access' => TRUE, 'type' => MENU_CALLBACK);
+ $items[] = array('path' => 'user/help', 'title' => t('help'),
+ 'callback' => 'user_help_page', 'type' => MENU_CALLBACK);
+
+ // Admin user pages
+ $items[] = array('path' => 'admin/user', 'title' => t('users'),
+ 'callback' => 'user_admin', 'access' => $admin_access);
+ $items[] = array('path' => 'admin/user/list', 'title' => t('list'),
+ 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -10);
+ $items[] = array('path' => 'admin/user/create', 'title' => t('add user'),
+ 'callback' => 'user_admin', 'access' => $admin_access,
+ 'type' => MENU_LOCAL_TASK);
+ $items[] = array('path' => 'admin/settings/user', 'title' => t('users'),
+ 'callback' => 'user_configure');
+
+ // Admin access pages
+ $items[] = array('path' => 'admin/access', 'title' => t('access control'),
+ 'callback' => 'user_admin_perm', 'access' => $access_access);
+ $items[] = array('path' => 'admin/access/permissions', 'title' => t('permissions'),
+ 'callback' => 'user_admin_perm', 'access' => $access_access,
+ 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -10);
+ $items[] = array('path' => 'admin/access/roles', 'title' => t('roles'),
+ 'callback' => 'user_admin_role', 'access' => $access_access,
+ 'type' => MENU_LOCAL_TASK);
+ $items[] = array('path' => 'admin/access/roles/edit', 'title' => t('edit role'),
+ 'callback' => 'user_admin_role', 'access' => $access_access,
+ 'type' => MENU_CALLBACK);
+ $items[] = array('path' => 'admin/access/rules', 'title' => t('access rules'),
+ 'callback' => 'user_admin_access', 'access' => $access_access,
+ 'type' => MENU_LOCAL_TASK, 'weight' => 10);
+ $items[] = array('path' => 'admin/access/rules/list', 'title' => t('list'),
+ 'access' => $access_access, 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -10);
+ $items[] = array('path' => 'admin/access/rules/add', 'title' => t('add rule'),
+ 'callback' => 'user_admin_access_add', 'access' => $access_access,
+ 'type' => MENU_LOCAL_TASK);
+ $items[] = array('path' => 'admin/access/rules/check', 'title' => t('check rules'),
+ 'callback' => 'user_admin_access_check', 'access' => $access_access,
+ 'type' => MENU_LOCAL_TASK);
+ $items[] = array('path' => 'admin/access/rules/edit', 'title' => t('edit rule'),
+ 'callback' => 'user_admin_access_edit', 'access' => $access_access,
+ 'type' => MENU_CALLBACK);
+ $items[] = array('path' => 'admin/access/rules/delete', 'title' => t('delete rule'),
+ 'callback' => 'user_admin_access_delete', 'access' => $access_access,
+ 'type' => MENU_CALLBACK);
+
+ if (module_exist('search')) {
+ $items[] = array('path' => 'admin/user/search', 'title' => t('search'),
+ 'callback' => 'user_admin', 'access' => $admin_access,
+ 'type' => MENU_LOCAL_TASK);
+ }
+
+ // Your personal page
+ if ($user->uid) {
+ $items[] = array('path' => 'user/'. $user->uid, 'title' => t('my account'),
+ 'callback' => 'user_view', 'callback arguments' => array(arg(1)), 'access' => TRUE,
+ 'type' => MENU_DYNAMIC_ITEM);
+ }
+
+ $items[] = array('path' => 'logout', 'title' => t('log out'),
+ 'access' => $user->uid != 0,
+ 'callback' => 'user_logout',
+ 'weight' => 10);
+ }
+ else {
+ if (arg(0) == 'user' && is_numeric(arg(1)) && arg(1) > 0) {
+ $account = user_load(array('uid' => arg(1)));
+
+ if ($user !== FALSE) {
+ // Always let a user view their own account
+ $view_access |= $user->uid == arg(1);
+ // Only admins can view blocked accounts
+ $view_access &= $account->status || $admin_access;
+
+ $items[] = array('path' => 'user/'. arg(1), 'title' => t('user'),
+ 'type' => MENU_CALLBACK, 'callback' => 'user_view',
+ 'callback arguments' => array(arg(1)), 'access' => $view_access);
+
+ $items[] = array('path' => 'user/'. arg(1) .'/view', 'title' => t('view'),
+ 'access' => $view_access, 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -10);
+
+ $items[] = array('path' => 'user/'. arg(1) .'/edit', 'title' => t('edit'),
+ 'callback' => 'user_edit', 'access' => $admin_access || $user->uid == arg(1),
+ 'type' => MENU_LOCAL_TASK);
+ $items[] = array('path' => 'user/'. arg(1) .'/delete', 'title' => t('delete'),
+ 'callback' => 'user_edit', 'access' => $admin_access,
+ 'type' => MENU_CALLBACK);
+
+ if (arg(2) == 'edit') {
+ if (($categories = _user_categories($account)) && (count($categories) > 1)) {
+ foreach ($categories as $key => $category) {
+ $items[] = array(
+ 'path' => 'user/'. arg(1) .'/edit/'. $category['name'],
+ 'title' => $category['title'],
+ 'type' => $category['name'] == 'account' ? MENU_DEFAULT_LOCAL_TASK : MENU_LOCAL_TASK,
+ 'weight' => $category['weight'],
+ 'access' => ($admin_access || $user->uid == arg(1)));
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return $items;
+}
+
+/**
+ * Accepts an user object, $account, or a DA name and returns an associative
+ * array of modules and DA names. Called at external login.
+ */
+function user_get_authmaps($authname = NULL) {
+ $result = db_query("SELECT authname, module FROM {authmap} WHERE authname = '%s'", $authname);
+ if (db_num_rows($result) > 0) {
+ while ($authmap = db_fetch_object($result)) {
+ $authmaps[$authmap->module] = $authmap->authname;
+ }
+ return $authmaps;
+ }
+ else {
+ return 0;
+ }
+}
+
+function user_set_authmaps($account, $authmaps) {
+ foreach ($authmaps as $key => $value) {
+ $module = explode('_', $key, 2);
+ if ($value) {
+ db_query("UPDATE {authmap} SET authname = '%s' WHERE uid = %d AND module = '%s'", $value, $account->uid, $module['1']);
+ if (!db_affected_rows()) {
+ db_query("INSERT INTO {authmap} (authname, uid, module) VALUES ('%s', %d, '%s')", $value, $account->uid, $module[1]);
+ }
+ }
+ else {
+ db_query("DELETE FROM {authmap} WHERE uid = %d AND module = '%s'", $account->uid, $module['1']);
+ }
+ }
+}
+
+function user_auth_help_links() {
+ $links = array();
+ foreach (module_list() as $module) {
+ if (module_hook($module, 'auth')) {
+ $links[] = l(module_invoke($module, 'info', 'name'), "user/help#$module");
+ }
+ }
+ return $links;
+}
+
+/*** User features *********************************************************/
+
+
+
+function user_login($msg = '') {
+ global $user, $base_url;
+
+ // If we are already logged on, go to the user
+ // page instead. The added time prevents caching.
+ if ($user->uid) {
+ drupal_goto('user/'. $user->uid, 'time='. time());
+ }
+
+ // Display login form:
+ if ($msg) {
+ $form['message'] = array('#value' => ''. check_plain($msg) .'
');
+ }
+ unset($_GET['time']);
+ $form['#action'] = url($_GET['q'], drupal_get_destination());
+ $form['name'] = array('#type' => 'textfield',
+ '#title' => t('Username'),
+ '#size' => 30,
+ '#maxlength' => 60,
+ '#required' => TRUE,
+ '#attributes' => array('tabindex' => '1'),
+ );
+ if (variable_get('drupal_authentication_service', FALSE) && count(user_auth_help_links()) > 0) {
+ $form['name']['#description'] = t('Enter your %s username, or an ID from one of our affiliates: %a.', array('%s' => variable_get('site_name', 'local'), '%a' => implode(', ', user_auth_help_links())));
+ }
+ else {
+ $form['name']['#description'] = t('Enter your %s username.', array('%s' => variable_get('site_name', 'local')));
+ }
+ $form['pass'] = array('#type' => 'password',
+ '#title' => t('Password'),
+ '#description' => t('Enter the password that accompanies your username.'),
+ '#required' => TRUE,
+ '#attributes' => array('tabindex' => '2'),
+ );
+ $form['submit'] = array('#type' => 'submit', '#value' => t('Log in'), '#weight' => 2, '#attributes' => array('tabindex' => '3'));
+ return drupal_get_form('user_login', $form);
+}
+
+function user_login_validate($form_id, $form_values) {
+ if ($form_values['name']) {
+ if (user_is_blocked($form_values['name'])) {
+ // blocked in user administration
+ form_set_error('login', t('The username %name has not been activated or is blocked.', array('%name' => theme('placeholder', $form_values['name']))));
+ }
+ else if (drupal_is_denied('user', $form_values['name'])) {
+ // denied by access controls
+ form_set_error('login', t('The name %name is a reserved username.', array('%name' => theme('placeholder', $form_values['name']))));
+ }
+ else if ($form_values['pass']) {
+ $user = user_authenticate($form_values['name'], trim($form_values['pass']));
+
+ if (!$user->uid) {
+ form_set_error('login', t('Sorry. Unrecognized username or password.') .' '. l(t('Have you forgotten your password?'), 'user/password'));
+ watchdog('user', t('Login attempt failed for %user.', array('%user' => theme('placeholder', $form_values['name']))));
+ }
+ }
+ }
+}
+
+function user_login_submit($form_id, $form_values) {
+ global $user;
+ if ($user->uid) {
+ // To handle the edge case where this function is called during a
+ // bootstrap, check for the existence of t().
+ if (function_exists('t')) {
+ $message = t('Session opened for %name.', array('%name' => theme('placeholder', $user->name)));
+ }
+ else {
+ $message = "Session opened for ". check_plain($user->name);
+ }
+ watchdog('user', $message);
+
+ // Update the user table timestamp noting user has logged in.
+ db_query("UPDATE {users} SET login = %d WHERE uid = %d", time(), $user->uid);
+
+ user_module_invoke('login', $form_values, $user);
+
+ $old_session_id = session_id();
+ session_regenerate_id();
+ db_query("UPDATE {sessions} SET sid = '%s' WHERE sid = '%s'", session_id(), $old_session_id);
+
+ // Create a timestamped final URL so that browsers don't return the user to
+ // a cached page (where it would appear as if they never logged in or out).
+ return array($_REQUEST['destination'], 'time='. time());
+ }
+}
+
+function user_authenticate($name, $pass) {
+ global $user;
+
+ // Try to log in the user locally. Don't set $user unless successful.
+ if ($account = user_load(array('name' => $name, 'pass' => $pass, 'status' => 1))) {
+ $user = $account;
+ }
+
+ // Strip name and server from ID:
+ if ($server = strrchr($name, '@')) {
+ $name = substr($name, 0, strlen($name) - strlen($server));
+ $server = substr($server, 1);
+ }
+
+ // When possible, determine corresponding external auth source. Invoke
+ // source, and log in user if successful:
+ if (!$user->uid && $server && $result = user_get_authmaps("$name@$server")) {
+ if (module_invoke(key($result), 'auth', $name, $pass, $server)) {
+ $user = user_external_load("$name@$server");
+ watchdog('user', t('External load by %user using module %module.', array('%user' => theme('placeholder', $name .'@'. $server), '%module' => theme('placeholder', key($result)))));
+ }
+ else {
+ $error = t('Invalid password for %s.', array('%s' => theme('placeholder', $name .'@'. $server)));
+ }
+ }
+
+ // Try each external authentication source in series. Register user if
+ // successful.
+ else if (!$user->uid && $server) {
+ foreach (module_list() as $module) {
+ if (module_hook($module, 'auth')) {
+ if (module_invoke($module, 'auth', $name, $pass, $server)) {
+ if (variable_get('user_register', 1) == 1) {
+ $account = user_load(array('name' => "$name@$server"));
+ if (!$account->uid) { // Register this new user.
+ $user = user_save('', array('name' => "$name@$server", 'pass' => user_password(), 'init' => "$name@$server", 'status' => 1, "authname_$module" => "$name@$server"));
+ watchdog('user', t('New external user: %user using module %module.', array('%user' => theme('placeholder', $name .'@'. $server), '%module' => theme('placeholder', $module))), WATCHDOG_NOTICE, l(t('edit'), 'user/'. $user->uid .'/edit'));
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+ return $user;
+}
+
+/**
+ * Menu callback; logs the current user out, and redirects to the home page.
+ */
+function user_logout() {
+ global $user;
+
+ watchdog('user', t('Session closed for %name.', array('%name' => theme('placeholder', $user->name))));
+
+ // Destroy the current session:
+ session_destroy();
+ module_invoke_all('user', 'logout', NULL, $user);
+
+ // We have to use $GLOBALS to unset a global variable:
+ $user = user_load(array('uid' => 0));
+
+ // The time prevents caching.
+ drupal_goto(NULL, 'time='. time());
+}
+
+function user_pass() {
+
+ // Display form:
+ $form['name'] = array('#type' => 'textfield',
+ '#title' => t('Username'),
+ '#size' => 30,
+ '#maxlength' => 60,
+ );
+ $form['mail'] = array('#type' => 'textfield',
+ '#title' => t('E-mail address'),
+ '#size' => 30,
+ '#maxlength' => 64,
+ );
+ $form['submit'] = array('#type' => 'submit',
+ '#value' => t('E-mail new password'),
+ '#weight' => 2,
+ );
+ return drupal_get_form('user_pass', $form);
+}
+
+function user_pass_validate() {
+ global $form_values;
+
+ $name = $form_values['name'];
+ $mail = $form_values['mail'];
+ if ($name && !($form_values['account'] = user_load(array('name' => $name, 'status' => 1)))) {
+ form_set_error('name', t('Sorry. The username %name is not recognized.', array('%name' => theme('placeholder', $name))));
+ }
+ else if ($mail && !($form_values['account'] = user_load(array('mail' => $mail, 'status' => 1)))) {
+ form_set_error('mail', t('Sorry. The e-mail address %email is not recognized.', array('%email' => theme('placeholder', $mail))));
+ }
+ else if (!$mail && !$name) {
+ form_set_error('password', t('You must provide either a username or e-mail address.'));
+ }
+}
+
+function user_pass_submit($form_id, $form_values) {
+ global $base_url;
+
+ $account = $form_values['account'];
+ $from = variable_get('site_mail', ini_get('sendmail_from'));
+
+ // Mail one time login URL and instructions.
+ $variables = array('%username' => $account->name, '%site' => variable_get('site_name', 'drupal'), '%login_url' => user_pass_reset_url($account), '%uri' => $base_url, '%uri_brief' => substr($base_url, strlen('http://')), '%mailto' => $account->mail, '%date' => format_date(time()), '%login_uri' => url('user', NULL, NULL, TRUE), '%edit_uri' => url('user/'. $account->uid .'/edit', NULL, NULL, TRUE));
+ $subject = _user_mail_text('pass_subject', $variables);
+ $body = _user_mail_text('pass_body', $variables);
+ $headers = "From: $from\nReply-to: $from\nX-Mailer: Drupal\nReturn-path: $from\nErrors-to: $from";
+ $mail_success = user_mail($account->mail, $subject, $body, $headers);
+
+ if ($mail_success) {
+ watchdog('user', t('Password reset instructions mailed to %name at %email.', array('%name' => ''. $account->name .'', '%email' => ''. $account->mail .'')));
+ drupal_set_message(t('Further instructions have been sent to your e-mail address.'));
+ }
+ else {
+ watchdog('user', t('Error mailing password reset instructions to %name at %email.', array('%name' => theme('placeholder', $account->name), '%email' => theme('placeholder', $account->mail))), WATCHDOG_ERROR);
+ drupal_set_message(t('Unable to send mail. Please contact the site admin.'));
+ }
+ return 'user';
+}
+
+function theme_user_pass($form) {
+ $output = ''. t('Enter your username or your e-mail address.') .'
';
+ $output .= form_render($form);
+ return $output;
+}
+
+/**
+ * Menu callback; process one time login link and redirects to the user page on success.
+ */
+function user_pass_reset($uid, $timestamp, $hashed_pass, $action = NULL) {
+ global $user;
+
+ // Check if the user is already logged in. The back button is often the culprit here.
+ if ($user->uid) {
+ drupal_set_message(t('You have already used this one-time login link. It is not necessary to use this link to login anymore. You are already logged in.'));
+ drupal_goto();
+ }
+ else {
+ // Time out, in seconds, until login URL expires. 24 hours = 86400 seconds.
+ $timeout = 86400;
+ $current = time();
+ // Some redundant checks for extra security ?
+ if ($timestamp < $current && $account = user_load(array('uid' => $uid, 'status' => 1)) ) {
+ // No time out for first time login.
+ if ($account->login && $current - $timestamp > $timeout) {
+ drupal_set_message(t('You have tried to use a one-time login link that has expired. Please request a new one using the form below.'));
+ drupal_goto('user/password');
+ }
+ else if ($account->uid && $timestamp > $account->login && $timestamp < $current && $hashed_pass == user_pass_rehash($account->pass, $timestamp, $account->login)) {
+ // First stage is a confirmation form, then login
+ if ($action == 'login') {
+ watchdog('user', t('User %name used one-time login link at time %timestamp.', array('%name' => "$account->name", '%timestamp' => $timestamp)));
+ // Update the user table noting user has logged in.
+ // And this also makes this hashed password a one-time-only login.
+ db_query("UPDATE {users} SET login = %d WHERE uid = %d", time(), $account->uid);
+ // Now we can set the new user.
+ $user = $account;
+ // And proceed with normal login, going to user page.
+ user_module_invoke('login', $edit, $user);
+ drupal_set_message(t('You have just used your one-time login link. It is no longer necessary to use this link to login. Please change your password.'));
+ drupal_goto('user/'. $user->uid .'/edit');
+ }
+ else {
+ $form['message'] = array('#value' => t('This is a one-time login for %user_name and will expire on %expiration_date
Click on this button to login to the site and change your password.
', array('%user_name' => theme('placeholder',$account->name), '%expiration_date' => format_date($timestamp + $timeout))));
+ $form['help'] = array('#value' => t('This login can be used only once.
'));
+ $form['submit'] = array('#type' => 'submit', '#value' => t('Log in'));
+ $form['#action'] = url("user/reset/$uid/$timestamp/$hashed_pass/login");
+ return drupal_get_form('user_pass_reset', $form);
+ }
+ }
+ else {
+ drupal_set_message(t('You have tried to use a one-time login link which has either been used or is no longer valid. Please request a new one using the form below.'));
+ drupal_goto('user/password');
+ }
+ }
+ else {
+ // Deny access, no more clues.
+ // Everything will be in the watchdog's URL for the administrator to check.
+ drupal_access_denied();
+ }
+ }
+}
+
+function user_pass_reset_url($account) {
+ $timestamp = time();
+ return url("user/reset/$account->uid/$timestamp/".user_pass_rehash($account->pass, $timestamp, $account->login), NULL, NULL, TRUE);
+}
+
+function user_pass_rehash($password, $timestamp, $login) {
+ return md5($timestamp . $password . $login);
+}
+
+function user_register() {
+ global $user;
+
+ $admin = user_access('administer users');
+
+ // If we aren't admin but already logged on, go to the
+ // user page instead. The added time prevents caching.
+ if (!$admin && $user->uid) {
+ drupal_goto('user/'. $user->uid, 'time='. time());
+ }
+
+ // Display the registration form.
+ if (!$admin) {
+ $form['user_registration_help'] = array('#type' => 'markup', '#value' => filter_xss_admin(variable_get('user_registration_help', '')));
+ }
+ $affiliates = user_auth_help_links();
+ if (!$admin && count($affiliates) > 0) {
+ $affiliates = implode(', ', $affiliates);
+ $form['affiliates'] = array('#type' => 'markup', '#value' => ''. t('Note: if you have an account with one of our affiliates (%s), you may login now instead of registering.', array('%s' => $affiliates, '%login_uri' => url('user'))) .'
');
+ }
+ $form['name'] = array('#type' => 'textfield',
+ '#title' => t('Username'),
+ '#size' => 30,
+ '#maxlength' => 60,
+ '#description' => t('Your full name or your preferred username; only letters, numbers and spaces are allowed.'),
+ '#required' => TRUE);
+ $form['mail'] = array('#type' => 'textfield',
+ '#title' => t('E-mail address'),
+ '#size' => 30,
+ '#maxlength' => 64,
+ '#description' => t('A password and instructions will be sent to this e-mail address, so make sure it is accurate.'),
+ '#required' => TRUE,
+ );
+ if ($admin) {
+ $form['pass'] = array('#type' => 'password',
+ '#title' => t('Password'),
+ '#size' => 30,
+ '#description' => t('Provide a password for the new account.'),
+ '#required' => TRUE,
+ );
+ $form['notify'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Notify user of new account')
+ );
+ }
+ $extra = _user_forms($null, $null, $null, 'register');
+
+ // Only display form_group around default fields if there are other groups.
+ if ($extra) {
+ $form['account'] = array('#type' => 'fieldset', '#title' => t('Account information'));
+ $form['account']['name'] = $form['name'];
+ $form['account']['mail'] = $form['mail'];
+ $form['account']['pass'] = $form['pass'];
+ $form['account']['notify'] = $form['notify'];
+ unset($form['name'], $form['mail'], $form['pass'], $form['notify']);
+ $form = array_merge($form, $extra);
+ }
+ $form['submit'] = array('#type' => 'submit', '#value' => t('Create new account'), '#weight' => 30);
+
+ return drupal_get_form('user_register', $form);
+}
+
+function user_register_validate($form_id, $form_values) {
+ user_module_invoke('validate', $form_values, $form_values, 'account');
+}
+
+function user_register_submit($form_id, $form_values) {
+ global $base_url;
+
+ $admin = user_access('administer users');
+
+ $mail = $form_values['mail'];
+ $name = $form_values['name'];
+ $pass = $admin ? $form_values['pass'] : user_password();
+ $notify = $form_values['notify'];
+ $from = variable_get('site_mail', ini_get('sendmail_from'));
+
+ if (!$admin && array_intersect(array_keys($form_values), array('uid', 'roles', 'init', 'session', 'status'))) {
+ watchdog('security', t('Detected malicious attempt to alter protected user fields.'), WATCHDOG_WARNING);
+ return 'user/register';
+ }
+
+ $account = user_save('', array_merge($form_values, array('pass' => $pass, 'init' => $mail, 'status' => ($admin || variable_get('user_register', 1) == 1))));
+ watchdog('user', t('New user: %name %email.', array('%name' => theme('placeholder', $name), '%email' => theme('placeholder', '<'. $mail .'>'))), WATCHDOG_NOTICE, l(t('edit'), 'user/'. $account->uid .'/edit'));
+
+ $variables = array('%username' => $name, '%site' => variable_get('site_name', 'drupal'), '%password' => $pass, '%uri' => $base_url, '%uri_brief' => substr($base_url, strlen('http://')), '%mailto' => $mail, '%date' => format_date(time()), '%login_uri' => url('user', NULL, NULL, TRUE), '%edit_uri' => url('user/'. $account->uid .'/edit', NULL, NULL, TRUE), '%login_url' => user_pass_reset_url($account));
+
+ // The first user may login immediately, and receives a customized welcome e-mail.
+ if ($account->uid == 1) {
+ user_mail($mail, t('Drupal user account details for %s', array('%s' => $name)), strtr(t("%username,\n\nYou may now login to %uri using the following username and password:\n\n username: %username\n password: %password\n\n%edit_uri\n\n--drupal"), $variables), "From: $from\nReply-to: $from\nX-Mailer: Drupal\nReturn-path: $from\nErrors-to: $from");
+ drupal_set_message(t('Welcome to Drupal. You are user #1, which gives you full and immediate access. All future registrants will receive their passwords via e-mail, so please make sure your website e-mail address is set properly under the general settings on the settings page.
Your password is %pass. You may change your password below.
', array('%pass' => $pass, '%settings' => url('admin/settings'))));
+ user_authenticate($account->name, trim($pass));
+
+ // Set the installed schema version of the system module to the most recent version.
+ include_once './includes/install.inc';
+ drupal_set_installed_schema_version('system', max(drupal_get_schema_versions('system')));
+
+ return 'user/1/edit';
+ }
+ else {
+ if ($admin && !$notify) {
+ drupal_set_message(t('Created a new user account. No e-mail has been sent.'));
+
+ return 'admin/user';
+ }
+ else if ($account->status || $notify) {
+ // Create new user account, no administrator approval required.
+ $subject = $notify ? _user_mail_text('admin_subject', $variables) : _user_mail_text('welcome_subject', $variables);
+ $body = $notify ? _user_mail_text('admin_body', $variables) : _user_mail_text('welcome_body', $variables);
+
+ user_mail($mail, $subject, $body, "From: $from\nReply-to: $from\nX-Mailer: Drupal\nReturn-path: $from\nErrors-to: $from");
+
+ if ($notify) {
+ drupal_set_message(t('Password and further instructions have been e-mailed to the new user %user.', array('%user' => theme('placeholder', $name))));
+ return 'admin/user';
+ }
+ else {
+ drupal_set_message(t('Your password and further instructions have been sent to your e-mail address.'));
+ return '';
+ }
+ }
+ else {
+ // Create new user account, administrator approval required.
+ $subject = _user_mail_text('approval_subject', $variables);
+ $body = _user_mail_text('approval_body', $variables);
+
+ user_mail($mail, $subject, $body, "From: $from\nReply-to: $from\nX-Mailer: Drupal\nReturn-path: $from\nErrors-to: $from");
+ user_mail(variable_get('site_mail', ini_get('sendmail_from')), $subject, t("%u has applied for an account.\n\n%uri", array('%u' => $account->name, '%uri' => url("user/$account->uid/edit", NULL, NULL, TRUE))), "From: $from\nReply-to: $from\nX-Mailer: Drupal\nReturn-path: $from\nErrors-to: $from");
+ drupal_set_message(t('Thank you for applying for an account. Your account is currently pending approval by the site administrator.
In the meantime, your password and further instructions have been sent to your e-mail address.'));
+
+ }
+ }
+}
+
+function user_edit_form($uid, $edit) {
+ // Account information:
+ $form['account'] = array('#type' => 'fieldset',
+ '#title' => t('Account information'),
+ );
+ if (user_access('change own username') || user_access('administer users')) {
+ $form['account']['name'] = array('#type' => 'textfield',
+ '#title' => t('Username'),
+ '#default_value' => $edit['name'],
+ '#maxlength' => 60,
+ '#description' => t('Your full name or your preferred username: only letters, numbers and spaces are allowed.'),
+ '#required' => TRUE,
+ );
+ }
+ $form['account']['mail'] = array('#type' => 'textfield',
+ '#title' => t('E-mail address'),
+ '#default_value' => $edit['mail'],
+ '#maxlength' => 64,
+ '#description' => t('Insert a valid e-mail address. All e-mails from the system will be sent to this address. The e-mail address is not made public and will only be used if you wish to receive a new password or wish to receive certain news or notifications by e-mail.'),
+ '#required' => TRUE,
+ );
+ $form['account']['pass'] = array('#type' => 'password_confirm',
+ '#title' => t('Password'),
+ '#description' => t('To change the current user password, enter the new password in both fields.'),
+ );
+ if (user_access('administer users')) {
+ $form['account']['status'] = array('#type' => 'radios', '#title' => t('Status'), '#default_value' => $edit['status'], '#options' => array(t('Blocked'), t('Active')));
+ }
+ if (user_access('administer access control')) {
+ $roles = user_roles(1);
+ unset($roles[DRUPAL_AUTHENTICATED_RID]);
+ if ($roles) {
+ $form['account']['roles'] = array('#type' => 'checkboxes', '#title' => t('Roles'), '#default_value' => array_keys((array)$edit['roles']), '#options' => $roles, '#description' => t('The user receives the combined permissions of the %au role, and all roles selected here.', array('%au' => theme('placeholder', t('authenticated user')))));
+ }
+ }
+
+ // Picture/avatar:
+ if (variable_get('user_pictures', 0)) {
+ $form['picture'] = array('#type' => 'fieldset', '#title' => t('Picture'), '#weight' => 1);
+ $picture = theme('user_picture', (object)$edit);
+ if ($picture) {
+ $form['picture']['current_picture'] = array('#type' => 'markup', '#value' => $picture);
+ $form['picture']['picture_delete'] = array('#type' => 'checkbox', '#title' => t('Delete picture'), '#description' => t('Check this box to delete your current picture.'));
+ } else {
+ $form['picture']['picture_delete'] = array('#type' => 'hidden');
+ }
+ $form['picture']['picture_upload'] = array('#type' => 'file', '#title' => t('Upload picture'), '#size' => 48, '#description' => t('Your virtual face or picture. Maximum dimensions are %dimensions and the maximum size is %size kB.', array('%dimensions' => variable_get('user_picture_dimensions', '85x85'), '%size' => variable_get('user_picture_file_size', '30'))) .' '. variable_get('user_picture_guidelines', ''));
+ }
+
+ return $form;
+}
+
+function _user_edit_validate($uid, &$edit) {
+ $user = user_load(array('uid' => $uid));
+ // Validate the username:
+ if (user_access('change own username') || user_access('administer users') || arg(1) == 'register') {
+ if ($error = user_validate_name($edit['name'])) {
+ form_set_error('name', $error);
+ }
+ else if (db_num_rows(db_query("SELECT uid FROM {users} WHERE uid != %d AND LOWER(name) = LOWER('%s')", $uid, $edit['name'])) > 0) {
+ form_set_error('name', t('The name %name is already taken.', array('%name' => theme('placeholder', $edit['name']))));
+ }
+ else if (drupal_is_denied('user', $edit['name'])) {
+ form_set_error('name', t('The name %name has been denied access.', array('%name' => theme('placeholder', $edit['name']))));
+ }
+ }
+
+ // Validate the e-mail address:
+ if ($error = user_validate_mail($edit['mail'])) {
+ form_set_error('mail', $error);
+ }
+ else if (db_num_rows(db_query("SELECT uid FROM {users} WHERE uid != %d AND LOWER(mail) = LOWER('%s')", $uid, $edit['mail'])) > 0) {
+ form_set_error('mail', t('The e-mail address %email is already taken.', array('%email' => theme('placeholder', $edit['mail']))));
+ }
+ else if (drupal_is_denied('mail', $edit['mail'])) {
+ form_set_error('mail', t('The e-mail address %email has been denied access.', array('%email' => theme('placeholder', $edit['mail']))));
+ }
+
+ // If required, validate the uploaded picture.
+ if ($file = file_check_upload('picture_upload')) {
+ user_validate_picture($file, $edit, $user);
+ }
+}
+
+function _user_edit_submit($uid, &$edit) {
+ $user = user_load(array('uid' => $uid));
+ // Delete picture if requested, and if no replacement picture was given.
+ if ($edit['picture_delete']) {
+ if ($user->picture && file_exists($user->picture)) {
+ file_delete($user->picture);
+ }
+ $edit['picture'] = '';
+ }
+ if (isset($edit['roles'])) {
+ $edit['roles'] = array_filter($edit['roles']);
+ }
+}
+
+function user_edit($category = 'account') {
+ global $user;
+
+ $account = user_load(array('uid' => arg(1)));
+ if ($account === FALSE) {
+ drupal_set_message(t('The account does not exist or has already been deleted.'));
+ drupal_goto('admin/user');
+ }
+ $edit = $_POST['op'] ? $_POST['edit'] : (array)$account;
+
+ if (arg(2) == 'delete') {
+ if ($edit['confirm']) {
+ db_query('DELETE FROM {users} WHERE uid = %d', $account->uid);
+ db_query('DELETE FROM {sessions} WHERE uid = %d', $account->uid);
+ db_query('DELETE FROM {users_roles} WHERE uid = %d', $account->uid);
+ db_query('DELETE FROM {authmap} WHERE uid = %d', $account->uid);
+ watchdog('user', t('Deleted user: %name %email.', array('%name' => theme('placeholder', $account->name), '%email' => theme('placeholder', '<'. $account->mail .'>'))), WATCHDOG_NOTICE);
+ drupal_set_message(t('The account has been deleted.'));
+ module_invoke_all('user', 'delete', $edit, $account);
+ drupal_goto('admin/user');
+ }
+ else {
+ return confirm_form('user_confirm_delete', array(), t('Are you sure you want to delete the account %name?', array('%name' => theme('placeholder', $account->name))), 'user/'. $account->uid, t('All submissions made by this user will be attributed to the anonymous account. This action cannot be undone.'), t('Delete'), t('Cancel'));
+ }
+ }
+ else if ($_POST['op'] == t('Delete')) {
+ if ($_REQUEST['destination']) {
+ $destination = drupal_get_destination();
+ unset($_REQUEST['destination']);
+ }
+ // Note: we redirect from user/uid/edit to user/uid/delete to make the tabs disappear.
+ drupal_goto("user/$account->uid/delete", $destination);
+ }
+
+ $form = _user_forms($edit, $account, $category);
+ $form['_category'] = array('#type' => 'value', '#value' => $category);
+ $form['_account'] = array('#type' => 'value', '#value' => $account);
+ $form['submit'] = array('#type' => 'submit', '#value' => t('Submit'), '#weight' => 30);
+ if (user_access('administer users')) {
+ $form['delete'] = array('#type' => 'submit', '#value' => t('Delete'), '#weight' => 31);
+ }
+ $form['#attributes']['enctype'] = 'multipart/form-data';
+
+ drupal_set_title($account->name);
+ return drupal_get_form('user_edit', $form);
+}
+
+function user_edit_validate($form_id, $form_values) {
+ user_module_invoke('validate', $form_values, $form_values['_account'], $form_values['_category']);
+ // Validate input to ensure that non-privileged users can't alter protected data.
+ if ((!user_access('administer users') && array_intersect(array_keys($form_values), array('uid', 'init', 'session'))) || (!user_access('administer access control') && isset($form_values['roles']))) {
+ $message = t('Detected malicious attempt to alter protected user fields.');
+ watchdog('security', $message, WATCHDOG_WARNING);
+ // set this to a value type field
+ form_set_error('category', $message);
+ }
+}
+
+function user_edit_submit($form_id, $form_values) {
+ $account = $form_values['_account'];
+ $category = $form_values['_category'];
+ unset($form_values['_account'], $form_values['submit'], $form_values['delete'], $form_values['form_id'], $form_values['_category']);
+ user_module_invoke('submit', $form_values, $account, $category);
+ user_save($account, $form_values, $category);
+ // Delete that user's menu cache.
+ cache_clear_all('menu:'. $account->uid, TRUE);
+ drupal_set_message(t('The changes have been saved.'));
+ return 'user/'. $account->uid;
+}
+
+function user_view($uid = 0) {
+ global $user;
+
+ $account = user_load(array('uid' => $uid));
+ if ($account === FALSE || ($account->access == 0 && !user_access('administer users'))) {
+ return drupal_not_found();
+ }
+ // Retrieve and merge all profile fields:
+ $fields = array();
+ foreach (module_list() as $module) {
+ if ($data = module_invoke($module, 'user', 'view', '', $account)) {
+ foreach ($data as $category => $items) {
+ foreach ($items as $item) {
+ $item['class'] = "$module-". $item['class'];
+ $fields[$category][] = $item;
+ }
+ }
+ }
+ }
+ drupal_set_title($account->name);
+ return theme('user_profile', $account, $fields);
+}
+
+/*** Administrative features ***********************************************/
+
+function _user_mail_text($messageid, $variables = array()) {
+
+ // Check if an admin setting overrides the default string.
+ if ($admin_setting = variable_get('user_mail_' . $messageid, FALSE)) {
+ return strtr($admin_setting, $variables);
+ }
+ // No override, return with default strings.
+ else {
+ switch ($messageid) {
+ case 'welcome_subject':
+ return t('Account details for %username at %site', $variables);
+ case 'welcome_body':
+ return t("%username,\n\nThank you for registering at %site. You may now log in to %login_uri using the following username and password:\n\nusername: %username\npassword: %password\n\nYou may also log in by clicking on this link or copying and pasting it in your browser:\n\n%login_url\n\nThis is a one-time login, so it can be used only once.\n\nAfter logging in, you will be redirected to %edit_uri so you can change your password.\n\nYour new %site membership also enables to you to login to other Drupal powered websites (e.g. http://drupal.org/) without registering. Just use the following Drupal ID and password:\n\nDrupal ID: %username@%uri_brief\npassword: %password\n\n\n-- %site team", $variables);
+ case 'admin_subject':
+ return t('An administrator created an account for you at %site', $variables);
+ case 'admin_body':
+ return t("%username,\n\nA site administrator at %site has created an account for you. You may now log in to %login_uri using the following username and password:\n\nusername: %username\npassword: %password\n\nYou may also log in by clicking on this link or copying and pasting it in your browser:\n\n%login_url\n\nThis is a one-time login, so it can be used only once.\n\nAfter logging in, you will be redirected to %edit_uri so you can change your password.\n\nYour new %site membership also enables to you to login to other Drupal powered websites (e.g. http://www.drupal.org/) without registering. Just use the following Drupal ID and password:\n\nDrupal ID: %username@%uri_brief\npassword: %password\n\n\n-- %site team", $variables);
+ case 'approval_subject':
+ return t('Account details for %username at %site (pending admin approval)', $variables);
+ case 'approval_body':
+ return t("%username,\n\nThank you for registering at %site. Your application for an account is currently pending approval. Once it has been granted, you may log in to %login_uri using the following username and password:\n\nusername: %username\npassword: %password\n\nYou may also log in by clicking on this link or copying and pasting it in your browser:\n\n%login_url\n\nThis is a one-time login, so it can be used only once.\n\nAfter logging in, you may wish to change your password at %edit_uri\n\nYour new %site membership also enables to you to login to other Drupal powered websites (e.g. http://www.drop.org/) without registering. Just use the following Drupal ID and password:\n\nDrupal ID: %username@%uri_brief\npassword: %password\n\n\n-- %site team", $variables);
+ case 'pass_subject':
+ return t('Replacement login information for %username at %site', $variables);
+ case 'pass_body':
+ return t("%username,\n\nA request to reset the password for your account has been made at %site.\n\nYou may now log in to %uri_brief clicking on this link or copying and pasting it in your browser:\n\n%login_url\n\nThis is a one-time login, so it can be used only once. It expires after one day and nothing will happen if it's not used.\n\nAfter logging in, you will be redirected to %edit_uri so you can change your password.", $variables);
+ }
+ }
+}
+
+function user_configure_settings() {
+}
+
+/**
+ * Menu callback: check an access rule
+ */
+function user_admin_access_check() {
+ $form['user'] = array('#type' => 'fieldset', '#title' => t('Username'));
+ $form['user']['test'] = array('#type' => 'textfield', '#title' => '', '#description' => t('Enter a username to check if it will be denied or allowed.'), '#size' => 30, '#maxlength' => 64);
+ $form['user']['type'] = array('#type' => 'hidden', '#value' => 'user');
+ $form['user']['submit'] = array('#type' => 'submit', '#value' => t('Check username'));
+ $output .= drupal_get_form('check_user', $form, 'user_admin_access_check');
+ unset($form); // prevent endless loop?
+
+ $form['mail'] = array('#type' => 'fieldset', '#title' => t('E-mail'));
+ $form['mail']['test'] = array('#type' => 'textfield', '#title' => '', '#description' => t('Enter an e-mail address to check if it will be denied or allowed.'), '#size' => 30, '#maxlength' => 64);
+ $form['mail']['type'] = array('#type' => 'hidden', '#value' => 'mail');
+ $form['mail']['submit'] = array('#type' => 'submit', '#value' => t('Check e-mail'));
+ $output .= drupal_get_form('check_mail', $form, 'user_admin_access_check');
+ unset($form); // prevent endless loop?
+
+ $form['host'] = array('#type' => 'fieldset', '#title' => t('Hostname'));
+ $form['host']['test'] = array('#type' => 'textfield', '#title' => '', '#description' => t('Enter a hostname or IP address to check if it will be denied or allowed.'), '#size' => 30, '#maxlength' => 64);
+ $form['host']['type'] = array('#type' => 'hidden', '#value' => 'host');
+ $form['host']['submit'] = array('#type' => 'submit', '#value' => t('Check hostname'));
+ $output .= drupal_get_form('check_host', $form, 'user_admin_access_check');
+ unset($form); // prevent endless loop?
+
+ return $output;
+}
+
+function user_admin_access_check_validate($form_id, $edit) {
+ if (empty($edit['test'])) {
+ form_set_error($edit['type'], t('No value entered. Please enter a test string and try again.'));
+ }
+}
+
+function user_admin_access_check_submit($form_id, $edit) {
+ switch ($edit['type']) {
+ case 'user':
+ if (drupal_is_denied('user', $edit['test'])) {
+ drupal_set_message(t('The username %name is not allowed.', array('%name' => theme('placeholder', $edit['test']))));
+ }
+ else {
+ drupal_set_message(t('The username %name is allowed.', array('%name' => theme('placeholder', $edit['test']))));
+ }
+ break;
+ case 'mail':
+ if (drupal_is_denied('mail', $edit['test'])) {
+ drupal_set_message(t('The e-mail address %mail is not allowed.', array('%mail' => theme('placeholder', $edit['test']))));
+ }
+ else {
+ drupal_set_message(t('The e-mail address %mail is allowed.', array('%mail' => theme('placeholder', $edit['test']))));
+ }
+ break;
+ case 'host':
+ if (drupal_is_denied('host', $edit['test'])) {
+ drupal_set_message(t('The hostname %host is not allowed.', array('%host' => theme('placeholder', $edit['test']))));
+ }
+ else {
+ drupal_set_message(t('The hostname %host is allowed.', array('%host' => theme('placeholder', $edit['test']))));
+ }
+ break;
+ default:
+ break;
+ }
+}
+
+/**
+ * Menu callback: add an access rule
+ */
+function user_admin_access_add($mask = NULL, $type = NULL) {
+ if ($edit = $_POST['edit']) {
+ if (!$edit['mask']) {
+ form_set_error('mask', t('You must enter a mask.'));
+ }
+ else {
+ $aid = db_next_id('{access}_aid');
+ db_query("INSERT INTO {access} (aid, mask, type, status) VALUES ('%s', '%s', '%s', %d)", $aid, $edit['mask'], $edit['type'], $edit['status']);
+ drupal_set_message(t('The access rule has been added.'));
+ drupal_goto('admin/access/rules');
+ }
+ }
+ else {
+ $edit['mask'] = $mask;
+ $edit['type'] = $type;
+ }
+
+ $form = _user_admin_access_form($edit);
+ $form['submit'] = array('#type' => 'submit', '#value' => t('Add rule'));
+
+ return drupal_get_form('access_rule', $form);
+}
+
+/**
+ * Menu callback: delete an access rule
+ */
+function user_admin_access_delete($aid = 0) {
+ $access_types = array('user' => t('username'), 'mail' => t('e-mail'));
+ $edit = db_fetch_object(db_query('SELECT aid, type, status, mask FROM {access} WHERE aid = %d', $aid));
+
+ $form = array();
+ $form['aid'] = array('#type' => 'hidden', '#value' => $aid);
+ $output = confirm_form('user_admin_access_delete_confirm', $form,
+ t('Are you sure you want to delete the %type rule for %rule?', array('%type' => $access_types[$edit->type], '%rule' => theme('placeholder', $edit->mask))),
+ 'admin/access/rules',
+ t('This action cannot be undone.'),
+ t('Delete'),
+ t('Cancel'));
+ return $output;
+}
+
+function user_admin_access_delete_confirm_submit($form_id, $edit) {
+ db_query('DELETE FROM {access} WHERE aid = %d', $edit['aid']);
+ drupal_set_message(t('The access rule has been deleted.'));
+ return 'admin/access/rules';
+}
+
+/**
+ * Menu callback: edit an access rule
+ */
+function user_admin_access_edit($aid = 0) {
+ if ($edit = $_POST['edit']) {
+ if (!$edit['mask']) {
+ form_set_error('mask', t('You must enter a mask.'));
+ }
+ else {
+ db_query("UPDATE {access} SET mask = '%s', type = '%s', status = '%s' WHERE aid = %d", $edit['mask'], $edit['type'], $edit['status'], $aid);
+ drupal_set_message(t('The access rule has been saved.'));
+ drupal_goto('admin/access/rules');
+ }
+ }
+ else {
+ $edit = db_fetch_array(db_query('SELECT aid, type, status, mask FROM {access} WHERE aid = %d', $aid));
+ }
+ $form = _user_admin_access_form($edit);
+ $form['submit'] = array('#type' => 'submit', '#value' => t('Save rule'));
+
+ return drupal_get_form('access_rule', $form);
+}
+
+function _user_admin_access_form($edit) {
+ $form['status'] = array(
+ '#type' => 'radios',
+ '#title' => t('Access type'),
+ '#default_value' => 0,
+ '#options' => array('1' => t('Allow'), '0' => t('Deny')),
+ );
+ $type_options = array('user' => t('Username'), 'mail' => t('E-mail'), 'host' => t('Host'));
+ $form['type'] = array(
+ '#type' => 'radios',
+ '#title' => t('Rule type'),
+ '#default_value' => (isset($type_options[$edit['type']]) ? $edit['type'] : 'user'),
+ '#options' => $type_options,
+ );
+ $form['mask'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Mask'),
+ '#size' => 30,
+ '#maxlength' => 64,
+ '#default_value' => $edit['mask'],
+ '#description' => '%: '. t('Matches any number of characters, even zero characters') .'.
_: '. t('Matches exactly one character.'),
+ '#required' => TRUE,
+ );
+ return $form;
+}
+
+/**
+ * Menu callback: list all access rules
+ */
+function user_admin_access() {
+ $header = array(array('data' => t('Access type'), 'field' => 'status'), array('data' => t('Rule type'), 'field' => 'type'), array('data' =>t('Mask'), 'field' => 'mask'), array('data' => t('Operations'), 'colspan' => 2));
+ $result = db_query("SELECT aid, type, status, mask FROM {access}". tablesort_sql($header));
+ $access_types = array('user' => t('username'), 'mail' => t('e-mail'), 'host' => t('host'));
+ $rows = array();
+ while ($rule = db_fetch_object($result)) {
+ $rows[] = array($rule->status ? t('allow') : t('deny'), $access_types[$rule->type], $rule->mask, l(t('edit'), 'admin/access/rules/edit/'. $rule->aid), l(t('delete'), 'admin/access/rules/delete/'. $rule->aid));
+ }
+ if (count($rows) == 0) {
+ $rows[] = array(array('data' => ''. t('There are currently no access rules.') .'', 'colspan' => 5));
+ }
+ $output .= theme('table', $header, $rows);
+
+ return $output;
+}
+
+/**
+ * Retrieve an array of roles matching specified conditions.
+ *
+ * @param $membersonly
+ * Set this to TRUE to exclude the 'anonymous' role.
+ * @param $permission
+ * A string containing a permission. If set, only roles containing that permission are returned.
+ *
+ * @return
+ * An associative array with the role id as the key and the role name as value.
+ */
+function user_roles($membersonly = 0, $permission = 0) {
+ $roles = array();
+
+ if ($permission) {
+ $result = db_query("SELECT r.* FROM {role} r INNER JOIN {permission} p ON r.rid = p.rid WHERE p.perm LIKE '%%%s%%' ORDER BY r.name", $permission);
+ }
+ else {
+ $result = db_query('SELECT * FROM {role} ORDER BY name');
+ }
+ while ($role = db_fetch_object($result)) {
+ if (!$membersonly || ($membersonly && $role->rid != DRUPAL_ANONYMOUS_RID)) {
+ $roles[$role->rid] = $role->name;
+ }
+ }
+ return $roles;
+}
+
+/**
+ * Menu callback: administer permissions.
+ */
+function user_admin_perm($str_rids = NULL) {
+ if (preg_match('/^([0-9]+[+ ])*[0-9]+$/', $str_rids)) {
+ // The '+' character in a query string may be parsed as ' '.
+ $rids = preg_split('/[+ ]/', $str_rids);
+ }
+
+ if($rids) {
+ $breadcrumbs = drupal_get_breadcrumb();
+ $breadcrumbs[] = l(t('all roles'), 'admin/access');
+ drupal_set_breadcrumb($breadcrumbs);
+ $result = db_query('SELECT r.rid, p.perm FROM {role} r LEFT JOIN {permission} p ON r.rid = p.rid WHERE r.rid IN (%s) ORDER BY name', implode(', ', $rids));
+ }
+ else {
+ $result = db_query('SELECT r.rid, p.perm FROM {role} r LEFT JOIN {permission} p ON r.rid = p.rid ORDER BY name');
+ }
+
+ // Compile role array:
+ $roles = array();
+ while ($role = db_fetch_object($result)) {
+ $role_permissions[$role->rid] = $role->perm;
+ }
+
+ if($rids) {
+ $result = db_query('SELECT rid, name FROM {role} r WHERE r.rid IN (%s) ORDER BY name', implode(', ', $rids));
+ }
+ else {
+ $result = db_query('SELECT rid, name FROM {role} ORDER BY name');
+ }
+ $role_names = array();
+ while ($role = db_fetch_object($result)) {
+ $role_names[$role->rid] = $role->name;
+ }
+
+ // Render role/permission overview:
+ $options = array();
+ foreach (module_list(FALSE, FALSE, TRUE) as $module) {
+ if ($permissions = module_invoke($module, 'perm')) {
+ $form['permission'][] = array('#type' => 'markup', '#value' => t('%module module', array('%module' => $module)));
+ asort($permissions);
+ foreach ($permissions as $perm) {
+ $options[$perm] = '';
+ $form['permission'][$perm] = array('#type' => 'markup', '#value' => t($perm));
+ foreach ($role_names as $rid => $name) {
+ // Builds arrays for checked boxes for each role
+ if (strstr($role_permissions[$rid], $perm)) {
+ $status[$rid][] = $perm;
+ }
+ }
+ }
+ }
+ }
+ // Have to build checkboxes here after checkbox arrays are built
+ foreach ($role_names as $rid => $name) {
+ $form['checkboxes'][$rid] = array('#type' => 'checkboxes', '#options' => $options, '#default_value' => $status[$rid]);
+ $form['role_names'][$rid] = array('#type' => 'markup', '#value' => l($name, 'admin/access/'. $rid), '#tree' => TRUE);
+ }
+ $form['submit'] = array('#type' => 'submit', '#value' => t('Save permissions'));
+
+ return drupal_get_form('user_admin_perm', $form);
+}
+
+function theme_user_admin_perm($form) {
+ foreach (element_children($form['permission']) as $key) {
+ // Don't take form control structures
+ if (is_array($form['permission'][$key])) {
+ $row = array();
+ // Module name
+ if (is_numeric($key)) {
+ $row[] = array('data' => form_render($form['permission'][$key]), 'class' => 'module', 'colspan' => count($form['role_names']) + 1);
+ // Permissions
+ }
+ else {
+ $row[] = array('data' => form_render($form['permission'][$key]), 'class' => 'permission');
+ foreach (element_children($form['checkboxes']) as $rid) {
+ if (is_array($form['checkboxes'][$rid])) {
+ $row[] = array('data' => form_render($form['checkboxes'][$rid][$key]), 'align' => 'center', 'title' => t($key));
+ }
+ }
+ }
+ $rows[] = $row;
+ }
+ }
+ $header[] = (t('Permission'));
+ foreach (element_children($form['role_names']) as $rid) {
+ if (is_array($form['role_names'][$rid])) {
+ $header[] = form_render($form['role_names'][$rid]);
+ }
+ }
+ $output = theme('table', $header, $rows, array('id' => 'permissions'));
+ $output .= form_render($form);
+ return $output;
+}
+
+function user_admin_perm_submit($form_id, $edit) {
+ // Save permissions:
+ $result = db_query('SELECT * FROM {role}');
+ while ($role = db_fetch_object($result)) {
+ if(isset($edit[$role->rid])) {
+ // Delete, so if we clear every checkbox we reset that role;
+ // otherwise permissions are active and denied everywhere.
+ db_query('DELETE FROM {permission} WHERE rid = %d', $role->rid);
+ foreach ($edit[$role->rid] as $key => $value) {
+ if (!$value) {
+ unset($edit[$role->rid][$key]);
+ }
+ }
+ if (count($edit[$role->rid])) {
+ db_query("INSERT INTO {permission} (rid, perm) VALUES (%d, '%s')", $role->rid, implode(', ', array_keys($edit[$role->rid])));
+ }
+ }
+ }
+
+ drupal_set_message(t('The changes have been saved.'));
+
+ // Clear the cached pages and menus:
+ menu_rebuild();
+
+}
+
+/**
+ * Menu callback: administer roles.
+ */
+function user_admin_role() {
+ $edit = isset($_POST['edit']) ? $_POST['edit'] : '';
+ $op = isset($_POST['op']) ? $_POST['op'] : '';
+ $id = arg(4);
+
+ if ($op == t('Save role')) {
+ if ($edit['name']) {
+ db_query("UPDATE {role} SET name = '%s' WHERE rid = %d", $edit['name'], $id);
+ drupal_set_message(t('The changes have been saved.'));
+ drupal_goto('admin/access/roles');
+ }
+ else {
+ form_set_error('name', t('You must specify a valid role name.'));
+ }
+ }
+ else if ($op == t('Delete role')) {
+ db_query('DELETE FROM {role} WHERE rid = %d', $id);
+ db_query('DELETE FROM {permission} WHERE rid = %d', $id);
+ // Update the users who have this role set:
+ db_query('DELETE FROM {users_roles} WHERE rid = %d', $id);
+
+ drupal_set_message(t('The role has been deleted.'));
+ drupal_goto('admin/access/roles');
+ }
+ else if ($op == t('Add role')) {
+ if ($edit['name']) {
+ db_query("INSERT INTO {role} (name) VALUES ('%s')", $edit['name']);
+ drupal_set_message(t('The role has been added.'));
+ drupal_goto('admin/access/roles');
+ }
+ else {
+ form_set_error('name', t('You must specify a valid role name.'));
+ }
+ }
+ else if ($id) {
+ // Display the role form.
+ $role = db_fetch_object(db_query('SELECT * FROM {role} WHERE rid = %d', $id));
+ $form['name'] = array('#type' => 'textfield', '#title' => t('Role name'), '#default_value' => $role->name, '#size' => 30, '#maxlength' => 64, '#description' => t('The name for this role. Example: "moderator", "editorial board", "site architect".'));
+ $form['submit'] = array('#type' => 'submit', '#value' => t('Save role'));
+ $form['delete'] = array('#type' => 'submit', '#value' => t('Delete role'));
+ return drupal_get_form('user_admin_role', $form);
+ }
+ $form['name'] = array('#type' => 'textfield', '#size' => 32, '#maxlength' => 64);
+ $form['submit'] = array('#type' => 'submit', '#value' => t('Add role'));
+ return drupal_get_form('user_admin_new_role', $form);
+}
+
+function theme_user_admin_new_role($form) {
+ $header = array(t('Name'), t('Operations'));
+ foreach (user_roles() as $rid => $name) {
+ if (!in_array($rid, array(DRUPAL_ANONYMOUS_RID, DRUPAL_AUTHENTICATED_RID))) {
+ $rows[] = array($name, l(t('edit'), 'admin/access/roles/edit/'. $rid));
+ }
+ else {
+ $rows[] = array($name, ''. t('locked') .'');
+ }
+ }
+ $rows[] = array(form_render($form['name']), form_render($form['submit']));
+
+ return theme('table', $header, $rows);
+}
+
+function user_admin_account() {
+ $header = array(
+ array('data' => t('Username'), 'field' => 'u.name'),
+ array('data' => t('Status'), 'field' => 'u.status'),
+ array('data' => t('Member for'), 'field' => 'u.created', 'sort' => 'desc'),
+ array('data' => t('Last access'), 'field' => 'u.access'),
+ t('Operations')
+ );
+ $sql = 'SELECT u.uid, u.name, u.status, u.created, u.access FROM {users} u WHERE uid != 0';
+ $sql .= tablesort_sql($header);
+ $result = pager_query($sql, 50);
+
+ $status = array(t('blocked'), t('active'));
+ while ($account = db_fetch_object($result)) {
+ $rows[] = array(theme('username', $account),
+ $status[$account->status],
+ format_interval(time() - $account->created),
+ $account->access ? t('%time ago', array('%time' => format_interval(time() - $account->access))) : t('never'),
+ l(t('edit'), "user/$account->uid/edit", array()));
+ }
+
+ $output = theme('table', $header, $rows);
+ $output .= theme('pager', NULL, 50, 0);
+ return $output;
+}
+
+function user_configure() {
+ // User registration settings.
+ $form['registration'] = array('#type' => 'fieldset', '#title' => t('User registration settings'));
+ $form['registration']['user_register'] = array('#type' => 'radios', '#title' => t('Public registrations'), '#default_value' => variable_get('user_register', 1), '#options' => array(t('Only site administrators can create new user accounts.'), t('Visitors can create accounts and no administrator approval is required.'), t('Visitors can create accounts but administrator approval is required.')));
+ $form['registration']['user_registration_help'] = array('#type' => 'textarea', '#title' => t('User registration guidelines'), '#default_value' => variable_get('user_registration_help', ''), '#description' => t('This text is displayed at the top of the user registration form. It\'s useful for helping or instructing your users.'));
+
+ // User e-mail settings.
+ $form['email'] = array('#type' => 'fieldset', '#title' => t('User e-mail settings'));
+ $form['email']['user_mail_welcome_subject'] = array('#type' => 'textfield', '#title' => t('Subject of welcome e-mail'), '#default_value' => _user_mail_text('welcome_subject'), '#maxlength' => 180, '#description' => t('Customize the subject of your welcome e-mail, which is sent to new members upon registering.') .' '. t('Available variables are:') .' %username, %site, %password, %uri, %uri_brief, %mailto, %date, %login_uri, %edit_uri, %login_url.');
+ $form['email']['user_mail_welcome_body'] = array('#type' => 'textarea', '#title' => t('Body of welcome e-mail'), '#default_value' => _user_mail_text('welcome_body'), '#rows' => 15, '#description' => t('Customize the body of the welcome e-mail, which is sent to new members upon registering.') .' '. t('Available variables are:') .' %username, %site, %password, %uri, %uri_brief, %mailto, %login_uri, %edit_uri, %login_url.');
+ $form['email']['user_mail_admin_subject'] = array('#type' => 'textfield', '#title' => t('Subject of welcome e-mail (user created by administrator)'), '#default_value' => _user_mail_text('admin_subject'), '#maxlength' => 180, '#description' => t('Customize the subject of your welcome e-mail, which is sent to new member accounts created by an administrator.') .' '. t('Available variables are:') .' %username, %site, %password, %uri, %uri_brief, %mailto, %date, %login_uri, %edit_uri, %login_url.');
+ $form['email']['user_mail_admin_body'] = array('#type' => 'textarea', '#title' => t('Body of welcome e-mail (user created by administrator)'), '#default_value' => _user_mail_text('admin_body'), '#rows' => 15, '#description' => t('Customize the body of the welcome e-mail, which is sent to new member accounts created by an administrator.') .' '. t('Available variables are:') .' %username, %site, %password, %uri, %uri_brief, %mailto, %login_uri, %edit_uri, %login_url.');
+ $form['email']['user_mail_approval_subject'] = array('#type' => 'textfield', '#title' => t('Subject of welcome e-mail (awaiting admin approval)'), '#default_value' => _user_mail_text('approval_subject'), '#maxlength' => 180, '#description' => t('Customize the subject of your awaiting approval welcome e-mail, which is sent to new members upon registering.') .' '. t('Available variables are:') .' %username, %site, %password, %uri, %uri_brief, %mailto, %date, %login_uri, %edit_uri, %login_url.');
+ $form['email']['user_mail_approval_body'] = array('#type' => 'textarea', '#title' => t('Body of welcome e-mail (awaiting admin approval)'), '#default_value' => _user_mail_text('approval_body'), '#rows' => 15, '#description' => t('Customize the body of the awaiting approval welcome e-mail, which is sent to new members upon registering.') .' '. t('Available variables are:') .' %username, %site, %password, %uri, %uri_brief, %mailto, %login_uri, %edit_uri, %login_url.');
+ $form['email']['user_mail_pass_subject'] = array('#type' => 'textfield', '#title' => t('Subject of password recovery e-mail'), '#default_value' => _user_mail_text('pass_subject'), '#maxlength' => 180, '#description' => t('Customize the Subject of your forgotten password e-mail.') .' '. t('Available variables are:') .' %username, %site, %login_url, %uri, %uri_brief, %mailto, %date, %login_uri, %edit_uri.');
+ $form['email']['user_mail_pass_body'] = array('#type' => 'textarea', '#title' => t('Body of password recovery e-mail'), '#default_value' => _user_mail_text('pass_body'), '#rows' => 15, '#description' => t('Customize the body of the forgotten password e-mail.') .' '. t('Available variables are:') .' %username, %site, %login_url, %uri, %uri_brief, %mailto, %login_uri, %edit_uri.');
+
+ // If picture support is enabled, check whether the picture directory exists:
+ if (variable_get('user_pictures', 0)) {
+ $picture_path = file_create_path(variable_get('user_picture_path', 'pictures'));
+ file_check_directory($picture_path, 1, 'user_picture_path');
+ }
+
+ $form['pictures'] = array('#type' => 'fieldset', '#title' => t('Pictures'));
+ $form['pictures']['user_pictures'] = array('#type' => 'radios', '#title' => t('Picture support'), '#default_value' => variable_get('user_pictures', 0), '#options' => array(t('Disabled'), t('Enabled')), '#description' => t('Enable picture support.'));
+ $form['pictures']['user_picture_path'] = array('#type' => 'textfield', '#title' => t('Picture image path'), '#default_value' => variable_get('user_picture_path', 'pictures'), '#size' => 30, '#maxlength' => 255, '#description' => t('Subdirectory in the directory "%dir" where pictures will be stored.', array('%dir' => file_directory_path() .'/')));
+ $form['pictures']['user_picture_default'] = array('#type' => 'textfield', '#title' => t('Default picture'), '#default_value' => variable_get('user_picture_default', ''), '#size' => 30, '#maxlength' => 255, '#description' => t('URL of picture to display for users with no custom picture selected. Leave blank for none.'));
+ $form['pictures']['user_picture_dimensions'] = array('#type' => 'textfield', '#title' => t('Picture maximum dimensions'), '#default_value' => variable_get('user_picture_dimensions', '85x85'), '#size' => 15, '#maxlength' => 10, '#description' => t('Maximum dimensions for pictures.'));
+ $form['pictures']['user_picture_file_size'] = array('#type' => 'textfield', '#title' => t('Picture maximum file size'), '#default_value' => variable_get('user_picture_file_size', '30'), '#size' => 15, '#maxlength' => 10, '#description' => t('Maximum file size for pictures, in kB.'));
+ $form['pictures']['user_picture_guidelines'] = array('#type' => 'textarea', '#title' => t('Picture guidelines'), '#default_value' => variable_get('user_picture_guidelines', ''), '#description' => t('This text is displayed at the picture upload form in addition to the default guidelines. It\'s useful for helping or instructing your users.'));
+
+ return system_settings_form('user_configure_settings', $form);
+}
+
+function user_admin() {
+ $edit = isset($_POST['edit']) ? $_POST['edit'] : '';
+ $op = isset($_POST['op']) ? $_POST['op'] : '';
+
+ if (empty($op)) {
+ $op = arg(2);
+ }
+
+ switch ($op) {
+ case 'search':
+ case t('Search'):
+ $output = search_form(url('admin/user/search'), $_POST['edit']['keys'], 'user') . search_data($_POST['edit']['keys'], 'user');
+ break;
+ case t('Create new account'):
+ case 'create':
+ $output = user_register();
+ break;
+ default:
+ $output = user_admin_account();
+ }
+ return $output;
+}
+
+/**
+ * Implementation of hook_help().
+ */
+function user_help($section) {
+ global $user;
+
+ switch ($section) {
+ case 'admin/help#user':
+ $output = ''. t('The user module allows users to register, login, and logout. Users benefit from being able to sign on because it associates content they create with their account and allows various permissions to be set for their roles. The user module supports user roles which can setup fine grained permissions allowing each role to do only what the administrator wants them to. Each user is assigned to one or more roles. By default there are two roles anonymous - a user who has not logged in, and authenticated a user who has signed up and who has been authorized. ') .'
';
+ $output .= ''. t('Users can use their own name or handle and can fine tune some personal configuration settings through their individual my account page. Registered users need to authenticate by supplying either a local username and password, or a remote username and password such as DelphiForums ID, or one from a Drupal powered website. A visitor accessing your website is assigned an unique ID, the so-called session ID, which is stored in a cookie. For security\'s sake, the cookie does not contain personal information but acts as a key to retrieve the information stored on your server. ') .'
';
+ $output .= t('You can
+
+- view your user page.
+- administer users at administer >> user.
+- allow users who have the "select different theme" permission to select themes from their user account by enabling themes in administer >> themes.
+- read user profile help at administer >> help >> profile.
+- read about distributed authentication in the system module help at administer >> help >> system.
+
+', array('%user' => url('user'), '%admin-user' => url('admin/user'), '%admin-themes' => url('admin/themes'), '%admin-help-profile' => url('admin/help/profile'), '%admin-help-system' => url('admin/help/system')));
+ $output .= ''. t('For more information please read the configuration and customization handbook User page.', array('%user' => 'http://drupal.org/handbook/modules/user/')) .'
';
+ return $output;
+ case 'admin/modules#description':
+ return t('Manages the user registration and login system.');
+ case 'admin/user':
+ return t('Drupal allows users to register, login, logout, maintain user profiles, etc. No participant can use his own name to post content until he signs up for a user account.
');
+ case 'admin/user/create':
+ case 'admin/user/account/create':
+ return t('This web page allows the administrators to register a new users by hand. Note that you cannot have a user where either the e-mail address or the username match another user in the system.
');
+ case strstr($section, 'admin/access/rules'):
+ return t('Set up username and e-mail address access rules for new and existing accounts (currently logged in accounts will not be logged out). If a username or e-mail address for an account matches any deny rule, but not an allow rule, then the account will not be allowed to be created or to log in. A host rule is effective for every page view, not just registrations.
');
+ case 'admin/access':
+ return t('Permissions let you control what users can do on your site. Each user role (defined on the user roles page) has its own set of permissions. For example, you could give users classified as "Administrators" permission to "administer nodes" but deny this power to ordinary, "authenticated" users. You can use permissions to reveal new features to privileged users (those with subscriptions, for example). Permissions also allow trusted users to share the administrative burden of running a busy site.
', array('%role' => url('admin/access/roles')));
+ case 'admin/access/roles':
+ return t('Roles allow you to fine tune the security and administration of Drupal. A role defines a group of users that have certain privileges as defined in user permissions. Examples of roles include: anonymous user, authenticated user, moderator, administrator and so on. In this area you will define the role names of the various roles. To delete a role choose "edit".
By default, Drupal comes with two user roles:
+
+ - Anonymous user: this role is used for users that don\'t have a user account or that are not authenticated.
+ - Authenticated user: this role is automatically granted to all logged in users.
+
', array('%permissions' => url('admin/access/permissions')));
+ case 'admin/user/search':
+ return t('Enter a simple pattern ("*" may be used as a wildcard match) to search for a username. For example, one may search for "br" and Drupal might return "brian", "brad", and "brenda".
');
+ case 'admin/user/configure':
+ case 'admin/user/configure/settings':
+ return t('In order to use the full power of Drupal a visitor must sign up for an account. This page lets you setup how a user signs up, logs out, the guidelines from the system about user subscriptions, and the e-mails the system will send to the user.
');
+ case 'user/help#user':
+ $site = variable_get('site_name', 'this website');
+
+ $output = t("
+ Distributed authentication
+ One of the more tedious moments in visiting a new website is filling out the registration form. Here at %site, you do not have to fill out a registration form if you are already a member of %help-links. This capability is called distributed authentication, and is unique to Drupal, the software which powers %site.
+ Distributed authentication enables a new user to input a username and password into the login box, and immediately be recognized, even if that user never registered at %site. This works because Drupal knows how to communicate with external registration databases. For example, lets say that new user 'Joe' is already a registered member of Delphi Forums. Drupal informs Joe on registration and login screens that he may login with his Delphi ID instead of registering with %site. Joe likes that idea, and logs in with a username of joe@remote.delphiforums.com and his usual Delphi password. Drupal then contacts the remote.delphiforums.com server behind the scenes (usually using XML-RPC, HTTP POST, or SOAP) and asks: \"Is the password for user Joe correct?\". If Delphi replies yes, then we create a new %site account for Joe and log him into it. Joe may keep on logging into %site in the same manner, and he will always be logged into the same account.
", array('%help-links' => (implode(', ', user_auth_help_links())), '%site' => "$site", '%drupal' => 'http://drupal.org', '%delphi-forums' => 'http://www.delphiforums.com', '%xml' => 'http://www.xmlrpc.com', '%http-post' => 'http://www.w3.org/Protocols/', '%soap' => 'http://www.soapware.org'));
+
+ foreach (module_list() as $module) {
+ if (module_hook($module, 'auth')) {
+ $output .= "". module_invoke($module, 'info', 'name') .'
';
+ $output .= module_invoke($module, 'help', "user/help#$module");
+ }
+ }
+
+ return $output;
+ }
+
+}
+
+/**
+ * Menu callback; Prints user-specific help information.
+ */
+function user_help_page() {
+ return user_help('user/help#user');
+}
+
+/**
+ * Retrieve a list of all user setting/information categories and sort them by weight.
+ */
+function _user_categories($account) {
+ $categories = array();
+
+ foreach (module_list() as $module) {
+ if ($data = module_invoke($module, 'user', 'categories', NULL, $account, '')) {
+ $categories = array_merge($data, $categories);
+ }
+ }
+
+ usort($categories, '_user_sort');
+
+ return $categories;
+}
+
+function _user_sort($a, $b) {
+ return $a['weight'] < $b['weight'] ? -1 : ($a['weight'] > $b['weight'] ? 1 : ($a['title'] < $b['title'] ? -1 : 1));
+}
+
+/**
+ * Retrieve a list of all form elements for the specified category.
+ */
+function _user_forms(&$edit, $account, $category, $hook = 'form') {
+ $groups = array();
+ foreach (module_list() as $module) {
+ if ($data = module_invoke($module, 'user', $hook, $edit, $account, $category)) {
+ $groups = array_merge_recursive($data, $groups);
+ }
+ }
+ uasort($groups, '_user_sort');
+
+ return empty($groups) ? FALSE : $groups;
+}
+
+/**
+ * Retrieve a pipe delimited string of autocomplete suggestions for existing users
+ */
+function user_autocomplete($string) {
+ $matches = array();
+ $result = db_query_range("SELECT name FROM {users} WHERE LOWER(name) LIKE LOWER('%s%%')", $string, 0, 10);
+ while ($user = db_fetch_object($result)) {
+ $matches[$user->name] = check_plain($user->name);
+ }
+ print drupal_to_js($matches);
+ exit();
+}
diff --git a/modules/watchdog.module b/modules/watchdog.module
new file mode 100644
index 0000000..3b0822b
--- /dev/null
+++ b/modules/watchdog.module
@@ -0,0 +1,194 @@
+'. t('The watchdog 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.') .'';
+ $output .= ''. t('The watchdog log is simply a list of recorded events containing usage data, performance data, errors, warnings and operational information. Administrators should check the watchdog report on a regular basis to ensure their site is working properly.') .'
';
+ $output .= t('You can
+
+- view watchdog logs at administer >> watchdog.
+- view watchdog event logs at administer >> watchdog >> events.
+
+', array('%admin-watchdog' => url('admin/watchdog'), '%admin-watchdog-events' => url('admin/watchdog/events')));
+ $output .= ''. t('For more information please read the configuration and customization handbook Watchdog page.', array('%watchdog' => 'http://drupal.org/handbook/modules/watchdog/')) .'
';
+ return $output;
+ case 'admin/modules#description':
+ return t('Logs and records system events.');
+ case 'admin/logs':
+ return t('The watchdog module monitors your web site, capturing system events in a log to be reviewed by an authorized individual at a later time. The watchdog log is simply a list of recorded events containing usage data, performance data, errors, warnings and operational information. It is vital to check the watchdog report on a regular basis as it is often the only way to tell what is going on.
');
+ }
+}
+
+/**
+ * Implementation of hook_menu().
+ */
+function watchdog_menu($may_cache) {
+ $items = array();
+
+ if ($may_cache) {
+ $items[] = array('path' => 'admin/logs', 'title' => t('logs'),
+ 'callback' => 'watchdog_overview');
+ $items[] = array('path' => 'admin/logs/event', 'title' => t('details'),
+ 'callback' => 'watchdog_event',
+ 'type' => MENU_CALLBACK);
+ }
+ return $items;
+}
+
+/**
+ * Implementation of hook_cron().
+ *
+ * Remove expired log messages and flood control events.
+ */
+function watchdog_cron() {
+ db_query('DELETE FROM {watchdog} WHERE timestamp < %d', time() - variable_get('watchdog_clear', 604800));
+ db_query('DELETE FROM {flood} WHERE timestamp < %d', time() - 3600);
+}
+
+/**
+ * Implementation of hook_user().
+ */
+function watchdog_user($op, &$edit, &$user) {
+ if ($op == 'delete') {
+ db_query('UPDATE {watchdog} SET uid = 0 WHERE uid = %d', $user->uid);
+ }
+}
+
+/**
+ * Menu callback; displays a listing of log messages.
+ */
+function watchdog_overview() {
+ $icons = array(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')));
+ $classes = array(WATCHDOG_NOTICE => 'watchdog-notice', WATCHDOG_WARNING => 'watchdog-warning', WATCHDOG_ERROR => 'watchdog-error');
+
+ $names['all'] = t('all messages');
+ foreach (_watchdog_get_message_types() as $type) {
+ $names[$type] = t('%type messages', array('%type' => t($type)));
+ }
+
+ if (empty($_SESSION['watchdog_overview_filter'])) {
+ $_SESSION['watchdog_overview_filter'] = 'all';
+ }
+
+ $form['filter'] = array(
+ '#type' => 'select',
+ '#title' => t('Filter by message type'),
+ '#options' => $names,
+ '#default_value' => $_SESSION['watchdog_overview_filter']
+ );
+ $form['#action'] = url('admin/logs');
+
+ $form['submit'] = array('#type' => 'submit', '#value' =>t('Filter'));
+ $output = drupal_get_form('watchdog_form_overview', $form);
+
+ $header = array(
+ ' ',
+ array('data' => t('Type'), 'field' => 'w.type'),
+ array('data' => t('Date'), 'field' => 'w.wid', 'sort' => 'desc'),
+ array('data' => t('Message'), 'field' => 'w.message'),
+ array('data' => t('User'), 'field' => 'u.name'),
+ array('data' => t('Operations'))
+ );
+
+ $sql = "SELECT w.*, u.name, u.uid FROM {watchdog} w INNER JOIN {users} u ON w.uid = u.uid";
+ $tablesort = tablesort_sql($header);
+ $type = $_SESSION['watchdog_overview_filter'];
+ if ($type != 'all') {
+ $result = pager_query($sql ." WHERE w.type = '%s'". $tablesort, 50, 0, NULL, $type);
+ }
+ else {
+ $result = pager_query($sql . $tablesort, 50);
+ }
+
+ while ($watchdog = db_fetch_object($result)) {
+ $rows[] = array('data' =>
+ array(
+ // Cells
+ $icons[$watchdog->severity],
+ t($watchdog->type),
+ format_date($watchdog->timestamp, 'small'),
+ l(truncate_utf8($watchdog->message, 56, TRUE, TRUE), 'admin/logs/event/'. $watchdog->wid, array(), NULL, NULL, FALSE, TRUE),
+ theme('username', $watchdog),
+ $watchdog->link,
+ ),
+ // Attributes for tr
+ 'class' => "watchdog-". preg_replace('/[^a-z]/i', '-', $watchdog->type) .' '. $classes[$watchdog->severity]
+ );
+ }
+
+ if (!$rows) {
+ $rows[] = array(array('data' => t('No log messages available.'), 'colspan' => 6));
+ }
+
+ $output .= theme('table', $header, $rows);
+ $output .= theme('pager', NULL, 50, 0);
+
+ return $output;
+}
+
+function theme_watchdog_form_overview($form) {
+ return ''. form_render($form) .'';
+}
+
+function watchdog_form_overview_submit($form_id, $form) {
+ global $form_values;
+ $_SESSION['watchdog_overview_filter'] = $form_values['filter'];
+}
+
+/**
+ * Menu callback; displays details about a log message.
+ */
+function watchdog_event($id) {
+ $severity = array(WATCHDOG_NOTICE => t('notice'), WATCHDOG_WARNING => t('warning'), WATCHDOG_ERROR => t('error'));
+ $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 ($watchdog = db_fetch_object($result)) {
+ $header = array(t('Type'), t('Date'), t('User'), t('Location'), t('Referrer'), t('Message'), t('Severity'), t('Hostname'));
+ $data = array(t($watchdog->type), format_date($watchdog->timestamp, 'large'), theme('username', $watchdog), l($watchdog->location, $watchdog->location), l($watchdog->referer, $watchdog->referer), $watchdog->message, $severity[$watchdog->severity], $watchdog->hostname);
+ $output = theme('watchdog_event', $header, $data);
+ }
+ return $output;
+}
+
+function theme_watchdog_event($header, $data) {
+ $output = '';
+ $output .= '';
+
+ $n = count($header);
+ for ($i = 0; $i < $n; $i++) {
+ $output .= '' . $header[$i] . ' ' . $data[$i] . ' ';
+ }
+
+ $output .= '
';
+
+ return $output;
+}
+
+function _watchdog_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;
+}
diff --git a/planetlab/about.php b/planetlab/about.php
new file mode 100644
index 0000000..8427c23
--- /dev/null
+++ b/planetlab/about.php
@@ -0,0 +1,41 @@
+GetPlcRelease ();
+
+print "\n";
+
+foreach ( array( "build", "tags", "rpms" ) as $field) {
+
+ print " \n";
+ print "" . ucwords($field . " details") . "
\n";
+ $keys=array_keys($release[$field]);
+ usort($keys,"__cmp_lower");
+
+ foreach ($keys as $key) {
+ print "" . $key . " ";
+ print "" . $release[$field][$key] . " ";
+ }
+}
+
+print '
';
+
+include 'plc_footer.php';
+
+?>
diff --git a/planetlab/addresses/index.php b/planetlab/addresses/index.php
new file mode 100644
index 0000000..f5c3e23
--- /dev/null
+++ b/planetlab/addresses/index.php
@@ -0,0 +1,40 @@
+person;
+$_roles= $_person['role_ids'];
+
+//print_r( $_person );
+
+
+print " This page is under construction
";
+
+if( !$_GET['id'] ) {
+
+
+}
+else {
+
+
+}
+
+// Print footer
+include 'plc_footer.php';
+
+?>
diff --git a/planetlab/events/calendar.php b/planetlab/events/calendar.php
new file mode 100644
index 0000000..494d728
--- /dev/null
+++ b/planetlab/events/calendar.php
@@ -0,0 +1,191 @@
+
+//"
+//
+echo "";
+
+echo "
+
+
+
+ START
+ END
+
+
+
+
+
+
+
+
+
+
+Select Date
+
+
+
+
+
+
+
+
+
+Select Date
+
+";
+?>
\ No newline at end of file
diff --git a/planetlab/events/calendar/CalendarPopup.js b/planetlab/events/calendar/CalendarPopup.js
new file mode 100644
index 0000000..1b506c1
--- /dev/null
+++ b/planetlab/events/calendar/CalendarPopup.js
@@ -0,0 +1,504 @@
+// CONSTRUCTOR for the CalendarPopup Object
+function CalendarPopup() {
+ var c;
+ if (arguments.length>0) {
+ c = new PopupWindow(arguments[0]);
+ }
+ else {
+ c = new PopupWindow();
+ c.setSize(150,175);
+ }
+ c.offsetX = -152;
+ c.offsetY = 25;
+ c.autoHide();
+ // Calendar-specific properties
+ c.monthNames = new Array("January","February","March","April","May","June","July","August","September","October","November","December");
+ c.monthAbbreviations = new Array("Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec");
+ c.dayHeaders = new Array("S","M","T","W","T","F","S");
+ c.returnFunction = "CP_tmpReturnFunction";
+ c.returnMonthFunction = "CP_tmpReturnMonthFunction";
+ c.returnQuarterFunction = "CP_tmpReturnQuarterFunction";
+ c.returnYearFunction = "CP_tmpReturnYearFunction";
+ c.weekStartDay = 0;
+ c.isShowYearNavigation = false;
+ c.displayType = "date";
+ c.disabledWeekDays = new Object();
+ c.disabledDatesExpression = "";
+ c.yearSelectStartOffset = 2;
+ c.currentDate = null;
+ c.todayText="Today";
+ c.cssPrefix="";
+ c.isShowNavigationDropdowns=false;
+ c.isShowYearNavigationInput=false;
+ window.CP_calendarObject = null;
+ window.CP_targetInput = null;
+ window.CP_dateFormat = "MM/dd/yyyy";
+ // Method mappings
+ c.copyMonthNamesToWindow = CP_copyMonthNamesToWindow;
+ c.setReturnFunction = CP_setReturnFunction;
+ c.setReturnMonthFunction = CP_setReturnMonthFunction;
+ c.setReturnQuarterFunction = CP_setReturnQuarterFunction;
+ c.setReturnYearFunction = CP_setReturnYearFunction;
+ c.setMonthNames = CP_setMonthNames;
+ c.setMonthAbbreviations = CP_setMonthAbbreviations;
+ c.setDayHeaders = CP_setDayHeaders;
+ c.setWeekStartDay = CP_setWeekStartDay;
+ c.setDisplayType = CP_setDisplayType;
+ c.setDisabledWeekDays = CP_setDisabledWeekDays;
+ c.addDisabledDates = CP_addDisabledDates;
+ c.setYearSelectStartOffset = CP_setYearSelectStartOffset;
+ c.setTodayText = CP_setTodayText;
+ c.showYearNavigation = CP_showYearNavigation;
+ c.showCalendar = CP_showCalendar;
+ c.hideCalendar = CP_hideCalendar;
+ c.getStyles = getCalendarStyles;
+ c.refreshCalendar = CP_refreshCalendar;
+ c.getCalendar = CP_getCalendar;
+ c.select = CP_select;
+ c.setCssPrefix = CP_setCssPrefix;
+ c.showNavigationDropdowns = CP_showNavigationDropdowns;
+ c.showYearNavigationInput = CP_showYearNavigationInput;
+ c.copyMonthNamesToWindow();
+ // Return the object
+ return c;
+ }
+function CP_copyMonthNamesToWindow() {
+ // Copy these values over to the date.js
+ if (typeof(window.MONTH_NAMES)!="undefined" && window.MONTH_NAMES!=null) {
+ window.MONTH_NAMES = new Array();
+ for (var i=0; i\n";
+ result += "."+p+"cpYearNavigation,."+p+"cpMonthNavigation { background-color:#C0C0C0; text-align:center; vertical-align:center; text-decoration:none; color:#000000; font-weight:bold; }\n";
+ result += "."+p+"cpDayColumnHeader, ."+p+"cpYearNavigation,."+p+"cpMonthNavigation,."+p+"cpCurrentMonthDate,."+p+"cpCurrentMonthDateDisabled,."+p+"cpOtherMonthDate,."+p+"cpOtherMonthDateDisabled,."+p+"cpCurrentDate,."+p+"cpCurrentDateDisabled,."+p+"cpTodayText,."+p+"cpTodayTextDisabled,."+p+"cpText { font-family:arial; font-size:8pt; }\n";
+ result += "TD."+p+"cpDayColumnHeader { text-align:right; border:solid thin #C0C0C0;border-width:0px 0px 1px 0px; }\n";
+ result += "."+p+"cpCurrentMonthDate, ."+p+"cpOtherMonthDate, ."+p+"cpCurrentDate { text-align:right; text-decoration:none; }\n";
+ result += "."+p+"cpCurrentMonthDateDisabled, ."+p+"cpOtherMonthDateDisabled, ."+p+"cpCurrentDateDisabled { color:#D0D0D0; text-align:right; text-decoration:line-through; }\n";
+ result += "."+p+"cpCurrentMonthDate, .cpCurrentDate { color:#000000; }\n";
+ result += "."+p+"cpOtherMonthDate { color:#808080; }\n";
+ result += "TD."+p+"cpCurrentDate { color:white; background-color: #C0C0C0; border-width:1px; border:solid thin #800000; }\n";
+ result += "TD."+p+"cpCurrentDateDisabled { border-width:1px; border:solid thin #FFAAAA; }\n";
+ result += "TD."+p+"cpTodayText, TD."+p+"cpTodayTextDisabled { border:solid thin #C0C0C0; border-width:1px 0px 0px 0px;}\n";
+ result += "A."+p+"cpTodayText, SPAN."+p+"cpTodayTextDisabled { height:20px; }\n";
+ result += "A."+p+"cpTodayText { color:black; }\n";
+ result += "."+p+"cpTodayTextDisabled { color:#D0D0D0; }\n";
+ result += "."+p+"cpBorder { border:solid thin #808080; }\n";
+ result += "\n";
+ return result;
+ }
+
+// Return a string containing all the calendar code to be displayed
+function CP_getCalendar() {
+ var now = new Date();
+ // Reference to window
+ if (this.type == "WINDOW") { var windowref = "window.opener."; }
+ else { var windowref = ""; }
+ var result = "";
+ // If POPUP, write entire HTML document
+ if (this.type == "WINDOW") {
+ result += "Calendar "+this.getStyles()+"\n";
+ result += '\n';
+ }
+ else {
+ result += '\n';
+ result += '\n';
+ result += '\n';
+ }
+ // Code for DATE display (default)
+ // -------------------------------
+ if (this.displayType=="date" || this.displayType=="week-end") {
+ if (this.currentDate==null) { this.currentDate = now; }
+ if (arguments.length > 0) { var month = arguments[0]; }
+ else { var month = this.currentDate.getMonth()+1; }
+ if (arguments.length > 1 && arguments[1]>0 && arguments[1]-0==arguments[1]) { var year = arguments[1]; }
+ else { var year = this.currentDate.getFullYear(); }
+ var daysinmonth= new Array(0,31,28,31,30,31,30,31,31,30,31,30,31);
+ if ( ( (year%4 == 0)&&(year%100 != 0) ) || (year%400 == 0) ) {
+ daysinmonth[2] = 29;
+ }
+ var current_month = new Date(year,month-1,1);
+ var display_year = year;
+ var display_month = month;
+ var display_date = 1;
+ var weekday= current_month.getDay();
+ var offset = 0;
+
+ offset = (weekday >= this.weekStartDay) ? weekday-this.weekStartDay : 7-this.weekStartDay+weekday ;
+ if (offset > 0) {
+ display_month--;
+ if (display_month < 1) { display_month = 12; display_year--; }
+ display_date = daysinmonth[display_month]-offset+1;
+ }
+ var next_month = month+1;
+ var next_month_year = year;
+ if (next_month > 12) { next_month=1; next_month_year++; }
+ var last_month = month-1;
+ var last_month_year = year;
+ if (last_month < 1) { last_month=12; last_month_year--; }
+ var date_class;
+ if (this.type!="WINDOW") {
+ result += "";
+ }
+ result += '\n';
+ var refresh = windowref+'CP_refreshCalendar';
+ var refreshLink = 'javascript:' + refresh;
+ if (this.isShowNavigationDropdowns) {
+ result += ' ';
+ result += ' ';
+
+ result += ' ';
+ }
+ else {
+ if (this.isShowYearNavigation) {
+ result += '< ';
+ result += ' ';
+ result += '> ';
+ result += ' ';
+
+ result += '< ';
+ if (this.isShowYearNavigationInput) {
+ result += ' ';
+ }
+ else {
+ result += ' ';
+ }
+ result += '> ';
+ }
+ else {
+ result += '<< \n';
+ result += ' \n';
+ result += '>> \n';
+ }
+ }
+ result += '
\n';
+ result += '\n';
+ result += '\n';
+ for (var j=0; j<7; j++) {
+
+ result += ''+this.dayHeaders[(this.weekStartDay+j)%7]+' \n';
+ }
+ result += ' \n';
+ for (var row=1; row<=6; row++) {
+ result += '\n';
+ for (var col=1; col<=7; col++) {
+ var disabled=false;
+ if (this.disabledDatesExpression!="") {
+ var ds=""+display_year+LZ(display_month)+LZ(display_date);
+ eval("disabled=("+this.disabledDatesExpression+")");
+ }
+ var dateClass = "";
+ if ((display_month == this.currentDate.getMonth()+1) && (display_date==this.currentDate.getDate()) && (display_year==this.currentDate.getFullYear())) {
+ dateClass = "cpCurrentDate";
+ }
+ else if (display_month == month) {
+ dateClass = "cpCurrentMonthDate";
+ }
+ else {
+ dateClass = "cpOtherMonthDate";
+ }
+ if (disabled || this.disabledWeekDays[col-1]) {
+ result += ' '+display_date+' \n';
+ }
+ else {
+ var selected_date = display_date;
+ var selected_month = display_month;
+ var selected_year = display_year;
+ if (this.displayType=="week-end") {
+ var d = new Date(selected_year,selected_month-1,selected_date,0,0,0,0);
+ d.setDate(d.getDate() + (7-col));
+ selected_year = d.getYear();
+ if (selected_year < 1000) { selected_year += 1900; }
+ selected_month = d.getMonth()+1;
+ selected_date = d.getDate();
+ }
+ result += ' '+display_date+' \n';
+ }
+ display_date++;
+ if (display_date > daysinmonth[display_month]) {
+ display_date=1;
+ display_month++;
+ }
+ if (display_month > 12) {
+ display_month=1;
+ display_year++;
+ }
+ }
+ result += ' ';
+ }
+ var current_weekday = now.getDay() - this.weekStartDay;
+ if (current_weekday < 0) {
+ current_weekday += 7;
+ }
+ result += '\n';
+ result += ' \n';
+ if (this.disabledDatesExpression!="") {
+ var ds=""+now.getFullYear()+LZ(now.getMonth()+1)+LZ(now.getDate());
+ eval("disabled=("+this.disabledDatesExpression+")");
+ }
+ if (disabled || this.disabledWeekDays[current_weekday+1]) {
+ result += ' '+this.todayText+'\n';
+ }
+ else {
+ result += ' '+this.todayText+'\n';
+ }
+ result += '
\n';
+ result += '
\n';
+ }
+
+ // Code common for MONTH, QUARTER, YEAR
+ // ------------------------------------
+ if (this.displayType=="month" || this.displayType=="quarter" || this.displayType=="year") {
+ if (arguments.length > 0) { var year = arguments[0]; }
+ else {
+ if (this.displayType=="year") { var year = now.getFullYear()-this.yearSelectStartOffset; }
+ else { var year = now.getFullYear(); }
+ }
+ if (this.displayType!="year" && this.isShowYearNavigation) {
+ result += "";
+ result += '\n';
+ result += ' << \n';
+ result += ' '+year+' \n';
+ result += ' >> \n';
+ result += '
\n';
+ }
+ }
+
+ // Code for MONTH display
+ // ----------------------
+ if (this.displayType=="month") {
+ // If POPUP, write entire HTML document
+ result += '\n';
+ for (var i=0; i<4; i++) {
+ result += '';
+ for (var j=0; j<3; j++) {
+ var monthindex = ((i*3)+j);
+ result += ''+this.monthAbbreviations[monthindex]+' ';
+ }
+ result += ' ';
+ }
+ result += '
\n';
+ }
+
+ // Code for QUARTER display
+ // ------------------------
+ if (this.displayType=="quarter") {
+ result += '
\n';
+ for (var i=0; i<2; i++) {
+ result += '';
+ for (var j=0; j<2; j++) {
+ var quarter = ((i*2)+j+1);
+ result += '
Q'+quarter+'
';
+ }
+ result += ' ';
+ }
+ result += '
\n';
+ }
+
+ // Code for YEAR display
+ // ---------------------
+ if (this.displayType=="year") {
+ var yearColumnSize = 4;
+ result += "";
+ result += '\n';
+ result += ' << \n';
+ result += ' >> \n';
+ result += '
\n';
+ result += '\n';
+ for (var i=0; i'+currentyear+'';
+ }
+ result += '';
+ }
+ result += '
\n';
+ }
+ // Common
+ if (this.type == "WINDOW") {
+ result += "\n";
+ }
+ return result;
+ }
diff --git a/planetlab/events/index.php b/planetlab/events/index.php
new file mode 100644
index 0000000..3f5c779
--- /dev/null
+++ b/planetlab/events/index.php
@@ -0,0 +1,435 @@
+person;
+$_roles= $_person['role_ids'];
+
+// paginate unit
+$page_size=30;
+
+$messages = array ();
+
+$event_form = <<< EOF
+
+
+EOF;
+
+function parse_date ($day,$month,$year) {
+ // if everything empty -> unspecified date, return 0
+ if ( empty($day) && empty($month) && empty($year)) {
+ return array ("xxx",0);
+ } else {
+ // fill missing fields with current value
+ if (empty($day)) $day=date('d');
+ if (empty($month)) $month=date('M');
+ if (empty($year)) $year=date('Y');
+ $date=sprintf("%s %s %s",$day,$month,$year);
+ $time=strtotime($date);
+ return array($date,$time);
+ }
+}
+
+function parse_dates () {
+ list($from_date,$from_time) = parse_date($_GET['from_d'],$_GET['from_m'],$_GET['from_y']);
+ list($until_date,$until_time) = parse_date($_GET['until_d'],$_GET['until_m'],$_GET['until_y']);
+ return array($from_date,$from_time,$until_date,$until_time);
+}
+
+function my_is_int ($x) {
+ return (is_numeric($x) ? intval($x) == $x : false);
+}
+
+function truncate ($text,$numb,$etc = "...") {
+ if (strlen($text) > $numb) {
+ $text = substr($text, 0, $numb);
+ $text = $text.$etc;
+ }
+ return $text;
+}
+
+// layout function to refine a row's content
+function layout ($param){
+
+ // format time
+ $time=$param['time'];
+ $date= date('d M Y H:i' ,$time);
+ $param['time']=$date;
+
+ // the call button
+ $message=htmlentities($param['message'], ENT_QUOTES);
+ $call=htmlentities($param['call'], ENT_QUOTES);
+ $detail_text=sprintf("message=<<%s>>\\n\\ncall=<<%s>>\\n\\nruntime=<<%f>>\\n",$message,$call,$param['runtime']);
+ $detail="%s',$call,$detail);
+ $param['call_name']=$detail;
+ unset ($param['call']);
+
+ // the message button
+ $trunc_mess=htmlentities(truncate($param['message'],40),ENT_QUOTES);
+ $detail="%s',$message,$detail);
+ $param['message']=$detail;
+
+ // shrink column name : event_id -> id - paginate_id used in paginate and does not show up
+ $param['id']=$param['event_id'] ;
+ // so that event_id shows up
+ $param['paginate_id']=$param['event_id']; unset($param['event_id']);
+
+ //// shrink column names
+ $param['fault']=$param['fault_code'] ; unset($param['fault_code']);
+ // seem empty on all rows - probably something that I screwed when importing tony's stuff
+ // $param['oty']=$param['object_type'] ; unset($param['object_type']);
+ // $param['oid']=$param['object_id'] ; unset($param['object_id']);
+ $param['otys']=$param['object_types'] ; unset($param['object_types']);
+ $param['oids']=$param['object_ids'] ; unset($param['object_ids']);
+ $param['nid']=$param['node_id'] ; unset($param['node_id']);
+ $param['pid']=$param['person_id'] ; unset($param['person_id']);
+ if (array_key_exists('auth_type',$param)) {
+ $param['at']=$param['auth_type'] ; unset($param['auth_type']);
+ }
+
+ // clears
+ unset($param['object_type']);
+ unset($param['object_id']);
+ unset($param['runtime']);
+ return $param;
+}
+
+//plc_debug('GET',$_GET);
+
+if ( !in_array ('10', $_roles)) {
+ echo " You need admin role to see this page. ";
+
+ } else if (!$_GET['type']) {
+
+ echo "What events would you like to consult :
";
+ // print the selection frame
+ echo $event_form;
+
+ } else {
+
+ // // xxx this of any use at all ?
+ // drupal_set_html_head('
+ //
+ // ');
+
+ // handle dates
+ list($from_date,$from_time,$until_date,$until_time) = parse_dates ();
+ if ( ($from_time != 0) && ($until_time != 0) && ($from_time > $until_time) ) {
+ $messages[] = "Warning - wrong date selection";
+ }
+
+ $filter=array();
+ if ($from_time != 0) {
+ $filter[']time']=$from_time;
+ }
+ if ($until_time != 0) {
+ $filter['[time']=$until_time;
+ }
+
+ //////////////////////////////////////// Events
+ $type=$_GET['type'];
+ if ($type == 'Event') {
+
+ // and the filter applied for fetching events using GetEvent
+ $user_desc=$_GET['event'];
+ if ( ! empty($user_desc)) {
+ // should parse stuff like 45-90,230-3000 - some other day
+ $filter['event_id']=intval($user_desc);
+ }
+ // the filter might be void here - in python we need an empty dict but that's not what we get so
+ if (empty($filter)) {
+ $filter[']time']=0;
+ }
+ $events = $api->GetEvents($filter);
+ if (empty($events)) {
+ $messages[] = "No event found - user input was [" . $user_desc . "]";
+ } else {
+ $title="Events matching " . ($user_desc ? $user_desc : "everything");
+ if ($from_time != 0)
+ $title .= " From " . $from_date;
+ if ($until_time != 0)
+ $title .= " Until " . $until_date;
+ drupal_set_title ($title);
+ }
+
+ // Show messages
+ if (!empty($messages)) {
+ print ' ";
+ }
+
+ if ( ! empty ($events)) {
+ $events= array_map(layout,$events);
+ echo paginate( $events, "paginate_id", "Events", $page_size, "event_id");
+ }
+ } else {
+
+ switch ($type) {
+ case 'Person':
+ $primary_key='person_id';
+ $string_key='email';
+ $user_input=$_GET['person'];
+ $method="GetPersons";
+ $object_type='Person';
+ break;
+
+ case 'Node':
+ $primary_key='node_id';
+ $string_key='hostname';
+ $user_input=$_GET['node'];
+ $method="GetNodes";
+ $object_type='Node';
+ break;
+
+ case 'Site':
+ $primary_key='site_id';
+ $string_key='login_base';
+ $user_input=$_GET['site'];
+ $method="GetSites";
+ $object_type='Site';
+ break;
+
+ case 'Slice':
+ $primary_key='slice_id';
+ $string_key='name';
+ $user_input=$_GET['slice'];
+ $method="GetSlices";
+ $object_type='Slice';
+ break;
+ }
+
+ $object_ids=array();
+ $title=sprintf('Events for type %s:',$object_type);
+ foreach ( split(",",$user_input) as $user_desc) {
+ # numeric
+ if (my_is_int($user_desc)) {
+ $obj_check = call_user_func(array($api,$method),array(intval($user_desc)),array($primary_key));
+ if (empty ($obj_check)) {
+ $messages[] = "No such " . $primary_key . ": " . $user_desc;
+ } else {
+ $object_ids[] = $obj_check[0][$primary_key];
+ $title .= $user_desc . ", " ;
+ }
+ } else {
+ # string
+ $new_object_ids=call_user_func (array($api,$method), array($string_key=>$user_desc),array($primary_key,$string_key));
+ if (empty($new_object_ids)) {
+ $messages[] = "No " . $string_key . " matching " . $user_desc;
+ } else {
+ foreach ($new_object_ids as $new_obj_id) {
+ $object_ids[] = $new_obj_id[$primary_key];
+ $title .= $new_obj_id[$primary_key] . ", ";
+ }
+ }
+ }
+ }
+
+ // Show messages
+ if (!empty($messages)) {
+ print ' ";
+ }
+
+ drupal_set_title($title);
+ $events = $api->GetEventObjects(array('object_id'=>$object_ids,'object_type'=>$object_type));
+
+ $events=array_map(layout,$events);
+ echo paginate( $events, "paginate_id", "--------" . $type . " EVENTS---------", $page_size, "hostname");
+ }
+ }
+
+echo "
Back to Events";
+
+ // Print footer
+include 'plc_footer.php';
+
+?>
+
diff --git a/planetlab/includes/comon.png b/planetlab/includes/comon.png
new file mode 100644
index 0000000..daa35f3
Binary files /dev/null and b/planetlab/includes/comon.png differ
diff --git a/planetlab/includes/delete.png b/planetlab/includes/delete.png
new file mode 100644
index 0000000..4fe6144
Binary files /dev/null and b/planetlab/includes/delete.png differ
diff --git a/planetlab/includes/event.png b/planetlab/includes/event.png
new file mode 100644
index 0000000..d19658e
Binary files /dev/null and b/planetlab/includes/event.png differ
diff --git a/planetlab/includes/js/bsn.Ajax.js b/planetlab/includes/js/bsn.Ajax.js
new file mode 100644
index 0000000..aeec8af
--- /dev/null
+++ b/planetlab/includes/js/bsn.Ajax.js
@@ -0,0 +1,84 @@
+/**
+ * author: Timothy Groves - http://www.brandspankingnew.net
+ * version: 1.0 - 2006-08-04
+ *
+ * requires: nothing
+ *
+ */
+
+var useBSNns;
+
+if (useBSNns)
+{
+ if (typeof(bsn) == "undefined")
+ bsn = {}
+ _bsn = bsn;
+}
+else
+{
+ _bsn = this;
+}
+
+
+
+
+
+
+
+
+
+_bsn.Ajax = function ()
+{
+ this.req = {};
+ this.isIE = false;
+}
+
+
+
+_bsn.Ajax.prototype.makeRequest = function (url, meth, onComp, onErr)
+{
+
+ if (meth != "POST")
+ meth = "GET";
+
+ this.onComplete = onComp;
+ this.onError = onErr;
+
+ var pointer = this;
+
+ // branch for native XMLHttpRequest object
+ if (window.XMLHttpRequest)
+ {
+ this.req = new XMLHttpRequest();
+ this.req.onreadystatechange = function () { pointer.processReqChange() };
+ this.req.open("GET", url, true); //
+ this.req.send(null);
+ // branch for IE/Windows ActiveX version
+ }
+ else if (window.ActiveXObject)
+ {
+ this.req = new ActiveXObject("Microsoft.XMLHTTP");
+ if (this.req)
+ {
+ this.req.onreadystatechange = function () { pointer.processReqChange() };
+ this.req.open(meth, url, true);
+ this.req.send();
+ }
+ }
+}
+
+
+_bsn.Ajax.prototype.processReqChange = function()
+{
+
+ // only if req shows "loaded"
+ if (this.req.readyState == 4) {
+ // only if "OK"
+ if (this.req.status == 200)
+ {
+ this.onComplete( this.req );
+ } else {
+ this.onError( this.req.status );
+ }
+ }
+}
\ No newline at end of file
diff --git a/planetlab/includes/js/bsn.AutoSuggest.js b/planetlab/includes/js/bsn.AutoSuggest.js
new file mode 100644
index 0000000..fcea5e5
--- /dev/null
+++ b/planetlab/includes/js/bsn.AutoSuggest.js
@@ -0,0 +1,336 @@
+/**
+ * author: Timothy Groves - http://www.brandspankingnew.net
+ * version: 1.0 - 2006-08-03
+ *
+ * requires: bsn.DOM.js
+ * bsn.Ajax.js
+ *
+ */
+
+var useBSNns;
+
+if (useBSNns)
+{
+ if (typeof(bsn) == "undefined")
+ bsn = {}
+ _bsn = bsn;
+}
+else
+{
+ _bsn = this;
+}
+
+
+if (typeof(_bsn.DOM) == "undefined")
+ _bsn.DOM = {}
+
+
+
+
+
+
+
+
+_bsn.AutoSuggest = function (fldID, param)
+{
+ if (!document.getElementById)
+ return false;
+
+ this.fld = _bsn.DOM.getElement(fldID);
+
+ if (!this.fld)
+ return false;
+
+
+ this.nInputChars = 0;
+ this.aSuggestions = [];
+ this.iHighlighted = 0;
+
+
+ // parameters object
+ this.oP = (param) ? param : {};
+ // defaults
+ if (!this.oP.minchars) this.oP.minchars = 1;
+ if (!this.oP.method) this.oP.meth = "get";
+ if (!this.oP.varname) this.oP.varname = "input";
+ if (!this.oP.className) this.oP.className = "autosuggest";
+ if (!this.oP.timeout) this.oP.timeout = 2500;
+ if (!this.oP.delay) this.oP.delay = 500;
+ if (!this.oP.maxheight && this.oP.maxheight !== 0) this.oP.maxheight = 250;
+ if (!this.oP.cache) this.oP.cache = true;
+
+ var pointer = this;
+
+ this.fld.onkeyup = function () { pointer.getSuggestions( this.value ) };
+ this.fld.setAttribute("autocomplete","off");
+}
+
+
+
+_bsn.AutoSuggest.prototype.getSuggestions = function (val)
+{
+
+ if (val.length == this.nInputChars)
+ return false;
+
+ if (val.length < this.oP.minchars)
+ {
+ this.nInputChars = val.length;
+ this.aSuggestions = [];
+ this.clearSuggestions();
+ return false;
+ }
+
+
+ if (val.length>this.nInputChars && this.aSuggestions.length && this.oP.cache)
+ {
+ // get from cache
+ var arr = [];
+ for (var i=0;i this.oP.maxheight && this.oP.maxheight != 0)
+ {
+ ul.style['height'] = this.oP.maxheight;
+ }
+
+
+ var TAB = 9;
+ var ESC = 27;
+ var KEYUP = 38;
+ var KEYDN = 40;
+ var RETURN = 13;
+
+
+
+ this.fld.onkeydown = function(ev)
+ {
+ var key = (window.event) ? window.event.keyCode : ev.keyCode;
+
+ switch(key)
+ {
+ case TAB:
+ pointer.setHighlightedValue();
+ break;
+
+ case ESC:
+ pointer.clearSuggestions();
+ break;
+
+ case KEYUP:
+ pointer.changeHighlight(key);
+ return false;
+ break;
+
+ case KEYDN:
+ pointer.changeHighlight(key);
+ return false;
+ break;
+ }
+
+ };
+
+ this.iHighlighted = 0;
+
+
+ // remove autosuggest after an interval
+ //
+ clearTimeout(this.toID);
+ var pointer = this;
+ this.toID = setTimeout(function () { pointer.clearSuggestions() }, this.oP.timeout);
+}
+
+
+
+
+
+
+
+
+
+_bsn.AutoSuggest.prototype.changeHighlight = function(key)
+{
+ var list = _bsn.DOM.getElement(this.idAs);
+ if (!list)
+ return false;
+
+
+ if (this.iHighlighted > 0)
+ list.childNodes[this.iHighlighted-1].className = "";
+
+ if (key == 40)
+ this.iHighlighted ++;
+ else if (key = 38)
+ this.iHighlighted --;
+
+
+ if (this.iHighlighted > list.childNodes.length)
+ this.iHighlighted = list.childNodes.length;
+ if (this.iHighlighted < 1)
+ this.iHighlighted = 1;
+
+ list.childNodes[this.iHighlighted-1].className = "highlight";
+
+ //alert( list.childNodes[this.iHighlighted-1].firstChild.firstChild.nodeValue );
+
+ this.killTimeout();
+}
+
+
+
+
+
+
+
+
+_bsn.AutoSuggest.prototype.killTimeout = function()
+{
+ clearTimeout(this.toID);
+}
+
+_bsn.AutoSuggest.prototype.resetTimeout = function()
+{
+ clearTimeout(this.toID);
+ var pointer = this;
+ this.toID = setTimeout(function () { pointer.clearSuggestions() }, 1000);
+}
+
+
+
+
+
+
+
+_bsn.AutoSuggest.prototype.clearSuggestions = function ()
+{
+ if (document.getElementById(this.idAs))
+ _bsn.DOM.removeElement(this.idAs);
+ this.fld.onkeydown = null;
+}
+
+
+
+
+
+
+
+_bsn.AutoSuggest.prototype.setHighlightedValue = function ()
+{
+ if (this.iHighlighted)
+ {
+ this.fld.value = document.getElementById(this.idAs).childNodes[this.iHighlighted-1].firstChild.firstChild.nodeValue;
+ this.killTimeout();
+ this.clearSuggestions();
+ }
+}
+
+
+
+_bsn.AutoSuggest.prototype.setValue = function (val)
+{
+ this.fld.value = val;
+ this.resetTimeout();
+}
\ No newline at end of file
diff --git a/planetlab/includes/js/bsn.DOM.js b/planetlab/includes/js/bsn.DOM.js
new file mode 100644
index 0000000..2b98e42
--- /dev/null
+++ b/planetlab/includes/js/bsn.DOM.js
@@ -0,0 +1,223 @@
+/**
+ * author: Timothy Groves - http://www.brandspankingnew.net
+ * version: 1.5 - 2006-08-03
+ *
+ * requires: nothing
+ *
+ */
+
+var useBSNns;
+
+if (useBSNns)
+{
+ if (typeof(bsn) == "undefined")
+ bsn = {}
+ _bsn = bsn;
+}
+else
+{
+ _bsn = this;
+}
+
+
+if (typeof(_bsn.DOM) == "undefined")
+ _bsn.DOM = {}
+
+
+
+
+_bsn.DOM.createElement = function ( type, attr, cont, html )
+{
+ var ne = document.createElement( type );
+ if (!ne)
+ return false;
+
+ for (var a in attr)
+ ne[a] = attr[a];
+
+ if (typeof(cont) == "string" && !html)
+ ne.appendChild( document.createTextNode(cont) );
+ else if (typeof(cont) == "string" && html)
+ ne.innerHTML = cont;
+ else if (typeof(cont) == "object")
+ ne.appendChild( cont );
+
+ return ne;
+}
+
+
+
+
+
+_bsn.DOM.clearElement = function ( id )
+{
+ var ele = this.getElement( id );
+
+ if (!ele)
+ return false;
+
+ while (ele.childNodes.length)
+ ele.removeChild( ele.childNodes[0] );
+
+ return true;
+}
+
+
+
+
+
+
+
+
+
+_bsn.DOM.removeElement = function ( ele )
+{
+ var e = this.getElement(ele);
+
+ if (!e)
+ return false;
+ else if (e.parentNode.removeChild(e))
+ return true;
+ else
+ return false;
+}
+
+
+
+
+
+_bsn.DOM.replaceContent = function ( id, cont, html )
+{
+ var ele = this.getElement( id );
+
+ if (!ele)
+ return false;
+
+ this.clearElement( ele );
+
+ if (typeof(cont) == "string" && !html)
+ ele.appendChild( document.createTextNode(cont) );
+ else if (typeof(cont) == "string" && html)
+ ele.innerHTML = cont;
+ else if (typeof(cont) == "object")
+ ele.appendChild( cont );
+}
+
+
+
+
+
+
+
+
+
+_bsn.DOM.getElement = function ( ele )
+{
+ if (typeof(ele) == "undefined")
+ {
+ return false;
+ }
+ else if (typeof(ele) == "string")
+ {
+ var re = document.getElementById( ele );
+ if (!re)
+ return false;
+ else if (typeof(re.appendChild) != "undefined" ) {
+ return re;
+ } else {
+ return false;
+ }
+ }
+ else if (typeof(ele.appendChild) != "undefined")
+ return ele;
+ else
+ return false;
+}
+
+
+
+
+
+
+
+_bsn.DOM.appendChildren = function ( id, arr )
+{
+ var ele = this.getElement( id );
+
+ if (!ele)
+ return false;
+
+
+ if (typeof(arr) != "object")
+ return false;
+
+ for (var i=0;i'lorem', '2'=>'ipsum' );
+// var sel = '2';
+
+_bsn.DOM.createSelect = function ( attr, opt, sel )
+{
+ var select = this.createElement( 'select', attr );
+ for (var a in opt)
+ {
+
+ var o = {id:a};
+ if (a == sel) o.selected = "selected";
+ select.appendChild( this.createElement( 'option', o, opt[a] ) );
+
+ }
+
+ return select;
+}
+
+
+
+
+_bsn.DOM.getPos = function ( ele )
+{
+ var ele = this.getElement(ele);
+
+ var obj = ele;
+
+ var curleft = 0;
+ if (obj.offsetParent)
+ {
+ while (obj.offsetParent)
+ {
+ curleft += obj.offsetLeft
+ obj = obj.offsetParent;
+ }
+ }
+ else if (obj.x)
+ curleft += obj.x;
+
+
+ var obj = ele;
+
+ var curtop = 0;
+ if (obj.offsetParent)
+ {
+ while (obj.offsetParent)
+ {
+ curtop += obj.offsetTop
+ obj = obj.offsetParent;
+ }
+ }
+ else if (obj.y)
+ curtop += obj.y;
+
+ return {x:curleft, y:curtop}
+}
\ No newline at end of file
diff --git a/planetlab/includes/plc_drupal.php b/planetlab/includes/plc_drupal.php
new file mode 100644
index 0000000..8034d5c
--- /dev/null
+++ b/planetlab/includes/plc_drupal.php
@@ -0,0 +1,51 @@
+
+// Copyright (C) 2006 The Trustees of Princeton University
+//
+// $Id: plc_drupal.php 144 2007-03-28 07:52:20Z thierry $ $
+//
+
+if (!function_exists('drupal_set_title')) {
+ function drupal_set_title($title = NULL)
+ {
+ static $stored_title;
+
+ if (isset($title)) {
+ $stored_title = $title;
+ }
+ return $stored_title;
+ }
+}
+
+if (!function_exists('drupal_get_title')) {
+ function drupal_get_title()
+ {
+ return drupal_set_title();
+ }
+}
+
+if (!function_exists('drupal_set_html_head')) {
+ function drupal_set_html_head($data = NULL)
+ {
+ static $stored_head = '';
+
+ if (!is_null($data)) {
+ $stored_head .= $data ."\n";
+ }
+ return $stored_head;
+ }
+}
+
+if (!function_exists('drupal_get_html_head')) {
+ function drupal_get_html_head()
+ {
+ $output = "\n";
+ // XXX Insert CSS link here
+ return $output . drupal_set_html_head();
+ }
+}
+
+?>
diff --git a/planetlab/includes/plc_footer.php b/planetlab/includes/plc_footer.php
new file mode 100644
index 0000000..252a218
--- /dev/null
+++ b/planetlab/includes/plc_footer.php
@@ -0,0 +1,22 @@
+
+// Copyright (C) 2006 The Trustees of Princeton University
+//
+// $Id: plc_footer.php 144 2007-03-28 07:52:20Z thierry $ $
+//
+
+require_once 'plc_drupal.php';
+
+if (!function_exists('drupal_page_footer')) {
+ print <<
+
+
+EOF;
+}
+
+?>
\ No newline at end of file
diff --git a/planetlab/includes/plc_functions.php b/planetlab/includes/plc_functions.php
new file mode 100644
index 0000000..91b2ef3
--- /dev/null
+++ b/planetlab/includes/plc_functions.php
@@ -0,0 +1,383 @@
+ $val ) {
+// if( substr( $key, -3 ) == "_id" )
+ if ( $key == $table_id ) {
+ $id[$key]= $val;
+ } else {
+ $data[$key]= $val;
+ }
+ }
+
+ foreach( $id as $key => $val )
+ $arr2[$key]= $val;
+
+ foreach( $data as $key => $val )
+ $arr2[$key]= $val;
+
+ $as_array[]= $arr2;
+ }
+
+ $totalrows= count( $as_array );
+
+ // if array has no rows display msg
+ if( $totalrows == 0 )
+ return "Nothing to display";
+
+ // set key and break up data struct
+ $newkey= $page - 1;
+ $newarray= array_chunk( $as_array, $limit );
+
+ // start table output
+ $echo.= "\n";
+
+ // if there is a caption add it to table
+ if( $caption )
+ $echo.= "$caption \n";
+
+ $echo.= "";
+
+ // go through keys of one array row for table headers
+ foreach( $newarray[$newkey][0] as $key => $val ) {
+// if( substr( $key, -3 ) != "_id" )
+ if ( $key != $table_id && $key != 'peer_id' )
+ $echo.= "". ucfirst( $key ) ." ";
+ }
+
+ if( $other_func == 'slivers' )
+ $echo.= "Slivers ";
+
+ $echo.= " \n";
+
+ // go through array row by row to output table rows
+ foreach( $newarray[$newkey] as $assoc ) {
+
+ $extraclass="";
+ if ($assoc['peer_id']) {
+ $extraclass="plc-foreign";
+ }
+
+
+ $echo.= "";
+
+ foreach( $assoc as $key => $val ) {
+ // do not rely on keys order
+ $id = $assoc[$table_id];
+// if( substr( $key, -3 ) == "_id" )
+ if ($key == $table_id) {
+// $id= $val;
+ continue;
+ } elseif( $key == $main_field ) {
+ $echo.= "$val ";
+ } elseif ($key != 'peer_id') {
+ $echo.= "";
+ if( is_array( $val ) ) {
+ $count= 1;
+ $tot= count( $val );
+ foreach( $val as $k => $v ) {
+ $echo.= $v;
+ if( $count != $tot )
+ $echo.= ", ";
+ $count++;
+ }
+ }
+ else
+ $echo.= $val;
+ $echo.= " ";
+ }
+
+ }
+
+ if( $other_func == 'slivers' )
+ $echo.= "view ";
+
+ $echo.= " \n";
+ }
+
+ // close table
+ $echo.= "
\n";
+ $echo.= "
\n";
+
+ // find total number of pages
+ $numofpages = $totalrows / $limit;
+
+ // start navigation links
+ if( $numofpages > 1 ) {
+ // if page is not 1 display first and prev links
+ if( $page != 1 && $page ) {
+ $pageprev= $page - 1;
+ $echo.= "FIRST ";
+ $echo.= " PREV ".$limit." ";
+ }
+ else
+ $echo.= "PREV ". $limit ." ";
+
+ // if less than 30 pages display all
+ // otherwise show 30 pages but put ... inbetween
+ if( $numofpages < 30 ) {
+ $npages= $numofpages;
+ $start= 1;
+ }
+ else {
+ $npages= $page + 9;
+ if( $npages > $numofpages )
+ $npages= $numofpages;
+ $start= $page - 10;
+ if( $start < 1 )
+ $start= 1;
+ if( $page != 1 )
+ $echo.= " ... ";
+ }
+
+ // display pages, no link if current page
+ for( $i= $start; $i <= $npages; $i++ ) {
+ if( $i == $page )
+ $echo.= $i ." ";
+ else
+ $echo.= "$i ";
+
+ }
+
+ if( ( $totalrows % $limit ) != 0 ) {
+ $last= $numofpages + 1;
+ if( $i == $page )
+ $echo.= $i ." ";
+ else
+ $echo.= "$i ";
+ }
+ else
+ $last= $numofpages;
+
+ if( $numofpages >= 30 ) {
+ if( $page != $numofpages )
+ $echo.= " ... ";
+ }
+
+ if( ( $totalrows - ($limit * $page) ) > 0 ) {
+ $pagenext= $page + 1;
+ $echo.= " NEXT ".$limit." ";
+ }
+ else
+ $echo.= "NEXT ". $limit;
+
+ $echo.= " LAST\n";
+
+ }
+
+ return $echo;
+
+}
+
+// function for getting the diff of multi dimention array
+function arr_diff( $a1, $a2 ) {
+ $diff= array();
+ foreach( $a1 as $k=>$v ) {
+ unset( $dv );
+ for( $x= 0; $x < count( $a2 ); $x++ ) {
+ if( is_int( $k ) ) {
+ if( array_search( $v, $a2 ) === false )
+ $dv=$v;
+ else if( is_array( $v ) )
+ $dv= arr_diff( $v, $a2[$x] );
+ if( $dv && !in_array( $dv, $diff ) )
+ $diff[]=$dv;
+ }
+ else {
+
+ if( !$a2[$k] )
+ $dv=$v;
+ else if(is_array($v))
+ $dv=arr_diff($v,$a2[$x]);
+ if($dv)
+ $diff[$x]=$dv;
+ }
+ }
+ }
+ return $diff;
+}
+
+
+function is_valid_email_addr($email)
+{
+ if( ereg("^.+@.+\\..+$", $email) )
+ return true;
+ else
+ return false;
+}
+
+function is_valid_url($url)
+{
+ if( ereg("^(http|https)://.+\..+$", strtolower($url) ) )
+ return true;
+ else
+ return false;
+}
+
+function is_valid_ip($ip)
+{
+ if( ereg("^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$", $ip ) )
+ {
+ // it's at least in the right format, now check to see if
+ // each part is equal to less than 255
+ $parts= explode( '.', $ip );
+ $count= count($parts);
+
+ for( $i= 0; $i < $count; $i++ )
+ {
+ if( intval($parts[$i]) > 255 )
+ return false;
+ }
+
+ return true;
+ }
+ else
+ return false;
+}
+
+
+function is_valid_network_addr($network_addr,$mask)
+{
+ $lNetwork= ip2long($network_addr);
+ $lMask= ip2long($mask);
+
+ // are they the correct format?
+ if( $lNetwork == -1 || $lMask == -1 )
+ return false;
+
+ // is network address valid for the mask?
+ if( ($lNetwork & $lMask) != $lNetwork )
+ return false;
+
+ return true;
+}
+
+
+// returns whether or not a network address is in the reserved space
+// in the case of a invalid network address, false will be returned.
+function is_reserved_network_addr($network_addr)
+{
+ $lNetwork= ip2long($network_addr);
+
+ if( $lNetwork == -1 )
+ return false;
+
+ // does the network address fall in a reserved block?
+ $reserved_ips = array (
+ array('10.0.0.0','10.255.255.255'),
+ array('172.16.0.0','172.31.0.0'),
+ array('192.168.0.0','192.168.255.0')
+ );
+ foreach ($reserved_ips as $r)
+ {
+ $min = ip2long($r[0]);
+ $max = ip2long($r[1]);
+
+ if (($lNetwork >= $min) && ($lNetwork <= $max))
+ return true;
+ }
+
+ return false;
+}
+
+// builds a table from an array of strings, with the given class
+function plc_make_table ($class, $messages) {
+ // pretty print the cell
+ $formatted = "";
+ if (! empty ($messages)) {
+ $formatted="";
+ foreach ($messages as $message) {
+ $formatted .= "" . $message . " ";
+ }
+ $formatted .= "
";
+ }
+ return $formatted;
+}
+
+// shows a php variable verbatim with a heading message
+function plc_debug($message,$object) {
+ print "
" . $message . "";
+ print_r ($object);
+ print "
";
+}
+
+// attempt to normalize the delete buttons and confirmations
+function plc_delete_button($width=15) {
+ return '';
+}
+
+function plc_js_confirm($message) {
+ return "onclick=\"javascript:return confirm('Are you sure you want to delete " . $message . " ?')\"";
+}
+
+function plc_delete_link($url,$delete_message,$visible) {
+ return "" . $visible . "";
+}
+
+function plc_delete_link_button($url,$delete_message,$width=15) {
+ return "" . plc_delete_button($width) . "";
+}
+
+function plc_event_button($type,$param,$id) {
+ return ' ';
+}
+
+function plc_comon_button ($field, $value,$target="") {
+ $result='';
+ $result.=' ';
+ return $result;
+}
+
+function plc_peers_option_list ($api) {
+
+ // get list of peers
+ $peers=$api->GetPeers(NULL,array('peer_id','peername'));
+ if (count($peers)==0) {
+ $predef=array(array("peer_id"=>"","peername"=>"All (no known peers)"));
+ } else {
+ $predef=array(array("peer_id"=>"","peername"=>"All peers"),
+ array("peer_id"=>"local","peername"=>"Local only"));
+ // show a 'foreign' button only if that makes sense
+ if (count($peers) >= 2) {
+ $predef [] = array("peer_id"=>"foreign","peername"=>"Foreign peers");
+ }
+ }
+
+ $result="";
+ foreach ($predef as $a) {
+ $peer_line = "\n";
+ $result .= $peer_line;
+ }
+
+ if (!empty($peers)) {
+ foreach ($peers as $a) {
+ $peer_line = "\n";
+ $result .= $peer_line;
+ }
+ }
+
+ return $result;
+}
+
+?>
diff --git a/planetlab/includes/plc_header.php b/planetlab/includes/plc_header.php
new file mode 100644
index 0000000..34b552c
--- /dev/null
+++ b/planetlab/includes/plc_header.php
@@ -0,0 +1,33 @@
+
+// Copyright (C) 2006 The Trustees of Princeton University
+//
+// $Id: plc_header.php 144 2007-03-28 07:52:20Z thierry $ $
+//
+
+require_once 'plc_drupal.php';
+drupal_set_html_head('');
+
+if (!function_exists('drupal_page_header')) {
+ $title = drupal_get_title();
+ $head = drupal_get_html_head();
+
+ print <<
+
+
+
+ $title
+ $head
+
+
+
+
+EOF;
+}
+
+?>
diff --git a/planetlab/includes/plc_login.php b/planetlab/includes/plc_login.php
new file mode 100644
index 0000000..b8d5b63
--- /dev/null
+++ b/planetlab/includes/plc_login.php
@@ -0,0 +1,27 @@
+
+// Copyright (C) 2006 The Trustees of Princeton University
+//
+// $Id: plc_login.php 144 2007-03-28 07:52:20Z thierry $ $
+//
+
+require_once 'plc_session.php';
+global $plc, $api;
+
+if (!$plc->person) {
+ // Where they were trying to go
+ $url = $_SERVER['PHP_SELF'];
+ if (!empty($_SERVER['QUERY_STRING'])) {
+ $url .= "?" . $_SERVER['QUERY_STRING'];
+ }
+
+ Header("Location: /db/login.php?url=" . urlencode($url));
+ exit();
+}
+
+?>
diff --git a/planetlab/includes/plc_script.js b/planetlab/includes/plc_script.js
new file mode 100644
index 0000000..2c1e05d
--- /dev/null
+++ b/planetlab/includes/plc_script.js
@@ -0,0 +1,35 @@
+
+function addLoadEvent(func) {
+ if (!document.getElementById | !document.getElementsByTagName) return
+ var oldonload=window.onload
+ if (typeof window.onload != 'function') { window.onload=func }
+ else {
+ window.onload=function() { oldonload(); func() }
+ }
+}
+
+// ----------------------------------------------------------------------------- //
+
+function show(id){
+ if (!document.getElementsByTagName) return
+ if (document.getElementById(id).style.display=='block'){
+ document.getElementById(id).style.display='none'
+ }
+ else{
+ document.getElementById(id).style.display='block'
+ }
+ // focus is moved by the href of the link to the start of the help
+}
+
+
+function hide(id){
+ if (document.getElementById) {document.getElementById(id).style.display='none'}
+}
+
+function copyValue (id1,id2) {
+ if (document.getElementById) {document.getElementById(id2).value=document.getElementById(id1).value}
+}
+
+
+// ----------------------------------------------------------------------------- //
+
diff --git a/planetlab/includes/plc_session.php b/planetlab/includes/plc_session.php
new file mode 100644
index 0000000..f3a3d0d
--- /dev/null
+++ b/planetlab/includes/plc_session.php
@@ -0,0 +1,157 @@
+
+// Copyright (C) 2006 The Trustees of Princeton University
+//
+// $Id: plc_session.php 804 2007-08-31 13:58:58Z thierry $ $
+//
+
+// Usually in /etc/planetlab/php
+require_once 'plc_config.php';
+
+// Usually in /usr/share/plc_api/php
+require_once 'plc_api.php';
+
+$cwd = getcwd();
+chdir($_SERVER['DOCUMENT_ROOT']);
+$included = include_once('./includes/bootstrap.inc');
+if ($included === TRUE) {
+ // Already included, no need to bootstrap
+} elseif ($included) {
+ // Not already included, initialize Drupal session handling
+ drupal_bootstrap(DRUPAL_BOOTSTRAP_SESSION);
+} else {
+ // Drupal not available, use regular PHP session handling
+ session_start();
+}
+chdir($cwd);
+
+class PLCSession
+{
+ var $api;
+ var $person;
+ var $alt_person;
+ var $alt_auth;
+
+ function PLCSession($name = NULL, $pass = NULL)
+ {
+ $name= strtolower( $name );
+ // User API access
+ if ($name && $pass) {
+ $api = new PLCAPI(array('AuthMethod' => "password",
+ 'Username' => $name,
+ 'AuthString' => $pass));
+
+ // Authenticate user and get session key
+ $session = $api->GetSession();
+ if (!$session) {
+ return NULL;
+ }
+
+ // Change GetSession() at some point to return expires as well
+ $expires = time() + (24 * 60 * 60);
+
+ // Change to session authentication
+ $api->auth = array('AuthMethod' => "session", 'session' => $session);
+ $this->api = $api;
+
+ // Get account details
+ list($person) = $api->GetPersons(array($name));
+ $this->person = $person;
+
+ // Save session variables
+ $_SESSION['plc'] = array('auth' => $api->auth,
+ 'person' => $person,
+ 'expires' => $expires);
+ }
+ }
+
+ function BecomePerson($person_id)
+ {
+ list($person) = $this->api->GetPersons(array($person_id));
+ if ($person)
+ {
+ //Get this users session if one exists, create
+ //one otherwise
+ list($session) = $this->api->GetSessions(array('person_id' => $person['person_id']));
+ if (!$session)
+ {
+ $session = $this->api->AddSession($person['person_id']);
+ }
+ else
+ {
+ $session = $session['session_id'];
+ }
+
+ // Update session authentication info
+ $this->alt_auth = $this->api->auth;
+ $this->api->auth = array('AuthMethod' => "session", 'session' => $session);
+
+ // su to user
+ $this->alt_person = $this->person;
+ $this->person = $person;
+
+ // Save session variables
+ $_SESSION['plc']['auth'] = $this->api->auth;
+ $_SESSION['plc']['person'] = $this->person;
+ $_SESSION['plc']['alt_person'] = $this->alt_person;
+ $_SESSION['plc']['alt_auth'] = $this->alt_auth;
+
+ }
+ }
+
+ function BecomeSelf()
+ {
+ if($this->alt_auth && $this->alt_person )
+ {
+ $this->person = $this->alt_person;
+ $this->api->auth = $this->alt_auth;
+ $this->alt_person = NULL;
+ $this->alt_auth = NULL;
+
+ $_SESSION['plc']['auth'] = $_SESSION['plc']['alt_auth'];
+ $_SESSION['plc']['person'] = $_SESSION['plc']['alt_person'];
+ unset($_SESSION['plc']['alt_auth']);
+ unset($_SESSION['plc']['alt_person']);
+ }
+ }
+
+
+ function logout()
+ {
+ $this->api->DeleteSession();
+ }
+}
+
+global $plc, $api;
+
+$plc = new PLCSession();
+
+if (!empty($_SESSION['plc'])) {
+ if ($_SESSION['plc']['expires'] > time()) {
+ $plc->person = $_SESSION['plc']['person'];
+ $plc->api = new PLCAPI($_SESSION['plc']['auth']);
+ $plc->alt_person = $_SESSION['plc']['alt_person'];
+ $plc->alt_auth = $_SESSION['plc']['alt_auth'];
+ } else {
+ // Destroy PHP session
+ session_destroy();
+ }
+}
+
+// For convenience
+$api = $plc->api;
+
+?>
diff --git a/planetlab/includes/plc_sorts.php b/planetlab/includes/plc_sorts.php
new file mode 100644
index 0000000..3c342cc
--- /dev/null
+++ b/planetlab/includes/plc_sorts.php
@@ -0,0 +1,78 @@
+ $val) {
+ if ($val == $bs[$key]) {
+ continue;
+ }
+ return ($val < $bs[$key]) ? -1 : 1;
+ }
+}
+
+function sort_nodes(&$nodes) {
+ return usort($nodes, "__cmp_nodes");
+}
+
+// node group sort on name
+function __cmp_nodegroups($a, $b) {
+ return strcasecmp($a['name'], $b['name']);
+}
+
+function sort_nodegroups(&$nodegroups) {
+ return usort($nodegroups, "__cmp_nodegroups");
+}
+
+// site sort on name
+function __cmp_sites($a, $b) {
+ return strcasecmp($a['name'], $b['name']);
+}
+
+function sort_sites(&$sites) {
+ return usort($sites, "__cmp_sites");
+}
+
+// slice sort on name
+function __cmp_slices($a, $b) {
+ return strcasecmp($a['name'], $b['name']);
+}
+
+function sort_slices(&$slices) {
+ return usort($slices, "__cmp_slices");
+}
+
+function __cmp_peers($a,$b) {
+ return strcmp($a['peername'],$b['peername']);
+}
+
+function sort_peers (&$peers) {
+ return usort ($peers, "__cmp_peers");
+}
+
+function __cmp_nodenetwork_settings($a,$b) {
+ $cat=strcmp($a['category'],$b['category']);
+ if ($cat != 0) {
+ return $cat;
+ } else {
+ return strcmp($a['name'],$b['name']);
+ }
+}
+
+function sort_nodenetwork_settings (&$nodenetwork_settings) {
+ return usort ($nodenetwork_settings,"__cmp_nodenetwork_settings");
+}
+
+?>
diff --git a/planetlab/includes/plc_style.css b/planetlab/includes/plc_style.css
new file mode 100644
index 0000000..b8feebe
--- /dev/null
+++ b/planetlab/includes/plc_style.css
@@ -0,0 +1,297 @@
+/*
+expanding links
+*/
+
+.example {padding:10px; color:#000; background:#fcf7ff; width:80%; clear:left; margin:2em auto 3em auto; border:1px solid #69c}
+a.expandlink {color:#00c}
+a:active.expandlink,
+a:focus.expandlink,
+a:hover.expandlink {color:#fff}
+
+/*
+body styles
+*/
+/*
+body {
+ font-family: sans-serif;
+ }
+
+#container {
+
+ }
+
+#topcontent {
+ position: relative;
+ width:100%;
+ height:30px;
+ }
+
+#loginbar {
+ padding:1px;
+ float:right;
+ font-size:14px;
+ border:2px solid #072F6F;
+ }
+
+#menu {
+ position: relative;
+ }
+
+#links {
+ top: 15px;
+ float:right;
+ }
+
+#boxcontainer {
+ margin-top:5px;
+ width:100%;
+ }
+
+#topbox {
+ width:100%;
+ height:49%;
+ top:50px;
+ }
+
+#topleftcontent {
+ float:left;
+ width:49%;
+ height:98%;
+ background:#fff;
+ padding:4px;
+ }
+
+#toprightcontent {
+ float:right;
+ width:49%;
+ height:98%;
+ background:#fff;
+ padding:4px;
+ }
+
+#bottombox {
+ margin-top:1%;
+ width: 100%;
+ height: 49%;
+ }
+
+#bottomleftcontent {
+ float: left;
+ width:49%;
+ height:98%;
+ background:#fff;
+ padding:4px;
+ }
+
+#bottomrightcontent {
+ float: right;
+ width:49%;
+ height:98%;
+ background:#fff;
+ padding:4px;
+ }
+
+#toprightcontent, #bottomrightcontent, #topleftcontent, #bottomleftcontent{
+ border:2px solid #072F6F;
+ }
+*/
+/*
+form styles
+*/
+/*
+form {
+ border: 1px solid #072F6F;
+ padding: 5px;
+ }
+
+.textarea_input {
+ background-color: #ddd;
+ color: #072F6F;
+ font-size:12px;
+ }
+
+.select_input {
+ background-color: #ddd;
+ color: #072F6F;
+ font-size:12px;
+ }
+
+.text_input {
+ font-size: 10px;
+ background-color: #CCCCCC;
+ border: 1px solid #666666;
+ }
+
+.input {
+ font-size:10px;
+ color:#072F6F;
+ background-color: #ddd;
+ }
+*/
+/*
+autocomplete
+*/
+
+body
+{
+ position: relative;
+}
+
+ul.autosuggest
+{
+ position: absolute;
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ overflow-y: auto;
+}
+
+ul.autosuggest li.autosuggest
+{
+ text-align: left;
+ border-bottom: 1px solid #ccc;
+ border-left: 1px solid #ccc;
+ border-right: 1px solid #ccc;
+}
+
+ul.autosuggest li.autosuggest a:link,
+ul.autosuggest li.autosuggest a:visited
+{
+ display: block;
+ padding: 2px;
+ text-decoration: none;
+ background-color: #eee;
+}
+
+ul.autosuggest li.autosuggest a:hover,
+ul.autosuggest li.autosuggest a:active
+{
+ color: #fff;
+ background-color: #f30;
+}
+
+
+ul.autosuggest li.highlight a:link,
+ul.autosuggest li.highlight a:visited
+{
+ color: #fff;
+ background-color: #f30;
+}
+
+
+/*
+drop down
+*/
+/*
+#csstopmenu, #csstopmenu ul{
+ padding: 0;
+ margin: 0;
+ list-style: none;
+}
+
+#csstopmenu li{
+ float: left;
+ position: relative;
+}
+
+#csstopmenu a{
+ text-decoration: none;
+}
+
+.mainitems{
+ border: 0px solid black;
+ border-left-width: 0;
+ width: 96px;
+ height: 16px;
+ padding-top: 7px;
+ background-repeat: no-repeat;
+}
+
+.mainitems:hover{
+ /*border-left-width: 1;
+ background-repeat: no-repeat;
+}
+/*
+.headerlinks a{
+ display:block;
+ padding:3px;
+ margin-right:2px;
+ font-size: 14px;
+ font-weight: bold;
+ text-align:center;
+ color: black;
+ background: #eee;
+ border:1px solid #072F6F;
+
+}
+
+.headerlinks a:hover{
+ background: #ddd;
+}
+
+.submenus{
+ display: none;
+ width: 10em;
+ position: absolute;
+
+ background-color: #E4E6EA;
+ border: 1px solid #072F6F;
+}
+
+.submenus li{
+ margin-top: 2px;
+ margin-bottom: 2px;
+ font-size:14px;
+ margin-left:10px;
+}
+
+.submenus li a{
+ display: block;
+ color: black;
+}
+
+html>body .submenus li a{ /* non IE browsers
+ width: auto;
+}
+
+.submenus li a:hover{
+ background-color: #072F6F;
+ color: white;
+}
+
+#csstopmenu li>ul {/* non IE browsers
+ top: auto;
+ left: auto;
+}
+
+#csstopmenu li:hover ul, li.over ul {
+ display: block;
+}
+
+html>body #clearmenu{ /* non IE browsers
+ height: 3px;
+}
+*/
+/*
+ * TODO: CSS for PlanetLab admin pages.
+ * Must remain compatible with Drupal standard CSS names.
+ *
+ * Reid Moran
+ * Mark Huang
+ * Copyright (C) 2006-2007 The Trustees of Princeton University
+ *
+ * $Id: plc_style.css 918 2007-10-10 14:56:16Z thierry $
+ */
+
+/* Thierry : class used for showing foreign items */
+.plc-foreign {
+ background: #d0d0d0;
+}
+
+/* Thierry : class used for showing various warnings */
+.plc-warning {
+ background: orange;
+}
+*.plc-warning a:link { text-decoration: none; color:white }
+*.plc-warning a:visited { text-decoration: none; color:white }
+*.plc-warning a:hover { text-decoration: none; color:black }
diff --git a/planetlab/login.php b/planetlab/login.php
new file mode 100644
index 0000000..1f52057
--- /dev/null
+++ b/planetlab/login.php
@@ -0,0 +1,90 @@
+
+// Copyright (C) 2006 The Trustees of Princeton University
+//
+// $Id: login.php 179 2007-04-03 15:46:19Z thierry $ $
+//
+
+// Get session and API handles
+require_once 'plc_session.php';
+global $plc, $api;
+
+// Print header
+require_once 'plc_drupal.php';
+drupal_set_title('Login');
+include 'plc_header.php';
+
+if (!empty($_REQUEST['email']) &&
+ !empty($_REQUEST['password'])) {
+ $plc = new PLCSession($_REQUEST['email'], $_REQUEST['password']);
+
+ if ($plc->person) {
+ // Login admins to Drupal as the superuser
+ if (in_array('admin', $plc->person['roles']) &&
+ function_exists('user_load')) {
+ global $user;
+ $user = user_load(array('uid' => 1));
+ }
+
+ if (empty($_REQUEST['url'])) {
+ // XXX Redirect to default home page
+ Header("Location: /");
+ exit();
+ } else {
+ // Make sure that redirections are always local
+ $url = urldecode($_REQUEST['url']);
+ if ($url[0] != "/") {
+ $url = "/$url";
+ }
+ Header("Location: $url");
+ exit();
+ }
+ } else {
+ // XXX Use our own stylesheet instead of drupal.css
+ print '
+
+
+
+EOF;
+
+include 'plc_footer.php';
+
+?>
diff --git a/planetlab/logout.php b/planetlab/logout.php
new file mode 100644
index 0000000..f40226c
--- /dev/null
+++ b/planetlab/logout.php
@@ -0,0 +1,30 @@
+
+// Copyright (C) 2006 The Trustees of Princeton University
+//
+// $Id: logout.php 144 2007-03-28 07:52:20Z thierry $ $
+//
+
+// Get session and API handles
+require_once 'plc_session.php';
+global $plc, $api;
+
+// Print header
+require_once 'plc_drupal.php';
+drupal_set_title('Login');
+include 'plc_header.php';
+
+// Invalidate session
+if ($plc->person) {
+ $plc->logout();
+}
+
+// Destroy PHP session
+session_destroy();
+
+Header("Location: /db/login.php");
+
+?>
\ No newline at end of file
diff --git a/planetlab/nodes/add_node.php b/planetlab/nodes/add_node.php
new file mode 100644
index 0000000..61de309
--- /dev/null
+++ b/planetlab/nodes/add_node.php
@@ -0,0 +1,303 @@
+person;
+$_roles= $_person['role_ids'];
+
+// if not a admin, pi, or tech then redirect to node index
+if( !(in_array( 10, $plc->person['role_ids'] ) || in_array( 20, $plc->person['role_ids'] ) || in_array( 40, $plc->person['role_ids'] )) ) {
+ header( "index.php" );
+ }
+
+
+// this sets up which box is to be checked the first time the page is loaded
+$method= $_POST['method'];
+if( $method == "" )
+ $method= "static";
+
+$model= $_POST['model'];
+if( $model == "" )
+ $model= "Custom";
+
+
+$submitted = false;
+
+// if submitted validate and add
+if( $_POST['submitted'] )
+{
+ $submitted = true;
+
+ $errors= array();
+
+ $method = trim($_POST['method']);
+ $ip = trim($_POST['ip']);
+ $netmask = trim($_POST['netmask']);
+ $network = trim($_POST['network']);
+ $gateway = trim($_POST['gateway']);
+ $broadcast = trim($_POST['broadcast']);
+ $dns1 = trim($_POST['dns1']);
+ $dns2 = trim($_POST['dns2']);
+ $hostname = trim($_POST['hostname']);
+ $model= trim($_POST['model']);
+
+ // used to generate error strings for static fields only
+ $static_fields= array();
+ $static_fields['netmask']= "Netmask address";
+ $static_fields['network']= "Network address";
+ $static_fields['gateway']= "Gateway address";
+ $static_fields['broadcast']= "Broadcast address";
+ $static_fields['dns1']= "Primary DNS address";
+
+ if( $method == 'static' )
+ {
+ foreach( $static_fields as $field => $desc )
+ {
+ if( trim($_POST[$field]) == "" )
+ {
+ $errors[] = "$desc is required";
+ }
+ elseif( !is_valid_ip(trim($_POST[$field])) )
+ {
+ $errors[] = "$desc is not a valid address";
+ }
+ }
+
+ if( !is_valid_network_addr($network,$netmask) )
+ {
+ $errors[] = "The network address does not coorespond to the netmask";
+ }
+ }
+
+ if( $hostname == "" )
+ {
+ $errors[] = "Hostname is required";
+ }
+
+ if( $ip == "" )
+ {
+ $errors[] = "IP is required";
+ }
+
+ if( count($errors) == 0 )
+ {
+ $success= 1;
+
+ // add new node and its network
+ $optional_vals= array( "hostname"=>$hostname, "model"=>$model );
+
+ $site_id= $_person['site_ids'][0];
+
+ $node_id= $api->AddNode( intval( $site_id ), $optional_vals );
+
+ if ( $api->error() ) {
+ $errors[] = "Hostname already present or not valid";
+ $success= 0;
+ }
+ else
+ {
+ // now, try to add the network.
+ $optional_vals= array();
+ $optional_vals['is_primary']= true;
+ $optional_vals['ip']= $ip;
+ $optional_vals['type']= 'ipv4';
+ $optional_vals['method']= $method;
+
+ if( $method == 'static' )
+ {
+
+ $optional_vals['gateway']= $gateway;
+ $optional_vals['network']= $network;
+ $optional_vals['broadcast']= $broadcast;
+ $optional_vals['netmask']= $netmask;
+ $optional_vals['dns1']= $dns1;
+ if (!empty($dns2)) {
+ $optional_vals['dns2']= $dns2;
+ }
+ }
+
+ $nodenetwork_id= $api->AddNodeNetwork( $node_id, $optional_vals);
+ // if AddNodeNetwork fails, we have the node created,
+ // but no primary interface is present.
+ // The primary interface can be added later,
+ // but take a look at the possible Methods,
+ // if we specify TUN/TAP Method we will have
+ // an error on download of the configuration file
+ }
+ }
+
+}
+
+
+// Print header
+require_once 'plc_drupal.php';
+drupal_set_title('Nodes');
+include 'plc_header.php';
+
+
+?>
+
+
+
+
+Node Created
+
+The node has been successfully added.
+
+
View node details and download a configuration
+file here.
+
+
+}
+else
+{
+?>
+
+
Add A New Node
+
+This page will allow you to add a new machine to your site. This must
+be done before the machine is turned on, as it will allow you to download
+a configuration file when complete for this node.
+
+
Even for DHCP, you must enter the IP address of the node.
+
+ 0 )
+{
+ print( "
The following errors occured:" );
+ print( "\n" );
+ foreach( $errors as $err )
+ {
+ print( "- $err\n" );
+ }
+ print( "
\n" );
+}
+
+$self = $_SERVER['PHP_SELF'];
+if (!empty($_SERVER['QUERY_STRING'])) {
+ $self .= "?" . $_SERVER['QUERY_STRING'];
+}
+
+?>
+
+
+
+
diff --git a/planetlab/nodes/all_hosts.php b/planetlab/nodes/all_hosts.php
new file mode 100644
index 0000000..a838f14
--- /dev/null
+++ b/planetlab/nodes/all_hosts.php
@@ -0,0 +1,5 @@
+
diff --git a/planetlab/nodes/all_ips.php b/planetlab/nodes/all_ips.php
new file mode 100644
index 0000000..eeb5d7d
--- /dev/null
+++ b/planetlab/nodes/all_ips.php
@@ -0,0 +1,5 @@
+
diff --git a/planetlab/nodes/alpha_hosts.php b/planetlab/nodes/alpha_hosts.php
new file mode 100644
index 0000000..ee0f2eb
--- /dev/null
+++ b/planetlab/nodes/alpha_hosts.php
@@ -0,0 +1,5 @@
+
diff --git a/planetlab/nodes/alpha_ips.php b/planetlab/nodes/alpha_ips.php
new file mode 100644
index 0000000..c8e5b7f
--- /dev/null
+++ b/planetlab/nodes/alpha_ips.php
@@ -0,0 +1,5 @@
+
diff --git a/planetlab/nodes/beta_hosts.php b/planetlab/nodes/beta_hosts.php
new file mode 100644
index 0000000..6637731
--- /dev/null
+++ b/planetlab/nodes/beta_hosts.php
@@ -0,0 +1,5 @@
+
diff --git a/planetlab/nodes/beta_ips.php b/planetlab/nodes/beta_ips.php
new file mode 100644
index 0000000..081ce1a
--- /dev/null
+++ b/planetlab/nodes/beta_ips.php
@@ -0,0 +1,5 @@
+
diff --git a/planetlab/nodes/comon.php b/planetlab/nodes/comon.php
new file mode 100644
index 0000000..d049e71
--- /dev/null
+++ b/planetlab/nodes/comon.php
@@ -0,0 +1,122 @@
+GetNodes(array("node_id"=>array($node_id)),$fields);
+ } else if ($_GET['site_id']) {
+ $site_id=intval($_GET['site_id']);
+ $nodes=$api->GetNodes(array("site_id"=>array($site_id)),$fields);
+ } else if ($_GET['slice_id']) {
+ $slice_id=intval($_GET['slice_id']);
+ $return=$api->GetSlices(array("slice_id"=>array($slice_id)),array("node_ids"));
+ $node_ids=$return[0]['node_ids'];
+ $nodes=$api->GetNodes(array("node_id"=>$node_ids),$fields);
+ } else if (isset($_GET['peer_id'])) {
+ $peer_id=intval($_GET['peer_id']);
+ if ( ($peer_id == 0) || ($peer_id == "") )
+ $peer_id=NULL;
+ $nodes=$api->GetNodes(array("peer_id"=>$peer_id),$fields);
+ } else {
+ echo " Unexpected args in comon.php \n";
+ exit();
+ }
+
+// first pass
+// * gather nodenetwork_ids for local nodes
+// * gather hostnames for foreign nodes
+
+$nodenetwork_ids=array();
+$hostnames = array();
+
+foreach ($nodes as $node) {
+ if (empty($node['peer_id'])) {
+ foreach ($node['nodenetwork_ids'] as $id=>$nnid) {
+ $nodenetwork_ids [] = $nnid;
+ }
+ } else {
+ $hostnames[] = $node['hostname'];
+ }
+}
+
+// Gather local ips from primary interfaces
+// fetch primary nodenetworks
+$local_ips=array();
+$nns = $api->GetNodeNetworks(array("is_primary"=>TRUE,"nodenetwork_id"=>$nodenetwork_ids),
+ array("ip"));
+foreach ($nns as $nn) {
+ $local_ips[] = $nn['ip'];
+}
+
+plc_debug('locals',$local_ips);
+
+// for foreign hosts we're left with dns resolving them
+$remote_ips=array();
+foreach ($hostnames as $hostname) {
+ $resolved=gethostbyname($hostname);
+ // no way to notify this
+ if ($resolved == $hostname) {
+ } else {
+ $remote_ips[] = $resolved;
+ }
+}
+
+plc_debug('remote ips',$remote_ips);
+
+
+// add both lists
+$all_ips=$local_ips+$remote_ips;
+// compute comon URL
+$url = plc_comon_url_from_ips("http://comon.cs.princeton.edu",$all_ips);
+
+plc_debug('url',$url);
+
+// redirect to comon
+header("Location: " . $url);
+
+?>
diff --git a/planetlab/nodes/etc_hosts.php b/planetlab/nodes/etc_hosts.php
new file mode 100644
index 0000000..c3ee907
--- /dev/null
+++ b/planetlab/nodes/etc_hosts.php
@@ -0,0 +1,5 @@
+
diff --git a/planetlab/nodes/index.php b/planetlab/nodes/index.php
new file mode 100644
index 0000000..94e23f1
--- /dev/null
+++ b/planetlab/nodes/index.php
@@ -0,0 +1,519 @@
+person;
+$_roles= $_person['role_ids'];
+
+////////////////////
+// The set of columns to fetch
+// and the filter applied for fetching sites
+$columns = array( "node_id", "hostname", "boot_state", "peer_id" ) ;
+$filter = array();
+if ( in_array( '10', $_roles ) || in_array('20', $_roles) || in_array('40',$_roles)) {
+ // admins, PIs and techs can see nodenetwork details
+ $columns [] = "nodenetwork_ids";
+ }
+
+//////////////////
+// perform post-processing on objects as returned by GetNodes
+// performs sanity check and summarize the result in a single column
+// performs in-place replacement, so passes a reference
+function layout_node ($node) {
+
+ // we need the 'nodenetwork_ids' field to do this
+ // so regular users wont run this
+ if ( ! array_key_exists ('nodenetwork_ids', $node))
+ return $node;
+
+ $messages=array();
+
+ // do all this stuff on local nodes only
+ if ( ! $node['peer_id'] ) {
+ // check that the node has keys
+ if (count($node['nodenetwork_ids']) == 0)
+ $messages [] = "No nodenetwork";
+
+ }
+ // but always cleanup $node columns
+ unset ($node['nodenetwork_ids']);
+ $node['status'] = plc_make_table('plc-warning',$messages);
+ $node['comon'] = plc_comon_button("node_id",$node['node_id']);
+ return $node;
+}
+
+// if nodepattern is set then set id to that node's id.
+// we use GET rather than POST so paginate can display the right contents on subsequent pages
+// can be useful for writing bookmarkable URL's as well
+if( $_GET['nodepattern'] || $_GET['peerscope']) {
+ $nodepattern= $_GET['nodepattern'];
+ if (empty($nodepattern)) {
+ $nodepattern="*";
+ }
+ $filter = array_merge (array( "hostname"=>$nodepattern ), $filter);
+ switch ($_GET['peerscope']) {
+ case '':
+ $peer_label="all peers";
+ break;
+ case 'local':
+ $filter=array_merge(array("peer_id"=>NULL),$filter);
+ $peer_label="local peer";
+ break;
+ case 'foreign':
+ $filter=array_merge(array("~peer_id"=>NULL),$filter);
+ $peer_label="foreign peers";
+ break;
+ default:
+ $peer_id=intval($_GET['peerscope']);
+ $filter=array_merge(array("peer_id"=>$peer_id),$filter);
+ $peer=$api->GetPeers(array("peer_id"=>$peer_id));
+ $peer_label='peer "' . $peer[0]['peername'] . '"';
+ break;
+ }
+ // need to use a hash filter for patterns to be properly handled
+ $nodes= $api->GetNodes($filter, $columns);
+ $nodes_count = count ($nodes);
+ if ( $nodes_count == 1) {
+ header( "location: index.php?id=". $nodes[0]['node_id'] );
+ exit();
+ } else if ( $nodes_count == 0) {
+ echo " No node matching $nodepattern ";
+ } else {
+ drupal_set_title ("Nodes matching $nodepattern on". $peer_label);
+ $nodes = array_map(layout_node,$nodes);
+ sort_nodes ($nodes);
+ echo paginate( $nodes, "node_id", "Nodes", 25, "hostname");
+ }
+}
+// if a site_id is given, display the site nodes only
+else if( $_GET['site_id'] ) {
+
+ $site_id= $_GET['site_id'];
+
+ // Get site info
+ $site_info= $api->GetSites( array( intval( $site_id ) ), array( "name", "node_ids" ) );
+ drupal_set_title("Nodes on site " . $site_info[0]['name']);
+
+ // Get site nodes
+ $nodes= $api->GetNodes( array_merge(array('node_id'=>$site_info[0]['node_ids']),$filter), $columns);
+
+ if ( empty ($nodes) ) {
+ echo "No node to display";
+ } else {
+
+ $nodes = array_map(layout_node,$nodes);
+ sort_nodes( $nodes );
+
+ echo paginate( $nodes, "node_id", "Nodes", 25, "hostname");
+ }
+
+}
+// if a slice_id is given, display only the nodes related to this slice
+else if( $_GET['slice_id'] ) {
+
+ $slice_id= $_GET['slice_id'];
+
+ // Get slice infos
+ $slice_info= $api->GetSlices( array( intval( $slice_id ) ), array( "name", "node_ids" ) );
+ drupal_set_title($slice_info[0]['name']."run on");
+
+ // Get slice nodes
+ $nodes= $api->GetNodes( array_merge(array('node_id'=>$slice_info[0]['node_ids']),$filter), $columns);
+
+ if ( empty ($nodes) ) {
+ echo "No node to display";
+ } else {
+
+ $nodes = array_map(layout_node,$nodes);
+ sort_nodes( $nodes );
+
+ echo paginate( $nodes, "node_id", "Nodes", 25, "hostname");
+ echo "
GetNodes( empty($filter) ? NULL : $filter, $columns );
+
+ if ( empty ($nodes) ) {
+ echo "No node to display";
+ } else {
+
+ $nodes = array_map(layout_node,$nodes);
+ sort_nodes( $nodes );
+
+ drupal_set_html_head('
+
+ ');
+
+ echo "\n
+ \n
+
\n";
+
+
+echo paginate( $nodes, "node_id", "Nodes", 25, "hostname" );
+
+ echo "\n";
+ }
+}
+
+if ( $_GET['id'] ) {
+ // get the node id from the URL
+ $node_id= intval( $_GET['id'] );
+
+ // make the api call to pull that nodes DATA
+ $node_info= $api->GetNodes( array( $node_id ) );
+
+ if (empty ($node_info)) {
+ echo "No such node.";
+ } else {
+
+ // node info
+ $hostname= $node_info[0]['hostname'];
+ $boot_state= $node_info[0]['boot_state'];
+ $site_id= $node_info[0]['site_id'];
+ $model= $node_info[0]['model'];
+ $version= $node_info[0]['version'];
+
+ // arrays of ids of node info
+ $slice_ids= $node_info[0]['slice_ids'];
+ $conf_file_ids= $node_info[0]['conf_file_ids'];
+ $nodenetwork_ids= $node_info[0]['nodenetwork_ids'];
+ $nodegroup_ids= $node_info[0]['nodegroup_ids'];
+ $pcu_ids= $node_info[0]['pcu_ids'];
+ $ports= $node_info[0]['ports'];
+
+ // get peer
+ $peer_id= $node_info[0]['peer_id'];
+
+ // gets site info
+ $site_info= $api->GetSites( array( $site_id ) );
+ $site_name= $site_info[0]['name'];
+ $site_nodes= $site_info[0]['node_ids'];
+
+ if( !empty( $site_nodes ) ) {
+ // get site node info basics
+ $site_node_list= $api->GetNodes( $site_nodes );
+
+ foreach( $site_node_list as $s_node ) {
+ $site_node[$s_node['node_id']]= $s_node['hostname'];
+ }
+ }
+
+ // gets slice info for each slice
+ if( !empty( $slice_ids ) )
+ $slice_info= $api->GetSlices( $slice_ids, array( "slice_id", "name" , "peer_id" ) );
+
+ // gets conf file info
+ if( !empty( $conf_file_ids ) )
+ $conf_files= $api->GetConfFiles( $conf_file_ids );
+
+ // get node network info
+ if( !empty( $nodenetwork_ids ) )
+ $node_networks= $api->GetNodeNetworks( $nodenetwork_ids );
+
+ // gets nodegroup info
+ if( !empty( $nodegroup_ids ) )
+ $node_groups= $api->GetNodeGroups( $nodegroup_ids );
+
+ // xxx Thierry : disabling call to GetEvents, that gets the session deleted in the DB
+ // gets events
+ // $filter= array( "object_type"=>"Node", "object_id"=>$node_id );
+ // $fields= array( "event_id", "person_id", "fault_code", "call_name", "call", "message", "time" );
+ //
+ // $event_info= $api->GetEvents( $filter, $fields );
+
+ // gets pcu and port info key to both is $pcu_id
+ if( !empty( $pcu_ids ) )
+ $PCUs= $api->GetPCUs( $pcu_ids );
+
+
+ // display node info
+ if ( $peer_id) {
+ echo "";
+ }
+
+ drupal_set_title("Node " . $hostname . " details");
+
+ $is_admin = in_array( 10, $_roles );
+ $is_pi = in_array( 20, $_roles );
+ $is_tech = in_array( 40, $_roles );
+ $in_site = in_array( $site_id, $_person['site_ids'] );
+
+ // available actions
+ if ( ! $peer_id && ( $is_admin || ( ($is_pi||$is_tech) && $in_site ) ) ) {
+
+ // the javascript callback we set on the form; this
+ // (*) checks whether we clicked on 'delete'
+ // (*) in this case performs a javascript 'confirm'
+ // (*) then, notice that if we select delete, then cancel, we can select back 'Choose action'
+ // so submit only when value is not empty
+ $change='if (document.basic.action.value=="delete") if (! confirm("Are you sure you want to delete ' . $hostname . ' ? ") ) return false; if (document.basic.action.value!="") submit();';
+
+ echo "";
+ if ($is_admin) {
+ echo plc_event_button("Node","node",$node_id);
+ echo " ";
+ }
+ echo plc_comon_button("node_id",$node_id);
+ echo " ";
+ echo "\n";
+
+ echo "
";
+ }
+
+ echo "
";
+ echo "\n";
+
+ echo "Hostname: $hostname \n";
+ echo "Model: $model \n";
+ echo "Version: $version \n";
+
+ echo "Boot State: ";
+ if ($peer_id) {
+ echo $boot_state;
+ } else {
+ echo "";
+ }
+ echo " \n";
+
+ if ( ! $peer_id && ( $is_admin || ( ($is_pi||$is_tech) && $in_site ) ) ) {
+ // handle legacy API
+ if ( ! method_exists ($api,"GetBootMedium")) {
+ $new_api_only=" disabled='disabled' ";
+ }
+
+ echo "Download ";
+ echo "";
+ echo " \n";
+
+ }
+
+ // site info and all site nodes
+ echo " \n";
+ echo "Site: $site_name \n";
+ echo "All site nodes: ";
+ if (empty($site_node)) {
+ echo "Site has no node";
+ } else {
+ foreach( $site_node as $key => $val ) {
+ echo "$val
";
+ }
+ }
+ echo " \n";
+
+ echo "
\n";
+
+ if ( ! $peer_id ) {
+
+ echo "
\n";
+
+ // display node networks
+ if( $node_networks ) {
+ echo "\n";
+ echo "Node Networks \n";
+ echo "";
+ // placeholder for the delete buttons
+ if ( $is_admin || ($is_pi && $in_site)) {
+ echo " ";
+ }
+ echo "IP Address Method Type MAC Bandwidth Limit \n";
+
+ foreach( $node_networks as $node_network ) {
+ $nn_id= $node_network['nodenetwork_id'];
+ $nn_ip= $node_network['ip'];
+ $nn_broad= $node_network['broadcast'];
+ $nn_primary= $node_network['is_primary'];
+ $nn_network= $node_network['network'];
+ $nn_dns1= $node_network['dns1'];
+ $nn_dns2= $node_network['dns2'];
+ $nn_hostname= $node_network['hostname'];
+ $nn_netmaks= $node_network['netmask'];
+ $nn_gatewary= $node_network['gateway'];
+ $nn_mac= $node_network['mac'];
+ $nn_bwlimit= $node_network['bwlimit'];
+ $nn_type= $node_network['type'];
+ $nn_method= $node_network['method'];
+
+ echo "";
+ if ( $is_admin || ($is_pi && $in_site)) {
+ echo "";
+ if (!$nn_primary) {
+ echo plc_delete_link_button('node_networks.php?id=' . $nn_id . '&delete=1&submitted=1', '\\nNode Network ' . $nn_ip);
+ } else {
+ echo ' P ';
+ }
+ echo " ";
+ }
+ echo "";
+ if( $is_admin || $is_pi || $is_tech ) {
+ echo "$nn_ip";
+ } else {
+ echo " $nn_ip ";
+ }
+ echo "$nn_method $nn_type $nn_mac $nn_bwlimit \n";
+ }
+
+ echo "
\n";
+
+ } else {
+ echo "No Node Network. Please add a node network to make this a usable PLC node
.\n";
+ }
+
+ echo "
Add a node network.\n";
+ echo "
\n";
+ }
+
+ // display node group info
+ if( !empty( $node_groups ) ) {
+
+ echo "\nNode Groups \nName Description ";
+ if( in_array( 10, $_roles ) ) echo " ";
+ echo " \n";
+
+ foreach( $node_groups as $node_group ) {
+ echo "". $node_group['name'] ." ". $node_group['description'] ." ";
+
+ if( in_array( '10', $_roles ) || ( in_array( 20, $_roles ) && in_array( $site_id, $_person['site_ids'] ) ) || ( in_array( 40, $_roles ) && in_array( $site_id, $_person['site_ids'] ) ) )
+ echo "Update remove ";
+
+ echo " \n";
+
+ }
+ echo "
\n";
+
+ } else {
+ echo "This node is not in any nodegroup.
\n";
+ }
+
+ // select list for adding to node group
+ // get nodegroup info
+ $full_ng_info= $api->GetNodeGroups( NULL );
+ if( empty( $node_groups ) ) {
+ $person_ng= $full_ng_info;
+ } else {
+ $person_ng= arr_diff( $full_ng_info, $node_groups );
+ }
+
+ sort_nodegroups( $person_ng );
+
+ if( !empty( $person_ng ) ) {
+ echo "Select nodegroup to add this node to.
\n";
+ echo "\n";
+ }
+
+
+ // display slices
+ echo "
\n";
+ if( !empty( $slice_info ) ) {
+ sort_slices( $slice_info );
+ echo paginate( $slice_info, "slice_id", "Slices", 15, "name", "slivers", $node_id );
+ } else {
+ echo "This node is not associated to any slice.
\n";
+ }
+
+
+ // display events - disabled, see GetEvents above
+ if( !empty( $event_info ) ) {
+ echo "
\n";
+ echo "\nNode Events \nCall Name Call Message Time \n";
+
+ // display event on rows of table
+ foreach( $event_info as $event ) {
+ echo "". $event['call'] ." ". $event['message'] ." ". $event['time'] ." \n";
+ }
+ echo "
\n";
+ }
+
+ if ( $peer_id ) {
+ echo "";
+ }
+ }
+ if( $peer_id )
+ echo "
";
+
+ echo "
Back to nodes list";
+
+ }
+
+// Print footer
+include 'plc_footer.php';
+
+?>
diff --git a/planetlab/nodes/known_hosts.php b/planetlab/nodes/known_hosts.php
new file mode 100644
index 0000000..89a0a54
--- /dev/null
+++ b/planetlab/nodes/known_hosts.php
@@ -0,0 +1 @@
+
diff --git a/planetlab/nodes/node_actions.php b/planetlab/nodes/node_actions.php
new file mode 100644
index 0000000..945e259
--- /dev/null
+++ b/planetlab/nodes/node_actions.php
@@ -0,0 +1,452 @@
+ actual UpdateNode
+ // (POST) node_id=node_id ng_add=nodegroup_id
+ // AddNodeToNodeGroup
+ // (POST) boot_state=boot_state node_id=node_id
+ // (POST) ng_add=nodegroup_id
+ // (*) downloadconf.php
+ // (GET) id=node_id [download=1]
+ // either showed current status, or performed actual config download
+
+ // in this new version we support only POST behaviour with the following interface
+ // REQUIRED : node_id=node_id
+ // (*) action='prompt-update' : former node_update.php
+ // (*) action='update'
+ // hostname=
+ // model= : actually updated the node
+ // (*) action='boot-state'
+ // boot_state= : changes bootstate
+ // (*) action='delete' : deletes node
+ // (*) action='download-node-floppy' :
+ // (*) action='download-node-iso' :
+ // (*) action='download-node-usb' :
+ // : same as former downloadconf.php with download unset
+ // if in addition POST contains a non-empty field 'download' :
+ // : performs actual node-dep download
+ // (*) action='download-generic-iso':
+ // (*) action='download-generic-usb':
+ // : performs actual generic download
+ // (*) action='add-in-nodegroup': should include this for compatibility, replaces ng_add,
+ // though I could not figure where it's called
+
+
+ // TODO : manage a post field for displaying results of previous page
+ // see the special 'flash' in rails
+
+// delivering node-dependant images requires larger memory limit
+// trial and error, based on the current sizes
+// generic-ISO 43980800
+// generic-usb 44720128
+// 256M OK
+// 128M OK
+// 96M OK
+// 88M KO
+// 80M KO
+// 64M KO
+// Bottom line is, looks like we need in the order of twice the file size
+// so let's play it safe
+// Thierry - for 4.2, we need a larger area, was 100 for 4.1
+ini_set("memory_limit","150M");
+
+// Require login
+require_once 'plc_login.php';
+
+// Get session and API handles
+require_once 'plc_session.php';
+global $plc, $api;
+
+// Common functions
+require_once 'plc_functions.php';
+require_once 'plc_sorts.php';
+
+// find person roles
+$_person= $plc->person;
+$_roles= $_person['role_ids'];
+
+// NOTE: this function exits() after it completes its job,
+// simply returning leads to html decorations being added around the contents
+function deliver_and_unlink ($filename) {
+
+ // for security reasons we want to be able to unlink the resulting file once downloaded
+ // so simply redirecting through a header("Location:") is not good enough
+
+ $size= filesize($filename);
+
+ // headers
+ header ("Content-Type: application/octet-stream");
+ header ("Content-Transfer-Encoding: binary");
+ header ("Content-Disposition: attachment; filename=" . basename($filename) );
+ header ("Content-Length: " . $size );
+ // for getting IE to work properly
+ // from princeton cvs new_plc_www/planetlab/nodes/downloadconf.php 1.2->1.3
+ header ("Pragma: hack");
+ header ("Cache-Control: public, must-revalidate");
+
+ // outputs the whole file contents
+ print (file_get_contents($filename));
+
+ // unlink the file
+ if (! unlink ($filename) ) {
+ // cannot unlink, but how can we notify this ?
+ // certainly not by printing
+ }
+ exit();
+}
+
+function show_download_confirm_button($api, $node_id, $action, $can_gen_config, $show_details) {
+
+ if( $can_gen_config ) {
+ if ($show_details && method_exists($api,"GetBootMedium")) {
+ $preview=$api->GetBootMedium($node_id,"node-preview","");
+ print ("
Current node configuration contents
");
+ print ("\n$preview
\n");
+ print ("
");
+ }
+ $action_labels = array ('download-node-floppy' => 'textual node config (for floppy)' ,
+ 'download-node-iso' => 'ISO image',
+ 'download-node-usb' => 'USB image' );
+
+ $format = $action_labels [ $action ] ;
+ print( "\n" );
+ } else {
+ echo "Configuration file cannot be created until missing values above are updated.";
+ }
+}
+
+// check arguments
+
+if (empty($_POST['node_id'])) {
+ header ('location:index.php');
+ exit();
+ } else {
+ $node_id = intval($_POST['node_id']);
+}
+
+$action=$_POST['action'];
+
+switch ($action) {
+
+ // ACTION: prompt-update
+ // from former node_update.php
+ case "prompt-update":
+
+ require_once('plc_drupal.php');
+ $node_info= $api->GetNodes( array( $node_id ), array( "hostname", "model" ) );
+ drupal_set_title("Updating " . $node_info[0]['hostname']);
+ include 'plc_header.php';
+
+ // start form
+ echo "
\n";
+
+ echo "Back to Node\n";
+
+ break;
+
+ // ACTION: update
+ // from former node_actions.php
+ case "update":
+
+ $hostname= $_POST['hostname'];
+ $model= $_POST['model'];
+
+ $fields= array( "hostname"=>$hostname, "model"=>$model );
+ $api->UpdateNode( intval( $node_id ), $fields );
+ $error= $api->error();
+
+ if( empty( $error ) ) {
+ header( "location: index.php?id=$node_id" );
+ exit();
+ } else {
+ echo "". $error . "\n" ;
+ echo "
Press back to retry
";
+ }
+
+ break;
+
+ // ACTION: delete
+ // from former node_actions.php
+ case "delete":
+ $api->DeleteNode( intval( $node_id ) );
+ header( "location: index.php" );
+ exit();
+ break;
+
+ // ACTION: boot-state
+ // from former node_actions.php
+ case "boot-state":
+ switch( $_POST['boot_state'] ) {
+ case 'boot':
+ case 'dbg':
+ case 'inst':
+ case 'rins':
+ case 'rcnf':
+ case 'new':
+ $api->UpdateNode( intval( $node_id ), array( "boot_state" => $_POST['boot_state'] ) );
+ header( "location: index.php?id=$node_id" );
+ exit();
+ break;
+ default:
+ print " no such boot state " . $_POST['boot_state'] . "";
+ break;
+ }
+ break;
+
+ // ACTION: download-generic
+ case "download-generic-iso":
+ case "download-generic-usb":
+
+ if ( ! method_exists($api,"GetBootMedium")) {
+ print (" API lacks support for GetBootMedium ");
+ return;
+ }
+
+ if ($action=="download-generic-iso") {
+ $boot_action="generic-iso";
+ } else {
+ $boot_action="generic-usb";
+ }
+
+ // place the result in a random-named sub dir for clear filenames
+ $filename = $api->GetBootMedium ($node_id, $boot_action, "%d/%n-%p-%v%s");
+ $error=$api->error();
+ // NOTE. for some reason, GetBootMedium sometimes does not report an error but the
+ // file is not created - this happens e.g. when directory owmer/modes are wrong
+ // in this case we get an empty filename
+ // see /etc/httpd/logs/error_log in this case
+ if (empty($error) && empty($filename)) {
+ $error="Unexpected error from GetBootMedium - probably wrong directory modes";
+ }
+ if (! empty($error)) {
+ print (" $error \n");
+ print ("Back to node \n");
+ return ;
+ } else {
+ deliver_and_unlink ($filename);
+ exit();
+ }
+ break;
+
+ // ACTION: download-node
+ // from former downloadconf.php
+
+ case "download-node-floppy":
+ case "download-node-iso":
+ case "download-node-usb":
+
+ $return= $api->GetNodes( array( $node_id ) );
+ $node_detail= $return[0];
+
+ // non-admin people need to be affiliated with the right site
+ if( !in_array( 10, $_roles ) ) {
+ $node_site_id = $node_detail['site_id'];
+ $in_site = in_array ($node_site_id,$_person['site_ids']);
+ if( ! $in_site) {
+ $error= "Insufficient permission. You cannot create configuration files for this node.";
+ }
+ }
+
+ $hostname= $node_detail['hostname'];
+ $return= $api->GetNodeNetworks( array( "node_id" => $node_id ), NULL );
+
+ $can_gen_config= 1;
+ $has_primary= 0;
+
+ if( count($return) > 0 ) {
+ foreach( $return as $node_network_detail ) {
+ if( $node_network_detail['is_primary'] == true ) {
+ $has_primary= 1;
+ break;
+ }
+ }
+ }
+
+ if( !$has_primary ) {
+ $can_gen_config= 0;
+ } else {
+ if( $node_detail['hostname'] == "" ) {
+ $can_gen_config= 0;
+ $node_detail['hostname']= "Missing";
+ }
+
+ $fields= array("method","ip");
+ foreach( $fields as $field ) {
+ if( $node_network_detail[$field] == "" ) {
+ $can_gen_config= 0;
+ $node_network_detail[$field]= "Missing";
+ }
+ }
+
+ if( $node_network_detail['method'] == "static" ) {
+ $fields= array("gateway","netmask","network","broadcast","dns1");
+ foreach( $fields as $field ) {
+ if( $node_network_detail[$field] == "" ) {
+ $can_gen_config= 0;
+ $node_network_detail[$field]= "Missing";
+ }
+ }
+ }
+
+ if( $node_network_detail['method'] != "static"
+ && $node_network_detail['method'] != "dhcp" ) {
+ $can_gen_config= 0;
+ $node_network_detail['method']= "Unknown method";
+ }
+ }
+
+ $download= $_POST['download'];
+
+ if( $can_gen_config && !empty($download) ) {
+ if (! method_exists ($api,"GetBootMedium")) {
+ $file_contents= $api->AdmGenerateNodeConfFile( $node_id );
+ header ("Content-Type: application/octet-stream");
+ header ("Content-Transfer-Encoding: binary");
+ header ("Content-Disposition: attachment; filename=plnode.txt" );
+ header ("Content-Length: " . strlen(file_contents));
+ header ("Pragma: hack");
+ header ("Cache-Control: public, must-revalidate");
+ print file_contents;
+ exit();
+ } else {
+ switch ($action) {
+ case 'download-node-floppy':
+ $boot_action='node-floppy';
+ $location = "%d/%n-%v-rename-into-plnode%s";
+ break;
+ case 'download-node-iso':
+ $boot_action='node-iso';
+ $location = "%d/%n-%v%s";
+ break;
+ case 'download-node-usb':
+ $boot_action='node-usb';
+ $location = "%d/%n-%v%s";
+ break;
+ }
+
+ $filename=$api->GetBootMedium($node_id,$boot_action,$location);
+ $error=$api->error();
+ if (empty($error) && empty($filename)) {
+ $error="Unexpected error from GetBootMedium - probably wrong directory modes";
+ }
+ if (! empty($error)) {
+ print ("
$error \n");
+ print ("Back to node \n");
+ return ;
+ } else {
+ deliver_and_unlink ($filename);
+ exit();
+ }
+ }
+
+ }
+
+ drupal_set_title("Download boot material for $hostname");
+
+ $header= <<In order to create a configuration file for this node using this page,
+all the node network settings must be up to date. Below is summary of these
+values. Any missing values must be entered before this can be used.
+
+EOF;
+
+ echo $header;
+
+ show_download_confirm_button($api, $node_id, $action, $can_gen_config, false);
+ print ("");
+ print ("
Current node network settings
\n");
+
+if( $has_primary ) {
+ print( "\n" );
+
+ print( "Node Details " );
+ print( "node_id: " );
+ print( "$node_id \n" );
+ print( "Hostname: " );
+ print( "" . $node_detail['hostname'] . " \n" );
+
+ $nn_id = $node_network_detail['nodenetwork_id'];
+ print( "Node Network Details " );
+
+ print( "Method: " );
+ print( "" . $node_network_detail['method'] . " \n" );
+ print( "IP: " );
+ print( "" . $node_network_detail['ip'] . " \n" );
+
+ if( $node_network_detail['method'] == "static" ) {
+ print( "Gateway: " );
+ print( "" . $node_network_detail['gateway'] . " \n" );
+ print( "Network mask: " );
+ print( "" . $node_network_detail['netmask'] . " \n" );
+ print( "Network address: " );
+ print( "" . $node_network_detail['network'] . " \n" );
+ print( "Broadcast address: " );
+ print( "" . $node_network_detail['broadcast'] . " \n" );
+ print( "DNS 1: " );
+ print( "" . $node_network_detail['dns1'] . " \n" );
+ print( "DNS 2: " );
+ if( $node_network_detail['dns2'] == "" ) {
+ print( "Optional, missing \n" );
+ } else {
+ print( "" . $node_network_detail['dns2'] . " \n" );
+ }
+ }
+
+ if (method_exists ($api,'GetNodeNetworkSettings')) {
+ print ("Additional Settings \n");
+ $nn_id = $node_network_detail['nodenetwork_id'];
+ $settings=$api->GetNodeNetworkSettings(array("nodenetwork_id" => array($nn_id)));
+ foreach ($settings as $setting) {
+ $category=$setting['category'];
+ $name=$setting['name'];
+ $value=$setting['value'];
+ print (" $category $name $value \n");
+ }
+ }
+
+ print( "
\n" );
+} else {
+ print( "This node has no configured primary network.
\n" );
+}
+
+ show_download_confirm_button($api, $node_id, $action, $can_gen_config, true);
+ break;
+
+ default:
+ echo "Unkown action <$action>.";
+ header("location:index.php?id=" . $node_id);
+ exit();
+ break;
+ }
+
+?>
diff --git a/planetlab/nodes/node_groups.php b/planetlab/nodes/node_groups.php
new file mode 100644
index 0000000..aad5f18
--- /dev/null
+++ b/planetlab/nodes/node_groups.php
@@ -0,0 +1,302 @@
+person;
+$_roles= $_person['role_ids'];
+
+
+// if sent here from another page remove then redirect
+if( $_GET['remove'] && $_GET['nodegroup_id'] ) {
+ $ng_id= $_GET['nodegroup_id'];
+ $node_id= $_GET['remove'];
+
+ $api->DeleteNodeFromNodeGroup( intval( $node_id ), intval( $ng_id ) );
+
+ header( "location: index.php?id=$node_id" );
+ exit();
+
+}
+
+
+// Print header
+require_once 'plc_drupal.php';
+drupal_set_title('Node Groups');
+include 'plc_header.php';
+
+
+// if no id display list of nodegroups
+if( !$_GET['id'] && !$_GET['nodegroup_id'] ) {
+ $nodegroup_info= $api->GetNodeGroups( NULL, array( "nodegroup_id", "name", "description" ) );
+
+ echo "Node Groups
\n
+ Name Description ";
+
+ // if admin we need to more cells
+ if( in_array( "10", $_person['role_ids'] ) )
+ echo " ";
+ echo " ";
+
+ foreach( $nodegroup_info as $type ) {
+ echo "". $type['name'] ." ". $type['description'] ." ";
+ // if admin display edit/delet links
+ if( in_array( "10", $_person['role_ids'] ) ) {
+ echo "Edit ";
+ echo plc_delete_link_button('node_groups.php?del_type=' . $type['nodegroup_id'],
+ $type['name']);
+ echo "";
+ }
+ echo " \n";
+
+ }
+
+ echo "
\n";
+
+}
+// if id is set then show nodegroup info
+elseif( $_GET['id'] ) {
+ $nodegroup_id= $_GET['id'];
+
+ $nodegroup_info= $api->GetNodeGroups( array( intval( $nodegroup_id ) ), array( "name", "nodegroup_id", "node_ids" ) );
+ $node_info= $api->GetNodes( $nodegroup_info[0]['node_ids'], array( "node_id", "hostname" ) );
+
+ //display info
+ echo "Node Group ". $nodegroup_info[0]['name'] ."
\n";
+
+ if( empty( $nodegroup_info[0]['node_ids'] ) )
+ echo "No nodes in node group.";
+ else {
+ echo "
Hostname ";
+
+ // if admin need more cells
+ if( in_array( 10, $_roles ) )
+ echo "Remove ";
+
+ echo " \n";
+
+ foreach( $node_info as $node ) {
+ echo "". $node['hostname'] ." ";
+
+ if( in_array( 10, $_roles ) )
+ echo "remove ";
+
+ echo " ";
+
+ }
+
+ echo "
\n";
+
+ }
+
+}
+// if no id add else update
+elseif( $_GET['add'] ) {
+ // add node group and redirect to update nodes for it
+ if( $_POST['add_sub'] ) {
+ $name= $_POST['name'];
+ $description= $_POST['description'];
+
+ $fields= array( 'name'=>$name, 'description'=>$description );
+
+ // add it
+ $api->AddNodeGroup( $fields );
+
+ // get nodegroup_id
+ $group_info= $api->GetNodeGroups( array( $name ), array( "nodegroup_id" ) );
+
+ // redirect
+ header( "location: node_groups.php?id=". $group_info[0]['nodegroup_id'] );
+ exit();
+
+ }
+
+ // add form
+ echo "Back to Node Index\n";
+
+}
+elseif( $_GET['nodegroup_id'] )
+{
+ // get node group id
+ $node_group_id= $_GET['nodegroup_id'];
+
+ // if add node submitted, add
+ if( $_POST['add'] )
+ {
+ $add_nodes= $_POST['add_nodes'];
+
+ // add nodes to node group
+ foreach( $add_nodes as $add_node )
+ {
+ $api->AddNodeToNodeGroup( intval( $add_node ), intval( $node_group_id ) );
+
+ }
+
+ }
+
+ // if remove node submitted, remove
+ if( $_POST['remove'] )
+ {
+ $rem_nodes= $_POST['rem_nodes'];
+
+ // remove nodes from node group
+ foreach( $rem_nodes as $rem_node )
+ {
+ $api->DeleteNodeFromNodeGroup( intval( $rem_node ), intval( $node_group_id ) );
+
+ }
+
+ }
+
+ // update name and description
+ $name= $_POST['name'];
+ $description= $_POST['description'];
+
+ $fields= array();
+
+ if( $name )
+ $fields['name']= $name;
+
+ if( $description )
+ $fields['description']= $description;
+
+ // api call
+ if( !empty( $fields ) )
+ $api->UpdateNodeGroup( intval( $node_group_id ), $fields );
+
+ // get node_group info
+ $group_info= $api->GetNodeGroups( array( intval( $node_group_id ) ), array( "node_ids", "name", "conf_file_ids", "description" ) );
+
+ $node_ids = $group_info[0]['node_ids'];
+ $name = $group_info[0]['name'];
+ $conf_file_ids = $group_info[0]['conf_file_ids'];
+ $description = $group_info[0]['description'];
+
+ // get node info
+ if( !empty( $node_ids ) )
+ $node_info= $api->GetNodes( $node_ids, array( "hostname", "node_id" ) );
+
+ // get site names and ids
+ $site_info= $api->GetSites( NULL, array( "site_id", "name" ) );
+ sort_sites( $site_info );
+
+ // if site_id is in post use it, if not use the user's primary
+ if( $_POST['site_id'] )
+ $site_id= $_POST['site_id'];
+ else
+ $site_id= $_person['site_ids'][0];
+
+ // get site nodes for $site_id
+ $sid= intval( $site_id );
+ $site_node_info= $api->GetSites( array( $sid ), array( "node_ids" ) );
+ $site_nodes= $site_node_info[0]['node_ids'];
+
+
+ // gets all node_ids from site that arent already associated with the node group
+ foreach( $site_nodes as $snode) {
+ if( !in_array( $snode, $node_ids ) )
+ $snodes[]= $snode;
+
+ }
+
+ // Get node info from new list
+ if( !empty( $snodes ) )
+ $snode_info= $api->GetNodes( $snodes, array( "hostname", "node_id" ) );
+
+
+ // start form
+ echo "\n";
+
+ echo "
Back to Node Group Index\n";
+
+
+}
+
+
+// Print footer
+include 'plc_footer.php';
+
+?>
diff --git a/planetlab/nodes/node_networks.php b/planetlab/nodes/node_networks.php
new file mode 100644
index 0000000..4b7ef71
--- /dev/null
+++ b/planetlab/nodes/node_networks.php
@@ -0,0 +1,245 @@
+person;
+$_roles= $_person['role_ids'];
+
+$nodenetwork = array();
+
+// If nodenetwork_id is specified, load data
+if( isset( $_GET['id'] ) ) {
+ $id= intval( $_GET['id'] );
+ $nodenetworks= $api->GetNodeNetworks( array( $id ) );
+ if( $nodenetworks ) {
+ $nodenetwork= $nodenetworks[0];
+ $node_id= $nodenetwork['node_id'];
+ }
+}
+
+if( $_GET['node_id'] )
+ $node_id= $_GET['node_id'];
+
+// Override fields with specified data
+foreach( array( 'method', 'type', 'ip', 'gateway', 'network', 'broadcast', 'netmask', 'dns1', 'dns2', 'hostname', 'mac', 'bwlimit', 'node_id' ) as $field ) {
+ if( isset( $_POST[$field] ) ) {
+ if( $_POST[$field] == "" ) {
+ $nodenetwork[$field]= NULL;
+ } else {
+ $nodenetwork[$field]= $_POST[$field];
+ if( in_array( $field, array( 'bwlimit', 'node_id' ) ) ) {
+ $nodenetwork[$field]= intval( $nodenetwork[$field] );
+ }
+ }
+ }
+ if( isset( $nodenetwork[$field] ) ) {
+ // E.g., $method = $nodenetwork['method'];
+ $$field= $nodenetwork[$field];
+ }
+}
+
+// Either nodenetwork_id or node_id must be specified in URL
+if( !isset( $_GET['node_id'] ) && !( $nodes= $api->GetNodes( array( intval($node_id) ), array( 'node_id', 'hostname', 'site_id' ) ) ) ) {
+ Header( "Location: index.php" );
+ exit();
+}
+
+
+$nodes= $api->GetNodes( array( intval($node_id) ), array( 'node_id', 'hostname', 'site_id' ) );
+$node= $nodes[0];
+
+$can_update= True;
+if( !in_array( 10, $_roles ) ) {
+ if ( !( in_array( 20, $_roles ) || in_array( 40, $_roles ) ) || !in_array( $node['site_id'], $_person['site_ids'] ) ) {
+ $can_update= False;
+ }
+}
+
+if( $can_update && (isset( $_POST['submitted'] ) || isset ($_GET['submitted'])) ) {
+ if( isset( $_POST['add'] ) ) {
+ $api->AddNodeNetwork( intval( $node_id ), $nodenetwork );
+ }
+ elseif ( isset( $_POST['delete'] ) || isset( $_GET['delete']) || isset( $_POST['update'] ) ) {
+ // nodenetwork_id must be specified in URL
+ if( !isset( $id ) ) {
+ Header( "Location: index.php?id=$node_id" );
+ exit();
+ }
+ if( isset( $_POST['delete'] ) || isset ($_GET['delete']) ) {
+ $api->DeleteNodeNetwork( $id );
+ }
+ elseif( isset( $_POST['update'] ) ) {
+ $api->UpdateNodeNetwork( $id, $nodenetwork );
+ }
+ }
+
+ $error= $api->error();
+
+ if( !empty( $error ) ) {
+ echo '' . $error . '.';
+ } else {
+ Header( "Location: index.php?id=$node_id" );
+ exit();
+ }
+
+}
+
+// Print header
+require_once 'plc_drupal.php';
+drupal_set_title($node['hostname']);
+include 'plc_header.php';
+
+// Start form
+$action= "node_networks.php";
+if( isset( $id ) ) {
+ $action.= "?id=" . $nodenetwork['nodenetwork_id'];
+}
+elseif( isset($node_id)) {
+ $action.= "?node_id=" . $node_id;
+}
+
+foreach( array( 'static', 'dhcp', 'proxy', 'tap', 'ipmi' ) as $option ) {
+ ${$option . "_selected"} = ( $method == $option ) ? 'selected="selected"' : '';
+}
+
+// XXX Query methods and types
+echo <<
+function updateStaticFields()
+{
+ var is_static= document.fm.method.options[document.fm.method.selectedIndex].text == 'Static';
+ var is_tap= document.fm.method.options[document.fm.method.selectedIndex].text == 'TUN/TAP';
+
+ document.fm.netmask.disabled= !is_static;
+ document.fm.network.disabled= !is_static;
+ document.fm.gateway.disabled= !is_static && !is_tap;
+ document.fm.broadcast.disabled= !is_static;
+ document.fm.dns1.disabled= !is_static;
+ document.fm.dns2.disabled= !is_static;
+}
+
+
+
+EOF;
+
+// displays related settings, if supported by the API
+if (method_exists ($api,'GetNodeNetworkSettings')) {
+
+ $is_admin=in_array( 10, $_roles );
+ $is_pi=in_array( 20, $_roles );
+ print "
";
+
+ if (empty ($nodenetwork['nodenetwork_setting_ids'])) {
+ print " This network interface has no additional setting
";
+ if( $is_admin || $is_pi )
+ echo "\n";
+ } else {
+ $nodenetwork_settings = $api->GetNodeNetworkSettings($nodenetwork['nodenetwork_setting_ids']);
+ sort_nodenetwork_settings ($nodenetwork_settings);
+ print "Additional Settings ";
+ print "";
+ // the column for the delete button
+ if( $is_admin )
+ print " ";
+ print "Name Category Description Value ";
+ foreach ($nodenetwork_settings as $setting) {
+ echo "";
+ if ($is_admin) {
+ echo("");
+ echo plc_delete_link_button('setting_action.php?rem_id=' . $setting['nodenetwork_setting_id'],
+ '\\n [ ' . $setting['name'] . ' = ' . $setting['value']);
+ echo(" ");
+ }
+ if ($is_admin || $is_pi)
+ printf (" %s ",$setting['nodenetwork_setting_id'],$setting['name']);
+ else
+ printf (" %s ",$setting['name']);
+ printf (" %s %s %s ",
+ $setting['category'],
+ $setting['description'],
+ $setting['value']);
+ }
+ if( $is_admin || $is_pi )
+ echo "Add a Network Setting \n";
+
+ print "
";
+ }
+ }
+
+echo << Back to Node
+
+EOF;
+
+// Print footer
+include 'plc_footer.php';
+
+?>
diff --git a/planetlab/nodes/production_hosts.php b/planetlab/nodes/production_hosts.php
new file mode 100644
index 0000000..e384416
--- /dev/null
+++ b/planetlab/nodes/production_hosts.php
@@ -0,0 +1,5 @@
+
diff --git a/planetlab/nodes/production_ips.php b/planetlab/nodes/production_ips.php
new file mode 100644
index 0000000..e5ee49e
--- /dev/null
+++ b/planetlab/nodes/production_ips.php
@@ -0,0 +1,5 @@
+
diff --git a/planetlab/nodes/setting_action.php b/planetlab/nodes/setting_action.php
new file mode 100644
index 0000000..3522148
--- /dev/null
+++ b/planetlab/nodes/setting_action.php
@@ -0,0 +1,131 @@
+person;
+$_roles= $_person['role_ids'];
+
+//plc_debug('GET',$_GET);
+//plc_debug('POST',$_POST);
+
+// attribute type updates
+if( $_POST['edit_type'] ) {
+ $setting_type_id= intval( $_POST['nodenetwork_setting_type_id'] );
+ $setting_type = array ('category' => $_POST['category'],
+ 'name' => $_POST['name'],
+ 'min_role_id' => intval( $_POST['min_role_id'] ),
+ 'description' => $_POST['description']);
+
+ // Update it!
+ $api->UpdateNodeNetworkSettingType( $setting_type_id, $setting_type );
+ $api_error=$api->error();
+ if (!empty($api_error)) {
+ print "" . $api_error . "";
+ }
+
+ header( "location: settings.php" );
+ exit();
+}
+
+// attribute type adds
+if( $_POST['add_type'] ) {
+ $setting_type = array ('category' => $_POST['category'],
+ 'name' => $_POST['name'],
+ 'min_role_id' => intval( $_POST['min_role_id'] ),
+ 'description' => $_POST['description']);
+ // add it!!
+ $api->AddNodeNetworkSettingType( $setting_type );
+
+ header( "location: settings.php" );
+ exit();
+}
+
+
+// attribute deletion
+if( $_GET['rem_id'] ) {
+ // get the id of the attrib to remove from GET
+ $setting_id= intval( $_GET['rem_id'] );
+
+ // get nodenetwork_id
+ $setting= $api->GetNodeNetworkSettings( array( $setting_id ), array( "nodenetwork_id" ) );
+ $nodenetwork_id= $setting[0]['nodenetwork_id'];
+
+ // delete the attribute
+ $api->DeleteNodeNetworkSetting( $setting_id );
+
+ header( "location: node_networks.php?id=$nodenetwork_id" );
+ exit();
+}
+
+// attribute adds
+if( $_POST['add_setting'] ) {
+ // get the nodenetwork_id, attribute_type_id, and value from POST
+ $nodenetwork_id= intval( $_POST['nodenetwork_id'] );
+ $nodenetwork_setting_type_id= intval( $_POST['nodenetwork_setting_type_id'] );
+ $value= $_POST['value'];
+
+ // add it!
+ $api->AddNodeNetworkSetting( $nodenetwork_id, $nodenetwork_setting_type_id, $value );
+
+ header( "location: node_networks.php?id=$nodenetwork_id" );
+ exit();
+}
+
+// attribute updates
+if( $_POST['edit_setting'] ) {
+ // get the id of the setting to update and the value from POST
+ $setting_id= intval( $_POST['setting_id'] );
+ $value= $_POST['value'];
+ $nodenetwork_id= $_POST['nodenetwork_id'];
+
+ // update it!
+ $api->UpdateNodeNetworkSetting($setting_id, $value );
+
+ header( "location: node_networks.php?id=$nodenetwork_id" );
+ exit();
+}
+
+// down here is some codqe from attrib_action.php that was not converted yet
+// Settings -------------------------------------------------
+
+// ATTRIBUTE TYPES ---------------------------------------------------
+
+// delete attribute types
+if( $_GET['del_type'] ) {
+ // get vars
+ $type_id= intval( $_GET['del_type'] );
+
+ // delete it!
+ $api->DeleteNodeNetworkSettingType( $type_id );
+
+ header( "location: settings.php" );
+ exit();
+}
+
+
+
+/*
+// Print footer
+include 'plc_footer.php';
+*/
+
+?>
diff --git a/planetlab/nodes/settings.php b/planetlab/nodes/settings.php
new file mode 100644
index 0000000..4d6c76a
--- /dev/null
+++ b/planetlab/nodes/settings.php
@@ -0,0 +1,234 @@
+person;
+$_roles= $_person['role_ids'];
+
+//plc_debug("person", $_person );
+
+$columns=array( "nodenetwork_setting_type_id", "category", "name", "description", "min_role_id" );
+
+// prepare dict role_id => role_name
+global $roles;
+$roles= $api->GetRoles();
+global $roles_id_to_name;
+$roles_id_to_name=array();
+foreach ($roles as $role) {
+ $roles_id_to_name[$role['role_id']] = $role['name'];
+}
+
+// compute person's smallest role
+global $person_role;
+$person_role=50;
+foreach ($_person['role_ids'] as $role_id) {
+ if ($role_id < $person_role) {
+ $person_role=$role_id;
+ }
+}
+//plc_debug("person_role",$person_role);
+
+// post-process results from GetNodeNetworkSettingTypes
+// with planetlab 4.2, we've moved to php-5.2
+// with the former 5.0 reelase, I could invoke array_map
+// with a function that took a reference and could do side-effects
+// Now I have to return the copy...
+// this new way of doing things might require more memory
+// on the other hand we should move to a schema where
+// pagination is done in the API, so it's no big deal hopefully
+function layout_setting_type ($setting_type) {
+ // replace role_id with name
+ global $roles_id_to_name;
+ $setting_type['min_role']=$roles_id_to_name[$setting_type['min_role_id']];
+ return $setting_type;
+}
+
+// if no id, display list of attributes types
+if( !$_GET['id'] && !$_GET['add'] && !$_GET['add_type'] && !$_GET['edit_type'] ) {
+ // get types
+ global $person_role;
+ $filter = array (']min_role_id'=>$person_role);
+ $setting_types= $api->GetNodeNetworkSettingTypes( $filter, $columns );
+ $setting_types = array_map(layout_setting_type,$setting_types);
+ sort_nodenetwork_settings ($setting_types);
+
+ // list them
+
+ echo "";
+ echo "";
+ // if admin we need one more cells for delete links
+ if( in_array( "10", $_person['role_ids'] ) )
+ echo " ";
+ $role_header="min Role
";
+ echo "Name ";
+ echo "Category ";
+ echo "" . $role_header . " ";
+ echo "Id ";
+ echo "Description ";
+ echo " ";
+ echo "";
+
+ foreach( $setting_types as $type ) {
+ echo "";
+ // if admin display delete links
+ if( in_array( "10", $_person['role_ids'] ) ) {
+ echo "";
+ echo plc_delete_link_button('setting_action.php?del_type='. $type['nodenetwork_setting_type_id'],
+ $type['name']);
+ echo " ";
+ }
+ // if admin, the name is a link to edition
+ if (in_array( "10", $_person['role_ids'])) {
+ echo "" . $type['name'] . " ";
+ } else {
+ echo "" . $type['name'] . " ";
+ }
+ echo "" . $type['category'] . " ";
+ echo "" . $type['min_role'] . " " . $type['min_role_id'] . " " . $type['description'] . " ";
+ echo " \n";
+ }
+
+ if( in_array( "10", $_person['role_ids'] ) )
+ echo "Add a Setting Type ";
+
+ echo "
\n";
+
+
+ // back link o nodes
+ echo "Back to Nodes\n";
+
+}
+elseif( $_GET['add_type'] || $_GET['edit_type'] ) {
+ // if its edit get the attribute info
+ if( $_GET['edit_type'] ) {
+ $type_id= intval( $_GET['edit_type'] );
+ $type= $api->GetNodeNetworkSettingTypes( array( $type_id ) );
+
+ $category=$type[0]['category'];
+ $name= $type[0]['name'];
+ $min_role_id= $type[0]['min_role_id'];
+ $description= $type[0]['description'];
+
+ }
+
+ // display form for setting types
+ echo "
\n";
+
+ echo "Back to Setting Types\n";
+}
+elseif( $_GET['add'] ) {
+
+ // get nodenetwork id from GET
+ $nodenetwork_id= intval( $_GET['add'] );
+
+ // get all setting types
+ global $person_role;
+ $filter = array (']min_role_id'=>$person_role);
+ $setting_types= $api->GetNodeNetworkSettingTypes( $filter, array( "nodenetwork_setting_type_id", "name" , "category") );
+ sort_nodenetwork_settings($setting_types);
+
+ // get nodenetwork's settings
+ $nodenetwork = $api->GetNodeNetworks( array( $nodenetwork_id ), array( "nodenetwork_setting_ids","ip" ) );
+
+ drupal_set_title("Add a setting to ". $nodenetwork[0]['ip']);
+
+ // start form
+ echo "
\n";
+
+}
+else {
+ $setting_id= intval( $_GET['id'] );
+
+ // get setting info
+ $setting= $api->GetNodeNetworkSettings( array( $setting_id ));
+
+ // nodenetwork info
+ $nodenetwork= $api->GetNodeNetworks( array( $setting[0]['nodenetwork_id'] ), array( "ip" ) );
+
+ drupal_set_title("Edit setting ". $setting[0]['name'] ." on ". $nodenetwork[0]['ip']);
+
+ // start form and put values in to be edited.
+ echo "\n";
+
+}
+
+// back link is case-dependant
+
+// Print footer
+include 'plc_footer.php';
+
+?>
diff --git a/planetlab/nodes/sliver_action.php b/planetlab/nodes/sliver_action.php
new file mode 100644
index 0000000..0817d3c
--- /dev/null
+++ b/planetlab/nodes/sliver_action.php
@@ -0,0 +1,60 @@
+person;
+$_roles= $_person['role_ids'];
+
+
+if( !empty( $_POST['add_sub'] ) ) {
+ $attrib_type= $_POST['sliver'];
+ $value= $_POST['value'];
+ $node_id= $_POST['node_id'];
+ $slice_id= $_POST['slice_id'];
+
+ $api->AddSliceAttribute( intval( $slice_id ), intval( $attrib_type ), $value, intval( $node_id ) );
+
+ header( "location: slivers.php?slice=$slice_id&node=$node_id" );
+ exit();
+
+}
+
+
+//
+if( $_GET['rem_id'] ) {
+ $attrib_id= $_GET['rem_id'];
+
+ // get the slivers for this node
+ $sliver_info= $api->GetSliceAttributes( array( "slice_attribute_id"=>intval( $attrib_id ) ), array( "slice_id", "node_id" ) );
+
+ $api->DeleteSliceAttribute( intval( $attrib_id ) );
+
+ header( "location: slivers.php?slice=". $sliver_info[0]['slice_id'] ."&node=". $sliver_info[0]['node_id'] );
+ exit();
+
+}
+
+
+/*
+// Print footer
+include 'plc_footer.php';
+*/
+
+?>
diff --git a/planetlab/nodes/slivers.php b/planetlab/nodes/slivers.php
new file mode 100644
index 0000000..cca34a7
--- /dev/null
+++ b/planetlab/nodes/slivers.php
@@ -0,0 +1,144 @@
+person;
+$_roles= $_person['role_ids'];
+
+//print_r( $_person );
+
+
+// if add is set, diplay add sliver form
+if( $_GET['add'] ) {
+ $node_id= $_GET['add'];
+ $slice_id= $_GET['slice'];
+
+ // slice info
+ $slice_info= $api->GetSlices( array( intval( $slice_id ) ), array( "name" ) );
+
+ // node info
+ $node_info= $api->GetNodes( array( intval( $node_id ) ), array( "hostname" ) );
+
+ // get attribute types
+ $type_info= $api->GetSliceAttributeTypes( NULL, array( "attribute_type_id", "name" ) );
+
+ // get the slivers for this node
+ $sliver_info= $api->GetSliceAttributes( array( "node_id"=>intval( $node_id ), "slice_id"=>intval( $slice_id ) ), array( "attribute_type_id", "name" ) );
+
+ $types_left= $type_info;
+
+
+ // start form
+ echo "\n";
+
+}
+
+
+// if slice and node ids are passed display slivers and attribs
+if( $_GET['slice'] && $_GET['node'] ) {
+ $slice_id= $_GET['slice'];
+ $node_id= $_GET['node'];
+
+ // slice info
+ $slice_info= $api->GetSlices( array( intval( $slice_id ) ), array( "name" ) );
+
+ // node info
+ $node_info= $api->GetNodes( array( intval( $node_id ) ), array( "hostname" ) );
+
+ // get the slivers for this node
+ $sliver_info= $api->GetSliceAttributes( array( "node_id"=>intval( $node_id ), "slice_id"=>intval( $slice_id ) ), array( "slice_attribute_id", "name", "value", "min_role_id", "description" ) );
+
+ // get the attrbibutes for this slice
+ $attrib_info= $api->GetSliceAttributes( array( intval( $slice_id ) ), array( "slice_attribute_id", "name", "value", "min_role_id", "description" ) );
+
+
+ // start form
+ echo "\n";
+
+}
+
+
+// Print footer
+include 'plc_footer.php';
+
+?>
diff --git a/planetlab/nodes/test.php b/planetlab/nodes/test.php
new file mode 100644
index 0000000..8d6ca59
--- /dev/null
+++ b/planetlab/nodes/test.php
@@ -0,0 +1,39 @@
+GetNodes( array( "hostname" => $input ), array("hostname") );
+ if (count($nodes)) {
+ foreach ( $nodes as $node ) {
+ $aResults[] = $node['hostname'];
+ }
+ }
+}
+
+header("Content-Type: text/xml");
+
+echo "
+";
+for ($i=0;$i".$aResults[$i]."";
+
+echo "
+
+";
+
+?>
diff --git a/planetlab/peers/index.php b/planetlab/peers/index.php
new file mode 100644
index 0000000..6261fed
--- /dev/null
+++ b/planetlab/peers/index.php
@@ -0,0 +1,108 @@
+person;
+$_roles= $_person['role_ids'];
+
+// layout : add a comon link
+function layout_peer ($peer) {
+ $peer['comon'] = plc_comon_button("peer_id",$peer['peer_id']);
+ return $peer;
+}
+
+
+// if peer_host is set then set id to that peer's id.
+if( $_POST['peername'] ) {
+ $peername= $_POST['peername'];
+
+ $peer_info= $api->GetPeers( array( $peername ), array( "peer_id" ) );
+
+ header( "location: index.php?id=". $peer_info[0]['peer_id'] );
+ exit();
+
+}
+
+if( !$_GET['id'] ) {
+
+ // GetPeers API call
+ $peers = $api->GetPeers( NULL, array("peer_id","peername","peer_url"));
+
+ $local_peer_comon = plc_comon_button("peer_id","0");
+ echo " See all local nodes through comon " . $local_peer_comon . "
";
+
+ if ( empty($peers)) {
+ echo "No known peer - standalone deployment";
+ } else {
+
+ $peers = array_map(layout_peer,$peers);
+ sort_peers( $peers );
+
+ echo "";
+ // xxx Thierry : mimicking what was done for nodes - not sure that makes sense here
+ if( $peername )
+ echo " $peername is not a valid peer.\n";
+
+ echo paginate( $peers, "peer_id", "Peers", 10, "peername" );
+ }
+
+} else {
+ // get the peer id from the URL
+ $peer_id= intval( $_GET['id'] );
+
+ // make the api call to pull that peers DATA
+ $peer_info= $api->GetPeers( array( $peer_id ) );
+
+ // peer info
+ $peername= $peer_info[0]['peername'];
+ $peer_url= $peer_info[0]['peer_url'];
+ // arrays of ids of peer info
+ $number_nodes= sizeof($peer_info[0]['node_ids']);
+ $number_slices= sizeof($peer_info[0]['slice_ids']);
+ $number_persons= sizeof($peer_info[0]['person_ids']);
+ $number_sites= sizeof($peer_info[0]['site_ids']);
+
+ // get peer id
+ $peer_id= $peer_info[0]['peer_id'];
+
+ drupal_set_title("Details for Peer " . $peername);
+
+ echo "\n";
+ echo "Peer name ";
+ echo " $peername ";
+ echo "API url ";
+ echo " $peer_url ";
+ echo " # nodes ";
+ echo " $number_nodes ";
+ echo " # slices ";
+ echo " $number_slices ";
+ echo " # sites ";
+ echo " $number_sites ";
+ echo " # persons ";
+ echo " $number_persons ";
+ echo "
\n";
+}
+
+echo "
Back to peer list";
+echo "
";
+
+
+// Print footer
+include 'plc_footer.php';
+
+?>
diff --git a/planetlab/persons/index.php b/planetlab/persons/index.php
new file mode 100644
index 0000000..783f311
--- /dev/null
+++ b/planetlab/persons/index.php
@@ -0,0 +1,473 @@
+person;
+$_roles= $_person['role_ids'];
+
+
+////////////////////
+// The set of columns to fetch
+// and the filter applied for fetching sites
+if ( !in_array( '10', $_roles ) ) {
+ $columns = array("person_id", "first_name", "last_name", "email", "roles" , "peer_id");
+ // PIs can see users not yet enabled
+ if ( ! in_array ('20', $_roles) ) {
+ $filter = array ("enabled" => TRUE);
+ } else {
+ $filter = array();
+ }
+ } else {
+ $columns = array("person_id", "first_name", "last_name", "email", "roles" , "peer_id", "key_ids", "enabled","slice_ids" );
+ $filter = array ();
+ }
+
+//////////////////
+// perform post-processing on site objects as returned by GetSites
+// performs sanity check and summarize the result in a single column
+// performs in-place replacement, so passes a reference
+function layout_person ($person) {
+
+ // we need the 'key_ids' field to do this
+ // so regular users wont run this
+ if ( ! array_key_exists ('key_ids', $person))
+ return $person ;
+
+ $messages=array();
+
+ // do all this stuff on local persons only
+ if ( $person['peer_id'] ) {
+ $class='plc-foreign';
+ } else {
+ $class='plc-warning';
+ // check that the person has keys, but dont do that for admins
+ if ( ! in_array ('admin',$person['roles']) && count($person['key_ids']) == 0)
+ $messages [] = "No Key";
+ }
+ if ( ! $person['enabled'] )
+ $messages[] = "Disabled";
+ // but always cleanup $person columns
+ unset ($person['key_ids']);
+ unset ($person['enabled']);
+ //detect tech already involved on some slices.
+ if( ( count($person['roles'])==1 ) && ( in_array('tech',$person['roles']) ) && (! empty($person["slice_ids"])) ) {
+ $messages[]="Tech involved in a Slice";
+ }
+ // cleanup $person columns
+ unset ($person['slice_ids']);
+ //display the extra status column
+ $person['status'] = plc_make_table($class,$messages);
+ return $person;
+}
+
+// if emailpattern or peerscope is set then search for Persons.
+// we use GET rather than POST so paginate can display the right contents on subsequent pages
+// can be useful for writing bookmarkable URL's as well
+if( $_GET['emailpattern'] || $_GET['peerscope']) {
+ $emailpattern= $_GET['emailpattern'];
+ if (empty($emailpattern)) {
+ $emailpattern="*";
+ }
+ $filter = array_merge (array( "email"=>$emailpattern ), $filter);
+ switch ($_GET['peerscope']) {
+ case '':
+ $peer_label="all peers";
+ break;
+ case 'local':
+ $filter=array_merge(array("peer_id"=>NULL),$filter);
+ $peer_label="local peer";
+ break;
+ case 'foreign':
+ $filter=array_merge(array("~peer_id"=>NULL),$filter);
+ $peer_label="foreign peers";
+ break;
+ default:
+ $peer_id=intval($_GET['peerscope']);
+ $filter=array_merge(array("peer_id"=>$peer_id),$filter);
+ $peer=$api->GetPeers(array("peer_id"=>$peer_id));
+ $peer_label='peer "' . $peer[0]['peername'] . '"';
+ break;
+ }
+ // need to use a hash filter for patterns to be properly handled
+ $persons= $api->GetPersons($filter , $columns );
+ $persons= array_map(layout_person,$persons);
+ $person_count = count ($persons);
+ if ( $person_count == 1) {
+ header( "location: index.php?id=". $persons[0]['person_id'] );
+ exit();
+ } else if ( $person_count == 0) {
+ echo " No person whose email matches $emailpattern ";
+ } else {
+ drupal_set_title ("Users matching $emailpattern on ". $peer_label);
+ sort_persons ($persons);
+ echo paginate( $persons, "person_id", "Persons", 25, "email");
+ }
+ }
+// if a site_id is given, display the site persons only
+else if( $_GET['site_id'] ) {
+ $site_id= $_GET['site_id'];
+ // Get site info
+ $site_info= $api->GetSites( array( intval( $site_id ) ), array( "name", "person_ids" ) );
+ drupal_set_title("People with " . $site_info[0]['name']);
+ // Get site nodes
+ $persons= $api->GetPersons( array_merge (array("person_id"=>$site_info[0]['person_ids']),$filter), $columns );
+ $persons= array_map(layout_person,$persons);
+ sort_persons( $persons );
+
+ echo paginate( $persons, "person_id", "Persons", 25, "email" );
+
+}
+// if a slice_id is given,display the persons involved in this slice
+else if( $_GET['slice_id'] ) {
+ $slice_id= $_GET['slice_id'];
+ // Get slice infos
+ $slice_info= $api->GetSlices( array( intval( $slice_id ) ), array( "name", "person_ids" ) );
+ drupal_set_title("People In " . $slice_info[0]['name']);
+ // Get slice persons
+ $persons= $api->GetPersons( array_merge (array("person_id"=>$slice_info[0]['person_ids']),$filter), $columns );
+ if ( empty ($persons) ) {
+ echo "No persons to display";
+ } else {
+
+ $persons= array_map(layout_person,$persons);
+ sort_persons( $persons );
+
+ echo paginate( $persons, "person_id", "Persons", 25, "email" );
+ echo "
\n";
+ }
+
+ }
+ else {
+ echo "Site has no addresses. \n";
+ }
+
+ // if eligable display add address
+ /*if( in_array( '10', $_roles ) || in_array( '20', $_roles ) )
+ echo "
Add an address\n";*/
+
+ }
+
+ echo "
Back to site list";
+
+}
+
+
+// Print footer
+include 'plc_footer.php';
+
+?>
diff --git a/planetlab/sites/join_request.php b/planetlab/sites/join_request.php
new file mode 100644
index 0000000..5f7d025
--- /dev/null
+++ b/planetlab/sites/join_request.php
@@ -0,0 +1,343 @@
+GetSites( array("enabled" => False, "peer_id" => NULL));
+ if (!empty($sites)) {
+ print("
");
+ print("Site Name site_id Submitted ");
+ foreach($sites as $site) {
+ printf(" %s ", $site['site_id'], $site['name']);
+ printf(" %d ", $site['site_id'], $site['site_id']);
+ printf(" %s ", date("d F Y, G:i",$site['date_created']));
+ }
+ print("
");
+ } else {
+ print(" No open join requests
");
+ }
+}
+
+function render_join_request_review($api, $site_id) {
+ $sites = $api->GetSites( array(intval($site_id)) );
+ if (empty($sites)) {
+ print(" Invalid request with site_id=$site_id
");
+ return;
+ }
+ $site = $sites[0];
+ if ($site['enabled']) {
+ print(" This site is already enabled
");
+ return;
+ }
+ $addresses = $api->GetAddresses ($site['address_ids']);
+ if (empty ($addresses)) {
+ print(" No address found for site_id=$site_id
");
+ return ;
+ }
+ $address = $addresses[0];
+ $address_id=$address['address_id'];
+# just in case there is no person attached yet
+ if (empty ($site['person_ids'])) {
+ $persons=array();
+ } else {
+ $person_ids = $site['person_ids'];
+ $persons = $api->GetPersons( $person_ids, array( "person_id", "role_ids", "first_name", "last_name", "email" , "phone") );
+ }
+ $tech = Null;
+ $pi = Null;
+ foreach($persons as $person) {
+ if ( in_array('20', $person['role_ids']) ) {
+ $pi = $person;
+ }
+ if ( in_array('40', $person['role_ids']) ) {
+ $tech = $person;
+ }
+ }
+ $pi_id = $pi['person_id'];
+ $tech_id = $tech['person_id'];
+
+ print <<< EOF
+ Please review the join request below.
+ Warning: the PI email address that was provided in this form will not be checked automatically. We expect that as part of the handshake with the site, the support team has had an opportunity to use this address so it can be considered safe. Please check it manually if this is not the case.
+
+
+
+
+
+
+
+EOF;
+
+ $site_form = build_site_form(FALSE);
+ $input = array ('site' => $site, 'address'=> $address, 'pi' => $pi, 'tech' => $tech);
+
+ // display the buttons
+ print <<< EOF
+
+
+
+
+
+
+
+
+EOF;
+
+ // render the form - not supposed to be empty
+ form_render_table2 ($site_form, $input, TRUE);
+
+ // display the buttons
+ print <<< EOF
+
+
+
+
+
+
+
+
+EOF;
+
+ print "
";
+}
+
+function notify_enabled_pi ($api, $pi_id, $pi, $site_id, $site) {
+ // notify the PI
+ $template= <<NotifyPersons(array($pi_id),$subject,$body);
+}
+
+
+// find person roles
+$_person= $plc->person;
+$_roles= $_person['role_ids'];
+
+// only admins are allowed to view this page
+if( !in_array( '10', $_roles ) ) {
+
+ print(" not allowed to view this page
");
+}
+else if ($_GET['review'])
+{
+
+ //print review page
+ drupal_set_title('Join Request - Review');
+ render_join_request_review($api, $_GET['site_id']);
+
+}
+else if ($_POST['submitted'] )
+{
+
+ // parse the form
+ $site_form = build_site_form(FALSE);
+ $input = parse_form ($site_form, $_REQUEST, $input);
+ // xxx should not happen ?
+ $empty_form = $input['is_empty'];
+ $error = "";
+ $messages= array();
+ if ( $empty_form ) {
+ $error .= 'Internal error - empty form !?!';
+ }
+ if (empty ($error)) {
+ $site=$input['site'];
+ $address=$input['address'];
+ $pi=$input['pi'];
+ $tech=$input['tech'];
+
+ // Look for missing/blank entries
+ $error .= form_check_required ($site_form, $input);
+ }
+
+ if (empty($error)) {
+ // get objects id from the request
+ $site_id = intval(trim($_POST['site_id']));
+ $address_id = intval(trim($_POST['address_id']));
+ $pi_id = intval(trim($_POST['pi_id']));
+ $tech_id = intval(trim($_POST['tech_id']));
+
+ switch ($_POST['submitted']) {
+ case 'Delete': {
+ $api->DeleteSite ($site_id);
+ $api_error=$api->error();
+ if (!empty($api_error)) {
+ $error .= $api->error();
+ } else {
+ $messages [] = "Site " . $site['name'] . " deleted";
+ }
+ break;
+ }
+ case 'Update': {
+ $api->begin();
+ $api->UpdateSite($site_id,$site);
+ $api->UpdateAddress($address_id,$address);
+ $api->UpdatePerson($pi_id,$pi);
+ $api->UpdatePerson($tech_id,$tech);
+ $api->commit();
+ $api_error=$api->error();
+ if (!empty($api_error)) {
+ $error .= $api->error();
+ } else {
+ $messages [] = "Join request updated for site " . $site['name'] ;
+ }
+
+ break;
+ }
+ case 'Approve': {
+ // Thierry - august 22 2007
+ // keep it simple - the admin who approves is now supposed to check
+ // the PI's email address, which makes the whole thing *a lot* simpler
+ // enable the site, enable the PI, and VerifyPerson the thec if different from the PI
+ $site['enabled'] = True;
+ $api->UpdateSite ($site_id,$site);
+ $api_error=$api->error();
+ if (!empty($api_error)) {
+ $error .= $api->error();
+ $messages [] = "Could not enable site";
+ } else {
+ $messages[] = "Site " . $site['name'] . " enabled";
+ }
+
+ if (empty ($error)) {
+ // Update Address
+ $api->UpdateAddress($address_id,$address);
+ $api_error=$api->error();
+ if ( ! empty($api_error)) {
+ $error .= $api->error();
+ $messages [] = "Could not update address";
+ }
+
+ foreach ( array("Billing","Shipping") as $address_type) {
+ $api->AddAddressTypeToAddress($address_type,$address_id);
+ $api_error=$api->error();
+ if ( ! empty($api_error)) {
+ $error .= $api->error();
+ $messages [] = "Could not add address type " . $address_type;
+ }
+ }
+
+ // Update pi, and enable him
+ $api->UpdatePerson ($pi_id,$pi);
+ if ( $pi ['enabled' ] ) {
+ $messages [] = "PI already enabled";
+ } else {
+ $api->UpdatePerson ($pi_id,array("enabled"=>True));
+ $api_error=$api->error();
+ if (empty($api_error)) {
+ $messages[] = "Enabled PI as " . $pi['email'] ;
+ notify_enabled_pi ($api, $pi_id,$pi,$site_id, $site);
+ $messages[] = "Notified PI by email";
+ } else {
+ $error .= $api->error();
+ $messages [] = "Could not update PI";
+ }
+ }
+
+ if ($pi_id != $tech_id) {
+ // Update tech, and VerifyPerson him if needed
+ $api->UpdatePerson ($tech_id,$tech);
+ if ( $tech [ 'enabled' ] ) {
+ $messages [] = "Tech already enabled";
+ } else {
+ $api->VerifyPerson($tech_id);
+ $api_error=$api->error();
+ if (empty($api_error)) {
+ $messages[] = "Created account registration for Tech as " . $tech['email'];
+ } else {
+ $error .= $api->error();
+ $messages [] = "Could not verify Tech";
+ }
+ }
+ }
+ }
+
+ break;
+ }
+ default: {
+ $error .= 'Internal error - unexpected request';
+ break;
+ }
+
+ } // end switch
+ }
+
+ // Show messages
+ if (!empty($messages)) {
+ print ' ";
+ }
+
+ // Show errors if any
+ if (!empty($error)) {
+ print ' ';
+ drupal_set_title('Join Request - Review');
+ render_join_request_review($api, $_POST['site_id']);
+ } else {
+ drupal_set_title('All currently pending join requests');
+ render_all_join_requests($api);
+ }
+
+ }
+ else // list all pending requests
+{
+
+ drupal_set_title('All currently pending join requests');
+ render_all_join_requests($api);
+}
+
+// Print footer
+include 'plc_footer.php';
+
+?>
diff --git a/planetlab/sites/pcu.php b/planetlab/sites/pcu.php
new file mode 100644
index 0000000..3f5c229
--- /dev/null
+++ b/planetlab/sites/pcu.php
@@ -0,0 +1,219 @@
+person;
+$_roles= $_person['role_ids'];
+
+
+// if no id: add, else: display(update)
+if( !$_GET['id'] ) {
+ if( $_POST['submitted'] ) {
+ // get person's site id
+ $site_id= $_person['site_ids'][0];
+
+ // build dict
+ $fields= array( 'protocol'=>$_POST['protocol'], 'hostname'=>$_POST['hostname'], 'model'=>$_POST['model'], 'username'=>$_POST['username'], 'password'=>$_POST['password'], 'notes'=>$_POST['notes'], 'ip'=>$_POST['ip'] );
+
+ $pcu_id= $api->AddPCU( $site_id, $fields );
+
+ if( $pcu_id != 0 ) {
+ header( "location: /db/sites/pcu.php?id=$pcu_id" );
+ exit();
+ } else {
+ $error= $api->error();
+ }
+ }
+
+ if( !empty( $error ) )
+ echo '' . $error . '.';
+
+ echo "Add a PCU
\n
+\n
+\n
+Protocol: \n
+Hostname: \n
+IP Address: \n
+Model: \n
+Username: \n
+Password: \n
+Notes: \n
+
\n
+
\n";
+} else {
+ // get PCU info
+ $pcu_id= intval( $_GET['id'] );
+ $pcu_info= $api->GetPCUs( array( intval( $pcu_id ) ) );
+
+ // if remove is set remove the node from the pcu
+ if( $_GET['remove'] ) {
+ $rem_id= $_GET['remove'];
+
+ $api->DeleteNodeFromPCU( intval( $rem_id ), $pcu_id );
+
+ header( "Location: /db/sites/pcu.php?id=$pcu_id" );
+ exit();
+
+ }
+
+ //if submitted, update node info
+ if( $_POST['submitted'] ) {
+ $protocol= $_POST['protocol'];
+ $username= $_POST['username'];
+ $hostname= $_POST['hostname'];
+ $ipaddress= $_POST['ip'];
+ $model= $_POST['model'];
+ $password= $_POST['password'];
+ $notes= $_POST['notes'];
+
+ $api->UpdatePCU( $pcu_id, array( "protocol"=>$protocol, "hostname"=>$hostname, "model"=>$model, "password"=>$password, "notes"=>$notes, "ip"=>$ipaddress ) );
+
+ header( "Location: /db/sites/pcu.php?id=$pcu_id" );
+ exit();
+
+ }
+
+ if( in_array( 10, $_roles ) || ( in_array( 20, $_roles ) && in_array( $pcu_info[0]['site_id'], $_person['site_ids'] ) ) || ( in_array( 40, $_roles ) && in_array( $pcu_info[0]['site_id'], $_person['site_ids'] ) ) )
+ $pcu_controller= true;
+
+ // get PCU node info
+ $node_info= $api->GetNodes( $pcu_info[0]['node_ids'], array( "hostname", "node_id", "boot_state" ) );
+ echo "
\n
+PCU: ". $pcu_info[0]['hostname'] ."
\n
+\n
+Protocol: ";
+
+ if( $pcu_controller )
+ echo "";
+
+ echo " \n
+ Hostname: ";
+
+ if( $pcu_controller )
+ echo "";
+
+ echo " \n
+ IP Address: ";
+ if( $pcu_controller )
+ echo "";
+
+ if( $pcu_controller )
+ echo " \n
+ Model: ";
+
+ if( $pcu_controller )
+ echo "";
+
+ echo " \n
+ Username: ";
+
+ if( $pcu_controller )
+ echo "";
+
+ echo " \n
+ Password: ";
+
+ if( $pcu_controller )
+ echo "";
+
+ echo " \n
+ Notes: ";
+
+ if( $pcu_controller )
+ echo "";
+
+ echo " \n
+
\n";
+
+ if( $pcu_controller )
+ echo "
\n";
+
+ if( !empty( $node_info ) ) {
+ echo "
\nNodes \nHostname State ";
+
+ // if user can control PCU add table cells
+ if( $pcu_controller )
+ echo " \n";
+
+ echo " \n";
+
+ // for port numbers
+ $count= 0;
+
+ foreach( $node_info as $node ) {
+ echo "". $pcu_info[0]['ports'][$count] ." ". $node['hostname'] ." ". $node['boot_state'] ." ";
+
+ if( $pcu_controller )
+ echo "remove \n";
+
+ echo " \n";
+
+ $count++;
+
+ }
+ echo "
\n";
+
+ } else {
+ echo "No nodes on PCU.";
+ }
+
+
+ echo "
Back to Site\n";
+
+}
+
+
+
+// Print footer
+include 'plc_footer.php';
+
+?>
diff --git a/planetlab/sites/peers.php b/planetlab/sites/peers.php
new file mode 100644
index 0000000..fbe6ae9
--- /dev/null
+++ b/planetlab/sites/peers.php
@@ -0,0 +1,137 @@
+person;
+$_roles= $_person['role_ids'];
+
+
+
+$count1=0;
+$count2=0;
+$count3=0;
+$j=0;
+$site_nt_enabled = array();
+$site_wno_nodes = array();
+$sitemembers_nodes= array();
+$nodes_ids=array();
+$site_up=array();
+function layout(&$param){
+
+ $class='plc-foreign';
+ $messages=array();
+ $new_site=($param[$j]['abbreviated_name']);
+ $temp= $new_site;
+ $messages[]=$temp;
+ echo $new_site;
+ //unset ($param['time']);
+ $param[$j]['abbreviated_name']=plc_make_table($class,$messages);
+ $j++;
+
+}
+
+
+/////////Quantavis and Alcatel case
+echo "
Quantavis
Member to join:no membership requested
" ;
+echo " Alcatel
Member to join:no membership requested
" ;
+
+//get all local site with the filter peer_id=None
+$filter=array("peer_id"=>NULL);
+$columns=array("abbreviated_name","name");
+$sites=$api->GetSites($filter,NULL);
+//layout($sites);
+
+if (empty($sites)){
+ echo "No PLE Nodes";
+ echo "
Back to Nodes List";
+ // return;
+ }
+
+
+
+for($i=0; $i <= count($sites) ; $i++)
+ {
+ $temp= $sites[$i]["site_id"];
+ $filter=array("site_id"=>$temp);
+ $columns=array("boot_state","hostname","node_id");
+ $Nodes=$api->GetNodes($filter,$columns);
+
+ //array_push($sitemembers_nodes,$Nodes[$i]);
+ //for($i=0; $i < count($Nodes) ; $i++)
+ // {
+ // echo $i;
+ // array_push($sitemembers_nodes,$Nodes[$i]);
+ // echo $Nodes[$i]['hostname'];
+ // }
+ //
+
+ if (empty($Nodes)){
+ if ($site_st= $sites[$i]["enabled"]==0){
+ $site_name= $sites[$i]["name"];
+ array_push($site_nt_enabled,$site_name);
+ $count1++;
+ }
+
+ elseif ($sites[$i]["name"]=="PlanetLabEurope Central"){}///planetlab Central case
+
+ else{
+ $site_name= $sites[$i]["name"];
+ array_push($site_wno_nodes,$site_name);
+ $count2++;
+ }
+ }
+
+ else{
+
+
+ $site_name= $sites[$i]["name"];
+ array_push($sitemembers_nodes,$Nodes);
+ array_push($nodes_ids,$Nodes[$j]["node_id"]);
+ array_push($site_up,$site_name);
+ $count3++;
+
+ //echo paginate($Nodes,"node_id", "----$site_name----", 5,"hostname","$site_name");
+ }
+}
+
+///site not already enabled
+for($i=0; $i != (($count1)-1); $i++)
+ { $site_name=$site_nt_enabled[$i];
+ echo "
$site_name
Site with a join request pending
" ;
+ }
+
+//site with no nodes
+for($i=0; $i < $count2; $i++)
+ { $site_name=$site_wno_nodes[$i];
+ echo " $site_name
Site has no nodes
" ;
+ }
+
+//////Site runing up
+for($i=0; $i < $count3; $i++)
+ { $nodes=$sitemembers_nodes[$i];
+ $site_name=$site_up[$i];
+ echo " $site_name
";
+ array_map(layout($site_up),$nodes);
+ echo paginate($nodes,"node_id", "Nodes", 5,"hostname","nodes","$nodes_ids[$i]");
+ }
+
+
+echo "
Back to node list";
+//// Print footer
+include 'plc_footer.php';
+?>
diff --git a/planetlab/sites/register.php b/planetlab/sites/register.php
new file mode 100644
index 0000000..618e047
--- /dev/null
+++ b/planetlab/sites/register.php
@@ -0,0 +1,221 @@
+AddSite($site);
+ $api_error .= $adm->error();
+ if (empty($api_error)) {
+ $verboses [] = "Site created as disabled";
+ } else {
+ $error .= $api_error;
+ }
+ }
+
+ if (empty($error)) {
+
+ // Thierry on august 22 2007
+ // use NotifySupport to send this message, and get rid of the fake account
+
+ $subject = "Site registration form : new site " . $site['name'];
+ // should use GetMessages and stuff, but I do not trust the way
+ // templates are created in db-config, for upgrades notably
+ $template = <<< EOF
+We received a site registration form for site name %s
+
+To review, please visit https://%s:%d/db/sites/join_request.php?review=t&site_id=%d.
+EOF;
+ $body=sprintf($template,$site['name'],PLC_WWW_HOST,PLC_WWW_SSL_PORT,$site_id);
+ $adm->NotifySupport($subject,$body);
+
+ $messages [] = "Your registration request has been received.";
+ $messages [] = "A mail was sent to the operations team, your application should be processed shortly.";
+ $messages [] = "Upon approval, the PI will receive an information e-mail";
+ $messages [] = "Please send a message to " . PLC_MAIL_SUPPORT_ADDRESS . " if this request is not instructed within a few days.";
+
+ // creating address
+ $adm->AddSiteAddress($site_id,$address);
+ $api_error = $adm->error();
+ if (empty($api_error)) {
+ $verboses [] = "Address created";
+ } else {
+ $error .= $api_error;
+ }
+
+ // creating PI
+ // Thierry 23 august 2007
+ // avoid using a pre-existing federated account
+ $known_pi = $adm->GetPersons(array("email"=>$pi['email'],
+ "peer_id"=>NULL),array("person_id"));
+ if ($known_pi) {
+ $messages [] = " Note: PI was already known";
+ $pi_id=$known_pi[0]['person_id'];
+ } else {
+ $pi['enabled']=FALSE;
+ $pi_id=$adm->AddPerson($pi);
+ $api_error = $adm->error();
+ if (empty($api_error)) {
+ $verboses [] = "PI created as disabled
";
+ } else {
+ $error .= $api_error;
+ }
+ }
+ if ($adm->AddPersonToSite($pi_id,$site_id)) {
+ $verboses [] = "PI attached to new site";
+ }
+ if ($adm->AddRoleToPerson('pi',$pi_id)) {
+ $verboses [] = $pi['email'] . " granted PI role
";
+ }
+
+ if ($pi['email'] == $tech['email']) {
+ // need to assign tech role so the registration request gets filled properly
+ if ($adm->AddRoleToPerson('tech',$pi_id)) {
+ $verboses [] = $pi['email'] . " granted Tech role
";
+ }
+ } else {
+ // creating TECH
+ $known_tech = $adm->GetPersons(array("email"=>$tech['email'],
+ "peer_id"=>NULL),array("person_id"));
+ if ($known_tech) {
+ $messages [] = " Note: Tech was already known";
+ $tech_id=$known_tech[0]['person_id'];
+ } else {
+ $tech['enabled']=FALSE;
+ $tech_id=$adm->AddPerson($tech);
+ $api_error = $adm->error();
+ if (empty($api_error)) {
+ $verboses [] = "Tech created as disabled ';
+ }
+}
+
+$self = $_SERVER['PHP_SELF'];
+if (!empty($_SERVER['QUERY_STRING'])) {
+ $self .= "?" . $_SERVER['QUERY_STRING'];
+}
+
+$url = htmlspecialchars($_REQUEST['url']);
+
+// XXX Use our own stylesheet instead of drupal.css
+print <<
";
+ } else {
+ $error .= $api_error;
+ }
+ }
+ if ($adm->AddPersonToSite($tech_id,$site_id)) {
+ $verboses [] = "Tech attached to new site";
+ }
+ if ($adm->AddRoleToPerson('tech',$tech_id)) {
+ $verboses [] = $tech['email'] . " granted Tech role";
+ }
+ if ( ($tech['user-role']) && $adm->AddRoleToPerson('user',$tech_id) ) {
+ $verboses [] = $tech['email'] . " granted User role";
+ }
+ }
+ }
+
+
+ // Show messages
+ if (!empty($messages)) {
+ print ' ";
+ }
+
+ if ($verbose && !empty($verboses)) {
+ print ' ";
+ }
+
+ if (!empty($error)) {
+ print ' ';
+ } else {
+ // to prevent resubmit
+ $site['site_id']="XXX";
+ }
+}
+
+$self = $_SERVER['PHP_SELF'];
+if (!empty($_SERVER['QUERY_STRING'])) {
+ $self .= "?" . $_SERVER['QUERY_STRING'];
+}
+
+print <<
+
+
+
+
+EOF;
+
+// Do not allow resubmits
+if (empty($site['site_id'])) {
+ print ' ';
+}
+
+form_render_table2 ($site_form, $input, ! $empty_form);
+
+// Do not allow resubmits
+if (empty($site['site_id'])) {
+ print ' ';
+ print ' ';
+}
+
+print "
";
+
+include 'plc_footer.php';
+
+?>
diff --git a/planetlab/sites/site_action.php b/planetlab/sites/site_action.php
new file mode 100644
index 0000000..f1dc4e8
--- /dev/null
+++ b/planetlab/sites/site_action.php
@@ -0,0 +1,55 @@
+person;
+$_roles= $_person['role_ids'];
+
+// if action exists figure out what to do
+if( $_POST['actions'] ) {
+ // get slice_id as int
+ $site_id= intval( $_POST['site_id'] );
+
+ // depending on action, run function
+ switch( $_POST['actions'] ) {
+ case "update":
+ header( "location: update_site.php?id=$site_id" );
+ exit();
+ break;
+ case "delete":
+ header( "location: delete_site.php?id=$site_id" );
+ exit();
+ break;
+ case "expire":
+ header( "location: expire.php?id=$site_id" );
+ exit();
+ break;
+
+ }
+
+}
+
+
+/*
+// Print footer
+include 'plc_footer.php';
+*/
+
+?>
diff --git a/planetlab/sites/site_form.php b/planetlab/sites/site_form.php
new file mode 100644
index 0000000..7e06ed0
--- /dev/null
+++ b/planetlab/sites/site_form.php
@@ -0,0 +1,246 @@
+ 'Site name', 'required' => TRUE,
+ 'maxlength' => 40, 'size' => 20,
+ 'comment' => 'Site Information:');
+ $form['site:login_base'] = array('title' => 'Login base', 'required' => TRUE,
+ 'maxlength' => 16, 'size' => 10);
+ $form['site:abbreviated_name'] = array('title' => 'Abbreviated name', 'required' => TRUE,
+ 'maxlength' => 20, 'size' => 12);
+ $form['site:url'] = array('title' => 'URL', 'required' => TRUE,
+ 'maxlength' => 30, 'size' => 30);
+ $form['site:latitude'] = array('title' => 'Latitude', 'required' => FALSE,
+ 'maxlength' => 10, 'size' => 10, 'type' => 'double');
+ $form['site:longitude'] = array('title' => 'Longitude', 'required' => FALSE,
+ 'maxlength' => 10, 'size' => 10, 'type' => 'double');
+
+ $form['address:line1'] = array('title' => 'Address', 'required' => TRUE,
+ 'maxlength' => 40, 'size' => 30,
+ 'comment' => 'Postal address:');
+ $form['address:line2'] = array('title' => 'Address (2)', 'required' => FALSE,
+ 'maxlength' => 40, 'size' => 30);
+ $form['address:line3'] = array('title' => 'Address (3)', 'required' => FALSE,
+ 'maxlength' => 40, 'size' => 30);
+ $form['address:city'] = array('title' => 'City', 'required' => TRUE,
+ 'maxlength' => 20, 'size' => 20);
+ $form['address:postalcode'] = array('title' => 'Postal Code', 'required' => TRUE,
+ 'maxlength' => 10, 'size' => 10);
+# would have liked it *not* required but it is mandatory in the DB - sigh
+ $form['address:state'] = array('title' => 'State', 'required' => TRUE,
+ 'maxlength' => 20, 'size' => 20);
+ $form['address:country'] = array('title' => 'Country', 'required' => TRUE,
+ 'maxlength' => 20, 'size' => 20);
+
+ $form['pi:first_name'] = array('title' => 'PI First Name', 'required' => TRUE,
+ 'maxlength' => 20, 'size' => 20,
+ 'comment' => 'Principal Investigator Information:');
+ $form['pi:last_name'] = array('title' => 'PI Last Name', 'required' => TRUE,
+ 'maxlength' => 20, 'size' => 20);
+ $form['pi:title'] = array('title' => 'PI Title', 'required' => FALSE,
+ 'maxlength' => 6, 'size' => 6);
+ $form['pi:phone'] = array('title' => 'PI Phone', 'required' => TRUE,
+ 'maxlength' => 20, 'size' => 20);
+ $form['pi:email'] = array('title' => 'PI email', 'required' => TRUE,
+ 'maxlength' => 40, 'size' => 20);
+ if ($register_mode) {
+ $form['pi:password'] = array('title' => 'PI password', 'required' => TRUE,
+ 'maxlength' => 20, 'size' => 20, 'type' => 'password');
+ }
+
+
+ if ($register_mode) {
+ // required for the following code
+ drupal_set_html_head('');
+
+ $fill_from_pi_button = <<< EOF
+
+EOF;
+ }
+
+
+ $form['tech:first_name'] = array('title' => 'Tech First Name', 'required' => TRUE,
+ 'maxlength' => 20, 'size' => 20,
+ 'comment' => 'Technical Contact Information:' . $fill_from_pi_button);
+ $form['tech:last_name'] = array('title' => 'Tech Last Name', 'required' => TRUE,
+ 'maxlength' => 20, 'size' => 20);
+ $form['tech:title'] = array('title' => 'Tech Title', 'required' => FALSE,
+ 'maxlength' => 6, 'size' => 6);
+ $form['tech:phone'] = array('title' => 'Tech Phone', 'required' => TRUE,
+ 'maxlength' => 20, 'size' => 20);
+ $form['tech:email'] = array('title' => 'Tech email', 'required' => TRUE,
+ 'maxlength' => 40, 'size' => 20);
+ if ($register_mode) {
+ $form['tech:password'] = array('title' => 'Tech password', 'required' => TRUE,
+ 'maxlength' => 20, 'size' => 20, 'type' => 'password');
+ $form['tech:user-role'] = array('type' => 'boolean', 'title' => 'Need user role', 'default' => TRUE);
+ }
+
+ return $form;
+}
+
+// input :
+// $form : the form as defined above
+// $request : usually $_REQUEST
+// $input : a dict ('site'=>$site ..)
+// takes the values from the request and fills $input accordingly
+// output
+// $input : the modified dict, with as many keys as form categories,
+// + the 'is_empty' key that returns a boolean, FALSE if any field was set in the request
+
+function parse_form ($form, $request, $input = NULL) {
+ if (empty ($input)) {
+ $input = array();
+ }
+ $empty_form = TRUE;
+
+ // fill with values form the form
+ foreach ($form as $fullname => $item) {
+ list($objname,$field) = split(":",$fullname);
+ $raw_input=$request[$fullname];
+ if (!empty($raw_input)) {
+ $empty_form = FALSE;
+ // implement type conversion
+ switch ($item['type']) {
+ case 'double':
+ $input[$objname][$field] = doubleval(trim($raw_input));
+ break;
+ case 'int':
+ $input[$objname][$field] = intval(trim($raw_input));
+ break;
+ case 'boolean':
+ $input[$objname][$field] = ($raw_input=="yes");
+ break;
+ case 'password':
+ case 'raw':
+ $input[$objname][$field] = $raw_input;
+ break;
+ default:
+ $input[$objname][$field] = trim($raw_input);
+ break;
+ }
+ } else {
+ switch ($item['type']) {
+ case 'double':
+ $input[$objname][$field] = 0.0;
+ break;
+ case 'int':
+ $input[$objname][$field] = 0;
+ break;
+ case 'boolean':
+ if (array_key_exists($field,$request)) {
+ $input[$objname][$field]=FALSE;
+ }
+ break;
+ default:
+ $input[$objname][$field] = '';
+ break;
+ }
+ }
+ }
+
+ $input['is_empty'] = $empty_form;
+
+ return $input;
+}
+
+// checks all required fields are filled
+// returns a - possibly empty - html error string
+function form_check_required ($form, $input) {
+ $missing = array();
+ foreach ($form as $fullname => $item) {
+ list($objname,$field) = split(":",$fullname);
+ if ($item['required'] && empty($input[$objname][$field])) {
+ $missing[] = $item['title'];
+ }
+ }
+ if (empty($missing))
+ return "";
+ $error = "";
+ foreach ($missing as $field) {
+ $error .= "- Field '$field' is required.
";
+ }
+ $error .= "
";
+ return $error;
+}
+
+// displays the actual form, with values from $input
+// if $outline_missing is set, missing required fields are outlined
+// fields typed as 'password' are displayed differently
+// expected to be embedded in a table with 2 columns
+function form_render_table2 ($form, $input, $outline_missing) {
+
+ foreach ($form as $fullname => $item) {
+
+ list($objname,$field) = split(":",$fullname);
+
+ // render the comment field
+ if ( ! empty($item['comment'])) {
+ $comment=$item['comment'];
+ print " $comment ";
+ }
+
+ // compute line attributes
+ $title = $item['title'];
+ $required = $item['required'] ? '*' : "";
+ $class = $item['required'] ? "required" : "";
+ if ($outline_missing && $item['required'] && empty($input[$objname][$field])) {
+ $class .= " error";
+ }
+
+ // Label part
+ print "";
+ print << \n
+EOF;
+
+ // input part
+ if ($item['type'] == 'boolean') {
+ // compute boolean default : whether we have this in the request
+ if (array_key_exists($field,$input[$objname])) {
+ $default = $input[$objname][$field] ;
+ } else { // or not - in which case we use the form default
+ $default = ($item['default'] == TRUE);
+ }
+ if ($default) {
+ $checkedyes = "checked='checked'";
+ $checkedno = "";
+ } else {
+ $checkedyes = "";
+ $checkedno = "checked='checked'";
+ }
+ print <<< EOF
+
+ Yes
+ No
+ \n
+EOF;
+ } else {
+ $type = ($item['type'] == 'password') ? "password" : "text";
+ $value = !empty($input[$objname][$field]) ? $input[$objname][$field] : "";
+ $maxlength = $item['maxlength'];
+ $size = $item['size'];
+ print << \n
+EOF;
+ }
+
+ print " \n";
+ }
+}
+
+?>
diff --git a/planetlab/sites/switch_site.php b/planetlab/sites/switch_site.php
new file mode 100644
index 0000000..5e5975d
--- /dev/null
+++ b/planetlab/sites/switch_site.php
@@ -0,0 +1,78 @@
+person;
+$_roles= $_person['role_ids'];
+
+
+// if no site id redirect
+if( !$_GET['id'] ) {
+ header( "location: index.php" );
+ exit();
+ }
+
+// get site_id
+$site_id= $_GET['id'];
+
+// if submitted
+if( $_POST['submitted'] ) {
+ $new_site= $_POST['new_site'];
+
+ // no primary site anymore..........
+
+}
+
+
+// Print header
+require_once 'plc_drupal.php';
+drupal_set_title('Sites');
+include 'plc_header.php';
+
+
+// if admin list all sites, else list just persons sites
+if( in_array( '10', $_roles ) )
+ $site_info= $api->GetSites( NULL, array( "site_id", "name" ) );
+else
+ $site_info= $api->GetSites( $_person['site_ids'], array( "site_id", "name" ) );
+
+
+sort_sites( $site_info );
+
+// start form
+echo "\n";
+echo "Switch Site
\n";
+echo "Change active site to: \n";
+echo "\n";
+echo "
This will change your Primary Site.\n";
+echo "
\n";
+
+echo "
Back to Site\n";
+
+echo "\n";
+
+
+
+// Print footer
+include 'plc_footer.php';
+
+?>
diff --git a/planetlab/sites/test.php b/planetlab/sites/test.php
new file mode 100644
index 0000000..4398f24
--- /dev/null
+++ b/planetlab/sites/test.php
@@ -0,0 +1,39 @@
+GetSites( array( "name" => $input ), array("name") );
+ if (count($sites)) {
+ foreach ( $sites as $site ) {
+ $aResults[] = $site['name'];
+ }
+ }
+}
+
+header("Content-Type: text/xml");
+
+echo "
+";
+for ($i=0;$i".$aResults[$i]."";
+
+echo "
+
+";
+
+?>
diff --git a/planetlab/sites/update_site.php b/planetlab/sites/update_site.php
new file mode 100644
index 0000000..80eaf69
--- /dev/null
+++ b/planetlab/sites/update_site.php
@@ -0,0 +1,151 @@
+person;
+$_roles= $_person['role_ids'];
+
+// redirect if no site id is set add instead
+if( !$_GET['id'] )
+ $do= 'Add';
+else {
+ $site_id= $_GET['id'];
+ $do= 'Update';
+}
+
+// if form not submitted get data from API
+if( $_POST['submitted'] ) {
+
+ $name= $_POST['name'];
+ $abbrev_name= $_POST['abbrev_name'];
+ $url= $_POST['url'];
+ $login_base= $_POST['login_base'];
+ $latitude= $_POST['latitude'];
+ $longitude= $_POST['longitude'];
+ //$max_slivers= $_POST['max_slivers'];
+ $max_slices= $_POST['max_slices'];
+
+
+ if( $name == "" )
+ $error['name']= "Name can not be blank.";
+
+ if( $abbrev_name == "" )
+ $error['abbrev_name']= "Abbreviated Name can not be blank.";
+
+ if( $login_base == "" )
+ $error['login_base']= "Login Base can not be blank.";
+
+ if( $url == "" || $url == "http://" )
+ $error['url']= "URL can not be blank.";
+
+ if( !is_numeric( $latitude ) )
+ $error['latitude']= "Latitude must be a number.";
+
+ if( !is_numeric( $longitude ) )
+ $error['longitude']= "Longitude must be a number.";
+
+ if( !is_numeric( $max_slices ) )
+ $error['max_slices']= "Max Slices must be a number.";
+
+
+ // if no errors add/update site
+ if( $do == 'Add' ) {
+ $fields= array( "name" => $name, "url" => $url, "longitude" => floatval( $longitude ), "login_base" => $login_base, "latitude" => floatval( $latitude ), "is_public" => true, "abbreviated_name" => $abbrev_name, "max_slices" => 0 );
+ $api->AddSite( $fields );
+ //echo "
"; print_r( $fields ); echo "
";
+ }
+
+ if( $do == 'Update' ) {
+ $fields= array( "name" => $name, "url" => $url, "longitude" => floatval( $longitude ), "login_base" => $login_base, "latitude" => floatval( $latitude ), "is_public" => true, "abbreviated_name" => $abbrev_name, "max_slices" => intval( $max_slices ) );
+ $api->UpdateSite( intval( $site_id ), $fields );
+ // Thierry aug 31 07 - redirect to the site's details page
+ header("location: index.php?id=$site_id");
+ //echo ""; print_r( $fields ); echo "
";
+ }
+
+}
+
+// if its an update get site info$max_slices &&
+if( $do == 'Update' && empty( $error ) ) {
+ // Get site api call
+ $site_info= $api->GetSites( array( intval( $site_id ) ), array( "name", "url", "longitude", "latitude", "login_base", "max_slices", "abbreviated_name" ) );
+
+ // var names to api return
+ $name= $site_info[0]['name'];
+ $abbrev_name= $site_info[0]['abbreviated_name'];
+ $url= $site_info[0]['url'];
+ $login_base= $site_info[0]['login_base'];
+ $latitude= $site_info[0]['latitude'];
+ $longitude= $site_info[0]['longitude'];
+ //$max_slivers= $site_info[0]['max_slivers'];
+ $max_slices= $site_info[0]['max_slices'];
+
+}
+
+// Print header
+require_once 'plc_drupal.php';
+drupal_set_title('Sites');
+include 'plc_header.php';
+
+// set error styles
+if( $error['name'] )
+ $name_err= " style='border: 1px solid red;'";
+
+if( $error['abbrev_name'] )
+ $abbrev_err= " style='border: 1px solid red;'";
+
+if( $error['login_base'] )
+ $base_err= " style='border: 1px solid red;'";
+
+if( $error['url'] )
+ $url_err= " style='border: 1px solid red;'";
+
+if( $error['latitude'] )
+ $lat_err= " style='border: 1px solid red;'";
+
+if( $error['longitude'] )
+ $long_err= " style='border: 1px solid red;'";
+
+if( $error['max_slices'] )
+ $max_err= " style='border: 1px solid red;'";
+
+// start form
+echo "\n";
+echo "$do $name
\n";
+
+echo "\n";
+// Thierry - displays name under double quotes - some site names have single quotes, none have double quotes as of now
+echo "Name: ". $error['name'] ." \n";
+echo "Abbreviated Name: ". $error['abbrev_name'] ." \n";
+echo "Login Base: ". $error['login_base'] ." \n";
+echo "URL: ". $error['url'] ." \n";
+echo "Latitude/Longitude: / ". $error['latitude'] ." ". $error['longitude'] ." \n";
+
+if( in_array( 10, $_roles ) ) {
+ echo "Max Slices: ". $error['max_slices'] ." \n";
+}
+else
+ echo " \n";
+
+echo " \n";
+echo "
\n";
+
+echo "Back to Site\n";
+
+echo "
\n";
+
+
+// Print footer
+include 'plc_footer.php';
+
+?>
diff --git a/planetlab/slices/add_slice.php b/planetlab/slices/add_slice.php
new file mode 100644
index 0000000..3a3ca8c
--- /dev/null
+++ b/planetlab/slices/add_slice.php
@@ -0,0 +1,169 @@
+person;
+$_roles= $_person['role_ids'];
+
+//print_r( $_person );
+
+// if not a PI or admin then redirect to slice index
+if( !in_array( '10', $_roles ) && !in_array( '20', $_roles ) )
+ header( "index.php" );
+
+if( !$_POST['name'] ) {
+ // get default site base
+ $site_info= $api->GetSites( array( $_person['site_ids'][0] ), array( "login_base" ) );
+ $base= $site_info[0]['login_base'] ."_";
+}
+
+// add it
+if( $_POST['add'] ) {
+ // get post vars
+ $url= $_POST['url'];
+ $instantiation= $_POST['instantiation'];
+ $name= $_POST['name'];
+ $description= $_POST['description'];
+
+ // validate input
+ if( $name == $base )
+ $error['name']= "You must enter a name for your slice.";
+ else {
+ $slice_name= $name;
+ // make sure slice name doesnt exist
+ $slice_info= $api->GetSlices( array( $slice_name ), array( "slice_id" ) );
+
+ if( !empty( $slice_info ) ) {
+ $error['name']= "Slice name already in use. Please choose another.";
+ $name= "";
+ }
+
+ }
+
+ if( $url == "http://" || "" )
+ $error['url']= "You must enter a URL for your slice's info.";
+
+ if( $description == "" )
+ $error['description']= "Your must enter a description for you slice.";
+
+ // if no errors then add
+ if( !$error ) {
+ $fields= array( "url" => $url, "instantiation" => $instantiation, "name" => $slice_name, "description" => $description );
+ echo "added: "; print_r( $fields ); echo "
\n";
+ // add it!
+ $slice_new_id= $api->AddSlice( $fields );
+
+ if( $slice_new_id ) {
+ header( "location: index.php?id=$slice_new_id" );
+ exit();
+ } else {
+ $error['api']= $api->error();
+ }
+ }
+
+}
+
+// Print header
+require_once 'plc_drupal.php';
+drupal_set_title('Slices');
+include 'plc_header.php';
+
+
+if( !$url )
+ $url= "http://";
+
+// check for errors and set error styles
+if( $error['name'] )
+ $name_error= " class='plc-warning'";
+
+if( $error['url'] )
+ $url_error= " class='plc-warning'";
+
+if( $error['description'] )
+ $desc_error= " class='plc-warning'";
+
+
+// add javaScript code
+echo "\n";
+
+
+// start form
+echo "\n";
+
+if( $error['api'] )
+ echo "". $error['api'] ."\n";
+
+echo "Create Slice
\n";
+
+echo "You must provide a short description of the new slice as well as a link to a project website before creating it. Do not provide bogus information; if a complaint is lodged against your slice and PlanetLab Operations is unable to determine what the normal behavior of your slice is, your slice may be deleted to resolve the complaint.\n";
+echo "
There are three possible \"instantiation\" states for a slice. PLC creates a slice with default settings. Delegated creates a ticket to use on each node. None allows you to reserve a slice name; you may instantiate the slice later.\n";
+echo "
NOTE: All PlanetLab users are strongly encouraged to join the PlanetLab Users mailing list. Most questions about running software on PlanetLab can be answered by posting to this list. Site administrators often use this list to post announcements about service outages. New software releases and available services are announced here as well.\n";
+
+echo "
\n";
+
+// displays a site select list for admins and multi-site users
+if( count( $_person['site_ids'] ) > 1 || in_array( 10, $_roles ) ) {
+ // get sites depending on role and sites associated.
+ if( in_array( 10, $_roles ) )
+ $site_info= $api->GetSites( NULL, array( "name", "site_id", "login_base" ) );
+ elseif( count( $_person['site_ids'] ) > 1 )
+ $site_info= $api->GetSites( $_person['site_ids'], array( "name", "site_id", "login_base" ) );
+
+ echo "Site: \n";
+
+}
+
+echo "Name: ". $error['name'] ." \n";
+echo "URL: ". $error['url'] ." \n";
+echo "Description: ". $error['description'] ." \n";
+echo "Instantiation: \n";
+
+echo "
\n";
+
+echo "\n";
+
+echo "
\n";
+
+
+
+
+// Print footer
+include 'plc_footer.php';
+
+?>
diff --git a/planetlab/slices/attrib_action.php b/planetlab/slices/attrib_action.php
new file mode 100644
index 0000000..789695a
--- /dev/null
+++ b/planetlab/slices/attrib_action.php
@@ -0,0 +1,133 @@
+person;
+$_roles= $_person['role_ids'];
+
+
+// ATTRIBUTES -------------------------------------------------
+
+// attribute deletion
+if( $_GET['rem_id'] ) {
+ // get the id of the attrib to remove from GET
+ $attribute_id= intval( $_GET['rem_id'] );
+
+ // get slice_id
+ $attrib_info= $api->GetSliceAttributes( array( $attribute_id ), array( "slice_id" ) );
+ $slice_id= $attrib_info[0]['slice_id'];
+
+ // delete the attribute
+ $api->DeleteSliceAttribute( $attribute_id );
+
+
+ header( "location: index.php?id=$slice_id" );
+ exit();
+}
+
+
+// attirbute updates
+if( $_POST['edit_attribute'] ) {
+ // get the id of the attrib to update and teh value from POST
+ $attribute_id= intval( $_POST['attribute_id'] );
+ $value= $_POST['value'];
+ $slice_id= $_POST['slice_id'];
+
+ // update it!
+ $api->UpdateSliceAttribute( $attribute_id, $value );
+
+ header( "location: index.php?id=$slice_id" );
+ exit();
+}
+
+
+// attribute adds
+if( $_POST['add_attribute'] ) {
+ // get the slice_id, attribute_type_id, and value from POST
+ $slice_id= intval( $_POST['slice_id'] );
+ $attribute_type_id= intval( $_POST['attribute_type_id'] );
+ $value= $_POST['value'];
+
+ // add it!
+ $api->AddSliceAttribute( $slice_id, $attribute_type_id, $value );
+
+ header( "location: index.php?id=$slice_id" );
+ exit();
+}
+
+// ATTRIBUTE TYPES ---------------------------------------------------
+
+// attribute type adds
+if( $_POST['add_type'] ) {
+ // get post vars
+ $name= $_POST['name'];
+ $min_role_id= intval( $_POST['min_role_id'] );
+ $description= $_POST['description'];
+
+ // make the attribute_type_fields dict
+ $attribute_type_fields= array( "min_role_id" => $min_role_id, "name" => $name, "description" => $description );
+
+ // add it!!
+ $api->AddSliceAttributeType( $attribute_type_fields );
+
+ header( "location: attributes.php" );
+ exit();
+}
+
+
+// attribute type updates
+if( $_POST['edit_type'] ) {
+ // get post vars
+ $name= $_POST['name'];
+ $min_role_id= intval( $_POST['min_role_id'] );
+ $description= $_POST['description'];
+ $attribute_type_id= intval( $_POST['attribute_type_id'] );
+
+ // make attribute_type_fields dict
+ $attribute_type_fields= array( "min_role_id" => $min_role_id, "name" => $name, "description" => $description );
+
+ // Update it!
+ $api->UpdateSliceAttributeType( $attribute_type_id, $attribute_type_fields );
+
+ header( "location: attributes.php" );
+ exit();
+}
+
+
+// delete attribute types
+if( $_GET['del_type'] ) {
+ // get vars
+ $type_id= intval( $_GET['del_type'] );
+
+ // delete it!
+ $api->DeleteSliceAttributeType( $type_id );
+
+ header( "location: attributes.php" );
+ exit();
+}
+
+
+
+/*
+// Print footer
+include 'plc_footer.php';
+*/
+
+?>
diff --git a/planetlab/slices/attributes.php b/planetlab/slices/attributes.php
new file mode 100644
index 0000000..f7479e1
--- /dev/null
+++ b/planetlab/slices/attributes.php
@@ -0,0 +1,177 @@
+person;
+$_roles= $_person['role_ids'];
+
+//print_r( $_person );
+
+// if no id, display list of attributes types
+if( !$_GET['id'] && !$_GET['add'] && !$_GET['add_type'] && !$_GET['edit_type'] ) {
+ // get types
+ $attrib_types= $api->GetSliceAttributeTypes( NULL, array( "attribute_type_id", "name", "description", "min_role_id" ) );
+
+ // get role names for the min role_ids
+ foreach( $attrib_types as $attrib_type ) {
+ $roles= $api->GetRoles();
+ foreach( $roles as $role ) {
+ if( $attrib_type['min_role_id'] == $role['role_id'] )
+ $role_name= $role['name'];
+ }
+
+ $attrib_type_info[]= array( "attribute_type_id" => $attrib_type['attribute_type_id'], "name" => $attrib_type['name'], "description" => $attrib_type['description'], "min_role" => $role_name );
+ }
+
+ // list them
+ echo "Slice Attribute Types
\n";
+
+ echo "Name Min Role Description ";
+ // if admin we need to more cells
+ if( in_array( "10", $_person['role_ids'] ) )
+ echo " ";
+ echo " ";
+
+ foreach( $attrib_type_info as $type ) {
+ echo "". $type['name'] ." ". $type['min_role'] ." ". $type['description'] ." ";
+ // if admin display edit/delet links
+ if( in_array( "10", $_person['role_ids'] ) ) {
+ echo "Edit ";
+ echo "";
+ echo plc_delete_link_button ('attrib_action.php?del_type='. $type['attribute_type_id'],
+ $type['name']);
+ echo " ";
+ }
+ echo " \n";
+
+ }
+
+ echo "
\n";
+
+ if( in_array( "10", $_person['role_ids'] ) )
+ echo "Add an Attribute Type";
+
+}
+elseif( $_GET['add_type'] || $_GET['edit_type'] ) {
+ // if its edit get the attribute info
+ if( $_GET['edit_type'] ) {
+ $type_id= intval( $_GET['edit_type'] );
+ $type_info= $api->GetSliceAttributeTypes( array( $type_id ) );
+
+ $name= $type_info[0]['name'];
+ $min_role_id= $type_info[0]['min_role_id'];
+ $description= $type_info[0]['description'];
+
+ }
+
+ // display form for attribute types
+ echo "
\n";
+ echo "Add Attribute Type
\n";
+ echo "Name: \n";
+ echo "
Min Role: \n";
+ echo "
Description:
\n";
+ echo "\n";
+ echo "
\n";
+ echo "\n";
+ }
+ else
+ echo "name='add_type' value='Add Attribute Type'>\n";
+
+ echo "
\n";
+
+}
+elseif( $_GET['add'] ) {
+ // get slice id from GET
+ $slice_id= intval( $_GET['add'] );
+
+ // get all attribute types
+ $attrib_types= $api->GetSliceAttributeTypes( NULL, array( "attribute_type_id", "name" ) );
+
+ foreach( $attrib_types as $attrib_type ) {
+ $all_attribs[$attrib_type['attribute_type_id']]= $attrib_type['name'];
+ }
+
+ // get slice's attribute types
+ $slice_info= $api->GetSlices( array( $slice_id ), array( "slice_attribute_ids" ) );
+ $attrib_info= $api->GetSliceAttributes( $slice_info[0]['slice_attribute_ids'], array( "attribute_type_id", "name" ) );
+
+ foreach( $attrib_info as $info ) {
+ $slice_attrib_types[$info['attribute_type_id']]= $info['name'];
+ }
+
+
+ $attribute_types= $all_attribs;
+
+ // start form
+ echo "\n";
+ echo "Edit ". $slice_info[0]['name'] ." attribute: ". $attrib_type[0]['name'] ."
\n";
+
+ echo "\n";
+
+ echo "Value: \n";
+
+ echo "
\n";
+ echo "\n";
+ echo "
\n";
+
+}
+else {
+ $attribute_id= intval( $_GET['id'] );
+
+ // get attribute info
+ $slice_attib= $api->GetSliceAttributes( array( $attribute_id ), array( "slice_id", "slice_attribute_id", "attribute_type_id", "value", "description", "min_role_id" ) );
+
+ // get type info
+ $attrib_type= $api->GetSliceAttributeTypes( array( $slice_attib[0]['attribute_type_id'] ), array( "attribute_type_id", "name", "description" ) );
+
+ // slice info
+ $slice_info= $api->GetSlices( array( $slice_attib[0]['slice_id'] ), array( "name" ) );
+
+ // start form and put values in to be edited.
+ echo "\n";
+ echo "Edit ". $slice_info[0]['name'] ." attribute: ". $attrib_type[0]['name'] ."
\n";
+
+ echo $slice_attib[0]['description'] ."
\n";
+ echo "Value:
\n";
+
+ echo "\n";
+ echo "\n";
+ echo "\n";
+ echo " \n";
+
+}
+
+echo "Back to Slices\n";
+
+// Print footer
+include 'plc_footer.php';
+
+?>
diff --git a/planetlab/slices/delete_slice.php b/planetlab/slices/delete_slice.php
new file mode 100644
index 0000000..c398517
--- /dev/null
+++ b/planetlab/slices/delete_slice.php
@@ -0,0 +1,62 @@
+person;
+$_roles= $_person['role_ids'];
+
+//print_r( $_person );
+
+// if no id then go back to slice index
+if( !$_GET['id'] )
+ header( "index.php" );
+
+// get slice id from get
+$slice_id= $_GET['id'];
+
+// delete it!
+if( $_POST['delete'] ) {
+ $api->DeleteSlice( intval( $slice_id ) );
+
+ header( "location: index.php" );
+ exit();
+}
+
+// Print header
+require_once 'plc_drupal.php';
+drupal_set_title('Slices');
+include 'plc_header.php';
+
+
+// get slice info from API call
+$slice_info= $api->GetSlices( array( intval( $slice_id ) ), array( "name", "expires", "description" ) );
+
+// start form
+echo "
\n";
+
+// show delete confirmation
+echo "Delete slice ". $slice_info[0]['name'] ."
\n";
+echo "Are you sure you want to delete this slice?\n";
+
+echo "
\n";
+echo "Name: ". $slice_info[0]['name'] ." \n";
+echo "Description: ". $slice_info[0]['description'] ." \n";
+echo "Expiration: ". gmstrftime( "%A %b-%d-%y %T %Z", $slice_info[0]['expires'] ) ." \n";
+echo "
\n";
+echo "\n";
+echo "
diff --git a/planetlab/slices/index.php b/planetlab/slices/index.php
new file mode 100644
index 0000000..5ffa1ed
--- /dev/null
+++ b/planetlab/slices/index.php
@@ -0,0 +1,509 @@
+person;
+$_roles= $_person['role_ids'];
+
+//print_r( $_person );
+
+
+// if node_host is set then set id to that node's id.
+if( $_POST['slicename'] ) {
+ $slicename= $_POST['slicename'];
+
+ $slice_info= $api->GetSlices( array( $slicename ), array( "slice_id" ) );
+
+ header( "location: index.php?id=". $slice_info[0]['slice_id'] );
+ exit();
+
+}
+
+
+// if no slice id, display list of slices
+if( !$_GET['id'] ) {
+ // diplay site select list for admins
+ if( in_array( 10, $_roles ) ) {
+ // auto complete box for finding a slice
+
+ drupal_set_html_head('
+
+ ');
+
+ echo "\n
+ \n";
+ if( $slicename ) echo "'$slicename' is not a valid slice name.\n";
+ echo "\n
+ \n
+ \n
+
\n
+
\n";
+
+ // get site info
+ $site_info= $api->GetSites( NULL, array( "site_id", "name", "peer_id" ) );
+ sort_sites( $site_info );
+
+ // Thierry -- try to select only one entry
+ // xxx still not right if _person in several sites, but that is good enough
+ //if( $site['site_id'] == $_POST['site_id'] || in_array( $site['site_id'], $_person['site_ids'] ) )
+ if ($_POST['site_id'])
+ $selected_site_id = $_POST['site_id'];
+ else if ($_GET['site_id'])
+ $selected_site_id = $_GET['site_id'];
+ else
+ $selected_site_id = $_person['site_ids'][0];
+
+ echo "Select a site to view slices from: ";
+ echo "\n";
+
+ }
+
+ if( $_POST['site_id'] ) {
+ $selection="Site";
+ $site= array( intval( $_POST['site_id'] ) );
+ } elseif( $_GET['site_id'] ) {
+ $selection="Site";
+ $site= array( intval( $_GET['site_id'] ) );
+ } else {
+ $selection="Person";
+ $site= $_person['site_ids'];
+ }
+
+ // get site's slices
+ $site_info= $api->GetSites( $site, array( "slice_ids","name" ) );
+
+ if ( $selection == "Site" )
+ drupal_set_title ("Slices for site " . $site_info[0]['name']);
+ else
+ drupal_set_title ("Slices for " . $_person['email'] . "'s sites");
+
+ // make an array of all slices
+ foreach( $site_info as $site ) {
+ foreach( $site['slice_ids'] as $slice_id ) {
+ $slice_ids[]= $slice_id;
+ }
+
+ }
+
+ if (empty ($slice_ids)) {
+ echo "No slice found, or all are expired.";
+ } else {
+
+ $slice_info= $api->GetSlices( $slice_ids, array( "slice_id", "name", "site_id", "state", "person_ids", "expires", "peer_id" ) );
+ //print '
'; print_r( $api->trace() ) ; print '
';
+
+ if ( ! $slice_info) {
+ echo "No Slices on site, or all are expired.\n";
+ } else {
+ echo "
\n";
+ echo "Slice list \n";
+ echo "Slice Name ";
+ echo "Users ";
+ echo "Expiration ";
+ echo "\n";
+
+ // create a list of person_ids
+ $person_ids = array();
+ foreach( $slice_info as $slice ) {
+ if ( !empty($slice['person_ids']) )
+ $person_ids = array_merge($person_ids, $slice['person_ids']);
+ }
+
+ // create an associative array of persons with person_id as the key
+ $person_list = $api->GetPersons( $person_ids, array("person_id", "email") );
+ $persons = array();
+ foreach( $person_list as $person)
+ {
+ $persons[$person['person_id']] = $person;
+ }
+
+ foreach( $slice_info as $slice ) {
+ $slice_id= $slice['slice_id'];
+ $slice_name= $slice['name'];
+ $slice_state= $slice['state'];
+ $slice_expires= date( "M j, Y", $slice['expires'] );
+ $peer_id = $slice['peer_id'];
+
+ $extraclass="";
+ if ( $peer_id )
+ $extraclass="plc-foreign";
+
+ echo "$slice_name ";
+
+ if( !empty( $slice['person_ids'] ) ) {
+ foreach( $slice['person_ids'] as $person_id ) {
+ $person = $persons[$person_id];
+ $id= $person['person_id'];
+ $email= $person['email'];
+ echo "$email
\n";
+ }
+ } else {
+ echo "None";
+ }
+
+ echo " $slice_expires \n";
+
+ }
+
+ echo "
\n";
+
+ }
+ }
+
+
+ echo "\n";
+
+ echo "\n";
+
+}
+// if nothing else then show slice info
+else {
+ $slice_id= intval( $_GET['id'] );
+
+ // GetSlices API call
+ $slice_info= $api->GetSlices( array( $slice_id ) );
+
+ if( empty( $slice_info ) ) {
+ header( "location: index.php" );
+ exit();
+ }
+
+ // pull all slice info to vars
+ $instantiation= $slice_info[0]['instantiation'];
+ $name= $slice_info[0]['name'];
+ $url= $slice_info[0]['url'];
+ $expires= date( "M j, Y", $slice_info[0]['expires'] );
+ $site_id= $slice_info[0]['site_id'];
+ $description= $slice_info[0]['description'];
+ $max_nodes= $slice_info[0]['max_nodes'];
+ $node_ids=$slice_info[0]['node_ids'];
+ $person_ids=$slice_info[0]['node_ids'];
+
+ // get peer id
+ $peer_id= $slice_info[0]['peer_id'];
+
+ $person_ids= $slice_info[0]['person_ids'];
+ $node_ids= $slice_info[0]['node_ids'];
+ $slice_attribute_ids= $slice_info[0]['slice_attribute_ids'];
+
+
+ // node info
+// looks unused
+// if( !empty( $node_ids ) )
+// $nodes= $api->GetNodes( $node_ids, array( "node_id", "hostname" ) );
+
+ // site info
+ $site_info= $api->GetSites( array( $site_id ), array( "site_id", "name", "person_ids" ) );
+
+ // gets all persons from site_id
+ // person info
+ if( !empty( $person_ids ) )
+ $persons= $api->GetPersons( $site_info[0]['person_ids'] , array( "person_id", "role_ids", "first_name", "last_name", "email" ) );
+
+ if( $persons ) {
+ // gets site contacts pis stores in dict
+ foreach( $persons as $person )
+ if( in_array( "20", $person['role_ids'] ) ) {
+ $pis[]= array( "email" => $person['email'], "first_name" => $person['first_name'], "last_name" => $person['last_name'], "person_id" => $person['person_id'] );
+
+ }
+ if ($pis) {
+ sort_persons( $pis );
+ }
+ }
+
+ // slice attribute info
+ if( !empty( $slice_attribute_ids ) )
+ $slice_attibs= $api->GetSliceAttributes( $slice_attribute_ids,
+ array( "slice_attribute_id", "attribute_type_id", "value", "description", "min_role_id", "node_id" ) );
+
+ // gets attrib type info and combines it to form all attrib info array
+ if( $slice_attibs ) {
+ foreach( $slice_attibs as $slice_attib ) {
+ $attrib_type= $api->GetSliceAttributeTypes( array( $slice_attib['attribute_type_id'] ),
+ array( "attribute_type_id", "name", "description" ) );
+
+ $attributes[]= array( "slice_attribute_id" => $slice_attib['slice_attribute_id'],
+ "attribute_type_id" => $slice_attib['attribute_type_id'],
+ "name" => $attrib_type[0]['name'],
+ "value" => $slice_attib['value'],
+ "description" => $slice_attib['description'],
+ "min_role_id" => $slice_attib['min_role_id'],
+ "node_id" => $slice_attib['node_id'] );
+ }
+
+ }
+
+ drupal_set_title("Slice details for " . $name);
+ // start form
+
+ if( $peer_id ) {
+ echo "\n";
+ }
+
+ // basic slice menu
+ if( ! $peer_id ) {
+
+ $actions= array( ''=>'Choose Action' );
+
+ if( in_array( 10, $_roles )
+ || ( in_array( 20, $_roles ) && in_array( $site_id, $_person['site_ids'] ) )
+ || in_array( $slice_id, $_person['slice_ids'] ) ) {
+ $actions['renew']= "Renew $name";
+ $actions['nodes']= "Manage Nodes";
+ }
+ if ( in_array( 10, $_roles )
+ || ( in_array( 20, $_roles ) && in_array( $site_id, $_person['site_ids'] ) ) ) {
+ $actions['users']= "Manage Users";
+ $actions['delete']= "Delete $name";
+ }
+
+ echo "\n";
+ if (in_array( 10, $_roles )) {
+ echo plc_event_button("slices","slice",$slice_id);
+ echo " ";
+ }
+ echo plc_comon_button("slice_id",$slice_id);
+ echo " \n";
+
+ echo "\n";
+ echo "\n";
+
+ echo "
\n";
+ echo " \n";
+
+ echo "
\n";
+ }
+
+ echo "\n
+ Slice Name: $name \n
+ Description: $description \n
+ URL: $url \n";
+
+ if( gmmktime() > $slice_info[0]['expires'] ) {
+ $class1= ' style="color:red;"';
+ $msg1= '(slice is expired)';
+ }
+ echo "Expiration: $expires $msg1 \n";
+ echo "Instantiation: ";
+
+ echo " \n";
+ echo "Site: ". $site_info[0]['name'] ." \n";
+ $href="'/db/nodes/index.php?slice_id=" . $slice_id . "'";
+ printf (" # Nodes Total %d nodes \n",$href,$href,count($node_ids));
+ $href="'/db/persons/index.php?slice_id=" . $slice_id . "'";
+ printf (" # Users Total %d users \n",$href,$href,count($person_ids));
+ echo "
\n";
+
+ if ( (!$class1) && in_array( $slice_id, $_person['slice_ids'] ) && (! $peer_id) )
+ echo "Update Information\n";
+
+ echo "
\n";
+
+
+ // slice attributes
+ if( $attributes ) {
+
+ // builds 2 arrays, one for attribs,one for slivers
+ foreach( $attributes as $attribute ) {
+ if( empty( $attribute['node_id'] ) ) {
+ $slice_attrib[]= $attribute;
+ }
+ else {
+ $sliver_attrib[]= $attribute;
+ $sliver_nodes[]= $attribute['node_id'];
+ }
+ }
+ }
+
+ // Get node info for those slivers
+ $sliver_node_info= $api->GetNodes( $sliver_nodes, array( "node_id", "hostname" ) );
+
+ if( $sliver_node_info ) {
+ foreach( $sliver_node_info as $sliv_node ) {
+ $new_sliver_node_info[$sliv_node['node_id']]= $sliv_node;
+ }
+ }
+
+ if( $peer_id ) {
+ echo "
\n";
+ }
+
+ // slice attributes
+ $is_admin=in_array( 10, $_roles );
+ $is_in_slice=in_array( $slice_id, $_person['slice_ids'] );
+ $is_pi=in_array( 20, $_roles );
+ if( $slice_attrib ) {
+ echo "Slice Attributes ";
+ echo "";
+ if( $is_admin )
+ echo " ";
+ echo "Attribute Value Description ";
+ echo " \n";
+
+ foreach( $attributes as $attribute ) {
+ // ignore sliver attributes at this stage
+ if( empty( $attribute['node_id'] ) ) {
+ echo("");
+ if( $is_admin ) {
+ printf("");
+ sprintf($label,"\\n [ %s = %s] \\n from %s",$attribute['name'],$attribute['value'],$name);
+ echo plc_delete_link_button ('attrib_action.php?rem_id=' . $attribute['slice_attribute_id'],
+ $label);
+ echo " ";
+ }
+ if( $is_admin || ($is_pi && $is_in_slice) ) {
+ printf ("%s ",
+ $attribute['slice_attribute_id'],$attribute['name']);
+ } else {
+ printf("%s ",$attribute['name']);
+ }
+ printf("%s %s ",
+ $attribute['value'],$attribute['description']);
+ echo " ";
+ }
+ }
+
+
+ echo "
\n";
+
+ }
+ if( $is_admin || ($is_pi && $is_in_slice) )
+ echo "Add a Slice Attribute\n";
+
+
+
+ // sliver attributes
+ if( $sliver_attrib ) {
+ echo "Sliver Attributes ";
+ echo "";
+ if( $is_admin )
+ echo " ";
+ echo "Attribute Value Description Node ";
+ echo " \n";
+
+ foreach( $attributes as $attribute ) {
+ $nodename=$new_sliver_node_info[$attribute['node_id']]['hostname'];
+ // consider only sliver attributes at this stage
+ if( !empty( $attribute['node_id'] ) ) {
+ echo("");
+ if( $is_admin ) {
+ echo("");
+ $label=sprintf("\\n [ %s = %s ] \\n from %s \\n on node %s",
+ $attribute['name'],$attribute['value'],$name,$nodename);
+ echo plc_delete_link_label('/db/nodes/sliver_action.php?rem_id=' . $attribute['slice_attribute_id'],
+ $label);
+ echo " ";
+ }
+ if( $is_admin ) {
+ printf("%s ",$attribute['slice_attribute_id'],$attribute['name']);
+ } else {
+ printf("%s ",$attribute['name']);
+ }
+ printf("%s %s %s ",
+ $attribute['value'],$attribute['description'],$attribute['node_id'],$nodename);
+
+ echo " ";
+ }
+ }
+
+ echo "
\n";
+
+ }
+
+ echo "
\n";
+
+ if( $pis && !$peer_id ) {
+ // site contacts
+ echo "Contacts
\n";
+
+ $pi_rows= count( $pis );
+ $tech_rows= count( $techs );
+ $table_row= 0;
+
+ echo "";
+ if( $pis ) {
+ echo "PI's: \n";
+
+ foreach( $pis as $pi ) {
+ if( $table_row != 0 )
+ echo "";
+ printf("%s %s %s \n",
+ $pi['person_id'],$pi['first_name'],$pi['last_name'],$pi['email'],$pi['email']);
+ $table_row++;
+ }
+
+ }
+
+ echo "
\n
\n";
+
+ }
+
+
+ echo "Back to slice list\n";
+ }
+
+// Print footer
+include 'plc_footer.php';
+
+?>
diff --git a/planetlab/slices/renew_slice.php b/planetlab/slices/renew_slice.php
new file mode 100644
index 0000000..88512c4
--- /dev/null
+++ b/planetlab/slices/renew_slice.php
@@ -0,0 +1,173 @@
+person;
+$_roles= $_person['role_ids'];
+
+
+// Constants
+$week= 7 * 24 * 60 * 60; // seconds
+$max_renewal_length= 8; // weeks from today
+$max_expiration= mktime() + ($max_renewal_length * $week); // seconds since epoch
+$max_expiration_date= gmstrftime("%A %b-%d-%y %T %Z", $max_expiration);
+
+// if submitted validate input
+if( $_POST['submitted'] ) {
+ // get post vars
+ $expire_len= $_POST['expire_len'];
+ $expires= $_POST['expires'];
+ $slice_id= intval( $_POST['id'] );
+
+ // create empty error array
+ $error= array( );
+
+ // check input
+
+ $url= $_POST['url'];
+ if( $url == '' || empty( $url ) )
+ $error['url']= "Provide a link to a project website.";
+
+ $description= htmlspecialchars( $_POST['description'] );
+ if ( $description == '' || empty( $description ) )
+ $error['description']= "Provide a short description of the slice.";
+
+ // if no errors update slice info
+ if( empty( $error ) ) {
+ // set new expiration
+
+ $expires+= ( $expire_len * $week );
+
+ // make slice field array
+ $slice_fields= array( "url" => $url, "description" => $description, "expires" => $expires );
+
+ // Update it!
+ $api->UpdateSlice( $slice_id, $slice_fields );
+
+ header( "location: index.php?id=$slice_id" );
+ exit();
+
+ }
+
+}
+
+// if no id is set redirect back to slice index
+if( !$_POST['id'] && !$_GET['id'] ) {
+ header( "location: index.php" );
+ exit();
+ }
+
+// Print header
+require_once 'plc_drupal.php';
+drupal_set_title('Slice Renewal');
+include 'plc_header.php';
+
+
+
+
+// get id
+if( $_GET['id'] )
+ $slice_id= intval( $_GET['id'] );
+if( $_POST['id'] )
+ $slice_id= intval( $_POST['id'] );
+
+// get slice info
+$slice_info= $api->GetSlices( array( $slice_id ), array( "expires", "name", "site_id", "description", "url" ) );
+
+echo "
Slice ". $slice_info[0]['name'] ." Renewal
\n";
+
+// get site info
+if( !empty( $slice_info[0]['site_id'] ) ) {
+ // get sliver/slice site info
+ $site_info= $api->GetSites( array( $slice_info[0]['site_id'] ), array( "max_slivers", "max_slices" ) );
+
+ // do not allow renew if max_slices are 0
+ if( $site_info[0]['max_slices'] <= 0 ) {
+ $support= '';
+ $site_id= $slice_info[0]['site_id'];
+
+ echo "Slice creation and renewal have been temporarily disabled for your site. This may have occurred because your site's nodes have been down or unreachable for several weeks, and multiple attempts to contact your site's PI(s) and Technical Contact(s) have all failed. If so, contact your site's PI(s) and Technical Contact(s) and ask them to bring up your site's nodes. If you believe that your site's nodes are up and reachable. Visit your site's Site Details page to find out more about your site's nodes, and how to contact your site's PI(s) and Technical Contact(s).
";
+
+ }
+ // else start renewal form
+ else {
+ // Calculate possible extension lengths
+ $renewal_lengths = array();
+ foreach ( array( 1 => "One more week", 2 => "Two more weeks", 4 => "One more month" ) as $weeks => $text ) {
+ if ( ( $slice_info[0]['expires'] + ( $weeks * $week ) ) < $max_expiration ) {
+ $renewal_lengths[$weeks]= "$text (". gmstrftime( "%A %b-%d-%y %T %Z", $slice_info[0]['expires'] + ( $weeks * $week ) ) .")";
+ }
+ }
+
+
+ if ( empty( $renewal_lengths ) ) {
+ echo "Slice cannot be renewed any further into the future, try again closer to expiration date. Go back to ". $slice_info[0]['name'] .".\n";
+ }
+ else {
+ // clean vars
+ $expiration_date = gmstrftime( "%A %b-%d-%y %T %Z", $slice_info[0]['expires'] );
+
+ // display form
+ echo "\n";
+ echo "\n";
+
+ echo "You must provide a short description as well as a link to a project website before renewing it. Do not provide bogus information; if a complaint is lodged against your slice and PlanetLab Operations is unable to determine what the normal behavior of your slice is, your slice may be deleted to resolve the complaint.
\n";
+
+ echo "NOTE: Slices cannot be renewed beyond $max_renewal_length weeks of today ($max_expiration_date).
\n";
+
+ echo "\n";
+
+ echo "Name: ". $slice_info[0]['name'] ." \n";
+
+ if( $error['url'] )
+ $url_style= " style='border: 1px solid red;'";
+ echo "URL: ". $error['url'] ." \n";
+
+ if( $error['description'] )
+ $desc_style= " style='border: 1px solid red;'";
+ echo "Description: ". $error['description'] ." \n";
+
+ echo "Expiration Date: $expiration_date \n";
+
+ echo "Renewal Length: \n \n
\n";
+
+ }
+
+ }
+
+}
+else
+ echo "No data for this slice ID. Go back to slices.\n";
+
+
+
+
+
+
+
+
+
+// Print footer
+include 'plc_footer.php';
+
+?>
diff --git a/planetlab/slices/slice_action.php b/planetlab/slices/slice_action.php
new file mode 100644
index 0000000..98a7d67
--- /dev/null
+++ b/planetlab/slices/slice_action.php
@@ -0,0 +1,64 @@
+person;
+$_roles= $_person['role_ids'];
+
+
+
+// echo ""; print_r( $_POST ); echo "
";
+// SLICES ------------------------------------------------------
+
+// if action exists figure out what to do
+if( $_POST['actions'] ) {
+ // get slice_id as int
+ $slice_id= intval( $_POST['slice_id'] );
+
+ // depending on action, run function
+ switch( $_POST['actions'] ) {
+ case "renew":
+ header( "location: renew_slice.php?id=$slice_id" );
+ exit();
+ break;
+ case "delete":
+ header( "location: delete_slice.php?id=$slice_id" );
+ exit();
+ break;
+ case "nodes":
+ header( "location: slice_nodes.php?id=$slice_id" );
+ exit();
+ break;
+ case "users":
+ header( "location: slice_users.php?id=$slice_id" );
+ exit();
+ break;
+
+ }
+
+}
+
+
+/*
+// Print footer
+include 'plc_footer.php';
+*/
+
+?>
diff --git a/planetlab/slices/slice_nodes.php b/planetlab/slices/slice_nodes.php
new file mode 100644
index 0000000..5a00ab0
--- /dev/null
+++ b/planetlab/slices/slice_nodes.php
@@ -0,0 +1,264 @@
+person;
+$_roles= $_person['role_ids'];
+
+//print_r( $_person );
+
+// if no id ... redirect to slice index
+if( !$_GET['id'] && !$_POST['id'] ) {
+ header( "location: index.php" );
+ exit();
+ }
+
+
+// get slice id from GET or POST
+if( $_GET['id'] )
+ $slice_id= intval( $_GET['id'] );
+elseif ( $_POST['id'] )
+ $slice_id= intval( $_POST['id'] );
+else
+ echo "no slice_id
\n";
+
+
+// if add node submitted add the nodes to slice
+if( $_POST['add'] ) {
+ $add_nodes= $_POST['add_nodes'];
+
+ foreach( $add_nodes as $nodes) {
+ $new_nodes[]= intval( $nodes );
+ }
+
+ // update it!
+ $api->AddSliceToNodes( $slice_id, $new_nodes );
+
+ $errors= $api->error();
+
+ if( empty( $errors ) )
+ $added= "Nodes Added.
";
+ else
+ $added= "Error: '$errors'
";
+}
+
+// if rem node submitted remove the nodes from slice
+if( $_POST['remove'] ) {
+ $rem_nodes= $_POST['rem_nodes'];
+
+ foreach( $rem_nodes as $nodes) {
+ $new_nodes[]= intval( $nodes );
+ }
+
+ // Delete them!
+ $api->DeleteSliceFromNodes( $slice_id, $new_nodes );
+
+ $errors= $api->error();
+
+ if( empty( $errors ) )
+ $removed= "Nodes Removed.
";
+ else
+ $removed= "Error: '$errors'
";
+
+}
+
+
+// get slice info
+$slice_info= $adm->GetSlices( array( $slice_id ), array( "name", "node_ids", "peer_id" ) );
+$slice_readonly = $slice_info[0]['peer_id'];
+drupal_set_title("Slice " . $slice_info[0]['name'] . " - Nodes");
+
+// get node info
+if( !empty( $slice_info[0]['node_ids'] ) )
+ $node_info= $adm->GetNodes( $slice_info[0]['node_ids'], array( "hostname", "node_id", "site_id" , "peer_id") );
+
+// get site names and ids
+$site_info= $adm->GetSites( NULL, array( "site_id", "name", "peer_id" ) );
+sort_sites( $site_info );
+
+// if site_id is in post use it, if not use the user's primary
+if( $_POST['site_id'] )
+ $site_id= $_POST['site_id'];
+else
+ $site_id= $_person['site_ids'][0];
+
+
+// get site nodes for $site_id
+if( $site_id == 'all_site' ) {
+ $full_node_info= $adm->GetNodes( NULL, array( "hostname", "node_id" , "peer_id", "boot_state","last_updated") );
+
+ $snode_info= array();
+ foreach( $full_node_info as $full_node ) {
+ if( !in_array( $full_node['node_id'], $slice_info[0]['node_ids'] ) )
+ $snode_info[]= $full_node;
+ }
+}
+else {
+ $sid= intval( $site_id );
+ $site_node_info= $adm->GetSites( array( $sid ), array( "node_ids" ) );
+ $site_nodes= $site_node_info[0]['node_ids'];
+
+ // gets all node_ids from site that arent already associated with the slice
+ foreach( $site_nodes as $snode) {
+ if( !in_array( $snode, $slice_info[0]['node_ids'] ) )
+ $snodes[]= $snode;
+ }
+
+ // Get node info from new list
+ if( !empty( $snodes ) )
+ $snode_info= $adm->GetNodes( $snodes, array( "hostname", "node_id" , "peer_id", "boot_state","last_updated" ) );
+
+}
+
+// start form
+if ( $slice_readonly)
+ echo "";
+else
+ echo "\n";
+
+// section for adding nodes : for local slices only
+if ( ! $slice_readonly ) {
+ echo "
";
+ echo " Select a site to add nodes from.
\n";
+ echo "";
+ if ($site_id != 'all_site') {
+ echo plc_comon_button("site_id",$site_id,"_blank");
+ echo " ";
+ }
+ echo "
\n";
+
+ // show all availible nodes at $site_id
+ //echo ""; print_r( $snode_info ); echo "
";
+ if( $snode_info ) {
+ echo $added;
+ echo "\n";
+ echo " check Hostname Boot State Last Update
+ ";
+ foreach( $snode_info as $snodes ) {
+ $class="";
+ if ($snodes['peer_id']) {
+ $class="class='plc-foreign'";
+ }
+ echo "";
+ echo plc_comon_button("node_id",$snodes['node_id'],"_blank");
+ echo " ";
+ echo "";
+ echo " ";
+ echo $snodes['hostname'];
+ echo " ";
+ echo $snodes['boot_state'];
+ echo " ";
+ echo date('Y-m-d',$snodes['last_updated']);
+ echo " \n";
+ }
+
+ echo "
\n";
+ echo "\n";
+ } else {
+ echo "
No site nodes or all are already added.\n";
+ }
+}
+
+echo "
\n";
+
+// show all nodes currently associated
+echo $removed;
+echo "Nodes currently associated with slice
\n";
+if( $node_info ) {
+ if ( ! $slice_readonly) {
+ echo "Check boxes of nodes to remove:\n";
+ echo "\n";
+ echo " check Hostname Boot State Last Update
+ ";
+ } else {
+ echo "\n";
+ echo " check Hostname Boot State Last Update
+ ";
+ }
+
+ foreach( $node_info as $node ) {
+ $class="";
+ if ($node['peer_id']) {
+ $class="class='plc-foreign'";
+ }
+ if ( ! $slice_readonly) {
+ echo "";
+ echo plc_comon_button("node_id",$node['node_id'],"_blank");
+ echo " ";
+ echo "";
+ echo " " ;
+ echo $node['hostname'];
+ echo " ";
+ echo $snodes['boot_state'];
+ echo " ";
+ echo date('Y-m-d',$snodes['last_updated']);
+ echo " \n";
+ } else {
+ echo "";
+ echo plc_comon_button("node_id",$node['node_id'],"_blank");
+ echo " " ;
+ echo $node['hostname'];
+ echo " ";
+ }
+
+ }
+
+ echo "
\n";
+ if ( ! $slice_readonly)
+ echo "\n";
+
+} else {
+ echo "
No nodes associated with slice.\n";
+}
+
+if ($slice_readonly)
+ echo "";
+ else
+ echo "";
+
+echo "
Back to Slice\n";
+
+
+// Print footer
+include 'plc_footer.php';
+
+?>
diff --git a/planetlab/slices/slice_users.php b/planetlab/slices/slice_users.php
new file mode 100644
index 0000000..5fb2297
--- /dev/null
+++ b/planetlab/slices/slice_users.php
@@ -0,0 +1,208 @@
+person;
+$_roles= $_person['role_ids'];
+
+//print_r( $_person );
+
+// if no id ... redirect to slice index
+if( !$_GET['id'] && !$_POST['id'] ) {
+ header( "location: index.php" );
+ exit();
+ }
+
+
+// get slice id from GET or POST
+if( $_GET['id'] )
+ $slice_id= intval( $_GET['id'] );
+elseif ( $_POST['id'] )
+ $slice_id= intval( $_POST['id'] );
+else
+ echo "no slice_id
\n";
+
+// if add node submitted add the nodes to slice
+if( $_POST['add'] ) {
+ $add_user= $_POST['add_user'];
+
+ foreach( $add_user as $user) {
+ $api->AddPersonToSlice( intval( $user ), $slice_id );
+ }
+
+ $added= "People Added.
";
+}
+
+// if rem node submitted remove the nodes from slice
+if( $_POST['remove'] ) {
+ $rem_user= $_POST['rem_user'];
+
+ foreach( $rem_user as $user) {
+ $api->DeletePersonFromSlice( intval( $user ), $slice_id );
+ }
+
+ $removed= "People Removed.
";
+}
+
+// get slice info
+$slice_info= $api->GetSlices( array( $slice_id ), array( "name", "person_ids" , "peer_id") );
+$slice_readonly = $slice_info[0]['peer_id'];
+drupal_set_title("Slice " . $slice_info[0]['name'] . " - Users");
+
+// get person info
+if( !empty( $slice_info[0]['person_ids'] ) ) {
+ $person_info= $adm->GetPersons( $slice_info[0]['person_ids'], array( "first_name", "last_name", "email", "person_id","roles" ) );
+ sort_persons( $person_info );
+}
+
+// if site_id is in post use it, if not use the user's primary
+if( $_POST['site_id'] )
+ $site_id= $_POST['site_id'];
+else
+ $site_id= $_person['site_ids'][0];
+
+// get site nodes for $site_id
+$sid= intval( $site_id );
+$site_user_info= $adm->GetSites( array( $sid ), array( "person_ids" ) );
+$site_user= $site_user_info[0]['person_ids'];
+
+
+// gets all person_ids from site that arent already associated with the slice
+foreach( $site_user as $suser) {
+ if( !in_array( $suser, $slice_info[0]['person_ids'] ) )
+ $susers[]= $suser;
+
+}
+
+// Get person info from new list
+if( !empty( $susers ) ) {
+ $all_suser_info= $adm->GetPersons( $susers, array( "email", "first_name", "last_name", "person_id", "role_ids", 'roles' ) );
+//Filter the new list of user info to omit the tech user
+ foreach( $all_suser_info as $user_info) {
+ if ( (count($user_info["role_ids"])==1 ) && ( in_array(40, $user_info["role_ids"]) )) {
+ continue;
+ }
+ $suser_info[]= $user_info;
+ }
+ if ( ! empty($suser_info) ) {
+ sort_persons( $suser_info );
+ }
+ }
+
+
+// start form
+if ( $slice_readonly)
+ echo "
";
+else
+ echo "\n";
+
+// section for adding people : for local slices only
+if ( ! $slice_readonly ) {
+ echo "
";
+ echo "Select a site to add People from.
\n";
+ echo "\n";
+
+
+ if( $suser_info ) {
+ echo $added;
+ echo "\n";
+ echo " Email First Name Last Name Roles
+ ";
+ $proles="";
+ foreach( $suser_info as $susers ) {
+ foreach ( $susers['roles'] as $prole)
+ $proles.=" ".$prole;
+ echo " ". $susers['email'] ." ". $susers['first_name'] ." ". $susers['last_name'] ." ".$proles." \n";
+ unset($proles);
+ }
+
+ echo "
\n";
+ echo "\n";
+ } else {
+ echo "
All People on site already added.\n";
+ }
+ }
+
+
+echo "
\n";
+
+// show all people currently associated
+echo $removed;
+echo "People currently associated with slice
\n";
+if( $person_info ) {
+ if ( ! $slice_readonly ) {
+ echo "Check boxes of people to remove:\n";
+ echo "\n";
+ echo " Email First Name Last Name Roles
+ ";
+ } else {
+ echo "\n";
+ echo " E-mail First name Last name Roles ";
+ }
+
+ foreach( $person_info as $person ) {
+ foreach ( $person['roles'] as $prole)
+ $proles.=" ".$prole;
+ if ( ! $slice_readonly )
+ echo " ". $person['email'] ." ". $person['first_name']." ".$person['last_name'] ." ".$proles." \n";
+ else
+ echo "" . $person['email'] . " " . $person['first_name'] . " " . $person['last_name'] ." ".$proles." ";
+ unset($proles);
+ }
+
+ echo "
\n";
+ if ( ! $slice_readonly )
+ echo "\n";
+
+} else {
+ echo "
No People associated with slice.\n";
+}
+
+if ($slice_readonly)
+ echo "";
+ else
+ echo "";
+
+echo "
Back to Slice\n";
+
+// Print footer
+include 'plc_footer.php';
+
+?>
+
diff --git a/planetlab/slices/test.php b/planetlab/slices/test.php
new file mode 100644
index 0000000..e293eb7
--- /dev/null
+++ b/planetlab/slices/test.php
@@ -0,0 +1,69 @@
+>> GetSlices({'name':'o*','~expires':0})
+ //Database error 52cfdd40-b57e-43cb-ace0-406d49408527:
+ //unindexable object
+ //Query:
+ //SELECT creator_person_id, instantiation, slice_attribute_ids, name, slice_id, created, url, max_nodes, person_ids, expires, site_id, peer_slice_id, node_ids, peer_id, description FROM view_slices WHERE is_deleted IS False AND expires > 1175085968 AND (True AND name LIKE 'o%' AND ( NOT expires = 0 ) )
+ //Params:
+ //{'api': ,
+ // 'columns': None,
+ // 'expires': 1175085968,
+ // 'self': [],
+ // 'slice_filter': {'creator_person_id': (, []), 'instantiation': (, []), 'name': (, []), 'slice_id': (, []), 'created': (, []), 'url': (, []), 'max_nodes': (, []), 'expires': (, []), 'site_id': (, []), 'peer_slice_id': (, []), 'peer_id': (, []), 'description': (, [])},
+ // 'sql': "SELECT creator_person_id, instantiation, slice_attribute_ids, name, slice_id, created, url, max_nodes, person_ids, expires, site_id, peer_slice_id, node_ids, peer_id, description FROM view_slices WHERE is_deleted IS False AND expires > 1175085968 AND (True AND name LIKE 'o%' AND ( NOT expires = 0 ) )"}
+ //Traceback (most recent call last):
+ // File "/usr/bin/plcsh", line 139, in ?
+ // result = eval(command)
+ // File "", line 0, in ?
+ // File "/usr/share/plc_api/PLC/Shell.py", line 51, in __call__
+ // return self.func(*args, **kwds)
+ // File "/usr/share/plc_api/PLC/Method.py", line 105, in __call__
+ // raise fault
+ //PLCDBError: and reference 52cfdd40-b57e-43cb-ace0-406d49408527'>
+
+// Get session and API handles
+require_once 'plc_session.php';
+global $plc, $api, $adm;
+
+
+$arr= $adm->GetSlices( NULL, array( "name" ) );
+
+foreach( $arr as $slices ) {
+ $useArr[]= $slices['name'];
+}
+
+
+$input = strtolower( $_GET['input'] );
+$len = strlen($input);
+
+$aResults = array();
+
+if ($len) {
+ for ( $i=0; $i
+";
+for ( $i=0; $i". $aResults[$i] ."";
+
+echo "
+
+";
+
+?>
diff --git a/planetlab/slices/update_slice.php b/planetlab/slices/update_slice.php
new file mode 100644
index 0000000..6605915
--- /dev/null
+++ b/planetlab/slices/update_slice.php
@@ -0,0 +1,111 @@
+person;
+$_roles= $_person['role_ids'];
+
+
+if( !empty( $_GET['id'] ) ) {
+ $slice_id= $_GET['id'];
+ // Fetch slice information
+ $slices= $api->GetSlices( array( intval( $slice_id ) ) );
+ if( !empty( $slices ) ) {
+ $slice= $slices[0];
+ }
+}
+
+// Invalid slice name
+if( !isset( $slice ) ) {
+ Header( "Location: index.php" );
+ exit();
+}
+
+// Defaults
+$url_error = "";
+$description_error = "";
+
+if( isset( $_POST['submitted'] ) ) {
+ if( !empty($_POST['url'] ) )
+ $slice['url']= $_POST['url'];
+ else
+ $url_error= "Provide a link to a project website.";
+
+ if( !empty($_POST['desc'] ) )
+ $slice['description'] = $_POST['desc'];
+ else
+ $description_error= "Provide a short description of the slice.";
+
+ if( empty( $url_error ) && empty( $description_error ) ) {
+ // Update the slice URL and description
+ $fields= array( "description"=>$slice['description'], "url"=>$slice['url'] );
+ $api->UpdateSlice( intval( $slice_id ), $fields );
+ Header( "Location: index.php?id=$slice_id" );
+ exit();
+ }
+}
+
+// Print header
+require_once 'plc_drupal.php';
+drupal_set_title('Slices');
+include 'plc_header.php';
+
+$slice_name= $slice['name'];
+
+print "Update Slice ". $slice['name'] ."
";
+
+//echo ""; print_r( $slice ); echo "
";
+$url = $slice['url'] ;
+$description = $slice['description'] ;
+
+echo <<You must provide a short description as well as a
+link to a project website. Do not provide
+bogus information; if a complaint is lodged against your slice and
+PlanetLab Operations is unable to determine what the normal behavior
+of your slice is, your slice may be deleted to resolve the
+complaint.
+
+
+
+
+
+
+ Name:
+ $slice_name
+
+
+
+ URL:
+
+ $url_error
+
+
+ Description:
+
+ $description_error
+
+
+
+
+
+
+
+
+EOF;
+
+// Print footer
+include 'plc_footer.php';
+
+?>
\ No newline at end of file
diff --git a/planetlab/sulogout.php b/planetlab/sulogout.php
new file mode 100644
index 0000000..636c562
--- /dev/null
+++ b/planetlab/sulogout.php
@@ -0,0 +1,28 @@
+
+// Copyright (C) 2006 The Trustees of Princeton University
+//
+// $Id: sulogout.php 1154 2008-01-22 14:36:00Z thierry $ $
+//
+
+// Get session and API handles
+require_once 'plc_session.php';
+global $plc, $api;
+
+// Print header
+require_once 'plc_drupal.php';
+#drupal_set_title('Login');
+include 'plc_header.php';
+
+// Invalidate session
+if ($plc->person) {
+ $plc->BecomeSelf();
+}
+
+
+Header("Location: /db/persons/index.php");
+
+?>
diff --git a/plot-latlong/.mapimages/Africa.png b/plot-latlong/.mapimages/Africa.png
new file mode 100644
index 0000000..cd4cfea
Binary files /dev/null and b/plot-latlong/.mapimages/Africa.png differ
diff --git a/plot-latlong/.mapimages/Australia.png b/plot-latlong/.mapimages/Australia.png
new file mode 100644
index 0000000..c0f6b04
Binary files /dev/null and b/plot-latlong/.mapimages/Australia.png differ
diff --git a/plot-latlong/.mapimages/Belgium.png b/plot-latlong/.mapimages/Belgium.png
new file mode 100644
index 0000000..335faca
Binary files /dev/null and b/plot-latlong/.mapimages/Belgium.png differ
diff --git a/plot-latlong/.mapimages/Canada.png b/plot-latlong/.mapimages/Canada.png
new file mode 100644
index 0000000..b782efb
Binary files /dev/null and b/plot-latlong/.mapimages/Canada.png differ
diff --git a/plot-latlong/.mapimages/Caribbean.png b/plot-latlong/.mapimages/Caribbean.png
new file mode 100644
index 0000000..3d37287
Binary files /dev/null and b/plot-latlong/.mapimages/Caribbean.png differ
diff --git a/plot-latlong/.mapimages/CentralAmerica.png b/plot-latlong/.mapimages/CentralAmerica.png
new file mode 100644
index 0000000..67ae790
Binary files /dev/null and b/plot-latlong/.mapimages/CentralAmerica.png differ
diff --git a/plot-latlong/.mapimages/China.png b/plot-latlong/.mapimages/China.png
new file mode 100644
index 0000000..c825fc0
Binary files /dev/null and b/plot-latlong/.mapimages/China.png differ
diff --git a/plot-latlong/.mapimages/Europe.png b/plot-latlong/.mapimages/Europe.png
new file mode 100644
index 0000000..c5728cb
Binary files /dev/null and b/plot-latlong/.mapimages/Europe.png differ
diff --git a/plot-latlong/.mapimages/France.png b/plot-latlong/.mapimages/France.png
new file mode 100644
index 0000000..a1f3b2d
Binary files /dev/null and b/plot-latlong/.mapimages/France.png differ
diff --git a/plot-latlong/.mapimages/Germany.png b/plot-latlong/.mapimages/Germany.png
new file mode 100644
index 0000000..a8bfa4b
Binary files /dev/null and b/plot-latlong/.mapimages/Germany.png differ
diff --git a/plot-latlong/.mapimages/Hawaii.png b/plot-latlong/.mapimages/Hawaii.png
new file mode 100644
index 0000000..fa417f8
Binary files /dev/null and b/plot-latlong/.mapimages/Hawaii.png differ
diff --git a/plot-latlong/.mapimages/India.png b/plot-latlong/.mapimages/India.png
new file mode 100644
index 0000000..3c609dd
Binary files /dev/null and b/plot-latlong/.mapimages/India.png differ
diff --git a/plot-latlong/.mapimages/Italy.png b/plot-latlong/.mapimages/Italy.png
new file mode 100644
index 0000000..2f7ba30
Binary files /dev/null and b/plot-latlong/.mapimages/Italy.png differ
diff --git a/plot-latlong/.mapimages/Japan.png b/plot-latlong/.mapimages/Japan.png
new file mode 100644
index 0000000..b4997d5
Binary files /dev/null and b/plot-latlong/.mapimages/Japan.png differ
diff --git a/plot-latlong/.mapimages/Korea.png b/plot-latlong/.mapimages/Korea.png
new file mode 100644
index 0000000..d2e3af3
Binary files /dev/null and b/plot-latlong/.mapimages/Korea.png differ
diff --git a/plot-latlong/.mapimages/MalaysiaIndonesia.png b/plot-latlong/.mapimages/MalaysiaIndonesia.png
new file mode 100644
index 0000000..3d8c0a0
Binary files /dev/null and b/plot-latlong/.mapimages/MalaysiaIndonesia.png differ
diff --git a/plot-latlong/.mapimages/MiddleEast.png b/plot-latlong/.mapimages/MiddleEast.png
new file mode 100644
index 0000000..eb8847f
Binary files /dev/null and b/plot-latlong/.mapimages/MiddleEast.png differ
diff --git a/plot-latlong/.mapimages/NOSEFI.png b/plot-latlong/.mapimages/NOSEFI.png
new file mode 100644
index 0000000..a5c1de2
Binary files /dev/null and b/plot-latlong/.mapimages/NOSEFI.png differ
diff --git a/plot-latlong/.mapimages/Netherlands.png b/plot-latlong/.mapimages/Netherlands.png
new file mode 100644
index 0000000..1e87009
Binary files /dev/null and b/plot-latlong/.mapimages/Netherlands.png differ
diff --git a/plot-latlong/.mapimages/NewZealand.png b/plot-latlong/.mapimages/NewZealand.png
new file mode 100644
index 0000000..3750a0f
Binary files /dev/null and b/plot-latlong/.mapimages/NewZealand.png differ
diff --git a/plot-latlong/.mapimages/Philippines.png b/plot-latlong/.mapimages/Philippines.png
new file mode 100644
index 0000000..18c0a3f
Binary files /dev/null and b/plot-latlong/.mapimages/Philippines.png differ
diff --git a/plot-latlong/.mapimages/SouthAmerica.png b/plot-latlong/.mapimages/SouthAmerica.png
new file mode 100644
index 0000000..763db3f
Binary files /dev/null and b/plot-latlong/.mapimages/SouthAmerica.png differ
diff --git a/plot-latlong/.mapimages/UK.png b/plot-latlong/.mapimages/UK.png
new file mode 100644
index 0000000..71dd394
Binary files /dev/null and b/plot-latlong/.mapimages/UK.png differ
diff --git a/plot-latlong/.mapimages/USA100.png b/plot-latlong/.mapimages/USA100.png
new file mode 100644
index 0000000..542c8b3
Binary files /dev/null and b/plot-latlong/.mapimages/USA100.png differ
diff --git a/plot-latlong/.mapimages/USA200.png b/plot-latlong/.mapimages/USA200.png
new file mode 100644
index 0000000..8dbafae
Binary files /dev/null and b/plot-latlong/.mapimages/USA200.png differ
diff --git a/plot-latlong/.mapimages/USA50-new.png b/plot-latlong/.mapimages/USA50-new.png
new file mode 100644
index 0000000..1833e27
Binary files /dev/null and b/plot-latlong/.mapimages/USA50-new.png differ
diff --git a/plot-latlong/.mapimages/World100.png b/plot-latlong/.mapimages/World100.png
new file mode 100644
index 0000000..82b65a4
Binary files /dev/null and b/plot-latlong/.mapimages/World100.png differ
diff --git a/plot-latlong/.mapimages/World50-new.png b/plot-latlong/.mapimages/World50-new.png
new file mode 100644
index 0000000..99af593
Binary files /dev/null and b/plot-latlong/.mapimages/World50-new.png differ
diff --git a/plot-latlong/.mapinfo b/plot-latlong/.mapinfo
new file mode 100644
index 0000000..0e32ee5
--- /dev/null
+++ b/plot-latlong/.mapinfo
@@ -0,0 +1,35 @@
+MAP World World100.png 90 -170 -90 190
+MAP USA USA100.png 50 -125 24 -66
+MAP World50 World50-new.png 90 -170 -90 190
+MAP World100 World100.png 90 -170 -90 190
+MAP Africa Africa.png 38 -18 -35 52
+MAP Australia Australia.png -10 110 -45 155
+MAP Canada Canada.png 75 -142 41 -50
+MAP Caribbean Caribbean.png 27 -85 10 -59
+MAP CentralAmerica CentralAmerica.png 19 -93 5 -77
+MAP China China.png 54 73 18 135
+MAP Europe Europe.png 60 -15 35 30
+MAP Hawaii Hawaii.png 23 -161 18 -154
+MAP India India.png 36 68 5 110
+MAP Japan Japan.png 42 125 30 149
+MAP Korea Korea.png 44 124 34 131
+MAP MalaysiaIndonesia MalaysiaIndonesia.png 8 95 -11 135
+MAP MiddleEast MiddleEast.png 43 25 12 78
+MAP NewZealand NewZealand.png -34 166 -48 180
+MAP NOSEFI NOSEFI.png 72 4 55 32
+MAP Philippines Philippines.png 19 117 5 127
+MAP USA50 USA50-new.png 50 -125 24 -66
+MAP USA100 USA100.png 50 -125 24 -66
+MAP USA200 USA200.png 50 -125 24 -66
+MAP SouthAmerica SouthAmerica.png 13 -82 -56 -34
+MAP Belgium Belgium.png 51.6 2.5 49.4 6.6
+MAP France France.png 52 -5 42 9
+MAP Germany Germany.png 56 5 47 16
+MAP Italy Italy.png 48 6 35 19
+MAP Netherlands Netherlands.png 53.6 3.3 50.7 7.3
+MAP UK UK.png 59 -11 49 2
+# Calculated via trial and error.
+PROJECTION USA50 ALBER 703.18 30.8 45.5 21.7 -99.9 232 390
+PROJECTION USA100 ALBER 1406.35 30.8 45.5 21.7 -99.9 464 781
+PROJECTION USA ALBER 1406.35 30.8 45.5 21.7 -99.9 464 781
+PROJECTION USA200 ALBER 2812.7 30.8 45.5 21.7 -99.9 929 1561
diff --git a/plot-latlong/CONFIG b/plot-latlong/CONFIG
new file mode 100644
index 0000000..367c7fd
--- /dev/null
+++ b/plot-latlong/CONFIG
@@ -0,0 +1,135 @@
+OVERVIEW
+
+This document describes the format of the .mapinfo configuration file,
+which specifies the maps (and their projection parameters) available to
+users of plot-latlong.
+
+
+- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+SYNTAX
+
+The .mapinfo file has a simple line-oriented syntax. Each line
+independently specifies some piece of information, and the order of the
+lines is unimportant. Blank lines and lines starting with '#', which
+may be preceded by whitespace, are ignored. All other lines must have
+the syntax described below.
+
+We use a few conventions in the following description of the syntax.
+Placeholders are denoted by words enclosed in angle brackets (e.g.,
+). All other words are literals that should be included
+verbatim. The order of the words on each line is significant. One or
+more whitespace characters should separate the individual words on each line.
+
+1) map definitions:
+
+ MAP
+
+ MAP World World100.png 90 -170 -90 190
+
+These lines specify a map that is available to the user and supply some
+basic attributes.
+
+The values are
+
+ -- the name of the map; this is the name used for the '-m'
+ command line argument; the name must consist of letters,
+ digits, and underscores--no spaces are allowed
+
+ -- the name of the PNG file containing the map image; the
+ path is relative to the location of the .mapimages directory,
+ so in most cases, the path can be a simple filename such as
+ "World100.png"
+
+
+ -- the lat/long of the top-left corner of the map image in
+ decimal degrees
+
+
+ -- the lat/long of the bottom-right corner of the map image in
+ decimal degrees
+
+
+2) map projection parameters:
+
+ PROJECTION
+
+ PROJECTION USA50 ALBER 704.0 30.8 45.5 21.86 -99.9 232 388
+
+These lines specify the parameters for nonlinear map projections. In
+contrast, the values given in MAP lines are sufficient for linear map
+projections.
+
+The values are
+
+ -- the name of the map; this value should match the name used
+ in the corresponding MAP line
+
+ -- the name of the nonlinear projection that will be
+ applied to the map; currently, if maps are not linear, they may
+ have one nonlinear projection specified for them; multiple
+ nonlinear projections for the same map are not allowed
+
+ -- a variable set of values that provides the
+ actual parameters for the map projection; see below for details
+
+
+The following describes the supported projections and their parameters.
+Only one projection is currently supported:
+
+ ALBER -- an Alber/Lambert projection; this is used by the USA maps
+ included in the distribution
+
+ See the publication "Map Projections Used by the U.S. Geological
+ Survey Bulletin 1532" for details about this projection.
+
+ parameters:
+
+
+
+ where
+
+ -- radius of sphere
+ -- standard parallel (lower)
+ -- standard parallel (upper)
+ -- origin latitude
+ -- origin longitude
+ -- the false easting amount
+ -- the false northing amount
+
+
+- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+TIPS & TRICKS
+
+Creating maps at different scales is both useful and relatively easy.
+The following describes the steps for creating maps at different scales
+using existing maps:
+
+ 1) Use an image editing tool/package like ImageMagick or NetPBM to create
+ a map image at a different size; e.g.,
+
+ convert -scale '50%' .mapimages/World100.png .mapimages/World50.png
+
+ would create a half-sized image using the 'convert' tool of ImageMagick.
+
+ 2) Add a MAP line to .mapinfo; specifically, copy the MAP line of the
+ source map and change the map name and image path--don't change the
+ lat/long parameters; e.g.,
+
+ MAP World100 World100.png 90 -170 -90 190
+ => MAP World50 World50.png 90 -170 -90 190
+
+ 3) For maps created with nonlinear projections, add a PROJECTION line to
+ .mapinfo; again, you may simply copy and tweak the PROJECTION line of
+ the source map; e.g.,
+
+ PROJECTION USA50 ALBER 704.0 30.8 45.5 21.86 -99.9 232 388
+ PROJECTION USA100 ALBER 1408.0 30.8 45.5 21.86 -99.9 464 776
+ PROJECTION USA200 ALBER 2816.0 30.8 45.5 21.86 -99.9 928 1552
+
+ For the Alber/Lambert projection, you need only change the ,
+ , and parameters. These values change
+ in direct proportion to the change in size of the image itself. For
+ example, if the size is halved, then these parameter values should be
+ halved; if the size is doubled, then the values should be doubled.
+ In the above example, USA100 is the USA map at 100%; USA50 and USA200
+ represent images at 50% and 200%, respectively.
diff --git a/plot-latlong/README b/plot-latlong/README
new file mode 100644
index 0000000..961224e
--- /dev/null
+++ b/plot-latlong/README
@@ -0,0 +1,236 @@
+ plot-latlong
+
+ version 0.3
+
+ Jun 10, 2005
+
+ (c) 2003,2004,2005 CAIDA/UCSD
+
+ (http://www.caida.org/tools/visualization/plot-latlong/index.xml)
+
+ plot-latlong-info@caida.org
+ plot-latlong-bugs@caida.org
+
+- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+CHANGES
+
+ * v0.3 released Jun 10, 2005
+
+ - added -i option to specify location of mapinfo file.
+
+ * v0.2 released Apr 6, 2004
+
+ - added test-gd script for testing the GD installation
+
+ * v0.1 released Oct 3, 2003 -- initial release
+
+- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+DESCRIPTION
+
+plot-latlong is a small command line tool written in perl for plotting
+points on geographic maps given a list of latitude/longitude (lat/long)
+pairs. This is aimed at situations in which
+
+ * a moderate amount of accuracy and precision is sufficient
+ * a large number of locations need to be plotted (tens of thousands of
+ locations can be easily handled)
+ * the plotting needs to be automatable (from a shell script, for example)
+ * a lightweight tool (both small and with few dependencies) that just plots
+ points is sufficient
+ * ease of modification is important (so that special requirements can be
+ met)
+
+plot-latlong can handle nonlinear map projections (currently the
+Alber/Lambert projection) and is intentionally minimalistic so that it can
+serve as a building block. Users can build upon it in three ways: (1) add
+new maps, (2) run the output images through packages like NetPBM to add
+titles, etc., and (3) modify the source to change how points are drawn, to
+add labels, etc. plot-latlong can also be used to simply compute the
+mapping from lat/long to pixel coordinates (for a given map). These pixel
+coordinates can then be fed to other programs to draw more elaborate
+pictures.
+
+
+- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+IMPLEMENTED FEATURES
+
+The list of implemented features is intentionally short:
+
+ * drawing points at a user-specified size
+ * printing out just the pixel coordinates of input lat/long pairs
+ * support for linear projections (the relationship between pixels
+ and lat/long values is linear)
+ * support for the Alber/Lambert nonlinear projection
+
+The distribution includes over two dozen maps, covering the continents
+and several country groups. Users can also supply maps to use, so
+long as the projection type is supported and the projection parameters
+are known.
+
+
+- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+CAVEATS
+
+plot-latlong is unsuitable for applications demanding high accuracy.
+Accuracy is likely no better than 5-10 miles--and even this is merely a
+shot in the dark, since we're not in a position to rigorously determine the
+accuracy of the generated plots. The following factors contribute to
+increased inaccuracy:
+
+ * the relative coarseness of the supplied maps
+ * unverified projection parameters for the supplied maps
+ * the USA maps, which use the Alber/Lambert projection, have parameters
+ calibrated by eye
+ * the geodetic datum assumed by the supplied maps is unknown to us
+ (http://www.colorado.edu/geography/gcraft/notes/datum/datum_f.html)
+
+ + lat/long coordinate values are not universal; values are always
+ specified in some system, the datum, and mismatches in the assumed
+ datum can lead to the specification of physical locations that are
+ separated by as much as 1km
+
+ + to assess whether plot-latlong will be sufficient for your needs,
+ you might try comparing the results with those from the
+ Tiger Map Server (http://tiger.census.gov/cgi-bin/mapbrowse-tbl/)
+ and MapQuest (http://www.mapquest.com/maps/latlong.adp).
+
+
+- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+REQUIREMENTS
+
+The requirements for running plot-latlong are:
+
+ * UNIX-like operating system
+ * perl (http://www.perl.com)
+ * GD.pm (http://stein.cshl.org/WWW/software/GD/), which in turn requires
+ (see the README of GD.pm):
+
+ + the gd graphics library (http://www.boutell.com/gd/)
+ + the PNG graphics library (http://www.libpng.org/pub/png/libpng.html)
+ + the zlib compression library (http://www.gzip.org/zlib/)
+
+
+- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+INSTALLATION
+
+No installation, per se, is required, but for the greatest convenience,
+it is best to copy some files to your home directory, as in the following:
+
+ cp plot-latlong $HOME/bin
+ cp -R .mapinfo .mapimages $HOME
+
+Assuming $HOME/bin is in your PATH, you can now run plot-latlong from
+anywhere.
+
+
+- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+USAGE
+
+plot-latlong reads lat/long values from stdin and writes a PNG to
+stdout. The input should contain one pair of lat/long values per line
+with the values separated by whitespace. Each coordinate should be a
+decimal value, with negative values indicating south or west. Blank lines
+and lines starting with the pound character ('#') are ignored.
+
+Command line options are
+
+ -m specifies the name of a map to use (default: the first
+ map listed in .mapinfo: 'World'); see .mapinfo for the valid map names;
+ see the file CONFIG for a description of the .mapinfo format
+
+ -s specifies the size of the points to draw (default: 1);
+ points are filled squares, and the size is the width in pixels
+
+ (NOTE: Points drawn at the default size of 1 pixel may be hard to see
+ when only a few points are plotted. Use '-s 10' when plotting
+ a small number of points.)
+
+ -c causes the pixel coordinates of each lat/long to be printed to stderr;
+ the coordinates (0, 0) are at the upper left corner, and values increase
+ to the right and down
+
+ -i specifies an alternate location for .mapinfo (other
+ than in the current directory or $HOME).
+
+Examples:
+
+$ cat locations.txt | ./plot-latlong >plot.png
+
+$ ./plot-latlong -m USA -s 10 >plot.png </dev/null 2>xy.txt
+$ head xy.txt
+33.58 -86.52 250.44 168.946555555556
+33.59 -86.96 249.12 168.916611111111
+...
+
+
+NOTE: You may safely ignore the following warning:
+
+ gd-png warning: alpha channel not supported
+
+ This warning says that the input map image had transparency information,
+ which some GD versions don't support. None of the map images included
+ in the distribution have transparency, but maps of your own may.
+ In such cases, use an image editing tool to remove the transparency
+ information.
+
+
+- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+ACKNOWLEDGMENTS
+
+The code for handling the Alber/Lambert map projection is derived from
+GTrace v1.0.0beta (http://www.caida.org/tools/visualization/gtrace),
+which was written by Ram Periakaruppan. The included set of maps are also
+derived from the GTrace distribution. GTrace redistributed these maps
+with the permission of VisualRoute (http://www.visualroute.com),
+the original source of the maps.
+
+
+- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+LICENSE
+
+Copyright (C) 2003,2004,2005 The Regents of the University of California.
+All Rights Reserved.
+
+Permission to use, copy, modify and distribute any part of this
+plot-latlong software package for educational, research and non-profit
+purposes, without fee, and without a written agreement is hereby
+granted, provided that the above copyright notice, this paragraph
+and the following paragraphs appear in all copies.
+
+Those desiring to incorporate this into commercial products or use
+for commercial purposes should contact the Technology Transfer
+Office, University of California, San Diego, 9500 Gilman Drive, La
+Jolla, CA 92093-0910, Ph: (858) 534-5815, FAX: (858) 534-7345.
+
+IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY
+PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF
+THE POSSIBILITY OF SUCH DAMAGE.
+
+THE SOFTWARE PROVIDED HEREIN IS ON AN "AS IS" BASIS, AND THE
+UNIVERSITY OF CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE,
+SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. THE UNIVERSITY
+OF CALIFORNIA MAKES NO REPRESENTATIONS AND EXTENDS NO WARRANTIES
+OF ANY KIND, EITHER IMPLIED OR EXPRESS, INCLUDING, BUT NOT LIMITED
+TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A
+PARTICULAR PURPOSE, OR THAT THE USE OF THE SOFTWARE WILL NOT INFRINGE
+ANY PATENT, TRADEMARK OR OTHER RIGHTS.
+
+The plot-latlong software is developed by the plot-latlong Team at the
+University of California, San Diego under the Cooperative Association
+for Internet Data Analysis (CAIDA) Program. Support for this work is
+provided by the National Communications System (NCS) via NSF grant
+ANI-0221172, entitled "Routing and Peering Analysis for Enhancing
+Internet Performance and Security."
diff --git a/plot-latlong/plot-latlong b/plot-latlong/plot-latlong
new file mode 100755
index 0000000..d1f574e
--- /dev/null
+++ b/plot-latlong/plot-latlong
@@ -0,0 +1,390 @@
+#!/usr/bin/perl -w
+
+##############################################################################
+## Plots user supplied lat/longs on a user chosen map.
+##
+## plot-latlong reads lat/long values from stdin and writes a PNG to
+## stdout. The input should contain one pair of lat/long values per line,
+## with the values separated by whitespace. Blank lines and lines starting
+## with the pound character ('#') are ignored.
+##
+## Command line options are
+##
+## -m specifies the name of a map to use (default: the first
+## map listed in .mapinfo: 'World'); see .mapinfo for the valid map names;
+## see the file CONFIG for a description of the .mapinfo format
+##
+## -s specifies the size of the points to draw (default: 1);
+## points are filled squares, and the size is the width in pixels
+##
+## -c causes the pixel coordinates of each lat/long to be printed to stderr;
+## the coordinates (0, 0) are at the upper left corner, and values increase
+## to the right and down
+##
+## -i specifies an alternate location for .mapinfo (other
+## than in the current directory or $HOME).
+##
+##----------------------------------------------------------------------------
+##
+## The code for handling the Alber/Lambert map projection is derived from
+## GTrace v1.0.0beta (http://www.caida.org/tools/visualization/gtrace),
+## which was written by Ram Periakaruppan. The included set of maps are also
+## derived from the GTrace distribution. GTrace redistributed these maps
+## with the permission of VisualRoute (http://www.visualroute.com),
+## the original source of the maps.
+##
+##----------------------------------------------------------------------------
+##
+## Copyright (C) 2003,2004,2005 The Regents of the University of California.
+## All Rights Reserved.
+##
+## Permission to use, copy, modify and distribute any part of this
+## plot-latlong software package for educational, research and non-profit
+## purposes, without fee, and without a written agreement is hereby
+## granted, provided that the above copyright notice, this paragraph
+## and the following paragraphs appear in all copies.
+##
+## Those desiring to incorporate this into commercial products or use
+## for commercial purposes should contact the Technology Transfer
+## Office, University of California, San Diego, 9500 Gilman Drive, La
+## Jolla, CA 92093-0910, Ph: (858) 534-5815, FAX: (858) 534-7345.
+##
+## IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY
+## PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL
+## DAMAGES, INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS
+## SOFTWARE, EVEN IF THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF
+## THE POSSIBILITY OF SUCH DAMAGE.
+##
+## THE SOFTWARE PROVIDED HEREIN IS ON AN "AS IS" BASIS, AND THE
+## UNIVERSITY OF CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE,
+## SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. THE UNIVERSITY
+## OF CALIFORNIA MAKES NO REPRESENTATIONS AND EXTENDS NO WARRANTIES
+## OF ANY KIND, EITHER IMPLIED OR EXPRESS, INCLUDING, BUT NOT LIMITED
+## TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A
+## PARTICULAR PURPOSE, OR THAT THE USE OF THE SOFTWARE WILL NOT INFRINGE
+## ANY PATENT, TRADEMARK OR OTHER RIGHTS.
+##
+## The plot-latlong software is developed by the plot-latlong Team at the
+## University of California, San Diego under the Cooperative Association
+## for Internet Data Analysis (CAIDA) Program. Support for this work is
+## provided by the National Communications System (NCS) via NSF grant
+## ANI-0221172, entitled "Routing and Peering Analysis for Enhancing
+## Internet Performance and Security."
+##
+##############################################################################
+
+use strict;
+use File::Basename;
+use GD;
+use Getopt::Std;
+use vars qw($opt_m $opt_s $opt_c $opt_i);
+
+sub usage
+{
+ die "usage: cat datapoints | plot-latlong [-m ] [-s ] [-c] [-i ] >output.png\n";
+}
+
+if (not getopts('m:s:ci:'))
+{
+ usage();
+}
+
+my $DEBUG = 0;
+my $PI = 3.141592654;
+
+my $point_size = $opt_s || 1;
+
+my $first_map;
+my $map_directory; # directory containing map images
+my %configuration;
+my @mapinfo_locations = (".mapinfo", "$ENV{HOME}/.mapinfo");
+if ($opt_i) {
+ unshift @mapinfo_locations, $opt_i;
+}
+load_configuration(@mapinfo_locations);
+
+my $selected_map = $opt_m || $first_map;
+
+# lat/long of the upper-left and lower-right corners of the map image
+my $map_top_lat;
+my $map_top_long;
+my $map_bottom_lat;
+my $map_bottom_long;
+
+# parameters for linear projection: pixels per degree
+my $map_lat_scale;
+my $map_long_scale;
+
+# parameters for Alber projection (supplied by user):
+# (in degrees)
+my $R; # radius of sphere
+#my $phi_1; # standard parallel
+#my $phi_2; # standard parallel
+#my $phi_0; # origin latitude
+#my $lambda_0; # origin longitude
+my $false_easting; # the false easting amount
+my $false_northing; # the false northing amount
+
+# values for Alber projection calculated from user parameters:
+# (in radians)
+my $PHI_1;
+my $PHI_2;
+my $PHI_0;
+my $LAMBDA_0;
+my $N;
+my $C;
+
+my $conversion_fn; # pointer to function for converting lat/long to (x, y)
+
+my $map = load_map($selected_map);
+
+#############################################################################
+
+my $green = $map->colorAllocate(64, 192, 64);
+my $red = $map->colorAllocate(220, 64, 64);
+
+while (<>)
+{
+ chomp;
+ next if /^\s*$/;
+ next if /^\s*#/;
+
+ # REQUIRE: -90 <= $lat <= 90
+ my ($lat, $long) = split /\s+/;
+ my $adjusted_long = $long;
+ if ($long < $map_top_long) {
+ $adjusted_long += 360;
+ } elsif ($long > $map_bottom_long) {
+ $adjusted_long -= 360;
+ }
+ my ($x, $y) = &$conversion_fn($lat, $adjusted_long);
+
+ print STDERR "$lat $long $x $y\n" if $DEBUG || $opt_c;
+
+ if ($point_size == 1)
+ {
+ $map->setPixel($x, $y, $red);
+ }
+ else
+ {
+ my $half = int($point_size / 2);
+ my $left_x = $x - $half;
+ my $top_y = $y - $half;
+ $map->filledRectangle($left_x, $top_y,
+ $left_x + $point_size - 1, $top_y + $point_size - 1,
+ $red);
+ }
+}
+
+print $map->png if not $DEBUG;
+
+#############################################################################
+#############################################################################
+
+sub convert_latlong_to_xy_linear
+{
+ my ($lat, $long) = @_;
+
+ my $lat_rel = $map_top_lat - $lat;
+ my $long_rel = $long - $map_top_long;
+
+ return ($long_rel * $map_long_scale, $lat_rel * $map_lat_scale);
+}
+
+#############################################################################
+
+
+# See the publication "Map Projections Used by the U.S. Geological
+# Survey Bulletin 1532" for details about this projection.
+#
+# However, the present coder does not know exactly which projection this
+# function corresponds to in the USGS Bulletin.
+
+sub round {
+ my ($number) = @_;
+ return int($number + .5 * ($number <=> 0));
+}
+
+sub convert_latlong_to_xy_alber
+{
+ my ($lat, $long) = @_;
+
+ my $phi = ($lat * $PI) / 180.0;
+ my $lambda = ($long * $PI) / 180.0;
+
+ my $p = ($R * sqrt($C - 2.0 * $N * sin($phi))) / $N;
+ my $p_0 = ($R * sqrt($C - 2.0 * $N * sin($PHI_0))) / $N;
+ my $theta = $N * ($lambda - $LAMBDA_0);
+
+ my $x = $false_easting + round($p * sin($theta));
+ my $y = $false_northing - round($p_0 - $p * cos($theta));
+
+ return ($x, $y);
+}
+
+############################################################################
+
+sub load_map
+{
+ my ($name) = @_;
+
+ if (not exists $configuration{"MAP $name"})
+ {
+ die "ERROR: Map '$name' not found in map configuration file.\n";
+ }
+
+ print("OPEN: $name ", $configuration{"MAP $name"}, "\n") if $DEBUG;
+
+ my $filename;
+ ($filename, $map_top_lat, $map_top_long, $map_bottom_lat, $map_bottom_long)
+ = split(/\s+/, $configuration{"MAP $name"});
+
+ my $path = "$map_directory/$filename";
+ my $retval = new GD::Image($path)
+ or die "ERROR: Couldn't open map image '$path': $!\n";
+
+ my ($map_width, $map_height) = $retval->getBounds();
+
+ print("DIM: $map_width $map_height\n") if $DEBUG;
+
+ $map_lat_scale = $map_height / ($map_top_lat - $map_bottom_lat);
+ $map_long_scale = $map_width / ($map_bottom_long - $map_top_long);
+
+ print("SCALE: $map_lat_scale $map_long_scale\n") if $DEBUG;
+
+ # -- projections -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+
+ if (not exists $configuration{"PROJECTION $name"})
+ {
+ $conversion_fn = \&convert_latlong_to_xy_linear;
+ }
+ else
+ {
+ my @F = split(/\s+/, $configuration{"PROJECTION $name"});
+ if ($F[0] eq "ALBER")
+ {
+ ($R, $false_easting, $false_northing) = @F[1,6,7];
+ my ($phi_1, $phi_2, $phi_0, $lambda_0) = @F[2..5];
+
+ $PHI_1 = ($phi_1 * $PI) / 180.0;
+ $PHI_2 = ($phi_2 * $PI) / 180.0;
+ $PHI_0 = ($phi_0 * $PI) / 180.0;
+ $LAMBDA_0 = ($lambda_0 * $PI) / 180.0;
+
+ $N = (sin($PHI_1) + sin($PHI_2)) / 2.0;
+ $C = cos($PHI_1) ** 2 + 2.0 * $N * sin($PHI_1);
+
+ $conversion_fn = \&convert_latlong_to_xy_alber;
+ }
+ else
+ {
+ die "INTERNAL ERROR: unknown projection '$F[0]'\n";
+ }
+ }
+
+ return $retval;
+}
+
+#############################################################################
+
+sub load_configuration
+{
+ foreach my $filename (@_)
+ {
+ if (-f $filename)
+ {
+ $map_directory = dirname($filename) . "/.mapimages";
+ load_configuration_aux($filename);
+ return;
+ }
+ }
+
+ die "ERROR: Couldn't find map configuration file; looked for: @_\n";
+}
+
+sub load_configuration_aux
+{
+ my ($filename) = @_;
+
+ open CONFIG, "$filename"
+ or die "ERROR: Couldn't open map configuration file '$filename': $!\n";
+ while ()
+ {
+ chomp;
+ next if /^\s*$/;
+ next if /^\s*#/;
+
+ my @F = split /\s+/;
+ if ($F[0] eq "MAP")
+ {
+ if ($#F == 6)
+ {
+ my ($top_lat, $top_long, $bot_lat, $bot_long) = @F[3..6];
+ if (!check_coordinate($top_lat)
+ || !check_coordinate($top_long)
+ || !check_coordinate($bot_lat)
+ || !check_coordinate($bot_long))
+ {
+ die "ERROR: Line $.: boundary coordinates are malformed in map configuration file.\n";
+ }
+
+ if ($top_lat < $bot_lat || $top_long > $bot_long)
+ {
+ die "ERROR: Line $.: boundary coordinates have wrong relations in map configuration file.\n";
+ }
+
+ $first_map = $F[1] if not defined $first_map;
+ $configuration{"MAP $F[1]"} = join(" ", @F[2..6]);
+ next;
+ }
+ }
+ elsif ($F[0] eq "PROJECTION")
+ {
+ if ($#F >= 2)
+ {
+ if ($F[2] eq "ALBER")
+ {
+ if ($#F == 9)
+ {
+ foreach my $x (@F[3..9])
+ {
+ if (not check_number($x))
+ {
+ die "ERROR: Line $.: projection parameters are malformed in map configuration file.\n";
+ }
+ }
+
+ $configuration{"PROJECTION $F[1]"} = join(" ", @F[2..9]);
+ next;
+ }
+ }
+ else
+ {
+ die "ERROR: Line $.: unknown map projection '$F[2]' in map configuration file.\n";
+ }
+ }
+ }
+ else
+ {
+ die "ERROR: Line $.: unknown key '$F[0]' in map configuration file.\n";
+ }
+
+ die "ERROR: Line $. of map configuration file is malformed.\n";
+ }
+ close CONFIG;
+}
+
+#############################################################################
+
+sub check_coordinate
+{
+ my ($x) = @_;
+
+ return $x =~ /^(\-?)\d+(\.\d+)?$/;
+}
+
+
+sub check_number
+{
+ return check_coordinate(@_);
+}
diff --git a/scripts/code-clean.sh b/scripts/code-clean.sh
new file mode 100644
index 0000000..10a3a26
--- /dev/null
+++ b/scripts/code-clean.sh
@@ -0,0 +1,10 @@
+#!/bin/sh
+# $Id: code-clean.sh 144 2007-03-28 07:52:20Z thierry $
+
+find . -name "*~" -type f | xargs rm -f
+find . -name ".#*" -type f | xargs rm -f
+find . -name "*.rej" -type f | xargs rm -f
+find . -name "*.orig" -type f | xargs rm -f
+find . -name "DEADJOE" -type f | xargs rm -f
+find . -type f | grep -v ".psp" | grep -v ".gif" | grep -v ".jpg" | grep -v ".png" | grep -v ".tgz" | grep -v ".ico" | grep -v "druplicon" | xargs perl -wi -pe 's/\s+$/\n/'
+find . -type f | grep -v ".psp" | grep -v ".gif" | grep -v ".jpg" | grep -v ".png" | grep -v ".tgz" | grep -v ".ico" | grep -v "druplicon" | xargs perl -wi -pe 's/\t/ /g'
diff --git a/scripts/code-style.pl b/scripts/code-style.pl
new file mode 100644
index 0000000..3b285a5
--- /dev/null
+++ b/scripts/code-style.pl
@@ -0,0 +1,162 @@
+#!/usr/bin/perl -w
+# $Id: code-style.pl 144 2007-03-28 07:52:20Z thierry $
+
+# Author: Alexander Schwartz (alexander.schwartz@gmx.net)
+# Licence: GPL
+# First version: 2001-10-15
+
+# Originally written for Drupal (http://drupal.org/) to ensure stylish
+# code. This program tries to show as many improvements as possible with
+# no false positives.
+
+# $Id: code-style.pl 144 2007-03-28 07:52:20Z thierry $
+
+$comment = 0;
+$program = 0;
+if ($ARGV[0] eq '-debug') {
+ $debug=1;
+ shift (@ARGV);
+}
+else {
+ $debug=0;
+}
+while (<>) {
+ $org=$_;
+ s/\\["']//g;
+ # please don't use nested comments for now... thanks!
+ # handles comments // style, but don't mess with http://
+ s/\/\/[^:].*//;
+ # handles comments /**/ on a single line
+ s/\/\*.*\*\///g;
+ # handles comments /**/ over several lines
+ if ($comment == 1) {
+ if (s/.*\*\///) {
+ $comment = 0;
+ }
+ else {
+ next;
+ }
+ }
+ if (s/\/\*.*//) {
+ $comment = 1;
+ }
+ if (/^\s*#/) {
+ next;
+ }
+
+ if (s/<\?php//) {
+ $program = 1;
+ }
+ if (/\?>/) {
+ $program = 0;
+ }
+
+ # enforce "bar". foo() ."bar" syntax
+ if (/^("[^"]*"|[^"])*("[^"]*")\.[^ ]/ && $program) {
+ $msg = "'\".' -> '\". '";
+ }
+ elsif (/^("[^"]*"|[^"])*("[^"]*")\s+\./ && $program) {
+ $msg = "'\" .' -> '\".'";
+ }
+ # enforce "bar". foo() ."bar" syntax
+ elsif (/^("[^"]*"|[^"])*[^ "]\.("[^"]*")/ && $program) {
+ $msg = "'.\"' -> '.\"'";
+ }
+ elsif (/^("[^"]*"|[^"])*[^ "]\.\s+("[^"]*")/ && $program) {
+ $msg = "'. \"' -> '.\"'";
+ }
+ # XHTML requires closing tag
+ elsif (/
/i) {
+ $msg = "'
' -> '
'";
+ }
+ elsif (/\$REQUEST_URI/i) {
+ $msg = "the use of REQUEST_URI is prone to XSS exploits and does not work on IIS; use request_uri() instead";
+ }
+ elsif (/\"REQUEST_URI\"/i) {
+ $msg = "the use of REQUEST_URI is prone to XSS exploits and does not work on IIS; use request_uri() instead";
+ }
+
+ # XHTML compatibility mode suggests a blank before /
+ # i.e.
+ elsif (/<[a-z][^>]*[^ >]\/>/i) {
+ $msg = "'' -> ' '";
+ }
+ # we write '{' on the same line, not on the next
+ elsif (/^\s*{/ && $program) {
+ $msg = "take '{' to previous line";
+ }
+ elsif (/([a-z])([A-Z])/) {
+ $msg = "no mixed case function or variable names, use lower case and _";
+ }
+ elsif (/<[\/]*[A-Z]+[^>]*>/) {
+ $msg = "XHTML demands tags to be lowercase";
+ }
+
+ # trying to recognize splitted lines
+ # there are only a few valid last characters in programming mode,
+ # only sometimes it is ( if you use if/else with a single statement
+
+ # from here on we need no more strings
+ while (s/^([^"]*)"[^"]*"/$1#/) {};
+ while (s/^([^']*)'[^']*'/$1#/) {};
+
+ # it should be 'if (' all the time
+ if (/(^|[^a-zA-Z])(if|else|elseif|while|foreach|switch|return|for)\(/) {
+ $msg = "'(' -> ' ('";
+ }
+ #elsif (/[^;{}:\s\n]\s*\n*$/ && $program && !/^[\s}]*(if|else)/) {
+ # $msg = "don't split lines";
+ #}
+ elsif (/\}\s*else/) {
+ $msg = "'} else' -> '}\\nelse'";
+ }
+ elsif (/[^{\s\n]\s*\n*$/ && $program && /^\s*(if|else)/) {
+ $msg = "every if/else needs a { at eol";
+ }
+ elsif (/([\(\[]) / && $program) {
+ $msg = "'$1 ' -> '$1'";
+ }
+ elsif (/ ([\)\]])/ && $program) {
+ $msg = "' $1' -> '$1'";
+ }
+ # but no brackets
+ elsif (/([a-z-A-Z_][a-zA-Z0-9_-]*)\s+\(/ && $program) {
+ if ($1 ne "switch" and $1 ne "if" and $1 ne "while" and $1 ne "foreach" and $1 ne "return" and $1 ne "for" and $1 ne "elseif") {
+ $msg = "'$1 (' -> '$1('";
+ }
+ }
+ # there should be a space before '{'
+ if (/[^ ]{/ && $program) {
+ $msg = "missing space before '{'";
+ }
+ # there should be a space after ','
+ elsif (/[,][^ \n\r]/ && $program) {
+ $msg = "missing space after ','";
+ }
+ # spaces before and after, only foreach may use $foo=>bar
+ elsif (/[^ =|\-|\+](\+|\-)[^ =>|\-|\+]/ && $program && !/foreach/) {
+ $msg = "'$1' -> ' $1 '";
+ }
+ elsif (/[^ =](\*|==|\.=|=>|=|\|\|)[^ =>]/ && $program && !/foreach/) {
+ $msg = "'$1' -> ' $1 '";
+ }
+ # ensure $bar["foo"] and $bar[$foo] and $bar[0]
+ elsif (/\[[^#][^\]]*\]/ && !/\[[0-9\$][^\]]*\]/ && !/\[\]/) {
+ $msg = "only [\"foo\"], [\$foo] or [0] is allowed";
+ }
+ # first try to find missing quotes after = in (X)HTML tags
+ elsif (/<[^>]*=[a-zA-Z0-9][^>]*>/) {
+ $msg = "=... -> =\"...\"";
+ }
+ if (defined $msg) {
+ if ($debug==0) {
+ print $ARGV .":". $. .": $msg : ". $org;
+ }
+ undef $msg;
+ }
+ elsif ($debug==1) {
+ print $org;
+ }
+} continue {
+ close ARGV if eof;
+}
diff --git a/scripts/cron-curl.sh b/scripts/cron-curl.sh
new file mode 100644
index 0000000..c3030ea
--- /dev/null
+++ b/scripts/cron-curl.sh
@@ -0,0 +1,4 @@
+#!/bin/sh
+# $Id: cron-curl.sh 144 2007-03-28 07:52:20Z thierry $
+
+curl --silent --compressed http://yoursite.com/cron.php
diff --git a/scripts/cron-lynx.sh b/scripts/cron-lynx.sh
new file mode 100644
index 0000000..7ab13db
--- /dev/null
+++ b/scripts/cron-lynx.sh
@@ -0,0 +1,4 @@
+#!/bin/sh
+# $Id: cron-lynx.sh 144 2007-03-28 07:52:20Z thierry $
+
+/usr/bin/lynx -source http://yoursite.com/cron.php > /dev/null 2>&1
diff --git a/scripts/prefix.sh b/scripts/prefix.sh
new file mode 100644
index 0000000..87fd498
--- /dev/null
+++ b/scripts/prefix.sh
@@ -0,0 +1,30 @@
+#!/bin/sh
+# $Id: prefix.sh 144 2007-03-28 07:52:20Z thierry $
+
+if [ $# != 2 ]; then
+ cat >&2 << EOH
+This is Drupal database prefixer.
+
+Usage:
+ $0 prefix original_db.sql >prefixed_db.sql
+
+- all tables will prefixed with 'prefix'
+EOH
+
+exit 1;
+fi
+
+PREFIX=$1;
+sed "s/^CREATE TABLE /CREATE TABLE $PREFIX/;
+ s/^INSERT INTO /INSERT INTO $PREFIX/;
+ s/^REPLACE /REPLACE $PREFIX/;
+ s/^ALTER TABLE /ALTER TABLE $PREFIX/;
+ s/^CREATE SEQUENCE /CREATE SEQUENCE $PREFIX/;
+ s/^ALTER SEQUENCE /ALTER SEQUENCE $PREFIX/;
+ s/^CREATE INDEX \(.*\) ON /CREATE INDEX $PREFIX\\1 ON $PREFIX/;
+ s/^CREATE UNIQUE INDEX \(.*\) ON /CREATE UNIQUE INDEX $PREFIX\\1 ON $PREFIX/;
+ s/^UPDATE \(.*\) SET /UPDATE $PREFIX\\1 SET /;
+ s/^DROP TABLE IF EXISTS /DROP TABLE IF EXISTS $PREFIX/;
+ s/ DEFAULT nextval('/ DEFAULT nextval('$PREFIX/;
+ " $2
+
diff --git a/sites/default/settings.php b/sites/default/settings.php
new file mode 100644
index 0000000..6f2ca2b
--- /dev/null
+++ b/sites/default/settings.php
@@ -0,0 +1,162 @@
+ 'main_',
+ * 'users' => 'shared_',
+ * 'sessions' => 'shared_',
+ * 'role' => 'shared_',
+ * 'authmap' => 'shared_',
+ * 'sequences' => 'shared_',
+ * );
+ *
+ * Database URL format:
+ * $db_url = 'mysql://username:password@localhost/databasename';
+ * $db_url = 'mysqli://username:password@localhost/databasename';
+ * $db_url = 'pgsql://username:password@localhost/databasename';
+ */
+
+require_once('plc_config.php');
+
+if (PLC_DB_TYPE == 'postgresql') {
+ $db_url = 'pgsql://';
+} elseif (PLC_DB_TYPE == 'mysql') {
+ $db_url = 'mysql://';
+}
+$db_url .= PLC_DB_USER . ':';
+$db_url .= PLC_DB_PASSWORD . '@';
+$db_url .= PLC_DB_HOST . ':';
+$db_url .= PLC_DB_PORT . '/';
+$db_url .= 'drupal';
+$db_prefix = '';
+
+/**
+ * Base URL (optional).
+ *
+ * If you are experiencing issues with different site domains,
+ * uncomment the Base URL statement below (remove the leading hash sign)
+ * and fill in the URL to your Drupal installation.
+ *
+ * You might also want to force users to use a given domain.
+ * See the .htaccess file for more information.
+ *
+ * Examples:
+ * $base_url = 'http://www.example.com';
+ * $base_url = 'http://www.example.com:8888';
+ * $base_url = 'http://www.example.com/drupal';
+ * $base_url = 'https://www.example.com:8888/drupal';
+ *
+ * It is not allowed to have a trailing slash; Drupal will add it
+ * for you.
+ */
+# $base_url = 'http://www.example.com'; // NO trailing slash!
+
+/**
+ * PHP settings:
+ *
+ * To see what PHP settings are possible, including whether they can
+ * be set at runtime (ie., when ini_set() occurs), read the PHP
+ * documentation at http://www.php.net/manual/en/ini.php#ini.list
+ * and take a look at the .htaccess file to see which non-runtime
+ * settings are used there. Settings defined here should not be
+ * duplicated there so as to avoid conflict issues.
+ */
+ini_set('arg_separator.output', '&');
+ini_set('magic_quotes_runtime', 0);
+ini_set('magic_quotes_sybase', 0);
+ini_set('session.cache_expire', 200000);
+ini_set('session.cache_limiter', 'none');
+ini_set('session.cookie_lifetime', 2000000);
+ini_set('session.gc_maxlifetime', 200000);
+ini_set('session.save_handler', 'user');
+ini_set('session.use_only_cookies', 1);
+ini_set('session.use_trans_sid', 0);
+ini_set('url_rewriter.tags', '');
+ini_set('memory_limit', '12M');
+
+/**
+ * Variable overrides:
+ *
+ * To override specific entries in the 'variable' table for this site,
+ * set them here. You usually don't need to use this feature. This is
+ * useful in a configuration file for a vhost or directory, rather than
+ * the default settings.php. Any configuration setting from the 'variable'
+ * table can be given a new value.
+ *
+ * Remove the leading hash signs to enable.
+ */
+# $conf = array(
+# 'site_name' => 'My Drupal site',
+# 'theme_default' => 'pushbutton',
+# 'anonymous' => 'Visitor'
+# );
+
diff --git a/themes/bluemarine/block.tpl.php b/themes/bluemarine/block.tpl.php
new file mode 100644
index 0000000..75f2708
--- /dev/null
+++ b/themes/bluemarine/block.tpl.php
@@ -0,0 +1,4 @@
+
+ subject; ?>
+ content; ?>
+
diff --git a/themes/bluemarine/box.tpl.php b/themes/bluemarine/box.tpl.php
new file mode 100644
index 0000000..a66bd27
--- /dev/null
+++ b/themes/bluemarine/box.tpl.php
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/themes/bluemarine/comment.tpl.php b/themes/bluemarine/comment.tpl.php
new file mode 100644
index 0000000..1cee35c
--- /dev/null
+++ b/themes/bluemarine/comment.tpl.php
@@ -0,0 +1,9 @@
+
+
+
+
+
+ »
+
diff --git a/themes/bluemarine/logo.png b/themes/bluemarine/logo.png
new file mode 100644
index 0000000..e72da09
Binary files /dev/null and b/themes/bluemarine/logo.png differ
diff --git a/themes/bluemarine/node.tpl.php b/themes/bluemarine/node.tpl.php
new file mode 100644
index 0000000..a5e1502
--- /dev/null
+++ b/themes/bluemarine/node.tpl.php
@@ -0,0 +1,10 @@
+
diff --git a/themes/bluemarine/page.tpl.php b/themes/bluemarine/page.tpl.php
new file mode 100644
index 0000000..e65278d
--- /dev/null
+++ b/themes/bluemarine/page.tpl.php
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/themes/bluemarine/screenshot.png b/themes/bluemarine/screenshot.png
new file mode 100644
index 0000000..9c6a909
Binary files /dev/null and b/themes/bluemarine/screenshot.png differ
diff --git a/themes/bluemarine/style.css b/themes/bluemarine/style.css
new file mode 100644
index 0000000..958ece1
--- /dev/null
+++ b/themes/bluemarine/style.css
@@ -0,0 +1,356 @@
+/* $Id: style.css 144 2007-03-28 07:52:20Z thierry $ */
+
+/*
+** HTML elements
+*/
+body {
+ margin: 0;
+ padding: 0;
+ color: #000;
+ background-color: #fff;
+ font: 76% Verdana, Arial, Helvetica, sans-serif;
+}
+tr.odd td, tr.even td {
+ padding: 0.3em;
+}
+h1, h2, h3, h4, h5, h6 {
+ margin-bottom: 0.5em;
+}
+h1 {
+ font-size: 1.3em;
+}
+h2 {
+ font-size: 1.2em;
+}
+h3, h4, h5, h6 {
+ font-size: 1.1em;
+}
+p {
+ margin-top: 0.5em;
+ margin-bottom: 0.9em;
+}
+a {
+ text-decoration: none;
+ font-weight: bold;
+}
+a:link {
+ color: #39c;
+}
+a:visited {
+ color: #369;
+}
+a:hover {
+ color: #39c;
+ text-decoration: underline;
+}
+fieldset {
+ border: 1px solid #ccc;
+}
+pre {
+ background-color: #eee;
+ padding: 0.75em 1.5em;
+ font-size: 12px;
+ border: 1px solid #ddd;
+}
+table {
+ /* make sizes relative to body size! */
+ font-size: 1em;
+}
+.form-item label {
+ font-size: 1em;
+ color: #222;
+}
+.item-list .title {
+ font-size: 1em;
+ color: #222;
+}
+.links {
+ margin-bottom: 0em;
+}
+.comment .links {
+ margin-bottom: 0em;
+}
+
+/*
+** Page layout blocks / IDs
+*/
+#header, #content {
+ width: 100%;
+}
+#header {
+ background-color: #69c;
+}
+#logo {
+ vertical-align: middle;
+ border: 0;
+}
+#logo img {
+ float: left;
+ padding: 0em 1.0em 0em 1em;
+ border: 0;
+}
+#menu {
+ padding: 0.5em 0.5em 0 0.5em;
+ text-align: right;
+ vertical-align: middle;
+}
+#primary {
+ font-size: 1.0em;
+ padding: 0em 0.8em 0.5em 0;
+ color: #9cf;
+}
+#primary a {
+ font-weight: bold;
+ color: #fff;
+}
+#secondary {
+ padding: 0 1em 0.5em 0;
+ font-size: 0.8em;
+ color: #9cf;
+}
+#secondary a {
+ font-weight: bold;
+ color: #9cf;
+}
+#search .form-text, #search .form-submit {
+ border: 1px solid #369;
+ font-size: 1.1em;
+ height: 1.5em;
+ vertical-align: middle;
+}
+#search .form-text {
+ width: 8em;
+ padding: 0 0.5em 0 0.5em;
+}
+#mission {
+ background-color: #369;
+ padding: 1.5em 2em;
+ color: #fff;
+}
+#mission a, #mission a:visited {
+ color: #9cf;
+ font-weight: bold;
+}
+.site-name {
+ margin: 0.6em 0em 0em 0em;
+ padding: 0em;
+ font-size: 2em;
+}
+.site-name a:link, .site-name a:visited {
+ color: #fff;
+}
+.site-name a:hover {
+ color: #369;
+ text-decoration: none;
+}
+.site-slogan {
+ font-size: 1em;
+ color: #eee;
+ display: block;
+ margin: 0em 0em 0em 0em;
+ font-style: italic;
+ font-weight: bold;
+}
+#main {
+ /* padding in px not ex because IE messes up 100% width tables otherwise */
+ padding: 10px;
+}
+#mission, .node .content, .comment .content {
+ line-height: 1.4;
+}
+#help {
+ font-size: 0.9em;
+ margin-bottom: 1em;
+}
+.breadcrumb {
+ margin-bottom: .5em;
+}
+.messages {
+ background-color: #eee;
+ border: 1px solid #ccc;
+ padding: 0.3em;
+ margin-bottom: 1em;
+}
+.error {
+ border-color: red;
+}
+#sidebar-left, #sidebar-right {
+ background-color: #ddd;
+ width: 16em;
+ /* padding in px not ex because IE messes up 100% width tables otherwise */
+ padding: 10px;
+ vertical-align: top;
+}
+#footer {
+ background-color: #eee;
+ padding: 1em;
+ font-size: 0.8em;
+}
+
+/*
+** Common declarations for child classes of node, comment, block, box, etc.
+** If you want any of them styled differently for a specific parent, add
+** additional rules /with only the differing properties!/ to .parent .class.
+** See .comment .title for an example.
+*/
+.title, .title a {
+ font-weight: bold;
+ font-size: 1.3em;
+ color: #777;
+ margin: 0 auto 0 auto; /* decrease default margins for h.title */
+}
+.submitted {
+ color: #999;
+ font-size: 0.8em;
+}
+.links {
+ color: #999;
+}
+.links a {
+ font-weight: bold;
+}
+.block, .box {
+ padding: 0 0 1.5em 0;
+}
+.block {
+ border-bottom: 1px solid #bbb;
+ padding-bottom: 0.75em;
+ margin-bottom: 1.5em;
+}
+.block .title {
+ margin-bottom: .25em;
+}
+.box .title {
+ font-size: 1.1em;
+}
+.node {
+ margin: .5em 0 2em 0;
+}
+.sticky {
+ padding: .5em;
+ background-color: #eee;
+ border: solid 1px #ddd;
+}
+.node .content, .comment .content {
+ margin: .5em 0 .5em 0;
+}
+.node .taxonomy {
+ color: #999;
+ font-size: 0.8em;
+ padding: 1.5em;
+}
+.node .picture {
+ border: 1px solid #ddd;
+ float: right;
+ margin: 0.5em;
+}
+.comment {
+ border: 1px solid #abc;
+ padding: .5em;
+ margin-bottom: 1em;
+}
+.comment .title a {
+ font-size: 1.1em;
+ font-weight: normal;
+}
+.comment .new {
+ text-align: right;
+ font-weight: bold;
+ font-size: 0.8em;
+ float: right;
+ color: red;
+}
+.comment .picture {
+ border: 1px solid #abc;
+ float: right;
+ margin: 0.5em;
+}
+
+/*
+** Module specific styles
+*/
+#aggregator .feed-source {
+ background-color: #eee;
+ border: 1px solid #ccc;
+ padding: 1em;
+ margin: 1em 0 1em 0;
+}
+#aggregator .news-item .categories, #aggregator .source, #aggregator .age {
+ color: #999;
+ font-style: italic;
+ font-size: 0.9em;
+}
+#aggregator .title {
+ margin-bottom: 0.5em;
+ font-size: 1em;
+}
+#aggregator h3 {
+ margin-top: 1em;
+}
+#forum table {
+ width: 100%;
+}
+#forum td {
+ padding: 0.5em 0.5em 0.5em 0.5em;
+}
+#forum td.forum, #forum td.posts {
+ background-color: #eee;
+}
+#forum td.topics, #forum td.last-reply {
+ background-color: #ddd;
+}
+#forum td.container {
+ background-color: #ccc;
+}
+#forum td.container a {
+ color: #555;
+}
+#forum td.statistics, #forum td.settings, #forum td.pager {
+ height: 1.5em;
+ border: 1px solid #bbb;
+}
+#forum td .name {
+ color: #96c;
+}
+#forum td .links {
+ padding-top: 0.7em;
+ font-size: 0.9em;
+}
+#profile .profile {
+ clear: both;
+ border: 1px solid #abc;
+ padding: .5em;
+ margin: 1em 0em 1em 0em;
+}
+#profile .profile .name {
+ padding-bottom: 0.5em;
+}
+.block-forum h3 {
+ margin-bottom: .5em;
+}
+.calendar a {
+ text-decoration: none;
+}
+.calendar td, .calendar th {
+ padding: 0.4em 0;
+ border-color: #888;
+}
+.calendar .day-today {
+ background-color: #69c;
+}
+.calendar .day-today a {
+ color: #fff;
+}
+.calendar .day-selected {
+ background-color: #369;
+ color: #fff;
+}
+.calendar .header-week {
+ background-color: #ccc;
+}
+.calendar .day-blank {
+ background-color: #ccc;
+}
+.calendar .row-week td a:hover {
+ background-color: #fff; color: #000;
+}
diff --git a/themes/chameleon/background.png b/themes/chameleon/background.png
new file mode 100644
index 0000000..194588a
Binary files /dev/null and b/themes/chameleon/background.png differ
diff --git a/themes/chameleon/chameleon.theme b/themes/chameleon/chameleon.theme
new file mode 100644
index 0000000..b0754a6
--- /dev/null
+++ b/themes/chameleon/chameleon.theme
@@ -0,0 +1,172 @@
+ t('left sidebar'),
+ 'right' => t('right sidebar')
+ );
+}
+
+function chameleon_page($content) {
+ $language = $GLOBALS['locale'];
+
+ if (theme_get_setting('toggle_favicon')) {
+ drupal_set_html_head('');
+ }
+
+ $title = drupal_get_title();
+
+ $output = "\n";
+ $output .= "\n";
+ $output .= "\n";
+ $output .= " ". ($title ? strip_tags($title) ." | ". variable_get("site_name", "drupal") : variable_get("site_name", "drupal") ." | ". variable_get("site_slogan", "")) ." \n";
+ $output .= drupal_get_html_head();
+ $output .= theme('stylesheet_import', base_path() . path_to_theme() ."/common.css");
+ $output .= theme_get_styles();
+ $output .= "";
+ $output .= "\n";
+ $output .= " ";
+
+ if ($logo = theme_get_setting('logo')) {
+ $output .= " ";
+ }
+ if (theme_get_setting('toggle_name')) {
+ $output .= " ". l(variable_get('site_name', 'drupal'), ""). "
";
+ }
+ if (theme_get_setting('toggle_slogan')) {
+ $output .= " ". variable_get('site_slogan', '') ."";
+ }
+
+ $output .= "\n";
+
+ $primary_links = theme('links', menu_primary_links());
+ $secondary_links = theme('links', menu_secondary_links());
+ if (isset($primary_links) || isset($secondary_links)) {
+ $output .= ' \n";
+ }
+
+ $output .= " \n";
+ $output .= " \n";
+
+ if ($blocks = theme_blocks("left")) {
+ $output .= " $blocks \n";
+ }
+
+ $output .= " \n";
+
+ if ($title) {
+ $output .= theme("breadcrumb", drupal_get_breadcrumb());
+ $output .= "$title
";
+ }
+
+ if ($tabs = theme('menu_local_tasks')) {
+ $output .= $tabs;
+ }
+
+ $output .= theme('help');
+
+ $output .= theme('status_messages');
+
+ $output .= "\n\n";
+ $output .= $content;
+ $output .= "\n\n";
+
+ if ($footer = variable_get('site_footer', '')) {
+ $output .= " \n";
+ }
+
+ $output .= " \n";
+
+ if ($blocks = theme_blocks("right")) {
+ $output .= " $blocks \n";
+ }
+
+ $output .= " \n";
+ $output .= "
\n";
+
+ $output .= theme_closure();
+ $output .= " \n";
+ $output .= "\n";
+
+ return $output;
+}
+
+function chameleon_node($node, $teaser = 0, $page = 0) {
+
+ $output = "status) ? ' node-unpublished' : '') ."\">\n";
+
+ if (!$page) {
+ $output .= " ". ($teaser ? l($node->title, "node/$node->nid") : check_plain($node->title)) ."
\n";
+ }
+
+ $output .= " \n";
+
+ if ($teaser && $node->teaser) {
+ $output .= $node->teaser;
+ }
+ else {
+ $output .= $node->body;
+ }
+
+ $output .= " \n";
+
+ $submitted = theme_get_setting("toggle_node_info_$node->type") ? array(t("By %author at %date", array('%author' => theme('username', $node), '%date' => format_date($node->created, 'small')))) : array();
+
+ $terms = array();
+ if (module_exist('taxonomy')) {
+ $terms = taxonomy_link("taxonomy terms", $node);
+ }
+
+ $links = array_merge($submitted, $terms);
+ if ($node->links) {
+ $links = array_merge($links, $node->links);
+ }
+ if (count($links)) {
+ $output .= " ". theme('links', $links) ."\n";
+ }
+
+ $output .= "\n";
+
+ return $output;
+}
+
+function chameleon_comment($comment, $links = "") {
+ $submitted = array(t('By %author at %date', array('%author' => theme('username', $comment), '%date' => format_date($comment->timestamp. 'small'))));
+
+ $output = "status == COMMENT_NOT_PUBLISHED ? ' comment-unpublished' : '') ."\">\n";
+ $output .= " ". l($comment->subject, $_GET['q'], NULL, NULL, "comment-$comment->cid") ."
\n";
+ $output .= " ". $comment->comment ."\n";
+ $output .= " ". theme('links', array_merge($submitted, $links)) ."\n";
+ $output .= "\n";
+
+ return $output;
+}
+
+function chameleon_help() {
+ if ($help = menu_get_active_help()) {
+ return ''. $help .'
';
+ }
+}
+
+?>
diff --git a/themes/chameleon/common.css b/themes/chameleon/common.css
new file mode 100644
index 0000000..363c23a
--- /dev/null
+++ b/themes/chameleon/common.css
@@ -0,0 +1,151 @@
+/* $Id: common.css 144 2007-03-28 07:52:20Z thierry $ */
+
+/*
+** HTML elements
+*/
+a, a:link, a:active {
+ font-weight: bold;
+ text-decoration: none;
+}
+a:hover {
+ text-decoration: underline;
+}
+body {
+ margin: 0;
+ padding: 3em;
+ font-size: .9em;
+ line-height: 1.3em;
+}
+blockquote {
+ font-style: italic;
+}
+table {
+ margin: 0;
+ padding: .5em;
+ border-collapse: collapse;
+}
+code, pre {
+ font-size: 1em;
+}
+pre {
+ font-size: 0.8em;
+ padding: 1em;
+ background: #eee;
+}
+li {
+ padding-bottom: .3em;
+}
+h1, h2, h3, h4, h5, h6 {
+ margin-bottom: .25em;
+}
+h1 {
+ font-size: 1.3em;
+}
+h2 {
+ font-size: 1.2em;
+}
+h3 {
+ font-size: 1.1em;
+}
+h4, h5, h6 {
+ font-size: 1em;
+}
+p {
+ margin: 0 0 .5em 0;
+}
+br {
+ line-height: 0.6em;
+}
+
+/*
+** Page layout blocks / IDs
+*/
+#header {
+ margin-bottom: 2em;
+}
+#help {
+ font-size: 0.8em;
+}
+#content {
+ clear: both;
+}
+#sidebar-left, #sidebar-right {
+ vertical-align: top;
+ padding: 10px;
+}
+#main {
+ padding-left: 1em;
+ padding-right: 1em;
+ vertical-align: top;
+}
+#footer {
+ font-size: 0.8em;
+ padding-top: 2em;
+ text-align: center;
+}
+
+/*
+** Common declarations for child classes of node, comment, block, box etc
+*/
+.title {
+ margin: 0 0 .25em 0;
+}
+.content {
+ margin: 0 0 .5em 0;
+}
+.links {
+ font-size: 0.8em;
+ line-height: 1.25em;
+}
+.block {
+ width: 180px;
+}
+.messages {
+ padding: 0.3em;
+ margin: 0.5em 0em 0.5em 0em;
+}
+.status {
+ border: 1px solid #3a3;
+ color: #3a3;
+}
+.error, form-item input.error {
+ border: 1px solid red;
+ color: red;
+}
+
+/*
+** Common navigation links added on the admin/themes/settings page
+*/
+.navlinks {
+ padding: 0em 0.5em 1.5em 0em;
+ clear: both;
+}
+.primary a {
+ font-size: 1.0em;
+ padding: 0em 0.5em 0em 0em;
+}
+.secondary a {
+ font-size: 0.9em;
+ padding: 0em 0.5em 0em 0em;
+}
+
+/*
+** Logo Image Positioning
+*/
+#header img {
+ float: left;
+ padding: 0em 2em .5em 0em;
+}
+#header {
+ clear: both;
+}
+/*
+** Module specific styles
+*/
+.form-item textarea {
+ font-size: 1em;
+}
+#aggregator .feed-source {
+ border: 1px solid gray;
+ padding: 1em;
+}
diff --git a/themes/chameleon/logo.png b/themes/chameleon/logo.png
new file mode 100644
index 0000000..320fa96
Binary files /dev/null and b/themes/chameleon/logo.png differ
diff --git a/themes/chameleon/marvin/bullet.png b/themes/chameleon/marvin/bullet.png
new file mode 100644
index 0000000..937c8ed
Binary files /dev/null and b/themes/chameleon/marvin/bullet.png differ
diff --git a/themes/chameleon/marvin/druplicon-watermark.png b/themes/chameleon/marvin/druplicon-watermark.png
new file mode 100644
index 0000000..4f91cd3
Binary files /dev/null and b/themes/chameleon/marvin/druplicon-watermark.png differ
diff --git a/themes/chameleon/marvin/logo.png b/themes/chameleon/marvin/logo.png
new file mode 100644
index 0000000..320fa96
Binary files /dev/null and b/themes/chameleon/marvin/logo.png differ
diff --git a/themes/chameleon/marvin/screenshot.png b/themes/chameleon/marvin/screenshot.png
new file mode 100644
index 0000000..b46b21c
Binary files /dev/null and b/themes/chameleon/marvin/screenshot.png differ
diff --git a/themes/chameleon/marvin/style.css b/themes/chameleon/marvin/style.css
new file mode 100644
index 0000000..a24de12
--- /dev/null
+++ b/themes/chameleon/marvin/style.css
@@ -0,0 +1,118 @@
+/* $Id: style.css 144 2007-03-28 07:52:20Z thierry $ */
+
+/*
+** HTML elements
+*/
+body {
+ background: #fff url(druplicon-watermark.png) no-repeat top right;
+ font-family: arial, helvetica, sans-serif;
+}
+a:link {
+ color: #656
+}
+a:visited {
+ color: #656
+}
+a:active {
+ color: #ccc
+}
+h2 {
+ background-color: #eaeaea;
+ border: solid 1px #777;
+ font-size: 1.1em;
+ margin: 0.5em 0em 0.5em 0em;
+ padding: 0.5em;
+}
+h2.title {
+ background-color: #fff;
+ border: solid 1px #888;
+ margin-top: 1em;
+}
+p {
+ margin: 0 1em 1em 0;
+ padding: 0;
+}
+table {
+ font-size: 1em;
+}
+
+/*
+** Page layout blocks / IDs
+*/
+#main {
+ width: 80%;
+}
+#header .title {
+ padding-top: .75em;
+}
+
+/*
+** Common declarations for child classes of node, comment, block, box etc
+*/
+.node .submitted {
+ color: #7c7c7c;
+ font-size: 0.9em;
+ float: left;
+ padding: 0.5em 0em 0.5em 1em;
+}
+.node .taxonomy {
+ color: #7c7c7c;
+ font-size: 0.9em;
+ float: right;
+}
+.node .content {
+ clear: both;
+ padding-left: 1em;
+}
+.node .links {
+ padding: 1em;
+}
+.comment {
+ border: solid 1px #777;
+ margin: 0.5em 0 0.5em 0;
+ padding: 0.5em;
+}
+.block {
+ margin-bottom: 10px;
+ font-size: 0.9em;
+}
+.block .content {
+ border: solid 1px #888;
+ border-top: none;
+ margin: 0;
+ padding: 5px;
+}
+.block h2.title {
+ margin: 0;
+}
+
+/*
+** Module specific styles
+*/
+.item-list ul li {
+ list-style-image: url(bullet.png);
+}
+.calendar .day-today {
+ background-color: #ccc;
+}
+.calendar .day-selected {
+ background-color: #bbb;
+}
+.calendar .header-month {
+ background-color: #ddd;
+}
+.calendar .header-week {
+ background-color: #ccc;
+}
+.calendar .day-blank {
+ background-color: #ddd;
+}
+.calendar .day-link a {
+ color: #000;
+}
+.calendar .row-week {
+ color: #aaa;
+}
+.path, .path a, .path a:visited {
+ color: #888;
+}
\ No newline at end of file
diff --git a/themes/chameleon/screenshot.png b/themes/chameleon/screenshot.png
new file mode 100644
index 0000000..7c8bdee
Binary files /dev/null and b/themes/chameleon/screenshot.png differ
diff --git a/themes/chameleon/style.css b/themes/chameleon/style.css
new file mode 100644
index 0000000..9b8030c
--- /dev/null
+++ b/themes/chameleon/style.css
@@ -0,0 +1,90 @@
+/* $Id: style.css 144 2007-03-28 07:52:20Z thierry $ */
+
+/*
+** HTML elements
+*/
+a, a:link, a:active {
+ color: #930;
+}
+a:visited {
+ color: #630;
+}
+body {
+ padding: 5em 0 0 3em;
+ background-image: url(background.png);
+ background-repeat: repeat-x;
+ font-family: trebuchet ms, tahoma, verdana, arial, helvetica;
+ border-top: 10px solid gray;
+}
+ul {
+ list-style-type: disc;
+}
+
+/*
+** Page layout blocks / IDs
+*/
+#main {
+ width: 500px;
+}
+#sidebar-left {
+ border-right: 1px solid gray;
+}
+#sidebar-right {
+ border-left: 1px solid gray;
+}
+
+/*
+** Common declarations for child classes of node, comment, block, box etc
+*/
+#header .title {
+ font-size: 2em;
+ font-weight: bold;
+ padding-top: .75em;
+}
+#header .title a,
+#header .title a:link,
+#header .title a:visited,
+#header .title a:active {
+ text-decoration: none;
+ color: #aaa;
+}
+#header .title a:hover {
+ color: #930;
+}
+#header .site-slogan {
+ margin-top: -0.1em;
+ font-size: 0.8em;
+}
+.node .title {
+ font-size: 1.2em;
+}
+.node .title a,
+.node .title a:link,
+.node .title a:active,
+.node .title a:visited {
+ text-decoration: none;
+ font-weight: normal;
+}
+.node .title a:hover {
+ text-decoration: underline;
+}
+.links {
+ margin: 1em 0 3em 0;
+ text-align: right;
+ }
+.comment .content, .block .content, .menu {
+ font-size: 0.9em;
+}
+.block {
+ padding-bottom: 1em;
+}
+.block .title {
+ font-size: 1em;
+}
+
+/*
+** Module specific styles
+*/
+.item-list ul li {
+ list-style: square;
+}
diff --git a/themes/engines/phptemplate/block.tpl.php b/themes/engines/phptemplate/block.tpl.php
new file mode 100644
index 0000000..ca670db
--- /dev/null
+++ b/themes/engines/phptemplate/block.tpl.php
@@ -0,0 +1,4 @@
+module" ?>" id="module-$block->delta"; ?>">
+ subject ?>
+ content ?>
+
diff --git a/themes/engines/phptemplate/box.tpl.php b/themes/engines/phptemplate/box.tpl.php
new file mode 100644
index 0000000..56667e7
--- /dev/null
+++ b/themes/engines/phptemplate/box.tpl.php
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/themes/engines/phptemplate/comment.tpl.php b/themes/engines/phptemplate/comment.tpl.php
new file mode 100644
index 0000000..2456c58
--- /dev/null
+++ b/themes/engines/phptemplate/comment.tpl.php
@@ -0,0 +1,15 @@
+
diff --git a/themes/engines/phptemplate/default.tpl.php b/themes/engines/phptemplate/default.tpl.php
new file mode 100644
index 0000000..8008a2c
--- /dev/null
+++ b/themes/engines/phptemplate/default.tpl.php
@@ -0,0 +1 @@
+
diff --git a/themes/engines/phptemplate/node.tpl.php b/themes/engines/phptemplate/node.tpl.php
new file mode 100644
index 0000000..028aeb3
--- /dev/null
+++ b/themes/engines/phptemplate/node.tpl.php
@@ -0,0 +1,18 @@
+
diff --git a/themes/engines/phptemplate/phptemplate.engine b/themes/engines/phptemplate/phptemplate.engine
new file mode 100644
index 0000000..2e03796
--- /dev/null
+++ b/themes/engines/phptemplate/phptemplate.engine
@@ -0,0 +1,344 @@
+filename) . '/template.php';
+ if (file_exists($file)) {
+ include_once "./$file";
+ }
+}
+
+function phptemplate_templates($directory = 'themes') {
+ return system_listing('^page\.tpl\.php$', $directory, 'filename');
+}
+
+/**
+ * Declare the available regions implemented by this engine.
+ *
+ * @return
+ * An array of regions. The first array element will be used as the default region for themes.
+ */
+function phptemplate_regions() {
+ return array(
+ 'left' => t('left sidebar'),
+ 'right' => t('right sidebar'),
+ 'content' => t('content'),
+ 'header' => t('header'),
+ 'footer' => t('footer')
+ );
+}
+
+/**
+ * Execute a template engine call.
+ *
+ * Each call to the template engine has two parts. Namely preparing
+ * the variables, and then doing something with them.
+ *
+ * The first step is done by all template engines / themes, the second
+ * step is dependent on the engine used.
+ *
+ * @param $hook
+ * The name of the theme function being executed.
+ * @param $variables
+ * A sequential array of variables passed to the theme function.
+ * @param $file
+ * A suggested template file to use. If the file is not found, the default $hook.tpl.php will be used.
+ * @return
+ * The HTML generated by the template system.
+ */
+function _phptemplate_callback($hook, $variables = array(), $file = NULL) {
+
+ $variables = array_merge($variables, _phptemplate_default_variables($hook, $variables));
+
+ // Allow specified variables to be overridden
+ if (function_exists('_phptemplate_variables')) {
+ $variables = array_merge($variables, _phptemplate_variables($hook, $variables));
+ }
+
+ if (isset($variables['template_file'])) {
+ $file = $variables['template_file'];
+ }
+
+ if (function_exists('_phptemplate_' . $hook)) {
+ return call_user_func('_phptemplate_' . $hook, $variables, $file);
+ }
+ elseif (function_exists('_phptemplate_default')) {
+ return call_user_func('_phptemplate_default', $hook, $variables, $file);
+ }
+
+}
+
+/**
+ * Adds additional helper variables to all templates.
+ *
+ * Counts how many times certain hooks have been called. Sidebar left / right are special cases.
+ *
+ * @param $hook
+ * The name of the theme function being executed.
+ * @param $variables
+ * A sequential array of variables passed to the theme function.
+ */
+function _phptemplate_default_variables($hook, $variables) {
+ global $theme, $sidebar_indicator;
+ static $count = array();
+
+ $count[$hook] = isset($count[$hook]) && is_int($count[$hook]) ? $count[$hook] : 1;
+ $variables['zebra'] = ($count[$hook] % 2) ? 'odd' : 'even';
+ $variables['id'] = $count[$hook]++;
+
+ if ($hook == 'block') {
+ $count['block_counter'][$sidebar_indicator] = isset($count['block_counter'][$sidebar_indicator]) && is_int($count['block_counter'][$sidebar_indicator]) ? $count['block_counter'][$sidebar_indicator] : 1;
+ $variables['block_zebra'] = ($count['block_counter'][$sidebar_indicator] % 2) ? 'odd' : 'even';
+ $variables['block_id'] = $count['block_counter'][$sidebar_indicator]++;
+ }
+ elseif ($hook == 'page') {
+ $regions = system_region_list($theme);
+ // Load all region content assigned via blocks.
+ foreach (array_keys($regions) as $region) {
+ // Skip blocks in this region that have already been loaded.
+ // This pre-loading is necessary because phptemplate uses variable names different from
+ // the region names, e.g., 'sidebar_left' instead of 'left'.
+ if (!in_array($region, array('left', 'right', 'footer'))) {
+ isset($variables[$region]) ? $variables[$region] .= theme('blocks', $region) : $variables[$region] = theme('blocks', $region);
+ }
+ }
+ }
+ // Tell all templates where they are located.
+ $variables['directory'] = path_to_theme();
+ $variables['is_front'] = drupal_is_front_page();
+
+ return $variables;
+}
+
+/**
+ * @return
+ * Array of template features
+ */
+function phptemplate_features() {
+ return array(
+ 'toggle_logo',
+ 'toggle_comment_user_picture',
+ 'toggle_favicon',
+ 'toggle_mission',
+ 'toggle_name',
+ 'toggle_node_user_picture',
+ 'toggle_search',
+ 'toggle_slogan'
+ );
+}
+
+/**
+ * Prepare the values passed to the theme_page function to be passed
+ * into a pluggable template engine.
+ */
+function phptemplate_page($content) {
+
+ /* Set title and breadcrumb to declared values */
+ if (drupal_is_front_page()) {
+ $mission = filter_xss_admin(theme_get_setting('mission'));
+ }
+
+ /* Add favicon */
+ if (theme_get_setting('toggle_favicon') && ($favicon_url = check_url(theme_get_setting('favicon')))) {
+ drupal_set_html_head('');
+ }
+
+ /**
+ * Populate sidebars.
+ */
+ $layout = 'none';
+ global $sidebar_indicator;
+ /**
+ * Sidebar_indicator tells the block counting code to count sidebars separately.
+ */
+ $sidebar_indicator = 'left';
+ $sidebar_left = theme('blocks', 'left');
+ if ($sidebar_left != '') {
+ $layout = 'left';
+ }
+
+ $sidebar_indicator = 'right';
+ $sidebar_right = theme('blocks', 'right');
+ if ($sidebar_right != '') {
+ $layout = ($layout == 'left') ? 'both' : 'right';
+ }
+ $sidebar_indicator = NULL;
+
+ // Construct page title
+ if (drupal_get_title()) {
+ $head_title = array(strip_tags(drupal_get_title()), variable_get('site_name', 'drupal'));
+ }
+ else {
+ $head_title = array(variable_get('site_name', 'drupal'));
+ if (variable_get('site_slogan', '')) {
+ $head_title[] = variable_get('site_slogan', '');
+ }
+ }
+
+ $variables = array(
+ 'base_path' => base_path(),
+ 'breadcrumb' => theme('breadcrumb', drupal_get_breadcrumb()),
+ 'closure' => theme('closure'),
+ 'content' => '' . $content . '',
+ 'footer_message' => filter_xss_admin(variable_get('site_footer', FALSE)) . "\n" . theme('blocks', 'footer'),
+ 'head' => drupal_get_html_head(),
+ 'head_title' => implode(' | ', $head_title),
+ 'help' => theme('help'),
+ 'language' => $GLOBALS['locale'],
+ 'layout' => $layout,
+ 'logo' => theme_get_setting('logo'),
+ 'messages' => theme('status_messages'),
+ 'mission' => isset($mission) ? $mission : '',
+ 'primary_links' => menu_primary_links(),
+ 'search_box' => (theme_get_setting('toggle_search') ? search_box() : ''),
+ 'secondary_links' => menu_secondary_links(),
+ 'sidebar_left' => $sidebar_left,
+ 'sidebar_right' => $sidebar_right,
+ 'site_name' => (theme_get_setting('toggle_name') ? variable_get('site_name', 'Drupal') : ''),
+ 'site_slogan' => (theme_get_setting('toggle_slogan') ? variable_get('site_slogan', '') : ''),
+ 'styles' => theme_get_styles(),
+ 'tabs' => theme('menu_local_tasks'),
+ 'title' => drupal_get_title()
+ );
+
+ if ((arg(0) == 'node') && is_numeric(arg(1))) {
+ $variables['node'] = node_load(arg(1));
+ }
+
+ return _phptemplate_callback('page', $variables);
+}
+
+/*
+ * Prepare the values passed to the theme_node function to be passed
+ * into a pluggable template engine.
+ */
+function phptemplate_node($node, $teaser = 0, $page = 0) {
+ if (module_exist('taxonomy')) {
+ $taxonomy = taxonomy_link('taxonomy terms', $node);
+ }
+ else {
+ $taxonomy = array();
+ }
+
+ $variables = array(
+ 'content' => ($teaser && $node->teaser) ? $node->teaser : $node->body,
+ 'date' => format_date($node->created),
+ 'links' => $node->links ? theme('links', $node->links) : '',
+ 'name' => theme('username', $node),
+ 'node' => $node, // we pass the actual node to allow more customization
+ 'node_url' => url('node/'. $node->nid),
+ 'page' => $page,
+ 'taxonomy' => $taxonomy,
+ 'teaser' => $teaser,
+ 'terms' => theme('links', $taxonomy),
+ 'title' => check_plain($node->title)
+ );
+
+ // Flatten the node object's member fields.
+ $variables = array_merge((array)$node, $variables);
+
+ // Display info only on certain node types.
+ if (theme_get_setting('toggle_node_info_' . $node->type)) {
+ $variables['submitted'] = t('Submitted by %a on %b.', array('%a' => theme('username', $node), '%b' => format_date($node->created)));
+ $variables['picture'] = theme_get_setting('toggle_node_user_picture') ? theme('user_picture', $node) : '';
+ }
+ else {
+ $variables['submitted'] = '';
+ $variables['picture'] = '';
+ }
+
+ return _phptemplate_callback('node', $variables, 'node-' . $node->type);
+}
+
+/**
+ * Prepare the values passed to the theme_comment function to be passed
+ * into a pluggable template engine.
+ */
+function phptemplate_comment($comment, $links = 0) {
+ return _phptemplate_callback('comment', array(
+ 'author' => theme('username', $comment),
+ 'comment' => $comment,
+ 'content' => $comment->comment,
+ 'date' => format_date($comment->timestamp),
+ 'links' => isset($links) ? theme('links', $links) : '',
+ 'new' => $comment->new ? t('new') : '',
+ 'picture' => theme_get_setting('toggle_comment_user_picture') ? theme('user_picture', $comment) : '',
+ 'submitted' => t('Submitted by %a on %b.',
+ array('%a' => theme('username', $comment),
+ '%b' => format_date($comment->timestamp))),
+ 'title' => l($comment->subject, $_GET['q'], NULL, NULL, "comment-$comment->cid")
+ ));
+}
+
+/**
+ * Prepare the values passed to the theme_block function to be passed
+ * into a pluggable template engine.
+ */
+function phptemplate_block($block) {
+ return _phptemplate_callback('block', array('block' => $block));
+}
+
+/**
+ * Prepare the values passed to the theme_box function to be passed
+ * into a pluggable template engine.
+ */
+function phptemplate_box($title, $content, $region = 'main') {
+ return _phptemplate_callback('box', array(
+ 'content' => $content,
+ 'region' => $region,
+ 'title' => $title
+ ));
+}
+
+/**
+ * Default callback for PHPTemplate.
+ *
+ * Load a template file, and pass the variable array to it.
+ * If the suggested file is not found, PHPTemplate will attempt to use
+ * a $hook.tpl.php file in the template directory, and failing that a
+ * $hook.tpl.php in the PHPTemplate directory.
+ *
+ * @param $hook
+ * The name of the theme function being executed.
+ * @param $variables
+ * A sequential array of variables passed to the theme function.
+ * @param $file
+ * A suggested template file to use.
+ */
+function _phptemplate_default($hook, $variables, $file = NULL) {
+ if (!empty($file) && file_exists(path_to_theme() . "/$file.tpl.php")) {
+ $file = path_to_theme() . "/$file.tpl.php";
+ }
+ else {
+ if (file_exists(path_to_theme() . "/$hook.tpl.php")) {
+ $file = path_to_theme() . "/$hook.tpl.php";
+ }
+ else {
+ if (in_array($hook, array('node', 'block', 'box', 'comment'))) {
+ $file = "themes/engines/phptemplate/$hook.tpl.php";
+ }
+ else {
+ $variables['hook'] = $hook;
+ watchdog('error', t('PHPTemplate was instructed to override the %name theme function, but no valid template file was found.', array('%name' => theme('placeholder', $hook))));
+ $file = 'themes/engines/phptemplate/default.tpl.php';
+ }
+ }
+ }
+
+ if (isset($file)) {
+ extract($variables, EXTR_SKIP); // Extract the variables to a local namespace
+ ob_start(); // Start output buffering
+ include "./$file"; // Include the file
+ $contents = ob_get_contents(); // Get the contents of the buffer
+ ob_end_clean(); // End buffering and discard
+ return $contents; // Return the contents
+ }
+
+}
+
+?>
diff --git a/themes/pushbutton/arrow-next-hover.png b/themes/pushbutton/arrow-next-hover.png
new file mode 100644
index 0000000..5e32cd2
Binary files /dev/null and b/themes/pushbutton/arrow-next-hover.png differ
diff --git a/themes/pushbutton/arrow-next-visited.png b/themes/pushbutton/arrow-next-visited.png
new file mode 100644
index 0000000..3928014
Binary files /dev/null and b/themes/pushbutton/arrow-next-visited.png differ
diff --git a/themes/pushbutton/arrow-next.png b/themes/pushbutton/arrow-next.png
new file mode 100644
index 0000000..cf3ae6a
Binary files /dev/null and b/themes/pushbutton/arrow-next.png differ
diff --git a/themes/pushbutton/arrow-prev-hover.png b/themes/pushbutton/arrow-prev-hover.png
new file mode 100644
index 0000000..cd85f90
Binary files /dev/null and b/themes/pushbutton/arrow-prev-hover.png differ
diff --git a/themes/pushbutton/arrow-prev-visited.png b/themes/pushbutton/arrow-prev-visited.png
new file mode 100644
index 0000000..38f6e31
Binary files /dev/null and b/themes/pushbutton/arrow-prev-visited.png differ
diff --git a/themes/pushbutton/arrow-prev.png b/themes/pushbutton/arrow-prev.png
new file mode 100644
index 0000000..e6764ec
Binary files /dev/null and b/themes/pushbutton/arrow-prev.png differ
diff --git a/themes/pushbutton/arrow-up-hover.png b/themes/pushbutton/arrow-up-hover.png
new file mode 100644
index 0000000..3bff735
Binary files /dev/null and b/themes/pushbutton/arrow-up-hover.png differ
diff --git a/themes/pushbutton/arrow-up-visited.png b/themes/pushbutton/arrow-up-visited.png
new file mode 100644
index 0000000..2df0fdd
Binary files /dev/null and b/themes/pushbutton/arrow-up-visited.png differ
diff --git a/themes/pushbutton/arrow-up.png b/themes/pushbutton/arrow-up.png
new file mode 100644
index 0000000..81dd410
Binary files /dev/null and b/themes/pushbutton/arrow-up.png differ
diff --git a/themes/pushbutton/background.png b/themes/pushbutton/background.png
new file mode 100644
index 0000000..889e1cc
Binary files /dev/null and b/themes/pushbutton/background.png differ
diff --git a/themes/pushbutton/block.tpl.php b/themes/pushbutton/block.tpl.php
new file mode 100644
index 0000000..782ed50
--- /dev/null
+++ b/themes/pushbutton/block.tpl.php
@@ -0,0 +1,4 @@
+module" ?>" id="module-$block->delta"; ?>">
+ subject ?>
+ content ?>
+
\ No newline at end of file
diff --git a/themes/pushbutton/box.tpl.php b/themes/pushbutton/box.tpl.php
new file mode 100644
index 0000000..cf48ffa
--- /dev/null
+++ b/themes/pushbutton/box.tpl.php
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/themes/pushbutton/comment.tpl.php b/themes/pushbutton/comment.tpl.php
new file mode 100644
index 0000000..2f2113b
--- /dev/null
+++ b/themes/pushbutton/comment.tpl.php
@@ -0,0 +1,11 @@
+
+
+
+
+
+ new) : ?> *
+
+
+ »
+
+
diff --git a/themes/pushbutton/forum-container.jpg b/themes/pushbutton/forum-container.jpg
new file mode 100644
index 0000000..46ea78f
Binary files /dev/null and b/themes/pushbutton/forum-container.jpg differ
diff --git a/themes/pushbutton/forum-link.png b/themes/pushbutton/forum-link.png
new file mode 100644
index 0000000..56c50b1
Binary files /dev/null and b/themes/pushbutton/forum-link.png differ
diff --git a/themes/pushbutton/header-a.jpg b/themes/pushbutton/header-a.jpg
new file mode 100644
index 0000000..26b6c01
Binary files /dev/null and b/themes/pushbutton/header-a.jpg differ
diff --git a/themes/pushbutton/header-b.jpg b/themes/pushbutton/header-b.jpg
new file mode 100644
index 0000000..17ed854
Binary files /dev/null and b/themes/pushbutton/header-b.jpg differ
diff --git a/themes/pushbutton/header-c.png b/themes/pushbutton/header-c.png
new file mode 100644
index 0000000..711a3c7
Binary files /dev/null and b/themes/pushbutton/header-c.png differ
diff --git a/themes/pushbutton/icon-block.png b/themes/pushbutton/icon-block.png
new file mode 100644
index 0000000..c12933b
Binary files /dev/null and b/themes/pushbutton/icon-block.png differ
diff --git a/themes/pushbutton/icon-comment.png b/themes/pushbutton/icon-comment.png
new file mode 100644
index 0000000..9ef36de
Binary files /dev/null and b/themes/pushbutton/icon-comment.png differ
diff --git a/themes/pushbutton/logo-active.jpg b/themes/pushbutton/logo-active.jpg
new file mode 100644
index 0000000..67e8908
Binary files /dev/null and b/themes/pushbutton/logo-active.jpg differ
diff --git a/themes/pushbutton/logo-background.jpg b/themes/pushbutton/logo-background.jpg
new file mode 100644
index 0000000..d47005f
Binary files /dev/null and b/themes/pushbutton/logo-background.jpg differ
diff --git a/themes/pushbutton/logo-hover.jpg b/themes/pushbutton/logo-hover.jpg
new file mode 100644
index 0000000..5e662f5
Binary files /dev/null and b/themes/pushbutton/logo-hover.jpg differ
diff --git a/themes/pushbutton/logo.png b/themes/pushbutton/logo.png
new file mode 100644
index 0000000..4d0a7e9
Binary files /dev/null and b/themes/pushbutton/logo.png differ
diff --git a/themes/pushbutton/node.tpl.php b/themes/pushbutton/node.tpl.php
new file mode 100644
index 0000000..8eccce9
--- /dev/null
+++ b/themes/pushbutton/node.tpl.php
@@ -0,0 +1,12 @@
+
diff --git a/themes/pushbutton/page.tpl.php b/themes/pushbutton/page.tpl.php
new file mode 100644
index 0000000..33fb1d3
--- /dev/null
+++ b/themes/pushbutton/page.tpl.php
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/themes/pushbutton/screenshot.png b/themes/pushbutton/screenshot.png
new file mode 100644
index 0000000..139e1a7
Binary files /dev/null and b/themes/pushbutton/screenshot.png differ
diff --git a/themes/pushbutton/style.css b/themes/pushbutton/style.css
new file mode 100644
index 0000000..99744f5
--- /dev/null
+++ b/themes/pushbutton/style.css
@@ -0,0 +1,617 @@
+/* $Id: style.css 144 2007-03-28 07:52:20Z thierry $ */
+
+/*
+** HTML elements
+*/
+body {
+ color: #000;
+ background-color: #fff;
+ margin: 0;
+ padding: 0;
+}
+body, p, td, li, ul, ol {
+ font-family: Verdana, Helvetica, Arial, sans-serif;
+}
+h1, h2, h3, h4, h5, h6 {
+ font-family: "Trebuchet MS", Geneva, Arial, Helvetica, SunSans-Regular, Verdana, sans-serif;
+ margin: 0;
+}
+h1 {
+ color: #369;
+ font-size: 1.6em;
+}
+tr.odd td, tr.even td {
+ padding: 0.3em;
+}
+a:link {
+ text-decoration: none;
+ font-weight: bold;
+ color: #ff8c00;
+}
+a:visited {
+ text-decoration: none;
+ font-weight: bold;
+ color: #c96;
+}
+a:hover, a:active {
+ font-weight: bold;
+ color: #ff4500;
+ text-decoration: underline;
+}
+fieldset {
+ border: 1px solid #ccc;
+}
+p {
+ margin: 0 0 1.3em 0;
+ padding: 0;
+}
+blockquote {
+ border-left: 4px solid #69c;
+ padding: 0 15px 0 15px;
+ margin: 25px 100px 25px 50px;
+ color: #696969;
+ text-align: left;
+ font-size: 1.2em;
+ line-height: 1.3em;
+ font-family: "Trebuchet MS", Geneva, Arial, Helvetica, SunSans-Regular, Verdana, sans-serif;
+}
+pre {
+ background-color: #eee;
+ padding: 0.75em 1.5em;
+ font-size: 1.2em;
+ border: 1px solid #ddd;
+}
+.form-item {
+ margin-top: 1em;
+}
+.form-item label {
+ color: #369;
+}
+.item-list .title {
+ color: #369;
+ font-size: 0.85em;
+}
+
+/*
+** Page layout blocks / IDs
+*/
+#primary-menu {
+ border-collapse: separate;
+ background-color: #e0edfb;
+ border-bottom: 3px solid #69c;
+}
+#primary-menu tr {
+ background: transparent url(header-a.jpg) left bottom repeat;
+}
+td#home {
+ background: transparent url(logo-background.jpg) left top repeat;
+}
+td#home a:link img, td#home a:visited img {
+ background: transparent url(logo-active.jpg) repeat;
+ width: 144px;
+ height: 63px;
+}
+td#home a:hover img {
+ background: transparent url(logo-hover.jpg) repeat;
+ width: 144px;
+ height: 63px;
+}
+.primary-links, .primary-links a:link, .primary-links a:visited {
+ color: #369;
+}
+.primary-links a:hover {
+ color: #000;
+}
+#primary-menu .primary-links {
+ background: transparent url(header-b.jpg) left top no-repeat;
+ font-size: 0.79em;
+}
+#primary-menu .primary-links h1, #primary-menu .primary-links h2, #primary-menu .primary-links h3 {
+ font-size: 2.3em;
+ color: #369;
+}
+#secondary-menu {
+ border-collapse: separate;
+ background-color: #369;
+ border-bottom: 3px solid #69c;
+}
+.secondary-links, .secondary-links a:link, .secondary-links a:visited {
+ color: #e4e9eb;
+}
+.secondary-links a:hover {
+ color: #fff;
+ text-decoration: underline;
+}
+#secondary-menu .secondary-links {
+ font-size: 0.85em;
+}
+.tabs {
+ margin: 15px 0 15px 0;
+}
+.tabs ul.primary {
+ border-collapse: collapse;
+ padding: 0 0 3px 10px;
+ white-space: nowrap;
+ list-style: none;
+ margin: 0 0 0 0;
+ height: auto;
+ line-height: normal;
+ border-bottom: 2px solid #336699;
+}
+.tabs ul.primary li {
+ display: inline;
+}
+.tabs ul.primary li a {
+ padding: 3px 10px 3px 10px;
+ background: #fff url(tabs-off.png) left top no-repeat;
+ border-color: #6699CC;
+ border-width: 2px;
+ border-style: none solid none none;
+ height: auto;
+ margin-right: 10px;
+ text-decoration: none;
+ text-transform: lowercase;
+}
+.tabs ul.primary li.active a {
+ background: #336699 url(tabs-on.png) left top no-repeat;
+ border-right: 2px solid #336699;
+ color: #fff;
+}
+.tabs ul.primary li a:hover {
+ background-color: #fffaf0;
+ color: #ff4500;
+}
+.tabs ul.secondary {
+ border-collapse: collapse;
+ padding: 10px 0 10px 0;
+ margin: 0 0 0 0;
+ white-space: nowrap;
+ width: 100%;
+ list-style: none;
+ height: auto;
+ line-height: normal;
+ border-bottom: none;
+}
+.tabs ul.secondary li {
+ display: inline;
+ height: auto;
+ padding: 0 0 0 10px;
+ text-decoration: none;
+ border-right: none;
+}
+.tabs ul.secondary li a {
+ background: #fff url(tabs-option-off.png) left center no-repeat;
+ padding: 10px 0 10px 25px;
+ margin: 0 0 0 0;
+}
+.tabs ul.secondary li a.active {
+ background: #fff url(tabs-option-on.png) left center no-repeat;
+ color: #369;
+ border-bottom: none;
+}
+.tabs ul.secondary li a:hover {
+ background: #fff url(tabs-option-hover.png) left center no-repeat;
+ color: #FF4500
+}
+#content {
+ background-color: #fff;
+}
+#contentstart {
+ background-color: #fff;
+}
+#menu {
+ padding: 0.5em 0.5em 0 0.5em;
+ text-align: right;
+ vertical-align: middle;
+}
+#search .form-text, #search .form-submit {
+ border: 1px solid #369;
+ font-size: 0.85em;
+ margin: 0.2em;
+}
+#search .form-text {
+ width: 9em;
+}
+#search .form-submit {
+ height: 1.5em;
+}
+#mission {
+ background-color: #fff;
+ color: #696969;
+ border-top: 2px solid #dcdcdc;
+ border-bottom: 2px solid #dcdcdc;
+ padding: 10px 10px 10px 10px;
+ margin: 20px 35px 0 35px;
+ font-family: "Trebuchet MS", Geneva, Arial, Helvetica, SunSans-Regular, Verdana, sans-serif;
+ font-size: 1.1em;
+ font-weight: normal;
+}
+#site-info {
+ background-color: #bdd3ea;
+ background-image: url('header-c.png');
+ font-family: "Trebuchet MS", Geneva, Arial, Helvetica, SunSans-Regular, Verdana, sans-serif;
+}
+.site-name {
+ font-size: 1.2em;
+}
+.site-name a:link, .site-name a:visited {
+ color: #fff;
+}
+.site-name a:hover {
+ color: #ff8c00;
+ text-decoration: none;
+}
+.site-slogan {
+ font-size: 0.8em;
+ font-weight: bold;
+}
+#main {
+ /* padding in px not ex because IE messes up 100% width tables otherwise */
+ padding: 30px 35px 50px 35px;
+ background: transparent url(background.png) center center no-repeat;
+ /* fix background overlapping text in IE (aka Peekaboo Bug) */
+ position: relative;
+}
+#mission, .node .content, .comment .content {
+ line-height: 1.4;
+}
+#help {
+ font-size: 0.9em;
+ margin-bottom: 1em;
+}
+.breadcrumb {
+ margin-bottom: .5em;
+}
+.messages {
+ background-color: #eee;
+ border: 1px solid #ccc;
+ padding: 0.3em;
+ margin-bottom: 1em;
+}
+.error {
+ border-color: red;
+}
+.nav {
+ padding: 0;
+ margin: 0;
+}
+#sidebar-left, #sidebar-right {
+ font-size: 0.75em;
+ width: 175px;
+ /* padding in px not ex because IE messes up 100% width tables otherwise */
+ padding: 25px 10px 75px 10px;
+ vertical-align: top;
+ background: #FFFAF0;
+}
+#sidebar-left {
+ border-right: 3px solid #f5f5f5;
+}
+#sidebar-right {
+ border-left: 3px solid #f5f5f5;
+}
+#sidebar-left li, #sidebar-right li {
+ font-size: 1em;
+}
+.node .content {
+ text-align: left;
+ font-size: 0.85em;
+ line-height: 1.3;
+}
+.comment .content {
+ text-align: left;
+ font-size: 0.85em;
+ line-height: 1.3;
+}
+#footer-message {
+ padding: 15px 100px 30px 100px;
+ font-size: 0.85em;
+ text-align: center;
+ color: #aaa;
+}
+table#footer-menu {
+ border-top: 3px solid #6699cc;
+ border-bottom: 3px solid #6699cc;
+ background-color: #369;
+ color: #e4e9eb;
+}
+#footer-menu td {
+ padding: 5px;
+ font-size: 0.75em;
+}
+#footer-menu .primary-links, #footer-menu a:link, #footer-menu a:visited {
+ color: #e4e9eb;
+}
+#footer-menu a:hover {
+ color: #fff;
+ text-decoration: underline;
+}
+#footer-menu .primary-links h1, #footer-menu .primary-links h2, #footer-menu .primary-links h3 {
+ font-size: 1.3em;
+ color: #e4e9eb;
+}
+/*
+** Common declarations for child classes of node, comment, block, box, etc.
+** If you want any of them styled differently for a specific parent, add
+** additional rules /with only the differing properties!/ to .parent .class.
+** See .comment .title for an example.
+*/
+#content .title, #content .title a {
+ color: #369;
+}
+.content h1 {
+ color: #369;
+ font-size: 1.9em;
+}
+.content h2 {
+ color: #58b;
+ font-size: 1.7em;
+}
+.content h3 {
+ color: #69c;
+ font-size: 1.5em;
+}
+.content h4 {
+ color: #8be;
+ font-size: 1.3em;
+}
+.content h5 {
+ color: #96c6f6;
+ font-size: 1.15em;
+}
+.submitted {
+ color: #999;
+ font-size: 0.79em;
+}
+.links {
+ color: #ff8c00;
+ font-size: 0.8em;
+ padding: 0;
+ margin: 0;
+}
+.links a {
+ font-weight: bold;
+}
+.box {
+ padding: 0 0 1.5em 0;
+}
+.box {
+ padding: 0;
+ margin: 0;
+}
+.box h2 {
+ font-size: 9px;
+}
+.block .title h3 {
+ border-bottom: 2px solid #6699cc;
+ color: #369;
+ font-size: 18px;
+ font-weight: bold;
+ padding: 10px 5px 10px 30px;
+ margin-bottom: .25em;
+ background: transparent url(icon-block.png) left center no-repeat;
+}
+.block .content {
+ padding: 5px 5px 5px 5px;
+}
+.block {
+ margin-bottom: 1.5em;
+}
+.box .title {
+ font-size: 1.1em;
+}
+.node {
+ margin: .5em 0 2.5em 0;
+}
+.node .content, .comment .content {
+ margin: .5em 0 .5em 0;
+}
+.node .taxonomy {
+ color: #999;
+ font-size: 0.83em;
+ padding: 1.5em;
+}
+.node .picture {
+ border: 1px solid #fff;
+ float: right;
+ margin: 0.5em;
+}
+.comment {
+ border: 1px solid #abc;
+ padding: .5em;
+ margin-bottom: 1em;
+}
+.comment .title {
+ font-size: 1em;
+ padding: 10px 0 12px 19px;
+ background: transparent url(icon-comment.png) left center no-repeat;
+}
+.comment .new {
+ font-weight: bold;
+ font-size: 1em;
+ margin-left: 2px;
+ color: red;
+}
+.comment .picture {
+ border: 1px solid #fff;
+ float: right;
+ margin: 10px;
+}
+.links {
+ font-size: 0.75em;
+}
+.links .prev, .links .next, .links .up {
+ font-size: 1.15em;
+}
+.titles .prev, .titles .next {
+ font-size: 0.85em;
+ font-weight: bold;
+ color: #444;
+}
+.hide {
+ display: none
+}
+.nav .links .next a:link {
+ padding: 17px 17px 17px 0;
+ background: transparent url(arrow-next.png) right center no-repeat;
+}
+.nav .links .next a:visited {
+ padding: 17px 17px 17px 0;
+ background: transparent url(arrow-next-visited.png) right center no-repeat;
+}
+.nav .links .next a:hover {
+ padding: 17px 17px 17px 0;
+ background: transparent url(arrow-next-hover.png) right center no-repeat;
+}
+.nav .links .prev a:link {
+ padding: 17px 0 17px 17px;
+ background: transparent url(arrow-prev.png) left center no-repeat;
+}
+.nav .links .prev a:visited {
+ padding: 17px 0 17px 17px;
+ background: transparent url(arrow-prev-visited.png) left center no-repeat;
+}
+.nav .links .prev a:hover {
+ padding: 17px 0 17px 17px;
+ background: transparent url(arrow-prev-hover.png) left center no-repeat;
+}
+.nav .links .up a:link {
+ padding: 11px 0 17px 0;
+ background: transparent url(arrow-up.png) center top no-repeat;
+}
+.nav .links .up a:visited {
+ padding: 11px 0 17px 0;
+ background: transparent url(arrow-up-visited.png) center top no-repeat;
+}
+.nav .links .up a:hover {
+ padding: 11px 0 17px 0;
+ background: transparent url(arrow-up-hover.png) center top no-repeat;
+}
+
+/*
+** Module specific styles
+*/
+.content .active {
+ color: #369;
+}
+#aggregator .feed-source {
+ background-color: #eee;
+ border: 1px solid #ccc;
+ padding: 1em;
+ margin: 1em 0 1em 0;
+}
+#aggregator .news-item .source {
+ color: #999;
+ font-style: italic;
+ font-size: 0.85em;
+}
+#aggregator .title {
+ font-size: 1em;
+}
+#aggregator h3 {
+ margin-top: 1em;
+}
+#tracker th {
+ text-align: center;
+ background-color: #f5f5f5;
+ border-bottom: 1px solid #ddd;
+ border-right: 1px solid #ddd;
+ border-left: 1px solid #fafafa;
+}
+#tracker th img {
+ float: right;
+}
+#tracker tr.even, #tracker tr.odd {
+ background-color: #fff;
+}
+#tracker td {
+ vertical-align: top;
+ padding: 1em 1em 1em 0;
+ border-bottom: 1px solid #bbb;
+}
+#forum {
+ margin: 15px 0 15px 0;
+ background-color: #fff;
+}
+#forum table {
+ width: 100%;
+ border: 2px solid #69c;
+}
+#forum table tr th {
+ text-align: center;
+ background: #69c;
+ color: #fff;
+ font-size: 0.75em;
+ border-bottom: 1px solid #aaa;
+}
+#forum table tr th a {
+ color: #fff;
+ text-decoration: underline;
+}
+#forum table tr th img {
+ margin: 0;
+}
+#forum tr.odd {
+ background: #e0edfb;
+}
+#forum tr.even {
+ background: #fff;
+}
+#forum td {
+ padding: 0.5em 0.5em 0.5em 0.5em;
+}
+#forum td.container {
+ color: #000;
+ background: #369 url(forum-container.jpg) right top no-repeat;
+ border: 2px solid #69c;
+}
+#forum td.container a {
+ color: #e4e9eb;
+ padding: 20px 0 20px 35px;
+ background: transparent url(forum-link.png) left center no-repeat;
+}
+#forum td.container a:visited {
+ color: #e4e9eb;
+}
+#forum td.statistics, #forum td.settings, #forum td.pager {
+ height: 1.5em;
+ border: 1px solid #bbb;
+}
+#forum td .name {
+ color: #96c;
+}
+#forum td .links {
+ padding-top: 0.7em;
+ font-size: 0.9em;
+}
+.block-forum h3 {
+ margin-bottom: .5em;
+}
+.calendar a {
+ text-decoration: none;
+}
+.calendar td, .calendar th {
+ padding: 0.4em 0;
+ border-color: #888;
+}
+.calendar .row-week td a {
+ padding: 0.4em 0;
+}
+.calendar .day-today {
+ background-color: #69c;
+}
+.calendar .day-today a {
+ color: #fff;
+}
+.calendar .day-selected {
+ background-color: #369;
+ color: #fff;
+}
+.calendar .header-week {
+ background-color: #ccc;
+}
+.calendar .day-blank {
+ background-color: #ccc;
+}
+.calendar .row-week td a:hover {
+ background-color: #fff; color: #000;
+}
diff --git a/themes/pushbutton/tabs-off.png b/themes/pushbutton/tabs-off.png
new file mode 100644
index 0000000..5199a9e
Binary files /dev/null and b/themes/pushbutton/tabs-off.png differ
diff --git a/themes/pushbutton/tabs-on.png b/themes/pushbutton/tabs-on.png
new file mode 100644
index 0000000..4f73e50
Binary files /dev/null and b/themes/pushbutton/tabs-on.png differ
diff --git a/themes/pushbutton/tabs-option-hover.png b/themes/pushbutton/tabs-option-hover.png
new file mode 100644
index 0000000..6a77858
Binary files /dev/null and b/themes/pushbutton/tabs-option-hover.png differ
diff --git a/themes/pushbutton/tabs-option-off.png b/themes/pushbutton/tabs-option-off.png
new file mode 100644
index 0000000..e3c3a3a
Binary files /dev/null and b/themes/pushbutton/tabs-option-off.png differ
diff --git a/themes/pushbutton/tabs-option-on.png b/themes/pushbutton/tabs-option-on.png
new file mode 100644
index 0000000..93ca794
Binary files /dev/null and b/themes/pushbutton/tabs-option-on.png differ
diff --git a/update.php b/update.php
new file mode 100644
index 0000000..edb4d4b
--- /dev/null
+++ b/update.php
@@ -0,0 +1,718 @@
+ $result !== FALSE, 'query' => check_plain($sql));
+}
+
+/**
+ * Add a column to a database using syntax appropriate for PostgreSQL.
+ * Save result of SQL commands in $ret array.
+ *
+ * Note: when you add a column with NOT NULL and you are not sure if there are
+ * already rows in the table, you MUST also add DEFAULT. Otherwise PostgreSQL won't
+ * work when the table is not empty. If NOT NULL and DEFAULT are set the
+ * PostgreSQL version will set values of the added column in old rows to the
+ * DEFAULT value.
+ *
+ * @param $ret
+ * Array to which results will be added.
+ * @param $table
+ * Name of the table, without {}
+ * @param $column
+ * Name of the column
+ * @param $type
+ * Type of column
+ * @param $attributes
+ * Additional optional attributes. Recognized attributes:
+ * not null => TRUE|FALSE
+ * default => NULL|FALSE|value (with or without '', it won't be added)
+ * @return
+ * nothing, but modifies $ret parameter.
+ */
+function db_add_column(&$ret, $table, $column, $type, $attributes = array()) {
+ if (array_key_exists('not null', $attributes) and $attributes['not null']) {
+ $not_null = 'NOT NULL';
+ }
+ if (array_key_exists('default', $attributes)) {
+ if (is_null($attributes['default'])) {
+ $default_val = 'NULL';
+ $default = 'default NULL';
+ }
+ elseif ($attributes['default'] === FALSE) {
+ $default = '';
+ }
+ else {
+ $default_val = "$attributes[default]";
+ $default = "default $attributes[default]";
+ }
+ }
+
+ $ret[] = update_sql("ALTER TABLE {". $table ."} ADD $column $type");
+ if ($default) { $ret[] = update_sql("ALTER TABLE {". $table ."} ALTER $column SET $default"); }
+ if ($not_null) {
+ if ($default) { $ret[] = update_sql("UPDATE {". $table ."} SET $column = $default_val"); }
+ $ret[] = update_sql("ALTER TABLE {". $table ."} ALTER $column SET NOT NULL");
+ }
+}
+
+/**
+ * Change a column definition using syntax appropriate for PostgreSQL.
+ * Save result of SQL commands in $ret array.
+ *
+ * Remember that changing a column definition involves adding a new column
+ * and dropping an old one. This means that any indices, primary keys and
+ * sequences from serial-type columns are dropped and might need to be
+ * recreated.
+ *
+ * @param $ret
+ * Array to which results will be added.
+ * @param $table
+ * Name of the table, without {}
+ * @param $column
+ * Name of the column to change
+ * @param $column_new
+ * New name for the column (set to the same as $column if you don't want to change the name)
+ * @param $type
+ * Type of column
+ * @param $attributes
+ * Additional optional attributes. Recognized attributes:
+ * not null => TRUE|FALSE
+ * default => NULL|FALSE|value (with or without '', it won't be added)
+ * @return
+ * nothing, but modifies $ret parameter.
+ */
+function db_change_column(&$ret, $table, $column, $column_new, $type, $attributes = array()) {
+ if (array_key_exists('not null', $attributes) and $attributes['not null']) {
+ $not_null = 'NOT NULL';
+ }
+ if (array_key_exists('default', $attributes)) {
+ if (is_null($attributes['default'])) {
+ $default_val = 'NULL';
+ $default = 'default NULL';
+ }
+ elseif ($attributes['default'] === FALSE) {
+ $default = '';
+ }
+ else {
+ $default_val = "$attributes[default]";
+ $default = "default $attributes[default]";
+ }
+ }
+
+ $ret[] = update_sql("ALTER TABLE {". $table ."} RENAME $column TO ". $column ."_old");
+ $ret[] = update_sql("ALTER TABLE {". $table ."} ADD $column_new $type");
+ $ret[] = update_sql("UPDATE {". $table ."} SET $column_new = ". $column ."_old");
+ if ($default) { $ret[] = update_sql("ALTER TABLE {". $table ."} ALTER $column_new SET $default"); }
+ if ($not_null) { $ret[] = update_sql("ALTER TABLE {". $table ."} ALTER $column_new SET NOT NULL"); }
+ $ret[] = update_sql("ALTER TABLE {". $table ."} DROP ". $column ."_old");
+}
+
+/**
+ * If the schema version for Drupal core is stored in the variables table
+ * (4.6.x and earlier) move it to the schema_version column of the system
+ * table.
+ *
+ * This function may be removed when update 156 is removed, which is the last
+ * update in the 4.6 to 4.7 migration.
+ */
+function update_fix_schema_version() {
+ if ($update_start = variable_get('update_start', FALSE)) {
+ // Some updates were made to the 4.6 branch and 4.7 branch. This sets
+ // temporary variables to prevent the updates from being executed twice and
+ // throwing errors.
+ switch ($update_start) {
+ case '2005-04-14':
+ variable_set('update_132_done', TRUE);
+ break;
+
+ case '2005-05-06':
+ variable_set('update_132_done', TRUE);
+ variable_set('update_135_done', TRUE);
+ break;
+
+ case '2005-05-07':
+ variable_set('update_132_done', TRUE);
+ variable_set('update_135_done', TRUE);
+ variable_set('update_137_done', TRUE);
+ break;
+
+ }
+ // The schema_version column (added below) was changed during 4.7beta.
+ // Update_170 is only for those beta users.
+ variable_set('update_170_done', TRUE);
+
+ $sql_updates = array(
+ '2004-10-31: first update since Drupal 4.5.0 release' => 110,
+ '2004-11-07' => 111, '2004-11-15' => 112, '2004-11-28' => 113,
+ '2004-12-05' => 114, '2005-01-07' => 115, '2005-01-14' => 116,
+ '2005-01-18' => 117, '2005-01-19' => 118, '2005-01-20' => 119,
+ '2005-01-25' => 120, '2005-01-26' => 121, '2005-01-27' => 122,
+ '2005-01-28' => 123, '2005-02-11' => 124, '2005-02-23' => 125,
+ '2005-03-03' => 126, '2005-03-18' => 127, '2005-03-21' => 128,
+ // The following three updates were made on the 4.6 branch
+ '2005-04-14' => 128, '2005-05-06' => 128, '2005-05-07' => 128,
+ '2005-04-08: first update since Drupal 4.6.0 release' => 129,
+ '2005-04-10' => 130, '2005-04-11' => 131, '2005-04-14' => 132,
+ '2005-04-24' => 133, '2005-04-30' => 134, '2005-05-06' => 135,
+ '2005-05-08' => 136, '2005-05-09' => 137, '2005-05-10' => 138,
+ '2005-05-11' => 139, '2005-05-12' => 140, '2005-05-22' => 141,
+ '2005-07-29' => 142, '2005-07-30' => 143, '2005-08-08' => 144,
+ '2005-08-15' => 145, '2005-08-25' => 146, '2005-09-07' => 147,
+ '2005-09-18' => 148, '2005-09-27' => 149, '2005-10-15' => 150,
+ '2005-10-23' => 151, '2005-10-28' => 152, '2005-11-03' => 153,
+ '2005-11-14' => 154, '2005-11-27' => 155, '2005-12-03' => 156,
+ );
+
+ // Add schema version column
+ switch ($GLOBALS['db_type']) {
+ case 'pgsql':
+ $ret = array();
+ db_add_column($ret, 'system', 'schema_version', 'smallint', array('not null' => TRUE, 'default' => -1));
+ break;
+
+ case 'mysql':
+ case 'mysqli':
+ db_query('ALTER TABLE {system} ADD schema_version smallint(3) not null default -1');
+ break;
+ }
+ // Set all enabled (contrib) modules to schema version 0 (installed)
+ db_query('UPDATE {system} SET schema_version = 0 WHERE status = 1');
+
+ // Set schema version for core
+ drupal_set_installed_schema_version('system', $sql_updates[$update_start]);
+ variable_del('update_start');
+ }
+}
+
+/**
+ * System update 130 changes the sessions table, which breaks the update
+ * script's ability to use session variables. This changes the table
+ * appropriately.
+ *
+ * This code, including the 'update_sessions_fixed' variable, may be removed
+ * when update 130 is removed. It is part of the Drupal 4.6 to 4.7 migration.
+ */
+function update_fix_sessions() {
+ $ret = array();
+
+ if (drupal_get_installed_schema_version('system') < 130 && !variable_get('update_sessions_fixed', FALSE)) {
+ if ($GLOBALS['db_type'] == 'mysql') {
+ db_query("ALTER TABLE {sessions} ADD cache int(11) NOT NULL default '0' AFTER timestamp");
+ }
+ elseif ($GLOBALS['db_type'] == 'pgsql') {
+ db_add_column($ret, 'sessions', 'cache', 'int', array('default' => 0, 'not null' => TRUE));
+ }
+
+ variable_set('update_sessions_fixed', TRUE);
+ }
+}
+
+/**
+ * System update 115 changes the watchdog table, which breaks the update
+ * script's ability to use logging. This changes the table appropriately.
+ *
+ * This code, including the 'update_watchdog_115_fixed' variable, may be removed
+ * when update 115 is removed. It is part of the Drupal 4.5 to 4.7 migration.
+ */
+function update_fix_watchdog_115() {
+ if (drupal_get_installed_schema_version('system') < 115 && !variable_get('update_watchdog_115_fixed', FALSE)) {
+ if ($GLOBALS['db_type'] == 'mysql') {
+ $ret[] = update_sql("ALTER TABLE {watchdog} ADD severity tinyint(3) unsigned NOT NULL default '0'");
+ }
+ else if ($GLOBALS['db_type'] == 'pgsql') {
+ $ret[] = update_sql('ALTER TABLE {watchdog} ADD severity smallint');
+ $ret[] = update_sql('UPDATE {watchdog} SET severity = 0');
+ $ret[] = update_sql('ALTER TABLE {watchdog} ALTER COLUMN severity SET NOT NULL');
+ $ret[] = update_sql('ALTER TABLE {watchdog} ALTER COLUMN severity SET DEFAULT 0');
+ }
+
+ variable_set('update_watchdog_115_fixed', TRUE);
+ }
+}
+
+/**
+ * System update 142 changes the watchdog table, which breaks the update
+ * script's ability to use logging. This changes the table appropriately.
+ *
+ * This code, including the 'update_watchdog_fixed' variable, may be removed
+ * when update 142 is removed. It is part of the Drupal 4.6 to 4.7 migration.
+ */
+function update_fix_watchdog() {
+ if (drupal_get_installed_schema_version('system') < 142 && !variable_get('update_watchdog_fixed', FALSE)) {
+ switch ($GLOBALS['db_type']) {
+ case 'pgsql':
+ $ret = array();
+ db_add_column($ret, 'watchdog', 'referer', 'varchar(128)', array('not null' => TRUE, 'default' => "''"));
+ break;
+ case 'mysql':
+ case 'mysqli':
+ db_query("ALTER TABLE {watchdog} ADD COLUMN referer varchar(128) NOT NULL");
+ break;
+ }
+
+ variable_set('update_watchdog_fixed', TRUE);
+ }
+}
+
+/**
+ * Perform one update and store the results which will later be displayed on
+ * the finished page.
+ *
+ * @param $module
+ * The module whose update will be run.
+ * @param $number
+ * The update number to run.
+ *
+ * @return
+ * TRUE if the update was finished. Otherwise, FALSE.
+ */
+function update_data($module, $number) {
+ $ret = module_invoke($module, 'update_'. $number);
+ // Assume the update finished unless the update results indicate otherwise.
+ $finished = 1;
+ if (isset($ret['#finished'])) {
+ $finished = $ret['#finished'];
+ unset($ret['#finished']);
+ }
+
+ // Save the query and results for display by update_finished_page().
+ if (!isset($_SESSION['update_results'])) {
+ $_SESSION['update_results'] = array();
+ }
+ if (!isset($_SESSION['update_results'][$module])) {
+ $_SESSION['update_results'][$module] = array();
+ }
+ if (!isset($_SESSION['update_results'][$module][$number])) {
+ $_SESSION['update_results'][$module][$number] = array();
+ }
+ $_SESSION['update_results'][$module][$number] = array_merge($_SESSION['update_results'][$module][$number], $ret);
+
+ if ($finished == 1) {
+ // Update the installed version
+ drupal_set_installed_schema_version($module, $number);
+ }
+
+ return $finished;
+}
+
+function update_selection_page() {
+ $output = 'The version of Drupal you are updating from has been automatically detected. You can select a different version, but you should not need to.
';
+ $output .= 'Click Update to start the update process.
';
+
+ $form = array();
+ $form['start'] = array(
+ '#tree' => TRUE,
+ '#type' => 'fieldset',
+ '#title' => 'Select versions',
+ '#collapsible' => TRUE,
+ '#collapsed' => TRUE,
+ );
+ foreach (module_list() as $module) {
+ $updates = drupal_get_schema_versions($module);
+ if ($updates !== FALSE) {
+ $updates = drupal_map_assoc($updates);
+ $updates[] = 'No updates available';
+
+ $form['start'][$module] = array(
+ '#type' => 'select',
+ '#title' => $module . ' module',
+ '#default_value' => array_search(drupal_get_installed_schema_version($module), $updates) + 1,
+ '#options' => $updates,
+ );
+ }
+ }
+
+ $form['has_js'] = array(
+ '#type' => 'hidden',
+ '#default_value' => FALSE,
+ '#attributes' => array('id' => 'edit-has_js'),
+ );
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => 'Update',
+ );
+
+ drupal_set_title('Drupal database update');
+ // Prevent browser from using cached drupal.js or update.js
+ drupal_add_js('misc/update.js', TRUE);
+ $output .= drupal_get_form('update_script_selection_form', $form);
+
+ return $output;
+}
+
+function update_update_page() {
+ // Set the installed version so updates start at the correct place.
+ $_SESSION['update_remaining'] = array();
+ foreach ($_POST['edit']['start'] as $module => $version) {
+ drupal_set_installed_schema_version($module, $version - 1);
+ $max_version = max(drupal_get_schema_versions($module));
+ if ($version <= $max_version) {
+ foreach (range($version, $max_version) as $update) {
+ $_SESSION['update_remaining'][] = array('module' => $module, 'version' => $update);
+ }
+ }
+ }
+ // Keep track of total number of updates
+ $_SESSION['update_total'] = count($_SESSION['update_remaining']);
+
+ if ($_POST['edit']['has_js']) {
+ return update_progress_page();
+ }
+ else {
+ return update_progress_page_nojs();
+ }
+}
+
+function update_progress_page() {
+ // Prevent browser from using cached drupal.js or update.js
+ drupal_add_js('misc/progress.js', TRUE);
+ drupal_add_js('misc/update.js', TRUE);
+
+ drupal_set_title('Updating');
+ $output = '';
+ $output .= 'Please wait while your site is being updated.
';
+ return $output;
+}
+
+/**
+ * Perform updates for one second or until finished.
+ *
+ * @return
+ * An array indicating the status after doing updates. The first element is
+ * the overall percentage finished. The second element is a status message.
+ */
+function update_do_updates() {
+ while (($update = reset($_SESSION['update_remaining']))) {
+ $update_finished = update_data($update['module'], $update['version']);
+ if ($update_finished == 1) {
+ // Dequeue the completed update.
+ unset($_SESSION['update_remaining'][key($_SESSION['update_remaining'])]);
+ $update_finished = 0; // Make sure this step isn't counted double
+ }
+ if (timer_read('page') > 1000) {
+ break;
+ }
+ }
+
+ if ($_SESSION['update_total']) {
+ $percentage = floor(($_SESSION['update_total'] - count($_SESSION['update_remaining']) + $update_finished) / $_SESSION['update_total'] * 100);
+ }
+ else {
+ $percentage = 100;
+ }
+
+ // When no updates remain, clear the cache.
+ if (!isset($update['module'])) {
+ db_query('DELETE FROM {cache}');
+ }
+
+ return array($percentage, isset($update['module']) ? 'Updating '. $update['module'] .' module' : 'Updating complete');
+}
+
+/**
+ * Perform updates for the JS version and return progress.
+ */
+function update_do_update_page() {
+ global $conf;
+
+ // HTTP Post required
+ if ($_SERVER['REQUEST_METHOD'] != 'POST') {
+ drupal_set_message('HTTP Post is required.', 'error');
+ drupal_set_title('Error');
+ return '';
+ }
+
+ // Error handling: if PHP dies, the output will fail to parse as JSON, and
+ // the Javascript will tell the user to continue to the op=error page.
+ list($percentage, $message) = update_do_updates();
+ print drupal_to_js(array('status' => TRUE, 'percentage' => $percentage, 'message' => $message));
+}
+
+/**
+ * Perform updates for the non-JS version and return the status page.
+ */
+function update_progress_page_nojs() {
+ drupal_set_title('Updating');
+
+ $new_op = 'do_update_nojs';
+ if ($_SERVER['REQUEST_METHOD'] == 'GET') {
+ // Error handling: if PHP dies, it will output whatever is in the output
+ // buffer, followed by the error message.
+ ob_start();
+ $fallback = 'An unrecoverable error has occurred. You can find the error message below. It is advised to copy it to the clipboard for reference. Please continue to the update summary.
';
+ print theme('maintenance_page', $fallback, FALSE, TRUE);
+
+ list($percentage, $message) = update_do_updates();
+ if ($percentage == 100) {
+ $new_op = 'finished';
+ }
+
+ // Updates successful; remove fallback
+ ob_end_clean();
+ }
+ else {
+ // This is the first page so return some output immediately.
+ $percentage = 0;
+ $message = 'Starting updates';
+ }
+
+ drupal_set_html_head('');
+ $output = theme('progress_bar', $percentage, $message);
+ $output .= '
Updating your site will take a few seconds.
';
+
+ // Note: do not output drupal_set_message()s until the summary page.
+ print theme('maintenance_page', $output, FALSE);
+ return NULL;
+}
+
+function update_finished_page($success) {
+ drupal_set_title('Drupal database update');
+ // NOTE: we can't use l() here because the URL would point to 'update.php?q=admin'.
+ $links[] = 'main page';
+ $links[] = 'administration pages';
+
+ // Report end result
+ if ($success) {
+ $output = 'Updates were attempted. If you see no failures below, you may proceed happily to the administration pages. Otherwise, you may need to update your database manually. All errors have been logged.
';
+ }
+ else {
+ $update = reset($_SESSION['update_remaining']);
+ $output = 'The update process was aborted prematurely while running update #'. $update['version'] .' in '. $update['module'] .'.module. All other errors have been logged. You may need to check the watchdog
database table manually.
';
+ }
+
+ if ($GLOBALS['access_check'] == FALSE) {
+ $output .= "Reminder: don't forget to set the \$access_check
value at the top of update.php
back to TRUE
.
";
+ }
+
+ $output .= theme('item_list', $links);
+
+ // Output a list of queries executed
+ if ($_SESSION['update_results']) {
+ $output .= '';
+ $output .= 'The following queries were executed
';
+ foreach ($_SESSION['update_results'] as $module => $updates) {
+ $output .= ''. $module .' module
';
+ foreach ($updates as $number => $queries) {
+ $output .= 'Update #'. $number .'
';
+ $output .= '';
+ foreach ($queries as $query) {
+ if ($query['success']) {
+ $output .= '- '. $query['query'] .'
';
+ }
+ else {
+ $output .= '- Failed: '. $query['query'] .'
';
+ }
+ }
+ if (!count($queries)) {
+ $output .= '- No queries
';
+ }
+ $output .= '
';
+ }
+ }
+ $output .= '';
+ unset($_SESSION['update_results']);
+ }
+
+ return $output;
+}
+
+function update_info_page() {
+ drupal_set_title('Drupal database update');
+ $output = "\n";
+ $output .= "- Use this script to upgrade an existing Drupal installation. You don't need this script when installing Drupal from scratch.
";
+ $output .= "- Before doing anything, backup your database. This process will change your database and its values, and some things might get lost.
\n";
+ $output .= "- Update your Drupal sources, check the notes below and run the database upgrade script. Don't upgrade your database twice as it may cause problems.
\n";
+ $output .= "- Go through the various administration pages to change the existing and new settings to your liking.
\n";
+ $output .= "
";
+ $output .= 'For more help, see the Installation and upgrading handbook. If you are unsure what these terms mean you should probably contact your hosting provider.
';
+ return $output;
+}
+
+function update_access_denied_page() {
+ drupal_set_title('Access denied');
+ return 'Access denied. You are not authorized to access this page. Please log in as the admin user (the first user you created). If you cannot log in, you will have to edit update.php
to bypass this access check. To do this:
+
+ - With a text editor find the update.php file on your system. It should be in the main Drupal directory that you installed all the files into.
+ - There is a line near top of update.php that says
$access_check = TRUE;
. Change it to $access_check = FALSE;
.
+ - As soon as the script is done, you must change the update.php script back to its original form to
$access_check = TRUE;
.
+ - To avoid having this problem in future, remember to log in to your website as the admin user (the user you first created) before you backup your database at the beginning of the update process.
+
';
+}
+
+// This code may be removed later. It is part of the Drupal 4.5 to 4.7 migration.
+function update_fix_system_table() {
+ drupal_bootstrap(DRUPAL_BOOTSTRAP_DATABASE);
+ $row = db_fetch_object(db_query_range('SELECT * FROM {system}', 0, 1));
+ if (!isset($row->weight)) {
+ $ret = array();
+ switch ($GLOBALS['db_type']) {
+ case 'pgsql':
+ db_add_column($ret, 'system', 'weight', 'smallint', array('not null' => TRUE, 'default' => 0));
+ $ret[] = update_sql('CREATE INDEX {system}_weight_idx ON {system} (weight)');
+ break;
+ case 'mysql':
+ case 'mysqli':
+ $ret[] = update_sql("ALTER TABLE {system} ADD weight tinyint(2) default '0' NOT NULL, ADD KEY (weight)");
+ break;
+ }
+ }
+}
+
+// This code may be removed later. It is part of the Drupal 4.6 to 4.7 migration.
+function update_fix_access_table() {
+ if (variable_get('update_access_fixed', FALSE)) {
+ return;
+ }
+
+ switch ($GLOBALS['db_type']) {
+ // Only for MySQL 4.1+
+ case 'mysqli':
+ break;
+ case 'mysql':
+ if (version_compare(mysql_get_server_info($GLOBALS['active_db']), '4.1.0', '<')) {
+ return;
+ }
+ break;
+ case 'pgsql':
+ return;
+ }
+
+ // Convert access table to UTF-8 if needed.
+ $result = db_fetch_array(db_query('SHOW CREATE TABLE {access}'));
+ if (!preg_match('/utf8/i', array_pop($result))) {
+ update_convert_table_utf8('access');
+ }
+
+ // Don't run again
+ variable_set('update_access_fixed', TRUE);
+}
+
+/**
+ * Convert a single MySQL table to UTF-8.
+ *
+ * We change all text columns to their corresponding binary type,
+ * then back to text, but with a UTF-8 character set.
+ * See: http://dev.mysql.com/doc/refman/4.1/en/charset-conversion.html
+ */
+function update_convert_table_utf8($table) {
+ $ret = array();
+ $types = array('char' => 'binary',
+ 'varchar' => 'varbinary',
+ 'tinytext' => 'tinyblob',
+ 'text' => 'blob',
+ 'mediumtext' => 'mediumblob',
+ 'longtext' => 'longblob');
+
+ // Get next table in list
+ $convert_to_binary = array();
+ $convert_to_utf8 = array();
+
+ // Set table default charset
+ $ret[] = update_sql('ALTER TABLE {'. $table .'} DEFAULT CHARACTER SET utf8');
+
+ // Find out which columns need converting and build SQL statements
+ $result = db_query('SHOW FULL COLUMNS FROM {'. $table .'}');
+ while ($column = db_fetch_array($result)) {
+ list($type) = explode('(', $column['Type']);
+ if (isset($types[$type])) {
+ $names = 'CHANGE `'. $column['Field'] .'` `'. $column['Field'] .'` ';
+ $attributes = ' DEFAULT '. ($column['Default'] == 'NULL' ? 'NULL ' :
+ "'". db_escape_string($column['Default']) ."' ") .
+ ($column['Null'] == 'YES' ? 'NULL' : 'NOT NULL');
+
+ $convert_to_binary[] = $names . preg_replace('/'. $type .'/i', $types[$type], $column['Type']) . $attributes;
+ $convert_to_utf8[] = $names . $column['Type'] .' CHARACTER SET utf8'. $attributes;
+ }
+ }
+
+ if (count($convert_to_binary)) {
+ // Convert text columns to binary
+ $ret[] = update_sql('ALTER TABLE {'. $table .'} '. implode(', ', $convert_to_binary));
+ // Convert binary columns to UTF-8
+ $ret[] = update_sql('ALTER TABLE {'. $table .'} '. implode(', ', $convert_to_utf8));
+ }
+ return $ret;
+}
+
+// Some unavoidable errors happen because the database is not yet up-to-date.
+// Our custom error handler is not yet installed, so we just suppress them.
+ini_set('display_errors', FALSE);
+
+include_once './includes/bootstrap.inc';
+update_fix_system_table();
+update_fix_access_table();
+
+drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
+drupal_maintenance_theme();
+
+// Turn error reporting back on. From now on, only fatal errors (which are
+// not passed through the error handler) will cause a message to be printed.
+ini_set('display_errors', TRUE);
+
+// Access check:
+if (($access_check == FALSE) || ($user->uid == 1)) {
+
+ include_once './includes/install.inc';
+
+ update_fix_schema_version();
+ update_fix_watchdog_115();
+ update_fix_watchdog();
+ update_fix_sessions();
+
+ $op = isset($_REQUEST['op']) ? $_REQUEST['op'] : '';
+ switch ($op) {
+ case 'Update':
+ $output = update_update_page();
+ break;
+
+ case 'finished':
+ $output = update_finished_page(true);
+ break;
+
+ case 'error':
+ $output = update_finished_page(false);
+ break;
+
+ case 'do_update':
+ $output = update_do_update_page();
+ break;
+
+ case 'do_update_nojs':
+ $output = update_progress_page_nojs();
+ break;
+
+ case 'selection':
+ $output = update_selection_page();
+ break;
+
+ default:
+ $output = update_info_page();
+ break;
+ }
+}
+else {
+ $output = update_access_denied_page();
+}
+
+if (isset($output)) {
+ print theme('maintenance_page', $output);
+}
diff --git a/xmlrpc.php b/xmlrpc.php
new file mode 100644
index 0000000..3fca0b7
--- /dev/null
+++ b/xmlrpc.php
@@ -0,0 +1,14 @@
+