Seditio Source
Root |
./othercms/b2evolution_7.2.3/plugins/_autolinks.plugin.php
<?php
/**
 * This file implements the Automatic Links plugin for b2evolution
 *
 * b2evolution - {@link http://b2evolution.net/}
 * Released under GNU GPL License - {@link http://b2evolution.net/about/gnu-gpl-license}
 * @copyright (c)2003-2020 by Francois Planque - {@link http://fplanque.com/}
 *
 * @package plugins
 */
if( !defined('EVO_MAIN_INIT') ) die( 'Please, do not access this page directly.' );


/**
 * Automatic links plugin.
 *
 * @todo dh> Provide a setting for: fp> This should be a DIFFERENT plugin that kicks in last in the rendering and actually prcesses ALL links, auto links as well as explicit/manual links
 *   - marking external and internal (relative URL or on the blog's URL) links with a HTML/CSS class
 *   - add e.g. 'target="_blank"' to external links
 * @todo Add "max. displayed length setting" and add full title + dots in the middle to shorten it.
 *       (e.g. plain long URLs with a lot of params and such). This should not cause the layout to
 *       behave ugly. This should only shorten non-whitespace strings in the link's innerHTML of course.
 *
 * @package plugins
 */
class autolinks_plugin extends Plugin
{
    var
$code = 'b2evALnk';
    var
$name = 'Auto Links';
    var
$priority = 63;
    var
$version = '7.2.3';
    var
$group = 'rendering';
    var
$short_desc;
    var
$long_desc;
    var
$help_topic = 'autolinks-plugin';
    var
$number_of_installs = null;    // Let admins install several instances with potentially different word lists

    /**
     * Lazy loaded from txt files
     *
     * @var array of array for each blog. Index 0 is for shared content
     */
   
var $link_array = array();

    var
$already_linked_array;

   
/**
     * Previous word from the text during the make clickable process
     *
     * @var string
     */
   
var $previous_word = null;
   
/**
     * Previous word in lower case format
     *
     * @var string
     */
   
var $previous_lword = null;
   
/**
     * Shows if the previous word was already used/converted to a link
     *
     * @var boolean
     */
   
var $previous_used = false;

    var
$already_linked_usernames;

   
/**
     * Array of tags used for current collection
     *
     * @var array
     */
   
var $tags_array = NULL;

    var
$already_linked_tags = NULL;

   
/**
     * Current Blog
     *
     * @var object
     */
   
var $current_Blog = NULL;

   
/**
     * Init
     */
   
function PluginInit( & $params )
    {
       
$this->short_desc = T_('Make URLs and specific terms/defintions clickable');
       
$this->long_desc = T_('This renderer automatically creates links for you. URLs can be made clickable automatically. Specific and frequently used terms can be configured to be automatically linked to a definition URL.');
    }


   
/**
     * Define the GLOBAL settings of the plugin here. These can then be edited in the backoffice in System > Plugins.
     *
     * @param array Associative array of parameters (since v1.9).
     *    'for_editing': true, if the settings get queried for editing;
     *                   false, if they get queried for instantiating {@link Plugin::$Settings}.
     * @return array see {@link Plugin::GetDefaultSettings()}.
     * The array to be returned should define the names of the settings as keys (max length is 30 chars)
     * and assign an array with the following keys to them (only 'label' is required):
     */
   
function GetDefaultSettings( & $params )
    {
        return array(
               
'autolink' => array(
                       
'label' => T_('Create auto-links for'),
                       
'type' => 'checklist',
                       
'options' => array(
                            array(
'defs_default', sprintf( T_('Definitions as defined in %s'), '<code>definitions.default.txt</code>' ), 1 ),
                            array(
'defs_local', sprintf( T_('Definitions as defined in %s'), '<code>definitions.local.txt</code>' ), 0 ),
                        )
                    ),
               
'autolink_defs_db' => array(
                       
'label' => T_('Custom definitions'),
                       
'type' => 'html_textarea',
                       
'rows' => 15,
                       
'note' => $this->T_( 'Enter custom definitions above.' ),
                       
'defaultvalue' => '',
                    ),
            );
    }


   
/**
     * Define here default custom settings that are to be made available
     *     in the backoffice for collections, private messages and newsletters.
     *
     * @param array Associative array of parameters.
     * @return array See {@link Plugin::get_custom_setting_definitions()}.
     */
   
function get_custom_setting_definitions( & $params )
    {
        return array(
           
'autolink_urls' => array(
                   
'label' => $this->T_('Autolink URLs'),
                   
'type' => 'checkbox',
                   
'note' => sprintf( $this->T_('Find URLs that match %s, %s or %s'), '<code>http://*</code>', '<code>https://*</code>', '<code>www.*.*</code>' ),
                   
'defaultvalue' => 1,
                ),
           
'autolink_emails' => array(
                   
'label' => $this->T_('Autolink email addresses'),
                   
'type' => 'checkbox',
                   
'note' => sprintf( $this->T_('Find addresses that match %s or %s'), '<code>mailto:</code>', '<code>*@*.*</code>' ),
                   
'defaultvalue' => 1,
                ),
           
'autolink_username' => array(
                   
'label' => T_( 'Autolink usernames' ),
                   
'type' => 'checkbox',
                   
// TRANS: the user can type in any username after "@" but it's typically only lowercase letters and no spaces.
                   
'note' => $this->T_( '@username will link to the user profile page' ),
                   
'defaultvalue' => 0,
                ),
           
'autolink_defs_coll_db' => array(
                   
'label' => T_( 'Custom autolink definitions' ),
                   
'type' => 'html_textarea',
                   
'rows' => 15,
                   
'note' => $this->T_( 'Enter custom definitions above.' ),
                   
'defaultvalue' => '',
                ),
        );
    }


   
/**
     * Define here default collection/blog settings that are to be made available in the backoffice.
     *
     * @param array Associative array of parameters.
     * @return array See {@link Plugin::GetDefaultSettings()}.
     */
   
function get_coll_setting_definitions( & $params )
    {
       
$default_values = array(
               
'autolink_tag'                       => 0,
               
'autolink_post_nofollow_auto'        => 0,
               
'autolink_comment_nofollow_auto'     => 0,
            );

       
// set params to allow rendering for comments by default
       
$default_params = array_merge( $params, array( 'default_comment_rendering' => 'stealth' ) );
       
$coll_params = parent::get_coll_setting_definitions( $default_params );

        if( isset(
$coll_params['autolink_defs_coll_db'] ) )
        {    
// Store this param in order to put under "Autolink tags":
           
$coll_param_autolink_defs_coll_db = $coll_params['autolink_defs_coll_db'];
            unset(
$coll_params['autolink_defs_coll_db'] );
        }

       
$coll_params['autolink_tag'] = array(
               
'label' => T_('Autolink tags'),
               
'type' => 'checkbox',
               
'note' => $this->T_( 'Find text matching tags of the current collection and autolink them to the tag page in the current collection' ),
               
'defaultvalue' => $default_values['autolink_tag'],
            );

        if( isset(
$coll_param_autolink_defs_coll_db ) )
        {    
// Put the setting under "Autolink tags":
           
$coll_params['autolink_defs_coll_db'] = $coll_param_autolink_defs_coll_db;
        }

        return
array_merge( $coll_params,
            array(
               
// No follow in posts
               
'autolink_post_nofollow' => array(
                       
'label' => T_('No follow in posts'),
                       
'type' => 'checklist',
                       
'options' => array(
                            array(
'auto', $this->T_('Add rel="nofollow" to links from autolink definitions'), $default_values['autolink_post_nofollow_auto'] ),
                        )
                    ),
               
// No follow in comments
               
'autolink_comment_nofollow' => array(
                       
'label' => T_('No follow in comments'),
                       
'type' => 'checklist',
                       
'options' => array(
                            array(
'auto', $this->T_('Add rel="nofollow" to links from autolink definitions'), $default_values['autolink_comment_nofollow_auto'] ),
                        )
                    ),
            )
        );
    }


   
/**
     * Define here default message settings that are to be made available in the backoffice.
     *
     * @param array Associative array of parameters.
     * @return array See {@link Plugin::GetDefaultSettings()}.
     */
   
function get_msg_setting_definitions( & $params )
    {
       
// set params to allow rendering for messages by default
       
$default_params = array_merge( $params, array( 'default_msg_rendering' => 'stealth' ) );
        return
array_merge( parent::get_msg_setting_definitions( $default_params ),
            array(
               
// No follow settings in messages:
               
'autolink_nofollow' => array(
                       
'label' => T_('No follow in messages'),
                       
'type' => 'checklist',
                       
'options' => array(
                            array(
'auto', $this->T_('Add rel="nofollow" to auto-links'), 0 ),
                        )
                    ),
            )
        );
    }


   
/**
     * Define here default email settings that are to be made available in the backoffice.
     *
     * @param array Associative array of parameters.
     * @return array See {@link Plugin::GetDefaultSettings()}.
     */
   
function get_email_setting_definitions( & $params )
    {
       
// set params to allow rendering for emails by default:
       
$default_params = array_merge( $params, array( 'default_email_rendering' => 'stealth' ) );
        return
array_merge( parent::get_email_setting_definitions( $default_params ),
            array(
               
// No follow settings in email campaigns:
               
'autolink_nofollow' => array(
                       
'label' => T_('No follow in messages'),
                       
'type' => 'checklist',
                       
'options' => array(
                            array(
'auto', $this->T_('Add rel="nofollow" to auto-links'), 0 ),
                        )
                    ),
            )
        );
    }


   
/**
     * Define here default shared settings that are to be made available in the backoffice.
     *
     * @param array Associative array of parameters.
     * @return array See {@link Plugin::GetDefaultSettings()}.
     */
   
function get_shared_setting_definitions( & $params )
    {
       
// set params to allow rendering for shared container widgets by default:
       
$default_params = array_merge( $params, array( 'default_shared_rendering' => 'stealth' ) );
        return
array_merge( parent::get_shared_setting_definitions( $default_params ),
            array(
               
// No follow settings in messages:
               
'autolink_nofollow' => array(
                       
'label' => T_('No follow in messages'),
                       
'type' => 'checklist',
                       
'options' => array(
                            array(
'auto', $this->T_('Add rel="nofollow" to auto-links'), 0 ),
                        )
                    ),
            )
        );
    }


   
/**
     * Lazy load global definitions array
     *
     * @param object Blog
     */
   
function load_link_array( $Blog )
    {
        global
$plugins_path;

        if( !isset(
$this->link_array[0]) )
        {    
// global defs NOT already loaded
           
$this->link_array[0] = array();

            if(
$this->get_checklist_setting( 'autolink', 'defs_default' ) )
            {    
// Load defaults:
               
$this->read_csv_file( $plugins_path.'autolinks_plugin/definitions.default.txt', 0 );
            }
            if(
$this->get_checklist_setting( 'autolink', 'defs_local' ) )
            {    
// Load local user defintions:
               
$this->read_csv_file( $plugins_path.'autolinks_plugin/definitions.local.txt', 0 );
            }
           
$text = $this->Settings->get( 'autolink_defs_db', 0 );
            if( !empty(
$text) )
            {    
// Load local user defintions:
               
$this->read_textfield( $text, 0 );
            }
        }

       
// load defs for current blog:
       
$coll_ID = !empty( $Blog ) ? $Blog->ID : 0;
        if( !isset(
$this->link_array[$coll_ID]) )
        {    
// This blog is not loaded yet:
           
$this->link_array[$coll_ID] = array();
        }
       
$text = $this->setting_autolink_defs_coll_db;
        if( ! empty(
$text ) )
        {    
// Load local user defintions:
           
$this->read_textfield( $text, $coll_ID );
        }

       
// Prepare working link array:
       
$this->replacement_link_array = array_merge( $this->link_array[0], $this->link_array[$coll_ID] );
    }


   
/**
      * Load contents of one specific CSV file
     *
     * @param string $filename
     */
   
function read_csv_file( $filename, $coll_ID )
    {
        if( !
$handle = @fopen( $filename, 'r') )
        {    
// File could not be opened:
           
return;
        }

        while( (
$data = fgetcsv($handle, 1000, ';', '"')) !== false )
        {
           
$this->read_line( $data, $coll_ID );
        }

       
fclose($handle);
    }


   
/**
      * Load contents of one large textfield to be treated as CSV
      *
      * Note: This method is probably not well suited for very large lists.
     *
     * @param string $filename
     */
   
function read_textfield( $text, $coll_ID )
    {
       
// split into lines:
       
$lines = preg_split( '#\r|\n#', $text );

        foreach(
$lines as $line )
        {
           
// CSV style decoding in memory:
            // $keywords = preg_split( "/[\s,]*\\\"([^\\\"]+)\\\"[\s,]*|[\s,]+/", "textline with, commas and \"quoted text\" inserted", 0, PREG_SPLIT_DELIM_CAPTURE );
           
$data = explode( ';', $line );
           
$this->read_line( $data, $coll_ID );
        }
    }


   
/**
     * read line
     *
     * @param exploded $data array
     */
   
function read_line( $data, $coll_ID )
    {
        if( empty(
$data[0] ) )
        {    
// Skip empty and comment lines
           
return;
        }

       
$word = $data[0];
       
$url = isset( $data[3] ) ? $data[3] : NULL;
        if(
$url == '-' || empty( $url ) )
        {
// Remove URL (useful to remove some defs on a specific site):
           
unset( $this->link_array[0][$word] );
            unset(
$this->link_array[$coll_ID][$word] );
        }
        else
        {
            if( ! isset(
$this->link_array[ $coll_ID ][ $word ] ) )
            {
// Initialize array only first time to store several previous words for each word:
               
$this->link_array[ $coll_ID ][ $word ] = array();
            }
           
$this->link_array[ $coll_ID ][ $word ][ $data[1] ] = $url;
        }
    }


   
/**
     * Perform rendering
     *
     * @param array Associative array of parameters
     *                             (Output format, see {@link format_to_output()})
     * @return boolean true if we can render something for the required output format
     */
   
function RenderItemAsHtml( & $params )
    {
       
$content = & $params['data'];

       
// Get collection from given params (also it is used to build link for tag links):
       
$this->current_Blog = $this->get_Blog_from_params( $params );

       
// Define the setting names depending on what is rendering now
       
if( !empty( $params['Comment'] ) )
        {    
// Comment is rendering
           
$this->setting_nofollow_auto = $this->get_checklist_setting( 'autolink_comment_nofollow', 'auto', 'coll', $this->current_Blog );
        }
        else
        {    
// Item is rendering
           
$this->setting_nofollow_auto = $this->get_checklist_setting( 'autolink_post_nofollow', 'auto', 'coll', $this->current_Blog );
        }

       
$this->setting_autolink_defs_coll_db = $this->get_coll_setting( 'autolink_defs_coll_db', $this->current_Blog );
       
$this->setting_autolink_urls = $this->get_coll_setting( 'autolink_urls', $this->current_Blog );
       
$this->setting_autolink_emails = $this->get_coll_setting( 'autolink_emails', $this->current_Blog );
       
$this->setting_autolink_username = $this->get_coll_setting( 'autolink_username', $this->current_Blog );
       
$this->setting_autolink_tag = $this->get_coll_setting( 'autolink_tag', $this->current_Blog );

        return
$this->render_content( $content, $this->current_Blog );
    }


   
/**
     * Perform rendering of Message content
     *
     * NOTE: Use default coll settings of comments as messages settings
     *
     * @see Plugin::RenderMessageAsHtml()
     */
   
function RenderMessageAsHtml( & $params )
    {
       
$content = & $params['data'];

       
// Message is rendering
       
$this->setting_nofollow_auto = $this->get_checklist_setting( 'autolink_nofollow', 'auto', 'msg' );
       
$this->setting_autolink_defs_coll_db = $this->get_msg_setting( 'autolink_defs_coll_db' );
       
$this->setting_autolink_urls = $this->get_msg_setting( 'autolink_urls' );
       
$this->setting_autolink_emails = $this->get_msg_setting( 'autolink_emails' );
       
$this->setting_autolink_username = $this->get_msg_setting( 'autolink_username' );
       
$this->setting_autolink_tag = false;

        return
$this->render_content( $content );
    }


   
/**
     * Perform rendering of Email content
     *
     * NOTE: Use default coll settings of comments as messages settings
     *
     * @see Plugin::RenderEmailAsHtml()
     */
   
function RenderEmailAsHtml( & $params )
    {
       
$content = & $params['data'];

       
// Email is rendering
       
$this->setting_nofollow_auto = $this->get_checklist_setting( 'autolink_nofollow', 'auto', 'email' );
       
$this->setting_autolink_defs_coll_db = $this->get_email_setting( 'autolink_defs_coll_db' );
       
$this->setting_autolink_urls = $this->get_email_setting( 'autolink_urls' );
       
$this->setting_autolink_emails = $this->get_email_setting( 'autolink_emails' );
       
$this->setting_autolink_username = $this->get_email_setting( 'autolink_username' );
       
$this->setting_autolink_tag = false;

        return
$this->render_content( $content );
    }


   
/**
     * Render content of Item, Comment, Message
     *
     * @param string Content
     * @param object Blog
     * return boolean
     */
   
function render_content( & $content, $item_Blog = NULL )
    {
       
// Reset already linked usernames:
       
$this->already_linked_usernames = array();

       
// Load global defs:
       
$this->load_link_array( $item_Blog );

       
// Load all tags of current collection:
       
$this->load_tags_array( $item_Blog, $content );
       
// Reset already linked tags:
       
$this->already_linked_tags = array();

       
// reset already linked:
       
$this->already_linked_array = array();
        if(
preg_match_all( '|[\'"](http://[^\'"]+)|i', $content, $matches ) )
        {    
// There are existing links:
           
$this->already_linked_array = $matches[1];
        }

        if(
$this->setting_autolink_urls )
        {    
// Make the URLs clickable:
           
$content = make_clickable( $content, '&amp;', array( $this, 'make_clickable_callback_urls' ), '', true );
        }

        if(
$this->setting_autolink_emails )
        {    
// Make the email addresses clickable:
           
$content = make_clickable( $content, '&amp;', array( $this, 'make_clickable_callback_emails' ), '', true );
        }

       
// Make the desired remaining terms/definitions, usernames or tags clickable:
       
$content = make_clickable( $content, '&amp;', array( $this, 'make_clickable_callback_definitions' ), '', true );

        return
true;
    }


    function
FilterCommentContent( & $params )
    {
       
$Comment = & $params['Comment'];
       
$comment_Item = & $Comment->get_Item();
       
$item_Blog = & $comment_Item->get_Blog();
        if(
in_array( $this->code, $Comment->get_renderers_validated() ) )
        {
// Always allow rendering for comment
           
$render_params = array_merge( array( 'data' => & $Comment->content, 'Item' => & $comment_Item ), $params );
           
$this->RenderItemAsHtml( $render_params );
        }
        return
false;
    }


   
/**
     * Callback function to make URLs clickable
     *
     * @param string Text
     * @param string Url delimeter
     * @param string Additional attributes for tag <a>
     * @return string The clickable text.
     */
   
function make_clickable_callback_urls( $text, $moredelim = '&amp;', $additional_attrs = '' )
    {
        if( ! empty(
$additional_attrs ) )
        {
           
$additional_attrs = ' '.trim( $additional_attrs );
        }

       
// Add style class to break long urls:
       
$additional_attrs = stripos( $additional_attrs, ' class="' ) === false
           
? $additional_attrs.' class="linebreak"'
           
: preg_replace( '/ class="([^"]*)"/i', ' class="$1 linebreak"', $additional_attrs );

       
$pattern_domain = '([\p{L}0-9\-]+\.[\p{L}0-9\-.\~]+)'; // a domain name (not very strict)
       
$text = preg_replace(
           
/* Tblue> I removed the double quotes from the first RegExp because
                        it made URLs in tag attributes clickable.
                        See http://forums.b2evolution.net/viewtopic.php?p=92073 */
           
array( '#(^|[\s>\(]|\[url=)(https?)://([^"<>{}\s]+[^".,:;!\?<>{}\s\]\)])#i',
               
'#(^|[\s>\(]|\[url=)www\.'.$pattern_domain.'([^"<>{}\s]*[^".,:;!\?\s\]\)])#i' ),
            array(
'$1<a href="$2://$3"'.$additional_attrs.'>$2://$3</a>',
               
'$1<a href="http://www.$2$3$4"'.$additional_attrs.'>www.$2$3$4</a>' ),
           
$text );

        return
$text;
    }


   
/**
     * Callback function to make email addresses clickable
     *
     * @param string Text
     * @param string Url delimeter
     * @param string Additional attributes for tag <a>
     * @return string The clickable text.
     */
   
function make_clickable_callback_emails( $text, $moredelim = '&amp;', $additional_attrs = '' )
    {
        if( ! empty(
$additional_attrs ) )
        {
           
$additional_attrs = ' '.trim( $additional_attrs );
        }

       
$pattern_domain = '([\p{L}0-9\-]+\.[\p{L}0-9\-.\~]+)'; // a domain name (not very strict)
       
$text = preg_replace(
           
/* Tblue> I removed the double quotes from the first RegExp because
                        it made URLs in tag attributes clickable.
                        See http://forums.b2evolution.net/viewtopic.php?p=92073 */
           
array( '#(^|[\s>\(]|\[url=)mailto://([^"<>{}\s]+[^".,:;!\?<>{}\s\]\)])#i',
               
'#(^|[\s>\(]|\[url=)([a-z0-9\-_.]+?)@'.$pattern_domain.'([^".,:;!\?&<\s\]\)]+)#i' ),
            array(
'$1<a href="mailto://$2"'.$additional_attrs.'>mailto://$2</a>',
               
'$1<a href="mailto:$2@$3$4"'.$additional_attrs.'>$2@$3$4</a>' ),
           
$text );

        return
$text;
    }


   
/**
     * Callback function to make terms/definitions, usernames or tags clickable
     *
     * @param string Text
     * @param string Url delimeter
     * @return string The clickable text.
     */
   
function make_clickable_callback_definitions( $text, $moredelim = '&amp;' )
    {
        global
$evo_charset;

        if( ! empty(
$this->replacement_link_array ) )
        {    
// Make the desired remaining terms/definitions clickable:
           
$regexp_modifier = '';
            if(
$evo_charset == 'utf-8' )
            {
// Add this modifier to work with UTF-8 strings correctly
               
$regexp_modifier = 'u';
            }

           
// Previous word in lower case format
           
$this->previous_lword = null;
           
// Previous word was already used/converted to a link
           
$this->previous_used = false;

           
// Optimization: Check if the text contains words from the replacement links strings, and call replace callback only if there is at least one word which needs to be replaced.
           
$text_words = preg_split( '/\s/', utf8_strtolower( $text ) );
            foreach(
$text_words as $text_word )
            {
// Trim the signs [({/ from start and the signs ])}/.,:;!? from end of each word
               
$clear_word = preg_replace( '#^[\[\({/]?([@\p{L}0-9_\-]{3,})[\.,:;!\?\]\)}/]?$#i', '$1', $text_word );
                if(
$clear_word != $text_word )
                {
// Append a clear word to array if word has the punctuation signs
                   
$text_words[] = $clear_word;
                }
            }
           
// Check if a content has at least one definition to make an url from word
           
$text_contains_replacement = ( count( array_intersect( $text_words, array_keys( $this->replacement_link_array ) ) ) > 0 );
            if(
$text_contains_replacement )
            {
// Find word with 3 characters at least:
               
$text = preg_replace_callback( '#(^|\s|[(),;\'\"\[{/])([@\p{L}0-9_\-\.]{3,})([\.,:;!\'\"\?\]\)}/]?)#i'.$regexp_modifier, array( & $this, 'replace_callback' ), $text );
            }

           
// Cleanup words to be deleted:
           
$text = preg_replace( '/[@\p{L}0-9_\-]+\s*==!#DEL#!==/i'.$regexp_modifier, '', $text );
        }

       
// Replace @usernames with user identity link:
       
$text = replace_outside_code_and_short_tags( '#@([a-z0-9_.\-]+)#i', '@', $text, array( $this, 'replace_usernames' ) );

       
// Make tag names clickable:
       
$text = $this->replace_tags( $text );

        return
$text;
    }


   
/**
     * This is the 2nd level of callback!!
     *
     * @param array The matches of regexp:
     *     1 => punctuation signs before word
     *     2 => a clear word without punctuation signs
     *     3 => punctuation signs after word
     */
   
function replace_callback( $matches )
    {
        global
$disp, $Item;

       
$link_attrs = '';
        if(
$this->setting_nofollow_auto )
        {    
// Add attribute rel="nofollow" for auto-links:
           
$link_attrs .= ' rel="nofollow"';
        }

       
$before_word = $matches[1];
       
$word = $matches[2];
       
$after_word = $matches[3];
        if(
substr( $word, -1 ) == '.' )
        {
// If word has a dot in the end
           
$word = substr( $word, 0, -1 );
           
$after_word = '.'.$after_word;
        }
       
$lword = utf8_strtolower( $word );
       
$r = $before_word.$word.$after_word;

        if( isset(
$this->replacement_link_array[ $lword ] ) )
        {
// There is an autolink definition with the current word
           
if( ! empty( $this->previous_lword ) && isset( $this->replacement_link_array[ $lword ][ $this->previous_lword ] ) )
            {
// Set an previous word and url from config array:
                // An optional previous required word (allows to create groups of 2 words)
               
$previous = $this->previous_lword;
               
// Url for current word
               
$url = $this->replacement_link_array[ $lword ][ $this->previous_lword ];
            }
            else
            {
// No previous word, it is a single word
               
foreach( $this->replacement_link_array[ $lword ] as $previous => $url )
                {
// Initialize an optional previous required word and url as first of the current word
                   
break;
                }
            }

            if( !
preg_match( '#(^|[a-z]+:)//#', $url ) )
            {    
// Use default URL scheme if it is not defined by config:
               
$url = 'http://'.$url;
            }

            if(
in_array( $url, $this->already_linked_array ) || in_array( $lword, $this->already_linked_usernames ) )
            {
// Do not repeat link to same destination:
                // pre_dump( 'already linked:'. $url );
                // save previous word in original and lower case format with the after word signs
               
$this->previous_word = $word.$after_word;
               
$this->previous_lword = $lword.$after_word;
               
$this->previous_used = false;
                return
$r;
            }

            if( (
$disp == 'single' || $disp == 'page' ) &&
                isset(
$Item ) &&
               
$Item->get_permanent_url() == $url )
            {    
// Do not make a link to same permalink URL of the current Item:
               
return $r;
            }

            if( !empty(
$previous ) )
            {
// This definitions is a group of two word separated with space
               
if( $this->previous_used || ( $this->previous_lword != $previous ) )
                {
// We do not have the required previous word or it was already used to another autolink definition
                    // pre_dump( 'previous word does not match', $this->previous_lword, $previous );
                    // save previous word in original and lower case format with the after word signs
                   
$this->previous_word = $word.$after_word;
                   
$this->previous_lword = $lword.$after_word;
                   
$this->previous_used = false;
                    return
$r;
                }
               
$r = '==!#DEL#!==<a href="'.$url.'"'.$link_attrs.'>'.$this->previous_word.' '.$word.'</a>'.$after_word;
            }
            else
            {
// Single word
               
$r = $before_word.'<a href="'.$url.'"'.$link_attrs.'>'.$word.'</a>'.$after_word;
            }

           
// Make sure we don't link to same destination twice in the same text/post:
           
$this->already_linked_array[] = $url;
           
// Mark that the previous word was already converted to a link
           
$this->previous_used = true;
        }
        else
        {
// Mark that the previous word was NOT converted to a link
           
$this->previous_used = false;
        }

       
// save previous word in original and lower case format with the after word signs
        // Note: after_word signs are important to be saved because in case of autlink definitions with two words the first word must have exact matching at the end!
       
$this->previous_word = $word.$after_word;
       
$this->previous_lword = $lword.$after_word;

        return
$r;
    }


   
/**
     * Replace @usernames with link to profile page
     *
     * @param string Content
     * @param array Search list
     * @param array Replace list
     * @return string Content
     */
   
function replace_usernames( $content, $search_list, $replace_list )
    {
        if( empty(
$this->setting_autolink_username ) )
        {    
// No data to correct username linking, Exit here:
           
return $content;
        }

        if(
preg_match_all( $search_list, $content, $user_matches ) )
        {
           
// Add this for rel attribute in order to activate bubbletips on usernames
           
$link_attrs = ' rel="bubbletip_user_%user_ID%"';
           
$link_attrs .= ' class="user"';

            if( !empty(
$user_matches[1] ) )
            {
               
$UserCache = & get_UserCache();
                foreach(
$user_matches[1] as $u => $username )
                {
                    if(
in_array( $username, $this->already_linked_usernames ) )
                    {    
// Skip this username, it was already linked before
                       
continue;
                    }

                   
$username_source = $user_matches[0][ $u ];
                    if( ! (
$User = & $UserCache->get_by_login( $username ) ) )
                    {    
// If user is not found try to find by removing a dot at the end,
                        // because usernames may contain dot and also we have a case like "some text to @username.",
                        // so firstly we find user by 'username.' and then by 'username':
                       
$username_without_dot = rtrim( $username, '.' );
                        if(
$username_without_dot != $username &&
                            (
$User = & $UserCache->get_by_login( $username_without_dot ) ) )
                        {    
// We found user by username without dots at the end:
                           
$username = $username_without_dot;
                           
$username_source = rtrim( $username_source, '.' );
                           
// Check again but already for username without dot:
                           
if( in_array( $username, $this->already_linked_usernames ) )
                            {    
// Skip this username, it was already linked before:
                               
continue;
                            }
                        }
                    }

                    if(
$User )
                    {    
// Replace @usernames
                       
$user_link_attrs = str_replace( '%user_ID%', $User->ID, $link_attrs );
                       
$user_link = '<a href="'.$User->get_userpage_url( $this->current_Blog->ID ).'"'.$user_link_attrs.'>'.$username_source.'</a>';
                       
$content = preg_replace( '#'.preg_quote( $username_source, '#' ).'#', $user_link, $content, 1 );
                       
$this->already_linked_usernames[] = $username;
                    }
                }
            }
        }

        return
$content;
    }


   
/**
     * Load tags array of collection
     *
     * @param object Blog
     */
   
function load_tags_array( $Blog )
    {
        if( empty(
$this->setting_autolink_tag ) )
        {    
// Don't load tags because it is not required by current settings:
           
return;
        }

        if(
is_array( $this->tags_array ) )
        {    
// The tags array is already initialized, Don't do this twice, Exit here:
           
return;
        }

       
// Get all tags from published posts of the requested collection:
       
$coll_tags = get_tags( $Blog->ID );

       
$this->tags_array = array();
        foreach(
$coll_tags as $coll_tag )
        {
           
$this->tags_array[] = $coll_tag->tag_name;
        }
    }


   
/**
     * Replace tag names with link to filter posts by the tag
     *
     * @param string Content
     * @return string Content
     */
   
function replace_tags( $content )
    {
        if( empty(
$this->setting_autolink_tag ) || empty( $this->tags_array ) || empty( $this->current_Blog ) )
        {    
// No data to correct tag linking, Exit here:
           
return $content;
        }

       
$new_linked_tags = array();
       
$tag_search_patterns = array();
       
$tag_replace_strings = array();
        foreach(
$this->tags_array as $tag_name )
        {
            if(
in_array( $tag_name, $this->already_linked_tags ) )
            {    
// Skip this tag because it has been already linked before:
               
continue;
            }
            if(
stristr( $content, $tag_name ) !== false )
            {    
// Replace tag name with its link if it is found in text:
               
$tag_search_patterns[] = '#(^|\W)('.preg_quote( $tag_name, '#' ).')(\W|$)#iu';
               
$tag_replace_strings[] = '$1'.$this->current_Blog->get_tag_link( $tag_name, "$2" ).'$3';
               
// Mark this tag as linked and don't link it twice:
               
$new_linked_tags[] = $tag_name;
            }
        }

        if(
count( $tag_search_patterns ) )
        {    
// Do replacement if at least one tag is found in content:
           
if( stristr( $content, '<a' ) !== false )
            {    
// Don't link tags in body of already existing links:
               
$content = callback_on_non_matching_blocks( $content, '~<a[^>]*>.*?</a>~is',
                   
'replace_content', array( $tag_search_patterns, $tag_replace_strings, 'preg', 1 ) );
            }
            else
            {    
// Replace in whole content:
               
$content = replace_content( $content, $tag_search_patterns, $tag_replace_strings, 'preg', 1 );
            }
        }

        foreach(
$new_linked_tags as $n => $new_linked_tag )
        {
            if(
stristr( $content, $new_linked_tag.'</a>' ) === false )
            {    
// This tag was not linked really in this call, Skip it:
                // It may happens when one tag is a substring of other tag, Example:
                //     - First tag is "long tag"
                //     - Second tag is "test long tag name"
                //     - $text = "1 test long tag name 2"
                //     So we should not mark the second as linked because only the first tag is linked only first time,
                //     and we should keep the second tag for other strings.
               
unset( $new_linked_tags[ $n ] );
            }
        }

        if(
count( $new_linked_tags ) )
        {    
// Append new linked tags to skip them in next times:
           
$this->already_linked_tags = array_merge( $this->already_linked_tags, $new_linked_tags );
        }

        return
$content;
    }
}

?>