On March 26, 2019 we launched new MODX Forums. Please join us at the new MODX Community Forums.
Subscribe: RSS
  • Fuzzical Logic Reply #1, 8 years ago
    This Plugin allows one or more resources specfied (by template) to be accessed across all Contexts.
    Uses include: having a single Login page (without having to worry about full urls, typed urls, etc.), serving AJAX style pages (without specifying jsonp), and combining the two. Proper use of this Plugin may reduce code, maintenance and the number of Resources within your Resource Tree.

    See Also:

    Related Tutorials/Plugins



    Updated as of 8/19/2012

    • Respects Parent resource paths
    • Respects Child Resources now
    • Simplified code logic for readability.
    • Added System Settings for Compatibility, Configuration, Security and Performance



    Summary

    As one progresses further into the ModX platform, there will be some increasingly apparent limitations imposed by the system. Whether or how these limitations affect your development depends largely on the type of site that you are building. One such limitation is the lack of the ability to create a Resource that is accessible from every Context with the same path.

    Under the ModX framework, we typically accomplish this by creating a copy of the Resource in each Context that we would like to access it from. This works well, as long as you don't have 50 contexts or do not want to have multiple copies. It is not easily supportable, as when we change one page, it is not necessarily changed across the board. In other words, for large web-applications, this is can actually be a hinderance to your site's development. This tutorial shows how to create a Resource or series of Resources that are accessible from the same path, regardless of the Context.



    The Process

    This tutorial is really simple. It takes about 15 minutes to setup. Honestly, you will probably spend more time reading than you will doing the actual work. Unlike the original version, you will not have to adjust any code whatsoever and it may be used out of the "box", so to speak. First, we'll create some System Settings. Next, we'll make a Template. After this, we'll create the Plugin and input some code. Once we have completed the Plugin, we can test using any Resource of said Template. Finally, we'll address compatibility with other Plugins that use the OnPageNotFound system event.



    Create the Template

    Create a new Template with the name "Alias (Site)". We will not delve into which content to place here, as it will be largely dependent upon what you desire. Since we often use Site Aliases for AJAX calls, our content is simple:

        [[*content]]
    


    Click Save and move on to the next step.



    System Settings

    We have to adjust one already provided System Setting, and then add a couple more. The System Settings we will be using will make the Plugin considerate to other Plugins (particularly our own). Additionally, one in particular, will reduce processing considerably if using Plugins from my other tutorials.

    Allow Cross-Context Forwarding

    Go to System Settings in the MODx Manager. In the search box, type 'allow_forward_across_contexts' and press Enter. Double-Click on the value and choose "Yes".

    Link to Template

    Go to System Settings in the MODx Manager. Click Create New Setting. Set the options as follows:

    • Key: id_site_alias
    • Type: Template
    • Name: Alias: Cross-Context Template
    • Description: The ID of the Template that may be used as a cross context resource.
    • Value: Alias (Site)

    Click Save and it is done.

    Improve Processing

    Go to System Settings in the MODx Manager. In the search box, type 'key_doc_found' and press Enter. If it exists (from another tutorial), we're done. If it doesn't exist, click Create New Setting. Set the options as follows:

    • Key: key_doc_found
    • Type: Text
    • Name: Page Found Placeholder
    • Description: The key of the "found page" flag in the $_REQUEST global variable. This defaults to 'hasResource'.
    • Value: hasResource

    Click Save and it is done.



    Create the Plugin

    This is where the magic is performed. Like the AJAX Framework (referenced above), we will use the OnPageNotFound event as this is the event that will fire when it cannot find your Resource outside of its normal Context. If it is an AJAX Request, it will fire every time you request it.

    Create a new Plugin with the name 'onFindSiteAlias'. Copy and Paste the following code into the content:

    //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');
    
    // Get "passed" variables
        $isFound = empty($_REQUEST[$keyFound])
            ? 'false'
            : $_REQUEST[$keyFound];
    // Only do this if we need to scan.
        if ($isFound == 'false')
        {//Get the Request URL
            $toURL = !empty($_REQUEST[$keyURL]) 
                ? $_REQUEST[$keyURL]
                : $_SERVER['REQUEST_URI'];
        // Remove GET Parameters
            $toURL = reset(explode('?', $toURL));
            $path = explode('/', trim($toURL, '/'));
        
        /*
         * 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;
            }
    
        /*
         * 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;
            }
            
        /*
         * 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;
            }
            
        // 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;
                }
            }
    
            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;
                    }
                } 
            }
        
        // 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;
                    }
                }
            }
            
        // Send to the Nearest found Child.
            if (!empty($toResource))
            {   $_REQUEST[$keyFound] = 'true';
                $modx->sendForward($toResource->get('id'));
            }
            else $_REQUEST[$keyFound] = 'false';
        }
    


    Save the plugin. Click on the System Events tab and scroll down to 'OnPageNotFound'. Click the check box and set a priority of 3 (or higher). Note: If you are using any of my other Custom Aliasing Plugins, AJAX Plugins, or the sponsored Articles Add-on, see On Compatibility below.



    Testing the Plugin

    Create the Resource

    Create a Resource anywhere in your resource tree. For demonstration, we will also keep this quite simple. Name it whatever you want, but give it an alias of "site-alias". Place some silly content in here.

    <h1>The proverbial tree did indeed make a sound!!</h1>


    Click Save. Now, right-click the Resource and choose "View Resource".

    Test One: Contexts

    Once the Resource has appeared, go to your Address Bar in your browser. Change the base url to the base url for another context. Sub-Domain Contexts change only the sub-domain in the URL. Domain Contexts change the domain. Press Enter and it should load just fine.

    Test Two: Parent Pathing

    Go to the Address Bar and change the parent path (the segments before your alias) to "/kittens/are/fuzzy/". The page should not load.

    Test Three: Child Pathing

    Add a Child Resource to your new Cross Context Resource (it may be any Template you choose). Save it and refresh the cache. Right-Click on the Child Resource and choose "View Resource". Perform Test One. Test Two is already successful. Between the Site Alias segment and the Child Alias segment, add "/puppies/are/fuzzy/too/". Press Enter. The page will not load (404 Error).



    On Compatibility

    Supports

    Does not interfere with GET or POST.

    Supports Static Resources. Since our call uses sendForward, static resources are no problem.

    All Content Types and their content are supported. If you use other Content Types, depending on setup you may run into issues with extensions or extra/missing slashes. If this is the case, you may also Remove Extensions and Extra Slashes.

    System Settings

    The System Settings used here (getOption()) are used by my other Plugins, as well. You don't need their Settings unless you are using their Plugins. If you would like to create them, refer to the updated Custom Aliasing: AJAX Framework.

    FuzzicalLogic's AJAX Tutorials

    Priority for this plugin must be after (higher than) all other AJAX Framework plugins that use OnPageNotFound. This includes the following: onGetRequestType, onParseURLParams. For ensured compatibility, make sure you "upgrade" them using the updated tutorials.

    FuzzicalLogic's Aliasing Tutorials

    Priority for this plugin must be before (lower than) Template-Based Actions. While it is not required to upgrade this plugin, updating provides much better compatibility, tighter security, optimized processing, and configuration options.

    Articles Plugin

    If you use Articles, you will need to change the priority of ArticlesPlugin to be after all of my plugins.



    Further Exploration

    There is a Tutorial for making Login a Cross-Context AJAX page. This demonstrates how to use the plugins practically with little effort.



    Conclusion

    This technique works particularly well for Contexts developed for use with Domains or Subdomains. It may have to be adjusted for Internationalized sites (particularly if using Babel). Using this, I am able to support multiple sites with the same document. Further, with the System Settings, I can set different values for different Contexts or Users, meaning that the violation of one site will not expose another unnecessarily. [ed. note: fuzzicallogic last edited this post 7 years, 11 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".
    • Hi, I'm setting up a site with the following structure:
      - web context: repository of articles to be used on the two following contexts:
      - context A: www.domainA.com
      - context B: www.domainB.com

      I want both domain A and B to use the articles that are in the web context, while keeping the correct urls. So an article with alias 'article1' will be accessible from both www.domainA.com/article1 and domainB.com/article1.

      Do you thing I could use your plugin to accomplish this? So far I've setup the gateway manager component for the domains and followed your instructions, but it's not working yet.
      If I go to www.domainA.com/article1 I just see the homepage that is set for domainA.

      I must note that the modx installation (and therefore the web context) is setup on domainA.com as well. So I'm not sure if that is correct.

      Could you explain a little about how to setup the domains/contexts if one wants to use your plugin?
      • Hello, I've managed to set the domains up like I wanted in my previous post, and your plugin is working correct! Thanks for sharing it! smiley

        Could you give me some hints on how to alter the plugin to respect the alias path? Because now the match is made on the first segment in the alias path, while it should be made for the last segment (the actual resource).

        I tried to change this:
        foreach($path as $key => $segment)
            {
                $res = isSiteAlias($segment);
                if (!empty($res))
                {
                    $toBaseId = $res->get('id');
                    $atSegment = $key;
                    break;
                }
            }


        To this:

        $res = isSiteAlias(end($path));
        				if (!empty($res))
                {
                    $toBaseId = $res->get('id');
                    $atSegment = $key;
                    break;
                }


        But I get a 404 now. [ed. note: michelle84 last edited this post 8 years ago.]
        • Fuzzical Logic Reply #4, 8 years ago
          Michelle, I've been asleep all day, actually.

          Could you explain a little about how to setup the domains/contexts if one wants to use your plugin?

          I've never used the plugin across domains (just sub-domains). I imagine if they both refer to the same MODX install, it would work great!!

          Really, there's not much setup that needs to occur. The only thing I'm not sure if it is compatible with is Babel. I'm a big fan of using templates to help the decision tree along (just a little). So, as long as you declare the template or multiple templates (we don't want this to happen with everything), it should be fine.

          Hello, I've managed to set the domains up like I wanted in my previous post, and your plugin is working correct!

          Glad to hear! I think it really helps keep my resource tree clean and makes editing more consistent and easier for me.

          Could you give me some hints on how to alter the plugin to respect the alias path?

          The plugin was made for my site so that AJAX calls are not cross-domain and, hence, don't have to be JSONP. This means that I often have path after alias that I must use to set dynamic $_REQUEST vars. If you want to respect path, it's not that bad. Basically, you can reconstruct the URL (without the domain) and do a comparison.

          I have a function that does this very quickly. I just have to pull it out for you. Give me a day to test it appropriately. Is it important that it respect only the path before the alias? Or do you need it to respect the path after the alias too? (What I really need to know is are you using my AJAX Framework plugin too?)
            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".
          • Hi fuzzicallogic, everyone needs to sleep for an entire day every now and then ;-)

            I'm not using your AJAX Framework plugin.

            Actually what I want is this: the links to a resource can be for example:
            www.mydomain.com/path1/resource1
            and
            www.mydomain.com/path2/resource1

            The paths are like categories. But in the end, all those URLs have to just lead to the resource with alias 'resource1'. (while keeping the url www.mydomain.com/pathx/resource1 in the address bar).
            For now, it looks for a resource with alias 'path1' or 'path2' and stops at that point.
            So it's more like the alias path has to be ignored instead of respected ;-).
            Hope I'm making sense!

            By the way I added something to your script to make it look only in one context (web), because that's the only place where should be looked for a resource (that's the 'repository'). I did it like this:

            function isSiteAlias($alias)
                {   global $modx;
                    $c = $modx->newQuery(
                        'modResource',
                        array(
                            'published'=>1,
                            'alias'=>$alias,
                            'template'=>4,    // <-- Change to your Template ID for Site Alias																    
                            'context_key'=>'web' // check in web context only
            												)
                    );


            It seems to work.

            Thanks for your help!
            • Fuzzical Logic Reply #6, 8 years ago
              So it's more like the alias path has to be ignored instead of respected.

              As far as I know, it ignores "pathx". Mine does any way... Based on both of your posts, though... Let me clarify...

              You have "path1/alias" and "path2/alias". You don't care what "path1" or "path2" are? What's really important here is that the alias is always the last segment of the path. In other words:

              domain.tld/alias == domain.tld/path1/alias == domain.tld/path2/alias???

              'context_key'=>'web'

              I did this in mine for awhile. It was awesome, until I realized I wanted to distribute the "load" a little bit.
                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".
              • Hmm, yeah sounds confusing if you put it that way ;-)

                I'll explain some more. It's actually about listings of locations. To each location (a resource), one or more categories can be assigned. Categories are: open on Monday, open at night, and so on.
                So if location A can fall under:
                www.domain1.com/open-monday/location-a
                and also www.domain1.com/open-night/location-a

                And the same location can also be found at www.domain2.com/open-sunday/location-a.
                (even more complicated, but on domain 2 some of the same locations can be found (only the ones that are open on Sunday).

                The paths are important to use because they enable the site visitor to go back to www.domain1.com/open-monday and see the complete list of locations that are open on Monday.

                Does this make sense to you or am I thinking the wrong way?

                I did this in mine for awhile. It was awesome, until I realized I wanted to distribute the "load" a little bit.

                Do you mean by distributing the load, that you did not want to have all resources in the web context, but also in other contexts?
                • Fuzzical Logic Reply #8, 8 years ago
                  The paths are important to use because ... see the complete list of locations ...

                  Yes. This makes sense. Here is my confusion: Your aliased resource does not affect the category resource, in any way. BUT!!... It sounds like you need to be able to distinguish WHETHER the location is open at X and are using the "path" to "make the query"...

                  In other words: domain.tld/open-monday/locA may be a valid URL, but domain.tld/open-sunday/locA may not be, and meanwhile domain.tld/open-at-night/locA is also valid. Is this more correct?

                  Do you mean by distributing the load, that you did not want to have all resources in the web context, but also in other contexts?

                  Yup. More important: I did not want all of my site aliases in the web context. My site has a LOT of resources. Keeping them all in one context was not feasible for me.
                    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".
                  • In other words: domain.tld/open-monday/locA may be a valid URL, but domain.tld/open-sunday/locA may not be, and meanwhile domain.tld/open-at-night/locA is also valid. Is this more correct?

                    Yes, that sounds correct. On the category resource, I use getResources to display a list of locations that fall under that category. (I created a checkbox TV for the categories and I use the tvFilters property in getResources to only display the correct locations on the various category pages). In the tpl for getResources I was thinking about creating the URLs to the locations like this:

                    [[+site_url]][[*alias]]/[[+alias]]


                    This way it always uses the correct domain (the current site_url), then the alias of the category page and then the alias of the resource. And your plugin finds the correct resource for it.

                    Yup. More important: I did not want all of my site aliases in the web context. My site has a LOT of resources. Keeping them all in one context was not feasible for me.

                    Ah, ok. My site won't have that many resources that it would become a problem I think. How much is a LOT?
                    • Fuzzical Logic Reply #10, 8 years ago
                      Yes, that sounds correct. ... I use the tvFilters property in getResources

                      Ah ha! OK. This is a little more complicated. You have (2) options here... You can 1) adjust my plugin, OR 2) you can add another plugin/snippet that performs the additional logic for you.

                      For example of #2: I have 3 plugins that perform on OnPageNotFound, and snippets for further handling. First, I make sure the URL is lowercase. Second, I check for a site-alias. AFTER I determine it is or is not a site-alias, it determines if it is an AJAX call. If it is an AJAX call, that plugin reads the URL and changes parameters and the AJAX page runs its special snippet. They are all compatible, so that I can type in SUB.DOMAIN.TLD/PATH/RESOURCE/AJAX/PARAMS and I get back the correct domain.tld/resource

                      The real question is, which path do you want to take? Site-aliases are useful, generic aliases that have no care about what the resource is going to do. In my expertise, it is generally good to keep it separate.

                      The other question is: what do you want to happen if they type in domain.tld/open-sunday/location-A and it is NOT open on sunday?

                      So, yes, it can be adjusted, but the trick to fixing this depends upon your answers,

                      How much is a LOT?

                      Right now? About 3000. My Home Page loads 400+ resources at once. [ed. note: fuzzicallogic last edited this post 8 years 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".