diff --git CHANGELOG.txt CHANGELOG.txt index 266e94e..02e1936 100644 --- CHANGELOG.txt +++ CHANGELOG.txt @@ -137,6 +137,13 @@ Drupal 7.0, xxxx-xx-xx (development version) - Added RDF support: * Modules can declare RDF namespaces which are serialized in the tag for RDFa support. +- Search engine optimization and web linking: + * Added a rel="canonical" link on node and comment pages to prevent + duplicate content indexing by search engines. + * Added default a rel="shortlink" link on node and comment pages + that advertises a short link as an alternative to third-party services. + * drupal_set_html_head now stores structured data for link, meta, and + other elements, allowing it to be altered by modules before rendering. - Field API: * Custom data fields may be attached to nodes, users, comments and taxonomy terms. diff --git includes/batch.inc includes/batch.inc index f626e31..d481469 100644 --- includes/batch.inc +++ includes/batch.inc @@ -190,7 +190,14 @@ function _batch_progress_page_nojs() { } $url = url($batch['url'], array('query' => array('id' => $batch['id'], 'op' => $new_op))); - drupal_add_html_head(''); + $element = array( + '#tag' => 'meta', + '#attributes' => array( + 'http-equiv' => 'Refresh', + 'content' => '0; URL=' . $url, + ), + ); + drupal_add_html_head($element, 'batch_progress_meta_refresh'); return theme('progress_bar', $percentage, $message); } diff --git includes/common.inc includes/common.inc index 680683c..7d53b1d 100644 --- includes/common.inc +++ includes/common.inc @@ -281,22 +281,71 @@ function drupal_get_rdf_namespaces() { * Add output to the head tag of the HTML page. * * This function can be called as long the headers aren't sent. + * + * @param $data + * A renderable array. If the '#type' key is not set then 'xhtml_tag' will be + * added as the default '#type'. + * @param $key + * A unique string key identifying the data. Required if $data is not NULL. + * + * @return + * An array of all the stored HEAD elements. */ -function drupal_add_html_head($data = NULL) { - $stored_head = &drupal_static(__FUNCTION__, ''); +function drupal_add_html_head($data = NULL, $key = NULL) { + $stored_head = &drupal_static(__FUNCTION__); - if (!is_null($data)) { - $stored_head .= $data . "\n"; + if (!isset($stored_head)) { + // Make sure the defaults, including Content-Type, come first. + $stored_head = _drupal_default_html_head(); + } + + if (isset($data) && isset($key)) { + if (!isset($data['#type'])) { + $data['#type'] = 'xhtml_tag'; + } + $stored_head[$key] = $data; } return $stored_head; } /** - * Retrieve output to be displayed in the head tag of the HTML page. + * Returns elements always displayed in the HEAD tag of the HTML page. + */ +function _drupal_default_html_head() { + // Add default elements. Make sure the Content-Type comes first because the + // IE browser may be vulnerable to XSS via encoding attacks from any content + // that comes before this META tag, such as a TITLE tag. + $elements['system_meta_content_type'] = array( + '#type' => 'xhtml_tag', + '#tag' => 'meta', + '#attributes' => array( + 'http-equiv' => 'Content-Type', + 'content' => 'text/html; charset=utf-8', + ), + ); + // Get the major version. + list($version, ) = explode('.', VERSION); + // Show Drupal and the major version number in the META GENERATOR tag. + $elements['system_meta_generator'] = array( + '#type' => 'xhtml_tag', + '#tag' => 'meta', + '#attributes' => array( + 'name' => 'Generator', + 'content' => 'Drupal ' . $version . ' (http://drupal.org)', + ), + ); + // Send Drupal and the major version number in the HTTP headers. + $elements['system_meta_generator']['#attached']['drupal_set_header'][] = array('X-Generator', 'Drupal ' . $version . ' (http://drupal.org)'); + return $elements; +} + +/** + * Retrieve output to be displayed in the HEAD tag of the HTML page. */ function drupal_get_html_head() { - $output = "\n"; - return $output . drupal_add_html_head(); + $head_elements = drupal_add_html_head(); + drupal_alter('html_head', $head_elements); + return drupal_render($head_elements); } /** @@ -319,7 +368,7 @@ function drupal_clear_path_cache() { function drupal_add_feed($url = NULL, $title = '') { $stored_feed_links = &drupal_static(__FUNCTION__, array()); - if (!is_null($url) && !isset($stored_feed_links[$url])) { + if (isset($url)) { $stored_feed_links[$url] = theme('feed_icon', $url, $title); drupal_add_link(array('rel' => 'alternate', @@ -2481,6 +2530,28 @@ function url($path = NULL, array $options = array()) { } /** + * Format an attribute string for a HTTP header. + * + * @param $attributes + * An associative array of attributes. + * + * @return + * A ; separated string ready for insertion in a HTTP header. No escaping is + * performed for HTML entities, so this string is not safe to be printed. + * + * @see drupal_set_header() + */ +function drupal_header_attributes(array $attributes = array()) { + foreach ($attributes as $attribute => &$data) { + if (is_array($data)) { + $data = implode(' ', $data); + } + $data = $attribute . '="' . $data . '"'; + } + return $attributes ? ' ' . implode('; ', $attributes) : ''; +} + +/** * Format an attribute string to insert in a tag. * * Each array key and its value will be formatted into an HTML attribute string. @@ -2692,12 +2763,31 @@ function base_path() { } /** - * Add a tag to the page's HEAD. + * Add a LINK tag with a distinct REL attribute to the page's HEAD. * - * This function can be called as long the HTML header hasn't been sent. - */ -function drupal_add_link($attributes) { - drupal_add_html_head('\n"); + * This function can be called as long the HTML header hasn't been sent, + * which on normal pages is up through the preprocess step of theme('html'). + * Adding a link will overwrite a prior link with the exact same REL and HREF + * attributes. + * + * @param $attributes + * Associative array of element attributes including 'href' and 'rel'. + * @param $header + * Optional flag to determine if a HTTP Link: header should be sent. + */ +function drupal_add_link($attributes, $header = FALSE) { + $element = array( + '#tag' => 'link', + '#attributes' => $attributes, + ); + + if ($header) { + // Set a HTTP Link: header also. + $href = '<' . check_plain($attributes['href']) . '>;'; + unset($attributes['href']); + $element['#attached']['drupal_set_header'][] = array('Link', $href . drupal_header_attributes($attributes), TRUE); + } + drupal_add_html_head($element, 'drupal_add_link:' . $attributes['rel'] . ':' . $attributes['href']); } /** @@ -2910,7 +3000,16 @@ function drupal_get_css($css = NULL) { break; case 'external': // Preprocessing for external CSS files is ignored. - $external_css .= '' . "\n"; + $element = array( + '#tag' => 'link', + '#attributes' => array( + 'type' => "text/css", + 'rel' => "stylesheet", + 'media' => $item['media'], + 'href' => $item['data'], + ), + ); + $external_css .= theme('xhtml_tag', $element) . "\n"; break; } } @@ -2921,7 +3020,16 @@ function drupal_get_css($css = NULL) { // starting with "ad*". $filename = 'css_' . md5(serialize($items) . $query_string) . '.css'; $preprocess_file = file_create_url(drupal_build_css_cache($items, $filename)); - $rendered_css['preprocess'] .= '' . "\n"; + $element = array( + '#tag' => 'link', + '#attributes' => array( + 'type' => "text/css", + 'rel' => "stylesheet", + 'media' => $media, + 'href' => $preprocess_file, + ), + ); + $rendered_css['preprocess'] .= theme('xhtml_tag', $element) . "\n"; } } // Enclose the inline CSS with the style tag if required. @@ -4868,6 +4976,9 @@ function drupal_common_theme() { 'indentation' => array( 'arguments' => array('size' => 1), ), + 'xhtml_tag' => array( + 'arguments' => array('element' => NULL), + ), // from pager.inc 'pager' => array( 'arguments' => array('tags' => array(), 'element' => 0, 'parameters' => array(), 'quantity' => 9), diff --git includes/theme.inc includes/theme.inc index e300eb6..6156882 100644 --- includes/theme.inc +++ includes/theme.inc @@ -1869,6 +1869,20 @@ function theme_feed_icon($url, $title) { } /** + * Generate the output for a generic XHTML tag with attributes. + * + * @ingroup themeable + */ +function theme_xhtml_tag(array $element) { + if (!isset($element['#value'])) { + return '<' . $element['#tag'] . drupal_attributes($element['#attributes']) . " />\n"; + } + else { + return '<' . $element['#tag'] . drupal_attributes($element['#attributes']) . '>' . $element['#value'] . '\n"; + } +} + +/** * Returns code that emits the 'more' link used on blocks. * * @param $url @@ -2158,7 +2172,7 @@ function template_preprocess_html(&$variables) { if (theme_get_setting('toggle_favicon')) { $favicon = theme_get_setting('favicon'); $type = theme_get_setting('favicon_mimetype'); - drupal_add_html_head(''); + drupal_add_link(array('rel' => 'shortcut icon', 'href' => check_url($favicon), 'type' => $type)); } // Construct page title. @@ -2327,7 +2341,7 @@ function template_preprocess_maintenance_page(&$variables) { if (theme_get_setting('toggle_favicon')) { $favicon = theme_get_setting('favicon'); $type = theme_get_setting('favicon_mimetype'); - drupal_add_html_head(''); + drupal_add_link(array('rel' => 'shortcut icon', 'href' => check_url($favicon), 'type' => $type)); } global $theme; diff --git modules/comment/comment.module modules/comment/comment.module index 0f94475..0ed8a4d 100644 --- modules/comment/comment.module +++ modules/comment/comment.module @@ -349,9 +349,6 @@ function comment_permalink($comment) { $_GET['q'] = 'node/' . $node->nid; $_GET['page'] = $page; - // Set the node path as the canonical URL to prevent duplicate content. - drupal_add_link(array('rel' => 'canonical', 'href' => url('node/' . $node->nid))); - // Return the node view, this will show the correct comment in context. return menu_execute_active_handler('node/' . $node->nid); } diff --git modules/node/node.module modules/node/node.module index 8de33ee..2bd1a6a 100644 --- modules/node/node.module +++ modules/node/node.module @@ -1986,6 +1986,10 @@ function node_page_default() { */ function node_page_view($node) { drupal_set_title($node->title); + // Set the node path as the canonical URL to prevent duplicate content. + drupal_add_link(array('rel' => 'canonical', 'href' => url('node/' . $node->nid)), TRUE); + // Set the non-aliased path as a default shortlink. + drupal_add_link(array('rel' => 'shortlink', 'href' => url('node/' . $node->nid, array('alias' => TRUE))), TRUE); return node_show($node); } diff --git modules/openid/tests/openid_test.module modules/openid/tests/openid_test.module index a2891f2..4a20ded 100644 --- modules/openid/tests/openid_test.module +++ modules/openid/tests/openid_test.module @@ -97,7 +97,14 @@ function openid_test_yadis_x_xrds_location() { * Menu callback; regular HTML page with element. */ function openid_test_yadis_http_equiv() { - drupal_add_html_head(''); + $element = array( + '#tag' => 'meta', + '#attributes' => array( + 'http-equiv' => 'X-XRDS-Location', + 'content' => url('openid-test/yadis/xrds', array('absolute' => TRUE)), + ), + ); + drupal_add_html_head($element, 'openid_test_yadis_http_equiv'); return t('This page includes a <meta equiv=...> element containing the URL of an XRDS document.'); } @@ -105,7 +112,7 @@ function openid_test_yadis_http_equiv() { * Menu callback; regular HTML page with OpenID 1.0 element. */ function openid_test_html_openid1() { - drupal_add_html_head(''); + drupal_add_link(array('rel' => 'openid.server', 'href' => url('openid-test/endpoint', array('absolute' => TRUE)))); return t('This page includes a <link rel=...> element containing the URL of an OpenID Provider Endpoint.'); } @@ -113,7 +120,7 @@ function openid_test_html_openid1() { * Menu callback; regular HTML page with OpenID 2.0 element. */ function openid_test_html_openid2() { - drupal_add_html_head(''); + drupal_add_link(array('rel' => 'openid2.provider', 'href' => url('openid-test/endpoint', array('absolute' => TRUE)))); return t('This page includes a <link rel=...> element containing the URL of an OpenID Provider Endpoint.'); } diff --git modules/simpletest/tests/browser_test.module modules/simpletest/tests/browser_test.module index f433312..df5a265 100644 --- modules/simpletest/tests/browser_test.module +++ modules/simpletest/tests/browser_test.module @@ -57,7 +57,14 @@ function browser_test_print_post_form_submit($form, &$form_state) { function browser_test_refresh_meta() { if (!isset($_GET['refresh'])) { $url = url('browser_test/refresh/meta', array('absolute' => TRUE, 'query' => array('refresh' => 'true'))); - drupal_add_html_head(''); + $element = array( + '#tag' => 'meta', + '#attributes' => array( + 'http-equiv' => 'Refresh', + 'content' => '0; URL=' . $url, + ) + ); + drupal_add_html_head($element, 'browser_test_refresh_meta'); return ''; } echo 'Refresh successful'; diff --git modules/system/system.api.php modules/system/system.api.php index 1ebd7fe..20f3a83 100644 --- modules/system/system.api.php +++ modules/system/system.api.php @@ -2256,6 +2256,27 @@ function hook_drupal_goto_alter(array $args) { } /** + * Alter XHTML HEAD tags before they are rendered by drupal_get_html_head(). + * + * Elements available to be altered are only those added using + * drupal_add_link() or drupal_add_html_head(). CSS and JS files are handled + * using drupal_add_css() and drupal_add_js(), so the head links for those + * files will not appear in the $head_elements array. + * + * @param $head_elements + * An array of renderable elements. Generally the values of the #attributes + * array will be the most likely target for changes. + */ +function hook_html_head_alter(&$head_elements) { + foreach($head_elements as $key => $element) { + if (isset($element['#attributes']['rel']) && $element['#attributes']['rel'] == 'canonical') { + // I want a custom canonical url. + $head_elements[$key]['#attributes']['href'] = mymodule_canonical_url(); + } + } +} + +/** * Alter MIME type mappings used to determine MIME type from a file extension. * * This hook is run when file_mimetype_mapping() is called. It is used to diff --git modules/system/system.module modules/system/system.module index b617862..aee96f6 100644 --- modules/system/system.module +++ modules/system/system.module @@ -194,12 +194,6 @@ function system_theme() { 'system_powered_by' => array( 'arguments' => array('image_path' => NULL), ), - 'meta_generator_html' => array( - 'arguments' => array('version' => NULL), - ), - 'meta_generator_header' => array( - 'arguments' => array('version' => NULL), - ), 'system_compact_link' => array(), 'system_run_cron_image' => array( 'arguments' => array('image_path' => NULL), @@ -300,6 +294,12 @@ function system_element_info() { '#items' => array(), ); + $types['xhtml_tag'] = array( + '#theme' => 'xhtml_tag', + '#attributes' => array(), + '#value' => NULL, + ); + // Input elements. $types['submit'] = array( '#input' => TRUE, @@ -2715,24 +2715,6 @@ function theme_system_compact_link() { } /** - * Send Drupal and the major version number in the META GENERATOR HTML. - * - * @ingroup themeable - */ -function theme_meta_generator_html($version = VERSION) { - drupal_add_html_head(''); -} - -/** - * Send Drupal and the major version number in the HTTP headers. - * - * @ingroup themeable - */ -function theme_meta_generator_header($version = VERSION) { - drupal_add_http_header('X-Generator', 'Drupal ' . $version . ' (http://drupal.org)'); -} - -/** * Implement hook_image_toolkits(). */ function system_image_toolkits() {