We launched new forums in March 2019—join us there. In a hurry for help with your website? Get Help Now!
    • 39932
    • 483 Posts
    Updated as of 8/19/2012



    Summary

    Revolution has inconsistent behavior when a Parent Resource is not a Container, but has children. This is especially true if you use 'makeUrl()' in Snippets to make your links. If the Parent Resource is not a Container, when you request it, the URL contains the extension based on Content Type. If you request a Child, the path excludes the extension from the Parent. This led to some tricksy code for my other plugins, so I decided to fix it.

    This tutorial will show you how to replace all extensions in a URL when MODx cannot find the resource. It will then redirect the user to the appropriate page. Understand that this will ignore any request for an extension, but it was all or individual custom fixes for every link.



    The Process

    This requires very little setup. First, we'll build the Plugin, with the complete code. Afterwards, we will perform a test by creating two resources under specific conditions. Then we will request them using by adjusting their address in the browser address bar. Next, I'll address compatibility, so if you are using any of my other plugins from the other Tutorials, be sure to read this. Finally, for those interested, an Explanation of the Code will be posted in a reply post.

    Note: All of the Plugins are getting updated (on 8/19/2012), due to fixes and better configuration. They are all compatible with each other now. smiley



    Create the Plugin

    Click the Elements tab where your Resource tree is. Create a new Plugin. For the sake of clarity with my other plugins, let's name it "onNoCustomAlias". Paste the following code:

    <?php
    //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')
        {//See if a previous plugin has set the URL.
            $toURL = !empty($_REQUEST[$keyURL]) 
                ? $_REQUEST[$keyURL]
                : $_SERVER['REQUEST_URI'];
            $toURL = reset(explode('?', $toURL));
    
            function findByURL($url, $alias)
            {   global $modx;
            // Strip the extension
                $alias = reset(explode('.',trim($alias)));
                
                $q = $modx->newQuery
                (   'modResource',
                    array
                    (   'published'=>1,
                        'context_key'=>$modx->context->key,
                        'alias'=>$alias
                    )
                );
                $q->select(array('id','alias'));
                $q->limit(25);
                $q->prepare();
                $matches=$modx->getCollection('modResource',$q);
                
                foreach($matches as $res)
                {   if (!empty($res))
                    {//Align URLs by getting rid of end slashes and extensions
                        $chk_url = $modx->makeUrl($res->get('id'));
                        $chk_url = preg_replace("/\.[\w\-\h]{2,}/", "", trim($chk_url, '/'));
                    // Now compare the URLs
                        if($chk_url == $url)
                            return $res->get('id');
                    }
                }
                return -1;
            }
        
        // Split the Path Segments
            $toPath = explode('/', trim($toURL, '/'));
        // Align URLs by getting rid of end slashes and extensions
            $toURL = preg_replace("/\.[\w\-\h]{2,}/", "", trim($toURL, '/'));
    
        // Get the Resource ID
            $toID = findByURL($toURL, end($toPath));
        // Forward the client
            if ($toID == -1) $_REQUEST[$keyFound] = 'false';
            else
            {   $_REQUEST[$keyFound] = 'true';
                $modx->sendForward($toID);
            }
        }
    


    Now that the code is placed, click Save. We still have to attach it to an event, however. So, click the System Events tab. Scroll down to OnPageNotFound. Mark the checkbox next to it. Set the priority to 3 or higher. Click Save again. Note: If you a) use Articles, b) any of my other plugins, c) or any plugin that utilizes OnPageNotFound, then see the section on Compatibility.



    Testing the Plugin

    Test One: Parent Resource

    Pick any Resource that is not a Container. Right-click on it and choose "View Resource". In general, it will apply the extension ".html". Now, in the address bar change the extension to ".xml" and press Enter. It will load the same resource. You may also remove the extension entirely and it should load.

    Test Two: Child Resource

    Use the Resource that you chose in the first test. Add a new Resource under it. Don't forget to set an alias, and title. Once it is Saved, Right-Click on it and choose "View Resource". It will load with whatever URL it likes, but the last path segment should have an extension. Do the following in the Address Bar and you should get the same results:


    • Change the extension to ".css" and press Enter.
    • Remove the extension and press Enter.
    • Add any extension to the parent path segment and press Enter.
    • Add an extension to child path segment and press Enter.



    On Compatibility

    Supports

    This supports long extensions with any letter, number, or hyphen. It does not currently support white space in extension. This does not tamper with POST or GET parameters in any way, so extensions and "." may be used as pleased in these.

    System Settings

    The System Settings used here (getOption()) are used by the other Plugins, as well. They aren't necessary to create unless you are using the other Plugins. If you would like to create them, refer to the updated Tutorial on my AJAX Framework and either Cross-Context Resources or Template Actions (links in my signature).

    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" using the updated Tutorial. (links in my signature)

    FuzzicalLogic's Aliasing Tutorials

    Priority for this plugin must be after (higher than) all other Custom Aliasing tutorials. This includes the following: Cross-Context Resources, Template Actions. While it is not required to upgrade these plugins, updating them provides much better compatibility, optimized processing, and configuration options (for ease and security), as well as better support for path enforcement. (links in my signature)

    Articles Plugin

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



    Conclusion

    I have had endless issues with extensions and extra slashes until the plugin was created. While most were derived from makeUrl(), or the ~id, this resolved some issues with Javascript as well. It's never a happy day when you request a page that you know exists, but the makeUrl either included an extension that MODx doesn't want, or removed one that it needs. This gets particularly hairy if you use multiple content types with children. Hopefully, the techniques demonstrated here will be useful, even if you have no need for the plugin itself. [ed. note: fuzzicallogic last edited this post 14 years, 1 month 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 code makes fairly good use of several features of xPDO and MODx. Here are the some of the things exhibited in the code:


      • Accessing User/Context/System settings (prioritized automatically by MODx)
      • Separation of processing for compatibility with other OnPageNotFound plugins
      • Using dynamic $_REQUEST variables as placeholders.
      • Getting multiple Resources with an xPDO Query
      • URL alignment and comparison
      • Forwarding to a document without a redirect

      Now, let's go through it bit by bit (in order). This first section creates the variables to process.

      //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');
      


      For each variable above, the following happens:

      • Checks if the variable was set via Properties. If not:
      • Checks if the variable was set by a previous OnPageNotFound Plugin. If not:
      • Gets the value from MODx Settings (via getOption). If the setting does not exist:
      • Sets the value to a default value.

      The next part gets a passed parameter to determine whether or not to continue processing. $isFound is a variable that may or may not be set by a previous OnPageNotFound Plugin. If it is set, then we get it from the $_REQUEST variable (notice we use the setting we got just previously). If not, we assume that it is 'false'. This variable is used by all of my other updated OnPageNotFound Plugins.

      // Get "passed" variables
          $isFound = empty($_REQUEST[$keyFound])
              ? 'false'
              : $_REQUEST[$keyFound];
      // Only do this if we need to scan.
          if ($isFound == 'false')
          {  
               // ALL OTHER CODE WILL BE IN HERE!!
          }
      


      Note: You may wonder why we use an array index that comes from the System Settings. This is for two reasons: a) hackers and script kiddies may try to set the value manually; and b) other Plugins outside of mine may require use of these. If you have the Setting, then you can change it according to user, context, or for the entire system at your leisure, in case something happens. It's not necessary, just super helpful.

      At this point, we need a URL to track. If you are using any of my custom aliasing (including parts of the AJAX Framework), you will see how we maintain compatibility.
      //See if a previous plugin has set the URL.
              $toURL = !empty($_REQUEST[$keyURL]) 
                  ? $_REQUEST[$keyURL]
                  : $_SERVER['REQUEST_URI'];
              $toURL = reset(explode('?', $toURL));
      


      The above code checks if a previous plugin changed the relevant URL to look for. This is necessary for URL Parameters and False Aliases. However, if you aren't using those plugins and the key isn't set, it gets it directly from the server. After we get the URL, we remove the GET parameters for the purposes of the Plugin. GET isn't affected in anyway, we just can't use it when comparing aliases. Next, we move onto the function code.

              function findByURL($url, $alias)
              {   global $modx;
              // Strip the extension
                  $alias = reset(explode('.',trim($alias)));
      
              // Query based on Alias. Only query the current Context
                  $q = $modx->newQuery
                  (   'modResource',
                      array
                      (   'published'=>1,
                          'context_key'=>$modx->context->key,
                          'alias'=>$alias
                      )
                  );
                  $q->select(array('id','alias')); // <-- We only need the "alias" and the "id"
                  $q->limit(25);   // <-- Account for multiple possibilities
                  $q->prepare();
                  $matches=$modx->getCollection('modResource',$q); // <-- This gets the results as an Array.
                  
                  foreach($matches as $res)
                  {   if (!empty($res))
                      {//Align URLs by getting rid of end slashes and extensions
                          $chk_url = $modx->makeUrl($res->get('id'));
                          $chk_url = preg_replace("/\.[\w\-\h]{2,}/", "", trim($chk_url, '/'));
                      // Now compare the URLs
                          if($chk_url == $url)
                              return $res->get('id');
                      }
                  }
                  return -1;
              }
      


      The xPDO Query is among the first. It is well commented and should be straight-forward. After this, we iterate through the results using the foreach(). In here are some lines of particular note. Once we makeUrl() the current resource, we clean it by getting rid of the aliases and trimming the slashes (preg_replace()). This same statement is used on the $url argument before we pass it. You'll see that below. If the cleaned URLs match, we return the ID of the Resource so that we can do what we need with it. If they don't, we send back -1 because it cannot possibly be an ID.

      Now, we're after the function declaration. This is where we pre-process the URL so that we can pass it to the function.

          // Split the Path Segments
              $toPath = explode('/', trim($toURL, '/'));
          // Align URLs by getting rid of end slashes and extensions
              $toURL = preg_replace("/\.[\w\-\h]{2,}/", "", trim($toURL, '/'));
      


      We do two things above. The first is we split the URL into path segments. This allows us to quickly and easily get the last segment in the next block. Second, we also align the original URL with the same regular expression we used in the function. At this point, it all comes down to the function call:

          // Get the Resource ID
              $toID = findByURL($toURL, end($toPath));
      


      You'll notice that when we make the call above, there's little work because we already did it all. The end() there is confusing for some. It simply gets the last path segment. This is because if the URL for the last segment matches a MODx URL, all of the previous segments must match. We store the result, so that we can use it below:

          // Flag for other OnPageNotFound Plugins
              if ($toID == -1) $_REQUEST[$keyFound] = 'false';
          // Forward the client
              else
              {   $_REQUEST[$keyFound] = 'true';
                  $modx->sendForward($toID);
              }
      


      Finishing up, we check for an invalid ID. If it was invalid, we simply set the $_REQUEST so that later Plugins know that they can keep looking. If it was valid (else), then setting the same variable tells later Plugins not to keep searching (if they are Plugins that seek Resources). Finally, we send the page back to the user by its id using the sendForward() call.

      Note: sendForward() is awesome. It is a great way to give the user desired results even though a URL might not really exist. Further, it doesn't violate MODx security policy, so if the user is not allowed to see the page, they'll get an UnAuthorized error (503).
        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".
        • 42393
        • 143 Posts
        Thanks for this plugin!

        I'm a new ModX user and dealing with alias extensions is one of the first things I wanted to fix. My existing site (to be replaced by ModX) allows for domain/products but the default alias for this would be domain/products.html.

        This is great combined with CaseInsensitiveURLs too : now someone can find my FAQ using /faq, /Faq, /fAq.htm, etc...

        Note, I also loaded CustomUrls, but as hard as I've tried, I just don't understand it or the page that describes it (um that goes for a number of your plugins and doc pages, sorry...). (And I couldn't get the Debug snippet working either.) So I'm really glad I found this page. smiley
          Loved ModX when I was using it a few years ago. Shifted to WordPress, sorry. Thanks, all.
          • 18373 ☆ A M B ☆
          • 3,141 Posts
          For anyone finding this thread that just wants to get rid of the extensions alltogether, go to System > Content Types, double click the extension you want to change or remove, and empty the inline editing field. That will make sure makeUrl or the [[~5]] syntax doesn't add the extension to begin with.
            Mark Hamstra • Developer spending his days working on Premium Extras and a MODX Site Dashboard with the ability to remotely upgrade MODX and extras to make the MODX world a little better.

            Tweet me @mark_hamstra, check my infrequent blog at markhamstra.com, my slightly more frequent ramblings at MODX.today or see code at Github.
            • 42393
            • 143 Posts
            Quote from: markh at Dec 24, 2012, 09:37 PM
            For anyone finding this thread that just wants to get rid of the extensions alltogether, go to System > Content Types, double click the extension you want to change or remove, and empty the inline editing field. That will make sure makeUrl or the [[~5]] syntax doesn't add the extension to begin with.

            Wow, that's a good tip!

            Um, is there anything wrong with completely removing all of those content types? I wouldn't want the environment to have a problem serving up RSS or other non-HTML markup pages just because I removed that entry.

            Thanks.
              Loved ModX when I was using it a few years ago. Shifted to WordPress, sorry. Thanks, all.
              • 18373 ☆ A M B ☆
              • 3,141 Posts
              If you don't use them, sure you can remove them. It's primarily used to set the extension when generating the URL and to apply the right header/mime type if needed.

              They should never cause issues with actual files, as the included .htaccess for friendly urls only sends requests to MODX if no matching file exists. So the xml or rss content types are only used if you have resources set to serve up with that content type (it's a dropdown on the settings tab). If you don't have that, you could remove it, but if you just leave it in there wont be any issues from it either. (Setup might recreate removed content types when you upgrade in the future)
                Mark Hamstra • Developer spending his days working on Premium Extras and a MODX Site Dashboard with the ability to remotely upgrade MODX and extras to make the MODX world a little better.

                Tweet me @mark_hamstra, check my infrequent blog at markhamstra.com, my slightly more frequent ramblings at MODX.today or see code at Github.