3

Wall

An instance of Wall allows the management of a depot of JPG, PNG, GIF or PDF files shown as a wall of images. The user can select a file on the local disk or drag and drop a file on the wall to automatically upload it on the server. In one click, she can download a file saved on the server or delete it. The code of the server in PHP manages the copies of the files and the generation of the thumbnails displayed on the wall. Accessing the files on a wall is protected.

REMINDER: The layout and the style of an interface are in the hands of the programmer. No graphical design is imposed. Coding the look of an instance of Wall is done instantly. The examples on this page use the icons from Font Awesome.

IMPORTANT: The files on a wall on this server are deleted automatically every 20 minutes.

Objective
  • Responder
    • View
      • Wall

Click on the load button to open a local file and upload it on the wall. Select a JPG, PNG or GIF file, a PDF document. NOTE: The maximum size of the file is configured to 5 MB. Open a folder containing images or documents with the explorer of your file system. Drag and drop a JPG, PNG, GIF or PDF file on the space to the left of the control buttons. The file is transferred in blocks of 100 kB. The progression is shown by a percentage. In case of error, the upload button turns red.

Click on an image to select a file and click on the trash can to delete this file. If the server returns an error, the trash can turns red.

Click on an image and press the download button or hold down the Shift key and click on an image to open a file.

To start uploading an image or a document from a file with a button, a code can run the method uploadFile of the wall when the button is clicked. IMPORTANT: For security reasons, uploadFile must be called in response to a user interaction.

  1. <p class="noprint"><button id="btn_upload_file" type="submit" class="narrow" title=""><i class="fa fa-upload"></i></button></p>
  2. <script>
  3. document.getElementById('btn_upload_file').onclick = () => wall.uploadFile();
  4. </script>

In the console of the navigator, display the list of files on the wall:

wall.files
Array [ "1.pdf", "2.jpg" ]
  1. function Wall(options = false) {
  2.     options = options || {};
  3.  
  4.     let draganddrop = options.draganddrop ? true : false;
  5.  
  6.     let tagsURL = options.tagsURL;
  7.  
  8.     let deleteURL = options.deleteURL;
  9.     let uploadURL = options.uploadURL;
  10.  
  11.     let downloadURL = options.downloadURL;
  12.  
  13.     if (! (typeof tagsURL === 'undefined' || tagsURL === null || typeof tagsURL === 'string'))
  14.         throw new TypeError();
  15.  
  16.     if (! (typeof deleteURL === 'undefined' || deleteURL === null || typeof deleteURL === 'string'))
  17.         throw new TypeError();
  18.  
  19.     if (! (typeof uploadURL === 'undefined' || uploadURL === null || typeof uploadURL === 'string'))
  20.         throw new TypeError();
  21.  
  22.     if (! (typeof downloadURL === 'undefined' || downloadURL === null || typeof downloadURL === 'string'))
  23.         throw new TypeError();
  24.  
  25.     if (uploadURL) {
  26.         let filetypes = options.filetypes;
  27.  
  28.         if (!Array.isArray(filetypes) && filetypes.length > 0 && filetypes.every((e) => typeof e === 'string'))
  29.             throw new TypeError();
  30.  
  31.         this._filetypes = filetypes;
  32.  
  33.         let maxfiles = options.maxfiles;
  34.  
  35.         if (maxfiles === undefined)
  36.             maxfiles = 10;
  37.         else if (!Number.isInteger(maxfiles))
  38.             throw new TypeError();
  39.         else if (maxfiles < 1)
  40.             throw new RangeError();
  41.  
  42.         this._maxfiles = maxfiles;
  43.  
  44.         let maxfilesize = options.maxfilesize;
  45.  
  46.         if (maxfilesize === undefined)
  47.             maxfilesize = 1000000;
  48.         else if (!Number.isInteger(maxfilesize))
  49.             throw new TypeError();
  50.         else if (maxfilesize < 100000)
  51.             throw new RangeError();
  52.  
  53.         this._maxfilesize = maxfilesize;
  54.  
  55.         let chunksize = options.chunksize;
  56.  
  57.         if (chunksize === undefined)
  58.             chunksize = 100000;
  59.         else if (!Number.isInteger(chunksize))
  60.             throw new TypeError();
  61.         else if (chunksize < 10000)
  62.             throw new RangeError();
  63.  
  64.         this._chunksize = chunksize;
  65.     }
  66.  
  67.     View.call(this);
  68.  
  69.     this._draganddrop = draganddrop;
  70.  
  71.     this._tagsURL = tagsURL;
  72.  
  73.     this._deleteURL = deleteURL;
  74.     this._uploadURL = uploadURL;
  75.  
  76.     this._downloadURL = downloadURL;
  77.  
  78.     this._tagsWidget = null;
  79.  
  80.     this._deleteWidget = null;
  81.     this._uploadWidget = null;
  82.     this._downloadWidget = null;
  83.     this._statusWidget = null;
  84.  
  85.     this._uploading = false;
  86.  
  87.     this._slots = {};
  88.  
  89.     this._tag = null;
  90.  
  91.     this._error = null;
  92. }
  93.  
  94. Wall.prototype = Object.create(View.prototype);
  95.  
  96. Object.defineProperty(Wall.prototype, 'constructor', { value: Wall, enumerable: false, writable: true });

The Wall class inherits from the View class. The parameter options of the constructor is an object which configures the options maxfiles, maxfilesize, filetypes, chunksize, draganddrop, tagsURL, uploadURL, deleteURL and downloadURL. maxfiles specifies the maximum number of files a user's space can contain, 10 by default. Triggers an error RangeError if < 1. maxfilesize specifies the maximum size of file, 1 MB by default. Triggers an error RangeError if < 100 kB. filetypes specifies the list of the MIME types of the files which the wall accepts to upload. By default, all types of files are accepted. chunksize specifies the size of the data blocks sent to the server, 100 kB by default. Triggers an error RangeError if < 10 kB. If draganddrop, false by default, is true, the instance accepts to upload a file that the user has dropped on the interface. tagsURL is a character string which specifies the URL of the GET sent to the server to get the thumbnail of a file. uploadURL is a character string which specifies the URL of the POST sent to the server to upload a file. deleteURL is a character string which specifies the URL of the POST sent to the server to delete a file. downloadURL is a character string which specifies the URL of the GET sent to the server to get a file. tagsURL, uploadURL, deleteURL and downloadURL are optional.

  1. Object.defineProperty(Wall.prototype, 'files', {
  2.     get:    function() {
  3.                 return Object.values(this._slots).map(e => e.file)
  4.             },
  5.     set:    function(filelist) {
  6.                 let slots = {};
  7.  
  8.                 if (filelist) {
  9.                     for (let f of filelist) {
  10.                         const id = Number.parseInt(f.split('.')[0]);
  11.  
  12.                         if (!Number.isInteger(id))
  13.                             throw new TypeError();
  14.  
  15.                         if (id < 1)
  16.                             throw new RangeError();
  17.  
  18.                         slots[id] = {file: f};
  19.                     }
  20.                 }
  21.  
  22.                 this._slots = slots;
  23.  
  24.                 if (this._tagsWidget)
  25.                     this.resetTagsWidget();
  26.             }
  27. });

files is an accessor which returns or changes the list of files managed by this. filelist is an array of character strings, e.g. ["1.pdf", "2.jpg"]. filelist is normally built by the code on the server.

  1. Wall.prototype.resetTagsWidget = function() {
  2.     let imglist = [];
  3.  
  4.     const timestamp = Date.now();
  5.  
  6.     this._tagsWidget.innerHTML = '';
  7.  
  8.     for (let id in this._slots) {
  9.         const img = document.createElement('img');
  10.  
  11.         img.src = `${this._tagsURL}/${id}.png?nocache=${Date.now()}`;
  12.  
  13.         img.addEventListener('click', (e) => this._clickImage(e, id));
  14.  
  15.         this._slots[id].widget = img;
  16.  
  17.         this._tagsWidget.appendChild(img);
  18.     }
  19.  
  20.     return this;
  21. }

resetTagsWidget displays the thumbnails of the files managed by this.

  1. Wall.prototype.resetWidget = function() {
  2.     if (this._uploadWidget) {
  3.         if (!this._uploading)
  4.             this._uploadWidget.classList.remove('disabled');
  5.         else
  6.             this._uploadWidget.classList.add('disabled');
  7.  
  8.         if (this._error == 'upload')
  9.             this._uploadWidget.classList.add('inerror');
  10.         else
  11.             this._uploadWidget.classList.remove('inerror');
  12.     }
  13.  
  14.     if (this._deleteWidget) {
  15.         if (!this._uploading && this._tag)
  16.             this._deleteWidget.classList.remove('disabled');
  17.         else
  18.             this._deleteWidget.classList.add('disabled');
  19.  
  20.         if (this._error == 'delete')
  21.             this._deleteWidget.classList.add('inerror');
  22.         else
  23.             this._deleteWidget.classList.remove('inerror');
  24.     }
  25.  
  26.     if (this._downloadWidget) {
  27.         if (this._tag)
  28.             this._downloadWidget.classList.remove('disabled');
  29.         else
  30.             this._downloadWidget.classList.add('disabled');
  31.     }
  32.  
  33.     return this;
  34. }

resetWidget enables or disables the different control buttons of the interface according the configuration and the state of this.

  1. Wall.prototype.setWidget = function(w) {
  2.     View.prototype.setWidget.call(this, w);
  3.  
  4.     this._tagsWidget = this._tagsURL ? w.querySelector('.tags') : false;
  5.  
  6.     this._fileWidget = this._uploadURL ? w.querySelector('.fileinput') : false;
  7.  
  8.     if (this._fileWidget && this._fileWidget.tagName != 'INPUT')
  9.         this._fileWidget = null;
  10.  
  11.     this._uploadWidget = w.querySelector('.fileupload');
  12.     this._deleteWidget = w.querySelector('.filedelete');
  13.     this._downloadWidget = w.querySelector('.filedownload');
  14.     this._statusWidget = w.querySelector('.filestatus');
  15.  
  16.     if (this._draganddrop && this._tagsWidget) {
  17.         this._tagsWidget.addEventListener('drop', (e) => {
  18.             const dt = e.dataTransfer;
  19.  
  20.             e.preventDefault();
  21.  
  22.             if (dt.types.indexOf('Files') != -1) {
  23.                 this._uploadFile(dt.files[0]);
  24.             }
  25.         });
  26.  
  27.         this._tagsWidget.addEventListener('dragenter', (e) => {
  28.             const dt = e.dataTransfer;
  29.  
  30.             if (dt.types.indexOf('Files') != -1) {
  31.                 e.preventDefault();
  32.             }
  33.         });
  34.  
  35.         this._tagsWidget.addEventListener('dragleave', (e) => {
  36.             e.preventDefault();
  37.         });
  38.  
  39.         this._tagsWidget.addEventListener('dragover', (e) => {
  40.             const dt = e.dataTransfer;
  41.  
  42.             e.preventDefault();
  43.  
  44.             dt.dropEffect = dt.types.indexOf('Files') != -1 && !(this._recording || this._uploading) ? 'copy' : 'none';
  45.         });
  46.     }
  47.  
  48.     if (this._fileWidget)
  49.         this._fileWidget.hidden = true;
  50.  
  51.     if (this._uploadWidget) {
  52.         this._uploadWidget.classList.add('disabled');
  53.  
  54.         if (this._uploadURL && this._fileWidget) {
  55.             this._uploadWidget.addEventListener('click', () => {
  56.                 if (!this._uploadWidget.classList.contains('disabled'))
  57.                     this._fileWidget.click();
  58.             });
  59.  
  60.             this._fileWidget.addEventListener('change', (e) => {
  61.                 if (e.target.value) {
  62.                     this._uploadFile(e.target.files[0]);
  63.                 }
  64.             });
  65.         }
  66.         else
  67.             this._uploadWidget = null;
  68.     }
  69.  
  70.     if (this._deleteWidget) {
  71.         this._deleteWidget.classList.add('disabled');
  72.  
  73.         if (this._deleteURL) {
  74.             this._deleteWidget.addEventListener('click', () => {
  75.                 if (!this._deleteWidget.classList.contains('disabled'))
  76.                     this.deleteFile();
  77.             });
  78.         }
  79.         else
  80.             this._deleteWidget = null;
  81.     }
  82.  
  83.     if (this._downloadWidget) {
  84.         this._downloadWidget.classList.add('disabled');
  85.  
  86.         if (this._downloadURL) {
  87.             this._downloadWidget.addEventListener('click', () => {
  88.                 if (!this._downloadWidget.classList.contains('disabled'))
  89.                     this.downloadFile();
  90.             });
  91.         }
  92.         else
  93.             this._downloadWidget = null;
  94.     }
  95.  
  96.     return this;
  97. }

setWidget redefines the method inherited from the View class. It initializes the elements of the interface of this with the different graphical components in w, i. e. the properties _tagsWidget, _uploadWidget, _fileWidget, _deleteWidget, _downloadWidget and _statusWidget of this.

  1. Wall.prototype.destroyWidget = function() {
  2.     View.prototype.destroyWidget.call(this);
  3.  
  4.     if (this._tagsWidget) {
  5.         for (let id in this._slots)
  6.             delete this._slots[id].widget;
  7.     }
  8.  
  9.     this._tagsWidget = null;
  10.  
  11.     this._deleteWidget = null;
  12.  
  13.     this._uploadWidget = null;
  14.     this._fileWidget = null;
  15.  
  16.     this._downloadWidget = null;
  17.  
  18.     this._statusWidget = null;
  19.  
  20.     this._tag = null;
  21.  
  22.     return this;
  23. }

destroyWidget redefines the method inherited from the View class. It sets all the widgets of the interface to null.

  1. Wall.prototype.uploadFile = function() {
  2.     if (this._uploading)
  3.         return this;
  4.  
  5.     if (this._fileWidget)
  6.         this._fileWidget.click();
  7.  
  8.     return this;
  9. }

uploadFile opens the file explorer of the navigator. If a file is selected by the user, the internal method _uploadFile is called with in argument the file descriptor. For security reasons, uploadFile must be called in response to a user interaction.

  1. Wall.prototype._uploadFile = function(fd) {
  2.     if (!this._uploadURL)
  3.         return this;
  4.  
  5.     const filesize = fd.size;
  6.     const filetype = fd.type;
  7.  
  8.     if ((this._filetypes && this._filetypes.indexOf(filetype) == -1) || (this._filetypes && filesize > this._maxfilesize)) {
  9.         this._error = 'upload';
  10.  
  11.         if (this.interfaced())
  12.             this.resetWidget();
  13.  
  14.         return this;
  15.     }
  16.  
  17.     let id;
  18.  
  19.     for (id = 1; id <= this._maxfiles; id++) {
  20.         if (id in this._slots === false)
  21.             break;
  22.     }
  23.  
  24.     if (id > this._maxfiles) {
  25.         this._error = 'upload';
  26.  
  27.         if (this.interfaced())
  28.             this.resetWidget();
  29.  
  30.         return this;
  31.     }
  32.  
  33.     const uploadurl = this._uploadURL;
  34.     const chunksize = this._chunksize;
  35.  
  36.     const filereader = new FileReader();
  37.  
  38.     filereader.onloadend = (e) => postdata(e.target.result);
  39.  
  40.     let offset = 0, progress = 0, blob;
  41.  
  42.     const uploadslice = () => {
  43.         if (this._statusWidget && filesize > chunksize)
  44.             this._statusWidget.innerText = `${progress}%`;
  45.  
  46.         blob = fd.slice(offset, offset + chunksize);
  47.         filereader.readAsDataURL(blob);
  48.     }
  49.  
  50.     const postdata = (data) => {
  51.         $.post(uploadurl, {file_id: id, file_size: filesize, file_type: filetype, file_offset: offset, file_data: data})
  52.             .done(() => {
  53.                 offset += blob.size;
  54.                 progress = Math.floor((offset / filesize) * 100);
  55.  
  56.                 if (progress < 100)
  57.                     uploadslice();
  58.                 else {
  59.                     if (this._statusWidget)
  60.                         this._statusWidget.innerText = '';
  61.  
  62.                     let f;
  63.  
  64.                     switch (filetype) {
  65.                         case 'image/jpeg':
  66.                             f = `${id}.jpg`;
  67.                             break;
  68.                         case 'image/png':
  69.                             f = `${id}.png`;
  70.                             break;
  71.                         case 'image/gif':
  72.                             f = `${id}.gif`;
  73.                             break;
  74.                         case 'application/pdf':
  75.                             f = `${id}.pdf`;
  76.                             break;
  77.                         default:
  78.                             f = `${id}`;
  79.                             break;
  80.                     }
  81.  
  82.                     this._slots[id] = {file: f};
  83.  
  84.                     if (this._tagsWidget) {
  85.                         const img = document.createElement('img');
  86.  
  87.                         img.src = `${this._tagsURL}/${id}.png?nocache=${Date.now()}`;
  88.  
  89.                         img.addEventListener('click', (e) => this._clickImage(e, id));
  90.  
  91.                         this._slots[id].widget = img;
  92.  
  93.                         this._tagsWidget.appendChild(img);
  94.                     }
  95.  
  96.                     this._uploading = false;
  97.  
  98.                     this._error = null;
  99.  
  100.                     if (this.interfaced())
  101.                         this.resetWidget();
  102.                 }
  103.             })
  104.             .fail(() => {
  105.                 if (this._statusWidget)
  106.                     this._statusWidget.innerText = '';
  107.  
  108.                 this._uploading = false;
  109.  
  110.                 this._error = 'upload';
  111.  
  112.                 if (this.interfaced())
  113.                     this.resetWidget();
  114.             });
  115.     };
  116.  
  117.     this._uploading = true;
  118.  
  119.     if (this.interfaced())
  120.         this.resetWidget();
  121.  
  122.     uploadslice();
  123.  
  124.     return this;
  125. }

_uploadFile reads the file described by fd and uploads it on the server. _uploadFile is an internal method called in response to a click on the widget to input a file name, i. e. _fileWidget, or in response to the drag and drop of a file on the widget which shows the thumbnails, i.e. _tagsWidget.

  1. Wall.prototype.deleteFile = function() {
  2.     if (!this._deleteURL)
  3.         return this;
  4.  
  5.     if (!this._tag)
  6.         return this;
  7.  
  8.     if (this._uploading)
  9.         return this;
  10.  
  11.     const deleteurl = this._deleteURL;
  12.  
  13.     const id = this._tag;
  14.  
  15.     const deletefile = () => {
  16.         $.post(deleteurl, {file_id: id} )
  17.             .done(() => {
  18.                 if (this._slots[id].widget)
  19.                     this._slots[id].widget.remove();
  20.  
  21.                 delete this._slots[id];
  22.  
  23.                 if (this._tag == id)
  24.                     this._tag = null;
  25.  
  26.                 this._error = null;
  27.  
  28.                 if (this.interfaced())
  29.                     this.resetWidget();
  30.             })
  31.             .fail(() => {
  32.                 this._error = 'delete';
  33.  
  34.                 if (this.interfaced())
  35.                     this.resetWidget();
  36.             });
  37.     };
  38.  
  39.     deletefile();
  40.  
  41.     return this;
  42. }

deleteFile asks the serveur to delete the file which has been selected on the wall.

  1. Wall.prototype.downloadFile = function() {
  2.     if (!this._downloadURL)
  3.         return this;
  4.  
  5.     if (!this._tag)
  6.         return this;
  7.  
  8.     window.open(`${this._downloadURL}/${this._slots[this._tag].file}`);
  9.  
  10.     return this;
  11. }

downloadFile asks the navigator to download the file which has been selected on the wall.

  1. Wall.prototype.selectTag = function(id) {
  2.     if (this._tag === id)
  3.         return this;
  4.  
  5.     const newtag = this._slots[id];
  6.  
  7.     if (newtag === undefined)
  8.         return this;
  9.  
  10.     if (this._tag) {
  11.         const oldtag = this._slots[this._tag];
  12.  
  13.         if (oldtag.widget)
  14.             oldtag.widget.classList.remove('selected');
  15.     }
  16.  
  17.     if (newtag.widget)
  18.         newtag.widget.classList.add('selected');
  19.  
  20.     this._tag = id;
  21.  
  22.     if (this.interfaced())
  23.         this.resetWidget();
  24.  
  25.     return this;
  26. }

selectTag selects the file of this specified by id.

  1. Wall.prototype.unselectTag = function() {
  2.     if (!this._tag)
  3.         return this;
  4.  
  5.     const tag = this._slots[this._tag];
  6.  
  7.     if (tag.widget)
  8.         tag.widget.classList.remove('selected');
  9.  
  10.     this._tag = null;
  11.  
  12.     if (this.interfaced())
  13.         this.resetWidget();
  14.  
  15.     return this;
  16. }

unselectTag deselects the file of this which is being selected.

  1. Wall.prototype._clickImage = function(e, id) {
  2.     if (e.shiftKey) {
  3.         if (!this._downloadURL)
  4.             return this;
  5.  
  6.         window.open(`${this._downloadURL}/${this._slots[id].file}`);
  7.     }
  8.     else {
  9.         if (this._tag == id)
  10.             this.unselectTag();
  11.         else
  12.             this.selectTag(id);
  13.     }
  14. }

_clickImage is an internal method which in response to a click on the thumbnail of a file, selects or deselects it, or if the Shift key is pressed, downloads the corresponding file.

Server

A file is copied in a disk space reserved to a user on the server in response to a POST by a function which extracts from it the identifier, the MIME type and the size of the file, the block of data which is transferred and its position in the file. The identifier of a file is an integer between 1 and the maximum number of files. A block of data is encoded in BASE64. When the last block of data is saved, the server generates the thumbnail of the file, a reduced image of an image or the first page of a document.

A file is deleted by the server in response to a POST by a function which extracts from it the identifier of the file. The server also the thumbnail associated to the file.

Accessing a file or a thumbnail is done indirectly by a GET which triggers an action on the server which returns the requested file in the disk space reserved to the user. IMPORTANT: A direct access to a file in a user's folder is best rejected by a Deny from all directive for the entire directory managed by a wall.

The code of the server which responds to a POST to copy a file, to a POST to delete a file and to a GET to obtain a file or the thumbnail of a file is programmed for iZend. The file wall.inc is installed in the folder models. The files wallfile.php, wallfileupload.php and wallfiledelete.php are installed in the folder actions. To activates the URLs /wallfile, /wallupload and /walldelete, entries are added in the file aliases.inc. Adapt this code to your development environment.

Every user, identified by her session, has a reserved folder in the directory wall at the root of the site. The thumbnails are in the subfolder tags of a user's folder.

To send back a file or a thumbnail, the server uses the Xsendfile module by Apache. To install and enable this module:

$ sudo apt-get install libapache2-mod-xsendfile
$ sudo a2enmod xsendfile

To activate this module and configure it for a site, add the following lines in the configuration file of the site for Apache:

XSendFile On
XsendFilePath /var/www/mysite.net/wall

<Directory /var/www/mysite.net/wall>
    Deny from all
</Directory>

Replace /var/www/mysite.net by the root directory of the site. Notice that a direct access to the contents of this directory is rejected.

The code which manages the user folders is in the file wall.inc.

  1. require_once 'dirclear.php';

Includes the code of the function dirclear.

  1. function dirclear($dir='.') {
  2.     if (!is_dir($dir)) {
  3.         return false;
  4.     }
  5.  
  6.     dirclearaux($dir);
  7.  
  8.     return true;
  9. }
  10.  
  11. function dirclearaux($dir) {
  12.     $handle = opendir($dir);
  13.     while (($file = readdir($handle)) !== false) {
  14.         if ($file == '.' || $file == '..') {
  15.             continue;
  16.         }
  17.         $filepath = $dir . DIRECTORY_SEPARATOR . $file;
  18.         if (is_link($filepath)) {
  19.             unlink($filepath);
  20.         }
  21.         else if (is_dir($filepath)) {
  22.             dirclearaux($filepath);
  23.             rmdir($filepath);
  24.         }
  25.         else {
  26.             unlink($filepath);
  27.         }
  28.     }
  29.     closedir($handle);
  30. }

dirclear deletes recursively all the files and all the subdirectories which are in the directory $dir. See dirclear in the documentation of the library of iZend.

.
  1. define('WALL_DIR', ROOT_DIR . DIRECTORY_SEPARATOR . 'wall');
  2. define('WALL_DIR_MODE', 0775);
  3.  
  4. define('WALL_NFILES', 20);
  5. define('WALL_FILE_MAX_SIZE', 5*1000000);
  6.  
  7. define('WALL_TAG_WIDTH', 180);
  8.  
  9. define('PNG_QUALITY', 6);

WALL_DIR defines the location of the users folders, i.e. the directory wall at the root of the site. WALL_DIR_MODE defines the creation mode of a folder, more precisely in write mode for the execution group of Apache. WALL_NFILES defines the maximum number of files in a user's folder and WALL_FILE_MAX_SIZE the maximum size of a file. WALL_TAG_WIDTH gives the width of a thumbnail and PNG_QUALITY the quality level of its PNG.

  1. define('GS_COVER', 'gs -q -o "%s" -sDEVICE=pngalpha -dBATCH -dNOPAUSE -DNOSAFER -dFirstPage=1 -dLastPage=1 -dUseCropBox "%s"');

Defines the command-line used to generate an image of the first page of a PDF document. The first %s is replaced by the name of the output file, a temporary PNG file. The second %s is replaced by the name of the input file, the PDF document in the user's folder.

  1. function wall_directory($wall_id) {
  2.     return WALL_DIR . DIRECTORY_SEPARATOR . $wall_id;
  3. }
  4.  
  5. function wall_tags_directory($wall_id) {
  6.     return wall_directory($wall_id) . DIRECTORY_SEPARATOR . 'tags';
  7. }

wall_directory returns the location of the folder $wall_id in WALL_DIR, i. e. the folder of a user. wall_tags_directory returns the location of the thumbnails of the folder $wall_id.

  1. function wall_url() {
  2.     return url('wallfile', false);
  3. }
  4.  
  5. function wall_tags_url() {
  6.     return wall_url() . '/tags';
  7. }

wall_url returns the URL which returns a file. wall_tags_url returns the URL which returns a thumbnail. NOTE: wallfile will look for the wall identifier in the user's session.

  1. function wall_create_directory($wall_id) {
  2.     $dir=wall_directory($wall_id);
  3.  
  4.     if (!file_exists($dir)) {
  5.         if (!@mkdir($dir, WALL_DIR_MODE)) {
  6.             return false;
  7.         }
  8.         $subdir = wall_tags_directory($wall_id);
  9.         if (!@mkdir($subdir, WALL_DIR_MODE)) {
  10.             return false;
  11.         }
  12.     }
  13.  
  14.     return true;
  15. }

wall_create_directory checks if the folder $wall_id exists and if not, creates it with the subfolder for the thumbnails.

  1. function wall_delete_directory($wall_id) {
  2.     $dir=wall_directory($wall_id);
  3.  
  4.     dirclear($dir);
  5.  
  6.     return @rmdir($dir);
  7. }

wall_delete_directory deletes all the contents of the folder $wall_id.

  1. function wall_contents($wall_id, $sort=true) {
  2.     $filelist=false;
  3.  
  4.     $dir=wall_directory($wall_id);
  5.     $files=glob($dir . DIRECTORY_SEPARATOR . '*.{jpg,png,gif,pdf}', GLOB_BRACE | GLOB_NOSORT);
  6.  
  7.     if ($files) {
  8.         if ($sort) {
  9.             usort($files, function($f1, $f2) {
  10.                 return filemtime($f1) > filemtime($f2);
  11.             });
  12.         }
  13.  
  14.         $filelist=array();
  15.         foreach ($files as $f) {
  16.             $filelist[]=pathinfo($f, PATHINFO_BASENAME);
  17.         }
  18.     }
  19.  
  20.     return $filelist;
  21. }

wall_contents returns the names of all the JPG, PNG, GIF or PDF files in the folder $wall_id. If $sort is true, the files are sorted by the date and the time they were last modified.

  1. function wall_tag_file($wall_id, $file_id) {
  2.     return wall_tags_directory($wall_id) . DIRECTORY_SEPARATOR . $file_id . '.png';
  3. }

wall_tag_file returns the full name of the thumbnail of the file $file_id in $wall_id.

  1. function wall_create_tag($wall_id, $file_id, $img, $width, $height, $imgtype) {
  2.     if ($width > WALL_TAG_WIDTH) {
  3.         $w=WALL_TAG_WIDTH;
  4.         $h=$height * (WALL_TAG_WIDTH / $width);
  5.     }
  6.     else {
  7.         $w=$width;
  8.         $h=$height;
  9.     }
  10.  
  11.     $imgcopy = imagecreatetruecolor($w, $h);
  12.  
  13.     if ($imgtype == IMAGETYPE_PNG or $imgtype == IMAGETYPE_GIF) {
  14.         $bg=imagecolorallocate($imgcopy, 255, 255, 255);
  15.         imagefill($imgcopy, 0, 0, $bg);
  16.     }
  17.  
  18.     if ($width > WALL_TAG_WIDTH) {
  19.         imagecopyresampled($imgcopy, $img, 0, 0, 0, 0, $w, $h, $width, $height);
  20.     }
  21.     else {
  22.         imagecopy($imgcopy, $img, 0, 0, 0, 0, $w, $h);
  23.     }
  24.  
  25.     $filecopy=wall_tag_file($wall_id, $file_id);
  26.  
  27.     if (!@imagepng($imgcopy, $filecopy, PNG_QUALITY))  {
  28.         return false;
  29.     }
  30.  
  31.     clearstatcache(true, $filecopy);
  32.  
  33.     imagedestroy($imgcopy);
  34.  
  35.     return true;
  36. }

wall_create_tag creates the thumbnail of the file $file_id in $wall_id from the image $img which has a width of $width pixels and a height of $height pixels and the type $imgtype. If $img is larger than WALL_TAG_WIDTH, the thumbnail has a width of WALL_TAG_WIDTH pixels and a height preserving the aspect ratio of $img. If $img is a PNG or a GIF, the thumbnail has a white background.

  1. require_once 'filemimetype.php';

Includes the code of the function file_mime_type.

  1. function file_mime_type($file, $encoding=true) {
  2.     $mime=false;
  3.  
  4.     if (function_exists('finfo_file')) {
  5.         $finfo = finfo_open(FILEINFO_MIME);
  6.         $mime = @finfo_file($finfo, $file);
  7.         finfo_close($finfo);
  8.     }
  9.     else if (substr(PHP_OS, 0, 3) == 'WIN') {
  10.         $mime = mime_content_type($file);
  11.     }
  12.     else {
  13.         $file = escapeshellarg($file);
  14.         $cmd = "file -iL $file";
  15.  
  16.         exec($cmd, $output, $r);
  17.  
  18.         if ($r == 0) {
  19.             $mime = substr($output[0], strpos($output[0], ': ')+2);
  20.         }
  21.     }
  22.  
  23.     if (!$mime) {
  24.         return false;
  25.     }
  26.  
  27.     if ($encoding) {
  28.         return $mime;
  29.     }
  30.  
  31.     return substr($mime, 0, strpos($mime, '; '));
  32. }

file_mime_type returns a character string giving the MIME type of the file $file, e.g. image/jpeg, and if $encoding is true, the character set of the file preceded by a ; (SEMICOLON), e.g. image/jpeg; charset=binary. See file_mime_type in the documentation of the library of iZend.

  1. require_once 'models/wall.inc';

Includes the code of the function wall_directory.

  1. function wallfile($lang, $arglist=false) {

Defines the function wallfile which is called by the URL /wallfile of the website. The arguments $lang and $arglist are prepared by iZend. Adapt the code to your development environment. $lang is false. The content returned by this action is independent of the user's language. $arglist is an array which contains the rest of the URL of the request and its parameters , e.g. array(0 => '1.jpg') for /wallfile/1.jpg and array(0 => 'tags', 1 => '1.png', 'nocache' => '1600709132578') for /wallfile/tags/1.png?nocache=1600709132578.

  1.     if (!isset($_SESSION['wall_id'])) {
  2.         goto badrequest;
  3.     }
  4.  
  5.     $wall_id=$_SESSION['wall_id'];

Extrracts the identifier of the user's wall saved in the session. Returns an error HTTP 400 Bad Request if no identifier is found in the session.

  1.     if (!is_array($arglist)) {
  2.         goto badrequest;
  3.     }
  4.  
  5.     $filepath=wall_directory($wall_id) . DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, array_filter($arglist, 'is_int', ARRAY_FILTER_USE_KEY));
  6.  
  7.     if (!file_exists($filepath)) {
  8.         goto notfound;
  9.     }

Checks that the URL isn't incomplete. Builds the full name of the requested file with the user's directory and the elements passed in the URL. EXAMPLE: The URL /wallfile/tags/1.png has been deconstructed to extract from it the action, /wallfile corresponding to the function wallfile, then the reset of the URL has been decomposed in the array passed in argument to the function, /tags/1.png giving the array array(0 => 'tags', 1 => '1.png'). If the user has the identifier 5f690ce2c930d, the full name of the file is /wall/5f690ce2c930d/tags/1.png in the root directory of the website. Returns an error HTTP 404 Not Found if the requested file cannot be found in the user's space..

  1.     $filesize=filesize($filepath);
  2.     $filetype=file_mime_type($filepath);
  3.     if (!$filetype) {
  4.         $filetype = 'application/octet-stream';
  5.     }

Computes the size and the type of the requested file.

  1.     header("X-Sendfile: $filepath");
  2.     header("Content-Type: $filetype");
  3.     header("Content-Length: $filesize");
  4.  
  5.     return false;

Returns a HTTP document with just in the header a directive X-Sendfile so Apache directly returns the specified file.

  1. badrequest:
  2.     header('HTTP/1.1 400 Bad Request');
  3.     return false;

Returns a plain header 400 Bad Request.

  1. notfound:
  2.     header('HTTP/1.1 404 Not Found');
  3.     return false;
  4. }

Returns a plain header 404 Not Found.

  1. require_once 'models/wall.inc';

Includes the code of the functions wall_directory and wall_create_tag, the constant WALL_NFILES.

  1. function wallfileupload($lang, $arglist=false) {

Defines the function wallfileupload which is called by the URL /wallupload of the site. The arguments $lang and $arglist are not used. The content returned by this action is independent of the user's language. No argument or parameter is expected from the URL.

  1.     if (!isset($_SESSION['wall_id'])) {
  2.         goto badrequest;
  3.     }
  4.  
  5.     $wall_id=$_SESSION['wall_id'];

Extrracts the identifier of the user's wall saved in the session. Returns an error HTTP 400 Bad Request if no identifier is found in the session.

  1.     $maxfilesize=WALL_FILE_MAX_SIZE;
  2.  
  3.     $filetypes=array('image/jpeg', 'image/png', 'image/gif', 'application/pdf');

Initializes $maxfilesize to WALL_FILE_MAX_SIZE and filetypes to the list of supported MIME types.

  1.     $id=$type=$data=false;
  2.     $size=$offset=0;

Initializes the variables of the arguments of the POST.

  1.     if (isset($_POST['file_id'])) {
  2.         $id=$_POST['file_id'];
  3.     }
  4.     if (isset($_POST['file_size'])) {
  5.         $size=$_POST['file_size'];
  6.     }
  7.     if (isset($_POST['file_type'])) {
  8.         $type=$_POST['file_type'];
  9.     }
  10.     if (isset($_POST['file_offset'])) {
  11.         $offset=$_POST['file_offset'];
  12.     }
  13.     if (isset($_POST['file_data'])) {
  14.         $data=explode(';base64,', $_POST['file_data']);
  15.         $data=is_array($data) && isset($data[1]) ? base64_decode($data[1]) : false;
  16.     }

Extracts the arguments of the POST. The block of data encoded in BASE64 is decoded.

  1.     if (!is_numeric($id) or $id < 1 or $id > WALL_NFILES) {
  2.         goto badrequest;
  3.     }
  4.  
  5.     if (!is_numeric($offset) or $offset < 0) {
  6.         goto badrequest;
  7.     }
  8.  
  9.     if (!is_numeric($size) or $size < 0 or ($maxfilesize and $size > $maxfilesize)) {
  10.         goto badrequest;
  11.     }
  12.  
  13.     if (!$type or !in_array($type, $filetypes)) {
  14.         goto badrequest;
  15.     }

Checks the arguments of the POST.

  1.     if (!$data) {
  2.         goto badrequest;
  3.     }
  4.  
  5.     $datasize=strlen($data);
  6.  
  7.     if ($offset + $datasize > $size) {
  8.         goto badrequest;
  9.     }

Checks the size of the data sent in the POST.

  1.     $fname=$id;
  2.  
  3.     switch ($type) {
  4.         case 'image/jpeg':
  5.             $fname=$id . '.jpg';
  6.             break;
  7.         case 'image/png':
  8.             $fname=$id . '.png';
  9.             break;
  10.         case 'image/gif':
  11.             $fname=$id . '.gif';
  12.             break;
  13.         case 'application/pdf':
  14.             $fname=$id . '.pdf';
  15.             break;
  16.         default:
  17.             break;
  18.     }
  19.  
  20.     $dir=wall_directory($wall_id);
  21.     $file=$dir . DIRECTORY_SEPARATOR . $fname;

Defines the name of the file from the identifier of the file and its MIME type. Computes the full name of the file in the user's space.

  1.     $fout = @fopen($file, $offset == 0 ? 'wb' : 'cb');
  2.  
  3.     if ($fout === false) {
  4.         goto internalerror;
  5.     }

Opens the file in binary mode. If the POST contains the first block of data, the file is created.

  1.     $r = fseek($fout, $offset);
  2.  
  3.     if ($r == -1) {
  4.         goto internalerror;
  5.     }

Moves the file pointer to the position of the block of data.

  1.     $r = fwrite($fout, $data);
  2.  
  3.     if ($r === false) {
  4.         goto internalerror;
  5.     }

Writes the block of data.

  1.     if ($offset + $datasize < $size) {
  2.         return false;
  3.     }

Returns a plain header 200 Ok if more blocks of data are expected.

  1.     $img=false;
  2.  
  3.     switch ($type) {
  4.         case 'image/jpeg':
  5.         case 'image/png':
  6.         case 'image/gif':
  7.             $fileinfo=@getimagesize($file);
  8.  
  9.             if (!$fileinfo) {
  10.                 break;
  11.             }
  12.  
  13.             $imgtype=$fileinfo[2];
  14.  
  15.             switch ($imgtype) {
  16.                 case IMAGETYPE_JPEG:
  17.                     $img = imagecreatefromjpeg($file);
  18.                     break;
  19.                 case IMAGETYPE_PNG:
  20.                     $img = imagecreatefrompng($file);
  21.                     break;
  22.                 case IMAGETYPE_GIF:
  23.                     $img = imagecreatefromgif($file);
  24.                     break;
  25.                 default:
  26.                     break;
  27.             }
  28.  
  29.             break;
  30.  
  31.         case 'application/pdf':
  32.             $tmpdir=ini_get('upload_tmp_dir') ?: ROOT_DIR . DIRECTORY_SEPARATOR . 'tmp';
  33.             $tmpfile=$tmpdir . DIRECTORY_SEPARATOR . uniqid() . '.png';
  34.             $cmdline=sprintf(GS_COVER, $tmpfile, $file);
  35.             @exec($cmdline, $output, $ret);
  36.  
  37.             $fileinfo=$ret == 0 ? @getimagesize($tmpfile) : false;
  38.  
  39.             if ($fileinfo) {
  40.                 $imgtype=$fileinfo[2];
  41.                 $img = imagecreatefrompng($tmpfile);
  42.             }
  43.  
  44.             @unlink($tmpfile);
  45.  
  46.             break;
  47.  
  48.         default:
  49.             break;
  50.     }
  51.  
  52.     if (!$img) {
  53.         @unlink($file);
  54.  
  55.         goto internalerror;
  56.     }

When the entire file is saved, if the file is an image, creates a PHP image according to its type, or if the file is a document, runs the proper gs command to generate an image of the first page of the PDF.

  1.     $w=$fileinfo[0];
  2.     $h=$fileinfo[1];
  3.  
  4.     $r = wall_create_tag($wall_id, $id, $img, $w, $h, $imgtype);
  5.  
  6.     if (!$r) {
  7.         @unlink($file);
  8.  
  9.         goto internalerror;
  10.     }
  11.  
  12.     return false;

Creates the thumbnail of the file with wall_create_tag and returns a plain HTTP document 200 Ok.

  1. badrequest:
  2.     header('HTTP/1.1 400 Bad Request');
  3.     return false;

Returns a plain header 400 Bad Request.

  1. internalerror:
  2.     header('HTTP/1.1 500 Internal Error');
  3.     return false;
  4. }

Returns a plain header 500 Internal Error.

  1. require_once 'models/wall.inc';

Includes the code of the functions wall_directory and wall_tags_directory, the constant WALL_NFILES.

  1. function wallfiledelete($lang, $arglist=false) {

Defines the function wallfiledelete which is called by the URL /walldelete of the web site. The arguments $lang and $arglist are not used. The content returned by this action is independent of the user's language. No argument or parameter is expected from the URL.

  1.     if (!isset($_SESSION['wall_id'])) {
  2.         goto badrequest;
  3.     }
  4.  
  5.     $wall_id=$_SESSION['wall_id'];

Extrracts the identifier of the user's wall saved in the session. Returns an error HTTP 400 Bad Request if no identifier is found in the session.

  1.     $id=false;

Initializes the identifier of the file in argument of the POST.

  1.     if (isset($_POST['file_id'])) {
  2.         $id=$_POST['file_id'];
  3.     }

Extracts the identifier of the file from the POST.

  1.     if (!is_numeric($id) or $id < 1 or $id > WALL_NFILES) {
  2.         goto badrequest;
  3.     }

Checks the identifier of the file.

  1.     $dir=wall_directory($wall_id);
  2.     $files=glob($dir . DIRECTORY_SEPARATOR . $id . '.{jpg,png,gif,pdf}', GLOB_BRACE | GLOB_NOSORT);
  3.  
  4.     if (!$files) {
  5.         goto badrequest;
  6.     }
  7.  
  8.     $file=$files[0];
  9.  
  10.     if (!file_exists($file)) {
  11.         goto badrequest;
  12.     }

Looks for the file in the user's space. Returns an error HTTP 400 Bad Request if the specified file isn't found.

  1.     $r = @unlink($file);
  2.  
  3.     if (!$r) {
  4.         goto internalerror;
  5.     }

Deletes the file.

  1.     $dir=wall_tags_directory($wall_id);
  2.     $file=$dir . DIRECTORY_SEPARATOR . $id . '.png';
  3.  
  4.     $r = @unlink($file);
  5.  
  6.     if (!$r) {
  7.         goto internalerror;
  8.     }
  9.  
  10.     return false;

Looks for the thumbnail of the file in the user's space and deletes it.

  1. badrequest:
  2.     header('HTTP/1.1 400 Bad Request');
  3.     return false;

Returns a plain header 400 Bad Request.

  1. internalerror:
  2.     header('HTTP/1.1 500 Internal Error');
  3.     return false;
  4. }

Returns a plain header 500 Internal Error.

Test
  1. <?php $debug=true; ?>

Setting $debug to true gives access in the console of the navigator to the variable wall, the instance of the Wall. If $debug is false, all the code in JavaScript is protected by a closure function.

  1. require_once 'models/wall.inc';
  2.  
  3. if (!isset($_SESSION['wall_id'])) {
  4.     $wall_id = uniqid();
  5.  
  6.     $_SESSION['wall_id'] = $wall_id;
  7.  
  8. }
  9. else {
  10.     $wall_id = $_SESSION['wall_id'];
  11. }
  12.  
  13. wall_create_directory($wall_id);
  14.  
  15. $filelist=wall_contents($wall_id);

Includes the code of the functions wall_create_directory and wall_contents. Initializes the identifier of the user's wall and saves it in the session, if necessary, otherwise recovers it. Creates the user's wall, i.e. the disk space for the user, if not already created. Recovers the list of the files on the user's wall.

  1. <?php $tags_url='/wallfile/tags'; ?>
  2. <?php $download_url='/wallfile'; ?>
  3. <?php $upload_url='/wallupload'; ?>
  4. <?php $delete_url='/walldelete'; ?>
  5. <?php $filetypes=array('image/jpeg', 'image/png', 'image/gif', 'application/pdf'); ?>
  6. <?php $maxfilesize=5*100000; ?>
  7. <?php $chunksize=100000; ?>
  8. <?php $maxfiles=20; ?>

Configures the options of the wall. Adapt the URLs to your environment.

  1. <?php $id=uniqid('id'); ?>

Defines the identifier of the <div> which surrounds the HTML of the wall.

  1. <div id="<?php echo $id; ?>" class="noprint">
  2. <div class="ojs_wall">
  3. <div class="tags"></div>
  4. <div class="filecontrols">
  5. <i class="fileupload fa fa-lg fa-fw fa-upload"></i>
  6. <i class="filedelete fa fa-lg fa-fw fa-trash"></i>
  7. <i class="filedownload fa fa-lg fa-fw fa-download"></i>
  8. <span class="filestatus"></span>
  9. </div>
  10. <input type="file" class="fileinput"/>
  11. </div>
  12. </div>

Creates the widgets of the interface. Notice the names of the classes of the different components which are expected by the setWidget method of a wall.

  1. <?php head('javascript', '/objectivejs/Objective.js'); ?>
  2. <?php head('javascript', '/objectivejs/Responder.js'); ?>
  3. <?php head('javascript', '/objectivejs/View.js'); ?>
  4. <?php head('javascript', '/objectivejs/Wall.js'); ?>

Includes the code of all the necessary classes. REMINDER: The function head of the iZend library adds a tag such as <script src="/objectivejs/Objective.js"></script> to the <head> section of the document in HTML. Adapt the code to your development environment.

  1. <?php if (!$debug): ?>
  2. (function() {
  3. <?php endif; ?>

Isolates all the code in JavaScript in a closure function if $debug is false.

  1.     const options = {
  2.         draganddrop: true,
  3.         maxfiles: 10,
  4.         maxfilesize: 5*1000000,
  5.         filetypes: [<?php echo implode(',', array_map(function($s) { return "'$s'"; }, $filetypes)); ?>],
  6.         tagsURL: '<?php echo $tags_url; ?>',
  7.         downloadURL: '<?php echo $download_url; ?>',
  8.         deleteURL: '<?php echo $delete_url; ?>',
  9.         uploadURL: '<?php echo $upload_url; ?>',
  10.         chunksize: <?php echo $chunksize; ?>
  11.     }
  12.  
  13.     const wall = new Wall(options);

Creates an instance of Wall with the desired options.

  1.     const container = document.querySelector('#<?php echo $id; ?>');

Retrieves the <div> which surrounds the HTML of the program.

  1.     wall.setManagedWidget(container.querySelector('.ojs_wall')).resetWidget();

Creates and displays the interface of the wall.

  1.     const filelist = [<?php echo implode(',', array_map(function($s) { return "'$s'"; }, $filelist)); ?>];
  2.  
  3.     wall.files = filelist;

Configures and displays the thumbnails of the wall from the list of the user's files.

  1. <?php if (!$debug): ?>
  2. })();
  3. <?php endif; ?>

Closes the function which isolates the code in JavaScript if $debug is false.

SEE ALSO

View, AudioPlayer, Write data on a server

Comments

Your comment:
[p] [b] [i] [u] [s] [quote] [pre] [br] [code] [url] [email] strip help 2000

Enter a maximum of 2000 characters.
Improve the presentation of your text with the following formatting tags:
[p]paragraph[/p], [b]bold[/b], [i]italics[/i], [u]underline[/u], [s]strike[/s], [quote]citation[/quote], [pre]as is[/pre], [br]line break,
[url]http://www.izend.org[/url], [url=http://www.izend.org]site[/url], [email]izend@izend.org[/email], [email=izend@izend.org]izend[/email],
[code]command[/code], [code=language]source code in c, java, php, html, javascript, xml, css, sql, bash, dos, make, etc.[/code].