How to create Server File Explorer using jQuery and PHP

I’ve created plugin called jQuery File Browser that can be used to created browser for your server files. Here we will use php as server side script but it can be replaced with any other server side language like Ruby On Rails, Python or Node.js.

TL;DR you can find whole code in this gist

We will use JSON-RPC as our protocol to communicate with the server and my library that implement the protocol in php and JavaScript.

After we clone json-rpc and jQuery File Browser, we need to create our service.php file:

<?php

require('json-rpc/json-rpc.php');


class Service {
    function username() {
        return get_current_user();
    }
    function ls($path) {
        $files = array();
        $dirs = array();
        foreach (new DirectoryIterator($path) as $file) {
            $name = $file->getBasename();
            if ($name != "." && $name != "..") {
                if ($file->isFile() || $file->isLink()) {
                    $files[] = $name;
                } else {
                    $dirs[] = $name;
                }
            }
        }
        return array(
            "files" => $files,
            "dirs" => $dirs
        );
    }
}

echo handle_json_rpc(new Service());

?>

Then we need to create base html file with imported libraries including jQuery and jQuery UI (we will use file browser using jQuery UI dialog):

<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <meta charset="utf-8" />
  <title>File Explorer using jQuery</title>
  <!-- remove underscore from sc_ript (wordpress replace skrypt tag with a tag)-->
  <sc_ript src="https://code.jquery.com/jquery-3.1.1.min.js"></sc_ript>
  <sc_ript src="http://code.jquery.com/ui/1.12.0/jquery-ui.js"></sc_ript>
  <sc_ript src="jquery.filebrowser/js/jquery.filebrowser.min.js"></sc_ript>
  <sc_ript src="json-rpc/json-rpc.js"></sc_ript>

  <link href="http://code.jquery.com/ui/1.12.1/themes/dark-hive/jquery-ui.css" rel="stylesheet"/>
  <link href="jquery.filebrowser/css/jquery.filebrowser.min.css" rel="stylesheet"/>

Then we need to create instance of file browser inside jQuery UI dialog (we will call your service to get the username):

   $(function() {
       rpc({
           url: 'service.php',
           // this will make json-rpc library return promises instead of function that need to be called
           promisify: true
       }).then(function(service) {
           service.username().then(function(username) {
               var browser = $('<div/>').appendTo('body').dialog({
                   width: 800,
                   height: 600
               }).browse({
                   root: '/home/' + username + '/',
                   dir: function(path) {
                       return service.ls(path);
                   }
               });
           });
       });
   });

And now you should have file explorer window open with content of your server user home directory. If you don’t see the output you may want to change root to ‘/home/username’ and don’t use server to get the username, which may be the user that run http server.

The icons that are inside jQuery File Browser are taken from my Clarity icons for GNU/Linux with GTK+.

Next you can add editor that will open on double click on the icon, we will use CodeMirror for this.

First we need to include codemirror:

  <link href="codemirror-5.29.0/lib/codemirror.css" rel="stylesheet"/>
  <link href="codemirror-5.29.0/theme/base16-dark.css" rel="stylesheet"/>
  <sc_ript src="codemirror-5.29.0/lib/codemirror.js"></sc_ript>
  <sc_ript src="codemirror-5.29.0/keymap/emacs.js"></sc_ript>

We use base16-dark theme (to match dark theme of the jQuery UI) and emacs keymap, but you can pick different one.

Then we need to open the editor, we will create open function that look like this:

function open(path) {
    var fname = path.replace(/.*\//, '');
    var readonly = true;
    var mtime;
    service.is_writable(path).then(function(is_writable) {
        readonly = !is_writable;
    });
    service.filemtime(path).then(function(time) {
        mtime = time;
    });
    var ext = fname.replace(/^.*\./, '');
    var mode;
    // you can add more modes here:
    switch(ext) {
        case 'js':
            mode = 'javascript';
            break;
        case 'rb':
            mode = 'ruby';
            break;
        case 'py':
            mode = 'python';
            break;
        case 'sql':
            mode = 'mysql';
            break;
        case 'svg':
            mode = 'xml';
            break;
        case 'xml':
        case 'css':
        case 'php':
            mode = ext;
            break;
    }
    var scripts = $('script').map(function() {
        return ($(this).attr('src') || '').replace(/.*\//, '');
    }).get();
    if (scripts.indexOf(mode + '.js') == -1) {
        var name = 'codemirror-5.29.0/mode/' + mode + '/' + mode + '.js';
        service.file_exists(name).then(function(exists) {
            if (exists) {
                $('<sc' + 'ript/>').attr('src', name).appendTo('head');
            }
        });
    }
    service.file(path).then(function(content) {
        var unsaved = false;
        var div = $('<div><textarea/><div>');
        var textarea = div.find('textarea').hide();
        textarea.val(content);
        div.dialog({
            title: fname,
            width: 650,
            beforeClose: function(e, ui) {
                if (unsaved) {
                    if (confirm('Unsaved file, are you sure?')) {
                        $(this).dialog('destroy');
                    } else {
                        return false;
                    }
                } else {
                    $(this).dialog('destroy');
                }
            },
            resize: function(e, ui) {
                editor.editor.setSize(textarea.width(), textarea.height());
            }
        });
        var editor = {
            dialog: div,
            editor: CodeMirror.fromTextArea(textarea[0], {
                lineNumbers: true,
                matchBrackets: true,
                mode: mode,
                readOnly: readonly,
                theme: 'base16-dark',
                indentUnit: 4,
                keyMap: 'emacs',
                extraKeys: {
                    "Ctrl-S": function(cm) {
                        if (unsaved) {
                            function saveFile() {
                                editor.editor.save();
                                var content = textarea.val();
                                service.save(path, content).then(function() {
                                    unsaved = false;
                                    div.dialog('option', 'title', fname);
                                });
                            }
                            service.filemtime(path).then(function(time) {
                                if (mtime != time) {
                                    if (confirm('File `' + fname + '\' changed ' +
                                                'on Disk are you sure?')) {
                                        saveFile();
                                    }
                                } else {
                                    saveFile();
                                }
                            });
                        }
                    },
                    "Tab": "indentAuto"
                }
            })
        };
        editor.editor.on('change', function(editor) {
            console.log('change');
            unsaved = true;
            div.dialog('option', 'title', '*' + fname);
        });
    });
}

it require few more server side functions:

class Service {
    function file_exists($path) {
        return file_exists($path);
    }
    function username() {
        return get_current_user();
    }
    function is_writable($path) {
        return is_writable($path);
    }
    function file($path) {
        return file_get_contents($path);
    }
    function save($path, $content) {
        $file = fopen($path, 'w');
        $ret = fwrite($file, $content);
        fclose($file);
        return $ret;
    }
    function filemtime($path) {
        return filemtime($path);
    }
    function ls($path) {
        $files = array();
        $dirs = array();
        foreach (new DirectoryIterator($path) as $file) {
            $name = $file->getBasename();
            if ($name != "." && $name != "..") {
                if ($file->isFile() || $file->isLink()) {
                    $files[] = $name;
                } else {
                    $dirs[] = $name;
                }
            }
        }
        return array(
            "files" => $files,
            "dirs" => $dirs
        );
    }
}

and then we need to add this function to our file explorer, we can do that by adding open option, (we can also embed whole function instead of adding it as a value):

   $(function() {
       rpc({
           url: 'service.php',
           promisify: true
       }).then(function(service) {
           service.username().then(function(username) {
               var browser = $('<div/>').appendTo('body').dialog({
                   width: 800,
                   height: 600
               }).browse({
                   root: '/home/' + username + '/',
                   dir: function(path) {
                       return service.ls(path);
                   },
                   open: open
               });
           });
       });
   });

we also need some css tweaks to make icons take more space and to make editor resize properly.

body {
    font-size: 10px;
}
.ui-dialog .ui-dialog-content {
    padding: 0;
    overflow: hidden;
}
.browser {
    width: 800px;
    height: 600px;
}
.browser-widget .content .selection {
    border-color: #fff;
}
.browser-widget li.file,
.browser-widget li.directory {
    width: 60px;
}
.ui-dialog textarea {
    width: 100%;
    height: 100%;
    padding: 0;
    position: absolute;
    left: 0;
    right: 0;
    top: 0;
    bottom: 0;
    resize: none;
}

Shell without ssh or root on your shared hosting

My site jcubic.pl is on shared hosting that don’t have ssh, and I wanted to have a shell access, so using my jQuery Terminal, I’ve stared a project leash. It’s a shell written in javascript with help from php and python cgi. It will give you a shell access, no need to install any new software, and you don’t need to be root (like with other shells like shell in a box, AjaxTerm or Buterfly).

Image color picker in javascript that work without canvas in every browser

If you want to create color picker from image in JavaScript you probably will use canvas, but what if you need it work in IE8 as I needed. You can use some server side help to fetch pixels data from the server. I use php for that and GD library. The code is below.

cross-browser color picker

Server side code that return json pixel data

<?php

function rgb($color) {
    $result[] = ($color >> 16) & 0xFF;
    $result[] = ($color >> 8) & 0xFF;
    $result[] = $color & 0xFF;
    return $result;
}

function imagecreatefrom($filename) {
    $path = pathinfo($filename);
    switch($path['extension']) {
        case 'png':
            return imagecreatefrompng($filename);
        case 'jpg':
            return imagecreatefromjpeg($filename);
        case 'gif':
            return imagecreatefromgif($filename);
        default:
            return null;
    }
}

function getImageData($filename) {
    list($width, $height) = getimagesize($filename);
    $image = imagecreatefrom($filename);
    $image_data = array();
    for ($y = 0; $y < $height; ++$y) {
        $row = array();
        for ($x = 0; $x < $width; ++$x) {
            $row[] = rgb(imagecolorat($image, $x, $y));
        }
        $image_data[] = $row;
    }
    return $image_data;
}

if (isset($_GET['filename'])) {
    $filename = $_GET['filename'];
    if (file_exists($filename)) {
        $result = array(
            'error' => null,
            'result' => getImageData($filename)
        );
    } else {
        $result = array(
            'error' => "The file '$filename' don't exist",
            'result' => null
        );
    }
} else {
    $result = array(
        'error' => "You need to put filename",
        'result' => null
    );
}
header('Content-Type: application/json');
echo json_encode($result);

?>

Then you can fetch the data using ajax and add get invidial pixels on mousemove

    var img = $('img');
    $.getJSON('image_data.php', {filename: img.attr('src')}, function(data) {
        if (data.error) {
            alert(data.error);
        } else {
            $('.eyedropper').click(function() {
                picker = true;
                return false;
            });
            img.mousemove(function(e) {
                if (picker) {
                    var x = Math.round(e.pageX - offset.left);
                    var y = Math.round(e.pageY - offset.top);
                    if (x >= 0) {
                        $('.color').css('background-color',
                                        'rgb('+data.result[y][x].join(',')+')');
                    }
                }
            }).click(function(e) {
                picker = false;
            });
        }
    });

Now all you need to have is element with class eyedropper like a link in your html:


<a href="#" class="eyedropper">pick the color</a>