2021-02-05 10:37:56 +01:00

839 lines
25 KiB
Plaintext

<?php
/**
* @file
* Main module file for Modernizr
*/
// Regular expression to determine which version of Modernizr is installed.
define('MODERNIZR_VERSION_REGEX', '/([23]\.\d\.\d)/');
// Regular expression to detect valid Modernizr filenames.
define('MODERNIZR_FILENAME_REGEX', '/^modernizr[A-Za-z0-9\.-]*\.js$/');
// Our drupal_add_js() and libraries_load() calls use this value to maintain
// consistency between the position of the library and its inline settings.
define('MODERNIZR_SCRIPT_GROUP', JS_LIBRARY - 10);
define('MODERNIZR_SCRIPT_WEIGHT', -100);
// As of 7.x-3.3 we support footer scope, inlining, and defer.
define('MODERNIZR_TYPE_DEFAULT', 'sync');
define('MODERNIZR_SCOPE_DEFAULT', 'header');
// Determines the severity of administrative errors.
// As of 7.x-3.7 we switched the default to TRUE since yepnope is now included
// automatically, so a custom build is no longer absolutely necessary in order
// to begin using hook_modernizr_load() functionality.
define('MODERNIZR_QUIET_DEFAULT', TRUE);
// As of 7.x-3.5 we support Modernizr v3, which cannot include yepnope.js in a
// custom build. For backward compat purposes, we will enable this flag in an
// update hook automatically, but the default it to not include it.
define('MODERNIZR_YEPNOPE_DEFAULT', FALSE);
/**
* Default drupal_add_js settings. Used in multiple places.
*
* @return array An array of settings for drupal_add_js.
*/
function _modernizr_js_settings() {
return array(
'type' => (variable_get('modernizr_type', MODERNIZR_TYPE_DEFAULT) != 'inline') ? 'file' : 'inline',
'scope' => variable_get('modernizr_scope', MODERNIZR_SCOPE_DEFAULT),
'group' => MODERNIZR_SCRIPT_GROUP,
'weight' => MODERNIZR_SCRIPT_WEIGHT,
'every_page' => TRUE,
'preprocess' => 0,
'defer' => (variable_get('modernizr_type', MODERNIZR_TYPE_DEFAULT) == 'defer') ? TRUE : FALSE,
);
}
/**
* Implements hook_page_build().
*
* We used to use hook_init(), but that loads the JS files needlessly
* on AJAX requests, private file requests, etc.
*/
function modernizr_page_build(&$page) {
global $base_url;
$modernizr_js_settings = _modernizr_js_settings();
// Load Modernizr on the page by invoking our implementation of
// hook_libraries_info().
//
// We can only use this method when Libraries API 2.0 is installed. Since Libraries 1.0
// did not contain a function called libraries_load(), we must explicitly check for a
// valid function to avoid fatal errors.
//
// @see http://drupal.org/node/1919796
if (module_exists('libraries') && function_exists('libraries_load')) {
libraries_load('modernizr');
}
else {
// First, figure out if we're inlining.
if (in_array(variable_get('modernizr_type', MODERNIZR_TYPE_DEFAULT), array('sync', 'defer'))) {
// We are loading external script. Load file path.
$modernizr_file = modernizr_get_path();
}
else {
// We are inlining. Load contents of file instead of path.
$modernizr_file = file_get_contents(modernizr_get_path());
}
// With no Libraries API, load the regular way.
drupal_add_js(
$modernizr_file,
$modernizr_js_settings
);
}
// We want yepnope() commands to be issued immediately after the call
// to Modernizr so that they download while the page renders. The overrides
// to $inline_js_settings will format the output as inline JS.
if ($output = _modernizr_load_generate()) {
// Modernizr v3 removed the ability to include yepnope.js directly in the
// custom builds. To ensure that previous users of this module can continue
// without breaking changes, we need to load a copy of yepnope manually,
// which Modernizr detects and aliases to yepnope().
if ($output && variable_get('modernizr_cb_load', MODERNIZR_YEPNOPE_DEFAULT)) {
$yepnope_settings = $modernizr_js_settings;
$yepnope_settings['type'] = 'inline';
$yepnope_settings['weight'] = MODERNIZR_SCRIPT_WEIGHT - 1;
// yepnope.js
drupal_add_js(
file_get_contents(drupal_get_path('module', 'modernizr') . '/js/yepnope.js'),
$yepnope_settings
);
$inline_js_settings = $modernizr_js_settings;
$inline_js_settings['type'] = 'inline';
$inline_js_settings['weight'] = MODERNIZR_SCRIPT_WEIGHT + 1;
// yepnope() statements
drupal_add_js(
$output,
$inline_js_settings
);
}
// If there are yepnope commands being requested, but the module does not
// have yepnope enabled, warn the user in the console.
else if ($output && !variable_get('modernizr_cb_load', MODERNIZR_YEPNOPE_DEFAULT)) {
drupal_add_js('console.warn("The Modernizr module is receiving requests to use yepnope.js but that option is currently disabled. Please enable yepnope.js by loading ' . $base_url . '/admin/config/development/modernizr/settings' . '");', array('type' => 'inline'));
}
}
}
/**
* Implements hook_permission().
*/
function modernizr_permission() {
return array(
'administer modernizr' => array(
'title' => t('Administer Modernizr'),
'description' => t('Perform administration tasks for Modernizr.'),
),
);
}
/**
* Implements hook_menu().
*/
function modernizr_menu() {
$items = array();
// Rebuild Modernizr
$items['admin/config/development/modernizr/rebuild'] = array(
'title' => 'Rebuild Modernizr',
'description' => 'Queries Drupal for Modernizr dependencies and generates a custom link to the Modernizr builder.',
'page callback' => 'modernizr_generate_url',
'file' => 'modernizr.admin.inc',
'type' => MENU_DEFAULT_LOCAL_TASK,
'access arguments' => array('administer modernizr'),
);
// Module settings
$items['admin/config/development/modernizr/settings'] = array(
'title' => 'Modernizr settings',
'description' => 'Administrative settings for Modernizr module.',
'page callback' => 'drupal_get_form',
'page arguments' => array('modernizr_admin'),
'file' => 'modernizr.admin.inc',
'type' => MENU_LOCAL_TASK,
'access arguments' => array('administer modernizr'),
);
// Admin menu item (duplicate of "Rebuild Modernizr")
$items['admin/config/development/modernizr'] = array(
'title' => 'Modernizr',
'description' => 'Queries Drupal for Modernizr dependencies and generates a custom link to the Modernizr builder.',
'page callback' => 'modernizr_generate_url',
'file' => 'modernizr.admin.inc',
'type' => MENU_NORMAL_ITEM,
'access arguments' => array('administer modernizr'),
);
return $items;
}
/**
* Implements hook_libraries_info().
*
* @return array
*/
function modernizr_libraries_info() {
$modernizr_js_settings = _modernizr_js_settings();
$libraries = array();
$file_name = modernizr_get_filename();
// Define Modernizr within Libraries API
$libraries['modernizr'] = array(
'name' => t('Modernizr'),
'vendor url' => 'http://modernizr.com',
'download url' => 'http://modernizr.com/download/',
'version arguments' => array(
'file' => $file_name,
'pattern' => MODERNIZR_VERSION_REGEX,
),
'files' => array(
'js' => array(
$file_name => $modernizr_js_settings,
),
),
);
return $libraries;
}
/**
* Returns the full path of modernizr, along with the filename.
*
* @return string
*/
function modernizr_get_path() {
$path = &drupal_static(__FUNCTION__);
if ($path === NULL) {
// Get possible paths for the file.
$paths = _modernizr_get_paths();
// Scan directories for files
$path = _modernizr_scan_for_library($paths);
}
return $path;
}
/**
* Helper function to scan for acceptably named libraries
*/
function _modernizr_get_paths() {
$paths = &drupal_static(__FUNCTION__);
if ($paths === NULL) {
$paths = array();
foreach (_modernizr_library_search_paths() as $search_path) {
$library_path = $search_path . '/modernizr';
if (file_exists($library_path)) {
$paths[] = $library_path;
}
}
}
return $paths;
}
/**
* Get library search paths.
*
* Original logic was taken from Libraries 7.x-2.3 since it doesn't provide a
* way to look up its search paths.
*
* @see libraries_get_libraries()
*/
function _modernizr_library_search_paths() {
$searchdir = array();
$profile = drupal_get_path('profile', drupal_get_profile());
$config = conf_path();
// Similar to 'modules' and 'themes' directories in the root directory,
// certain distributions may want to place libraries into a 'libraries'
// directory in Drupal's root directory.
$searchdir[] = 'libraries';
// Always search sites/all/libraries.
$searchdir[] = 'sites/all/libraries';
// $profile should never be empty in a proper Drupal setup. Check to make sure
// it exists before adding path.
if ($profile) {
// Similar to 'modules' and 'themes' directories inside an installation
// profile, installation profiles may want to place libraries into a
// 'libraries' directory.
$searchdir[] = $profile . '/libraries';
}
// $config should never be empty in a proper Drupal setup. Check to make sure
// it exists before adding path.
if ($config) {
// Also search sites/<domain>/*.
$searchdir[] = $config . '/libraries';
}
return $searchdir;
}
/**
* Helper function to scan for acceptably named libraries
*/
function _modernizr_scan_for_library($paths) {
$path = '';
if (is_array($paths) && !empty($paths)) {
foreach ($paths as $p) {
if ($files = file_scan_directory($p, MODERNIZR_FILENAME_REGEX)) {
$path = reset($files)->uri;
break;
}
}
}
return $path;
}
/**
* Helper function to fetch the active Modernizr library.
*/
function modernizr_get_filename() {
// Get the full path to the library,
$full_path = modernizr_get_path();
// Break it up into its directories and file
$file_parts = explode('/', $full_path);
// Isolate the filename
$file_name = $file_parts[count($file_parts)-1];
return $file_name;
}
/**
* Guesses the modernizr library version.
*
* This function is using a regex, which assumes that the format of the version
* string won't change. If it changes, feel free to submit a bug report.
*
* @return mixed The version number if exists, or a boolean FALSE if it can't be
* determined.
*/
function modernizr_get_version($reset = FALSE) {
$version = &drupal_static(__FUNCTION__);
if ($version === NULL || $reset == TRUE) {
if ($cached = cache_get('modernizr_version') && isset($cached->data) && $reset != TRUE) {
$version = $cached->data;
}
else {
$version = FALSE;
$modernizr_path = modernizr_get_path();
if (file_exists($modernizr_path)) {
$modernizr = file_get_contents($modernizr_path);
$matches = array();
preg_match(MODERNIZR_VERSION_REGEX, $modernizr, $matches);
if (isset($matches[0])) {
$version = $matches[0];
if ($version) {
cache_set('modernizr_version', $version);
}
}
unset($modernizr);
}
}
}
return $version;
}
/**
* Implements MODULE_preprocess_html().
*/
function modernizr_preprocess_html(&$vars, $hook) {
// This will set up all of our tests for Modernizr.
modernizr_load_data();
}
/**
* A function to generate the load data from the current themes.
*
* Reads async-loaded CSS/JS from theme .info files. Stores info in variable.
* Prints yepnope() calls into drupal_add_js() as inline settings.
*
* @return array
*/
function modernizr_load_data() {
$load = &drupal_static(__FUNCTION__);
if (!isset($load)) {
// This is the first time this is called.
global $base_url, $base_theme_info, $theme_info;
$load = array();
$num_tests = 0;
// Make a list of base themes and the current theme.
$themes = $base_theme_info;
$themes[] = $theme_info;
foreach (array_keys($themes) as $key) {
$theme_path = dirname($themes[$key]->filename) . '/';
if (isset($themes[$key]->info['modernizr'])) {
// Loop through Modernizr calls and assemble Load variable.
foreach (array_keys($themes[$key]->info['modernizr']) as $test_key => $test) {
// If no tests are defined, simply add them as resources for loading them unconditionally.
if (is_numeric($test)) {
$load[] = array(_modernizr_sanitize_resource($themes[$key]->info['modernizr'][$test_key], $theme_path));
}
// Skip the ['tests'] variable because it is reserved for selecting
// specific tests that Modernizr must include.
elseif ($test != 'tests') {
// All other entries inside a theme's modernizr[] settings should be scanned
$load[$num_tests]['test'] = $test;
foreach (array_keys($themes[$key]->info['modernizr'][$test]) as $action) {
foreach ($themes[$key]->info['modernizr'][$test][$action] as $asset) {
// First figure out which property we're reading.
// callback/complete need different processing than yep/nope/both/load
$functions = array('callback', 'complete');
// Is this a function or a resource?
if (in_array($action, $functions)) {
// It's a function
$load[$num_tests][$action][] = _modernizr_sanitize_callback($asset);
}
else {
// It's a resource
$load[$num_tests][$action][] = _modernizr_sanitize_resource($asset, $theme_path);
}
}
}
$num_tests++;
}
}
}
}
}
return $load;
}
/**
* Helper function to render the yepnope() calls.
*/
function _modernizr_load_generate() {
$output = FALSE;
// Get yepnope() calls from the active theme.
$theme = modernizr_load_data();
// Collect data from modules that implement hook_modernizr_load().
$modules = modernizr_load_list();
// Combine the data from the .info file and the Drupal modules.
// Themes go first because they are more visual and in most cases
// it's probably best to load them first. Modules whose assets
// truly need to be loaded first have hook_modernizr_load_alter()
// at their disposal.
$test_objects = array_merge($theme, $modules);
// Build the yepnope() commands.
if (count($test_objects)) {
$num_tests = 0;
$items = array();
foreach ($test_objects as $load) {
// If test is defined, this entry will be an object.
if (isset($load['test'])) {
$item = '{' . "\n";
$item .= ' test: ' . $load['test'] . ',' . "\n";
// Print each action and its resources
$actions = array('yep', 'nope', 'both', 'load');
foreach ($actions as $action) {
if (isset($load[$action])) {
// Begin output for this action
$item .= ' ' . sprintf('%-4s', $action) . ': ';
// How many resources for this action?
if (count($load[$action]) == 1) {
// Single resource
$item .= "'" . $load[$action][0] . "',\n";
}
else {
// Multiple resources
$item .= '[';
foreach ($load[$action] as $resource) {
$item .= "'" . $resource . "',";
}
// Truncate last comma
$item = substr($item, 0, -1);
$item .= "],\n";
}
}
}
// Output these two properties without quotes around the output
$callbacks = array('callback', 'complete');
foreach ($callbacks as $action) {
if (isset($load[$action])) {
// Begin output for this action
$item .= ' ' . sprintf('%-4s', $action) . ': ';
// How many callbacks for this action?
if (count($load[$action]) == 1) {
// Single resource
$item .= $load[$action][0] . ",\n";
}
else {
// Multiple resources
$item .= '[';
foreach ($load[$action] as $callback) {
$item .= $callback . ",";
}
// Truncate last comma
$item = substr($item, 0, -1);
$item .= "],\n";
}
}
}
// Truncate last comma and newline
$item = substr($item, 0, -2);
$item .= "\n}";
$num_tests++;
}
// No test is defined, add the resource(s) to the loader unconditionally.
else {
$resources = array();
foreach ($load as $resource) {
$resources[] = "'" . $resource . "'";
}
$item = implode(",\n", $resources);
$num_tests++;
}
$items[] = $item;
}
$output .= 'yepnope(';
// Issue commands as array if there is more than resource to load.
$output .= ($num_tests > 1) ? '[' : '';
// Add commands.
$output .= implode(",\n", $items);
// Finally, close the yepnope() function parenthesis.
$output .= ($num_tests > 1) ? ']' : '';
$output .= ');';
}
return $output;
}
/**
* Implements MODULE_preprocess_maintenance_page().
*/
function modernizr_preprocess_maintenance_page(&$vars, $hook) {
modernizr_preprocess_html($vars, $hook);
}
/**
* Helper function to sanitize yepnope() callbacks
*/
function _modernizr_sanitize_callback($callback) {
global $base_url;
$output = '';
$function_regex = '/^function(\s)*\(\)(\s)*\{(.*)\}$/';
// Save the people who don't wrap their code in anonymous functions.
// Yes, an extra semi-colon has been added for safety :)
$output = (preg_match($function_regex, $callback)) ? $callback : 'function(){' . $callback . ';}';
return $output;
}
/**
* Helper function to sanitize yepnope() assets
*/
function _modernizr_sanitize_resource($resource, $theme_path) {
global $base_path, $base_url;
$output = '';
// If a path starts with 'sites/' or 'profiles/' we assume they know exactly
// where they're going. Otherwise, they seem like relative URLs so append
// theme path.
$output = (strpos($resource, 'sites/') === 0 || strpos($resource, 'profiles/') === 0) ? $base_path . $resource : $base_url . '/' . $theme_path . $resource;
return $output;
}
/**
* Helper function for hook_modernizr_info().
* Returns a Modernizr argument's type.
*/
function _modernizr_get_type($arg) {
$data = _modernizr_get_arg_info($arg, 'type');
// Since community-created detects are by far the most likely unknown entry,
// we assume that a value not found in modernizr.args.inc is a community detect.
// Note: 'tests' does NOT need t() because it is a machine value.
return $data ? $data : 'tests';
}
/**
* Helper function for hook_modernizr_info().
* Returns a Modernizr argument's description.
*/
function _modernizr_get_desc($arg) {
$data = _modernizr_get_arg_info($arg, 'desc');
// If we can't find a description, just admit it.
return $data ? $data : '<em>' . t('No description available.') . '</em>';
}
/**
* A helper function to get the information stored in modernizr.args.inc.
*
* @param string $arg
* The test machine name.
* @param string $type (default: 'desc')
* The data wanted, currently just 'desc' or 'type'.
* @return
* The data in the field, or FALSE if it doesn't exist.
*/
function _modernizr_get_arg_info($arg, $type = 'desc') {
static $loaded = FALSE;
if (!$loaded) {
$loaded = module_load_include('inc', 'modernizr', 'modernizr.args');
}
$data = _modernizr_args_return($arg);
// This data doesnt exist.
return ($data && isset($data[$type])) ? $data[$type] : FALSE;
}
/**
* Helper function to pulls all tests from the current modernizr.js
*/
function _modernizr_current_build() {
$tests = &drupal_static(__FUNCTION__);
if (!isset($tests)) {
$path = modernizr_get_path();
$path_parts = explode('/', $path);
$file = ($path) ? file_get_contents($path) : NULL;
$filename = $path_parts[count($path_parts)-1];
$tests = array();
// $matches holds two items:
// - [0] the full URL
// - [1] a string containing the args captured in the parens vvvv
$build_url = preg_match('/https?:\/\/modernizr.com\/download\/[#?]-(.*) /', $file, $matches);
// Turn URL args into test entries for Drupal module
if (isset($matches[1])) {
$args_and_prefix = explode(':', $matches[1]);
$build_args = explode('-', $args_and_prefix[0]);
foreach ($build_args as $arg) {
$tests[] = $arg;
}
}
else {
// Modernizr must not be downloaded, return null.
return NULL;
}
}
return $tests;
}
/**
* Asks other Drupal modules which Modernizr tests they need.
*
* @return array
*/
function modernizr_api_list() {
$tests = &drupal_static(__FUNCTION__);
if (!isset($tests)) {
// Grab all module implementations
// Note: this is a slightly augmented version of module_invoke_all(), so
// that we can know which module is providing which test.
$hook = 'modernizr_info';
foreach (module_implements($hook) as $module) {
$function = $module . '_' . $hook;
if (function_exists($function)) {
$result = call_user_func($function);
if (isset($result) && is_array($result)) {
$tests[$module] = $result;
}
}
}
// Grabbing the information with an hook_alter is not enough for themes
// because they will not all be active, nor their code added into the session.
$themes = list_themes();
$active_themes = array();
foreach ($themes as $theme_name => $theme) {
if ($theme->status == 1) {
$active_themes[$theme_name] = $theme;
if (isset($theme->base_themes)) {
foreach ($theme->base_themes as $base_theme_name => $base_theme) {
$active_themes[$base_theme_name] = $themes[$base_theme_name];
}
}
}
}
// We now go into every active theme and pull from the .info file the tests.
foreach ($active_themes as $active_theme) {
$data = drupal_parse_info_file($active_theme->filename);
if (isset($data['modernizr']) && isset($data['modernizr']['tests'])) {
// There are modernizr tests within this theme.
$theme_name = $data['name'];
$tests[$theme_name] = $data['modernizr']['tests'];
}
}
// The last thing we do is send it to have its data cleaned and organized.
$tests = _modernizr_api_list_clean($tests);
}
return $tests;
}
/**
* Cleans up the array of tests to be unified.
*
* @param $raw_tests array
* An array of tests provided by hook_modernizr_info.
* @return array
*/
function _modernizr_api_list_clean($raw_tests) {
$clean_tests = array();
foreach ($raw_tests as $module => $tests) {
foreach ($tests as $name => $data) {
// First, we check and correct if the tests have been added using indexed
// arrays, fixing the name variable.
if (is_int($name) && !is_array($data)) {
// The test is stored as a simple array, therefore the data is the name.
$name = $data;
$data = array();
}
elseif (is_int($name) && is_array($data)) {
// Still stored as a indexed array, but the data is an array.
$name = $data['name'];
}
// Now, we add these tests to our array of cleaned up data.
if (isset($clean_tests[$name])) {
// We already have the test, we are just going to add our module name.
$clean_tests[$name]['source'][] = $module;
}
else {
// The test has not been marked, we are adding it to the array.
$clean_tests[$name] = $data;
$clean_tests[$name]['source'] = array($module);
}
}
}
// Cleaning up the data to ensure all data we need is present.
foreach ($clean_tests as $name => $clean_test) {
$data = array(
'name' => $name,
'desc' => _modernizr_get_desc($name),
'docs' => '',
'camiuse' => '',
);
$clean_tests[$name] = array_merge($data, $clean_test);
}
return $clean_tests;
}
/**
* Implements hook_modernizr_info().
*
* This function implements our own hook to ensure that any custom Modernizr
* builds downloaded from either the settings screen or drush commands contain
* the essential components needed to support the module's functionality.
*
* html5shiv w/ printshiv
* - Includes some utility JS that allows IE to recognize HTML5 elements.
*/
function modernizr_modernizr_info() {
$items = array();
// If a site admin has chosen to require printshiv, add it to dependencies.
if (variable_get('modernizr_cb_printshiv', FALSE)) {
$items[] = 'printshiv';
}
return $items;
}
/**
* Asks other Drupal modules for yepnope() commands.
*
* @return array
*/
function modernizr_load_list($reset = FALSE) {
$load = &drupal_static(__FUNCTION__);
if (!isset($load) || $reset) {
$load = module_invoke_all('modernizr_load');
drupal_alter('modernizr_load', $load);
}
return $load;
}
/**
* Private function to look for missing Modernizr tests.
*/
function _modernizr_info_missing_tests() {
$requested_tests = modernizr_api_list();
$current_build = _modernizr_current_build();
$missing_tests = array();
if (is_null($current_build)) {
// There is no installed version of Modernizr. Return all tests.
return $requested_tests;
}
foreach($requested_tests as $test => $test_info) {
if (!in_array($test, $current_build)) {
$missing_tests[$test] = $test_info;
}
}
return $missing_tests;
}