'. t('On this page you can configure which blocks should be provided on a per-content-type basis. If you enabled a content type, please make sure to provided a block title.') .'

'; $output .= '

'. t('The Limit field allows you to provide a maximum number of nodes to be displayed for that block.') .'

'; $output .= '

'. t('The Block Header Text field allows you to provide some text which can appear at the top of the block - good for explaining to the user what the block is.') .'

'; return $output; } } /** * Implementation of hook_perm(). */ function relevant_content_perm() { return array('administer relevant content'); } /** * Implementation of hook_menu(). */ function relevant_content_menu() { // Rebuild the settings when the menu gets rebuilt - this is the only way to // clear the settings cache on a module submit relevant_content_get_settings(NULL, TRUE); $items = array(); $admin_base = array( 'file' => 'relevant_content.admin.inc', 'access arguments' => array('administer relevant content'), ); $items['admin/settings/relevant_content'] = array( 'title' => 'Relevant Content', 'description' => 'Configure the sites relevant content blocks.', 'page callback' => 'relevant_content_admin', ) + $admin_base; $items['admin/settings/relevant_content/overview'] = array( 'title' => 'Overview', 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -10, ) + $admin_base; $items['admin/settings/relevant_content/add'] = array( 'title' => 'Add', 'page callback' => 'drupal_get_form', 'page arguments' => array('relevant_content_admin_block_form'), 'type' => MENU_LOCAL_TASK, ) + $admin_base; $items['admin/settings/relevant_content/%relevant_content_block/%relevant_content_admin_op'] = array( 'title callback' => 'relevant_content_admin_block_operator_title', 'title arguments' => array(3, 4), 'page callback' => 'relevant_content_admin_block_operator_callback', 'page arguments' => array(3, 4), 'type' => MENU_CALLBACK, ) + $admin_base; return $items; } /** * Relevant Content Block loader callback for the Menu API */ function relevant_content_block_load($delta) { return relevant_content_get_settings($delta); } /** * Relevant Content admin op loader callback for the Menu API * This allows a single Admin URL to serve several operations on a block */ function relevant_content_admin_op_load($op) { $ret = array('op' => $op); switch ($op) { case 'edit' : $ret['title'] = t('Edit'); $ret['callback'] = 'relevant_content_admin_block_form'; break; case 'delete' : $ret['title'] = t('Delete'); $ret['callback'] = 'relevant_content_admin_delete_confirm'; break; case 'revert' : $ret['title'] = t('Revert'); $ret['callback'] = 'relevant_content_admin_revert_confirm'; break; case 'enable' : $ret['title'] = t('Enable'); $ret['callback'] = 'relevant_content_admin_enable_confirm'; break; case 'disable' : $ret['title'] = t('Disable'); $ret['callback'] = 'relevant_content_admin_disable_confirm'; break; default : return FALSE; } return $ret; } /** * Admin block operation title callback */ function relevant_content_admin_block_operator_title($block, $op) { return t('!op Relevant Content Block - @id', array('!op' => $op['title'], '@id' => $block['id'])); } /** * Implementation of hook_block(). */ function relevant_content_block($op = 'list', $delta = 0, $edit = array()) { switch ($op) { case 'list' : $blocks = array(); $settings = relevant_content_get_settings(); if (!empty($settings)) { foreach ($settings as $block) { if ($block['status'] == RELEVANT_CONTENT_STATUS_ENABLED) { $blocks[$block['id']] = array( 'info' => t('Relevant Content: @title', array('@title' => $block['id'])), 'cache' => BLOCK_CACHE_PER_ROLE | BLOCK_CACHE_PER_PAGE, 'visibility' => 1, 'pages' => 'node/*', ); } } } return $blocks; case 'view' : // Load the block for the delta (id) $block = relevant_content_get_settings($delta); // If we have no block - it could be a bad delta passed in. Abort here. if (!$block) { return; } //Get the terms for the current page using a little reusable wrapper function $terms = relevant_content_get_page_terms(); //If there are no terms, not a lot of point in continuing if (empty($terms)) { return; } //Filter out the terms which are not in a selected vocabulary foreach ($terms as $key => $term) { if (isset($block['vocabs'][$term->vid])) { $terms[$key] = $term->tid; } else { unset($terms[$key]); } } //Again - if there are no terms, no need to continue! if (empty($terms)) { return; } //Create a node exclusion list - this will exclude the currently viewed node - if applicable. //This stops the currently viewed node appearing top of a list - afterall, it IS the most relevant! $exclude = array(); if ($node = menu_get_object()) { $exclude[] = $node->nid; } // Build a list of relevant nodes if ($nodes = relevant_content_get_nodes($block['types'], $terms, $exclude, $block['max_items'])) { // Return a block. // See @template_preprocess_relevant_content_block() return array( 'subject' => t('Relevant Content'), 'content' => theme('relevant_content_block', $nodes, $block), ); } break; } } /** * Handy wrapper function to find the terms for the current page */ function relevant_content_get_page_terms($node = NULL) { /** * If we have passed a node in, check if this node has taxonomy and use that. If not, try to load the terms using taxonomy_node_get_terms. * This is a rare situation, but sometimes happens if your module has a lower weight than taxonomy so on node_load, you get the node object pre-taxonomy. This happens with CCK Fields... */ if ($node) { // Use the node's terms.... if (isset($node->taxonomy) && is_array($node->taxonomy)) { $terms = $node->taxonomy; } // If we have a revision ID on the node, then we can try to load through taxonomy node get terms... elseif (isset($node->vid)) { $terms = taxonomy_node_get_terms($node); } } // If the URL is node/% then we can use Drupal 6's new menu_get_object. This method has it's risks and should be used with care. It is possible to end up in an infinit loop with one loading cycle invoking the next... elseif (arg(0) == 'node' && is_numeric(arg(1))) { $node = menu_get_object(); $terms = taxonomy_node_get_terms($node); } // Fall back to the term_cache if none of the above worked else { $terms = relevant_content_term_cache(); } // Provide a hook_relevant_content_terms where other modules can change the relevant terms if needed... drupal_alter('relevant_content_terms', $terms); return $terms; } /** * Implementation of hook_theme(). */ function relevant_content_theme($existing, $type, $theme, $path) { return array( 'relevant_content_block' => array( 'arguments' => array('nodes' => array(), 'block' => array()), 'template' => 'relevant_content_block', ), 'relevant_content_admin_overview' => array( 'arguments' => array('rows' => array()), 'template' => 'relevant_content_admin_overview', ), ); } /** * Theme preprocess function for rendering the relevant nodes into a block. * * This is provided so that an item list is the default, however a themer can * easily override this to make a teaser list or table. * * @param $vars * Contains the variables for the theme function, including: * $vars['nodes'] - An associative array of node information * $vars['block'] - the block being rendered */ function template_preprocess_relevant_content_block(&$vars) { // Get the definition of the block we are rendering $block = &$vars['block']; // Set an "is_empty" flag $vars['is_empty'] = empty($vars['nodes']); // Default to "link" type $type = 'link'; // Check tokens is enabled; it's optional. Also check we have token settings if (module_exists('token') && !empty($block['token_settings'])) { // Cleanup the token pattern // Cleanup the token pattern. See @relevant_content_filter_xss($string) // Unfortunately, we cannot 'clean up' the token pattern once due to the // bug with filter_xss on '[node:url]' which breaks the token. $token_pattern = trim($block['token_settings']); // If the token pattern is not empty, switch to tokens mode if (!empty($token_pattern)) { $type = 'tokens'; } } foreach ($vars['nodes'] as $nid => $node) { // If we're a link, default to a hyperlink - otherwise we should use tokens. switch ($type) { default : case 'link' : $node['output'] = l($node['title'], 'node/'. $node['nid']); break; case 'tokens' : $objects = array('global' => NULL, 'node' => node_load($node['nid'])); $node['output'] = token_replace_multiple($token_pattern, $objects); $node['output'] = relevant_content_filter_xss($node['output']); break; } // Build some classes for the node link row $node['classes'] = array( 'relevant-content-item', 'relevant-node-'. (int)$node['nid'], 'relevant-node-type-'. check_plain($node['type']), ); // Store the updated node over the old one $vars['nodes'][$nid] = $node; } // Build the header text - defaults to empty $vars['header'] = ''; if (!empty($block['header_text'])) { $vars['header'] = check_markup($block['header_text'], $block['header_format'], FALSE); } // Add a template suggestion; relevant_content_block-{block-id}.tpl.php $vars['template_files'][] = 'relevant_content_block-'. $block['id']; } /** * Function to get a set of nodes. * * This returns a set of nodes based on the provided type and array of term * ID's. * * @param $types * Array representing the node types * @param $terms * Array of Term ID's * @param $exclude * Array - Optional: An array of Node ID's to exclude. Useful for excluding the node you might be comparing to currently. Default: No exclusions. * @param $limit * Integer - Optional: Integer controlling the maximum number of nodes returned. Default: 5 * @param $languages * Array - Optional: An array of languages to restrict nodes to. * An empty string in the array corresponds to Language Neutral nodes. * An empty array will include all nodes regardless of language. * * @return mixed * FALSE if no result or error or an associative array with node ID's as keys and the value's as arrays of nid's, vid's, title's, type's & term match counts. */ function relevant_content_get_nodes($types, $terms, $exclude = array(), $limit = 5, $languages = array()) { // If terms or types are empty, there isn't anything to match to so not a lot of point continuing. if (empty($terms) || empty($types)) return FALSE; // Initialize the values array for the SQL $values = array(); // Define the SQL for term inclusion $term_sql = 'tn.tid IN('. db_placeholders($terms, 'int') .')'; $values = $values + array_values($terms); // Define the SQL for Node Type inclusion $types_sql = 'n.type IN ('. db_placeholders($types, 'varchar') .')'; $values = array_merge($values, array_values($types)); // Define the SQL for Node Exclusion (optional) $exclude_sql = ''; if (!empty($exclude)) { $exclude_sql = 'AND n.nid NOT IN ('. db_placeholders($exclude, 'int') .')'; $values = array_merge($values, array_values($exclude)); } // Define SQL for language restriction $language_sql = ''; if (!empty($languages)) { $language_sql = 'AND n.language IN ('. db_placeholders($languages, 'varchar') .')'; $values = array_merge($values, array_values($languages)); } // Add the result limit to the values array $values = array_merge($values, array($limit)); // Define the SQL using HereDoc $sql = <<data)) { $settings = $cache->data; } // Nope, ok - lets make them from scratch else { // Initialize the settings $settings = array(); // Get anything we have defined from the DB $result = db_query('SELECT * FROM {relevant_content_blocks}'); while ($block = db_fetch_array($result)) { // This ensures all blocks have a complete set of default values if not already provided. // This includes a "normal" storage $block = _relevant_content_santize_block($block); // TODO - tidy this line a bit... $block['status'] = ($block['status'] == RELEVANT_CONTENT_STATUS_ENABLED) ? RELEVANT_CONTENT_STATUS_ENABLED : RELEVANT_CONTENT_STATUS_DISABLED; // Load types $types_result = db_query("SELECT type FROM {relevant_content_blocks_types} WHERE id = '%s'", $block['id']); while ($type = db_result($types_result)) { $block['types'][$type] = $type; } // Load vocabs $vocabs_result = db_query("SELECT vid FROM {relevant_content_blocks_vocabs} WHERE id = '%s'", $block['id']); while ($vid = db_result($vocabs_result)) { // Cast the $vid to an integer $vid = (int)$vid; $block['vocabs'][$vid] = $vid; } // Store the block $settings[$block['id']] = $block; } // Now loop over the hook to get any module defined relevant content // blocks, using hook_relevant_content_default_blocks(). $default_blocks = module_invoke_all('relevant_content_default_blocks'); // Allow other modules to alter the module-defined blocks using: // hook_relevant_content_default_blocks_alter(&$default_blocks) drupal_alter('relevant_content_default_blocks', $default_blocks); // Add each defined block to our list... foreach ($default_blocks as $block) { // ... but only if an id is set! if (!empty($block['id'])) { $block = _relevant_content_santize_block($block); // If the id is already in our settings, then we have overridden a module-level item if (isset($settings[$block['id']])) { // Set the block as 'overridden', but do not store the module defined block $settings[$block['id']]['storage'] = RELEVANT_CONTENT_STORAGE_OVERRIDDEN; } // Otherwise, it's a custom block else { // Store the block as a default store $block['storage'] = RELEVANT_CONTENT_STORAGE_DEFAULT; $settings[$block['id']] = $block; } } } // Cache it for later use cache_set('relevant_content:settings', $settings); } } // Did we ask for a SPECIFIC block by delta? If so, return it (or FALSE if // it is not available) if ($block_delta) { return isset($settings[$block_delta]) ? $settings[$block_delta] : FALSE; } // Otherwise, return ALL settings return $settings; } /** * Internal function for santizing a block. Also useful for defining "defaults" for a form. */ function _relevant_content_santize_block($block = array()) { // Define the defaults $defaults = array( 'id' => '', 'types' => array(), 'vocabs' => array(), 'max_items' => 5, 'header_text' => '', 'header_format' => FILTER_FORMAT_DEFAULT, 'token_settings' => '', 'absolute_links' => 0, 'status' => RELEVANT_CONTENT_STATUS_DISABLED, 'storage' => RELEVANT_CONTENT_STORAGE_NORMAL, ); // Overlay the defaults onto $block $block = $block + $defaults; // Cast some items to "int" - this helps keep feature exporting clean $block['max_items'] = (int)$block['max_items']; $block['header_format'] = (int)$block['header_format']; return $block; } /** * Delete Block Handler */ function relevant_content_delete_block($block_id, $flush_cache = TRUE) { db_query("DELETE FROM {relevant_content_blocks} WHERE id = '%s'", $block_id); db_query("DELETE FROM {relevant_content_blocks_types} WHERE id = '%s'", $block_id); db_query("DELETE FROM {relevant_content_blocks_vocabs} WHERE id = '%s'", $block_id); // We may want to delete without flushing - for example when saving. if ($flush_cache) { // Reset the settings cache relevant_content_get_settings(NULL, TRUE); } } /** * Disable Block Handler */ function relevant_content_set_block_status($block_id, $status = 1, $flush_cache = TRUE) { // Load the block from cached settings $block = relevant_content_get_settings($block_id); // Set the status $block['status'] = $status; // Save the block - this also flushes all caches and rebuilds the block hash relevant_content_save_block($block); } /** * Save block handler - it essentially deletes and inserts for 'updates'. */ function relevant_content_save_block($block) { relevant_content_delete_block($block['id'], FALSE); $params = array( 'id' => array('type' => "'%s'", 'value' => $block['id']), 'max_items' => array('type' => "%d", 'value' => $block['max_items']), 'header_text' => array('type' => "'%s'", 'value' => $block['header_text']), 'header_format' => array('type' => "%d", 'value' => $block['header_format']), 'token_settings' => array('type' => "'%s'", 'value' => $block['token_settings']), 'absolute_links' => array('type' => "%d", 'value' => $block['absolute_links']), 'status' => array('type' => "%d", 'value' => $block['status']), ); // Insert the block row $placeholders = $param_values = array(); foreach ($params as $v) { $placeholders[] = $v['type']; $param_values[] = $v['value']; } $fields = implode(', ', array_keys($params)); $placeholders = implode(', ', $placeholders); db_query("INSERT INTO {relevant_content_blocks} ({$fields}) VALUES({$placeholders})", $param_values); // Add one row for each assigned type if (is_array($block['types'])) { foreach ($block['types'] as $type) { db_query("INSERT INTO {relevant_content_blocks_types} (id, type) VALUES('%s', '%s')", $block['id'], $type); } } // Add one row for each assigned vocab if (is_array($block['vocabs'])) { foreach ($block['vocabs'] as $vid) { db_query("INSERT INTO {relevant_content_blocks_vocabs} (id, vid) VALUES('%s', %d)", $block['id'], $vid); } } // Reset the settings cache relevant_content_get_settings(NULL, TRUE); // Rehash all the blocks for all themes module_invoke('block', 'flush_caches'); _block_rehash(); } /** * Implementation of hoko_features_api(). */ function relevant_content_features_api() { return array( 'relevant_content_block' => array( 'name' => t('Relevant Content Block'), 'default_hook' => 'relevant_content_default_blocks', 'default_file' => FEATURES_DEFAULTS_INCLUDED_COMMON, 'features_source' => TRUE, 'file' => drupal_get_path('module', 'relevant_content') .'/relevant_content.features.inc', ), ); } /** * Implementation of hook_relevant_content_default_blocks(). */ function relevant_content_relevant_content_default_blocks() { return array( 'test' => array( 'id' => 'test', 'max_items' => 10, 'vocabs' => array(1 => 1, 2 => 2, 3 => 3), 'types' => array('page' => 'page', 'story' => 'story'), 'header_text' => t('TESTING TEXT!!'), 'header_format' => 0, ), ); } /** * Module specific XSS Filtering. */ function relevant_content_filter_xss($string) { return filter_xss($string, _relevant_content_filter_xss_allowed_tags()); } /** * List of allowed tags for the relevant content token field. * This is more permissive than normal filter_xss() but less permissive than * filter_xss_admin(). Also gives us more control. */ function _relevant_content_filter_xss_allowed_tags() { return array('a', 'b', 'big', 'code', 'del', 'em', 'i', 'ins', 'pre', 'q', 'small', 'span', 'strong', 'sub', 'sup', 'tt', 'ol', 'ul', 'li', 'p', 'br', 'img'); } /** * Human readable list of allowed tags, handy for help text. */ function _relevant_content_filter_xss_display_allowed_tags() { return '<'. implode('> <', _relevant_content_filter_xss_allowed_tags()) .'>'; }