Commit a8f8a26f authored by Dries's avatar Dries

- Patch #553944 by David_Rothstein, ksenzee | JacobSingh, sun, jhodgdon,...

- Patch #553944 by David_Rothstein, ksenzee | JacobSingh, sun, jhodgdon, pwolanin: allow modules to specify per-page custom themes in hook_menu().
parent 15343a99
......@@ -3918,7 +3918,10 @@ function _drupal_bootstrap_full() {
ini_set('log_errors', 1);
ini_set('error_log', file_directory_path() . '/error.log');
}
// Set a custom theme for the current page, if there is one. We need to run
// this before invoking hook_init(), since any modules which initialize the
// theme system will prevent a custom theme from being correctly set later.
menu_set_custom_theme();
// Let all modules take action before menu system handles the request
// We do not want this while running update.php.
if (!defined('MAINTENANCE_MODE') || MAINTENANCE_MODE != 'update') {
......
......@@ -359,8 +359,9 @@ function menu_set_item($path, $router_item) {
* The router item, an associate array corresponding to one row in the
* menu_router table. The value of key map holds the loaded objects. The
* value of key access is TRUE if the current user can access this page.
* The values for key title, page_arguments, access_arguments will be
* filled in based on the database values and the objects loaded.
* The values for key title, page_arguments, access_arguments, and
* theme_arguments will be filled in based on the database values and the
* objects loaded.
*/
function menu_get_item($path = NULL, $router_item = NULL) {
$router_items = &drupal_static(__FUNCTION__);
......@@ -391,6 +392,7 @@ function menu_get_item($path = NULL, $router_item = NULL) {
if ($router_item['access']) {
$router_item['map'] = $map;
$router_item['page_arguments'] = array_merge(menu_unserialize($router_item['page_arguments'], $map), array_slice($map, $router_item['number_parts']));
$router_item['theme_arguments'] = array_merge(menu_unserialize($router_item['theme_arguments'], $map), array_slice($map, $router_item['number_parts']));
}
}
$router_items[$path] = $router_item;
......@@ -936,6 +938,8 @@ function menu_tree_all_data($menu_name, $link = NULL, $max_depth = NULL) {
'title',
'title_callback',
'title_arguments',
'theme_callback',
'theme_arguments',
'type',
'description',
));
......@@ -1117,6 +1121,8 @@ function menu_tree_page_data($menu_name, $max_depth = NULL) {
'title',
'title_callback',
'title_arguments',
'theme_callback',
'theme_arguments',
'type',
'description',
));
......@@ -1389,6 +1395,38 @@ function menu_get_active_help() {
return $output;
}
/**
* Gets the custom theme for the current page, if there is one.
*
* @param $initialize
* This parameter should only be used internally; it is set to TRUE in order
* to force the custom theme to be initialized from the menu router item for
* the current page.
* @return
* The machine-readable name of the custom theme, if there is one.
*
* @see menu_set_custom_theme()
*/
function menu_get_custom_theme($initialize = FALSE) {
$custom_theme = &drupal_static(__FUNCTION__);
// Skip this if the site is offline or being installed or updated, since the
// menu system may not be correctly initialized then.
if ($initialize && !_menu_site_is_offline(TRUE) && (!defined('MAINTENANCE_MODE') || (MAINTENANCE_MODE != 'update' && MAINTENANCE_MODE != 'install'))) {
$router_item = menu_get_item();
if (!empty($router_item['access']) && !empty($router_item['theme_callback']) && function_exists($router_item['theme_callback'])) {
$custom_theme = call_user_func_array($router_item['theme_callback'], $router_item['theme_arguments']);
}
}
return $custom_theme;
}
/**
* Sets a custom theme for the current page, if there is one.
*/
function menu_set_custom_theme() {
menu_get_custom_theme(TRUE);
}
/**
* Build a list of named menus.
*/
......@@ -2728,6 +2766,13 @@ function _menu_router_build($callbacks) {
$item['file path'] = $parent['file path'];
}
}
// Same for theme callbacks.
if (!isset($item['theme callback']) && isset($parent['theme callback'])) {
$item['theme callback'] = $parent['theme callback'];
if (!isset($item['theme arguments']) && isset($parent['theme arguments'])) {
$item['theme arguments'] = $parent['theme arguments'];
}
}
}
}
if (!isset($item['access callback']) && isset($item['access arguments'])) {
......@@ -2749,6 +2794,8 @@ function _menu_router_build($callbacks) {
'block callback' => '',
'title arguments' => array(),
'title callback' => 't',
'theme arguments' => array(),
'theme callback' => '',
'description' => '',
'position' => '',
'tab_parent' => '',
......@@ -2798,6 +2845,8 @@ function _menu_router_save($menu, $masks) {
'title',
'title_callback',
'title_arguments',
'theme_callback',
'theme_arguments',
'type',
'block_callback',
'description',
......@@ -2823,6 +2872,8 @@ function _menu_router_save($menu, $masks) {
'title' => $item['title'],
'title_callback' => $item['title callback'],
'title_arguments' => ($item['title arguments'] ? serialize($item['title arguments']) : ''),
'theme_callback' => $item['theme callback'],
'theme_arguments' => serialize($item['theme arguments']),
'type' => $item['type'],
'block_callback' => $item['block callback'],
'description' => $item['description'],
......@@ -2853,20 +2904,24 @@ function menu_path_is_external($path) {
* This function will log the current user out and redirect to front page
* if the current user has no 'access site in maintenance mode' permission.
*
* @param $check_only
* If this is set to TRUE, the function will perform the access checks and
* return the site offline status, but not log the user out or display any
* messages.
* @return
* FALSE if the site is not in maintenance mode, the user login page is
* displayed, or the user has the 'access site in maintenance mode'
* permission. TRUE for anonymous users not being on the login page when the
* site is in maintenance mode.
*/
function _menu_site_is_offline() {
function _menu_site_is_offline($check_only = FALSE) {
// Check if site is in maintenance mode.
if (variable_get('maintenance_mode', 0)) {
if (user_access('access site in maintenance mode')) {
// Ensure that the maintenance mode message is displayed only once
// (allowing for page redirects) and specifically suppress its display on
// the maintenance mode settings page.
if ($_GET['q'] != 'admin/config/development/maintenance') {
if (!$check_only && $_GET['q'] != 'admin/config/development/maintenance') {
if (user_access('administer site configuration')) {
drupal_set_message(t('Operating in maintenance mode. <a href="@url">Go online.</a>', array('@url' => url('admin/config/development/maintenance'))), 'status', FALSE);
}
......@@ -2881,8 +2936,10 @@ function _menu_site_is_offline() {
return ($_GET['q'] != 'user' && $_GET['q'] != 'user/login');
}
// Logged in users are unprivileged here, so they are logged out.
require_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'user') . '/user.pages.inc';
user_logout();
if (!$check_only) {
require_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'user') . '/user.pages.inc';
user_logout();
}
}
}
return FALSE;
......
......@@ -37,11 +37,25 @@
* @} End of "Content markers".
*/
/**
* Determines if a theme is available to use.
*
* @param $theme
* An object representing the theme to check.
* @return
* Boolean TRUE if the theme is enabled or is the site administration theme;
* FALSE otherwise.
*/
function drupal_theme_access($theme) {
$admin_theme = variable_get('admin_theme');
return !empty($theme->status) || ($admin_theme && $theme->name == $admin_theme);
}
/**
* Initialize the theme system by loading the theme.
*/
function drupal_theme_initialize() {
global $theme, $user, $custom_theme, $theme_key;
global $theme, $user, $theme_key;
// If $theme is already set, assume the others are set, too, and do nothing
if (isset($theme)) {
......@@ -52,12 +66,13 @@ function drupal_theme_initialize() {
$themes = list_themes();
// Only select the user selected theme if it is available in the
// list of enabled themes.
$theme = !empty($user->theme) && !empty($themes[$user->theme]->status) ? $user->theme : variable_get('theme_default', 'garland');
// list of themes that can be accessed.
$theme = !empty($user->theme) && isset($themes[$user->theme]) && drupal_theme_access($themes[$user->theme]) ? $user->theme : variable_get('theme_default', 'garland');
// Allow modules to override the present theme... only select custom theme
// if it is available in the list of installed themes.
$theme = $custom_theme && $themes[$custom_theme] ? $custom_theme : $theme;
// if it is available in the list of themes that can be accessed.
$custom_theme = menu_get_custom_theme();
$theme = $custom_theme && isset($themes[$custom_theme]) && drupal_theme_access($themes[$custom_theme]) ? $custom_theme : $theme;
// Store the identifier for retrieving theme settings with.
$theme_key = $theme;
......
......@@ -10,11 +10,6 @@
* Menu callback for admin/structure/block.
*/
function block_admin_display($theme = NULL) {
global $custom_theme;
// If non-default theme configuration has been selected, set the custom theme.
$custom_theme = isset($theme) ? $theme : variable_get('theme_default', 'garland');
// Fetch and sort blocks.
$blocks = _block_rehash();
usort($blocks, '_block_compare');
......@@ -26,14 +21,10 @@ function block_admin_display($theme = NULL) {
* Generate main blocks administration form.
*/
function block_admin_display_form($form, &$form_state, $blocks, $theme = NULL) {
global $theme_key, $custom_theme;
global $theme_key;
drupal_add_css(drupal_get_path('module', 'block') . '/block.css', array('preprocess' => FALSE));
// If non-default theme configuration has been selected, set the custom theme.
$custom_theme = isset($theme) ? $theme : variable_get('theme_default', 'garland');
drupal_theme_initialize();
$block_regions = system_region_list($theme_key, REGIONS_VISIBLE) + array(BLOCK_REGION_NONE => '<' . t('none') . '>');
// Weights range from -delta to +delta, so delta should be at least half
......
......@@ -76,6 +76,7 @@ function block_menu() {
'description' => 'Configure what block content appears in your site\'s sidebars and other regions.',
'page callback' => 'block_admin_display',
'access arguments' => array('administer blocks'),
'theme callback' => '_block_custom_theme',
'file' => 'block.admin.inc',
);
$items['admin/structure/block/list'] = array(
......@@ -123,6 +124,8 @@ function block_menu() {
'weight' => $key == $default ? -10 : 0,
'access callback' => '_block_themes_access',
'access arguments' => array($theme),
'theme callback' => '_block_custom_theme',
'theme arguments' => array($key),
'file' => 'block.admin.inc',
);
}
......@@ -133,8 +136,23 @@ function block_menu() {
* Menu item access callback - only admin or enabled themes can be accessed.
*/
function _block_themes_access($theme) {
$admin_theme = variable_get('admin_theme');
return user_access('administer blocks') && ($theme->status || ($admin_theme && ($theme->name == $admin_theme)));
return user_access('administer blocks') && drupal_theme_access($theme);
}
/**
* Theme callback for the block configuration pages.
*
* @param $theme
* The theme whose blocks are being configured. If not set, the default theme
* is assumed.
* @return
* The theme that should be used for the block configuration page, or NULL
* to indicate that the default theme should be used.
*/
function _block_custom_theme($theme = NULL) {
// We return exactly what was passed in, to guarantee that the page will
// always be displayed using the theme whose blocks are being configured.
return $theme;
}
/**
......
......@@ -40,6 +40,13 @@
* user_access() unless a value is inherited from a parent menu item.
* - "access arguments": An array of arguments to pass to the access callback
* function. Integer values pass the corresponding URL component.
* - "theme callback": Optional. A function returning the machine-readable
* name of the theme that will be used to render the page. If the function
* returns nothing, the main site theme will be used. If no function is
* provided, the main site theme will also be used, unless a value is
* inherited from a parent menu item.
* - "theme arguments": An array of arguments to pass to the theme callback
* function. Integer values pass the corresponding URL component.
* - "file": A file that will be included before the callbacks are accessed;
* this allows callback functions to be in separate files. The file should
* be relative to the implementing module's directory unless otherwise
......
......@@ -1652,6 +1652,7 @@ function node_menu() {
'access callback' => '_node_add_access',
'weight' => 1,
'menu_name' => 'management',
'theme callback' => '_node_custom_theme',
'file' => 'node.pages.inc',
);
$items['rss.xml'] = array(
......@@ -1713,6 +1714,7 @@ function node_menu() {
'page arguments' => array(1),
'access callback' => 'node_access',
'access arguments' => array('update', 1),
'theme callback' => '_node_custom_theme',
'weight' => 1,
'type' => MENU_LOCAL_TASK,
'file' => 'node.pages.inc',
......@@ -1776,6 +1778,17 @@ function node_page_title($node) {
return $node->title;
}
/**
* Theme callback for creating and editing nodes.
*/
function _node_custom_theme() {
// Use the administration theme if the site is configured to use it for
// nodes.
if (variable_get('node_admin_theme')) {
return variable_get('admin_theme');
}
}
/**
* Implement hook_init().
*/
......
......@@ -18,6 +18,10 @@ class MenuIncTestCase extends DrupalWebTestCase {
function setUp() {
// Enable dummy module that implements hook_menu.
parent::setUp('menu_test');
// Make the tests below more robust by explicitly setting the default theme
// and administrative theme that they expect.
variable_set('theme_default', 'garland');
variable_set('admin_theme', 'seven');
}
/**
......@@ -29,6 +33,80 @@ class MenuIncTestCase extends DrupalWebTestCase {
$this->assertNoText(t('A title with @placeholder', array('@placeholder' => 'some other text')), t('Text with placeholder substitutions not found.'));
}
/**
* Test the theme callback when it is set to use an administrative theme.
*/
function testThemeCallbackAdministrative() {
$this->drupalGet('menu-test/theme-callback/use-admin-theme');
$this->assertText('Requested theme: seven. Actual theme: seven.', t('The administrative theme can be correctly set in a theme callback.'));
$this->assertRaw('seven/style.css', t("The administrative theme's CSS appears on the page."));
}
/**
* Test that the theme callback is properly inherited.
*/
function testThemeCallbackInheritance() {
$this->drupalGet('menu-test/theme-callback/use-admin-theme/inheritance');
$this->assertText('Requested theme: seven. Actual theme: seven. Theme callback inheritance is being tested.', t('Theme callback inheritance correctly uses the administrative theme.'));
$this->assertRaw('seven/style.css', t("The administrative theme's CSS appears on the page."));
}
/**
* Test the theme callback when the site is in maintenance mode.
*/
function testThemeCallbackMaintenanceMode() {
variable_set('maintenance_mode', TRUE);
// For a regular user, the fact that the site is in maintenance mode means
// we expect the theme callback system to be bypassed entirely.
$this->drupalGet('menu-test/theme-callback/use-admin-theme');
$this->assertRaw('minnelli/minnelli.css', t("The maintenance theme's CSS appears on the page."));
// An administrator, however, should continue to see the requested theme.
$admin_user = $this->drupalCreateUser(array('access site in maintenance mode'));
$this->drupalLogin($admin_user);
$this->drupalGet('menu-test/theme-callback/use-admin-theme');
$this->assertText('Requested theme: seven. Actual theme: seven.', t('The theme callback system is correctly triggered for an administrator when the site is in maintenance mode.'));
$this->assertRaw('seven/style.css', t("The administrative theme's CSS appears on the page."));
}
/**
* Test the theme callback when it is set to use an optional theme.
*/
function testThemeCallbackOptionalTheme() {
// Request a theme that is not enabled.
$this->drupalGet('menu-test/theme-callback/use-stark-theme');
$this->assertText('Requested theme: stark. Actual theme: garland.', t('The theme callback system falls back on the default theme when a theme that is not enabled is requested.'));
$this->assertRaw('garland/style.css', t("The default theme's CSS appears on the page."));
// Now enable the theme and request it again.
$admin_user = $this->drupalCreateUser(array('administer site configuration'));
$this->drupalLogin($admin_user);
$this->drupalPost('admin/appearance', array('status[stark]' => 1), t('Save configuration'));
$this->drupalLogout();
$this->drupalGet('menu-test/theme-callback/use-stark-theme');
$this->assertText('Requested theme: stark. Actual theme: stark.', t('The theme callback system uses an optional theme once it has been enabled.'));
$this->assertRaw('stark/layout.css', t("The optional theme's CSS appears on the page."));
}
/**
* Test the theme callback when it is set to use a theme that does not exist.
*/
function testThemeCallbackFakeTheme() {
$this->drupalGet('menu-test/theme-callback/use-fake-theme');
$this->assertText('Requested theme: fake_theme. Actual theme: garland.', t('The theme callback system falls back on the default theme when a theme that does not exist is requested.'));
$this->assertRaw('garland/style.css', t("The default theme's CSS appears on the page."));
}
/**
* Test the theme callback when no theme is requested.
*/
function testThemeCallbackNoThemeRequested() {
$this->drupalGet('menu-test/theme-callback/no-theme-requested');
$this->assertText('Requested theme: NONE. Actual theme: garland.', t('The theme callback system falls back on the default theme when no theme is requested.'));
$this->assertRaw('garland/style.css', t("The default theme's CSS appears on the page."));
}
/**
* Tests for menu_link_maintain().
*/
......
......@@ -44,6 +44,20 @@ function menu_test_menu() {
'title' => 'Unattached subchild router',
'page callback' => 'node_page_default',
);
// Theme callback tests.
$items['menu-test/theme-callback/%'] = array(
'title' => 'Page that displays different themes',
'page callback' => 'menu_test_theme_page_callback',
'access arguments' => array('access content'),
'theme callback' => 'menu_test_theme_callback',
'theme arguments' => array(2),
);
$items['menu-test/theme-callback/%/inheritance'] = array(
'title' => 'Page that tests theme callback inheritance.',
'page callback' => 'menu_test_theme_page_callback',
'page arguments' => array(TRUE),
'access arguments' => array('access content'),
);
return $items;
}
......@@ -57,6 +71,56 @@ function menu_test_callback() {
return $this->randomName();
}
/**
* Page callback to use when testing the theme callback functionality.
*
* @param $inherited
* An optional boolean to set to TRUE when the requested page is intended to
* inherit the theme of its parent.
* @return
* A string describing the requested custom theme and actual theme being used
* for the current page request.
*/
function menu_test_theme_page_callback($inherited = FALSE) {
global $theme_key;
// Initialize the theme system so that $theme_key will be populated.
drupal_theme_initialize();
// Now check both the requested custom theme and the actual theme being used.
$custom_theme = menu_get_custom_theme();
$requested_theme = empty($custom_theme) ? 'NONE' : $custom_theme;
$output = "Requested theme: $requested_theme. Actual theme: $theme_key.";
if ($inherited) {
$output .= ' Theme callback inheritance is being tested.';
}
return $output;
}
/**
* Theme callback to use when testing the theme callback functionality.
*
* @param $argument
* The argument passed in from the URL.
* @return
* The name of the custom theme to request for the current page.
*/
function menu_test_theme_callback($argument) {
// Test using the variable administrative theme.
if ($argument == 'use-admin-theme') {
return variable_get('admin_theme');
}
// Test using a theme that exists, but may or may not be enabled.
elseif ($argument == 'use-stark-theme') {
return 'stark';
}
// Test using a theme that does not exist.
elseif ($argument == 'use-fake-theme') {
return 'fake_theme';
}
// For any other value of the URL argument, do not return anything. This
// allows us to test that returning nothing from a theme callback function
// causes the page to correctly fall back on using the main site theme.
}
/**
* Helper function for the testMenuName() test. Used to change the menu_name
* parameter of a menu.
......
......@@ -233,7 +233,7 @@ function system_themes_form() {
);
$options[$theme->name] = $theme->info['name'];
if (!empty($theme->status) || $theme->name == variable_get('admin_theme', 0)) {
if (drupal_theme_access($theme)) {
$form[$theme->name]['operations'] = array('#markup' => l(t('configure'), 'admin/appearance/settings/' . $theme->name) );
}
else {
......
......@@ -955,6 +955,20 @@ function system_schema() {
'not null' => TRUE,
'default' => '',
),
'theme_callback' => array(
'description' => 'A function which returns the name of the theme that will be used to render this page. If left empty, the default theme will be used.',
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
'default' => '',
),
'theme_arguments' => array(
'description' => 'A serialized array of arguments for the theme callback.',
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
'default' => '',
),
'type' => array(
'description' => 'Numeric representation of the type of the menu item, like MENU_LOCAL_TASK.',
'type' => 'int',
......@@ -2548,6 +2562,15 @@ function system_update_7038() {
db_change_field('actions', 'description', 'label', array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => '0'));
}
/**
* Adds fields to the {menu_router} table to allow custom themes to be set per
* page.
*/
function system_update_7039() {
db_add_field('menu_router', 'theme_callback', array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => ''));
db_add_field('menu_router', 'theme_arguments', array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => ''));
}
/**
* @} End of "defgroup updates-6.x-to-7.x"
* The next series of updates should start at 8000.
......
......@@ -509,6 +509,8 @@ function system_menu() {
'page callback' => 'system_main_admin_page',
'weight' => 9,
'menu_name' => 'management',
'theme callback' => 'variable_get',
'theme arguments' => array('admin_theme'),
'file' => 'system.admin.inc',
);
$items['admin/compact'] = array(
......@@ -1309,7 +1311,7 @@ function blocked_ip_load($iid) {
* Menu item access callback - only admin or enabled themes can be accessed.
*/
function _system_themes_access($theme) {
return user_access('administer site configuration') && ($theme->status || $theme->name == variable_get('admin_theme', 0));
return user_access('administer site configuration') && drupal_theme_access($theme);
}
/**
......@@ -1423,14 +1425,10 @@ function _system_filetransfer_backend_form_common() {
* Implement hook_init().
*/
function system_init() {
// Use the administrative theme if the user is looking at a page in the admin/* path.
// Add the CSS for this module.
if (arg(0) == 'admin' || (variable_get('node_admin_theme', '0') && arg(0) == 'node' && (arg(1) == 'add' || arg(2) == 'edit'))) {
global $custom_theme;
$custom_theme = variable_get('admin_theme', 0);
drupal_add_css(drupal_get_path('module', 'system') . '/admin.css', array('weight' => CSS_SYSTEM));
}
// Add the CSS for this module.
drupal_add_css(drupal_get_path('module', 'system') . '/defaults.css', array('weight' => CSS_SYSTEM));
drupal_add_css(drupal_get_path('module', 'system') . '/system.css', array('weight' => CSS_SYSTEM));
drupal_add_css(drupal_get_path('module', 'system') . '/system-menus.css', array('weight' => CSS_SYSTEM));
......@@ -1665,7 +1663,7 @@ function system_admin_menu_block($item) {
$default_task = NULL;
$has_subitems = FALSE;
$result = db_query("
SELECT m.load_functions, m.to_arg_functions, m.access_callback, m.access_arguments, m.page_callback, m.page_arguments, m.title, m.title_callback, m.title_arguments, m.type, m.description, m.path, m.weight as router_weight, ml.*
SELECT m.load_functions, m.to_arg_functions, m.access_callback, m.access_arguments, m.page_callback, m.page_arguments, m.title, m.title_callback, m.title_arguments, m.theme_callback, m.theme_arguments, m.type, m.description, m.path, m.weight as router_weight, ml.*
FROM {menu_router} m
LEFT JOIN {menu_links} ml ON m.path = ml.router_path
WHERE (ml.plid = :plid AND ml.menu_name = :name AND hidden = 0) OR (m.tab_parent = :path AND m.type IN (:local_task, :default_task))", array(':plid' => $item['mlid'], ':name' => $item['menu_name'], ':path' => $item['path'], ':local_task' => MENU_LOCAL_TASK, ':default_task' => MENU_DEFAULT_LOCAL_TASK), array('fetch' => PDO::FETCH_ASSOC));
......@@ -2353,7 +2351,7 @@ function system_get_module_admin_tasks($module) {
if (empty($items)) {
$result = db_query("
SELECT m.load_functions, m.to_arg_functions, m.access_callback, m.access_arguments, m.page_callback, m.page_arguments, m.title, m.title_callback, m.title_arguments, m.type, ml.*
SELECT m.load_functions, m.to_arg_functions, m.access_callback, m.access_arguments, m.page_callback, m.page_arguments, m.title, m.title_callback, m.title_arguments, m.theme_callback, m.theme_arguments, m.type, ml.*
FROM {menu_links} ml INNER JOIN {menu_router} m ON ml.router_path = m.path WHERE ml.link_path LIKE 'admin/%' AND hidden >= 0 AND module = 'system' AND m.number_parts > 2", array(), array('fetch' => PDO::FETCH_ASSOC));
foreach ($result as $item) {
_menu_link_translate($item);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment