t('all'), '%other' => t('other')); $short_help = t(_multilink_setting($mod_prefix, 'short_help'), $tvars); $long_help = $short_help; return t($long? $long_help : $short_help, $tvars); } } // Get $mod_id function _multilink_get_ids($format) { $mod_name = 'MultiLink'; $mod_id = 'multilink'; // Define the prefix used for config values: $mod_prefix = $mod_id.'_'.$format.'_'; // If set, use shared config. if (_multilink_setting($mod_prefix, 'shared')) { $mod_prefix = $mod_id.'_shared_'; } $GLOBALS['multilink_prefix'] = $mod_prefix; return compact('mod_name', 'mod_id', 'mod_prefix'); } /* * Build and return a url */ function multilink_url($path, $absolute = null) { $options['absolute'] = ($absolute !== null)? $absolute : $GLOBALS['multilink_absolute']; // We use the first of _multilink_preferred_languages() // to build the url (language subdomain or path-prefix). $pref_languages = _multilink_preferred_languages(); $language_list = language_list(); $options['language'] = $language_list[$pref_languages[0]]; // If MultiLink SecurePages is installed, use it. if (function_exists('multilink_securepages_url')) { return multilink_securepages_url($path, $options); } else { return url($path, $options); } } /* * Wrapper for _multilink_getnode adding handling for invalid nid and access control. */ function _multilink_filter_getnode($nid, $get_title = TRUE) { $link = _multilink_getnode($nid); if (!$link) { // $nid was invalid. $msg = t('Referenced node @nid does not exist', array('@nid' => $nid)); watchdog('multilink', $msg, array(), WATCHDOG_WARNING); $link->nid = $nid; $link->title = t('Not found'); } elseif ($get_title){ // Security - check access to the node before using title. $access = $link->access? TRUE : node_access('view', node_load($link->nid)); $link->title = $access? $link->title : t('Not allowed'); } return $link; } /* * Process multilink links. */ function _multilink_process($matches) { // matches will be: Array ( [0] => [123:title-text] [1] => 123 [2] => title-text ) // Get link path and title. $nid = $matches[1]; $link = _multilink_filter_getnode($nid); // If the link was specified as [123:$] then use node-title as link text. if ($matches[2] == '$') $matches[2] = $link->title; // Build target link. $options = array('external' => 1, 'attributes' => array('title' => $link->title)); $result = l($matches[2], multilink_url('node/'.$link->nid), $options); ////multilink_set_message('multilink process: ' . (print_r($result, 1)), 'warning'); return $result; } /* * Process other types of link. */ function _multilink_others_process($matches) { // Are we processing a link? if (count($matches) == 5) { // URL-based - Markdown, HTML anchor tag, etc. // matches: 0:full, 1:pre-path, 2:path-without-nid , 3:nid, 4:post-path. // To keep things simple regexp is such that , $matches[2] will be 'node' or '/node' $link = _multilink_filter_getnode($matches[3], FALSE); $path = trim($matches[2], '/') . '/' . $link->nid; $result = $matches[1] . multilink_url($path) . $matches[4]; } // Otherwise we're processing a tag. elseif (count($matches) == 4) { // Pathfilter, InsertNode, etc. // matches: 0:full, 1:pre-nid, 2:nid, 3:post-nid. $link = _multilink_getnode($matches[2], FALSE); $result = $matches[1] . $link->nid . $matches[3]; } else { $result = $matches[0]; } ////multilink_set_message('others process:
' . htmlentities(print_r($matches, 1)), 'warning'); return $result; } /* * Utility function to test if this page is going to be cached by Drupal. * See: http://api.drupal.org/api/drupal/includes--common.inc/function/drupal_page_footer/6 * and: http://api.drupal.org/api/drupal/includes--common.inc/function/page_set_cache/6 */ function _multilink_page_will_be_cached() { static $result; if (!isset($result)) { $result = FALSE; if ((variable_get('cache', CACHE_DISABLED) != CACHE_DISABLED)) { global $user; if (!$user->uid && $_SERVER['REQUEST_METHOD'] == 'GET' && page_get_cache(TRUE)) { $result = TRUE; } } } return $result; } /* * Implement hook_filter() */ function multilink_filter($op, $delta = 0, $format = -1, $text = '', $cache_id = 0) { // Get $mod_name, $mod_id and $mod_prefix; extract(_multilink_get_ids($format)); switch ($op) { case 'process': $patterns = _multilink_enabled_patterns($format); if (!empty($patterns)) { // Handle an important special case: // If this page is going to be cached, we need different processing // because our generated links will end up in the cached page. // @todo Consider alternative method: Don't allow page to be cached if generated links are not generic. if (_multilink_page_will_be_cached()) { $GLOBALS['multilink_pref_languages'] = array($GLOBALS['language']->language); } // Need to set a global so that callbacks can use the correct value for 'absolute'. $GLOBALS['multilink_absolute'] = _multilink_setting($mod_prefix, 'absolute'); if (isset($patterns['MultiLink'])) { $pattern = $patterns['MultiLink'][1]; $text = preg_replace_callback($pattern, '_multilink_process', $text); unset($patterns['MultiLink']); } foreach($patterns as $name => $test) { ////multilink_set_message($name . ' - check: ' . htmlspecialchars($test[0]) . ' regexp: ' . htmlspecialchars($test[1]), 'warning'); if (!$test[0] || strpos($text, $test[0]) !== FALSE) { $text = preg_replace_callback($test[1], '_multilink_others_process', $text); } } // Unset language preferences override which may have been set above. unset($GLOBALS['multilink_pref_languages']); } return $text; case 'no cache': $patched = defined('check_markup_language_patch_1'); $enabled = variable_get($mod_prefix . 'allow_cache', FALSE); $no_cache = !($patched && $enabled); return $no_cache; case 'list': return array(0 => t($mod_name)); case 'description': return t('Creates links to nodes, selecting a translated version if available.'); case 'settings': return _multilink_settings($format); default: return $text; } } /* * Define patterns to support other types of links (formats used by other input filters modules.) */ function _multilink_link_formats($format) { // Get $mod_name, $mod_id and $mod_prefix; extract(_multilink_get_ids($format)); // Add MultiLink formats $formats['MultiLink'] = array(FALSE, _multilink_setting($mod_prefix, 'pattern')); // This is a list of filter modules whose format we support. // Uses: module_filename => array(Module_Name, test-string, regexp-pattern // Regexp must define either 3 or 4 groups. $modules = array( // Linodef - http://drupal.org/project/linodef 'lindodef' => array('Linodef', '[#', '/(\[#)(\d+)(.*?\])/'), // Path Filter - http://drupal.org/project/pathfilter 'pathfilter' => array('Path Filter', 'internal:node', '/(["\']internal:node\/)(\d+)(.*?["\'])/'), // Pathologic - http://drupal.org/project/pathologic (offers Path Filter compatibility) 'pathologic' => array('Pathologic (Path Filter)', 'internal:node', '/(["\']internal:node\/)(\d+)(.*?["\'])/'), // Link node - http://drupal.org/project/link_node (format is identical to InsertNode.) 'link_node' => array('Link node', '[node:', '/(\[node:)(\d+)(.*?\])/'), // InsertNode - http://drupal.org/project/InsertNode (format is identical to Link node.) 'InsertNode' => array('InsertNode', '[node:', '/(\[node:)(\d+)(.*?\])/'), // Markdown - http://drupal.org/project/markdown 'markdown' => array('Markdown', '(/node/','/(\[.*?\]\()(\/node\/)(\d+)(.*?\))/'), ); // Build an array of allowed module-formats... if (_multilink_setting($mod_prefix, 'format_test')) { // Test-mode - allow all module-formats. foreach ($modules as $module) { $formats[$module[0]] = array($module[1], $module[2]); } } else { // Scan the filter to see which supported modules are enabled in the filter. foreach(filter_list_format($format) as $filter) { $module_id = $filter->module; if (isset($modules[$module_id])) { $module = $modules[$module_id]; $formats[$module[0]] = array($module[1], $module[2]); } } } // Add Generic HTML format to the end. // Note: For simplicity, we only allow for relative paths such as /node/123. $formats['Generic HTML'] = array('/node/', '/(.*<\/a>)/'); return $formats; } /* * Get patterns to support other types of links. */ function _multilink_enabled_patterns($format) { static $patterns; if(!isset($patterns[$format])) { // Get $mod_name, $mod_id and $mod_prefix; extract(_multilink_get_ids($format)); $patterns[$format] = array(); // Get available link formats. $link_formats = _multilink_link_formats($format); // Add each enabled link format to the result. foreach($link_formats as $name => $settings) { $key = 'enable_' . str_replace(' ', '_', $name); if (_multilink_setting($mod_prefix, $key)) { $patterns[$format][$name] = $settings; } } } ////multilink_set_message($format . ': enabled formats: ' . htmlspecialchars(print_r($patterns[$format], 1)), 'error'); return $patterns[$format]; } /* * Get an array of preferred languages. */ function _multilink_preferred_languages() { // Note: multilink_filter() overrides normal language preferences while processing text. if (isset($GLOBALS['multilink_pref_languages'])) { return $GLOBALS['multilink_pref_languages']; } // Build an array of preferred languages (if not already done) ... static $pref_languages; if (!isset($pref_languages)) { // Get user and broswer languages. global $user; $user_language_id = @$user->language; $browser_language_id = @language_from_browser()->language; if (!$user_language_id) { $user_language_id = $browser_language_id; } // Define an array of possible languages in default order. $languages = array( // The most recent language chosen via a language-switcher (MultiLink-specific.) 'selected' => _multilink_session_language(), // User-preferred language, from account if available. 'preferred' => $user_language_id, // Language from browser (first preference). 'browser' => $browser_language_id, // Language selected by Drupal for interface (always same as "content"?) 'current' => @$GLOBALS['language']->language, // Language selected by Drupal for content (always same as "current"?) //'content' => @$GLOBALS['language_content']->language, // Language specified in the url //'url' => @$GLOBALS['language_url']->language, // Site default language 'default' => @language_default()->language, ); // Set order of preference - default can be changed via settings.php or variable_set(). // @todo Consider Providing a UI to set order. $language_order = variable_get('multilink_language_order', FALSE); if ($language_order === FALSE) { // Use default order for preferred languages. $pref_languages = $languages; } else { // Build array of preferred languages in given order. $pref_languages = array(); foreach($language_order as $key) { if ($languages[$key]) { $pref_languages[$key] = $languages[$key]; } }; } //multilink_set_message('recalcuate: ' . print_r($pref_languages, 1)); $pref_languages = array_values(array_unique($pref_languages)); //multilink_set_message('unique: ' . print_r($pref_languages, 1)); // Remove any blank languages; if (in_array('', $pref_languages)) { while (($n = array_search('', $pref_languages)) !== FALSE) { unset($pref_languages[$n]); } $pref_languages = array_values($pref_languages); } //multilink_set_message('_multilink_preferred_languages() -> ' . print_r($pref_languages, 1)); } //multilink_set_message('_multilink_preferred_languages() -> ' . print_r($pref_languages, 1)); return array_values($pref_languages); } /* * Return a $node object containing (at least) nid, language, title for translated version of $node. * Return original $node if no suitable translation is available. */ function _multilink_get_translation($node, $pref_languages) { global $language; // If the node is not 'language neutral'... if ($node->language) { // Get translations; drupal_load('module', 'translation'); $translations = translation_node_get_translations($node->tnid); foreach($pref_languages as $lang) { if (isset($translations[$lang])) { $node = $translations[$lang]; break; } } } /* // @todo Else if Language Sections is in use... elseif (function_exists('language_sections_format_check')) { // If node uses Language Sections filtering... if (language_sections_format_check($node->format)) { // @todo: Get supported languages from LS. Needs supporting updates in LS. } } */ return $node; } /* * Cache operations. */ function _multilink_cache($op, $nid, $pref_languages = array(), $data = null) { // We are using our own cache table, so don't need to worry about key conflicts with other modules. $table = 'cache_multilink'; $base_key = $nid? sprintf('nid:%d:', $nid) : '*'; $full_key = sprintf('%s%08x', $base_key, crc32(implode(':', $pref_languages))); // Additional static cache, effective when same nodes are referenced several times per page. static $saved; switch ($op) { case 'get': // Retrieve from cache. if(isset($saved[$full_key])) { return $saved[$full_key]; } else { $cached = cache_get($full_key, $table); if ($cached) { $saved[$full_key] = $cached->data; return $saved[$full_key]; } else { return FALSE; } } case 'set': // Store in cache for 24 hours. cache_set($full_key, $data, $table, time() + 86400); $saved[$full_key] = $data; return; case 'clear': // Clear selected cache entries. cache_clear_all($base_key, $table, TRUE); unset($saved); return; } } /* * Implement hook_flush_caches */ function multilink_flush_caches() { return array('cache_multilink'); } function _multilink_getnode($nid) { // Load from cache if available. $pref_languages = _multilink_preferred_languages(); $result = _multilink_cache('get', $nid, $pref_languages); if (!$result) { // Build from scratch and store in cache. $result = _multilink_buildnode($nid, $pref_languages); if ($result) { _multilink_cache('set', $nid, $pref_languages, $result); } } return $result; } function _multilink_buildnode($nid, $pref_languages) { global $language; // We need to load the node to get its language and tnid. drupal_load('module', 'node'); drupal_load('module', 'user'); $node = node_load($nid); // MultiLink Redirect may call here with with $nid of non-existant node. if (!$node) { return; } // If the node has a language but it's not the first preferred language... if ($node->language && $node->language != $pref_languages[0]) { // Try to find a suitable translation. $node = _multilink_get_translation($node, $pref_languages); } // Set nid and language into result. $result->nid = $node->nid; $result->language = $node->language; // If node language is different from interface language, show actual language in title. // @todo Ideally we would do this when language is different from *current content* language. if ($node->language && $node->language != $language->language) { $result->title = sprintf('[%s] %s', locale_language_name($node->language), $node->title); } else { $result->title = $node->title; } // Security: Store a flag indicating whether anonymous access is allowed. $result->access = node_access('view', $node, drupal_anonymous_user()); return $result; } /* * Define default values for settings, get individual setting. */ function _multilink_setting($mod_prefix, $key) { static $defaults; if (!isset($defaults)) { $defaults = array( 'pattern' => '/\[(\d+): ?(.+?)\]/', 'short_help' => 'Enter node links as [1234:text] ' . 'where 1234 is a node number and text is what should be displayed ' . 'or $ to display the node\'s title.', 'enable_MultiLink' => TRUE, ); } return variable_get($mod_prefix.$key, $defaults[$key]); } /* * Build and return the settings form. */ function _multilink_settings($format) { // Get $mod_name, $mod_id and $mod_prefix; extract(_multilink_get_ids($format)); //require_once(dirname(__FILE__) . '/help.html'); global $language; $textsize = 30; // Some settings should be reset to default if set blank in the form: $keys = array('pattern', 'short_help'); foreach ($keys as $key) { if (!($$key = _multilink_setting($mod_prefix, $key))) { variable_del($mod_prefix.$key); $$key = _multilink_setting($mod_prefix, $key); } } // Create collapsible section for this module in the filters configuration form. $section =& $form[$mod_id]; $fieldset =& $section; $shared = _multilink_setting($mod_prefix, 'shared'); $section = array( '#type' => 'fieldset', '#title' => $shared? sprintf('%s (%s)', $mod_name, t('shared configuration')) : $mod_name, '#collapsible' => TRUE, ); $key = 'absolute'; $fieldset[$mod_prefix.$key] = array( '#type' => 'checkbox', '#title' => t('Absolute urls'), '#default_value' => _multilink_setting($mod_prefix, $key), '#description' => t('If set, links will be generated with absolute urls, i.e: http://example.com/node/1'), ); $key = 'short_help'; $fieldset[$mod_prefix.$key] = array( '#type' => 'textarea', '#title' => t('User help'), '#rows' => 2, '#default_value' => $short_help, '#description' => t('Filter-help shown to the user. This text is passed through t(). Blank to reset to default value.'), ); /* $section['help'] = array( '#type' => 'markup', '#value' => '

Help goes here.

', ); */ // --- Link formats --- $link_formats = array_keys(_multilink_link_formats($format)); $fieldset =& $section['link_formats']; $fieldset = array( '#type' => 'fieldset', '#title' => t('Link formats'), '#description' => t('This section allows you to define what types of links/tags should be processed by MultiLink.'), '#collapsible' => TRUE, '#collapsed' => TRUE, ); foreach($link_formats as $name) { $key = 'enable_' . str_replace(' ', '_', $name); $fieldset[$mod_prefix.$key] = array( '#type' => 'checkbox', '#title' => $name, '#default_value' => _multilink_setting($mod_prefix, $key), '#description' => t('Process links/tags in %module format.', array('%module' => $name)), ); } // --- Other format options --- $key = 'format_test'; $fieldset[$mod_prefix.$key] = array( '#prefix' => '
', '#type' => 'checkbox', '#title' => t('Test supported formats'), '#default_value' => _multilink_setting($mod_prefix, $key), '#description' => t('Allow all supported link-formats regardless of whether the corresponding filter modules are enabled. ' . 'Enabling this option will activate additional settings above (after saving.)' ), ); /* $key = 'use_alias'; $fieldset[$mod_prefix.$key] = array( '#type' => 'checkbox', '#title' => t('Convert to alias'), '#default_value' => _multilink_setting($mod_prefix, $key), '#description' => t('Should multlink convert /node/1234 to its alias (if available)?'), ); */ // --- Advanced settings --- $fieldset =& $section['advanced']; $fieldset = array( '#type' => 'fieldset', '#title' => t('Advanced'), '#collapsible' => TRUE, '#collapsed' => TRUE, ); $key = 'shared'; $fieldset[$mod_prefix.$key] = array( '#type' => 'checkbox', '#title' => t('Shared configuration'), '#default_value' => $shared, '#description' => t('Use the same configuration for all filters. If you change this, save and then check all configuration values.'), ); $key = 'pattern'; $fieldset[$mod_prefix.$key] = array( '#type' => 'textfield', '#title' => t('Pattern'), '#size' => 40, '#default_value' => $pattern, '#description' => t('This pattern will be used to find %name links in the text.' . ' You should not change the number of parenthesised groups in the expression.' . ' Blank to reset default.', array('%name' => $mod_name)), ); // Display caching options depending whether caching is possible (patch installed.) $fieldset =& $fieldset['cache']; $patched = defined('check_markup_language_patch_1'); $msg = array('Patch for $func is not installed - output cannot be cached. See included README.txt', 'Patch for $func is installed - output can be cached.', ); $fieldset = array( '#type' => 'fieldset', '#title' => t('Output caching'), '#value' => t($msg[$patched], array('$func' => 'check_markup()')), ); if ($patched) { $key = 'allow_cache'; $fieldset[$mod_prefix.$key] = array( '#type' => 'checkbox', '#title' => t('Additional caching'), '#default_value' => _multilink_setting($mod_prefix, $key), '#description' => t('In addition to MultiLink\'s internal caching, allow Drupal to cache filtered output.' . ' This should improve performance but displayed links may be out of date.' . ' Even if this option is enabled, other filters may prevent caching.' . ' If you change this option you must re-save the !settings afterwards.' , array('!settings' => l(t('filter settings'), 'admin/settings/filters/' . $format))), ); } return $form; } /* * Implement hook nodeapi. * If a node is added, updated or deleted, clear any cached entries we may have for that node and its translations. */ function multilink_nodeapi($node, $op) { // Trap node insert, update and delete if (strpos(':insert:update:delete:', $op)) { // Special case: if the translation "source" node was just deleted, we can't get the translations for it any more! // The only simple solution is to clear the entire cache. In practice this would not be a common event. if ($op == 'delete' && $node->nid == $node->tnid) { _multilink_cache('clear', 0); } else { // Get translations for this node, and clear cached entries. $tnid = ($op == 'insert')? $node->translation_source->tnid : $node->tnid; $translations = translation_node_get_translations($tnid); // If we have no translations or we are deleting, include the current node. if (empty($translations) || $op == 'delete') { $translations[] = $node; } foreach($translations as $translation) { _multilink_cache('clear', $translation->nid); } } } } /** * Implement hook_translation_link_alter(). * Add a query string to the link to disable redirect. * This works with standard Drupal Language Switcher block (at least.) */ function multilink_translation_link_alter(&$links, $path) { foreach($links as &$link) { _multilink_update_link($link); } } /** * Implement hook_link_alter(). * Add a query string to the link to disable redirect. * This works for the standard translation links on node pages, plus any other links which set class='translation-link' */ function multilink_link_alter(&$links, $node, $comment = NULL) { foreach($links as &$link) { if (isset($link['attributes']) && isset($link['attributes']['class']) && $link['attributes']['class'] == 'translation-link') { _multilink_update_link($link); } } } /* * Utility function to update a link (used above.) */ function _multilink_update_link(&$link) { $link['query']['multilink'] = 'switch'; ////multilink_set_message(print_r($link, 1), 'warning'); } /* * Implement hook_init * Note: May also be called from multilink_redirect_boot in which case Drupal boot is not yet complete. */ function multilink_init() { static $done; if (!$done) { //multilink_set_message('multilink_init'); $done = TRUE; global $language; // $language is not set, drupal_init_language() has not been called yet, so do it now. if (!is_object($language)) { drupal_init_language(); } //multilink_set_message(sprintf('Global $language -> %s', $language->language)); // If the user arriving from a "language switcher" or similar link, // we will regard the chosen language as their preferred language from now on. // Note: $_GET['multilink'] is added to links via our _link_alter hooks. if (isset($_GET['multilink']) && $_GET['multilink'] == 'switch') { _multilink_session_language($language->language); // For tidiness/SEO, redirect to same url without $_GET['multilink']. $get = $_GET; unset($get['q'], $get['multilink']); drupal_goto($_GET['q'], drupal_query_string_encode($get)); } } } /* * Return and optionally set the "preferred" language for current session. * We use cookies rather than $_SESSION so that this can work during bootstrap. */ function _multilink_session_language($language = FALSE) { $cookie = 'multilink_pl'; if ($language) { // Set a cookie to expire in 24 hours. setcookie($cookie, $language, time() + 86400, '/', $GLOBALS['cookie_domain']); $_COOKIE[$cookie] = $language; } ////multilink_set_message(sprintf('session_language: %s', $_COOKIE[$cookie])); return (isset($_COOKIE[$cookie]))? $_COOKIE[$cookie] : NULL; } /* * Utility to display or log debug messages. */ /*** function multilink_set_message($message = NULL, $type = 'status') { if ($GLOBALS['user']->uid == 1) { drupal_set_message($message, $type); } else { static $log; $logname = '/var/log/multilink.log'; if (!isset($log)) { $log = fopen($logname, 'a'); } fwrite($log, sprintf("[%s][%s] %s\n", time(), $type, $message)); } } ***/ // --- Drupal docs advise NOT closing PHP tags ---