Upload web_path

Upload web_path

rf1234rf1234 Posts: 3,000Questions: 87Answers: 421

I am using the Editor Upload functionality to upload files. When I started using this I wasn't aware that basically anyone can access files in "document_root" (e.g. public_html and its subolders) just based on the URL. The URL ist pretty hard to guess but still: It shouldn't be like that.

Last weekend I moved all of those files out of document_root one level up so that they are no longer directly accessible based on an URL.On the client side I kept most of the web_path based logic because there is some complex rendering attached to this. All I wanted to do is to change the system_path and keep web_path the same. I know that sounds a bit strange because the web_path no longer exists: The files are no longer available in document_root. But I didn't want to have more code changes ...

I changed all of my upload instance to use "dirname($_SERVER['DOCUMENT_ROOT'])" instead of "$_SERVER['DOCUMENT_ROOT']" which effectively moves the files one level up. That was easy.

But then web_path no longer worked. It became the same as system_path because of this logic that I had to adjust in Editor (lib->Editor->Upload.php):

if ( count( $pathFields ) ) {
        // For this to operate the action must be a string, which is
        // validated in the `exec` method
        $path = $this->_path( $upload['name'], $id );

        //change change rf1234
        //we need to go one level up!!
        //$webPath = str_replace($_SERVER['DOCUMENT_ROOT'], '', $path);
        $webPath = str_replace(dirname($_SERVER['DOCUMENT_ROOT']), '', $path);
        //end-change

        $q = $db
                ->query( 'update' )
                ->table( $this->_dbTable )
                ->where( $this->_dbPKey, $id );

        foreach ( $pathFields as $column => $type ) {
                $q->set( $column, $type === self::DB_WEB_PATH ? $webPath : $path );
        }

        $q->exec();
}

I tried using a trigger but that didn't work because Editor makes file table updates without specifying system_path which makes the trigger crash...

Is there any other way I can avoid having the above hack in the Editor code without having to make more changes in my code?

This question has accepted answers - jump to:

Answers

  • allanallan Posts: 63,527Questions: 1Answers: 10,473 Site admin

    For something exactly matching that, I'm afraid that no, there isn't a simple option to do that, since the path being used is no longer web accessible (and thus wouldn't fall under what I've defined as a web path in this context.

    However, what I would suggest doing is having two fields in the database:

    1. The directory path, and
    2. The file name (inc. extension)

    That way, if you combine the two together, you get the full path.

    That said, is the full path something you would need with this database setup? The client-side can't just request the file directly since it isn't available directly through the http server. So I'm assuming you are using a PHP proxy file that checks login details and then reads the original file from the server and sends it back to the client. Is that correct? If so, you could just have the client-side request the image id or name from your PHP script and it would then look up the required information (if it is a flat directory then the id might be the best way - you might want to keep a path if you have a lot of files though).

    Allan

  • rf1234rf1234 Posts: 3,000Questions: 87Answers: 421

    However, what I would suggest doing is having two fields in the database:

    1. The directory path, and
    2. The file name (inc. extension)

    That way, if you combine the two together, you get the full path.

    I have the full path anyway - and I know that "web_path" is completely redundant but I am using it about a million times ... and don't want to change it. Hence I figured removing "web_path" from the Editor upload instances (which would only mean to delete one line of code about 30 times) and use a trigger to fill web_path based on system_path. But that requires system_path to be specified in any INSERT or UPDATE statement wich apparently isn't the case in the Editor libriaries. Hence it crashed. Since I can't control the SQL being generated by Editor there is nothing I can do about this.

    This is what my table looks like:

    "name" should be the download name but unfortunately it isn't in all cases because I do additional rendering etc. etc.

    The client-side can't just request the file directly since it isn't available directly through the http server. So I'm assuming you are using a PHP proxy file that checks login details and then reads the original file from the server and sends it back to the client. Is that correct?

    Almost, it is actually even simpler. There is no need to access the files directly outside the front end application. The files can only be read using a script that is only available in case you are logged in.

    This is what the script does:

    $(document).on('ajaxStop', function () {
        setTimeout(function(){
            initDownloadFile();
            $.fn.dataTable.tables( { visible: true, api: true } )
                .on( 'responsive-display', function ( e, datatable, row, shown, update ) {
                    if ( shown ) {
                        initDownloadFile();
                    }
                });
        }, 1000);
    });
    

    And the simple function called which calls the php script making the file available for a couple of seconds:

    //make sure the file links aren't followed
    function initDownloadFile() {
        $("a.fLk").off("click contextmenu");
        $("a.fLk").contextmenu(function(){
            $(this).click();
            return false;
        });
        $("a.fLk").on("click", function(e) {
            $.ajax({
                type: "POST",
                url: 'actions.php?action=downloadFile',
                data: {
                    web_path:       $(this).attr('href'),
                    download_name:  $(this).attr('download')
                },
                dataType: "json",
                success: function (data) {
                    var link = document.createElement("a");
                    link.href = data.path;
                    link.download = data.download;
                    link.target = '_blank';
                    link.click();
                    setTimeout(function() {
                        link.remove();
                    }, 50);
                    setTimeout(function () {
                        $.ajax({
                            type: "POST",
                            url: 'actions.php?action=deleteTmpFile',
                            data: {
                                deleteWebPath: data.path
                            }
                        });
                    }, 10000);
                }
            });
            return false; //prevent default and stop propagation of click on link
        });
    }
    

    And a typical upload instance I use:

    Mjoin::inst( 'file' )
        ->link( 'ctr.id', 'ctr_has_file.ctr_id' )
        ->link( 'file.id', 'ctr_has_file.file_id' )
        ->fields(
            Field::inst( 'id' )
            ->upload( Upload::inst( dirname($_SERVER['DOCUMENT_ROOT']).'/lgfuploads/contract_management/__ID__.__EXTN__' )
                    ->db( 'file', 'id', array(
                        'soft_deleted'  => 0, 
                        'about'         => 'Z',  //contract management
                        'name'          => Upload::DB_FILE_NAME,
                        'size'          => Upload::DB_FILE_SIZE,
                        'web_path'      => Upload::DB_WEB_PATH,
                        'system_path'   => Upload::DB_SYSTEM_PATH,
                        'creator_id'    => $_SESSION['id']
                    ) )
                    ->validator( function ( $file ) use ( $msg ) {
                        if ($file['size'] >= 52428800) {
                            return $msg[3];
                        } else {
                            return true;
                        }
                    } )
                    ->allowedExtensions( array  //php is not case sensitive here
                      ( 'pdf', 'xls', 'xlsx', 'csv', 'doc', 'docx', 'rtf', 'ppt',  
                        'pptx', 'odt', 'ods', 'odp' ), $msg[2] )
            ),
    
  • allanallan Posts: 63,527Questions: 1Answers: 10,473 Site admin
    Answer ✓

    Clever! I should have known you'd have looked into other options thoroughly. In which case, I'm afraid your workaround is probably as good as it gets at the moment.

    Allan

  • rf1234rf1234 Posts: 3,000Questions: 87Answers: 421
    edited November 2022

    While it was easy to check whether a user is logged in in the PHP downloadFile script I found great difficulty in checking whether the user is entitled to see the respective file at all. This would have been very complex to determine via SQL because it is different for each editor / data table - and I have many of them.

    Since the user should only be entitled to click on and view those files that she sees on the screen (or more precisely whose web_paths have just been loaded from the server for display) I use a session variable now to make sure I know the list of the permitted web paths when checking.

    I added "postGet" to the respective Editor instances (22 times :neutral: ):

    ->on('postGet', function ( $editor, $data, $id ) {
        $_SESSION["userFiles"] = [];
        foreach ( $data as $val ) {
            if ( isset($val["file"]) ) {
                foreach ( $val["file"] as $file ) {
                    $_SESSION["userFiles"][] = $file["web_path"];
                }
            }
        }
    })
    

    Then I do these checks in my downloadFile script:

    //user is not logged in ==> do nothing
    if ( ! isset($_SESSION['id']) ) {
        return [ "path"      => "", 
                 "download" => "not authorized" ];    
    }
    if ( ! isset($_SESSION["userFiles"]) ) {
        return [ "path"      => "", 
                 "download" => "not authorized" ];    
    }
    if ( array_search($path, $_SESSION["userFiles"]) === false ) {
        return [ "path"      => "", 
                 "download" => "not authorized" ];    
    }
    

    Question: Is there any way to attach the "postGet" event handler above to all data tables without repeating the code. Just like here in Javascript?

    $.fn.dataTable.tables( { visible: true, api: true } )
        .on( 'responsive-display', function ( e, datatable, row, shown, update ) {
            if ( shown ) {
                initDownloadFile();
            }
        });
    
  • allanallan Posts: 63,527Questions: 1Answers: 10,473 Site admin
    Answer ✓

    Yes, using basically the same approach. Put your postGet function into a a named function:

    function commonPostGet ( $editor, $data, $id ) {
        $_SESSION["userFiles"] = [];
        foreach ( $data as $val ) {
            if ( isset($val["file"]) ) {
                foreach ( $val["file"] as $file ) {
                    $_SESSION["userFiles"][] = $file["web_path"];
                }
            }
        }
    }
    

    And stick that into a file such as include.php. Then include that in any files where you want to execute that function and do:

    ->on('postGet', commonPostGet)
    

    Allan

Sign In or Register to comment.