Find that Placeholder III

Handling included files that use MODX constants


In the last article, we looked at a utility snippet that lets you find the code that's setting a particular placeholder. Our snippet looked in files included by a snippet or plugin, but only if the full path to the file was present. It didn't detect files where the developer had expressed the file path using one of the MODX constants like MODX_CORE_PATH. We'll fix that problem in this article by adding code to interpret the MODX constants used for file paths.

MODX logo

The Snippet Tag

As a reminder, here's our snippet tag:

[[!FindPlaceholder? &placeholder=`SomePlaceholder`]]

The MODX Constants

The MODX constants are PHP constants that are defined on every page request. There are actually a couple dozen of them, but we're only concerned with the ones that could be used for a file path:

  • MODX_CORE_PATH
  • XPDO_CORE_PATH
  • MODX_ASSETS_PATH
  • MODX_PROCESSORS_PATH
  • MODX_CONNECTORS_PATH
  • MODX_MANAGER_PATH
  • MODX_BASE_PATH

When these constants appear in code, PHP replaces them with their defined values. Most of the ones above are defined in the main config file: config.inc.php. The MODX_BASE_PATH constant points to the root of the MODX install (where the main index.php file is). The rest are set to the path to the directory in their name.

The constants listed above are also available as System Settings (with the exception of XPDO_CORE_PATH), even though most of them are not visible in the System Settings grid unless you create them (not recommended, except as Context Settings for multi-context sites). They don't appear in the grid because MODX creates them on the fly based on the config file, so they're not in the database. They can be retrieved with code like this:

$corePath = $modx=>getOption('core_path');

When getting them as System Settings, the 'MODX_' prefix is removed and they are in all lowercase. The values will be the same as they are for the constants except for XPDO_CORE_PATH which is not available as a System setting.

Experienced MODX developers always use these constants (or their System Setting equivalents) so that the paths to their files will still work if the site is moved to another directory or to another server.


A Slight Digression

I wanted to make sure that all the constant values were available as both constants and System Settings and that their values were equal before I made the claims above, so I created a quick snippet to test this. Here's the snippet:

$cs = array(
    'MODX_CORE_PATH',
    'XPDO_CORE_PATH',
    'MODX_ASSETS_PATH',
    'MODX_PROCESSORS_PATH',
    'MODX_CONNECTORS_PATH',
    'MODX_MANAGER_PATH',
    'MODX_BASE_PATH',
);

foreach($cs as $c) {
    echo "\n\n" . $c;
    echo "\n    CONSTANT VALUE: " . constant($c);
    if ($c == 'XPDO_CORE_PATH') {
        continue;
    }
    echo "\n    SYSTEM SETTING: " . $modx->getOption(strtolower(substr($c, 5))) ;
}

The foreach loop iterates through the constant names. The first line of the loop shows the constant name. The second line shows the value of the constant using PHP's constant() function, which takes a string with the constant name and converts it to the constant's value. The code then skips the second test if it's testing XPDO_CORE_PATH, because, as far as I know, it's not available as a System Settings.

The last line strips off the 'MODX_' prefix, converts the constant name to lower case, and calls $modx->getOption() on the result to get the System Setting value.

I used echo and newlines ("\n") because I ran the code in my code editor, PhpStorm. In a real snippet, you would set an $output variable and return it. You'd also use a br tag instead of the newlines.

And here are the results on my local install:

MODX_CORE_PATH
CONSTANT VALUE: c:/xampp/htdocs/revolution/core/
SYSTEM SETTING: c:/xampp/htdocs/revolution/core/

XPDO_CORE_PATH
CONSTANT VALUE: C:/xampp/htdocs/revolution/core/xpdo/

MODX_ASSETS_PATH
CONSTANT VALUE: C:/xampp/htdocs/revolution/assets/
SYSTEM SETTING: C:/xampp/htdocs/revolution/assets/

MODX_PROCESSORS_PATH
CONSTANT VALUE: C:/xampp/htdocs/revolution/core/model/modx/processors/
SYSTEM SETTING: C:/xampp/htdocs/revolution/core/model/modx/processors/

MODX_CONNECTORS_PATH
CONSTANT VALUE: C:/xampp/htdocs/revolution/connectors/
SYSTEM SETTING: C:/xampp/htdocs/revolution/connectors/

MODX_MANAGER_PATH
CONSTANT VALUE: C:/xampp/htdocs/revolution/manager/
SYSTEM SETTING: C:/xampp/htdocs/revolution/manager/

MODX_BASE_PATH
CONSTANT VALUE: C:/xampp/htdocs/revolution/
SYSTEM SETTING: C:/xampp/htdocs/revolution/

Back to our Snippet

The only change to our snippet is in the getIncludes() function. We need to detect the MODX constants and replace them with their values. It's not as simple as just modifying the pattern and using str_replace(). We don't want to capture the quotation marks or the dot between the constant and the rest of the string (or any spaces).

There's actually an easier way. If you remember the discussion of preg_match_all() in the previous article, you'll recall that preg_match_all() returned an array containing two sub-arrays. In the previous article, we only used the second sub-array ($matches[1])), which contains just the file path between the quotes. In this one, we're also going to have to look at the first one ($matches[0]), which contains the full match for our pattern.

What we're going to do is look at the full pattern to see if it contains a MODX constant. If it does, we'll simply use the constant's value as a prefix for the file path captured in the second sub-array.

We'll need to modify the pattern slightly because the original pattern looks for a key word like require or include, followed by a space and a quotation mark. That won't find paths that use the MODX constants at all. Here's the new pattern:

/(?:include|require)(?:_once)*[^"']*['"]([^'"]+)['"]/i

The pattern still looks for our key terms at the beginning, but then it looks for 0 or more instances of anything that's not a quote before capturing the file path. The remaining pattern is the same as before. We don't need to worry about capturing just the constant or including the constant names in the pattern, because were just going to look at the whole match to see if it contains one of the constants.

Here's the code of our new checkIncludes() function:

function checkIncludes($content, $ph, $type, $name, &$output) {

    $cs = array(
        'MODX_CORE_PATH' => MODX_CORE_PATH,
        'XPDO_CORE_PATH' => MODX_CORE_PATH,
        'MODX_ASSETS_PATH' => MODX_ASSETS_PATH,
        'MODX_PROCESSORS_PATH' => MODX_PROCESSORS_PATH,
        'MODX_CONNECTORS_PATH' => MODX_CONNECTORS_PATH,
        'MODX_MANAGER_PATH' => MODX_MANAGER_PATH,
        'MODX_BASE_PATH' => MODX_BASE_PATH,
);

    $pattern = <<<EOT
/(?:include|require)(?:_once)*[^"']*['"]([^'"]+)['"]/i
EOT;

    $matches = array();
    $count = 0;

    $success = preg_match_all($pattern, $content, $matches);

    if (!empty($success )) {
        $i = 0;
        foreach($matches[1] as $path) {
            /* Skip include that have a PHP variable in them */
            if (strpos($path, '$') !== false) {
                continue;
            }

            $fullMatch = $matches[0][$i];
            foreach($cs as $constName => $constValue) {
                if (strpos($fullMatch, $constName) !== false) {
                    $path = $constValue . $path;
                    break;
                }
            }

            /* Make sure the file exists */
            if (file_exists($path)) {
                /* Make sure it's not a directory */
                if (is_dir($path)) {
                    continue;
                }
                $c = file_get_contents($path);
                $type = 'file';
                if (!empty($c)) {
                    $count += checkContent($c, $ph, $type, $path, $output);
                    break;
                }
            }

            $i++;

        }
    }


    return $count;

}

We mentioned the change in the pattern above. Another change is the addition of the array at the top. Each member of the array has the name of a MODX constant as the key (on the left) and the value of that constant as its value (on the right). Our outer foreach loop is the same, but we've added an inner foreach that loops through the new array. In that loop, we look for the constant names in the full match $matches[0][$i]. We've added an index variable ($i, which starts at 0 and is incremented at near the end of the outer loop so that we're always looking at the full match for the include or require being processed.

The $fullMatch variable holding the full match is set outside the inner loop because it will be the same for each constant. Setting it inside the loop would work, but would slow things down for no reason.

We use strpos() to see if the constant is in the full match string. If it is, we modify the $path. Notice that there's no str_replace() or preg_replace() here. We already have the end of the path in the $path variable. If the full match contains one of the MODX constants, all we have to do is tack the value of that constant on to the beginning of the path. If there's no constant, the original path should already be a full path.


The Full Code

As a reminder, here's our snippet tag:

[[!FindPlaceholder? &placeholder=`SomePlaceholder`]]

Here's the modified code with our new checkIncludes() function:

/* FindPlaceholder snippet */

function checkContent($content, $ph, $type, $name, &$output) {
    $found = 0;
    if (strpos($content, $ph) !== false) {
        $found = 1;
        $output .= "\n<p> " . $type . ' ' . $name . ' contains placeholder ' . $ph . '</p>';
    }
    return $found;
}

function checkIncludes($content, $ph, $type, $name, &$output) {

    $cs = array(
        'MODX_CORE_PATH' => MODX_CORE_PATH,
        'XPDO_CORE_PATH' => MODX_CORE_PATH,
        'MODX_ASSETS_PATH' => MODX_ASSETS_PATH,
        'MODX_PROCESSORS_PATH' => MODX_PROCESSORS_PATH,
        'MODX_CONNECTORS_PATH' => MODX_CONNECTORS_PATH,
        'MODX_MANAGER_PATH' => MODX_MANAGER_PATH,
        'MODX_BASE_PATH' => MODX_BASE_PATH,
);

    $pattern = <<<EOT
/(?:include|require)(?:_once)*[^"']*['"]([^'"]+)['"]/i
EOT;

    $matches = array();
    $count = 0;

    $success = preg_match_all($pattern, $content, $matches);

    if (!empty($success )) {
        $i = 0;
        foreach($matches[1] as $path) {
            /* Skip includes that have a PHP variable in them */
            if (strpos($path, '$') !== false) {
                continue;
            }

            $fullMatch = $matches[0][$i];
            foreach($cs as $constName => $constValue) {
                if (strpos($fullMatch, $constName) !== false) {
                    $path = $constValue . $path;
                    break;
                }
            }

            /* Make sure the file exists */
            if (file_exists($path)) {
                /* Make sure it's not a directory */
                if (is_dir($path)) {
                    continue;
                }
                $c = file_get_contents($path);
                $type = 'file';
                if (!empty($c)) {
                    $count += checkContent($c, $ph, $type, $path, $output);
                    break;
                }
            }

            $i++;

        }
    }

    return $count;

}

/* Code execution begins here */

$ph = $modx->getOption('placeholder', $scriptProperties);

$output = "\n\n<br>
<h3>Searching Snippets</h3>";

if (!empty($ph)) {

    $count = 0;
    $type = 'snippet';

    $snippets = $modx->getCollection('modSnippet');

    foreach ($snippets as $snippet) {
        $name = $snippet->get('name');
        $content = $snippet->get('snippet');

        $count += checkContent($content, $ph, $type, $name, $output);
        $count += checkIncludes($content, $ph, 'File', $name, $output);

    }
    if ($count == 0) {
        $output .= '<p>Placeholder not found in snippets</p>';
    }

} else {
    return "\n<p>Error: Placeholder property is empty</p>";
}

$output .= "\n\n<br>
<h3>Searching Plugins</h3>";

$count = 0;
$type = 'plugin';
$plugins = $modx->getCollection('modPlugin');
foreach ($plugins as $plugin) {
    $name = $plugin->get('name');
    $content = $plugin->get('plugincode');

    $count += checkContent($content, $ph, $type, $name, $output);
    $count += checkIncludes($content, $ph, 'File', $name, $output);
}
if ($count == 0) {
    $output .= "\n<p>Placeholder not found in plugins</p>";
}

return $output;

Limitations

There are a some cases where our strategy will fail. If the path is set as a variable, we're not going to find the file. We also won't find the file if the path (not counting any MODX constant) is not in a single string. There is also the case where developers use a System Setting for certain paths in their development install so that the code will use the development version of a file instead of the one in core/components. Those files will be missed as well. It would be theoretically possible to correct some of those paths, but it would be quite difficult and often unreliable, so I decided to quit with a snippet that is still better than tediously loading snippets, plugins, and included files into the Manager and looking though their code. Feel free to modify the code to handle those edge cases if you have a lot of spare time.


Coming Up

In the next article, we'll see how to make the files in a moved or renamed core directory show up in the Manager's File tree.


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)