4

I am coding my first experimental administrator/module so that I can understand how Joomla performs ajax calls. I only need to refresh the content of my module with some server info and a couple of Google Charts calls. I want to use Joomla techniques so that I can confidently build more modules in the future using best practices.

I have cut out 90% of the functionality from my project to isolate where I am stuck.

mod_filefinder.php

defined('_JEXEC') or die;
require_once dirname(__FILE__) . '/helper.php';
if(!defined('DS')) define('DS', DIRECTORY_SEPARATOR);
$filefinder = new FileFinder(JFactory::getApplication()->input->post->getString('path'));
require JModuleHelper::getLayoutPath('mod_filefinder');

helper.php

class FileFinder
{
    public $currentdirectory = "";
    public $subdirectories = array();
    public $js = "";
    public $html = "";
    function __construct($path = null)
    {
        if (!$path) {
            chdir('../');
            $this->currentdirectory = basename(JPATH_BASE);
            $this->subdirectories = glob($this->currentdirectory.DS."*", GLOB_ONLYDIR);
            $this->html = "container";
            $this->js = "
                    jQuery(function($) {
                        $('#path').on('change', function() {
                        var path = $(this).val();
                        console.log({path: path});
                        $.ajax({
                            url: 'index.php?option=com_ajax&module=filefinder&method=FileFinder&format=raw',  // &ignoreMessages 
                            type: 'POST',
                            async: true,
                            cache: false,
                            data: {path: path},
                            success: function(response){
                                console.log(response); 
                            }
                        });
                    });
                    $('#path').trigger('change');
                });
             ";
        } else {
            $this->currentdirectory = $path;
            $this->subdirectories = glob($this->currentdirectory.DS."*", GLOB_ONLYDIR);
        }
    }

    public function FileFinderAjax() {
        ?><script>console.log('hello');</script><?php
        echo "hello";  // how do I get Ajax to get in here?!?
    }
}

default.php

defined('_JEXEC') or die;

if ($filefinder->html == "container") { // provide container with default contents
    echo "<div id=\"filefinder_container\">";
        echo "<span>", JTEXT::_("Current Directory"), " ", $filefinder->currentdirectory, "</span>";
        echo "<select id=\"path\"><option value=\"\">Select a Subdirectory</option>";
        foreach ($filefinder->subdirectories as $subdirectory) {
            echo "<option>$subdirectory</option>";  //  value=\"$filefinder->currentdirectory".DS."$subdirectory\"
        }
    echo "</select>";
    echo "</div>";
} else {  // return only the new contents to be inserted into old container
    echo "<span>", JTEXT::_("Current Directory"), " ", $filefinder->currentdirectory, "</span>";
    echo "<select id=\"path\"><option value=\"\">Select a Subdirectory</option>";
    foreach ($filefinder->subdirectories as $subdirectory) {
        echo "<option>$subdirectory</option>";  //  value=\"$filefinder->currentdirectory".DS."$subdirectory\"
    }
    echo "</select>";
}
if ($filefinder->js) {
    JHtml::_('jquery.framework');
    $document = JFactory::getDocument();
    $document->addScriptDeclaration($filefinder->js);
}

I do have the index.html and mod_filefinder.xml files in place, I am just not adding them to my post because it is unnecessary bloat.

Right now, my console.log is displaying these things after I select a new directory:

{path: "administrator\cache"}
POST http://localhost/jdem01/administrator/index.php?option=com_ajax&module=filefinder&method=FileFinder&format=raw 404 (Not Found)

I have also tried:

  • making FileFinderAjax() a static public function
  • and approximately 50 hailmary code adjustments that I cannot recall and don't care to post.

I have fruitlessly read:

https://www.ostraining.com/blog/joomla/search-ajax/ https://docs.joomla.org/Using_Joomla_Ajax_Interface https://docs.joomla.org/J3.x:Creating_a_simple_module/Developing_a_Basic_Module Sending POST data within module - AJAX Using AJAX in a custom module - how to do it?
(... and countless other docs/forums/stackoverflow/stackexchange resources)
(... and all of the "Similar Questions" recommended in the yellow box to the right of my question textarea)

I am using Joomla 3.8.6


Update after preliminary support from @jamesgarrett and @Lodder...

I am just not getting the desired response string.

Here are my files (stripped down even farther):

administrator/modules/mod_filefinder/mod_filefinder.php

defined('_JEXEC') or die;
require_once dirname(__FILE__) . '/helper.php';    
$filefinder = new modFileFinderHelper();
require JModuleHelper::getLayoutPath('mod_filefinder');

administrator/modules/mod_filefinder/helper.php

class modFileFinderHelper
{
    function __construct() {
        $this->currentdirectory = basename(JPATH_BASE);
    }

    public function FileFinderAjax() {
        $data = JFactory::getApplication()->input->post->getArray([]);
        $path = $data['data']['path'];
        ?><script>console.log('<?=$path?>');</script><?php
        echo "hello $path";
        JExit;
    }
}

administrator/modules/mod_filefinder/tmpl/default.php

defined('_JEXEC') or die;
JHtml::_('jquery.framework');
JHtml::_('script', 'administrator/modules/mod_filefinder/ajax.js');

echo "<div id=\"filefinder_container\">{$filefinder->currentdirectory}</div>";

administrator/modules/mod_filefinder/ajax.js

jQuery(function($) {
    function loadContent(path) {
        console.log({path: path});
        var request = {
            option       : 'com_ajax',
            module       : 'filefinder',  // to target: mod_filefinder
            method       : 'FileFinder',  // to target: function FileFinderAjax in class modFileFinderHelper
            format       : 'raw',
            data         : {path: path}
        };
        console.log(request);
        $.ajax({
            method: 'POST',
            data: request
        })
        .success(function(response){
            console.log('response: '+response);
        });
    }
    console.log('onload, calling ajax with default path value: '+$('#filefinder_container').html());
    loadContent($('#filefinder_container').html());

    $('#path').on('change', function() {
        console.log('change event triggered, calling ajax with path value: '+$(this).val());
        loadContent($(this).val());
    });
});

This is my console.log data:

enter image description here

So let me try to ask this differently.

What is the simplest file structure that will allow me to:

  1. Load an empty html structure.
  2. Send an AJAX call to a single-purpose (build a json string) php file
  3. Receive the json response and distribute the json data into the html structure

Simple right? Please help.

4

For the ajax request to map to your module's helper the class must be named "modFilefinderHelper"

From the documentation:

Module requests must include the module variable in the URL, paired with the name of the module (i.e. module=session for mod_session).

This value is also used for:

The name of the directory to check for the helper file, e.g. /modules/mod_session/helper.php The class name to call, e.g. modSessionHelper

relevant doc


Edit after question update

I've updated and commented two of the files where you're having problems

<?php
class modFileFinderHelper
{
    function __construct() {
        $this->currentdirectory = basename(JPATH_BASE);
    }

    // static methods only (the constructor above wont run for your ajax request)
    public static function FileFinderAjax() {
        // Here we get the data passed via ajax - note the values themselves have not been filtered!
        $data = JFactory::getApplication()->input->post->get('data',[],'array');

        // Presumably you would here do something worthwhile
        $response = [
            'something' => 12345,
            'your_path' => $data['path'],
        ];
        // Usually you can dump json and die, but there's a inbuild responder that'll encode things and die on your behalf
        // you'll see client side it also adds a few generic values like "success"
        echo new JResponseJson($response);
    }
}


jQuery(function($) {

    // grab a dom selector so we don't have to grab it each time we update
    var filefinder_container = $('#filefinder_container');

    function loadContent(path) {
        console.log({path: path});
        var request = {
            option       : 'com_ajax',
            module       : 'filefinder',  // to target: mod_filefinder
            method       : 'FileFinder',  // to target: function FileFinderAjax in class modFileFinderHelper
            format       : 'raw',
            data         : {path: path}
        };
        console.log(request);
        $.ajax({
            method: 'POST',
            data: request
        })
        .success(function(response){
            console.log('response: '+response);
            // we've got json but we need an object so let's parse it!
            response = jQuery.parseJSON(response);
            // and now we can use it like this.
            filefinder_container.text(response.data.something + " and " + response.data.your_path);
        });
    }
    console.log('onload, calling ajax with default path value: '+$('#filefinder_container').html());
    loadContent($('#filefinder_container').html());

    $('#path').on('change', function() {
        console.log('change event triggered, calling ajax with path value: '+$(this).val());
        loadContent($(this).val());
    });
});

Also, it's not an issue with jQuery, but with some other FE frameworks like angularjs or vuejs you may/will run into an issue of Joomla been not aware of your input because it fed as real JSON, not a form. To get over it you'd like to add something like

$input = Factory::getApplication()->input;
$jsonInput = json_decode(file_get_contents('php://input'));
if($jsonInput){
    foreach ($jsonInput as $k => $v){
        $input->def($k,$v);
    }
}
  • Those latest adjustments were just what I needed. Specifically, the static Ajax function and that magical new JResponseJson() call (which is nice because now I don't think I need to write a separate php file just to generate the response data). Brilliant support, thanks. – mickmackusa Mar 23 '18 at 10:18
  • glad you're on your way. – jamesgarrett Mar 23 '18 at 11:47
  • I've edited the answer and added a few additional lines, in the end, you'll see those after moderation goes through. Overall this would work as it is with jQuery, with something more advanced - you'll need those lines in PHP. – Alexandr Mar 24 '18 at 8:01
  • @jamesgarrett A quick question about $data = JFactory::getApplication()->input->post->get('data',[],'array'); I am passing a data object from js like this: {"path":"something","exts":[]}, but JFactory is telling me that isset($data['exts']) is false. Is there something magical in the filter parameter happening? – mickmackusa Apr 5 '18 at 6:18
  • You can always dump the $_POST to the screen and die and read the response in dev tools. That’ll allow you to see the structure of the unfiltered data. – jamesgarrett Apr 5 '18 at 8:17
3

Ok first thing is first, get your Javascript out of the PHP file and into a separate .js file.

The file should be placed in the media directory, but just for temporary measure, just place it in the root of the module directory.

Here is the code with a couple of changes, such as passing the URL parameters through as a rwquest object, as opposed to a direct URL. This will help when working with multilingual websites and SEF URL's

ajax.js:

jQuery(function($) {
    var path = $('#path');

    path.on('change', function() {
        var value = $(this).val();
        var request = {
            option       : 'com_ajax',
            module       : 'filefinder',
            method       : 'FileFinder',
            format       : 'raw',
            'data[path]' : value,
        };

        $.ajax({
            type: 'POST',
            data: request,
            success: function(response){
                console.log(response); 
            }
        });

    });

    path.trigger('change');
});

The the revised code for the Helper with some additional code tweaks for you to get the Ajax data.

helper.php:

defined('_JEXEC') or die('Restricted access');

class ModfileFinderHelper
{
    public $currentdirectory = '';
    public $subdirectories = array();
    public $container = false;

    public function __construct($path = null)
    {
        if (!$path)
        {
            chdir('../');
            $this->currentdirectory = basename(JPATH_BASE);
            $this->subdirectories = glob($this->currentdirectory . DS . '*', GLOB_ONLYDIR);
            $this->container = true;

        }
        else
        {
            $this->currentdirectory = $path;
            $this->subdirectories = glob($this->currentdirectory . DS . '*', GLOB_ONLYDIR);
        }
    }

    public function FileFinderAjax()
    {
        $data = JFactory::getApplication()->input->post->getArray([]);
        $path = $data['data']['server'];

        var_dump($path);
        exit;

        // Check the browser console (Network tab) and the path should be displayed under the "response"
    }
}

And finally the default.php:

<?php

defined('_JEXEC') or die;

JHtml::_('jquery.framework');
JHtml::_('script', 'modules/mod_filefinder/ajax.js');

?>

<?php if ($filefinder->container) : ?>
    <div id="filefinder_container">
<?php endif; ?>

    <span><?php echo JText::_('Current Directory') . ' ' . $filefinder->currentdirectory; ?></span>
    <select id="path">
        <option value="">Select a Subdirectory</option>
        <?php foreach ($filefinder->subdirectories as $subdirectory) : ?>
            <option><?php echo $subdirectory; ?></option>
        <?php endforeach; ?>
    </select>

<?php if ($filefinder->container) : ?>
    </div>
<?php endif; ?>

mod_filefinder.php:

Due to the class name change, update the following line in this file from:

$filefinder = new FileFinder(JFactory::getApplication()->input->post->getString('path'));

to:

$path = JFactory::getApplication()->input->post->getString('path');
$filefinder = new ModfileFinderHelper($path);

Overall, just some code simplification and improvements here and there.

Hope this helps


Security

It's recommended to implement a token check for Ajax calls:

How to add CSRF anti-spoofing to forms

  • I have made some more updates to the answer and also upvoted @mickmackusa's answer as he made a good point which I forgot about. – Lodder Mar 22 '18 at 8:54
  • Hey @Lodder you've put your path.trigger('change'); inside the on change function instead of after it. Nom nom nom... recursion... – jamesgarrett Mar 22 '18 at 9:00
  • @jamesgarrett - Good spot. Not finished coffee yet – Lodder Mar 22 '18 at 9:16
  • 2
    Lol. In Melbourne and am 3 wines in. – jamesgarrett Mar 22 '18 at 9:17
  • 1
    @mickmackusa - It's always better to use a separate .js file. This means you're CSP compliant and users can easily override the file is need be. You need to move the file to the root/media/mod_filefinder directory after though else users wont be able to override it. – Lodder Mar 22 '18 at 10:08

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.