From: Thierry Parmentelat Date: Tue, 4 Mar 2008 09:03:43 +0000 (+0000) Subject: initial import from onelab svn codebase X-Git-Tag: PLEWWW-PLE.5.0-0~28 X-Git-Url: http://git.onelab.eu/?p=plewww.git;a=commitdiff_plain;h=ba1db01078f414acb4c2280df2ebfd0cabe97b41 initial import from onelab svn codebase --- ba1db01078f414acb4c2280df2ebfd0cabe97b41 diff --git a/CHANGELOG.txt b/CHANGELOG.txt new file mode 100644 index 0000000..decbbf2 --- /dev/null +++ b/CHANGELOG.txt @@ -0,0 +1,542 @@ +// $Id: CHANGELOG.txt 144 2007-03-28 07:52:20Z thierry $ + +Drupal 4.7.3, 2006-08-02 +------------------------ +- fixed security issue (XSS), see SA-2006-011 + +Drupal 4.7.2, 2006-06-01 +------------------------ +- fixed critical upload issue, see SA-2006-007 +- fixed taxonomy XSS issue, see SA-2006-008 +- fixed a variety of small bugs. + +Drupal 4.7.1, 2006-05-24 +------------------------ +- fixed critical SQL issue, see SA-2006-005 +- fixed a serious upgrade related bug. +- fixed a variety of small bugs. + +Drupal 4.7.0, 2006-05-01 +------------------------ +- added free tagging support. +- added a site-wide contact form. +- theme system: + * added the PHPTemplate theme engine and removed the Xtemplate engine. + * converted the bluemarine theme from XTemplate to PHPTemplate. + * converted the pushbutton theme from XTemplate to PHPTemplate. +- usability: + * reworked the 'request new password' functionality. + * reworked the node and comment edit forms. + * made it easy to add nodes to the navigation menu. + * added site 'offline for maintenance' feature. + * added support for auto-complete forms (AJAX). + * added support for collapsible page sections (JS). + * added support for resizable text fields (JS). + * improved file upload functionality (AJAX). + * reorganized some settings pages. + * added friendly database error screens. + * improved styling of update.php. +- refactored the forms API. + * made it possible to alter, extend or theme forms. +- comment system: + * added support for "mass comment operations" to ease repetitive tasks. + * comment moderation has been removed. +- node system: + * reworked the revision functionality. + * removed the bookmarklet code. Third-party modules can now handle + this. +- upgrade system: + * allows contributed modules to plug into the upgrade system. +- profiles: + * added a block to display author information along with posts. + * added support for private profile fields. +- statistics module: + * added the ability to track page generation times. + * made it possible to block certain IPs/hostnames. +- block system: + * added support for theme-specific block regions. +- syndication: + * made the aggregator module parse Atom feeds. + * made the aggregator generate RSS feeds. + * added RSS feed settings. +- XML-RPC: + * replaced the XML-RPC library by a better one. +- performance: + * added 'loose caching' option for high-traffic sites. + * improved performance of path aliasing. + * added the ability to track page generation times. +- internationalization: + * improved Unicode string handling API. + * added support for PHP's multibyte string module. +- added support for PHP5's 'mysqli' extension. +- search module: + * made indexer smarter and more robust. + * added advanced search operators (e.g. phrase, node type, ...). + * added customizable result ranking. +- PostgreSQL support: + * removed dependency on PL/pgSQL procedural language. +- menu system: + * added support for external URLs. +- queue module: + * removed from core. +- HTTP handling: + * added support for a tolerant Base URL. + * output URIs relative to the root, without a base tag. + +Drupal 4.6.6, 2006-03-13 +------------------------ +- fixed bugs, including 4 security vulnerabilities. + +Drupal 4.6.5, 2005-12-12 +------------------------ +- fixed bugs: no critical bugs were identified. + +Drupal 4.6.4, 2005-11-30 +------------------------ +- fixed bugs, including 3 security vulnerabilities. + +Drupal 4.6.3, 2005-08-15 +------------------------ +- fixed bugs, including a critical "arbitrary PHP code execution" bug. + +Drupal 4.6.2, 2005-06-29 +------------------------ +- fixed bugs, including two critical "arbitrary PHP code execution" bugs. + +Drupal 4.6.1, 2005-06-01 +------------------------ +- fixed bugs, including a critical input validation bug. + +Drupal 4.6.0, 2005-04-15 +------------------------ +- PHP5 compliance +- search: + * added UTF-8 support to make it work with all languages. + * improved search indexing algorithm. + * improved search output. + * impose a throttle on indexing of large sites. + * added search block. +- syndication: + * made the ping module ping pingomatic.com which, in turn, will ping all the major ping services. + * made Drupal generate RSS 2.0 feeds. + * made RSS feeds extensible. + * added categories to RSS feeds. + * added enclosures to RSS feeds. +- flood control mechanism: + * added a mechanism to throttle certain operations. +- usability: + * refactored the block configuration pages. + * refactored the statistics pages. + * refactored the watchdog pages. + * refactored the throttle module configuration. + * refactored the access rules page. + * refactored the content administration page. + * introduced forum configuration pages. + * added a 'add child page' link to book pages. +- contact module: + * added a simple contact module that allows users to contact each other using e-mail. +- multi-site configuration: + * made it possible to run multiple sites from a single code base. +- added an image API: enables better image handling. +- block system: + * extended the block visibility settings. +- theme system: + * added new theme functions. +- database backend: + * the PEAR database backend is no longer supported. +- performance: + * improved performance of the forum topics block. + * improved performance of the tracker module. + * improved performance of the node pages. +- documentation: + * improved and extended PHPDoc/Doxygen comments. + +Drupal 4.5.8, 2006-03-13 +------------------------ +- fixed bugs, including 3 security vulnerabilities. + +Drupal 4.5.7, 2005-12-12 +------------------------ +- fixed bugs: no critical bugs were identified. + +Drupal 4.5.6, 2005-11-30 +------------------------ +- fixed bugs, including 3 security vulnerabilities. + +Drupal 4.5.5, 2005-08-15 +------------------------ +- fixed bugs, including a critical "arbitrary PHP code execution" bug. + +Drupal 4.5.4, 2005-06-29 +------------------------ +- fixed bugs, including two critical "arbitrary PHP code execution" bugs. + +Drupal 4.5.3, 2005-06-01 +------------------------ +- fixed bugs, including a critical input validation bug. + +Drupal 4.5.2, 2005-01-15 +------------------------ +- fixed bugs: a cross-site scripting (XSS) vulnerability has been fixed. + +Drupal 4.5.1, 2004-12-01 +------------------------ +- fixed bugs: no critical bugs were identified. + +Drupal 4.5.0, 2004-10-18 +------------------------ +- navigation: + * made it possible to add, delete, rename and move menu items. + * introduced tabs and subtabs for local tasks. + * reorganized the navigation menus. +- user management: + * added support for multiple roles per user. + * made it possible to add custom profile fields. + * made it possible to browse user profiles by field. +- node system: + * added support for node-level permissions. +- comment module: + * made it possible to leave contact information without having to register. +- upload module: + * added support for uploading documents (includes images). +- forum module: + * added support for sticky forum topics. + * made it possible to track forum topics. +- syndication: + * added support for RSS ping-notifications of http://technorati.com/. + * refactored the categorization of syndicated news items. + * added an URL alias for 'rss.xml'. + * improved date parsing. +- database backend: + * added support for multiple database connections. + * the PostgreSQL backend does no longer require PEAR. +- theme system: + * changed all GIFs to PNGs. + * reorganised the handling of themes, template engines, templates and styles. + * unified and extended the available theme settings. + * added theme screenshots. +- blocks: + * added 'recent comments' block. + * added 'categories' block. +- blogger API: + * added support for auto-discovery of blogger API via RSD. +- performance: + * added support for sending gzip compressed pages. + * improved performance of the forum module. +- accessibility: + * improved the accessibility of the archive module's calendar. + * improved form handling and error reporting. + * added HTTP redirects to prevent submitting twice when refreshing right after a form submission. +- refactored 403 (forbidden) handling and added support for custom 403 pages. +- documentation: + * added PHPDoc/Doxygen comments. +- filter system: + * added support for using multiple input formats on the site + * expanded the embedded PHP-code feature so it can be used everywhere + * added support for role-dependant filtering, through input formats +- UI translation: + * managing translations is now completely done through the administration interface + * added support for importing/exporting gettext .po files + +Drupal 4.4.3, 2005-06-01 +------------------------ +- fixed bugs, including a critical input validation bug. + +Drupal 4.4.2, 2004-07-04 +------------------------ +- fixed bugs: no critical bugs were identified. + +Drupal 4.4.1, 2004-05-01 +------------------------ +- fixed bugs: no critical bugs were identified. + +Drupal 4.4.0, 2004-04-01 +------------------------ +- added support for the MetaWeblog API and MovableType extensions. +- added a file API: enables better document management. +- improved the watchdog and search module to log search keys. +- news aggregator: + * added support for conditional GET. + * added OPML feed subscription list. + * added support for , , , , and . +- comment module: + * made it possible to disable the "comment viewing controls". +- performance: + * improved module loading when serving cached pages. + * made it possible to automatically disable modules when under heavy load. + * made it possible to automatically disable blocks when under heavy load. + * improved performance and memory footprint of the locale module. +- theme system: + * made all theme functions start with 'theme_'. + * made all theme functions return their output. + * migrated away from using the BaseTheme class. + * added many new theme functions and refactored existing theme functions. + * added avatar support to 'Xtemplate'. + * replaced theme 'UnConeD' by 'Chameleon'. + * replaced theme 'Marvin' by 'Pushbutton'. +- usability: + * added breadcrumb navigation to all pages. + * made it possible to add context-sensitive help to all pages. + * replaced drop-down menus by radio buttons where appropriate. + * removed the 'magic_quotes_gpc = 0' requirement. + * added a 'book navigation' block. +- accessibility: + * made themes degrade gracefully in absence of CSS. + * grouped form elements using '
' and '' tags. + * added '
\n"; + +} + +/** + * Format a radio button. + * + * @param $element + * An associative array containing the properties of the element. + * Properties used: required, return_value, value, attributes, title, description + * @return + * A themed HTML string representing the form item group. + */ +function theme_radio($element) { + _form_set_class($element, array('form-radio')); + $output = ''; + if (!is_null($element['#title'])) { + $output = ''; + } + return theme('form_element', NULL, $output, $element['#description'], $element['#id'], $element['#required'], form_get_error($element)); +} + +/** + * Format a set of radio buttons. + * + * @param $element + * An associative array containing the properties of the element. + * Properties used: title, value, options, description, required and attributes. + * @return + * A themed HTML string representing the radio button set. + */ +function theme_radios($element) { + if ($element['#title'] || $element['#description']) { + return theme('form_element', $element['#title'], $element['#children'], $element['#description'], NULL, $element['#required'], form_get_error($element)); + } + else { + return $element['#children']; + } +} + +/** + * Format a password_confirm item. + * + * @param $element + * An associative array containing the properties of the element. + * Properties used: title, value, id, required, error. + * @return + * A themed HTML string representing the form item. + */ +function theme_password_confirm($element) { + return theme('form_element', $element['#title'], '
'. $element['#children']. '
', $element['#description'], $element['#id'], $element['#required'], form_get_error($element)); +} + +/* + * Expand a password_confirm field into two text boxes. + */ +function expand_password_confirm($element) { + $element['pass1'] = array('#type' => 'password', '#size' => 12, '#value' => $element['#value']['pass1']); + $element['pass2'] = array('#type' => 'password', '#size' => 12, '#value' => $element['#value']['pass2']); + $element['#validate'] = array('password_confirm_validate' => array()); + $element['#tree'] = TRUE; + + return $element; +} + +/** + * Validate password_confirm element. + */ +function password_confirm_validate($form) { + $pass1 = trim($form['pass1']['#value']); + if (!empty($pass1)) { + $pass2 = trim($form['pass2']['#value']); + if ($pass1 != $pass2) { + form_error($form, t('The specified passwords do not match.')); + } + } + elseif ($form['#required'] && !empty($_POST['edit'])) { + form_error($form, t('Password field is required.')); + } + + // Password field must be converted from a two-element array into a single + // string regardless of validation results. + form_set_value($form['pass1'], NULL); + form_set_value($form['pass2'], NULL); + form_set_value($form, $pass1); + + return $form; +} + +/** + * Format a date selection element. + * + * @param $element + * An associative array containing the properties of the element. + * Properties used: title, value, options, description, required and attributes. + * @return + * A themed HTML string representing the date selection boxes. + */ +function theme_date($element) { + $output = '
' . $element['#children'] . '
'; + return theme('form_element', $element['#title'], $output, $element['#description'], $element['#id'], $element['#required'], form_get_error($element)); +} + +/** + * Roll out a single date element. + */ +function expand_date($element) { + // Default to current date + if (!isset($element['#value'])) { + $element['#value'] = array('day' => format_date(time(), 'custom', 'j'), + 'month' => format_date(time(), 'custom', 'n'), + 'year' => format_date(time(), 'custom', 'Y')); + } + + $element['#tree'] = TRUE; + + // Determine the order of day, month, year in the site's chosen date format. + $format = variable_get('date_format_short', 'm/d/Y'); + $sort = array(); + $sort['day'] = max(strpos($format, 'd'), strpos($format, 'j')); + $sort['month'] = max(strpos($format, 'm'), strpos($format, 'M')); + $sort['year'] = strpos($format, 'Y'); + asort($sort); + $order = array_keys($sort); + + // Output multi-selector for date + foreach ($order as $type) { + switch ($type) { + case 'day': + $options = drupal_map_assoc(range(1, 31)); + break; + case 'month': + $options = drupal_map_assoc(range(1, 12), 'map_month'); + break; + case 'year': + $options = drupal_map_assoc(range(1900, 2050)); + break; + } + $parents = $element['#parents']; + $parents[] = $type; + $element[$type] = array( + '#type' => 'select', + '#value' => $element['#value'][$type], + '#attributes' => $element['#attributes'], + '#options' => $options, + ); + } + + return $element; +} + +/** + * Validates the FAPI date type to stop dates like 30/Feb/2006 + */ +function date_validate($form) { + if (!checkdate($form['#value']['month'], $form['#value']['day'], $form['#value']['year'])) { + form_error($form, t('The specified date is invalid.')); + } +} + +/** + * Helper function for usage with drupal_map_assoc to display month names. + */ +function map_month($month) { + return format_date(gmmktime(0, 0, 0, $month, 2, 1970), 'custom', 'M', 0); +} + +/** + * Helper function to load value from default value for checkboxes + */ +function checkboxes_value(&$form) { + $value = array(); + foreach ((array)$form['#default_value'] as $key) { + $value[$key] = 1; + } + $form['#value'] = $value; +} + +/** + * If no default value is set for weight select boxes, use 0. + */ +function weight_value(&$form) { + if (isset($form['#default_value'])) { + $form['#value'] = $form['#default_value']; + } + else { + $form['#value'] = 0; + } +} + +/** + * Roll out a single radios element to a list of radios, + * using the options array as index. + */ +function expand_radios($element) { + if (count($element['#options']) > 0) { + foreach ($element['#options'] as $key => $choice) { + if (!isset($element[$key])) { + $element[$key] = array('#type' => 'radio', '#title' => $choice, '#return_value' => $key, '#default_value' => $element['#default_value'], '#attributes' => $element['#attributes'], '#parents' => $element['#parents'], '#spawned' => TRUE); + } + } + } + return $element; +} + +/** + * Format a form item. + * + * @param $element + * An associative array containing the properties of the element. + * Properties used: title, value, description, required, error + * @return + * A themed HTML string representing the form item. + */ +function theme_item($element) { + return theme('form_element', $element['#title'], $element['#value'] . $element['#children'], $element['#description'], $element['#id'], $element['#required'], $element['#error']); +} + +/** + * Format a checkbox. + * + * @param $element + * An associative array containing the properties of the element. + * Properties used: title, value, return_value, description, required + * @return + * A themed HTML string representing the checkbox. + */ +function theme_checkbox($element) { + _form_set_class($element, array('form-checkbox')); + $checkbox = ''; + + if (!is_null($element['#title'])) { + $checkbox = ''; + } + + return theme('form_element', NULL, $checkbox, $element['#description'], $element['#id'], $element['#required'], form_get_error($element)); +} + +/** + * Format a set of checkboxes. + * + * @param $element + * An associative array containing the properties of the element. + * @return + * A themed HTML string representing the checkbox set. + */ +function theme_checkboxes($element) { + if ($element['#title'] || $element['#description']) { + return theme('form_element', $element['#title'], $element['#children'], $element['#description'], NULL, $element['#required'], form_get_error($element)); + } + else { + return $element['#children']; + } +} + +function expand_checkboxes($element) { + $value = is_array($element['#value']) ? $element['#value'] : array(); + $element['#tree'] = TRUE; + if (count($element['#options']) > 0) { + if (!isset($element['#default_value']) || $element['#default_value'] == 0) { + $element['#default_value'] = array(); + } + foreach ($element['#options'] as $key => $choice) { + if (!isset($element[$key])) { + $element[$key] = array('#type' => 'checkbox', '#processed' => TRUE, '#title' => $choice, '#return_value' => $key, '#default_value' => isset($value[$key]), '#attributes' => $element['#attributes']); + } + } + } + return $element; +} + +function theme_submit($element) { + return theme('button', $element); +} + +function theme_button($element) { + //Make sure not to overwrite classes + if (isset($element['#attributes']['class'])) { + $element['#attributes']['class'] = 'form-'. $element['#button_type'] .' '. $element['#attributes']['class']; + } + else { + $element['#attributes']['class'] = 'form-'. $element['#button_type']; + } + + return '\n"; +} + +/** + * Format a hidden form field. + * + * @param $element + * An associative array containing the properties of the element. + * Properties used: value, edit + * @return + * A themed HTML string representing the hidden form field. + */ +function theme_hidden($element) { + return '\n"; +} + +/** + * Format a textfield. + * + * @param $element + * An associative array containing the properties of the element. + * Properties used: title, value, description, size, maxlength, required, attributes autocomplete_path + * @return + * A themed HTML string representing the textfield. + */ +function theme_textfield($element) { + $size = $element['#size'] ? ' size="' . $element['#size'] . '"' : ''; + $class = array('form-text'); + $extra = ''; + if ($element['#autocomplete_path']) { + drupal_add_js('misc/autocomplete.js'); + $class[] = 'form-autocomplete'; + $extra = ''; + } + _form_set_class($element, $class); + $output = ''; + return theme('form_element', $element['#title'], $output, $element['#description'], $element['#id'], $element['#required'], form_get_error($element)). $extra; +} + +/** + * Format a form. + * + * @param $element + * An associative array containing the properties of the element. + * Properties used: action, method, attributes, children + * @return + * A themed HTML string representing the form. + */ +function theme_form($element) { + // Anonymous div to satisfy XHTML compliance. + $action = $element['#action'] ? 'action="' . check_url($element['#action']) . '" ' : ''; + return '
\n
". $element['#children'] ."\n
\n"; +} + +/** + * Format a textarea. + * + * @param $element + * An associative array containing the properties of the element. + * Properties used: title, value, description, rows, cols, required, attributes + * @return + * A themed HTML string representing the textarea. + */ +function theme_textarea($element) { + $class = array('form-textarea'); + if ($element['#resizable'] !== false) { + drupal_add_js('misc/textarea.js'); + $class[] = 'resizable'; + } + + $cols = $element['#cols'] ? ' cols="'. $element['#cols'] .'"' : ''; + _form_set_class($element, $class); + return theme('form_element', $element['#title'], ''. check_plain($element['#value']) .'', $element['#description'], $element['#id'], $element['#required'], form_get_error($element)); +} + +/** + * Format HTML markup for use in forms. + * + * This is used in more advanced forms, such as theme selection and filter format. + * + * @param $element + * An associative array containing the properties of the element. + * Properties used: prefix, value, children and suffix. + * @return + * A themed HTML string representing the HTML markup. + */ + +function theme_markup($element) { + return $element['#value'] . $element['#children']; +} + +/** +* Format a password field. +* +* @param $element +* An associative array containing the properties of the element. +* Properties used: title, value, description, size, maxlength, required, attributes +* @return +* A themed HTML string representing the form. +*/ +function theme_password($element) { + $size = $element['#size'] ? ' size="'. $element['#size'] .'" ' : ''; + + _form_set_class($element, array('form-text')); + $output = ''; + + return theme('form_element', $element['#title'], $output, $element['#description'], $element['#id'], $element['#required'], form_get_error($element)); +} + +/** + * Format a weight selection menu. + * + * @param $element + * An associative array containing the properties of the element. + * Properties used: title, delta, description + * @return + * A themed HTML string representing the form. + */ +function theme_weight($element) { + for ($n = (-1 * $element['#delta']); $n <= $element['#delta']; $n++) { + $weights[$n] = $n; + } + $element['#options'] = $weights; + $element['#type'] = 'select'; + + return form_render($element); +} + +/** + * Format a file upload field. + * + * @param $title + * The label for the file upload field. + * @param $name + * The internal name used to refer to the field. + * @param $size + * A measure of the visible size of the field (passed directly to HTML). + * @param $description + * Explanatory text to display after the form item. + * @param $required + * Whether the user must upload a file to the field. + * @return + * A themed HTML string representing the field. + * + * For assistance with handling the uploaded file correctly, see the API + * provided by file.inc. + */ +function theme_file($element) { + _form_set_class($element, array('form-file')); + return theme('form_element', $element['#title'], '\n", $element['#description'], $element['#id'], $element['#required'], form_get_error($element)); +} + +/** + * Sets a form element's class attribute. + * + * Adds 'required' and 'error' classes as needed. + * + * @param &$element + * The form element + * @param $name + * Array of new class names to be added + */ +function _form_set_class(&$element, $class = array()) { + if ($element['#required']) { + $class[] = 'required'; + } + if (form_get_error($element)){ + $class[] = 'error'; + } + if (isset($element['#attributes']['class'])) { + $class[] = $element['#attributes']['class']; + } + $element['#attributes']['class'] = implode(' ', $class); +} + +/** + * Remove invalid characters from an HTML ID attribute string. + * + * @param $id + * The ID to clean + * @return + * The cleaned ID + */ +function form_clean_id($id = NULL) { + $id = str_replace('][', '-', $id); + return $id; +} + +/** + * @} End of "defgroup form". + */ diff --git a/includes/image.inc b/includes/image.inc new file mode 100644 index 0000000..cebb648 --- /dev/null +++ b/includes/image.inc @@ -0,0 +1,303 @@ + descriptive title. + */ +function image_get_available_toolkits() { + $toolkits = file_scan_directory('includes', 'image\..*\.inc$'); + + $output = array(); + foreach ($toolkits as $file => $toolkit) { + include_once "./$file"; + $function = str_replace('.', '_', $toolkit->name) .'_info'; + if (function_exists($function)) { + $info = $function(); + $output[$info['name']] = $info['title']; + } + } + $output['gd'] = t('Built-in GD2 toolkit'); + return $output; +} + +/** + * Retrieve the name of the currently used toolkit. + * + * @return String containing the name of the toolkit. + */ +function image_get_toolkit() { + static $toolkit; + if (!$toolkit) { + $toolkit = variable_get('image_toolkit', 'gd'); + $toolkit_file = './includes/image.'.$toolkit.'.inc'; + if ($toolkit != 'gd' && file_exists($toolkit_file)) { + include_once $toolkit_file; + } + elseif (!image_gd_check_settings()) { + $toolkit = false; + } + } + + return $toolkit; +} + +/** + * Invokes the given method using the currently selected toolkit. + * + * @param $method A string containing the method to invoke. + * @param $params An optional array of parameters to pass to the toolkit method. + * + * @return Mixed values (typically Boolean for successful operation). + */ +function image_toolkit_invoke($method, $params = array()) { + if ($toolkit = image_get_toolkit()) { + $function = 'image_'. $toolkit .'_'. $method; + if (function_exists($function)) { + return call_user_func_array($function, $params); + } + else { + watchdog('php', t("The selected image handling toolkit '%toolkit' can not correctly process '%function'.", array('%toolkit' => "$toolkit", '%function' => "$function")), WATCHDOG_ERROR); + return false; + } + } + else { + if ($method == 'settings') { + return image_gd_settings(); + } + } +} + + +/** + * Get details about an image. + * + * @return array containing information about the image + * 'width': image's width in pixels + * 'height': image's height in pixels + * 'extension': commonly used extension for the image + * 'mime_type': image's MIME type ('image/jpeg', 'image/gif', etc.) + * 'file_size': image's physical size (in bytes) + */ +function image_get_info($file) { + if (!is_file($file)) { + return false; + } + + $details = false; + $data = @getimagesize($file); + $file_size = @filesize($file); + + if (isset($data) && is_array($data)) { + $extensions = array('1' => 'gif', '2' => 'jpg', '3' => 'png'); + $extension = array_key_exists($data[2], $extensions) ? $extensions[$data[2]] : ''; + $details = array('width' => $data[0], + 'height' => $data[1], + 'extension' => $extension, + 'file_size' => $file_size, + 'mime_type' => $data['mime']); + } + + return $details; +} + +/** + * Scales an image to the given width and height while maintaining aspect + * ratio. + * + * @param $source The filepath of the source image + * @param $destination The file path of the destination image + * @param $width The target width + * @param $height The target height + * + * @return True or false, based on success + */ +function image_scale($source, $destination, $width, $height) { + $info = image_get_info($source); + + // don't scale up + if ($width > $info['width'] && $height > $info['height']) { + return false; + } + + $aspect = $info['height'] / $info['width']; + if ($aspect < $height / $width) { + $width = (int)min($width, $info['width']); + $height = (int)round($width * $aspect); + } + else { + $height = (int)min($height, $info['height']); + $width = (int)round($height / $aspect); + } + + return image_toolkit_invoke('resize', array($source, $destination, $width, $height)); +} + +/** + * Resize an image to the given dimensions (ignoring aspect ratio). + * + * @param $source The filepath of the source image. + * @param $destination The file path of the destination image. + * @param $width The target width. + * @param $height The target height. + */ +function image_resize($source, $destination, $width, $height) { + return image_toolkit_invoke('resize', array($source, $destination, $width, $height)); +} + +/** + * Rotate an image by the given number of degrees. + * + * @param $source The filepath of the source image + * @param $destination The file path of the destination image + * @param $degrees The number of (clockwise) degrees to rotate the image + */ +function image_rotate($source, $destination, $degrees) { + return image_toolkit_invoke('rotate', array($source, $destination, $degrees)); +} + +/** + * Crop an image to the rectangle specified by the given rectangle. + * + * @param $source The filepath of the source image + * @param $destination The file path of the destination image + * @param $x The top left co-ordinate of the crop area (x axis value) + * @param $y The top left co-ordinate of the crop area (y axis value) + * @param $width The target width + * @param $height The target height + */ +function image_crop($source, $destination, $x, $y, $width, $height) { + return image_toolkit_invoke('crop', array($source, $destination, $x, $y, $width, $height)); +} + +/** + * GD2 toolkit functions + * With the minimal requirements of PHP 4.3 for Drupal, we use the built-in version of GD. + */ + +/** + * Retrieve settings for the GD2 toolkit (not used). + */ +function image_gd_settings() { + if (image_gd_check_settings()) { + return t('The built-in GD2 toolkit is installed and working properly.'); + } + else { + form_set_error('image_toolkit', t("The built-in GD image toolkit requires that the GD module for PHP be installed and configured properly. For more information see %url.", array('%url' => 'http://php.net/image'))); + return false; + } +} + +/** + * Verify GD2 settings (that the right version is actually installed). + * + * @return boolean + */ +function image_gd_check_settings() { + if ($check = get_extension_funcs('gd')) { + if (in_array('imagegd2', $check)) { + // GD2 support is available. + return true; + } + } + return false; +} + +/** + * Scale an image to the specified size using GD. + */ +function image_gd_resize($source, $destination, $width, $height) { + if (!file_exists($source)) { + return false; + } + + $info = image_get_info($source); + if (!$info) { + return false; + } + + $im = image_gd_open($source, $info['extension']); + if (!$im) { + return false; + } + + $res = imageCreateTrueColor($width, $height); + imageCopyResampled($res, $im, 0, 0, 0, 0, $width, $height, $info['width'], $info['height']); + $result = image_gd_close($res, $destination, $info['extension']); + + imageDestroy($res); + imageDestroy($im); + + return $result; +} + +/** + * Rotate an image the given number of degrees. + */ +function image_gd_rotate($source, $destination, $degrees, $bg_color = 0) { + if (!function_exists('imageRotate')) { + return false; + } + + $info = image_get_info($source); + if (!$info) { + return false; + } + + $im = image_gd_open($source, $info['extension']); + if (!$im) { + return false; + } + + $res = imageRotate($im, $degrees, $bg_color); + $result = image_gd_close($res, $destination, $info['extension']); + + return $result; +} + +/** + * Crop an image using the GD toolkit. + */ +function image_gd_crop($source, $destination, $x, $y, $width, $height) { + $info = image_get_info($source); + if (!$info) { + return false; + } + + $im = image_gd_open($source, $info['extension']); + $res = imageCreateTrueColor($width, $height); + imageCopy($res, $im, 0, 0, $x, $y, $width, $height); + $result = image_gd_close($res, $destination, $info['extension']); + + imageDestroy($res); + imageDestroy($im); + + return $result; +} + +/** + * GD helper function to create an image resource from a file. + */ +function image_gd_open($file, $extension) { + $extension = str_replace('jpg', 'jpeg', $extension); + $open_func = 'imageCreateFrom'. $extension; + if (!function_exists($open_func)) { + return false; + } + return $open_func($file); +} + +/** + * GD helper to write an image resource to a destination file. + */ +function image_gd_close($res, $destination, $extension) { + $extension = str_replace('jpg', 'jpeg', $extension); + $close_func = 'image'. $extension; + if (!function_exists($close_func)) { + return false; + } + return $close_func($res, $destination); +} + + diff --git a/includes/install.inc b/includes/install.inc new file mode 100644 index 0000000..7a12c31 --- /dev/null +++ b/includes/install.inc @@ -0,0 +1,81 @@ +name] = $row->schema_version; + } + } + + return $versions[$module]; +} + +/** + * Update the installed version information for a module. + * + * @param $module + * A module name. + * @param $version + * The new schema version. + */ +function drupal_set_installed_schema_version($module, $version) { + db_query("UPDATE {system} SET schema_version = %d WHERE name = '%s'", $version, $module); +} diff --git a/includes/locale.inc b/includes/locale.inc new file mode 100644 index 0000000..5c6d83e --- /dev/null +++ b/includes/locale.inc @@ -0,0 +1,1548 @@ +lid, $code); + } + + // If only the language was added, and not a PO file import triggered + // the language addition, we need to inform the user on how to start + // a translation + if ($onlylanguage) { + drupal_set_message(t('The language %locale has been created, and can now be used to import a translation. More information is available in the help screen.', array('%locale' => theme('placeholder', t($name)), '%locale-help' => url('admin/help/locale')))); + } + else { + drupal_set_message(t('The language %locale has been created.', array('%locale' => theme('placeholder', t($name))))); + } + + watchdog('locale', t('The %language language (%locale) has been created.', array('%language' => theme('placeholder', $name), '%locale' => theme('placeholder', $code)))); +} + +/** + * User interface for the language management screen. + */ +function _locale_admin_manage_screen() { + $languages = locale_supported_languages(TRUE, TRUE); + + $options = array(); + $form['name'] = array('#tree' => TRUE); + foreach ($languages['name'] as $key => $lang) { + $options[$key] = ''; + $status = db_fetch_object(db_query("SELECT isdefault, enabled FROM {locales_meta} WHERE locale = '%s'", $key)); + if ($status->enabled) { + $enabled[] = $key; + } + if ($status->isdefault) { + $isdefault = $key; + } + if ($key == 'en') { + $form['name']['en'] = array('#value' => check_plain($lang)); + } + else { + $original = db_fetch_object(db_query("SELECT COUNT(*) AS strings FROM {locales_source}")); + $translation = db_fetch_object(db_query("SELECT COUNT(*) AS translation FROM {locales_target} WHERE locale = '%s' AND translation != ''", $key)); + + $ratio = ($original->strings > 0 && $translation->translation > 0) ? round(($translation->translation/$original->strings)*100., 2) : 0; + + $form['name'][$key] = array('#type' => 'textfield', + '#default_value' => $lang, + '#size' => 15, + '#maxlength' => 64, + ); + $form['translation'][$key] = array('#value' => "$translation->translation/$original->strings ($ratio%)"); + } + } + $form['enabled'] = array('#type' => 'checkboxes', + '#options' => $options, + '#default_value' => $enabled, + ); + $form['site_default'] = array('#type' => 'radios', + '#options' => $options, + '#default_value' => $isdefault, + ); + $form['submit'] = array('#type' => 'submit', '#value' => t('Save configuration')); + + return drupal_get_form('_locale_admin_manage_screen', $form, 'locale_admin_manage_screen'); +} + +/** + * Theme the locale admin manager form. + */ +function theme_locale_admin_manage_screen($form) { + foreach ($form['name'] as $key => $element) { + // Do not take form control structures. + if (is_array($element) && element_child($key)) { + $rows[] = array(check_plain($key), form_render($form['name'][$key]), form_render($form['enabled'][$key]), form_render($form['site_default'][$key]), ($key != 'en' ? form_render($form['translation'][$key]) : message_na()), ($key != 'en' ? l(t('delete'), 'admin/locale/language/delete/'. $key) : '')); + } + } + $header = array(array('data' => t('Code')), array('data' => t('English name')), array('data' => t('Enabled')), array('data' => t('Default')), array('data' => t('Translated')), array('data' => t('Operations'))); + $output = theme('table', $header, $rows); + $output .= form_render($form); + + return $output; +} + +/** + * Process locale admin manager form submissions. + */ +function _locale_admin_manage_screen_submit($form_id, $form_values) { + // Save changes to existing languages. + $languages = locale_supported_languages(FALSE, TRUE); + foreach($languages['name'] as $key => $value) { + if ($form_values['site_default'] == $key) { + $form_values['enabled'][$key] = 1; // autoenable the default language + } + $enabled = $form_values['enabled'][$key] ? 1 : 0; + if ($key == 'en') { + // Disallow name change for English locale. + db_query("UPDATE {locales_meta} SET isdefault = %d, enabled = %d WHERE locale = 'en'", ($form_values['site_default'] == $key), $enabled); + } + else { + db_query("UPDATE {locales_meta} SET name = '%s', isdefault = %d, enabled = %d WHERE locale = '%s'", $form_values['name'][$key], ($form_values['site_default'] == $key), $enabled, $key); + } + } + drupal_set_message(t('Configuration saved.')); + + // Changing the locale settings impacts the interface: + cache_clear_all(); + + return 'admin/locale/language/overview'; +} + +/** + * User interface for the language addition screen. + */ +function _locale_admin_manage_add_screen() { + $isocodes = _locale_prepare_iso_list(); + + $form = array(); + $form['language list'] = array('#type' => 'fieldset', + '#title' => t('Language list'), + '#collapsible' => TRUE, + ); + $form['language list']['langcode'] = array('#type' => 'select', + '#title' => t('Language name'), + '#default_value' => key($isocodes), + '#options' => $isocodes, + '#description' => t('Select your language here, or add it below, if you are unable to find it.'), + ); + $form['language list']['submit'] = array('#type' => 'submit', '#value' => t('Add language')); + + $output = drupal_get_form('locale_add_language_form', $form); + + $form = array(); + $form['custom language'] = array('#type' => 'fieldset', + '#title' => t('Custom language'), + '#collapsible' => TRUE, + ); + $form['custom language']['langcode'] = array('#type' => 'textfield', + '#title' => t('Language code'), + '#size' => 12, + '#maxlength' => 60, + '#required' => TRUE, + '#description' => t("Commonly this is an ISO 639 language code with an optional country code for regional variants. Examples include 'en', 'en-US' and 'zh-cn'.", array('%iso-codes' => 'http://www.w3.org/WAI/ER/IG/ert/iso639.htm')), + ); + $form['custom language']['langname'] = array('#type' => 'textfield', + '#title' => t('Language name in English'), + '#maxlength' => 64, + '#required' => TRUE, + '#description' => t('Name of the language. Will be available for translation in all languages.'), + ); + $form['custom language']['submit'] = array('#type' => 'submit', '#value' => t('Add custom language')); + + // Use the validation and submit functions of the add language form. + $output .= drupal_get_form('locale_custom_language_form', $form, 'locale_add_language_form'); + + return $output; +} + +/** + * Validate the language addition form. + */ +function locale_add_language_form_validate($form_id, $form_values) { + if ($duplicate = db_num_rows(db_query("SELECT locale FROM {locales_meta} WHERE locale = '%s'", $form_values['langcode'])) != 0) { + form_set_error(t('The language %language (%code) already exists.', array('%language' => theme('placeholder', check_plain($form_values['langname'])), '%code' => theme('placeholder', $form_values['langcode'])))); + } + + if (!isset($form_values['langname'])) { + $isocodes = _locale_get_iso639_list(); + if (!isset($isocodes[$form_values['langcode']])) { + form_set_error('langcode', t('Invalid language code.')); + } + } +} + +/** + * Process the language addition form submission. + */ +function locale_add_language_form_submit($form_id, $form_values) { + if (isset($form_values['langname'])) { + // Custom language form. + _locale_add_language($form_values['langcode'], $form_values['langname']); + } + else { + $isocodes = _locale_get_iso639_list(); + _locale_add_language($form_values['langcode'], $isocodes[$form_values['langcode']][0]); + } + + return 'admin/locale'; +} + +/** + * User interface for the translation import screen. + */ +function _locale_admin_import_screen() { + $languages = locale_supported_languages(FALSE, TRUE); + $languages = array_map('t', $languages['name']); + unset($languages['en']); + + if (!count($languages)) { + $languages = _locale_prepare_iso_list(); + } + else { + $languages = array( + t('Already added languages') => $languages, + t('Languages not yet added') => _locale_prepare_iso_list() + ); + } + + $form = array(); + $form['import'] = array('#type' => 'fieldset', + '#title' => t('Import translation'), + ); + $form['import']['file'] = array('#type' => 'file', + '#title' => t('Language file'), + '#size' => 50, + '#description' => t('A gettext Portable Object (.po) file.'), + ); + $form['import']['langcode'] = array('#type' => 'select', + '#title' => t('Import into'), + '#options' => $languages, + '#description' => t('Choose the language you want to add strings into. If you choose a language which is not yet set up, then it will be added.'), + ); + $form['import']['mode'] = array('#type' => 'radios', + '#title' => t('Mode'), + '#default_value' => 'overwrite', + '#options' => array('overwrite' => t('Strings in the uploaded file replace existing ones, new ones are added'), 'keep' => t('Existing strings are kept, only new strings are added')), + ); + $form['import']['submit'] = array('#type' => 'submit', '#value' => t('Import')); + $form['#attributes']['enctype'] = 'multipart/form-data'; + + return drupal_get_form('_locale_admin_import', $form); +} + +/** + * Process the locale import form submission. + */ +function _locale_admin_import_submit($form_id, $form_values) { + // Add language, if not yet supported + $languages = locale_supported_languages(TRUE, TRUE); + if (!isset($languages['name'][$form_values['langcode']])) { + $isocodes = _locale_get_iso639_list(); + _locale_add_language($form_values['langcode'], $isocodes[$form_values['langcode']][0], FALSE); + } + + // Now import strings into the language + $file = file_check_upload('file'); + if ($ret = _locale_import_po($file, $form_values['langcode'], $form_values['mode']) == FALSE) { + $message = t('The translation import of %filename failed.', array('%filename' => theme('placeholder', $file->filename))); + drupal_set_message($message, 'error'); + watchdog('locale', $message, WATCHDOG_ERROR); + } + + return 'admin/locale'; +} + +/** + * User interface for the translation export screen + */ +function _locale_admin_export_screen() { + $languages = locale_supported_languages(FALSE, TRUE); + $languages = array_map('t', $languages['name']); + unset($languages['en']); + + // Offer language specific export if any language is set up + if (count($languages)) { + $form = array(); + $form['export'] = array('#type' => 'fieldset', + '#title' => t('Export translation'), + '#collapsible' => TRUE, + ); + $form['export']['langcode'] = array('#type' => 'select', + '#title' => t('Language name'), + '#options' => $languages, + '#description' => t('Select the language you would like to export in gettext Portable Object (.po) format.'), + ); + $form['export']['submit'] = array('#type' => 'submit', '#value' => t('Export')); + $output = drupal_get_form('_locale_export_po', $form); + } + + // Complete template export of the strings + $form = array(); + $form['export'] = array('#type' => 'fieldset', + '#title' => t('Export template'), + '#collapsible' => TRUE, + '#description' => t('Generate a gettext Portable Object Template (.pot) file with all the interface strings from the Drupal locale database.'), + ); + $form['export']['submit'] = array('#type' => 'submit', '#value' => t('Export')); + $output .= drupal_get_form('_locale_export_pot', $form, '_locale_export_po'); + + return $output; +} + +/** + * Process a locale export form submissions. + */ +function _locale_export_po_submit($form_id, $form_values) { + _locale_export_po($form_values['langcode']); +} + +/** + * User interface for the string search screen + */ +function _locale_string_seek_form() { + // Get *all* languages set up + $languages = locale_supported_languages(FALSE, TRUE); + asort($languages['name']); unset($languages['name']['en']); + $languages['name'] = array_map('check_plain', $languages['name']); + + // Present edit form preserving previous user settings + $query = _locale_string_seek_query(); + $form = array(); + $form['search'] = array('#type' => 'fieldset', + '#title' => t('Search'), + ); + $form['search']['string'] = array('#type' => 'textfield', + '#title' => t('Strings to search for'), + '#default_value' => $query->string, + '#size' => 30, + '#maxlength' => 30, + '#description' => t('Leave blank to show all strings. The search is case sensitive.'), + ); + $form['search']['language'] = array('#type' => 'radios', + '#title' => t('Language'), + '#default_value' => ($query->language ? $query->language : 'all'), + '#options' => array_merge(array('all' => t('All languages'), 'en' => t('English (provided by Drupal)')), $languages['name']), + ); + $form['search']['searchin'] = array('#type' => 'radios', + '#title' => t('Search in'), + '#default_value' => ($query->searchin ? $query->searchin : 'all'), + '#options' => array('all' => t('All strings in that language'), 'translated' => t('Only translated strings'), 'untranslated' => t('Only untranslated strings')), + ); + $form['search']['submit'] = array('#type' => 'submit', '#value' => t('Search')); + $form['#redirect'] = FALSE; + + return drupal_get_form('_locale_string_seek', $form); +} + +/** + * User interface for string editing. + */ +function _locale_string_edit($lid) { + $languages = locale_supported_languages(FALSE, TRUE); + unset($languages['name']['en']); + + $result = db_query('SELECT DISTINCT s.source, t.translation, t.locale FROM {locales_source} s INNER JOIN {locales_target} t ON s.lid = t.lid WHERE s.lid = %d', $lid); + $form = array(); + $form['translations'] = array('#tree' => TRUE); + while ($translation = db_fetch_object($result)) { + $orig = $translation->source; + + // Approximate the number of rows in a textfield with a maximum of 10. + $rows = min(ceil(str_word_count($orig) / 12), 10); + + $form['translations'][$translation->locale] = array( + '#type' => 'textarea', + '#title' => $languages['name'][$translation->locale], + '#default_value' => $translation->translation, + '#rows' => $rows, + ); + unset($languages['name'][$translation->locale]); + } + + // Handle erroneous lid. + if (!isset($orig)){ + drupal_set_message(t('String not found.')); + drupal_goto('admin/locale/string/search'); + } + + // Add original text. Assign negative weight so that it floats to the top. + $form['item'] = array('#type' => 'item', + '#title' => t('Original text'), + '#value' => check_plain(wordwrap($orig, 0)), + '#weight' => -1, + ); + + foreach ($languages['name'] as $key => $lang) { + $form['translations'][$key] = array( + '#type' => 'textarea', + '#title' => $lang, + '#rows' => $rows, + ); + } + + $form['lid'] = array('#type' => 'value', '#value' => $lid); + $form['submit'] = array('#type' => 'submit', '#value' => t('Save translations')); + + return drupal_get_form('_locale_string_edit', $form); +} + +/** + * Process string editing form submissions. + * Saves all translations of one string submitted from a form. + */ +function _locale_string_edit_submit($form_id, $form_values) { + $lid = $form_values['lid']; + foreach ($form_values['translations'] as $key => $value) { + $value = filter_xss_admin($value); + $trans = db_fetch_object(db_query("SELECT translation FROM {locales_target} WHERE lid = %d AND locale = '%s'", $lid, $key)); + if (isset($trans->translation)) { + db_query("UPDATE {locales_target} SET translation = '%s' WHERE lid = %d AND locale = '%s'", $value, $lid, $key); + } + else { + db_query("INSERT INTO {locales_target} (lid, translation, locale) VALUES (%d, '%s', '%s')", $lid, $value, $key); + } + } + drupal_set_message(t('The string has been saved.')); + + // Refresh the locale cache. + locale_refresh_cache(); + // Rebuild the menu, strings may have changed. + menu_rebuild(); + + return 'admin/locale/string/search'; +} + +/** + * Delete a language string. + */ +function _locale_string_delete($lid) { + db_query('DELETE FROM {locales_source} WHERE lid = %d', $lid); + db_query('DELETE FROM {locales_target} WHERE lid = %d', $lid); + locale_refresh_cache(); + drupal_set_message(t('The string has been removed.')); + + drupal_goto('admin/locale/string/search'); +} + +/** + * Parses Gettext Portable Object file information and inserts into database + * + * @param $file Object contains properties of local file to be imported + * @param $lang Language code + * @param $mode should existing translations be replaced? + */ +function _locale_import_po($file, $lang, $mode) { + // If not in 'safe mode', increase the maximum execution time: + if (!ini_get('safe_mode')) { + set_time_limit(240); + } + + // Check if we have the language already in the database + if (!db_fetch_object(db_query("SELECT locale FROM {locales_meta} WHERE locale = '%s'", $lang))) { + drupal_set_message(t('The language selected for import is not supported.'), 'error'); + return FALSE; + } + + // Get strings from file (returns on failure after a partial import, or on success) + $status = _locale_import_read_po($file, $mode, $lang); + if ($status === FALSE) { + // error messages are set in _locale_import_read_po + return FALSE; + } + + // Get status information on import process + list($headerdone, $additions, $updates) = _locale_import_one_string('report', $mode); + + if (!$headerdone) { + drupal_set_message(t('The translation file %filename appears to have a missing or malformed header.', array('%filename' => theme('placeholder', $file->filename))), 'error'); + } + + // rebuild locale cache + cache_clear_all("locale:$lang"); + + // rebuild the menu, strings may have changed + menu_rebuild(); + + drupal_set_message(t('The translation was successfully imported. There are %number newly created translated strings and %update strings were updated.', array('%number' => $additions, '%update' => $updates))); + watchdog('locale', t('Imported %file into %locale: %number new strings added and %update updated.', array('%file' => theme('placeholder', $file->filename), '%locale' => theme('placeholder', $lang), '%number' => $additions, '%update' => $updates))); + return TRUE; +} + +/** + * Parses Gettext Portable Object file into an array + * + * @param $file Object with properties of local file to parse + * @author Jacobo Tarrio + */ +function _locale_import_read_po($file, $mode, $lang) { + + $message = theme('placeholder', $file->filename); + $fd = fopen($file->filepath, "rb"); // File will get closed by PHP on return + if (!$fd) { + drupal_set_message(t('The translation import failed, because the file %filename could not be read.', array('%filename' => $message)), 'error'); + return FALSE; + } + + $context = "COMMENT"; // Parser context: COMMENT, MSGID, MSGID_PLURAL, MSGSTR and MSGSTR_ARR + $current = array(); // Current entry being read + $plural = 0; // Current plural form + $lineno = 0; // Current line + + while (!feof($fd)) { + $line = fgets($fd, 10*1024); // A line should not be this long + $lineno++; + $line = trim(strtr($line, array("\\\n" => ""))); + + if (!strncmp("#", $line, 1)) { // A comment + if ($context == "COMMENT") { // Already in comment context: add + $current["#"][] = substr($line, 1); + } + elseif (($context == "MSGSTR") || ($context == "MSGSTR_ARR")) { // End current entry, start a new one + _locale_import_one_string($current, $mode, $lang); + $current = array(); + $current["#"][] = substr($line, 1); + $context = "COMMENT"; + } + else { // Parse error + drupal_set_message(t('The translation file %filename contains an error: "msgstr" was expected but not found on line %line.', array('%filename' => $message, '%line' => $lineno)), 'error'); + return FALSE; + } + } + elseif (!strncmp("msgid_plural", $line, 12)) { + if ($context != "MSGID") { // Must be plural form for current entry + drupal_set_message(t('The translation file %filename contains an error: "msgid_plural" was expected but not found on line %line.', array('%filename' => $message, '%line' => $lineno)), 'error'); + return FALSE; + } + $line = trim(substr($line, 12)); + $quoted = _locale_import_parse_quoted($line); + if ($quoted === false) { + drupal_set_message(t('The translation file %filename contains a syntax error on line %line.', array('%filename' => $message, '%line' => $lineno)), 'error'); + return FALSE; + } + $current["msgid"] = $current["msgid"] ."\0". $quoted; + $context = "MSGID_PLURAL"; + } + elseif (!strncmp("msgid", $line, 5)) { + if ($context == "MSGSTR") { // End current entry, start a new one + _locale_import_one_string($current, $mode, $lang); + $current = array(); + } + elseif ($context == "MSGID") { // Already in this context? Parse error + drupal_set_message(t('The translation file %filename contains an error: "msgid" is unexpected on line %line.', array('%filename' => $message, '%line' => $lineno)), 'error'); + return FALSE; + } + $line = trim(substr($line, 5)); + $quoted = _locale_import_parse_quoted($line); + if ($quoted === false) { + drupal_set_message(t('The translation file %filename contains a syntax error on line %line.', array('%filename' => $message, '%line' => $lineno)), 'error'); + return FALSE; + } + $current["msgid"] = $quoted; + $context = "MSGID"; + } + elseif (!strncmp("msgstr[", $line, 7)) { + if (($context != "MSGID") && ($context != "MSGID_PLURAL") && ($context != "MSGSTR_ARR")) { // Must come after msgid, msgid_plural, or msgstr[] + drupal_set_message(t('The translation file %filename contains an error: "msgstr[]" is unexpected on line %line.', array('%filename' => $message, '%line' => $lineno)), 'error'); + return FALSE; + } + if (strpos($line, "]") === false) { + drupal_set_message(t('The translation file %filename contains a syntax error on line %line.', array('%filename' => $message, '%line' => $lineno)), 'error'); + return FALSE; + } + $frombracket = strstr($line, "["); + $plural = substr($frombracket, 1, strpos($frombracket, "]") - 1); + $line = trim(strstr($line, " ")); + $quoted = _locale_import_parse_quoted($line); + if ($quoted === false) { + drupal_set_message(t('The translation file %filename contains a syntax error on line %line.', array('%filename' => $message, '%line' => $lineno)), 'error'); + return FALSE; + } + $current["msgstr"][$plural] = $quoted; + $context = "MSGSTR_ARR"; + } + elseif (!strncmp("msgstr", $line, 6)) { + if ($context != "MSGID") { // Should come just after a msgid block + drupal_set_message(t('The translation file %filename contains an error: "msgstr" is unexpected on line %line.', array('%filename' => $message, '%line' => $lineno)), 'error'); + return FALSE; + } + $line = trim(substr($line, 6)); + $quoted = _locale_import_parse_quoted($line); + if ($quoted === false) { + drupal_set_message(t('The translation file %filename contains a syntax error on line %line.', array('%filename' => $message, '%line' => $lineno)), 'error'); + return FALSE; + } + $current["msgstr"] = $quoted; + $context = "MSGSTR"; + } + elseif ($line != "") { + $quoted = _locale_import_parse_quoted($line); + if ($quoted === false) { + drupal_set_message(t('The translation file %filename contains a syntax error on line %line.', array('%filename' => $message, '%line' => $lineno)), 'error'); + return FALSE; + } + if (($context == "MSGID") || ($context == "MSGID_PLURAL")) { + $current["msgid"] .= $quoted; + } + elseif ($context == "MSGSTR") { + $current["msgstr"] .= $quoted; + } + elseif ($context == "MSGSTR_ARR") { + $current["msgstr"][$plural] .= $quoted; + } + else { + drupal_set_message(t('The translation file %filename contains an error: there is an unexpected string on line %line.', array('%filename' => $message, '%line' => $lineno)), 'error'); + return FALSE; + } + } + } + + // End of PO file, flush last entry + if (($context == "MSGSTR") || ($context == "MSGSTR_ARR")) { + _locale_import_one_string($current, $mode, $lang); + } + elseif ($context != "COMMENT") { + drupal_set_message(t('The translation file %filename ended unexpectedly at line %line.', array('%filename' => $message, '%line' => $lineno)), 'error'); + return FALSE; + } + +} + +/** + * Imports a string into the database + * + * @param $value Information about the string + * @author Jacobo Tarrio + */ +function _locale_import_one_string($value, $mode, $lang = NULL) { + static $additions = 0; + static $updates = 0; + static $headerdone = FALSE; + + // Report the changes made (called at end of import) + if ($value == 'report') { + return array($headerdone, $additions, $updates); + } + // Current string is the header information + elseif ($value['msgid'] == '') { + $hdr = _locale_import_parse_header($value['msgstr']); + + // Get the plural formula + if ($hdr["Plural-Forms"] && $p = _locale_import_parse_plural_forms($hdr["Plural-Forms"], $file->filename)) { + list($nplurals, $plural) = $p; + db_query("UPDATE {locales_meta} SET plurals = %d, formula = '%s' WHERE locale = '%s'", $nplurals, $plural, $lang); + } + else { + db_query("UPDATE {locales_meta} SET plurals = %d, formula = '%s' WHERE locale = '%s'", 0, '', $lang); + } + $headerdone = TRUE; + } + // Some real string to import + else { + $comments = filter_xss_admin(_locale_import_shorten_comments($value['#'])); + + // Handle a translation for some plural string + if (strpos($value['msgid'], "\0")) { + $english = explode("\0", $value['msgid'], 2); + $entries = array_keys($value['msgstr']); + for ($i = 3; $i <= count($entries); $i++) { + $english[] = $english[1]; + } + $translation = array_map('_locale_import_append_plural', $value['msgstr'], $entries); + $english = array_map('_locale_import_append_plural', $english, $entries); + foreach ($translation as $key => $trans) { + if ($key == 0) { + $plid = 0; + } + $loc = db_fetch_object(db_query("SELECT lid FROM {locales_source} WHERE source = '%s'", $english[$key])); + if ($loc->lid) { // a string exists + $lid = $loc->lid; + // update location field + db_query("UPDATE {locales_source} SET location = '%s' WHERE lid = %d", $comments, $lid); + $trans2 = db_fetch_object(db_query("SELECT lid, translation, plid, plural FROM {locales_target} WHERE lid = %d AND locale = '%s'", $lid, $lang)); + if (!$trans2->lid) { // no translation in current language + db_query("INSERT INTO {locales_target} (lid, locale, translation, plid, plural) VALUES (%d, '%s', '%s', %d, %d)", $lid, $lang, filter_xss_admin($trans), $plid, $key); + $additions++; + } // translation exists + else if ($mode == 'overwrite' || $trans2->translation == '') { + db_query("UPDATE {locales_target} SET translation = '%s', plid = %d, plural = %d WHERE locale = '%s' AND lid = %d", filter_xss_admin($trans), $plid, $key, $lang, $lid); + if ($trans2->translation == '') { + $additions++; + } + else { + $updates++; + } + } + } + else { // no string + db_query("INSERT INTO {locales_source} (location, source) VALUES ('%s', '%s')", $comments, filter_xss_admin($english[$key])); + $loc = db_fetch_object(db_query("SELECT lid FROM {locales_source} WHERE source = '%s'", $english[$key])); + $lid = $loc->lid; + db_query("INSERT INTO {locales_target} (lid, locale, translation, plid, plural) VALUES (%d, '%s', '%s', %d, %d)", $lid, $lang, filter_xss_admin($trans), $plid, $key); + if ($trans != '') { + $additions++; + } + } + $plid = $lid; + } + } + + // A simple translation + else { + $english = $value['msgid']; + $translation = $value['msgstr']; + $loc = db_fetch_object(db_query("SELECT lid FROM {locales_source} WHERE source = '%s'", $english)); + if ($loc->lid) { // a string exists + $lid = $loc->lid; + // update location field + db_query("UPDATE {locales_source} SET location = '%s' WHERE source = '%s'", $comments, $english); + $trans = db_fetch_object(db_query("SELECT lid, translation FROM {locales_target} WHERE lid = %d AND locale = '%s'", $lid, $lang)); + if (!$trans->lid) { // no translation in current language + db_query("INSERT INTO {locales_target} (lid, locale, translation) VALUES (%d, '%s', '%s')", $lid, $lang, filter_xss_admin($translation)); + $additions++; + } // translation exists + else if ($mode == 'overwrite') { //overwrite in any case + db_query("UPDATE {locales_target} SET translation = '%s' WHERE locale = '%s' AND lid = %d", filter_xss_admin($translation), $lang, $lid); + if ($trans->translation == '') { + $additions++; + } + else { + $updates++; + } + } // overwrite if empty string + else if ($trans->translation == '') { + db_query("UPDATE {locales_target} SET translation = '%s' WHERE locale = '%s' AND lid = %d", $translation, $lang, $lid); + $additions++; + } + } + else { // no string + db_query("INSERT INTO {locales_source} (location, source) VALUES ('%s', '%s')", $comments, $english); + $loc = db_fetch_object(db_query("SELECT lid FROM {locales_source} WHERE source = '%s'", $english)); + $lid = $loc->lid; + db_query("INSERT INTO {locales_target} (lid, locale, translation) VALUES (%d, '%s', '%s')", $lid, $lang, filter_xss_admin($translation)); + if ($translation != '') { + $additions++; + } + } + } + } +} + +/** + * Parses a Gettext Portable Object file header + * + * @param $header A string containing the complete header + * @return An associative array of key-value pairs + * @author Jacobo Tarrio + */ +function _locale_import_parse_header($header) { + $hdr = array(); + + $lines = explode("\n", $header); + foreach ($lines as $line) { + $line = trim($line); + if ($line) { + list($tag, $contents) = explode(":", $line, 2); + $hdr[trim($tag)] = trim($contents); + } + } + + return $hdr; +} + +/** + * Parses a Plural-Forms entry from a Gettext Portable Object file header + * + * @param $pluralforms A string containing the Plural-Forms entry + * @param $filename A string containing the filename + * @return An array containing the number of plurals and a + * formula in PHP for computing the plural form + * @author Jacobo Tarrio + */ +function _locale_import_parse_plural_forms($pluralforms, $filename) { + // First, delete all whitespace + $pluralforms = strtr($pluralforms, array(" " => "", "\t" => "")); + + // Select the parts that define nplurals and plural + $nplurals = strstr($pluralforms, "nplurals="); + if (strpos($nplurals, ";")) { + $nplurals = substr($nplurals, 9, strpos($nplurals, ";") - 9); + } + else { + return FALSE; + } + $plural = strstr($pluralforms, "plural="); + if (strpos($plural, ";")) { + $plural = substr($plural, 7, strpos($plural, ";") - 7); + } + else { + return FALSE; + } + + // Get PHP version of the plural formula + $plural = _locale_import_parse_arithmetic($plural); + + if ($plural !== FALSE) { + return array($nplurals, $plural); + } + else { + drupal_set_message(t('The translation file %filename contains an error: the plural formula could not be parsed.', array('%filename' => theme('placeholder', $filename))), 'error'); + return FALSE; + } +} + +/** + * Parses and sanitizes an arithmetic formula into a PHP expression + * + * While parsing, we ensure, that the operators have the right + * precedence and associativity. + * + * @param $string A string containing the arithmetic formula + * @return The PHP version of the formula + * @author Jacobo Tarrio + */ +function _locale_import_parse_arithmetic($string) { + // Operator precedence table + $prec = array("(" => -1, ")" => -1, "?" => 1, ":" => 1, "||" => 3, "&&" => 4, "==" => 5, "!=" => 5, "<" => 6, ">" => 6, "<=" => 6, ">=" => 6, "+" => 7, "-" => 7, "*" => 8, "/" => 8, "%" => 8); + // Right associativity + $rasc = array("?" => 1, ":" => 1); + + $tokens = _locale_import_tokenize_formula($string); + + // Parse by converting into infix notation then back into postfix + $opstk = array(); + $elstk = array(); + + foreach ($tokens as $token) { + $ctok = $token; + + // Numbers and the $n variable are simply pushed into $elarr + if (is_numeric($token)) { + $elstk[] = $ctok; + } + elseif ($ctok == "n") { + $elstk[] = '$n'; + } + elseif ($ctok == "(") { + $opstk[] = $ctok; + } + elseif ($ctok == ")") { + $topop = array_pop($opstk); + while (($topop != NULL) && ($topop != "(")) { + $elstk[] = $topop; + $topop = array_pop($opstk); + } + } + elseif ($prec[$ctok]) { + // If it's an operator, then pop from $oparr into $elarr until the + // precedence in $oparr is less than current, then push into $oparr + $topop = array_pop($opstk); + while (($topop != NULL) && ($prec[$topop] >= $prec[$ctok]) && !(($prec[$topop] == $prec[$ctok]) && $rasc[$topop] && $rasc[$ctok])) { + $elstk[] = $topop; + $topop = array_pop($opstk); + } + if ($topop) { + $opstk[] = $topop; // Return element to top + } + $opstk[] = $ctok; // Parentheses are not needed + } + else { + return false; + } + } + + // Flush operator stack + $topop = array_pop($opstk); + while ($topop != NULL) { + $elstk[] = $topop; + $topop = array_pop($opstk); + } + + // Now extract formula from stack + $prevsize = count($elstk) + 1; + while (count($elstk) < $prevsize) { + $prevsize = count($elstk); + for ($i = 2; $i < count($elstk); $i++) { + $op = $elstk[$i]; + if ($prec[$op]) { + $f = ""; + if ($op == ":") { + $f = $elstk[$i - 2] ."):". $elstk[$i - 1] .")"; + } + elseif ($op == "?") { + $f = "(". $elstk[$i - 2] ."?(". $elstk[$i - 1]; + } + else { + $f = "(". $elstk[$i - 2] . $op . $elstk[$i - 1] .")"; + } + array_splice($elstk, $i - 2, 3, $f); + break; + } + } + } + + // If only one element is left, the number of operators is appropriate + if (count($elstk) == 1) { + return $elstk[0]; + } + else { + return FALSE; + } +} + +/** + * Backward compatible implementation of token_get_all() for formula parsing + * + * @param $string A string containing the arithmetic formula + * @return The PHP version of the formula + * @author Gerhard Killesreiter + */ +function _locale_import_tokenize_formula($formula) { + $formula = str_replace(" ", "", $formula); + $tokens = array(); + for ($i = 0; $i < strlen($formula); $i++) { + if (is_numeric($formula[$i])) { + $num = $formula[$i]; + $j = $i + 1; + while($j < strlen($formula) && is_numeric($formula[$j])) { + $num .= $formula[$j]; + $j++; + } + $i = $j - 1; + $tokens[] = $num; + } + elseif ($pos = strpos(" =<>!&|", $formula[$i])) { // We won't have a space + $next = $formula[$i + 1]; + switch ($pos) { + case 1: + case 2: + case 3: + case 4: + if ($next == '=') { + $tokens[] = $formula[$i] .'='; + $i++; + } + else { + $tokens[] = $formula[$i]; + } + break; + case 5: + if ($next == '&') { + $tokens[] = '&&'; + $i++; + } + else { + $tokens[] = $formula[$i]; + } + break; + case 6: + if ($next == '|') { + $tokens[] = '||'; + $i++; + } + else { + $tokens[] = $formula[$i]; + } + break; + } + } + else { + $tokens[] = $formula[$i]; + } + } + return $tokens; +} + +/** + * Modify a string to contain proper count indices + * + * This is a callback function used via array_map() + * + * @param $entry An array element + * @param $key Index of the array element + */ +function _locale_import_append_plural($entry, $key) { + // No modifications for 0, 1 + if ($key == 0 || $key == 1) { + return $entry; + } + + // First remove any possibly false indices, then add new ones + $entry = preg_replace('/(%count)\[[0-9]\]/', '\\1', $entry); + return preg_replace('/(%count)/', "\\1[$key]", $entry); +} + +/** + * Generate a short, one string version of the passed comment array + * + * @param $comment An array of strings containing a comment + * @return Short one string version of the comment + */ +function _locale_import_shorten_comments($comment) { + $comm = ''; + while (count($comment)) { + $test = $comm . substr(array_shift($comment), 1) .', '; + if (strlen($comm) < 130) { + $comm = $test; + } + else { + break; + } + } + return substr($comm, 0, -2); +} + +/** + * Parses a string in quotes + * + * @param $string A string specified with enclosing quotes + * @return The string parsed from inside the quotes + */ +function _locale_import_parse_quoted($string) { + if (substr($string, 0, 1) != substr($string, -1, 1)) { + return FALSE; // Start and end quotes must be the same + } + $quote = substr($string, 0, 1); + $string = substr($string, 1, -1); + if ($quote == '"') { // Double quotes: strip slashes + return stripcslashes($string); + } + elseif ($quote == "'") { // Simple quote: return as-is + return $string; + } + else { + return FALSE; // Unrecognized quote + } +} + +/** + * Exports a Portable Object (Template) file for a language + * + * @param $language Selects a language to generate the output for + */ +function _locale_export_po($language) { + global $user; + + // Get language specific strings, or all strings + if ($language) { + $meta = db_fetch_object(db_query("SELECT * FROM {locales_meta} WHERE locale = '%s'", $language)); + $result = db_query("SELECT s.lid, s.source, s.location, t.translation, t.plid, t.plural FROM {locales_source} s INNER JOIN {locales_target} t ON s.lid = t.lid WHERE t.locale = '%s' ORDER BY t.plid, t.plural", $language); + } + else { + $result = db_query("SELECT s.lid, s.source, s.location, t.plid, t.plural FROM {locales_source} s INNER JOIN {locales_target} t ON s.lid = t.lid ORDER BY t.plid, t.plural"); + } + + // Build array out of the database results + $parent = array(); + while ($child = db_fetch_object($result)) { + if ($child->source != '') { + $parent[$child->lid]['comment'] = $child->location; + $parent[$child->lid]['msgid'] = $child->source; + if ($child->plid) { + $parent[$child->lid][$child->plid]['plural'] = $child->lid; + $parent[$child->lid][$child->plid]['translation'] = $child->translation; + $parent[$child->lid][$child->plid]['msgid'] = $child->source; + } + else { + $parent[$child->lid]['translation'] = $child->translation; + } + } + } + + // Generating Portable Object file for a language + if ($language) { + $filename = $language .'.po'; + $header .= "# $meta->name translation of ". variable_get('site_name', 'Drupal') ."\n"; + $header .= '# Copyright (c) '. date('Y') .' '. $user->name .' <'. $user->mail .">\n"; + $header .= "#\n"; + $header .= "msgid \"\"\n"; + $header .= "msgstr \"\"\n"; + $header .= "\"Project-Id-Version: PROJECT VERSION\\n\"\n"; + $header .= "\"POT-Creation-Date: ". date("Y-m-d H:iO") ."\\n\"\n"; + $header .= "\"PO-Revision-Date: ". date("Y-m-d H:iO") ."\\n\"\n"; + $header .= "\"Last-Translator: ". $user->name .' <'. $user->mail .">\\n\"\n"; + $header .= "\"Language-Team: ". $meta->name .' <'. $user->mail .">\\n\"\n"; + $header .= "\"MIME-Version: 1.0\\n\"\n"; + $header .= "\"Content-Type: text/plain; charset=utf-8\\n\"\n"; + $header .= "\"Content-Transfer-Encoding: 8bit\\n\"\n"; + if ($meta->formula && $meta->plurals) { + $header .= "\"Plural-Forms: nplurals=". $meta->plurals ."; plural=". strtr($meta->formula, '$', '') .";\\n\"\n"; + } + $header .= "\n"; + watchdog('locale', t('Exported %locale translation file: %filename.', array('%locale' => theme('placeholder', $meta->name), '%filename' => theme('placeholder', $filename)))); + } + + // Generating Portable Object Template + else { + $filename = 'drupal.pot'; + $header .= "# LANGUAGE translation of PROJECT\n"; + $header .= "# Copyright (c) YEAR NAME \n"; + $header .= "#\n"; + $header .= "msgid \"\"\n"; + $header .= "msgstr \"\"\n"; + $header .= "\"Project-Id-Version: PROJECT VERSION\\n\"\n"; + $header .= "\"POT-Creation-Date: ". date("Y-m-d H:iO") ."\\n\"\n"; + $header .= "\"PO-Revision-Date: YYYY-mm-DD HH:MM+ZZZZ\\n\"\n"; + $header .= "\"Last-Translator: NAME \\n\"\n"; + $header .= "\"Language-Team: LANGUAGE \\n\"\n"; + $header .= "\"MIME-Version: 1.0\\n\"\n"; + $header .= "\"Content-Type: text/plain; charset=utf-8\\n\"\n"; + $header .= "\"Content-Transfer-Encoding: 8bit\\n\"\n"; + $header .= "\"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\\n\"\n"; + $header .= "\n"; + watchdog('locale', t('Exported translation file: %filename.', array('%filename' => theme('placeholder', $filename)))); + } + + // Start download process + header("Content-Disposition: attachment; filename=$filename"); + header("Content-Type: text/plain; charset=utf-8"); + + print $header; + + foreach ($parent as $lid => $message) { + if (!isset($done[$lid])) { + if ($message['comment']) { + print '#: '. $message['comment'] ."\n"; + } + print 'msgid '. _locale_export_print($message['msgid']); + if (isset($message[1]['plural'])) { + print 'msgid_plural '. _locale_export_print($message[1]['msgid']); + if ($language) { + for ($i = 0; $i < $meta->plurals; $i++) { + print 'msgstr['. $i .'] '. _locale_export_print(_locale_export_remove_plural($message[${i}]['translation'])); + $done[$message[${i}]['plural']] = 1; + } + } + else { + print 'msgstr[0] ""'. "\n"; + print 'msgstr[1] ""'. "\n"; + $done[$message[0]['plural']] = 1; + $done[$message[1]['plural']] = 1; + } + } + else { + if ($language) { + print 'msgstr '. _locale_export_print($message['translation']); + } + else { + print 'msgstr ""'. "\n"; + } + } + print "\n"; + } + } + die(); +} + +/** + * Print out a string on multiple lines + */ +function _locale_export_print($str) { + $stri = addcslashes($str, "\0..\37\\\""); + $parts = array(); + + // Cut text into several lines + while ($stri != "") { + $i = strpos($stri, "\\n"); + if ($i === FALSE) { + $curstr = $stri; + $stri = ""; + } + else { + $curstr = substr($stri, 0, $i + 2); + $stri = substr($stri, $i + 2); + } + $curparts = explode("\n", _locale_export_wrap($curstr, 70)); + $parts = array_merge($parts, $curparts); + } + + if (count($parts) > 1) { + return "\"\"\n\"". implode("\"\n\"", $parts) ."\"\n"; + } + else { + return "\"$parts[0]\"\n"; + } +} + +/** + * Custom word wrapping for Portable Object (Template) files. + * + * @author Jacobo Tarrio + */ +function _locale_export_wrap($str, $len) { + $words = explode(' ', $str); + $ret = array(); + + $cur = ""; + $nstr = 1; + while (count($words)) { + $word = array_shift($words); + if ($nstr) { + $cur = $word; + $nstr = 0; + } + elseif (strlen("$cur $word") > $len) { + $ret[] = $cur . " "; + $cur = $word; + } + else { + $cur = "$cur $word"; + } + } + $ret[] = $cur; + + return implode("\n", $ret); +} + +/** + * Removes plural index information from a string + */ +function _locale_export_remove_plural($entry) { + return preg_replace('/(%count)\[[0-9]\]/', '\\1', $entry); +} + +/** + * List languages in search result table + */ +function _locale_string_language_list($translation) { + $languages = locale_supported_languages(FALSE, TRUE); + unset($languages['name']['en']); + $output = ''; + foreach ($languages['name'] as $key => $value) { + if (isset($translation[$key])) { + $output .= ($translation[$key] != '') ? $key .' ' : "$key "; + } + } + + return $output; +} + +/** + * Build object out of search criteria specified in request variables + */ +function _locale_string_seek_query() { + static $query = NULL; + + if (is_null($query)) { + $fields = array('string', 'language', 'searchin'); + $query = new StdClass; + if (is_array($_REQUEST['edit'])) { + foreach ($_REQUEST['edit'] as $key => $value) { + if (!empty($value) && in_array($key, $fields)) { + $query->$key = $value; + } + } + } + else { + foreach ($_REQUEST as $key => $value) { + if (!empty($value) && in_array($key, $fields)) { + $query->$key = strpos(',', $value) ? explode(',', $value) : $value; + } + } + } + } + return $query; +} + +/** + * Perform a string search and display results in a table + */ +function _locale_string_seek() { + // We have at least one criterion to match + if ($query = _locale_string_seek_query()) { + $join = "SELECT s.source, s.location, s.lid, t.translation, t.locale FROM {locales_source} s INNER JOIN {locales_target} t ON s.lid = t.lid "; + + $arguments = array(); + // Compute LIKE section + switch ($query->searchin) { + case 'translated': + $where = "WHERE (t.translation LIKE '%%%s%%' AND t.translation != '')"; + $orderby = "ORDER BY t.translation"; + $arguments[] = $query->string; + break; + case 'untranslated': + $where = "WHERE (s.source LIKE '%%%s%%' AND t.translation = '')"; + $orderby = "ORDER BY s.source"; + $arguments[] = $query->string; + break; + case 'all' : + default: + $where = "WHERE (s.source LIKE '%%%s%%' OR t.translation LIKE '%%%s%%')"; + $orderby = ''; + $arguments[] = $query->string; + $arguments[] = $query->string; + break; + } + + switch ($query->language) { + // Force search in source strings + case "en": + $sql = $join ." WHERE s.source LIKE '%%%s%%' ORDER BY s.source"; + $arguments = array($query->string); // $where is not used, discard its arguments + break; + // Search in all languages + case "all": + $sql = "$join $where $orderby"; + break; + // Some different language + default: + $sql = "$join $where AND t.locale = '%s' $orderby"; + $arguments[] = $query->language; + } + + $result = pager_query($sql, 50, 0, NULL, $arguments); + + $header = array(t('String'), t('Locales'), array('data' => t('Operations'), 'colspan' => '2')); + $arr = array(); + while ($locale = db_fetch_object($result)) { + $arr[$locale->lid]['locales'][$locale->locale] = $locale->translation; + $arr[$locale->lid]['location'] = $locale->location; + $arr[$locale->lid]['source'] = $locale->source; + } + foreach ($arr as $lid => $value) { + $rows[] = array(array('data' => check_plain(truncate_utf8($value['source'], 150, FALSE, TRUE)) .'
'. $value['location'] .''), array('data' => _locale_string_language_list($value['locales']), 'align' => 'center'), array('data' => l(t('edit'), "admin/locale/string/edit/$lid"), 'class' => 'nowrap'), array('data' => l(t('delete'), "admin/locale/string/delete/$lid"), 'class' => 'nowrap')); + } + + $request = array(); + if (count($query)) { + foreach ($query as $key => $value) { + $request[$key] = (is_array($value)) ? implode(',', $value) : $value; + } + } + + if (count($rows)) { + $output .= theme('table', $header, $rows); + } + if ($pager = theme('pager', NULL, 50, 0, $request)) { + $output .= $pager; + } + } + + return $output; +} + +// --------------------------------------------------------------------------------- +// List of some of the most common languages (administration only) + +/** + * Prepares the language code list for a select form item with only the unsupported ones + */ +function _locale_prepare_iso_list() { + $languages = locale_supported_languages(FALSE, TRUE); + $isocodes = _locale_get_iso639_list(); + foreach ($isocodes as $key => $value) { + if (isset($languages['name'][$key])) { + unset($isocodes[$key]); + continue; + } + if (count($value) == 2) { + $tname = t($value[0]); + $isocodes[$key] = ($tname == $value[1]) ? $tname : "$tname ($value[1])"; + } + else { + $isocodes[$key] = t($value[0]); + } + } + asort($isocodes); + return $isocodes; +} + +/** + * Some of the common languages with their English and native names + * + * Based on ISO 639 and http://people.w3.org/rishida/names/languages.html + */ +function _locale_get_iso639_list() { + return array( + "aa" => array("Afar"), + "ab" => array("Abkhazian", "аҧсуа бызшәа"), + "ae" => array("Avestan"), + "af" => array("Afrikaans"), + "ak" => array("Akan"), + "am" => array("Amharic", "አማርኛ"), + "ar" => array("Arabic", "العربية"), + "as" => array("Assamese"), + "av" => array("Avar"), + "ay" => array("Aymara"), + "az" => array("Azerbaijani", "azərbaycan"), + "ba" => array("Bashkir"), + "be" => array("Belarusian", "Беларуская"), + "bg" => array("Bulgarian", "Български"), + "bh" => array("Bihari"), + "bi" => array("Bislama"), + "bm" => array("Bambara", "Bamanankan"), + "bn" => array("Bengali"), + "bo" => array("Tibetan"), + "br" => array("Breton"), + "bs" => array("Bosnian", "Bosanski"), + "ca" => array("Catalan", "Català"), + "ce" => array("Chechen"), + "ch" => array("Chamorro"), + "co" => array("Corsican"), + "cr" => array("Cree"), + "cs" => array("Czech", "Čeština"), + "cu" => array("Old Slavonic"), + "cv" => array("Welsh", "Cymraeg"), + "cy" => array("Welch"), + "da" => array("Danish", "Dansk"), + "de" => array("German", "Deutsch"), + "dv" => array("Maldivian"), + "dz" => array("Bhutani"), + "ee" => array("Ewe", "Ɛʋɛ"), + "el" => array("Greek", "Ελληνικά"), + "en" => array("English"), + "eo" => array("Esperanto"), + "es" => array("Spanish", "Español"), + "et" => array("Estonian", "Eesti"), + "eu" => array("Basque", "Euskera"), + "fa" => array("Persian", "فارسی"), + "ff" => array("Fulah", "Fulfulde"), + "fi" => array("Finnish", "Suomi"), + "fj" => array("Fiji"), + "fo" => array("Faeroese"), + "fr" => array("French", "Français"), + "fy" => array("Frisian", "Frysk"), + "ga" => array("Irish", "Gaeilge"), + "gd" => array("Scots Gaelic"), + "gl" => array("Galician", "Galego"), + "gn" => array("Guarani"), + "gu" => array("Gujarati"), + "gv" => array("Manx"), + "ha" => array("Hausa"), + "he" => array("Hebrew", "עברית"), + "hi" => array("Hindi", "हिन्दी"), + "ho" => array("Hiri Motu"), + "hr" => array("Croatian", "Hrvatski"), + "hu" => array("Hungarian", "Magyar"), + "hy" => array("Armenian", "Հայերեն"), + "hz" => array("Herero"), + "ia" => array("Interlingua"), + "id" => array("Indonesian", "Bahasa Indonesia"), + "ie" => array("Interlingue"), + "ig" => array("Igbo"), + "ik" => array("Inupiak"), + "is" => array("Icelandic", "Íslenska"), + "it" => array("Italian", "Italiano"), + "iu" => array("Inuktitut"), + "ja" => array("Japanese", "日本語"), + "jv" => array("Javanese"), + "ka" => array("Georgian"), + "kg" => array("Kongo"), + "ki" => array("Kikuyu"), + "kj" => array("Kwanyama"), + "kk" => array("Kazakh", "Қазақ"), + "kl" => array("Greenlandic"), + "km" => array("Cambodian"), + "kn" => array("Kannada", "ಕನ್ನಡ"), + "ko" => array("Korean", "한국어"), + "kr" => array("Kanuri"), + "ks" => array("Kashmiri"), + "ku" => array("Kurdish", "Kurdî"), + "kv" => array("Komi"), + "kw" => array("Cornish"), + "ky" => array("Kirghiz", "Кыргыз"), + "la" => array("Latin", "Latina"), + "lb" => array("Luxembourgish"), + "lg" => array("Luganda"), + "ln" => array("Lingala"), + "lo" => array("Laothian"), + "lt" => array("Lithuanian", "Lietuviškai"), + "lv" => array("Latvian", "Latviešu"), + "mg" => array("Malagasy"), + "mh" => array("Marshallese"), + "mi" => array("Maori"), + "mk" => array("Macedonian", "Македонски"), + "ml" => array("Malayalam", "മലയാളം"), + "mn" => array("Mongolian"), + "mo" => array("Moldavian"), + "mr" => array("Marathi"), + "ms" => array("Malay", "Bahasa Melayu"), + "mt" => array("Maltese", "Malti"), + "my" => array("Burmese"), + "na" => array("Nauru"), + "nd" => array("North Ndebele"), + "ne" => array("Nepali"), + "ng" => array("Ndonga"), + "nl" => array("Dutch", "Nederlands"), + "no" => array("Norwegian", "Norsk"), + "nr" => array("South Ndebele"), + "nv" => array("Navajo"), + "ny" => array("Chichewa"), + "oc" => array("Occitan"), + "om" => array("Oromo"), + "or" => array("Oriya"), + "os" => array("Ossetian"), + "pa" => array("Punjabi"), + "pi" => array("Pali"), + "pl" => array("Polish", "Polski"), + "ps" => array("Pashto", "پښتو"), + "pt" => array("Portuguese", "Português"), + "qu" => array("Quechua"), + "rm" => array("Rhaeto-Romance"), + "rn" => array("Kirundi"), + "ro" => array("Romanian", "Română"), + "ru" => array("Russian", "Русский"), + "rw" => array("Kinyarwanda"), + "sa" => array("Sanskrit"), + "sc" => array("Sardinian"), + "sd" => array("Sindhi"), + "se" => array("Northern Sami"), + "sg" => array("Sango"), + "sh" => array("Serbo-Croatian"), + "si" => array("Singhalese"), + "sk" => array("Slovak", "Slovenčina"), + "sl" => array("Slovenian", "Slovenščina"), + "sm" => array("Samoan"), + "sn" => array("Shona"), + "so" => array("Somali"), + "sq" => array("Albanian", "Shqip"), + "sr" => array("Serbian", "Српски"), + "ss" => array("Siswati"), + "st" => array("Sesotho"), + "su" => array("Sudanese"), + "sv" => array("Swedish", "Svenska"), + "sw" => array("Swahili", "Kiswahili"), + "ta" => array("Tamil", "தமிழ்"), + "te" => array("Telugu", "తెలుగు"), + "tg" => array("Tajik"), + "th" => array("Thai", "ภาษาไทย"), + "ti" => array("Tigrinya"), + "tk" => array("Turkmen"), + "tl" => array("Tagalog"), + "tn" => array("Setswana"), + "to" => array("Tonga"), + "tr" => array("Turkish", "Türkçe"), + "ts" => array("Tsonga"), + "tt" => array("Tatar", "Tatarça"), + "tw" => array("Twi"), + "ty" => array("Tahitian"), + "ug" => array("Uighur"), + "uk" => array("Ukrainian", "Українська"), + "ur" => array("Urdu", "اردو"), + "uz" => array("Uzbek", "o'zbek"), + "ve" => array("Venda"), + "vi" => array("Vietnamese", "Tiếng Việt"), + "wo" => array("Wolof"), + "xh" => array("Xhosa", "isiXhosa"), + "yi" => array("Yiddish"), + "yo" => array("Yoruba", "Yorùbá"), + "za" => array("Zhuang"), + "zh-hans" => array("Chinese, Simplified", "简体中文"), + "zh-hant" => array("Chinese, Traditional", "繁體中文"), + "zu" => array("Zulu", "isiZulu"), + ); +} diff --git a/includes/menu.inc b/includes/menu.inc new file mode 100644 index 0000000..c5fdd45 --- /dev/null +++ b/includes/menu.inc @@ -0,0 +1,1362 @@ + $item['description'])). + * - 'children' - A linear list of the menu ID's of this item's children. + * + * Menu ID 0 is the "root" of the menu. The children of this item are the + * menus themselves (they will have no associated path). Menu ID 1 will + * always be one of these children; it is the default "Navigation" menu. + */ +function menu_get_menu() { + global $_menu; + global $user; + global $locale; + + if (!isset($_menu['items'])) { + // _menu_build() may indirectly call this function, so prevent infinite loops. + $_menu['items'] = array(); + + $cid = "menu:$user->uid:$locale"; + if ($cached = cache_get($cid)) { + $_menu = unserialize($cached->data); + } + else { + _menu_build(); + // Cache the menu structure for this user, to expire after one day. + cache_set($cid, serialize($_menu), time() + (60 * 60 * 24)); + } + + // Make sure items that cannot be cached are added. + _menu_append_contextual_items(); + + // Reset the cached $menu in menu_get_item(). + menu_get_item(NULL, NULL, TRUE); + } + + return $_menu; +} + +/** + * Return the local task tree. + * + * Unlike the rest of the menu structure, the local task tree cannot be cached + * nor determined too early in the page request, because the user's current + * location may be changed by a menu_set_location() call, and the tasks shown + * (just as the breadcrumb trail) need to reflect the changed location. + */ +function menu_get_local_tasks() { + global $_menu; + + // Don't cache the local task tree, as it varies by location and tasks are + // allowed to be dynamically determined. + if (!isset($_menu['local tasks'])) { + // _menu_build_local_tasks() may indirectly call this function, so prevent + // infinite loops. + $_menu['local tasks'] = array(); + $pid = menu_get_active_nontask_item(); + if (!_menu_build_local_tasks($pid)) { + // If the build returned FALSE, the tasks need not be displayed. + $_menu['local tasks'][$pid]['children'] = array(); + } + } + + return $_menu['local tasks']; +} + +/** + * Retrieves the menu item specified by $mid, or by $path if $mid is not given. + * + * @param $mid + * The menu ID of the menu item to retrieve. + * @param $path + * The internal path of the menu item to retrieve. Defaults to NULL. Only + * used if $mid is not set. + * @param $reset + * Optional flag that resets the static variable cache of the menu tree, if + * set to TRUE. Default is FALSE. + * + * @return + * The menu item found in the site menu, or an empty array if none could be + * found. + */ +function menu_get_item($mid, $path = NULL, $reset = FALSE) { + static $menu; + + if (!isset($menu) || $reset) { + $menu = menu_get_menu(); + } + + if (isset($mid)) { + return $menu['items'][$mid]; + } + + if (isset($path)) { + return $menu['items'][$menu['path index'][$path]]; + } + + return array(); +} + +/** + * Retrieves the menu ID and title of all root menus. + * + * @return + * Array containing all menus (but not menu items), in the form mid => title. + */ +function menu_get_root_menus() { + $menu = menu_get_menu(); + $root_menus = array(); + + foreach ($menu['items'][0]['children'] as $mid) { + $root_menus[$mid] = $menu['items'][$mid]['title']; + } + + return $root_menus; +} + +/** + * Change the current menu location of the user. + * + * Frequently, modules may want to make a page or node act as if it were + * in the menu tree somewhere, even though it was not registered in a + * hook_menu() implementation. If the administrator has rearranged the menu, + * the newly set location should respect this in the breadcrumb trail and + * expanded/collapsed status of menu items in the tree. This function + * allows this behavior. + * + * @param $location + * An array specifying a complete or partial breadcrumb trail for the + * new location, in the same format as the return value of hook_menu(). + * The last element of this array should be the new location itself. + * + * This function will set the new breadcrumb trail to the passed-in value, + * but if any elements of this trail are visible in the site tree, the + * trail will be "spliced in" to the existing site navigation at that point. + */ +function menu_set_location($location) { + global $_menu; + $temp_id = min(array_keys($_menu['items'])) - 1; + $prev_id = 0; + + // Don't allow this function to change the actual current path, just the + // position in the menu tree. + $location[count($location) - 1]['path'] = $_GET['q']; + + foreach (array_reverse($location) as $item) { + if (isset($_menu['path index'][$item['path']])) { + $mid = $_menu['path index'][$item['path']]; + if (isset($_menu['visible'][$mid])) { + // Splice in the breadcrumb at this location. + if ($prev_id) { + $_menu['items'][$prev_id]['pid'] = $mid; + } + $prev_id = 0; + break; + } + else { + // A hidden item; show it, but only temporarily. + $_menu['items'][$mid]['type'] |= MENU_VISIBLE_IN_BREADCRUMB; + if ($prev_id) { + $_menu['items'][$prev_id]['pid'] = $mid; + } + $prev_id = $mid; + } + } + else { + $item['type'] |= MENU_VISIBLE_IN_BREADCRUMB; + if ($prev_id) { + $_menu['items'][$prev_id]['pid'] = $temp_id; + } + $_menu['items'][$temp_id] = $item; + $_menu['path index'][$item['path']] = $temp_id; + + $prev_id = $temp_id; + $temp_id--; + } + } + + if ($prev_id) { + // Didn't find a home, so attach this to the main navigation menu. + $_menu['items'][$prev_id]['pid'] = 1; + } + + $final_item = array_pop($location); + menu_set_active_item($final_item['path']); +} + +/** + * Execute the handler associated with the active menu item. + * + * This is called early in the page request. The active menu item is at + * this point determined exclusively by the URL. The handler that is called + * here may, as a side effect, change the active menu item so that later + * menu functions (that display the menus and breadcrumbs, for example) + * act as if the user were in a different location on the site. + */ +function menu_execute_active_handler() { + if (_menu_site_is_offline()) { + return MENU_SITE_OFFLINE; + } + + $menu = menu_get_menu(); + + // Determine the menu item containing the callback. + $path = $_GET['q']; + while ($path && !isset($menu['callbacks'][$path])) { + $path = substr($path, 0, strrpos($path, '/')); + } + + if (!isset($menu['callbacks'][$path])) { + return MENU_NOT_FOUND; + } + + if (!function_exists($menu['callbacks'][$path]['callback'])) { + return MENU_NOT_FOUND; + } + + if (!_menu_item_is_accessible(menu_get_active_item())) { + return MENU_ACCESS_DENIED; + } + + // We found one, and are allowed to execute it. + $arguments = isset($menu['callbacks'][$path]['callback arguments']) ? $menu['callbacks'][$path]['callback arguments'] : array(); + $arg = substr($_GET['q'], strlen($path) + 1); + if (strlen($arg)) { + $arguments = array_merge($arguments, explode('/', $arg)); + } + + return call_user_func_array($menu['callbacks'][$path]['callback'], $arguments); +} + +/** + * Returns the ID of the active menu item. + */ +function menu_get_active_item() { + return menu_set_active_item(); +} + +/** + * Sets the path of the active menu item. + */ +function menu_set_active_item($path = NULL) { + static $stored_mid; + + if (!isset($stored_mid) || isset($path)) { + $menu = menu_get_menu(); + if (!isset($path)) { + $path = $_GET['q']; + } + else { + $_GET['q'] = $path; + } + + while ($path && !isset($menu['path index'][$path])) { + $path = substr($path, 0, strrpos($path, '/')); + } + $stored_mid = isset($menu['path index'][$path]) ? $menu['path index'][$path] : 0; + + // Search for default local tasks to activate instead of this item. + $continue = TRUE; + while ($continue) { + $continue = FALSE; + if (isset($menu['items'][$stored_mid]['children'])) { + foreach ($menu['items'][$stored_mid]['children'] as $cid) { + if ($menu['items'][$cid]['type'] & MENU_LINKS_TO_PARENT) { + $stored_mid = $cid; + $continue = TRUE; + } + } + } + } + + // Reset the cached $menu in menu_get_item(). + menu_get_item(NULL, NULL, TRUE); + } + + return $stored_mid; +} + +/** + * Returns the ID of the current menu item or, if the current item is a + * local task, the menu item to which this task is attached. + */ +function menu_get_active_nontask_item() { + $mid = menu_get_active_item(); + + // Find the first non-task item: + while ($mid) { + $item = menu_get_item($mid); + + if (!($item['type'] & MENU_IS_LOCAL_TASK)) { + return $mid; + } + + $mid = $item['pid']; + } +} + +/** + * Returns the title of the active menu item. + */ +function menu_get_active_title() { + if ($mid = menu_get_active_nontask_item()) { + $item = menu_get_item($mid); + return $item['title']; + } +} + +/** + * Returns the help associated with the active menu item. + */ +function menu_get_active_help() { + $path = $_GET['q']; + $output = ''; + + if (!_menu_item_is_accessible(menu_get_active_item())) { + // Don't return help text for areas the user cannot access. + return; + } + + foreach (module_list() as $name) { + if (module_hook($name, 'help')) { + if ($temp = module_invoke($name, 'help', $path)) { + $output .= $temp . "\n"; + } + if (module_hook('help', 'page')) { + if (substr($path, 0, 6) == "admin/") { + if (module_invoke($name, 'help', 'admin/help#' . substr($path, 6))) { + $output .= theme("more_help_link", url('admin/help/' . substr($path, 6))); + } + } + } + } + } + return $output; +} + +/** + * Returns an array of rendered menu items in the active breadcrumb trail. + */ +function menu_get_active_breadcrumb() { + + // No breadcrumb for the front page. + if (drupal_is_front_page()) { + return array(); + } + + $links[] = l(t('Home'), variable_get('site_frontpage', 'node')); + + $trail = _menu_get_active_trail(); + foreach ($trail as $mid) { + $item = menu_get_item($mid); + if ($item['type'] & MENU_VISIBLE_IN_BREADCRUMB) { + $links[] = menu_item_link($mid); + } + } + + // The last item in the trail is the page title; don't display it here. + array_pop($links); + + return $links; +} + +/** + * Returns true when the menu item is in the active trail. + */ +function menu_in_active_trail($mid) { + $trail = _menu_get_active_trail(); + + return in_array($mid, $trail); +} + +/** + * Returns true when the menu item is in the active trail within a + * specific subsection of the menu tree. + * + * @param $mid + * The menu item being considered. + * @param $pid + * The root of the subsection of the menu tree in which to look. + */ +function menu_in_active_trail_in_submenu($mid, $pid) { + $trail = _menu_get_active_trail_in_submenu($pid); + + if (!$trail) { + return FALSE; + } + + return in_array($mid, $trail); +} + +/** + * Populate the database representation of the menu. + * + * This need only be called at the start of pages that modify the menu. + */ +function menu_rebuild() { + // Clear the page cache, so that changed menus are reflected for anonymous users. + cache_clear_all(); + // Also clear the menu cache. + cache_clear_all('menu:', TRUE); + + _menu_build(); + + if (module_exist('menu')) { + $menu = menu_get_menu(); + + // Fill a queue of new menu items which are modifiable. + $new_items = array(); + foreach ($menu['items'] as $mid => $item) { + if ($mid < 0 && ($item['type'] & MENU_MODIFIABLE_BY_ADMIN)) { + $new_items[$mid] = $item; + } + } + + $old_count = -1; + // Save the new items updating the pids in each iteration + while (($c = count($new_items)) && ($c != $old_count)) { + $old_count = count($new_items); + foreach($new_items as $mid => $item) { + // If the item has a valid parent, save it + if ($item['pid'] >= 0) { + // The new menu ID gets passed back by reference as $item['mid'] + menu_save_item($item); + // Fix parent IDs for the children of the menu item just saved + if ($item['children']) { + foreach ($item['children'] as $child) { + if (isset($new_items[$child])) { + $new_items[$child]['pid'] = $item['mid']; + } + } + } + // remove the item + unset($new_items[$mid]); + } + } + } + // Rebuild the menu to account for the changes. + _menu_build(); + } + + // Reset the cached $menu in menu_get_item(). + menu_get_item(NULL, NULL, TRUE); + +} + +/** + * Generate the HTML for a menu tree. + * + * @param $pid + * The parent id of the menu. + * + * @ingroup themeable + */ +function theme_menu_tree($pid = 1) { + if ($tree = menu_tree($pid)) { + return "\n
    \n". $tree ."\n
\n"; + } +} + +/** + * Returns a rendered menu tree. + * + * @param $pid + * The parent id of the menu. + */ +function menu_tree($pid = 1) { + $menu = menu_get_menu(); + $output = ''; + + if (isset($menu['visible'][$pid]) && $menu['visible'][$pid]['children']) { + foreach ($menu['visible'][$pid]['children'] as $mid) { + $type = isset($menu['visible'][$mid]['type']) ? $menu['visible'][$mid]['type'] : NULL; + $children = isset($menu['visible'][$mid]['children']) ? $menu['visible'][$mid]['children'] : NULL; + $output .= theme('menu_item', $mid, menu_in_active_trail($mid) || ($type & MENU_EXPANDED) ? theme('menu_tree', $mid) : '', count($children) == 0); + } + } + + return $output; +} + +/** + * Generate the HTML output for a single menu item. + * + * @param $mid + * The menu id of the item. + * @param $children + * A string containing any rendered child items of this menu. + * @param $leaf + * A boolean indicating whether this menu item is a leaf. + * + * @ingroup themeable + */ +function theme_menu_item($mid, $children = '', $leaf = TRUE) { + return '
  • '. menu_item_link($mid) . $children ."
  • \n"; +} + +/** + * Generate the HTML representing a given menu item ID. + * + * @param $item + * The menu item to render. + * @param $link_item + * The menu item which should be used to find the correct path. + * + * @ingroup themeable + */ +function theme_menu_item_link($item, $link_item) { + return l($item['title'], $link_item['path'], isset($item['description']) ? array('title' => $item['description']) : array()); +} + +/** + * Returns the rendered link to a menu item. + * + * @param $mid + * The menu item id to render. + */ +function menu_item_link($mid) { + $item = menu_get_item($mid); + $link_item = $item; + + while ($link_item['type'] & MENU_LINKS_TO_PARENT) { + $link_item = menu_get_item($link_item['pid']); + } + + return theme('menu_item_link', $item, $link_item); +} + +/** + * Returns the rendered local tasks. The default implementation renders + * them as tabs. + * + * @ingroup themeable + */ +function theme_menu_local_tasks() { + $output = ''; + + if ($primary = menu_primary_local_tasks()) { + $output .= "
      \n". $primary ."
    \n"; + } + if ($secondary = menu_secondary_local_tasks()) { + $output .= "
      \n". $secondary ."
    \n"; + } + + return $output; +} + +/** + * Returns the rendered HTML of the primary local tasks. + */ +function menu_primary_local_tasks() { + $local_tasks = menu_get_local_tasks(); + $pid = menu_get_active_nontask_item(); + $output = ''; + + if (count($local_tasks[$pid]['children'])) { + foreach ($local_tasks[$pid]['children'] as $mid) { + $output .= theme('menu_local_task', $mid, menu_in_active_trail($mid), TRUE); + } + } + + return $output; +} + +/** + * Returns the rendered HTML of the secondary local tasks. + */ +function menu_secondary_local_tasks() { + $local_tasks = menu_get_local_tasks(); + $pid = menu_get_active_nontask_item(); + $output = ''; + + if (count($local_tasks[$pid]['children'])) { + foreach ($local_tasks[$pid]['children'] as $mid) { + if (menu_in_active_trail($mid) && count($local_tasks[$mid]['children']) > 1) { + foreach ($local_tasks[$mid]['children'] as $cid) { + $output .= theme('menu_local_task', $cid, menu_in_active_trail($cid), FALSE); + } + } + } + } + + return $output; +} + +/** + * Generate the HTML representing a given menu item ID as a tab. + * + * @param $mid + * The menu ID to render. + * @param $active + * Whether this tab or a subtab is the active menu item. + * @param $primary + * Whether this tab is a primary tab or a subtab. + * + * @ingroup themeable + */ +function theme_menu_local_task($mid, $active, $primary) { + if ($active) { + return '
  • '. menu_item_link($mid) ."
  • \n"; + } + else { + return '
  • '. menu_item_link($mid) ."
  • \n"; + } +} + +/** + * Returns an array containing the primary links. + * Can optionally descend from the root of the Primary links menu towards the + * current node for a specified number of levels and return that submenu. + * Used to generate a primary/secondary menu from different levels of one menu. + * + * @param $start_level + * This optional parameter can be used to retrieve a context-sensitive array + * of links at $start_level levels deep into the Primary links menu. + * The default is to return the top-level links. + * @param $pid + * The parent menu ID from which to search for children. Defaults to the + * menu_primary_menu setting. + * @return An array containing the themed links as the values. The keys of + * the array contain some extra encoded information about the results. + * The format of the key is {level}-{num}{-active}. + * level is the depth within the menu tree of this list. + * num is the number within this array, used only to make the key unique. + * -active is appended if this element is in the active trail. + */ +function menu_primary_links($start_level = 1, $pid = 0) { + if (!module_exist('menu')) { + return NULL; + } + if (!$pid) { + $pid = variable_get('menu_primary_menu', 0); + } + if (!$pid) { + return NULL; + } + + if ($start_level < 1) { + $start_level = 1; + } + + if ($start_level > 1) { + $trail = _menu_get_active_trail_in_submenu($pid); + if (!$trail) { + return NULL; + } + else { + $pid = $trail[$start_level - 1]; + } + } + + $menu = menu_get_menu(); + $links = array(); + if ($pid && is_array($menu['visible'][$pid]) && isset($menu['visible'][$pid]['children'])) { + $count = 1; + foreach ($menu['visible'][$pid]['children'] as $cid) { + $index = "menu-$start_level-$count"; + if (menu_in_active_trail_in_submenu($cid, $pid)) { + $index .= "-active"; + } + $links[$index] = menu_item_link($cid); + $count++; + } + } + + // Special case - provide link to admin/menu if primary links is empty. + if (empty($links) && $start_level == 1 && $pid == variable_get('menu_primary_menu', 0)) { + $links['1-1'] = l(t('edit primary links'),'admin/menu'); + } + + return $links; +} + +/** + * Returns an array containing the secondary links. + * Secondary links can be either a second level of the Primary links + * menu or generated from their own menu. + */ +function menu_secondary_links() { + $msm = variable_get('menu_secondary_menu', 0); + if ($msm == 0) { + return NULL; + } + + if ($msm == variable_get('menu_primary_menu', 0)) { + return menu_primary_links(2, $msm); + } + + return menu_primary_links(1, $msm); +} + +/** + * Returns the themed HTML for primary and secondary links. + * Note that this function is overridden by most core themes because + * those themes display links in "link | link" format, not from a list. + * Also note that by default links rendered with this function are + * displayed with the same CSS as is used for the local tasks. + * If a theme wishes to render links from a ul it is expected that + * the theme will provide suitable CSS. + * + * @param $links + * An array containing links to render. + * @return + * A string containing the themed links. + * + * @ingroup themeable + */ +function theme_menu_links($links) { + if (!count($links)) { + return ''; + } + $level_tmp = explode('-', key($links)); + $level = $level_tmp[0]; + $output = "