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 ---