Explanation of Code
This post gives a detailed step-by-step explanation of the code used in this Plugin. The code here demonstrates both simple and advance MODx concepts, including:
- Querying xPDO
- Accessing System Settings
- Forward data to other Plugins
- Receiving data from other Plugins
- Accessing/Setting data through configurable Array Keys
- URL construction for comparison
Aside from SQL Injection, one of the most common attacks comes in the form of hijacking $_GET, $_POST, and $_REQUEST variables. In these plugins, we allow the user to easily change keys via System Settings. This serves 3 purposes: a) immediate end to hijacks without adjustment of code; b) compatibility between similar plugins; and c) obscurity of data based on resource reference. Below is how we retrieve the Settings.
Note: For more customizable or complex situations, getOption() retrieves settings from the User (if set), then the Context (if set), then the System. You could logistically have a different setting for each depending on your needs, and the code would function across all related plugins. This would lead to more security, but also more maintenance.
//Get the System Settings (if we haven't already...)
$keyURL = !empty($keyURL) ? $keyURL : $modx->getOption('key_url_request', null, 'toURL');
$keyFound = !empty($keyFound) ? $keyFound : $modx->getOption('key_doc_found', null, 'hasResource');
Next, we need to know if a previous plugin (within the same event) has found the appropriate resource. This uses a key from the System Settings. After all, we want to make sure we don't conflict with similar Plugins, but also want to limit processing if we don't actually need it. If we do need to, we can set it for the later Plugins.
// Get "passed" variables
$isFound = empty($_REQUEST[$keyFound])
? 'false'
: $_REQUEST[$keyFound];
// Only do this if we need to scan.
if ($isFound == 'false')
{
Similar to before, we need the URL we are looking for. Since a previous Plugin may modify the URL according to its needs, we need the real URL to act upon. This uses the other key we got from above. This line gets the URL from the $_REQUEST (if already set by another Plugin), or from the $_SERVER (if not modified by a previous Plugin). Afterwards, we make sure that we aren't tampering with $_GET or $_POST by removing them from our stored URL.
//Get the Request URL
$toURL = !empty($_REQUEST[$keyURL])
? $_REQUEST[$keyURL]
: $_SERVER['REQUEST_URI'];
// Remove GET Parameters
$toURL = reset(explode('?', $toURL));
$path = explode('/', trim($toURL, '/'));
This function uses an xPDO Query to find a
published Resource based on its alias. To speed and simplify the query, we check up front whether its Template matches a site-wide Alias. Notice, we get another System Setting here with getOption. This allows developers to add or change functionality without modifying the current Template. When they are done, they may change the System Setting and Template on the active Resources. If successful, it returns the actual Resource. If not, it returns null.
/*
* Checks if the alias is a Cross Context Alias
*/
function isSiteAlias($alias)
{//Get MODx instance
global $modx;
// Get System Setting
$idAliasTemplate = $modx->getOption('id_site_alias', null, 1);
$q = $modx->newQuery(
'modResource',
array(
'published'=>1,
'alias'=>$alias,
'template'=>$idAliasTemplate,
)
);
$q->select(array('id','alias'));
$q->prepare();
// Iterate through potential matches
$matches = $modx->getCollection('modResource',$q);
if (!empty($matches))
foreach($matches as $res)
// Respect parents
return $res;
// Return empty variable
return null;
}
This function is how we make sure that an Aliased Resource respects the path. Prior to this addition, it did not and every Site Alias could be accessed regardless of path. This allowed for greater stability, but did not allow for adequate debugging. Now, developers will have an easier time debugging, but will have to check their links. It was a tradeoff, but fixing a link is easier than debugging an alias that doesn't truly exist.
/*
* Checks if the alias is a child of the previous path.
*/
function isChildOf($alias, $child)
{// Strip the extension
$alias = reset(explode('.',trim($alias)));
$parent = $child->getOne('Parent');
// Simple climb
if (!empty($parent))
if ($alias == $parent->get('alias'))
return $parent;
// Return empty result
return null;
}
This function allows us to access Children of the Custom Alias. This isn't strictly necessary for all, but it is necessary if we are allowing for Template-Based Actions. If the Child path segment is not actually a Child of the Resource, it will return a 404.
/*
* Checks if the alias is a parent of the next path.
*/
function isParentOf($alias, $parent)
{// Strip the extension
$alias = reset(explode('.',trim($alias)));
$children = $parent->getMany('Children');
// Simple climb
foreach($children as $key => $res)
{ if (!empty($res))
if ($alias == $res->get('alias'))
return $res;
}
// Return empty result
return null;
}
Here is where the code actually begins. Since we broke up the path above, we need to go through each segment and determine if it is an Alias. If it is, we take the previous segments (if there are any) and move them to the Parents array. We also take the supplemental segments and move them to the Children array.
// Initialize Tracking Variables
$toResource = null;
// Find the Alias (one alias per Request)
foreach($path as $key => $segment)
{//Check if it could be an Alias
$toResource = isSiteAlias($segment);
if (!empty($toResource))
{
// Split into two arrays
$parents = explode('/', reset(explode($segment.'/', $toURL)));
$children = explode('/', end(explode('/'.$segment, $toURL)));
break;
}
}
Now, we check to make sure we are respecting the path of the parents. If the path does not match, the Plugin will set the Resource to null, forcing a 404 error.
if (!empty($toResource) && !empty($parents))
{ $child = $toResource;
foreach($parents as $key => $segment)
{ if (!empty($segment))
{ $tmp = isChildOf($segment, $child);
if (empty($tmp))
{ $toResource = null;
break;
}
else $child = $tmp;
}
}
}
If we have Child segments, we obviously have the wrong Resource, so now we check if the Child exists (using the function above). If it does not, we set the Resource to null, forcing a 404.
// Keep track of Children (for Paths deeper than Alias)
if (!empty($toResource) && !empty($children))
{ foreach($children as $key => $segment)
{ if (!empty($segment))
{ $tmp = isParentOf($segment, $toResource);
if (empty($tmp))
{ $toResource = null;
break;
}
else $toResource = $tmp;
}
}
}
Finally, if we still have a Resource, we send the user to it. sendForward() is awesome, because it does its job without changing the URL on the browser. We also use the System Setting to let subsequent Plugins know that we found the Resource. If we don't have a Resource still, we let the plugins know that too. Here is where $_REQUEST really shines... we just modified the $_REQUEST meaning it will reset as soon as any other request for a page is made, but we didn't modify $_GET or $_POST at all, meaning the user's queries are intact.
// Send to the Nearest found Child.
if (!empty($toResource))
{ $_REQUEST[$keyFound] = 'true';
$modx->sendForward($toResource->get('id'));
}
else $_REQUEST[$keyFound] = 'false';
}
[ed. note: fuzzicallogic last edited this post 11 years, 7 months ago.]