Add noopener to Link Tags VII

A plugin to correct link tags with target="_blank" as objects are saved to the database

In the previous articles, we saw how to modify existing tags with target="_blank" throughout the site. In this final article in the series, we'll create a plugin to change future tags in resources, chunks, TVs, and templates as they are saved in the Manager.

The snippet in the previous article will correct any existing tags, but unless users remember to insert the noopener noreferrer code in every new tag, the snippet will have be run fairly often and the code may be insecure between runs. The plugin below will correct all new tags as they are saved to the database.

MODX logo

Correcting Tags on Save

This plugin will correct tags as they are saved in the Manager. Our two functions at the top are the same, but the code has to change to respond to the various System Events that fire when objects are saved. We can handle TVs more easily because we can get them as related objects of the modResource object that's passed to OnDocFormSave whenever a resource is saved.

Create a plugin called FixTags with the code below and attach it to OnDocFormSave, OnDocChunkFormSave, and OnTempFormSave:

/** @var $modx modX */
if (!function_exists('fixContent')) {
    function fixContent(&$string) {
        $retVal = false;

        /* pattern to find link with _blank */
        $pattern = '/<a [^>]+_blank[^>]*>/i';

        /* Pattern for adding noopener */
        $replacePattern = '/target\s*=\s*[\'"]_blank[\'"]/i';

        /* Get all tags with _blank on the page */
        if (preg_match_all($pattern, $string, $matches)) {

            /* If we got any, loop through them and replace if necessary */
            foreach ($matches[0] as $key => $value) {
                /* Don't process tags that already have noopener and noreferrer */
                if (strpos($value, 'noopener noreferrer') !== false) {
                } else {
                    /* Save the old tag value */
                    $oldValue = $value;

                    /* Handle case with noopener, but not noreferrer */
                    if ( (strpos($value, 'noopener') !== false)
                            && strpos($value, 'noreferrer') === false) {
                        $newValue = str_replace('noopener',
                            'noopener noreferrer', $value);

                    /* handle cases with noreferrer, but not noopener */
                    } elseif ( (strpos($value, 'noreferrer') !== false)
                            && strpos($value, 'noopener') === false) {
                        $newValue = str_replace('noreferrer',
                            'noopener noreferrer', $value);

                    /* Handle cases with neither noopener, nor noreferrer */
                    } else {

                        /* Do the replacement within the current tag */
                        $newValue = preg_replace($replacePattern,
                            'target="_blank" rel="noopener noreferrer"',

                    /* Do the replacement of the tag in the page */
                    $string = str_replace($oldValue, $newValue, $string);

                    /* Set return as true to tell calling function
                       we did at least one replacement for this page */
                    $retVal = true;
        } else {
            $retVal = false;

        return $retVal;
    } // End of fixContent function

if (!function_exists('has_blank')) {
    function has_blank($string) {
        return stripos($string, '_blank') !== false;

/* Main action starts here */
switch($modx->event->name) {
    /* Handle Resources and TVs */

    /** @var $resource modResource */
    case 'OnDocFormSave':
        $docId = $resource->get('id');
        $allFields = $resource->toArray("", true);
        $dirty = false;
        $fieldsToCheck = array(

        foreach ($fieldsToCheck as $fieldToCheck) {
            $string = $allFields[$fieldToCheck];
            if (has_blank($string)) {
                if (fixContent($string)) {
                    $dirty = true;
                    $allFields[$fieldToCheck] = $string;

        if ($dirty) {
            $resource->fromArray($allFields, "", false, true);

        $tvs = $resource->getMany('TemplateVars');

        /** @var $tv modTemplateVar */
        foreach ($tvs as $tv) {
            /* Check TV value for this resource */
            $tvId = $tv->get('id');
            $tvr = $modx->getObject('modTemplateVarResource',
                array('contentid' => $docId,
                    'tmplvarid' => $tvId));
            if ($tvr) {
                $string = $tvr->get('value');

                if (is_string($string) && has_blank($string)) {
                    if (fixContent($string)) {
                        $tvr->set('value', $string);
            /* Check default value of TV */
            $string = $tv->get('default_text');
            if (is_string($string) && has_blank($string)) {
                if (fixContent($string)) {
                    $tv->set('default_text', $string);

    case 'OnChunkFormSave':
        /** @var $chunk modChunk */
        $string = $chunk->getContent();
        if (has_blank($string)) {
            if (fixContent($string)) {

    case 'OnTempFormSave':
        /** @var $template modTemplate */
        $string = $template->getContent();
        if (has_blank($string)) {
            if (fixContent($string)) {

Wrapping Up

This code, along with the code in the previous articles, fixes all existing link tags with target="_blank" on the site and makes sure that future ones are corrected when saved. You might think that you could just install the plugin above and write a utility snippet that retrieved and saved all the object. Unfortunately, retrieving and saving the objects does not fire the events that the plugin listens for. In older versions of Revolution, there were System Events that fired when a resource was saved in code. At this writing, however, those events no longer exist.

Coming Up

In the following articles, we'll see an easy way to create Custom Manager Pages (CMPs) using the new code in recent versions of MODX.

Looking for high-quality, MODX-friendly hosting? As of May 2016, Bob's Guides is hosted at A2 hosting. (More information in the box below.)

Comments (0)

Please login to comment.