type); } } } /** * Implements hook_nodeapi(). */ function nodewords_nodeapi(&$node, $op, $teaser = NULL, $page = NULL) { switch ($op) { case 'load': $output['nodewords']['metatags'] = nodewords_load_tags(array( 'type' => NODEWORDS_TYPE_NODE, 'id' => $node->nid, )); return $output; case 'insert': case 'update': if (isset($node->nodewords['metatags'])) { nodewords_save_tags($node->nodewords['metatags'], array( 'type' => NODEWORDS_TYPE_NODE, 'id' => $node->nid, )); } break; case 'delete': nodewords_delete_tags(array( 'type' => NODEWORDS_TYPE_NODE, 'id' => $node->nid, )); break; } } /** * Implements hook_perm(). */ function nodewords_perm() { return array('administer meta tags', 'edit meta tags'); } /** * Implements hook_preprocess_page(). */ function nodewords_preprocess_page(&$variables) { $output_tags = array(); $head_tags = variable_get('nodewords_head', array()); $options = nodewords_detect_type_id(); if ($options['type'] != NODEWORDS_TYPE_NONE && $options['type'] != NODEWORDS_TYPE_OFFLINE) { $options['output'] = 'head'; if ($options['type'] == NODEWORDS_TYPE_PAGER) { nodewords_load_all_includes('nodewords.tags.inc'); foreach (nodewords_get_possible_tags() as $name => $info) { if (empty($head_tags[$name])) { continue; } $bool = ( !empty($info['context']['allowed']) && in_array(NODEWORDS_TYPE_PAGER, $info['context']['allowed']) && function_exists($function = $info['callback'] . '_prepare') ); if ($bool) { $options['parameters'] = !empty($info['callback arguments']) ? $info['callback arguments'] : array(); $function($output_tags, array(), $options); } } } else { // Load the values from the database if ($options['type'] != NODEWORDS_TYPE_NODE || node_access('view', node_load($options['id']))) { $tags = nodewords_load_tags($options); } else { $tags = array(); } nodewords_load_all_includes('nodewords.tags.inc'); // Prepare the tags. foreach (nodewords_get_possible_tags() as $name => $info) { if (empty($head_tags[$name])) { continue; } if (function_exists($function = $info['callback'] . '_prepare')) { $options['parameters'] = !empty($info['callback arguments']) ? $info['callback arguments'] : array(); $function( $output_tags, _nodewords_tag_value($name, isset($tags[$name]) ? $tags[$name] : array(), $options + array('admin' => FALSE)), $options ); } } } nodewords_load_all_includes('nodewords.hooks.inc'); drupal_alter('nodewords_tags', $output_tags, $options); $output = _nodewords_output_tags($output_tags); drupal_alter('nodewords_tags_output', $output, $options); // Output the tags to the header. drupal_set_html_head($output); $variables['head'] = drupal_get_html_head(); } } /** * Implements hook_taxonomy(). */ function nodewords_taxonomy($op, $type, $array = NULL) { switch ($type) { case 'term': $id = $array['tid']; $type = NODEWORDS_TYPE_TERM; break; case 'vocabulary': $id = $array['vid']; $type = NODEWORDS_TYPE_VOCABULARY; break; default: return; } switch ($op) { case 'insert': case 'update': if (isset($array['nodewords']['metatags'])) { nodewords_save_tags($array['nodewords']['metatags'], array('type' => $type, 'id' => $id)); unset($array['nodewords']); } break; case 'delete': nodewords_delete_tags(array('type' => $type, 'id' => $id)); break; } } /** * Implements hook_user(). */ function nodewords_user($op, &$edit, &$account, $category = NULL) { switch ($op) { case 'load': $account->nodewords['metatags'] = nodewords_load_tags(array( 'type' => NODEWORDS_TYPE_USER, 'id' => $account->uid, )); break; case 'insert': case 'update': if (isset($edit['nodewords']['metatags'])) { nodewords_save_tags($edit['nodewords']['metatags'], array( 'type' => NODEWORDS_TYPE_USER, 'id' => $account->uid, )); } unset($edit['nodewords']); break; case 'delete': nodewords_delete_tags(array( 'type' => NODEWORDS_TYPE_USER, 'id' => $account->uid, )); break; } } /** * Add the tokens help to the form. * * Add the tokens help to the form passed as argument. * It is responsability of the calling function to verify that token.module is * enabled. * * @param &$form * The form to which the help text will be added. * @param $type * An array containing information about the object for which the meta tags * are being edited. */ function nodewords_add_tokens_help(&$form, $type) { // Always include the global context. $token_types[] = 'global'; switch ($type['type']) { case NODEWORDS_TYPE_DEFAULT: case NODEWORDS_TYPE_PAGE: $token_types[] = 'node'; $token_types[] = 'taxonomy'; $token_types[] = 'vocabulary'; $token_types[] = 'user'; break; case NODEWORDS_TYPE_CONTENT_TYPE: case NODEWORDS_TYPE_NODE: $token_types[] = 'node'; break; case NODEWORDS_TYPE_TERM: $token_types[] = 'taxonomy'; break; case NODEWORDS_TYPE_VOCABULARY: $token_types[] = 'vocabulary'; break; case NODEWORDS_TYPE_USER: $token_types[] = 'user'; break; } $form['token_help'] = array( '#title' => t('Replacement patterns'), '#type' => 'fieldset', '#collapsible' => TRUE, '#collapsed' => TRUE, '#description' => t('Use the raw versions of tokens to avoid problems with HTML entities.'), ); $form['token_help']['help'] = array( '#value' => theme('token_tree', $token_types), ); } /** * Verify the API version is one currently supported by Nodewords. * * @param $version * The version string as accepted by version_compare(). * * @return * TRUE, if the API version is currently supported by Nodewords. */ function nodewords_check_api_version($version) { return ( version_compare($version, NODEWORDS_MINIMUM_API_VERSION, '>=') && version_compare($version, NODEWORDS_API_VERSION, '<=') ); } /** * Delete tags from table. */ function nodewords_delete_tags($options = array()) { $options += _nodewords_get_default_metatags_type(); db_query("DELETE FROM {nodewords} WHERE type = %d AND id = %d", $options['type'], $options['id']); nodewords_load_all_includes('nodewords.hooks.inc'); module_invoke_all('nodewords_delete_tags', $options); } /** * Try to guess the type and the ID associated with the viewed page. * * @param $options * An array of parameters that describe the page to check. If the array is * is not passed, the function will return the type of the currently viewed * page. * * @return * An array containing information about the type of the page. */ function nodewords_detect_type_id($options = array()) { $options += array( 'page' => isset($_REQUEST['page']) ? $_REQUEST['page'] : -1, 'headers' => drupal_get_headers(), 'q' => $_GET['q'], ); $arg = explode('/', $options['q']) + array_fill(0, MENU_MAX_PARTS, NULL); // Start out with the default type. $result = array('type' => NODEWORDS_TYPE_DEFAULT, 'id' => 0); if (variable_get('site_offline', 0) && !user_access('administer site configuration')) { // Offline page always has the highest weight. $result['type'] = NODEWORDS_TYPE_OFFLINE; } elseif (preg_match('@HTTP/1\.[01]\x20+(403|404)[^a-zA-Z0-9]@', $options['headers'], $matches)) { // Error pages (403 or 404s) $result['type'] = NODEWORDS_TYPE_ERRORPAGE; $result['id'] = (int) $matches[1]; } elseif (!variable_get('nodewords_list_repeat', FALSE) && intval($options['page']) > 0) { $result['type'] = NODEWORDS_TYPE_PAGER; } elseif (!isset($arg[0])) { // @todo WTF is this doing? $result['type'] = NODEWORDS_TYPE_NONE; } else { // Allow other modules to alter the current page context. _nodewords_load_hook_files(); foreach (module_implements('nodewords_type_id') as $module) { $function = $module . '_nodewords_type_id'; $function($result, $arg); if ($result['type'] != NODEWORDS_TYPE_DEFAULT) { break; } } } return $result; } /** * Query all the modules implementing meta tags and return the list of meta tags. * * @return * An array containing the list of meta tags definitions. */ function nodewords_get_possible_tags() { static $tags_info = array(); nodewords_load_all_includes('nodewords.hooks.inc'); if (empty($tags_info)) { // Allow third-party modules to alter the meta tags list, or to add new // meta tags. foreach (module_implements('nodewords_tags_info') as $module) { $result = module_invoke($module, 'nodewords_tags_info'); if (isset($result) && is_array($result)) { $tags_info = array_merge($tags_info, $result); } } } drupal_alter('nodewords_tags_info', $tags_info); return $tags_info; } /** * Return the term object matching a term ID. This is a modified version of * taxonomy_get_term() which uses db_rewrite_sql(). * * @param $tid * A term's ID. * @param $uid * The user ID; if not passed, the function will use the global user ID. * * @return * A term object, or FALSE. Results are statically cached. */ function nodewords_get_term($tid, $uid = NULL) { global $user; static $terms = array(); if (!isset($uid)) { $uid = $user->uid; } if (!isset($terms[$uid][$tid])) { $query = db_query(db_rewrite_sql('SELECT * FROM {term_data} t WHERE t.tid = %d', 't', 'tid'), $tid); $terms[$uid][$tid] = db_fetch_object($query); } return !empty($terms[$uid][$tid]) ? $terms[$uid][$tid] : FALSE; } /** * Load an include file for each of the modules that have support for Nodewords. * * @param $file * The file to load; the name of the module will be preappended to the file * name passed. By default the file name is 'nodewords.inc'. */ function nodewords_load_all_includes($file = 'nodewords.inc') { foreach (module_implements('nodewords_api') as $module) { $info = module_invoke($module, 'nodewords_api'); if (isset($info['version']) && nodewords_check_api_version($info['version'])) { if (isset($info['path']) && $info['path'] == '') { // Special case: if the path is an empty string, the directory used will be // the directory include in the module directory. $include_path = drupal_get_path('module', $module) . '/includes/'; } elseif (isset($info['path'])) { $include_path = $info['path'] . '/'; } else { $include_path = drupal_get_path('module', $module) . '/'; } $include_file = $include_path . $module . '.' . $file; if (is_file($include_file)) { require_once $include_file; } elseif ($file != 'nodewords.inc' && is_file($include_file = $include_path . $module . '.' . 'nodewords.inc')) { require_once $include_file; } } } } /** * Load an include file. * * @param $module * The module for which the file is loaded. * * @param $file * The file to load; the name of the module will be preappended to the file * name passed. By default the file name is 'nodewords.inc'. * */ function nodewords_load_include($module, $file = 'nodewords.inc') { $info = module_invoke($module, 'nodewords_api'); if (isset($info['path']) && $info['path'] == '') { // Special case: if the path is an empty string, the directory used will be // the directory include in the module directory. $include_path = drupal_get_path('module', $module) . '/includes/'; } elseif (isset($info['path'])) { $include_path = $info['path'] . '/'; } else { $include_path = drupal_get_path('module', $module) . '/'; } $include_file = $include_path . $module . '.' . $file; if (is_file($include_file)) { require_once $include_file; } elseif ($file != 'nodewords.inc' && is_file($include_file = $include_path . $module . '.' . 'nodewords.inc')) { require_once $include_file; } else { return FALSE; } } /** * Load tags from table. * * @param $options * An array of options. * * @return * An array of meta tags data as saved in the database table. */ function nodewords_load_tags($options = array()) { $tags = array(); $options += _nodewords_get_default_metatags_type(); $tags_info = nodewords_get_possible_tags(); $result = db_query("SELECT * FROM {nodewords} WHERE type = %d AND id = %d", $options['type'], $options['id']); while ($row = db_fetch_object($result)) { if (isset($tags_info[$row->name])) { $tags[$row->name] = unserialize($row->content); } } if (empty($tags) && $options['type'] == NODEWORDS_TYPE_TERM) { $id = db_result(db_query_range('SELECT vid FROM {term_data} WHERE tid = %d', $options['id'], 0, 1)); if ($id !== FALSE) { $options['type'] = NODEWORDS_TYPE_VOCABULARY; $options['id'] = $id; return nodewords_load_tags($options); } } return $tags; } function nodewords_replace_tokens($content, $options = array()) { if (empty($content) || !variable_get('nodewords_enable_tokens', TRUE) || !module_exists('token')) { return $content; } // Always include the global context. $token_objects['global'] = NULL; $options += _nodewords_get_default_metatags_type(); // Allow other modules to alter the context used for tokens below if the // current context is not yet specific. switch ($options['type']) { case NODEWORDS_TYPE_DEFAULT: case NODEWORDS_TYPE_FRONTPAGE: case NODEWORDS_TYPE_PAGE: case NODEWORDS_TYPE_PAGER: $original_type = $options['type']; $arg = arg() + array_fill(0, MENU_MAX_PARTS, NULL); foreach (module_implements('nodewords_type_id') as $module) { $function = $module . '_nodewords_type_id'; $function($options, $arg); if ($options['type'] != $original_type) { break; } } break; } switch ($options['type']) { case NODEWORDS_TYPE_NODE: $token_objects['node'] = node_load($options['id']); break; case NODEWORDS_TYPE_TERM: $token_objects['taxonomy'] = taxonomy_get_term($options['id']); break; case NODEWORDS_TYPE_VOCABULARY: $token_objects['vocabulary'] = taxonomy_vocabulary_load($options['id']); break; case NODEWORDS_TYPE_USER: $token_objects['user'] = user_load($options['id']); break; } // Perform token replacement, making sure all tokens are replaced even if // there is no replacement value for a token. $content = token_replace_multiple($content, $token_objects, TOKEN_PREFIX, TOKEN_SUFFIX, array('clear' => TRUE)); return $content; } /** * Update or insert tags in the table. */ function nodewords_save_tags($tags, $options = array()) { $done = FALSE; $options += (_nodewords_get_default_metatags_type() + array('log_message' => TRUE)); $tags_info = nodewords_get_possible_tags(); $types_str = _nodewords_get_type_strings(); foreach ($tags as $name => $content) { if (isset($tags_info[$name])) { $content = serialize($content); $result = _nodewords_get_tags_data($name, $options); if ($result === FALSE) { $row = _nodewords_init_tags_data($name, $options); } else { $row = $result; } $row->content = $content; $ret = drupal_write_record('nodewords', $row, isset($row->mtid) ? 'mtid' : array()); if (!$done && $options['log_message'] && $ret) { watchdog('meta tags', 'Meta tags changed for type @type @id.', array( '@type' => isset($types_str[$options['type']]) ? $types_str[$options['type']] : t('unknown'), '@id' => $options['id'], )); $done = TRUE; } } } } /** * Return the form used to set the meta tags values. * * @param $type * An array containing the type of the object, and its ID. * * @param $tags * The meta tags array as returned by nodewords_load_tags(). * * @return * An array as requested by the form API. */ function nodewords_tags_edit_fields($type, $tags, $options = array()) { $form = array(); $tokens_support = FALSE; $tokens_type = array(); $options += array( 'admin' => FALSE, 'collapsed' => TRUE, 'collapsible' => TRUE, 'description' => '', 'fieldset' => FALSE, 'title' => t('Meta tags'), 'weight' => 20, ); $edit_tags = variable_get(!empty($options['admin']) ? 'nodewords_admin_edit' : 'nodewords_ui_edit', array()); $head_output = variable_get('nodewords_head', array()); $tokens_enabled = module_exists('token') && variable_get('nodewords_enable_tokens', TRUE); $type += _nodewords_get_default_metatags_type(); if (isset($options['tag options']) && is_array($options['tag options'])) { $tag_options = $options['tag options'] + $type; } else { $tag_options = $type; } nodewords_load_all_includes('nodewords.tags.inc'); nodewords_load_all_includes('nodewords.hooks.inc'); $filter_metatags = ($type['type'] != NODEWORDS_TYPE_DEFAULT); foreach (nodewords_get_possible_tags() as $name => $info) { $description = array(); $permission = TRUE; drupal_alter('nodewords_tags_permission', $permission, $type, $name, $info); if ($permission) { $bool = ( ( ( !empty($info['context']['allowed']) && is_array($info['context']['allowed']) && !in_array($type['type'], $info['context']['allowed']) ) || ( !empty($info['context']['denied']) && is_array($info['context']['denied']) && in_array($type['type'], $info['context']['denied']) ) ) ); if ($bool || ($filter_metatags && empty($edit_tags[$name]))) { continue; } if (!empty($info['tokens'])) { $description[] = ' ' . t('This meta tag supports tokens.') . ''; $tokens_support = TRUE; } if (empty($head_output[$name]) && user_access('administer meta tags')) { $description[] = ' ' . t('This meta tag has not been selected to be output in HTML.'); } if (function_exists($function = $info['callback'] . '_form')) { if (!empty($description)) { $tag_options['description'] = implode('', $description); } else { $tag_options['description'] = ''; } $tag_options['parameters'] = !empty($info['callback arguments']) ? $info['callback arguments'] : array(); $function($form_metatags, _nodewords_tag_value($name, isset($tags[$name]) ? $tags[$name] : array(), $type + array('admin' => $options['admin'])), $tag_options); } } } if (!empty($form_metatags) && $options['fieldset']) { $form['#tree'] = TRUE; $form['#type'] = 'fieldset'; $form['#title'] = $options['title']; $form['#description'] = $options['description']; $form['#collapsible'] = $options['collapsible']; $form['#collapsed'] = $options['collapsed']; $form['#weight'] = $options['weight']; $form['#group'] = 'additional_settings'; } if (!empty($options['first fields'])) { $form = $options['first fields'] + $form; } if (!empty($form_metatags)) { $form['metatags'] = $form_metatags; } if ($tokens_enabled && $tokens_support) { nodewords_add_tokens_help($form, $type); } if (!empty($options['last fields'])) { $form += $options['last fields']; } return $form; } /** * Remove the duplicates from a list of items separated from the separator, * preserving the order in which they appear. * @param $text * The string containing the list of items concatenated using $separator. * @param $separator * The string used to split the string into an array. A space will be appended * to the string before it is used to create the string from the array of * unique items found in the string passed as argument. * @param $max_items * The maximum number of items accepted in the returned array; the default * value is -1, which means no limit. * * @return * A string containing only unique items present in the string of concatenated * items. */ function nodewords_unique_values($text, $separator = ',', $max_items = -1) { $lc_values = array(); $unique_values = array(); if (empty($text)) { return ''; } foreach (array_filter(array_map('trim', explode($separator, $text))) as $item) { $lowercase = drupal_strtolower($item); if (!in_array($lowercase, $lc_values)) { $lc_values[] = $lowercase; $unique_values[] = $item; } } if ($max_items > 0) { $unique_values = array_slice($uniq_values, 0, $max_items); } return implode($separator, $unique_values); } /** * Return the absolute URL of a path. * * Return the absolute URL of a path built using the base URL saved in the * Drupal variable nodewords_base_url. * * @param $path * The path for which the absolute must be built. * @param $options * An array of options as used by url(). */ function nodewords_url($path, $options = array()) { $options += array( 'alias' => TRUE, 'absolute' => TRUE, 'base_url' => rtrim(variable_get('nodewords_base_url', ''), '/'), 'fragment' => '', 'query' => '', 'prefix' => '' ); if (empty($options['base_url'])) { $options['base_url'] = NULL; } return url($path, $options); } /** * Return the default values that identify the object associated with the meta tags. * * @return * An array containing the indexes 'type', and 'id'. */ function _nodewords_get_default_metatags_type() { return array('type' => NODEWORDS_TYPE_DEFAULT, 'id' => 0); } function _nodewords_get_tags_data($name, $options) { return db_fetch_object(db_query_range("SELECT * FROM {nodewords} WHERE type = %d AND id = %d AND name = '%s'", $options['type'], $options['id'], $name, 0, 1)); } function _nodewords_get_type_strings() { return array( NODEWORDS_TYPE_DEFAULT => t('default'), NODEWORDS_TYPE_ERRORPAGE => t('HTTP error page'), NODEWORDS_TYPE_NODE => t('node'), NODEWORDS_TYPE_PAGE => t('custom page'), NODEWORDS_TYPE_TERM => t('taxonomy term'), NODEWORDS_TYPE_USER => t('user profile'), NODEWORDS_TYPE_VOCABULARY => t('vocabulary'), ); } function _nodewords_init_tags_data($name, $options) { $row = new stdClass(); $row->type = $options['type']; $row->id = $options['id']; $row->name = $name; return $row; } /** * Load the files in the directory "includes" basing on the enabled modules. * */ function _nodewords_load_hook_files() { $dir = drupal_get_path('module', 'nodewords') . '/includes'; foreach (file_scan_directory($dir, '.*\.inc', array('.', '..', 'CVS'), 0, FALSE) as $filename => $info) { if (module_exists($info->name) && !module_hook($info->name, 'nodewords_type_id')) { module_load_include('inc', 'nodewords', 'includes/' . $info->name); } } nodewords_load_all_includes('nodewords.hooks.inc'); } /** * Render the meta tag values as HTML. * * @param $tags * An array of tags. * * @param $output_type * The type of output, 'head' (default), or 'update index'. * * @return * A string containing the HTML output for the META tag. */ function _nodewords_output_tags($tags, $output_type = 'head') { $output = array(); $tags_info = nodewords_get_possible_tags(); $weights = array(); foreach ($tags as $name => $content) { if (empty($content)) { continue; } $parts = explode(':', $name); $template = ''; if (!isset($parts[1])) { $parts[1] = $parts[0]; } $bool = ( isset($tags_info[$parts[0]]['templates']['head'][$parts[1]]) && ($meta_name = check_plain(decode_entities(strip_tags($parts[1])))) && ($meta_content = trim(check_plain(decode_entities(strip_tags(preg_replace('/(\s)\s+/', '$1', $content)))))) ); if ($bool) { $replace = array( '%name' => $meta_name, '%content' => $meta_content, '%attributes' => empty($tags_info[$parts[0]]['attributes'][$parts[1]]) ? '' : drupal_attributes($tags_info[$parts[0]]['attributes'][$parts[1]]), ); $template = $tags_info[$parts[0]]['templates']['head'][$parts[1]]; $weight = isset($tags_info[$parts[0]]['weight'][$parts[1]]) ? $tags_info[$parts[0]]['weight'][$parts[1]] : 0; switch ($template) { case NODEWORDS_META: $template = ''; break; case NODEWORDS_HTTP_EQUIV: $template = ''; break; case NODEWORDS_LINK_REL: $template = ''; break; case NODEWORDS_LINK_REV: $template = ''; break; default: if (!is_string($template)) { $template = ''; } break; } } if (!empty($template)) { $output[] = strtr($template, $replace); $weights[] = $weight; } } if (count($output)) { array_multisort($weights, $output); return implode("\n", $output); } else { return ''; } } function _nodewords_tag_value($tag, $value, $options = array()) { static $default = NULL; $options += _nodewords_get_default_metatags_type(); $id = $options['id']; $type = $options['type']; if (!isset($default[$type][$id])) { $default_values = array(); nodewords_load_all_includes('nodewords.hooks.inc'); drupal_alter('nodewords_default_value', $default_values, $options + array('phase' => 'pre_load')); if (!empty($default_values)) { $default[$type][$id] = $default_values; } else { $default[$type][$id] = nodewords_load_tags(); drupal_alter('nodewords_default_value', $default[$type][$id], $options + array('phase' => 'post_load')); } } if ($type == NODEWORDS_TYPE_DEFAULT) { $value = isset($default[$type][$id][$tag]) ? $default[$type][$id][$tag] : array(); } else { $tags_info = nodewords_get_possible_tags(); $variable = empty($options['admin']) ? 'nodewords_ui_use_default_value_' . $tag : 'nodewords_admin_use_default_value_' . $tag; switch (variable_get($variable, 'empty')) { case 'empty': $bool = ( isset($tags_info[$tag]['callback']) && function_exists($function = $tags_info[$tag]['callback'] . '_is_empty') ); if ($bool && $function($value)) { $value = isset($default[$type][$id][$tag]) ? $default[$type][$id][$tag] : array(); } elseif (!$bool && empty($value['value'])) { $value = isset($default[$type][$id][$tag]) ? $default[$type][$id][$tag] : array(); } break; case 'always': $value = isset($default[$type][$id][$tag]) ? $default[$type][$id][$tag] : array(); break; } } return $value; }