How to create a Macro

One of the problem with publishing your email address on the internet is that eventually some malicious spider bots will harvest email addresses that you list on your website. One of the solution is to obfuscate it with Javascript. The challenge is how to keep the convenience of simply typing an email address in the WYSIWYG editor without having to manually copy and paste the code from many obfuscators that you can find with internet search. Moreover, what if there are 30 email addresses? Do we have to copy and paste it from obfuscators everytime? What if those obfuscators are also collecting email addresses?

The solution to this problem is to write a macro that automatically converts email addresses to obfuscated Javascript code when a page is requested (either by spider bots or human with a web browser). When a website editor/owner type an email address in the WYSIWYG editor, it will not make any changes. It only change the output when the page is requested. Your task is to write a macro that will automatically and obfuscate email addresses in application pages with Javascript so a spam harvester bot will be misled.

To create a macro, do the following steps:

  1. Create a new macro using Plugin Creator.
  2. Start coding
    1. Configure config.template.php
    2. Write the macro function to search and replace email address
    3. Test it
  3. Publish it to Extensions Directory (registration required)

1. Create a new macro using Plugin Creator

From Plugin Creator, click New Macro. Have a 128x128 pixels transparent PNG icon ready. If you don't have one, look for a commercial-free icon from http://www.iconarchive.com. Determine the name, for instance - HideEmail.

Create a macro with SCHLIX Plugin Creator

Once you've clicked Save, a skeleton code will be generated in your /web/{site_name}/macros/{macro_name} folder.

Macro saved

Ensure that your macro is enabled so you can test it while you're coding it by going to the Macro Manager and change the status.

Activate macro

2. Start Coding

a. Configure config.template.php

Let's give an option to customize the message to the website editor/owner.

<?php
/**
 * HideEmail - Configuration
 * 
 * Macro to obfuscate mailto link in a-href to prevent spammer
 *
 * @copyright 2020 ABC Web Design Agency
 *
 * @license MIT
 *
 * @package hideemail
 * @version 1.0
 * @author  ABC Web Design Agency <info@mycompany.com>
 * @link    https://www.mycompany.com
 */
if (!defined('SCHLIX_VERSION'))
    die('No Access');
?>
<h3><?= ___('Hide Email Macro') ?></h3>
<p>This macro converts <?= ___h('<a href="mailto:email@example.com">Email</a>') ?> to obfuscated Javascript code to prevent email harvesting</p>

<schlix-config:textbox config-key="str_text_to_display" label="<?= ('Text to display to spider bots') ?>" config-default-value="xxxxxxxxxxx" placeholder="<?= ___('Type the text to display when Javascript is disabled') ?>" />

It should look like this when you go to the Macro Manager.

SCHLIX macro config example

b. Write the macro function to search and replace email address

We're going to delete some of the pre-generated code from Plugin Creator and add a logic to the macro.

<?php
namespace Macro;
/**
 * HideEmail - macro class
 * 
 * Macro to obfuscate mailto link in a-href to prevent spammer
 * 
 * @copyright 2020 ABC Web Design Agency
 *
 * @license MIT
 *
 * @package hideemail
 * @version 1.0
 * @author  ABC Web Design Agency <info@mycompany.com>
 * @link    https://www.mycompany.com
 */

class HideEmail extends \SCHLIX\cmsMacro {

    /**
     * Text to display
     * @var string 
     */
    protected $str_text_to_display;
    /**
     * Unused for this example, just ignore
     * @var mixed
     */
    protected static $has_this_macro_been_called;

    /**
     * Logic part 1 - replace all mailto links
     * @param string $html
     * @return string
     */
    private function replaceAllMailtoLinks($html)
    {
        // mission: 
        // 1) search for all <a href="mailto:*">innerhtml</a>
        // 2) replace it with obfuscated javascript link
        // 3) when viewed by spider bots/spam harvesters, it will be just
        //    something like <span id="e1234567">[xxxxxxxxxxxxx]</span>
        preg_match_all ("/<a href=\"mailto\:([^\"]+)\"[^>]*>([^<]*)<\/a>/",$html,$matches);
        // if the regular expression yields matches
        if (___c($matches) > 0)
        {
            $count = ___c($matches[0]);
            for($i = 0; $i < $count; $i++)
            {            
                // the full <a href"mailto:*">innerhtml</a>
                $full_tag_to_replace = $matches[0][$i];    
                // the email address part
                $ahref_mailto =  $matches[1][$i];
                // the innerhtml
                $ahref_inner_html =  $matches[2][$i];
                if (filter_var($ahref_mailto, FILTER_VALIDATE_EMAIL) !== FALSE)
                {
                     $replacement = $this->hideEmailAddress($ahref_mailto, $ahref_inner_html);
                     $html = str_replace($full_tag_to_replace, $replacement, $html);
                }
            }
        }
        return $html;
    }


    /**
     * Logic part 2 - convert email address to Javascript link
     * Source code copied and pasted from: http://www.maurits.vdschee.nl/php_hide_email/
     * with slight modification (added $inner_html)
     * @param string $mailto_link
     * @param string $inner_html
     * @return string
     */
    private function hideEmailAddress($mailto_link, $inner_html)
    { 
      // the inner part of the link, which could be an email address
      // or it could be just a text. 
      // if it's an email address, let's replace it as well
      if (filter_var($inner_html, FILTER_VALIDATE_EMAIL) !== FALSE)
      {
         list($name, $domain) = explode('@', $inner_html);
         $inner_html = $name.' [at] '.$domain;         
      }
      // end modification of http://www.maurits.vdschee.nl/php_hide_email/
      $character_set = '+-.0123456789@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz';
      $key = str_shuffle($character_set); $cipher_text = ''; $id = 'e'.rand(1,999999999);
      for ($i=0;$i<strlen($mailto_link);$i+=1) $cipher_text.= $key[strpos($character_set,$mailto_link[$i])];
      $script = 'var a="'.$key.'";var b=a.split("").sort().join("");var c="'.$cipher_text.'";var d="";';
      $script.= 'for(var e=0;e<c.length;e++)d+=b.charAt(a.indexOf(c.charAt(e)));';
      $script.= 'document.getElementById("'.$id.'").innerHTML="<a href=\\"mailto:"+d+"\\">'.$inner_html.'</a>"';
      $script = "eval(\"".str_replace(array("\\",'"'),array("\\\\",'\"'), $script)."\")"; 
      $script = '<script type="text/javascript">/*<![CDATA[*/'.$script.'/*]]>*/</script>';
      // always use ___h (htmlspecialchars)
      return '<span id="'.$id.'">['.___h($this->str_text_to_display).']</span>'.$script;
    }


    /*
     * Run the macro
     * @global \SCHLIX\cmsHTMLPageHeader $HTMLHeader
     * @param array|string $data
     * @param object $caller_object
     * @param string $caller_function
     * @param array $extra_info
     * @return bool
     */
    public function Run(&$data, $caller_object, $caller_function, $extra_info = NULL) {
        global $HTMLHeader;

        // If it hasn't been configured, simply display xxxxxxxxx
        $this->str_text_to_display = $this->config['str_text_to_display'];
        if (empty($this->str_text_to_display))
            $this->str_text_to_display = 'xxxxxxxxxxxxxx';

        // This is just an example macro
        // we're going to ignore which caller function it is
        // because it doesn't matter.
        // regardless, we want to search and replace the text
        /*
        switch ($caller_function)
        {
            case 'viewItemByID':   break;
            case 'viewCategoryByID':  break;
            case 'viewChildCategory':  break;
            case 'viewChildItem': break;
            case 'viewMainPage':  break;
            default:  break;
        }*/
	// Who's calling this?
        // only cmsApplication_List and its inherited classes have
        // database fields.
        if (is_a($caller_object,'\\SCHLIX\\cmsApplication_List'))
        {
            // look for summary and description from database field            
            // $app_name  = $caller_object->getApplicationName();
            $fields_to_replace = ['summary', 'description'];
            foreach ($fields_to_replace as $field)
            {
                if (array_key_exists($field, $data) && !empty ($data[$field]))
                {
                    $data[$field] = $this->replaceAllMailtoLinks($data[$field]);
                }
            }
        } else if (is_a($caller_object,'\\SCHLIX\\cmsBlock'))
        {
            if (is_string($data))
            {
                $data = $this->replaceAllMailtoLinks($data);
            }

        }
        // with &$data, the data is replaced right away
        return true;
    }

}
            

c. Test it

Create a simple test case by creating a new page filled with a couple of email addresses.

SCHLIX macro hide email test case

When you click the view source <> button (last one on the right), it should look like these:

summary field:

<h2>Field: Summary</h2>
<p>I have a friend and his email address is <a href="mailto:info@localhost.local">info@localhost.local</a>.&nbsp; His boss' email is <a href="mailto:conan.o'neill@localhost.local">conan.o'neill@localhost.local</a>. A single quote in an email address is valid and it should be converted as well.</p>

description field:

<h2>Field: Description</h2>
<p>This is the second field. This <a href="mailto:test@localhost.local">email address</a> should also be converted as well.</p>

When Javascript is enabled, this is what it looks like:

Screenshot of example when Javascript is enabled

Let's set the configuration value to info@emailaddress.com to mislead the spam harvester. Open a Chrome/Firefox Developer Tools by hitting Ctrl+Shift+I (or Cmd+Shift+I on Mac OS X), and test it by enabling and disabling the Javascript. In this example, we used Firefox Developer Tools. As you can see from the example below, it will just display info@emailaddress.com.

Firefox developer tools - enable/disable Javascript test

Let's test to see if the configuration change is working. Go to Macro Manager and change the text to display to xxxxxxxxxx.

Change config parameter

When the Javascript is disabled, this is what it looks like. See the highlighted xxxxxx text? It means that it's working.

Javascript disabled

You can also test it by clicking the View Page Source button to ensure that the generated code is Javascript.

3. Publish it to Extensions Directory (registration required)

Export ZIP

Once you're done, go back to Plugin Creator and click Export ZIP. Publish it to the Extensions Directory so others can use this wonderful extension for their site. It's a good way to promote your website and have it linked from ours.

Download the fulll source code of this tutorial (23 Kb)