Add noopener to Link Tags V

Fix link tags with target="_blank" in TVs


In the previous article, we looked at a way to use a snippet to automatically add rel="noopener noreferrer" to link code in MODX resources, templates, and chunks. In this one, we'll use similar code to make the change in template variable values.

MODX logo

Review

In the last article, we used a function to correct the links and another to test for "_blank". We'll use them again, with one minor change, in this snippet for changing TV values. The only difference in those functions is that we'll wrap them in if (function_exists()) in case the code ever runs along with the code from the previous article, or the plugin we'll create later. Without that, we might get a redeclaration error and a crash when the function shows up a second time.

/** @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) {
                    continue;
                }

                if (strpos($value, 'noreferrer noopener')
                    !== false) {
                    continue;
                }

                /* 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, no noreferrer */
                } else {

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

                /* 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;
    }
}


TV Values

Template Variables are complicated enough that they deserve separate treatment. We could have added this capability to the code we used in the last article, but because there are at least three separate locations for the TV values we want to change, it would have made that code much more complicated and difficult to follow. The new code below could certainly be added as a separate function in the previous code, but on large sites, that would increase the chances of the script timing out.

There are actually several places that a link tag containing target="_blank" might be stored: The default value of the TV, the modTemplateVarResource object, and the input and output options field of the TV object. We'll look at those, and the code to modify them, in order below.


Correcting TV Default Values

The first location we'll check is the default value of the TV. That's stored in the default_text field of the TV object. There is only one of these for each TV, and they're often left empty or set to one of the @ bindings, like @INHERIT. Here's the code to modify them (we'll skip any output for now):

$tvObjects = $modx->getCollection('modTemplateVar');

foreach ($tvObjects as $obj) {
    /* Get default value */
    $content = $obj->get('default_text');

    if (has_blank($content)) {
        /* Send default value to fixContent() */
        $fixed = fixContent($content);

        /* if $fixed is true, we changed something */
        if ($fixed) {
            $obj->set('default_text', $content);
            $obj->save();
        }
    }
}

Correcting TV Values for Resources

There must be another place to store TV values, because a TV can have a different value for each resource. Since a TV can be used in multiple resources and a resource can have multiple TVs, it's a many-to-many relationship. That requires a separate table in addition to the resource table and the TV table. The table is modx_site_tmplvar_contentvalues, and the object is called modTemplateVarResource. Each line in that table contains the TV's value for a particular resource.

For our purposes, we don't actually care what resource the TV value is for, since we're changing all of them. In fact, we don't need the modResource object or the modTemplateVar object. All the values for every resource are in the value field of the modTemplateVarResource objects. We'll just get all of them, and change their value fields where appropriate. Here's the code to make changes to the TV values for specific resources:

$tvrs = $modx->getCollection('modTemplateVarResource');

foreach($tvrs as $tvr) {
    /* Get current value */
    $content = $tvr->get('value');

    if (is_string($content) && has_blank($content)) {
        /* Send current value to fixContent() */
        $fixed = fixContent($content);

        /* if $fixed is true, we changed something */
        if ($fixed) {
            $tvr->set('value', $content);
            $tvr->save();
        }
    }

}

We added the is_string() test because TV values that are not strings, can't contain tags. The code above simply cycles through all the modTemplateVarResource objects, and gets each one's value field. if the value field contains _blank, the value gets sent off to our fixContent() function. If that function returns true, we update and save the modTemplateVarResource object.


Correcting TV Input and Output Options

There's one more place a link in a TV might be stored. It's unlikely, but it could be in the Input Options (input_properties field) or the Output Options (output_properties field). Dealing with these (like user extended fields) is a little tricky. They are stored in the database as a JSON string, which is a single string of text that in this case represents a PHP array. In fact, if you get the value through the typical MODX methods, you'll get the PHP array, because MODX will convert it for you on the way out.

In our case, though, we'd actually prefer to get the JSON string, since we're just going to do a simple replacement on it. Unfortunately, there's no simple way to do that. You can get the raw value of the TV for a given resource, but there's no simple way to get the raw value of a JSON field in the TV object. We could bypass MODX altogether and make a direct DB query, but there's an easier way. We can get the raw value of all the fields of the TV using the toArray() method.

$fields = $tv->toArray("", true);

The first argument is a prefix (we don't want one), and the second argument, if true, says that we want the raw values of the fields. When it's time to save changed valued to the database, we'll use the fromArray() method, which is the reverse of toArray(), to set the values.

We could also have converted the modified JSON values to an array with $modx->fromJSON and used $tv->set('fieldName')where MODX would turn them back in to JSON strings, but using fromArray() is simpler and avoids two unnecessary conversions since it uses the raw values in both directions.

Because were potentially modifying two fields, we'll need an extra variable, $dirty to indicate whether anything needs to be saved. Here is code to modify links in the input and output properties fields:

$tvObjects = $modx->getCollection('modTemplateVar');

foreach ($tvObjects as $obj) {
    $dirty = false;

    /* Get raw field values */
    $fields = $obj->toArray('', true);

    /* Send input_properties to fixContent() */
    $content = $fields['input_properties'];
    $fixed = fixContent($content);

    /* if $fixed is true, we changed something */
    if ($fixed) {
        $dirty = true;
        $fields['input_properties'] = $content;
    }

    /* Now do the same with the output_properties field */

    /* Send out_properties to fixContent() */
    $content = $fields['out_properties'];
    $fixed = fixContent($content);

    /* if $fixed is true, we changed something */
    if ($fixed) {
        $dirty = true;
        $fields['output_properties'] = $content;
    }

    /* Save changes if any were made */
    if ($dirty) {
        $obj->fromArray($fields, "", false, true);
        $obj->save();
    }
}

When we use toArray() above, notice that we send true as the second argument (the first is a prefix that we don't want). That tells MODX that we want the raw values of all the fields. When we use fromArray() later on, true is the fourth argument. The second is the prefix (which we still don't want), the third tells MODX not to update the ID of the object.


Putting it All Together

You probably noticed that when running the three sections of code together, we're getting all the TV objects twice. There's no harm in that, but it's not very efficient. We might as well make any changes to the input and output options in the first loop. We'll do that in the next article where we combine all the TV code into a single script.


Coming Up

In the following article, we'll combine the code above into a single script to modify TV values and make it more efficient.




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.

  (Login)