b2evolution PHP Cross Reference Blogging Systems

Source: /plugins/_autolinks.plugin.php - 607 lines - 19413 bytes - Summary - Text - Print

Description: This file implements the Automatic Links plugin for b2evolution b2evolution - {@link http://b2evolution.net/} Released under GNU GPL License - {@link http://b2evolution.net/about/license.html}

   1  <?php
   2  /**
   3   * This file implements the Automatic Links plugin for b2evolution
   4   *
   5   * b2evolution - {@link http://b2evolution.net/}
   6   * Released under GNU GPL License - {@link http://b2evolution.net/about/license.html}
   7   * @copyright (c)2003-2014 by Francois Planque - {@link http://fplanque.com/}
   8   *
   9   * @package plugins
  10   */
  11  if( !defined('EVO_MAIN_INIT') ) die( 'Please, do not access this page directly.' );
  12  
  13  
  14  /**
  15   * Automatic links plugin.
  16   *
  17   * @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
  18   *   - marking external and internal (relative URL or on the blog's URL) links with a HTML/CSS class
  19   *   - add e.g. 'target="_blank"' to external links
  20   * @todo Add "max. displayed length setting" and add full title + dots in the middle to shorten it.
  21   *       (e.g. plain long URLs with a lot of params and such). This should not cause the layout to
  22   *       behave ugly. This should only shorten non-whitespace strings in the link's innerHTML of course.
  23   *
  24   * @package plugins
  25   */
  26  class autolinks_plugin extends Plugin
  27  {
  28      var $code = 'b2evALnk';
  29      var $name = 'Auto Links';
  30      var $priority = 60;
  31      var $version = '5.0.0';
  32      var $group = 'rendering';
  33      var $short_desc;
  34      var $long_desc;
  35      var $help_url = 'http://b2evolution.net/man/technical-reference/renderer-plugins/autolinks-plugin';
  36      var $number_of_installs = null;    // Let admins install several instances with potentially different word lists
  37  
  38      /**
  39       * Lazy loaded from txt files
  40       *
  41       * @var array of array for each blog. Index 0 is for shared content
  42       */
  43      var $link_array = array();
  44  
  45      var $already_linked_array;
  46  
  47      /**
  48       * Previous word from the text during the make clickable process
  49       *
  50       * @var string
  51       */
  52      var $previous_word = null;
  53      /**
  54       * Previous word in lower case format
  55       *
  56       * @var string
  57       */
  58      var $previous_lword = null;
  59      /**
  60       * Shows if the previous word was already used/converted to a link
  61       *
  62       * @var boolean
  63       */
  64      var $previous_used = false;
  65  
  66      var $already_linked_usernames;
  67  
  68      /**
  69       * Init
  70       */
  71  	function PluginInit( & $params )
  72      {
  73          $this->short_desc = T_('Make URLs and specific terms/defintions clickable');
  74          $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.');
  75      }
  76  
  77  
  78      /**
  79       * @return array
  80       */
  81  	function GetDefaultSettings()
  82      {
  83          global $rsc_subdir;
  84          return array(
  85                  'autolink_urls' => array(
  86                          'label' => T_( 'Autolink URLs' ),
  87                          'defaultvalue' => 1,
  88                          'type' => 'checkbox',
  89                          'note' => T_('Autolink URLs starting with http: https: mailto: aim: icq: as well as adresses of the form www.*.* or *@*.*'),
  90                      ),
  91                  'autolink_defs_default' => array(
  92                          'label' => T_( 'Autolink definitions' ),
  93                          'defaultvalue' => 1,
  94                          'type' => 'checkbox',
  95                          'note' => T_('As defined in definitions.default.txt'),
  96                      ),
  97                  'autolink_defs_local' => array(
  98                          'label' => '',
  99                          'defaultvalue' => 0,
 100                          'type' => 'checkbox',
 101                          'note' => T_('As defined in definitions.local.txt'),
 102                      ),
 103                  'autolink_defs_db' => array(
 104                          'label' => T_('Custom definitions'),
 105                          'type' => 'html_textarea',
 106                          'rows' => 15,
 107                          'note' => $this->T_( 'Enter custom definitions above.' ),
 108                          'defaultvalue' => '',
 109                      ),
 110              );
 111      }
 112  
 113  
 114      /**
 115       * Define here default collection/blog settings that are to be made available in the backoffice.
 116       *
 117       * @return array See {@link Plugin::GetDefaultSettings()}.
 118       */
 119  	function get_coll_setting_definitions( & $params )
 120      {
 121          $default_values = array(
 122                  'autolink_defs_coll_db'              => '',
 123                  'autolink_username'                  => 0,
 124                  'autolink_post_nofollow_exist'       => 0,
 125                  'autolink_post_nofollow_explicit'    => 0,
 126                  'autolink_post_nofollow_auto'        => 0,
 127                  'autolink_comment_nofollow_exist'    => 1,
 128                  'autolink_comment_nofollow_explicit' => 1,
 129                  'autolink_comment_nofollow_auto'     => 0,
 130              );
 131  
 132          if( !empty( $params['blog_type'] ) )
 133          {    // Set the default settings depends on blog type
 134              switch( $params['blog_type'] )
 135              {
 136                  case 'forum':
 137                  case 'manual':
 138                      $default_values['autolink_post_nofollow_exist'] = 1;
 139                      $default_values['autolink_post_nofollow_explicit'] = 1;
 140                      break;
 141              }
 142          }
 143  
 144          // set params to allow rendering for comments by default
 145          $default_params = array_merge( $params, array( 'default_comment_rendering' => 'stealth' ) );
 146          return array_merge( parent::get_coll_setting_definitions( $default_params ),
 147              array(
 148                  'autolink_defs_coll_db' => array(
 149                          'label' => T_( 'Custom autolink definitions' ),
 150                          'type' => 'html_textarea',
 151                          'rows' => 15,
 152                          'note' => $this->T_( 'Enter custom definitions above.' ),
 153                          'defaultvalue' => $default_values['autolink_defs_coll_db'],
 154                      ),
 155                  'autolink_username' => array(
 156                          'label' => T_( 'Autolink usernames' ),
 157                          'type' => 'checkbox',
 158                          'note' => $this->T_( '@username will link to the user profile page' ),
 159                          'defaultvalue' => $default_values['autolink_username'],
 160                      ),
 161                  // No follow in posts
 162                  'autolink_post_nofollow_exist' => array(
 163                          'label' => T_( 'No follow in posts' ),
 164                          'type' => 'checkbox',
 165                          'note' => $this->T_( 'Add rel="nofollow" to pre-existings links' ),
 166                          'defaultvalue' => $default_values['autolink_post_nofollow_exist'],
 167                      ),
 168                  'autolink_post_nofollow_explicit' => array(
 169                          'label' => '',
 170                          'type' => 'checkbox',
 171                          'note' => $this->T_( 'Add rel="nofollow" to explicit links' ),
 172                          'defaultvalue' => $default_values['autolink_post_nofollow_explicit'],
 173                      ),
 174                  'autolink_post_nofollow_auto' => array(
 175                          'label' => '',
 176                          'type' => 'checkbox',
 177                          'note' => $this->T_( 'Add rel="nofollow" to auto-links' ),
 178                          'defaultvalue' => $default_values['autolink_post_nofollow_auto'],
 179                      ),
 180                  // No follow in comments
 181                  'autolink_comment_nofollow_exist' => array(
 182                          'label' => T_( 'No follow in comments' ),
 183                          'type' => 'checkbox',
 184                          'note' => $this->T_( 'Add rel="nofollow" to pre-existings links' ),
 185                          'defaultvalue' => $default_values['autolink_comment_nofollow_exist'],
 186                      ),
 187                  'autolink_comment_nofollow_explicit' => array(
 188                          'label' => '',
 189                          'type' => 'checkbox',
 190                          'note' => $this->T_( 'Add rel="nofollow" to explicit links' ),
 191                          'defaultvalue' => $default_values['autolink_comment_nofollow_explicit'],
 192                      ),
 193                  'autolink_comment_nofollow_auto' => array(
 194                          'label' => '',
 195                          'type' => 'checkbox',
 196                          'note' => $this->T_( 'Add rel="nofollow" to auto-links' ),
 197                          'defaultvalue' => $default_values['autolink_comment_nofollow_auto'],
 198                      ),
 199              )
 200          );
 201      }
 202  
 203  
 204      /**
 205       * Lazy load global definitions array
 206       *
 207       * @param Blog
 208       */
 209  	function load_link_array( $Blog )
 210      {
 211          global $plugins_path;
 212  
 213          if( !isset($this->link_array[0]) )
 214          {    // global defs NOT already loaded
 215              $this->link_array[0] = array();
 216  
 217              if( $this->Settings->get( 'autolink_defs_default' ) )
 218              {    // Load defaults:
 219                  $this->read_csv_file( $plugins_path.'autolinks_plugin/definitions.default.txt', 0 );
 220              }
 221              if( $this->Settings->get( 'autolink_defs_local' ) )
 222              {    // Load local user defintions:
 223                  $this->read_csv_file( $plugins_path.'autolinks_plugin/definitions.local.txt', 0 );
 224              }
 225              $text = $this->Settings->get( 'autolink_defs_db', 0 );
 226              if( !empty($text) )
 227              {    // Load local user defintions:
 228                  $this->read_textfield( $text, 0 );
 229              }
 230          }
 231  
 232          // load defs for current blog:
 233          $coll_ID = $Blog->ID;
 234          if( !isset($this->link_array[$coll_ID]) )
 235          {    // This blog is not loaded yet:
 236              $this->link_array[$coll_ID] = array();
 237              $text = $this->get_coll_setting( 'autolink_defs_coll_db', $Blog );
 238              if( !empty($text) )
 239              {    // Load local user defintions:
 240                  $this->read_textfield( $text, $coll_ID );
 241              }
 242          }
 243  
 244          // Prepare working link array:
 245          $this->replacement_link_array = array_merge( $this->link_array[0], $this->link_array[$coll_ID] );
 246      }
 247  
 248  
 249      /**
 250        * Load contents of one specific CSV file
 251       *
 252       * @param string $filename
 253       */
 254  	function read_csv_file( $filename, $coll_ID )
 255      {
 256          if( ! $handle = @fopen( $filename, 'r') )
 257          {    // File could not be opened:
 258              return;
 259          }
 260  
 261          while( ($data = fgetcsv($handle, 1000, ';', '"')) !== false )
 262          {
 263              $this->read_line( $data, $coll_ID );
 264          }
 265  
 266          fclose($handle);
 267      }
 268  
 269  
 270      /**
 271        * Load contents of one large textfield to be treated as CSV
 272        *
 273        * Note: This method is probably not well suited for very large lists.
 274       *
 275       * @param string $filename
 276       */
 277  	function read_textfield( $text, $coll_ID )
 278      {
 279          // split into lines:
 280          $lines = preg_split( '#\r|\n#', $text );
 281  
 282          foreach( $lines as $line )
 283          {
 284              // CSV style decoding in memory:
 285              // $keywords = preg_split( "/[\s,]*\\\"([^\\\"]+)\\\"[\s,]*|[\s,]+/", "textline with, commas and \"quoted text\" inserted", 0, PREG_SPLIT_DELIM_CAPTURE );
 286              $data = explode( ';', $line );
 287              $this->read_line( $data, $coll_ID );
 288          }
 289      }
 290  
 291  
 292      /**
 293       * read line
 294       *
 295       * @param exploded $data array
 296       */
 297  	function read_line( $data, $coll_ID )
 298      {
 299          if( empty( $data[0] ) )
 300          {    // Skip empty and comment lines
 301              return;
 302          }
 303  
 304          $word = $data[0];
 305          $url = isset( $data[3] ) ? $data[3] : NULL;
 306          if( $url == '-' || empty( $url ) )
 307          {    // Remove URL (useful to remove some defs on a specific site):
 308              unset( $this->link_array[0][$word] );
 309              unset( $this->link_array[$coll_ID][$word] );
 310          }
 311          else
 312          {
 313              $this->link_array[$coll_ID][$word] = array( $data[1], $url );
 314          }
 315      }
 316  
 317  
 318      /**
 319       * Perform rendering
 320       *
 321       * @param array Associative array of parameters
 322       *                             (Output format, see {@link format_to_output()})
 323       * @return boolean true if we can render something for the required output format
 324       */
 325  	function RenderItemAsHtml( & $params )
 326      {
 327          $content = & $params['data'];
 328          $Item = & $params['Item'];
 329          /**
 330           * @var Blog
 331           */
 332          $item_Blog = $params['Item']->get_Blog();
 333  
 334          // Define the setting names depending on what is rendering now
 335          if( !empty( $params['Comment'] ) )
 336          {    // Comment is rendering
 337              $this->setting_nofollow_exist = 'autolink_comment_nofollow_exist';
 338              $this->setting_nofollow_explicit = 'autolink_comment_nofollow_explicit';
 339              $this->setting_nofollow_auto = 'autolink_comment_nofollow_auto';
 340          }
 341          else
 342          {    // Item is rendering
 343              $this->setting_nofollow_exist = 'autolink_post_nofollow_exist';
 344              $this->setting_nofollow_explicit = 'autolink_post_nofollow_explicit';
 345              $this->setting_nofollow_auto = 'autolink_post_nofollow_auto';
 346          }
 347  
 348          // Prepare existing links
 349          $content = $this->prepare_existing_links( $content, $item_Blog );
 350  
 351          // reset already linked usernames
 352          $this->already_linked_usernames = array();
 353          if( !empty( $item_Blog ) && $this->get_coll_setting( 'autolink_username', $item_Blog ) )
 354          {    // Replace @usernames with user identity link
 355              $content = replace_content_outcode( '#@([A-Za-z0-9_.]+)#i', '@', $content, array( $this, 'replace_usernames' ) );
 356          }
 357  
 358          // load global defs
 359          $this->load_link_array( $item_Blog );
 360  
 361          // reset already linked:
 362          $this->already_linked_array = array();
 363          if( preg_match_all( '|[\'"](http://[^\'"]+)|i', $content, $matches ) )
 364          {    // There are existing links:
 365              $this->already_linked_array = $matches[1];
 366          }
 367  
 368          $link_attrs = '';
 369          if( !empty( $item_Blog ) && $this->get_coll_setting( $this->setting_nofollow_explicit, $item_Blog ) )
 370          {    // Add attribute rel="nofollow" for auto-links
 371              $link_attrs .= ' rel="nofollow"';
 372          }
 373  
 374          if( $this->Settings->get( 'autolink_urls' ) )
 375          {    // First, make the URLs clickable:
 376              $content = make_clickable( $content, '&amp;', 'make_clickable_callback', $link_attrs );
 377          }
 378  
 379          if( !empty( $this->replacement_link_array ) )
 380          {    // Make the desired remaining terms/definitions clickable:
 381              $content = make_clickable( $content, '&amp;', array( $this, 'make_clickable_callback' ), $link_attrs );
 382          }
 383  
 384          return true;
 385      }
 386  
 387  
 388  	function FilterCommentContent( & $params )
 389      {
 390          $Comment = & $params['Comment'];
 391          $comment_Item = & $Comment->get_Item();
 392          $item_Blog = & $comment_Item->get_Blog();
 393          if( in_array( $this->code, $Comment->get_renderers_validated() ) )
 394          { // Always allow rendering for comment
 395              $render_params = array_merge( array( 'data' => & $Comment->content, 'Item' => & $comment_Item ), $params );
 396              $this->RenderItemAsHtml( $render_params );
 397          }
 398          return false;
 399      }
 400  
 401  
 402      /**
 403       * Callback function for {@link make_clickable()}.
 404       *
 405       * @param string Text
 406       * @param string Url delimeter
 407       * @return string The clickable text.
 408       */
 409  	function make_clickable_callback( $text, $moredelim = '&amp;' )
 410      {
 411          global $evo_charset;
 412  
 413          $regexp_modifier = '';
 414          if( $evo_charset == 'utf-8' )
 415          { // Add this modifier to work with UTF-8 strings correctly
 416              $regexp_modifier = 'u';
 417          }
 418  
 419          // Previous word in lower case format
 420          $this->previous_lword = null;
 421          // Previous word was already used/converted to a link
 422          $this->previous_used = false;
 423  
 424          // 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.
 425          $text_words = explode( ' ', evo_strtolower( $text ) );
 426          foreach( $text_words as $text_word )
 427          { // Trim the signs [({/ from start and the signs ])}/.,:;!? from end of each word
 428              $clear_word = preg_replace( '#^[\[\({/]?([@\p{L}0-9_\-\.]{3,})[\.,:;!\?\]\)}/]?$#i', '$1', $text_word );
 429              if( $clear_word != $text_word )
 430              { // Append a clear word to array if word has the punctuation signs
 431                  $text_words[] = $clear_word;
 432              }
 433          }
 434          // Check if a content has at least one definition to make an url from word
 435          $text_contains_replacement = ( count( array_intersect( $text_words, array_keys( $this->replacement_link_array ) ) ) > 0 );
 436          if( $text_contains_replacement )
 437          { // Find word with 3 characters at least:
 438              $text = preg_replace_callback( '#(^|\s|[(),;\[{/])([@\p{L}0-9_\-\.]{3,})([\.,:;!\?\]\)}/]?)#i'.$regexp_modifier, array( & $this, 'replace_callback' ), $text );
 439          }
 440  
 441          // Cleanup words to be deleted:
 442          $text = preg_replace( '/[@\p{L}0-9_\-]+\s*==!#DEL#!==/i'.$regexp_modifier, '', $text );
 443  
 444          return $text;
 445      }
 446  
 447  
 448      /**
 449       * This is the 2nd level of callback!!
 450       *
 451       * @param array The matches of regexp:
 452       *     1 => punctuation signs before word
 453       *     2 => a clear word without punctuation signs
 454       *     3 => punctuation signs after word
 455       */
 456  	function replace_callback( $matches )
 457      {
 458          global $Blog;
 459  
 460          $link_attrs = '';
 461          if( !empty( $Blog ) && $this->get_coll_setting( $this->setting_nofollow_auto, $Blog ) )
 462          {    // Add attribute rel="nofollow" for auto-links
 463              $link_attrs .= ' rel="nofollow"';
 464          }
 465  
 466          $before_word = $matches[1];
 467          $word = $matches[2];
 468          $after_word = $matches[3];
 469          if( substr( $word, -1 ) == '.' )
 470          { // If word has a dot in the end
 471              $word = substr( $word, 0, -1 );
 472              $after_word = '.'.$after_word;
 473          }
 474          $lword = evo_strtolower( $word );
 475          $r = $before_word.$word.$after_word;
 476  
 477          if( isset( $this->replacement_link_array[ $lword ] ) )
 478          { // There is an autolink definition with the current word
 479              // An optional previous required word (allows to create groups of 2 words)
 480              $previous = $this->replacement_link_array[ $lword ][0];
 481              // Url for current word
 482              $url = 'http://'.$this->replacement_link_array[ $lword ][1];
 483  
 484              if( in_array( $url, $this->already_linked_array ) || in_array( $lword, $this->already_linked_usernames ) )
 485              { // Do not repeat link to same destination:
 486                  // pre_dump( 'already linked:'. $url );
 487                  // save previous word in original and lower case format with the after word signs
 488                  $this->previous_word = $word.$after_word;
 489                  $this->previous_lword = $lword.$after_word;
 490                  $this->previous_used = false;
 491                  return $r;
 492              }
 493  
 494              if( !empty( $previous ) )
 495              { // This definitions is a group of two word separated with space
 496                  if( $this->previous_used || ( $this->previous_lword != $previous ) )
 497                  { // We do not have the required previous word or it was already used to another autolink definition
 498                      // pre_dump( 'previous word does not match', $this->previous_lword, $previous );
 499                      // save previous word in original and lower case format with the after word signs
 500                      $this->previous_word = $word.$after_word;
 501                      $this->previous_lword = $lword.$after_word;
 502                      $this->previous_used = false;
 503                      return $r;
 504                  }
 505                  $r = '==!#DEL#!==<a href="'.$url.'"'.$link_attrs.'>'.$this->previous_word.' '.$word.'</a>'.$after_word;
 506              }
 507              else
 508              { // Single word
 509                  $r = $before_word.'<a href="'.$url.'"'.$link_attrs.'>'.$word.'</a>'.$after_word;
 510              }
 511  
 512              // Make sure we don't link to same destination twice in the same text/post:
 513              $this->already_linked_array[] = $url;
 514              // Mark that the previous word was already converted to a link
 515              $this->previous_used = true;
 516          }
 517          else
 518          { // Mark that the previous word was NOT converted to a link
 519              $this->previous_used = false;
 520          }
 521  
 522          // save previous word in original and lower case format with the after word signs
 523          // 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!
 524          $this->previous_word = $word.$after_word;
 525          $this->previous_lword = $lword.$after_word;
 526  
 527          return $r;
 528      }
 529  
 530  
 531      /**
 532       * Prepare existing links
 533       *
 534       * @param string Text
 535       * @param object Blog
 536       * @return string Prepared text
 537       */
 538  	function prepare_existing_links( $text, $Blog )
 539      {
 540          if( !empty( $Blog ) && $this->get_coll_setting( $this->setting_nofollow_exist, $Blog ) )
 541          {    // Add attribute rel="nofollow" for preexisting links
 542              // Remove all existing attributes "rel" from tag <a>
 543              $text = preg_replace( '#<a([^>]*) rel="([^"]+?)"([^>]*)>#is', '<a$1$3>', $text );
 544              // Add rel="nofollow"
 545              $text = preg_replace( '#(<a[^>]+?)>#is', '$1 rel="nofollow">', $text );
 546          }
 547  
 548          return $text;
 549      }
 550  
 551  
 552      /**
 553       * Replace @usernames with link to profile page
 554       *
 555       * @param string Content
 556       * @param array Search list
 557       * @param array Replace list
 558       * @return string Content
 559       */
 560  	function replace_usernames( $content, $search_list, $replace_list )
 561      {
 562          global $Blog;
 563  
 564          if( empty( $Blog ) )
 565          {    // No Blog, Exit here
 566              return $content;
 567          }
 568  
 569          if( preg_match_all( $search_list, $content, $user_matches ) )
 570          {
 571              $blog_url = $Blog->gen_blogurl();
 572  
 573              // Add this for rel attribute in order to activate bubbletips on usernames
 574              $link_attr_rel = 'bubbletip_user_%user_ID%';
 575  
 576              if( $this->get_coll_setting( $this->setting_nofollow_auto, $Blog ) )
 577              {    // Add attribute rel="nofollow" for auto-links
 578                  $link_attr_rel .= ' nofollow';
 579              }
 580              $link_attrs = ' rel="'.$link_attr_rel.'"';
 581  
 582              if( !empty( $user_matches[1] ) )
 583              {
 584                  $UserCache = & get_UserCache();
 585                  foreach( $user_matches[1] as $u => $username )
 586                  {
 587                      if( in_array( $username, $this->already_linked_usernames ) )
 588                      {    // Skip this username, it was already linked before
 589                          continue;
 590                      }
 591  
 592                      if( $User = & $UserCache->get_by_login( $username ) )
 593                      {    // Replace @usernames
 594                          $user_link_attrs = str_replace( '%user_ID%', $User->ID, $link_attrs );
 595                          $user_link = '<a href="'.url_add_param( $blog_url, 'disp=user&amp;user_ID='.$User->ID ).'"'.$user_link_attrs.'>'.$user_matches[0][ $u ].'</a>';
 596                          $content = preg_replace( '#'.$user_matches[0][ $u ].'#', $user_link, $content, 1 );
 597                          $this->already_linked_usernames[] = $user_matches[1][ $u ];
 598                      }
 599                  }
 600              }
 601          }
 602  
 603          return $content;
 604      }
 605  }
 606  
 607  ?>

title

Description

title

Description

title

Description

title

title

Body