We launched new forums in March 2019—join us there. In a hurry for help with your website? Get Help Now!
    • 33968
    • 863 Posts
    I had a similar requirement where I have a large number of contexts representing domains. I want to keep 'utility' resources such as ajax connectors in the default 'web' context without having to repeat and maintain duplicates in every context, as well as set 'default' resources when not available in the other contexts.

    This is basically a simplified version of the original plugin here, but it only searches one context (eg. web) instead of looking for a match in all:
    <?php
    // fire on 'OnPageNotFound'
    $curCtx = $modx->context->get('key');
    $defaultCtx = $modx->getOption('default_context',null,'web');
    
    if ($curCtx !== 'mgr' && $curCtx !== $defaultCtx) {
        $alias = $modx->request->getResourceIdentifier('alias');
        $modx->switchContext($defaultCtx);
        $resourceID = $modx->aliasMap[$alias];
        if (!empty($resourceID)) {
            $modx->sendForward($resourceID);
        }
    }
    
      • 39932
      • 483 Posts
      This tutorial has been updated! It now is "considerate" by using System Settings for configuration, optimization and transparency. It is more secure by allowing changes instantly. It is more compatible with my other plugins and tutorials. The Explanation of Code will appear below this reply and a link will be posted in the Tutorial itself.

      Quick Link: Tutorial [ed. note: fuzzicallogic last edited this post 11 years, 7 months ago.]
        Website: Extended Dialog Development Blog: on Extended Dialog
        Add-ons: AJAX Revolution, RO.IDEs Editor & Framework (in works) Utilities: Plugin Compatibility List
        Tutorials: Create Cross-Context Resources, Cross-Context AJAX Login, Template-Based Actions, Remove Extensions from URLs

        Failure is just another word for saying you didn't want to try. "It can't be done" means "I don't know how".
        • 39932
        • 483 Posts
        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.]
          Website: Extended Dialog Development Blog: on Extended Dialog
          Add-ons: AJAX Revolution, RO.IDEs Editor & Framework (in works) Utilities: Plugin Compatibility List
          Tutorials: Create Cross-Context Resources, Cross-Context AJAX Login, Template-Based Actions, Remove Extensions from URLs

          Failure is just another word for saying you didn't want to try. "It can't be done" means "I don't know how".
          • 39932
          • 483 Posts
          Detailed Explanation of Code complete.
            Website: Extended Dialog Development Blog: on Extended Dialog
            Add-ons: AJAX Revolution, RO.IDEs Editor & Framework (in works) Utilities: Plugin Compatibility List
            Tutorials: Create Cross-Context Resources, Cross-Context AJAX Login, Template-Based Actions, Remove Extensions from URLs

            Failure is just another word for saying you didn't want to try. "It can't be done" means "I don't know how".
            • 19328
            • 433 Posts
            Hi Donald, I finally got the chance to really test this thoroughly. Thanks again for all your help!!
            I found a few problems with the code (the one that was especially for me).
            First, on line 24 there seems to be a '(' to much.

            And also this line gives me an error:
            if ($catVal in $res->getTVValue('categories'))


            I get Parse error: syntax error, unexpected T_STRING . If I remove the line it works again.
            Should it be in_array or something?
              • 39932
              • 483 Posts
              @Michelle,

              Try that. The original code has been modified. Just for the future, it is really hard to support code that you can't remember anything about, so if there's a next time: please, get back to me as soon as possible. Otherwise, I can't guarantee the efficacy of the fix. It just so happened that these were really simple.
                Website: Extended Dialog Development Blog: on Extended Dialog
                Add-ons: AJAX Revolution, RO.IDEs Editor & Framework (in works) Utilities: Plugin Compatibility List
                Tutorials: Create Cross-Context Resources, Cross-Context AJAX Login, Template-Based Actions, Remove Extensions from URLs

                Failure is just another word for saying you didn't want to try. "It can't be done" means "I don't know how".
                • 19328
                • 433 Posts
                You are right, I'm very sorry...
                I'm going to try it right now.
                  • 19328
                  • 433 Posts
                  It works!
                  one little typo in this line: if (in_array($catVal, $categories')) (the ' has to be removed).
                  Thank you!!!
                    • 44057
                    • 2 Posts
                    Thank you for the plugin. It´s working fine, but i have one issue, maybey you can help.

                    When displaying cross-context content, it is displayed in de template that is set in the article container. ) I use the plugin in combination with the articles/plugin.

                    On other contexts, i would like the article to display in the deafault template of that site, like a symlink can.
                    I have created the context-setting "default_template" for each context.

                    Can you give me a hint where to start for that implementation? Can it be done inside the plugin or should it be done in the article plugin?

                    I've moddified the plugin a bit already, so it looks in one ID only for the aliases, in the article container. So it could be detatched from a specific template.

                    I've added this two lines, right before the final sendForward:

                    $TemplateID = $modx->getOption('default_template', null, 'default');
                    $toResource->set('template',$TemplateID);

                    But with no effect.
                    [ed. note: robbedoes last edited this post 10 years, 9 months ago.]