'Theme Developer', 'description' => 'Display or hide the textual template log', 'page callback' => 'drupal_get_form', 'page arguments' => array('devel_themer_admin_settings'), 'access arguments' => array('administer site configuration'), 'type' => MENU_NORMAL_ITEM, ); $items['devel_themer/variables'] = array( 'title' => 'Theme Development AJAX variables', 'page callback' => 'devel_themer_ajax_variables', 'access callback' => 'devel_themer_user_access', 'type' => MENU_CALLBACK, ); $items['devel/devel_themer'] = array( 'title callback' => 'devel_themer_set_menu_text', 'description' => 'Quickly enable or disable theme developer module.', 'page callback' => 'devel_themer_toggle', 'access arguments' => array('access devel information'), 'menu_name' => 'devel', // Have a weight other than zero here, so that the item does not flip around // position depending on the text of the link. Most of the other links are weight zero. // The link can easily be moved to another position via admin -> menu 'weight' => 5, ); return $items; } /** * Helper function to set the menu link text. */ function devel_themer_set_menu_text() { return devel_themer_enabled() ? t('Disable Theme Developer') : t('Enable Theme Developer'); } /** * Implementation of hook_menu_link_alter(). * Flag this link as needing alter at display time. This is more robust than setting * alter in hook_menu(). See devel_themer_translated_menu_link_alter(). * */ function devel_themer_menu_link_alter(&$item, $menu) { if ($item['link_path'] == 'devel/devel_themer') { $item['options']['alter'] = TRUE; } } /** * Implementation of hook_translated_menu_item_alter(). * Append dynamic querystring 'destination' to our own menu item. * */ function devel_themer_translated_menu_link_alter(&$item) { if ($item['href'] == 'devel/devel_themer') { $item['localized_options']['query'] = drupal_get_destination(); } } /** * A menu callback used by popup to retrieve variables from cache for a recent page. * * @param $request_id * A unique key that is sent to the browser in Drupal.Settings.devel_themer_request_id * @param $call * The theme call for which you wish to retrieve variables. * @return string * A chunk of HTML with the devel_print_object() rendering of the variables. */ function devel_themer_ajax_variables($request_id, $call) { $file = file_directory_temp(). "/devel_themer_$request_id"; if ($data = unserialize(file_get_contents($file))) { $variables = $data[$call]['variables']; if (has_krumo()) { print krumo_ob($variables); } elseif ($data[$call]['type'] == 'func') { print devel_print_object($variables, NULL, FALSE); } else { print devel_print_object($variables, '$', FALSE); } } else { print 'Ajax variables file not found. -'. check_plain($file); } $GLOBALS['devel_shutdown'] = FALSE; return; } /** * Toggle the Devel Themer. * This is called from the Devel menu link. * * @return void */ function devel_themer_toggle() { if (devel_themer_enabled()) { devel_themer_enabled(FALSE); drupal_set_message(t('Devel Themer module is now disabled.')); } else { devel_themer_enabled(TRUE); drupal_set_message(t('Devel Themer module is now enabled. Tick the "themer info" box to display theme information.')); } drupal_goto(); } /** * Set and/or return the status of Devel Themer. * @param * $enable - TRUE to enable the themer. * FALSE to disable the themer. * NULL to leave it unchanged. * * @return * TRUE if themer is enabled, FALSE if not. */ function devel_themer_enabled($enable = NULL) { if (!is_null($enable)) { variable_set('devel_themer_enabled', (bool)$enable); } return variable_get('devel_themer_enabled', TRUE); } function devel_themer_admin_settings() { $form['devel_themer_log'] = array('#type' => 'checkbox', '#title' => t('Display theme log'), '#default_value' => variable_get('devel_themer_log', FALSE), '#description' => t('Display the list of theme templates and theme functions which could have been be used for a given page. The one that was actually used is bolded. This is the same data as the represented in the popup, but all calls are listed in chronological order and can alternately be sorted by time.'), ); return system_settings_form($form); } /** * Implementation of hook_perm(). */ function devel_themer_perm() { return array('access devel theme information'); } function devel_themer_init() { if (devel_themer_enabled() && devel_themer_user_access()) { $path = drupal_get_path('module', 'devel_themer'); $path_to_devel = drupal_get_path('module', 'devel'); // we inject our HTML after page has loaded we have to add this manually. if (has_krumo()) { drupal_add_js($path_to_devel. '/krumo/krumo.js'); drupal_add_css($path_to_devel. '/krumo/skins/default/skin.css'); } drupal_add_css($path .'/devel_themer.css'); drupal_add_js($path .'/devel_themer.js'); // The order these last two are loaded is important. if (module_exists('jquery_ui')) { jquery_ui_add('ui.core'); // Early versions of jquery_ui still need ui.mouse included. $jquery_version = explode(".",jquery_ui_get_version()); if ($jquery_version[0] < 1 || ($jquery_version[0] == 1 && $jquery_version[1] < 5)) { jquery_ui_add('ui.mouse'); } jquery_ui_add('ui.draggable'); } else { drupal_add_js($path_to_devel .'/ui.mouse.js'); drupal_add_js($path_to_devel .'/ui.draggable.js'); } // This needs to happen after all the other CSS. drupal_set_html_head(''); devel_themer_popup(); if (!devel_silent() && variable_get('devel_themer_log', FALSE)) { register_shutdown_function('devel_themer_shutdown'); } } } function devel_themer_shutdown() { print devel_themer_log(); } /** * An implementation of hook_theme_registry_alter() * Iterate over theme registry, injecting our catch function into every theme call, including template calls. * The catch function logs theme calls and performs divine nastiness. * * @return void **/ function devel_themer_theme_registry_alter($theme_registry) { foreach ($theme_registry as $hook => $data) { if (isset($theme_registry[$hook]['function'])) { // If the hook is a function, store it so it can be run after it has been intercepted. // This does not apply to template calls. $theme_registry[$hook]['devel_function_intercept'] = $theme_registry[$hook]['function']; } // Add our catch function to intercept functions as well as templates. $theme_registry[$hook]['function'] = 'devel_themer_catch_function'; } } /** * Show all theme templates and functions that could have been used on this page. **/ function devel_themer_log() { if (isset($GLOBALS['devel_theme_calls'])) { foreach ($GLOBALS['devel_theme_calls'] as $counter => $call) { // Sometimes $call is a string. Not sure why. if (is_array($call)) { $id = "devel_theme_log_link_$counter"; $marker = "
\n"; $used = $call['used']; if ($call['type'] == 'func') { $name = $call['name']. '()'; foreach ($call['candidates'] as $item) { if ($item == $used) { $items[] = "$used"; } else { $items[] = $item; } } } else { $name = $call['name']; foreach ($call['candidates'] as $item) { if ($item == basename($used)) { $items[] = "$used"; } else { $items[] = $item; } } } $rows[] = array($call['duration'], $marker. $name, implode(', ', $items)); unset($items); } } $header = array('Duration (ms)', 'Template/Function', "Candidate template files or function names"); $output = theme('table', $header, $rows); return $output; } } // Would be nice if theme() broke this into separate function so we don't copy logic here. this one is better - has cache function devel_themer_get_extension() { global $theme_engine; static $extension = NULL; if (!$extension) { $extension_function = $theme_engine .'_extension'; if (function_exists($extension_function)) { $extension = $extension_function(); } else { $extension = '.tpl.php'; } } return $extension; } /** * Intercepts all theme calls (including templates), adds to template log, and dispatches to original theme function. * This function gets injected into theme registry in devel_themer_theme_registry_alter(). */ function devel_themer_catch_function() { $args = func_get_args(); // Get the hook argument passed to theme() by stepping through the backtrace // to the closest entry of theme() being called, and get its first argument. $trace = debug_backtrace(); foreach ($trace as $trace_entry) { if ($trace_entry['function'] == 'theme') { $hook = $trace_entry['args'][0]; break; } } array_unshift($args, $hook); $counter = devel_counter(); $timer_name = "thmr_$counter"; timer_start($timer_name); // The twin of theme(). All rendering done through here. list($return, $meta) = call_user_func_array('devel_themer_theme_twin', $args); $time = timer_stop($timer_name); $skip_calls = array('hidden', 'form_element', 'placeholder'); if (devel_themer_enabled() && !empty($return) && !is_array($return) && !is_object($return) && devel_themer_user_access()) { list($prefix, $suffix) = devel_theme_call_marker($hook, $counter, 'func'); $start_return = substr($return, 0, 31); $start_prefix = substr($prefix, 0, 31); if ($start_return != $start_prefix && !in_array($hook, $skip_calls) && empty($GLOBALS['devel_themer_stop'])) { if ($hook == 'page') { $GLOBALS['devel_theme_calls']['page_id'] = $counter; // Stop logging theme calls after we see theme('page'). This prevents // needless logging of devel module's query log, for example. Other modules may set this global as needed. $GLOBALS['devel_themer_stop'] = TRUE; } else { $output = variable_get('devel_themer_no_whitespace', FALSE) ? ($prefix . $return . $suffix) : ($prefix. "\n ". $return. $suffix. "\n"); } if ($meta['type'] == 'func') { $name = $meta['used']; $used = $meta['used']; if (empty($meta['wildcards'])) { $meta['wildcards'][$hook] = ''; } $candidates = devel_themer_ancestry(array_reverse(array_keys($meta['wildcards']))); if (empty($meta['variables'])) { $variables = array(); } } else { $name = $hook; if (empty($suggestions)) { array_unshift($meta['suggestions'], $meta['used']); } $candidates = array_reverse(array_map('devel_themer_append_extension', $meta['suggestions'])); $used = $meta['template_file']; } $key = "thmr_$counter"; // This variable gets sent to the browser in Drupal.settings. $GLOBALS['devel_theme_calls'][$key] = array( 'name' => $name, 'type' => $meta['type'], 'duration' => $time['time'], 'used' => $used, 'candidates' => $candidates, 'preprocessors' => isset($meta['preprocessors']) ? $meta['preprocessors'] : array(), ); // This variable gets serialized and cached on the server. $GLOBALS['devel_themer_server'][$key] = array( 'variables' => $meta['variables'], 'type' => $meta['type'], ); } else { $output = $return; } } return isset($output) ? $output : $return; } function devel_themer_append_extension($string) { return $string. devel_themer_get_extension(); } /** * For given theme *function* call, return the ancestry of function names which could have handled the call. * This mimics the way the theme registry is built. * * @param array * A list of theme calls. * @return array() * An array of function names. **/ function devel_themer_ancestry($calls) { global $theme, $theme_engine, $base_theme_info; static $prefixes; if (!isset($prefixes)) { $prefixes[] = 'theme'; if (isset($base_theme_info)) { foreach ($base_theme_info as $base) { $prefixes[] = $base->name; } } $prefixes[] = $theme_engine; $prefixes[] = $theme; $prefixes = array_filter($prefixes); } foreach ($calls as $call) { foreach ($prefixes as $prefix) { $candidates[] = $prefix. '_'. $call; } } return array_reverse($candidates); } /** * An unfortunate copy/paste of theme(). This one is called by the devel_themer_catch_function() * and processes all theme calls but gives us info about the candidates, timings, etc. Without this twin, * it was impossible to capture calls to module owned templates (e.g. user_profile) and awkward to determine * which template was finally called and how long it took. * * @return array * A two element array. First element contains the HTML from the theme call. The second contains * a metadata array about the call. * **/ function devel_themer_theme_twin() { $args = func_get_args(); $hook = array_shift($args); static $hooks = NULL; if (!isset($hooks)) { init_theme(); $hooks = theme_get_registry(); } // Gather all possible wildcard functions. $meta['wildcards'] = array(); if (is_array($hook)) { foreach ($hook as $candidate) { $meta['wildcards'][$candidate] = FALSE; if (isset($hooks[$candidate])) { $meta['wildcards'][$candidate] = TRUE; break; } } $hook = $candidate; } // This should not be needed but some users are getting errors. See http://drupal.org/node/209929 if (!isset($hooks[$hook])) { return array('', $meta); } $info = $hooks[$hook]; $meta['hook'] = $hook; $meta['path'] = $info['theme path']; // Include a file if the theme function or preprocess function is held elsewhere. if (!empty($info['include files'])) { foreach ($info['include files'] as $include_file) { include_once($include_file); } } // Handle compatibility with theme_registry_alters to prevent failures. if (!empty($info['file'])) { static $included_files = array(); $include_file = $info['file']; if (!empty($info['path'])) { $include_file = $info['path'] .'/'. $include_file; } if (empty($included_files[$include_file])) { // Statically cache files we've already tried to include so we don't // run unnecessary file_exists calls. $included_files[$include_file] = TRUE; if (file_exists('./'. $include_file)) { include_once('./'. $include_file); } } } if (isset($info['devel_function_intercept'])) { // The theme call is a function. $output = call_user_func_array($info['devel_function_intercept'], $args); $meta['type'] = 'func'; $meta['used'] = $info['devel_function_intercept']; // Try to populate the keys of $args with variable names. Works on PHP5+. if (!empty($args) && class_exists('ReflectionFunction')) { $reflect = new ReflectionFunction($info['devel_function_intercept']); $params = $reflect->getParameters(); for ($i=0; $i < count($args); $i++) { // The implementation of the theme function may recieve less parameters than were passed to it. if ($i < count($params)) { $meta['variables'][$params[$i]->getName()] = $args[$i]; } else { // @TODO: Consider informing theme developers of theme functions that accept less parameters // than are passed to them. This could be disabled by default, with an option to // enable at admin/settings/devel_themer. Given this is a theme developer module // used by implementors of theme override functions, it would probably be a useful // default feature. $meta['variables'][] = $args[$i]; } } } else { $meta['variables'] = $args; } } else { // The theme call is a template. $meta['type'] = 'tpl'; $meta['used'] = str_replace($info['theme path'] .'/', '', $info['template']); $variables = array( 'template_files' => array() ); if (!empty($info['arguments'])) { $count = 0; foreach ($info['arguments'] as $name => $default) { $variables[$name] = isset($args[$count]) ? $args[$count] : $default; $count++; } } // default render function and extension. $render_function = 'theme_render_template'; $extension = '.tpl.php'; // Run through the theme engine variables, if necessary global $theme_engine; if (isset($theme_engine)) { // If theme or theme engine is implementing this, it may have // a different extension and a different renderer. if ($info['type'] != 'module') { if (function_exists($theme_engine .'_render_template')) { $render_function = $theme_engine .'_render_template'; } $extension_function = $theme_engine .'_extension'; if (function_exists($extension_function)) { $extension = $extension_function(); } } } if (isset($info['preprocess functions']) && is_array($info['preprocess functions'])) { // This construct ensures that we can keep a reference through // call_user_func_array. $args = array(&$variables, $hook); foreach ($info['preprocess functions'] as $preprocess_function) { if (function_exists($preprocess_function)) { call_user_func_array($preprocess_function, $args); } } } // Get suggestions for alternate templates out of the variables // that were set. This lets us dynamically choose a template // from a list. The order is FILO, so this array is ordered from // least appropriate first to most appropriate last. $suggestions = array(); if (isset($variables['template_files'])) { $suggestions = $variables['template_files']; } if (isset($variables['template_file'])) { $suggestions[] = $variables['template_file']; } if ($suggestions) { $template_file = drupal_discover_template($info['theme paths'], $suggestions, $extension); } if (empty($template_file)) { $template_file = $info['template'] . $extension; if (isset($info['path'])) { $template_file = $info['path'] .'/'. $template_file; } } $output = $render_function($template_file, $variables); $meta['suggestions'] = $suggestions; $meta['template_file'] = $template_file; $meta['variables'] = $variables; $meta['preprocessors'] = $info['preprocess functions']; } return array($output, $meta); } // We save the huge js array here instead of hook_footer so we can catch theme('page') function devel_themer_exit($destination = NULL) { if (!isset($destination) && !empty($GLOBALS['devel_theme_calls']) && $_SERVER['REQUEST_METHOD'] != 'POST') { // A random string that is sent to the browser. It enables the popup to retrieve params/variables from this request. $request_id = uniqid(rand()); // Write the variables information to the a file. It will be retrieved on demand via AJAX. // We used to write this to DB but was getting 'Warning: Got a packet bigger than 'max_allowed_packet' bytes' // Writing to temp dir means we don't worry about folder existence/perms and cleanup is free. devel_put_contents(file_directory_temp(). "/devel_themer_$request_id", serialize($GLOBALS['devel_themer_server'])); $GLOBALS['devel_theme_calls']['request_id'] = $request_id; $GLOBALS['devel_theme_calls']['devel_themer_uri'] = url("devel_themer/variables/$request_id"); print '\n"; } } function devel_theme_call_marker($name, $counter, $type) { $id = "thmr_". $counter; return array("", "\n"); } // just hand out next counter, or return current value function devel_counter($increment = TRUE) { static $counter = 0; if ($increment) { $counter++; } return $counter; } /** * Return the popup template * placed here for easy editing */ function devel_themer_popup() { $majorver = substr(VERSION, 0, strpos(VERSION, '.')); // add translatable strings drupal_add_js(array('thmrStrings' => array( 'themer_info' => t('Themer info'), 'toggle_throbber' => ' ', 'parents' => t('Parents: '), 'function_called' => t('Function called: '), 'template_called' => t('Theme hook called: '), 'candidate_files' => t('Candidate template files: '), 'preprocessors' => t('Preprocess functions: '), 'candidate_functions' => t('Candidate function names: '), 'drupal_api_docs' => t('link to Drupal API documentation'), 'source_link_title' => t('link to source code'), 'function_arguments' => t('Function Arguments'), 'template_variables' => t('Template Variables'), 'file_used' => t('File used: '), 'duration' => t('Duration: '), 'api_site' => variable_get('devel_api_site', 'http://api.drupal.org/'), 'drupal_version' => $majorver, 'source_link' => url('devel/source', array('query' => array('file' => ''))), )) , 'setting'); $title = t('Drupal Themer Information'); $intro = t('Click on any element to see information about the Drupal theme function or template that created it.'); $popup = <<
X $title
$intro
EOT; drupal_add_js(array('thmr_popup' => $popup), 'setting'); } // Temporary - for D6 only since we support PHP4 function devel_put_contents($n, $d, $flag = false) { if (function_exists('file_put_contents')){ file_put_contents($n, $d, $flag); } else { $mode = ($flag == FILE_APPEND || strtoupper($flag) == 'FILE_APPEND') ? 'a' : 'w'; $f = @fopen($n, $mode); if ($f === false) { return 0; } else { if (is_array($d)) $d = implode($d); fwrite($f, $d); fclose($f); } } } /** * Clean up the files we dropped in the temp dir in devel_themer_exit(). * * Limitation: one more devel_themer_exit() will run after this function is * called and drop one more file, since hook_exit() is called after the normal * page cycle. * * @return * void. */ function devel_themer_cleanup() { foreach (array_keys(file_scan_directory(file_directory_temp(), 'devel_themer_*', array('.', '..', 'CVS'), 0, FALSE)) as $file) { file_delete($file); } } /** * Implement hook_cron() for periodic cleanup. * * @return * void. */ function devel_themer_cron() { devel_themer_cleanup(); } /** * Determine whether the user has permission to access the information provided by this module. * * @param $account * (optional) The account to check, if not given use currently logged in user. * * @return * Boolean TRUE if the user has permission to access the information provided * by this module. * * @see user_access() */ function devel_themer_user_access($account = NULL) { // Users with access to devel information from the devel module should // automatically get access to the theme information as well. return (user_access('access devel information', $account) || user_access('access devel theme information', $account)); }