51

Wall

An instance of Wall manages a deposit of JPG, PNG or GIF images and PDF documents shown as a wall of thumbnails. 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. Adding the management of other types of files like LibreOffice documents is a simple exercise.

To edit files directly in a navigator with CODE – Collabora Online Development Edition from a wall of documents, see Collaboractor.

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.

Move the pointer of the mouse over an image to display the name of the file.

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.

<p class="noprint"><button id="btn_upload_file" type="submit" class="narrow" title=""><i class="fa fa-upload"></i></button></p>
<script>
document.getElementById('btn_upload_file').onclick = () => wall.uploadFile();
</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 tagURL = options.tagURL;
  7.  
  8.     let deleteURL = options.deleteURL;
  9.     let uploadURL = options.uploadURL;
  10.  
  11.     let fileURL = options.fileURL;
  12.  
  13.     if (! (typeof tagURL === 'undefined' || tagURL === null || typeof tagURL === '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 fileURL === 'undefined' || fileURL === null || typeof fileURL === '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._tagURL = tagURL;
  72.  
  73.     this._deleteURL = deleteURL;
  74.     this._uploadURL = uploadURL;
  75.  
  76.     this._fileURL = fileURL;
  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, tagURL, uploadURL, deleteURL and fileURL. 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. tagURL 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. fileURL is a character string which specifies the URL of the GET sent to the server to get a file. tagURL, uploadURL, deleteURL and fileURL are optional.

  1. Object.defineProperty(Wall.prototype, 'files', {
  2.     get:    function() {
  3.         return Object.keys(this._slots);
  4.     },
  5.     set:    function(filelist) {
  6.         let slots = {};
  7.  
  8.         if (filelist) {
  9.             if (!Array.isArray(filelist))
  10.                 throw new TypeError();
  11.  
  12.             for (let filename of filelist) {
  13.                 if (typeof filename !== 'string')
  14.                     throw new TypeError();
  15.  
  16.                 slots[filename] = null;
  17.             }
  18.         }
  19.  
  20.         this._slots = slots;
  21.  
  22.         if (this._tagsWidget)
  23.             this.resetTagsWidget();
  24.  
  25.         if (this.interfaced())
  26.             this.resetWidget();
  27.     }
  28. });

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. Object.defineProperty(Wall.prototype, 'tag', {
  2.     get:    function() {
  3.         return this._tag;
  4.     }
  5. });

tag is an accessor which returns the name of the file selected in this.

  1. Wall.prototype.resetTagsWidget = function() {
  2.     this._tagsWidget.innerHTML = '';
  3.  
  4.     for (let filename in this._slots) {
  5.         const img = document.createElement('img');
  6.  
  7.         img.src = `${this._tagURL}/${encodeURI(filename)}`;
  8.  
  9.         img.title = filename;
  10.  
  11.         img.addEventListener('click', (e) => this._clickImage(e, filename));
  12.  
  13.         this._slots[filename] = img;
  14.  
  15.         this._tagsWidget.appendChild(img);
  16.     }
  17.  
  18.     return this;
  19. };

resetTagsWidget displays the thumbnails of the files managed by this.

  1. Wall.prototype.resetWidget = function() {
  2.     if (this._uploadWidget) {
  3.         if (!this._uploading && Object.keys(this._slots).length < this._maxfiles)
  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._tagURL ? w.querySelector('.tags') : null;
  5.  
  6.     this._fileWidget = this._uploadURL ? w.querySelector('.fileinput') : null;
  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._uploading && Object.keys(this._slots).length < this._maxfiles ? '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._fileURL) {
  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 filename in this._slots)
  6.             this._slots[filename] = null;
  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 filename = fd.name;
  6.     const filesize = fd.size;
  7.     const filetype = fd.type;
  8.  
  9.     if (filename in this._slots || Object.keys(this._slots).length >= this._maxfiles) {
  10.         this._error = 'upload';
  11.  
  12.         if (this.interfaced())
  13.             this.resetWidget();
  14.  
  15.         return this;
  16.     }
  17.  
  18.     if ((this._filetypes && this._filetypes.indexOf(filetype) == -1) || (this._maxfilesize && filesize > this._maxfilesize)) {
  19.         this._error = 'upload';
  20.  
  21.         if (this.interfaced())
  22.             this.resetWidget();
  23.  
  24.         return this;
  25.     }
  26.  
  27.     const uploadurl = this._uploadURL;
  28.     const chunksize = this._chunksize;
  29.  
  30.     const filereader = new FileReader();
  31.  
  32.     filereader.onloadend = (e) => postdata(e.target.result);
  33.  
  34.     let offset = 0, progress = 0, blob;
  35.  
  36.     const uploadslice = () => {
  37.         blob = fd.slice(offset, offset + chunksize);
  38.         filereader.readAsDataURL(blob);
  39.  
  40.         if (this._statusWidget) {
  41.             progress = Math.floor(((offset + blob.size) / filesize) * 100);
  42.             this._statusWidget.innerText = `${progress}%`;
  43.         }
  44.  
  45.     };
  46.  
  47.     const postdata = (data) => {
  48.         $.post(uploadurl, {file_name: filename, file_size: filesize, file_type: filetype, file_offset: offset, file_data: data})
  49.             .done(() => {
  50.                 offset += blob.size;
  51.  
  52.                 if (offset < filesize)
  53.                     uploadslice();
  54.                 else {
  55.                     if (this._statusWidget)
  56.                         this._statusWidget.innerText = '';
  57.  
  58.                     this._slots[filename] = null;
  59.  
  60.                     this.respondTo('wallFileAdded', this, filename);
  61.  
  62.                     if (this._tagsWidget) {
  63.                         const img = document.createElement('img');
  64.  
  65.                         img.src = `${this._tagURL}/${encodeURI(filename)}?nocache=${Date.now()}`;
  66.  
  67.                         img.title = filename;
  68.  
  69.                         img.addEventListener('click', (e) => this._clickImage(e, filename));
  70.  
  71.                         this._slots[filename] = img;
  72.  
  73.                         this._tagsWidget.appendChild(img);
  74.                     }
  75.  
  76.                     this._uploading = false;
  77.  
  78.                     this._error = null;
  79.  
  80.                     if (this.interfaced())
  81.                         this.resetWidget();
  82.                 }
  83.             })
  84.             .fail(() => {
  85.                 if (this._statusWidget)
  86.                     this._statusWidget.innerText = '';
  87.  
  88.                 this._uploading = false;
  89.  
  90.                 this._error = 'upload';
  91.  
  92.                 if (this.interfaced())
  93.                     this.resetWidget();
  94.             });
  95.     };
  96.  
  97.     this._uploading = true;
  98.  
  99.     if (this.interfaced())
  100.         this.resetWidget();
  101.  
  102.     uploadslice();
  103.  
  104.     return this;
  105. };

_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 filename = this._tag;
  14.  
  15.     const deletefile = () => {
  16.         $.post(deleteurl, {file_name: filename} )
  17.             .done(() => {
  18.                 if (this._slots[filename])
  19.                     this._slots[filename].remove();
  20.  
  21.                 delete this._slots[filename];
  22.  
  23.                 this.respondTo('wallFileDeleted', this, filename);
  24.  
  25.                 if (this._tag == filename) {
  26.                     this._tag = null;
  27.  
  28.                     this.respondTo('wallSelectionChanged', this);
  29.                 }
  30.  
  31.                 this._error = null;
  32.  
  33.                 if (this.interfaced())
  34.                     this.resetWidget();
  35.             })
  36.             .fail(() => {
  37.                 this._error = 'delete';
  38.  
  39.                 if (this.interfaced())
  40.                     this.resetWidget();
  41.             });
  42.     };
  43.  
  44.     deletefile();
  45.  
  46.     return this;
  47. };

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

  1. Wall.prototype.downloadFile = function() {
  2.     if (!this._fileURL)
  3.         return this;
  4.  
  5.     if (!this._tag)
  6.         return this;
  7.  
  8.     const url = `${this._fileURL}/${encodeURI(this._tag)}`;
  9.  
  10.     window.open(url);
  11.  
  12.     return this;
  13. };

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

  1. Wall.prototype.selectTag = function(filename) {
  2.     if (this._tag === filename)
  3.         return this;
  4.  
  5.     if (this._slots[filename] === undefined)
  6.         return this;
  7.  
  8.     if (this._tag && this._slots[this._tag])
  9.         this._slots[this._tag].classList.remove('selected');
  10.  
  11.     this._tag = filename;
  12.  
  13.     if (this._slots[this._tag])
  14.         this._slots[this._tag].classList.add('selected');
  15.  
  16.     if (this.interfaced())
  17.         this.resetWidget();
  18.  
  19.     return this;
  20. };

selectTag selects the file of this specified by id.

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

unselectTag deselects the file of this which is being selected.

  1. Wall.prototype._clickImage = function(e, filename) {
  2.     if (e.shiftKey) {
  3.         if (!this._fileURL)
  4.             return;
  5.  
  6.         const url = `${this._fileURL}/${encodeURI(filename)}`;
  7.  
  8.         window.open(url);
  9.     }
  10.     else {
  11.         if (this._tag == filename)
  12.             this.unselectTag();
  13.         else
  14.             this.selectTag(filename);
  15.  
  16.         this.respondTo('wallSelectionChanged', this);
  17.     }
  18.  
  19. };

_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 name, the MIME type and the size of the file, the block of data which is transferred and its position in the file. 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 name of the file. The server deletes 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.

function dirclear($dir='.') {
    if (!is_dir($dir)) {
        return false;
    }

    dirclearaux($dir);

    return true;
}

function dirclearaux($dir) {
    $handle = opendir($dir);
    while (($file = readdir($handle)) !== false) {
        if ($file == '.' || $file == '..') {
            continue;
        }
        $filepath = $dir . DIRECTORY_SEPARATOR . $file;
        if (is_link($filepath)) {
            unlink($filepath);
        }
        else if (is_dir($filepath)) {
            dirclearaux($filepath);
            rmdir($filepath);
        }
        else {
            unlink($filepath);
        }
    }
    closedir($handle);
}

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_FILE_TYPES', array(
  8.     'application/pdf',
  9.     'image/jpeg',
  10.     'image/png',
  11.     'image/gif'
  12. ));
  13.  
  14. define('WALL_TAG_WIDTH', 180);
  15.  
  16. 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_FILE_TYPES lists the file types managed by the wall. 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" -dBATCH -dNOPAUSE -dSAFER -sDEVICE=png16m -dTextAlphaBits=4 -dGraphicsAlphaBits=4 -dAutoRotatePages=/None -dUseCropBox -r96 -dFirstPage=1 -dLastPage=1 "%s" 2>&1');

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_create_directory($wall_id) {
  2.     $dir=wall_directory($wall_id);
  3.  
  4.     if (file_exists($dir)) {
  5.         return true;
  6.     }
  7.  
  8.     if (!@mkdir($dir, WALL_DIR_MODE)) {
  9.         return false;
  10.     }
  11.  
  12.     $subdir = wall_tags_directory($wall_id);
  13.  
  14.     if (!@mkdir($subdir, WALL_DIR_MODE)) {
  15.         return false;
  16.     }
  17.  
  18.     return true;
  19. }

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 . '*.*', GLOB_NOSORT);
  6.  
  7.     if ($files) {
  8.         if ($sort) {
  9.             usort($files, function($f1, $f2) {
  10.                 $t1=filemtime($f1);
  11.                 $t2=filemtime($f2);
  12.  
  13.                 if ($t1 == $t2)  {
  14.                     return 0;
  15.                 }
  16.  
  17.                 return ($t1 < $t2) ? -1 : 1;
  18.             });
  19.         }
  20.  
  21.         $filelist=array();
  22.         foreach ($files as $f) {
  23.             $filelist[]=pathinfo($f, PATHINFO_BASENAME);
  24.         }
  25.     }
  26.  
  27.     return $filelist;
  28. }

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_file($wall_id, $fname) {
  2.     return wall_directory($wall_id) . DIRECTORY_SEPARATOR . $fname;
  3. }
  4.  
  5. function wall_file_tag($wall_id, $fname) {
  6.     return wall_tags_directory($wall_id) . DIRECTORY_SEPARATOR . str_replace('.', '_', $fname) . '.png';
  7. }

wall_file returns the full name of the file $fname in $wall_id. wall_file_tag returns the full name of the thumbnail of the file $fname in $wall_id.

  1. function wall_tag_pdf($wall_id, $fname, $pdffile) {
  2.     $tmpdir=ini_get('upload_tmp_dir') ?: ROOT_DIR . DIRECTORY_SEPARATOR . 'tmp';
  3.     $tmpfile=$tmpdir . DIRECTORY_SEPARATOR . uniqid('pdf') . '.png';
  4.  
  5.     $cmdline=sprintf(GS_COVER, $tmpfile, $pdffile);
  6.  
  7.     @exec($cmdline, $output, $ret);
  8.  
  9.     if ($ret != 0) {
  10.         return false;
  11.     }
  12.  
  13.     $r = wall_create_tag($wall_id, $fname, $tmpfile);
  14.  
  15.     @unlink($tmpfile);
  16.  
  17.     return $r;
  18. }

wall_tag_pdf creates the thumbnail of the file $fname in $wall_id from the document in $pdffile. wall_tag_pdf creates a temporary file containing the image of the first page of the document.

  1. function wall_tag_img($wall_id, $fname, $imgfile) {
  2.     return  wall_create_tag($wall_id, $fname, $imgfile);
  3. }

wall_tag_img creates the thumbnail of the file $fname in $wall_id from the image in $imgfile.

  1. function wall_create_tag($wall_id, $fname, $imgfile) {
  2.     $fileinfo=@getimagesize($imgfile);
  3.  
  4.     if (!$fileinfo) {
  5.         return false;
  6.     }
  7.  
  8.     $width=$fileinfo[0];
  9.     $height=$fileinfo[1];
  10.  
  11.     $imgtype=$fileinfo[2];
  12.  
  13.     $img=false;
  14.  
  15.     switch ($imgtype) {
  16.         case IMAGETYPE_JPEG:
  17.             $img=imagecreatefromjpeg($imgfile);
  18.             break;
  19.         case IMAGETYPE_PNG:
  20.             $img=imagecreatefrompng($imgfile);
  21.             break;
  22.         case IMAGETYPE_GIF:
  23.             $img=imagecreatefromgif($imgfile);
  24.             break;
  25.         default:
  26.             break;
  27.     }
  28.  
  29.     if (!$img) {
  30.         return false;
  31.     }
  32.  
  33.     if ($imgtype == IMAGETYPE_JPEG) {
  34.         $exif = @exif_read_data($imgfile);
  35.  
  36.         if (!empty($exif['Orientation'])) {
  37.             switch ($exif['Orientation']) {
  38.                 case 3:
  39.                     $deg=180;
  40.                     break;
  41.                 case 6:
  42.                     $deg=-90;
  43.                     break;
  44.                 case 8:
  45.                     $deg=90;
  46.                     break;
  47.                 default:
  48.                     $deg=0;
  49.                     break;
  50.             }
  51.  
  52.             if ($deg != 0) {
  53.                 $img = imagerotate($img, $deg, 0);
  54.  
  55.                 if ($deg=90 or $deg=-90) {
  56.                     $width=$fileinfo[1];
  57.                     $height=$fileinfo[0];
  58.                 }
  59.             }
  60.         }
  61.     }
  62.  
  63.     if ($width > WALL_TAG_WIDTH) {
  64.         $w=WALL_TAG_WIDTH;
  65.         $h=$height * (WALL_TAG_WIDTH / $width);
  66.     }
  67.     else {
  68.         $w=$width;
  69.         $h=$height;
  70.     }
  71.  
  72.     $imgcopy = imagecreatetruecolor($w, $h);
  73.  
  74.     if ($imgtype == IMAGETYPE_PNG or $imgtype == IMAGETYPE_GIF) {
  75.         $bg=imagecolorallocate($imgcopy, 255, 255, 255);
  76.         imagefill($imgcopy, 0, 0, $bg);
  77.     }
  78.  
  79.     if ($width > WALL_TAG_WIDTH) {
  80.         imagecopyresampled($imgcopy, $img, 0, 0, 0, 0, $w, $h, $width, $height);
  81.     }
  82.     else {
  83.         imagecopy($imgcopy, $img, 0, 0, 0, 0, $w, $h);
  84.     }
  85.  
  86.     $filecopy=wall_file_tag($wall_id, $fname);
  87.  
  88.     if (!@imagepng($imgcopy, $filecopy, PNG_QUALITY))  {
  89.         return false;
  90.     }
  91.  
  92.     clearstatcache(true, $filecopy);
  93.  
  94.     imagedestroy($imgcopy);
  95.  
  96.     return true;
  97. }

wall_create_tag creates the thumbnail of the file $fname in $wall_id from the image in the file $imgfile. If the image is larger than WALL_TAG_WIDTH, the thumbnail has a width of WALL_TAG_WIDTH pixels and a height preserving the aspect ratio of the image. If the image is a PNG or a GIF, the thumbnail has a white background. If the image is a JPG, the thumbnail is properly orientated.

  1. function wall_init_file($wall_id, $fname, $file, $mimetype) {
  2.     switch ($mimetype) {
  3.         case 'image/jpeg':
  4.         case 'image/png':
  5.         case 'image/gif':
  6.             return wall_tag_img($wall_id, $fname, $file);
  7.  
  8.         case 'application/pdf':
  9.             return wall_tag_pdf($wall_id, $fname, $file);
  10.  
  11.         default:
  12.             break;
  13.     }
  14.  
  15.     return false;
  16. }

wall_init_file creates the thumbnail of the file $fname in $wall_id from the file $file whose content is of the MIME type $mimetype. wall_init_file returns the result of the function wall_tag_img or the function wall_tag_pdf or false if $mimetype isn't supported.

NOTE: Managing more file types is fairly easy. To generate thumbnails for files edited with LibreOffice, look at unoconv - Universal Office Converter.

  1. require_once 'filemimetype.php';

Includes the code of the function file_mime_type.

function file_mime_type($file, $encoding=true) {
    $mime=false;

    if (function_exists('finfo_file')) {
        $finfo = finfo_open(FILEINFO_MIME);
        $mime = @finfo_file($finfo, $file);
        finfo_close($finfo);
    }
    else if (substr(PHP_OS, 0, 3) == 'WIN') {
        $mime = mime_content_type($file);
    }
    else {
        $file = escapeshellarg($file);
        $cmd = "file -iL $file";

        exec($cmd, $output, $r);

        if ($r == 0) {
            $mime = substr($output[0], strpos($output[0], ': ')+2);
        }
    }

    if (!$mime) {
        return false;
    }

    if ($encoding) {
        return $mime;
    }

    return substr($mime, 0, strpos($mime, '; '));
}

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 'validatefilename.php';

Includes the code of the function validate_filename.

function validate_filename($name) {
    return preg_match('/^[[:alnum:]]+[[:alnum:]_-]*(\.[[:alnum:]]+)?$/', $name);
}

validate_filename returns true if $name starts with an alphanumeric character followed by a series of alphanumeric characters, underscores or dashes terminated in option by a dot and at least one alphanumeric character. See validate_filename in the documentation of the library of iZend.

NOTE: Adapt this function to the syntax of the file names accepted by the website. IMPORTANT: Don't give access to files outside the user's folder. Don't allow in particular the character / (SLASH).

function validate_filename($name) {
    return preg_match('/^[0-9\pL][0-9\pL \._+-]*(\.[[:alnum:]]+)$/u', $name);
}

In this version, accented characters are accepted and a file name must start with a letter or a digit and end with an extension with in between in option, letters, digits, spaces, dots, underscores, plus signs and dashes.

  1. require_once 'models/wall.inc';

Includes the code of the function wall_file.

  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 => '1.png', 'nocache' => '1600709132578') for /wallfile/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.     $name=false;
  2.  
  3.     if (is_array($arglist)) {
  4.         if (isset($arglist[0])) {
  5.             $name=$arglist[0];
  6.         }
  7.     }
  8.  
  9.     if (!$name or !validate_filename($name)) {
  10.         goto badrequest;
  11.     }
  12.  
  13.     $file=wall_file($wall_id, $name);
  14.  
  15.     if (!file_exists($file)) {
  16.         goto notfound;
  17.     }

Checks that the URL contains a valid file name. Builds the full name of the requested file with the user's directory and the file name passed in the URL. EXAMPLE: The URL /wallfile/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, /1.png giving the array array(0 => '1.png'). If the user has the identifier 5f690ce2c930d, the full name of the file is /wall/5f690ce2c930d/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($file);
  2.     $filetype=file_mime_type($file);
  3.     if (!$filetype) {
  4.         $filetype = 'application/octet-stream';
  5.     }

Computes the size and the type of the requested file.

  1.     header("X-Sendfile: $file");
  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. function walltag($lang, $arglist=false) {

Defines the function walltag which is called by the URL /walltag of the website. The function walltag is nearly identical to the function wallfile.

  1. require_once 'validatefilename.php';
  2. require_once 'models/wall.inc';

Includes the code of the functions validate_filename, wall_file and wall_init_file, the constants WAL_FILE_MAX_SIZE and WAL_FILE_TYPES.

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

Defines the function wallupload 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.     $filetypes=WALL_FILE_TYPES;

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

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

Initializes the variables of the arguments of the POST.

  1.     if (isset($_POST['file_name'])) {
  2.         $name=$_POST['file_name'];
  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 (!$name or !validate_filename($name)) {
  2.         goto badrequest;
  3.     }
  4.  
  5.     if (!$type or !in_array($type, $filetypes)) {
  6.         goto badrequest;
  7.     }
  8.  
  9.     if (!is_numeric($offset) or $offset < 0) {
  10.         goto badrequest;
  11.     }
  12.  
  13.     if (!is_numeric($size) or $size < 0 or ($maxfilesize and $size > $maxfilesize)) {
  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.     $file=wall_file($wall_id, $name);

Gets 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.     if (!wall_init_file($wall_id, $name, $file, $type)) {
  2.         goto internalerror;
  3.     }
  4.  
  5.     return false;

When the entire file is saved, calls wall_init_file to create the thumbnail of the file.

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

Returns a plain header 400 Bad Request.

  1. internalerror:
  2.     @unlink($file);
  3.  
  4.     header('HTTP/1.1 500 Internal Error');

Returns a plain header 500 Internal Error.

  1. require_once 'validatefilename.php';
  2. require_once 'models/wall.inc';

Includes the code of the functions validate_filename, wall_file and wall_file_tag.

  1. function walldelete($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.     $name=false;

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

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

Extracts the name of the file from the POST.

  1.     if (!$name or !validate_filename($name)) {
  2.         goto badrequest;
  3.     }

Checks the name of the file.

  1.     $file=wall_file($wall_id, $name);
  2.  
  3.     if (!file_exists($file)) {
  4.         goto badrequest;
  5.     }

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.     $file=wall_file_tag($wall_id, $name);
  2.  
  3.     $r = @unlink($file);
  4.  
  5.     if (!$r) {
  6.         goto internalerror;
  7.     }
  8.  
  9.     return false;

Deletes the thumbnail of the file.

  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 $tag_url='/walltag'; ?>
  2. <?php $file_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*1000000; ?>
  7. <?php $chunksize=100000; ?>
  8. <?php $maxfiles=10; ?>

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. <span class="fileupload"><i class="fas fa-lg fa-fw fa-upload"></i></span>
  6. <span class="filedelete"><i class="fas fa-lg fa-fw fa-trash"></i></span>
  7. <span class="filedownload"><i class="fas fa-lg fa-fw fa-download"></i></span>
  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: <?php echo $maxfiles; ?>,
  4.         maxfilesize: <?php echo $maxfilesize; ?>,
  5.         filetypes: [<?php echo implode(',', array_map(function($s) { return "'$s'"; }, $filetypes)); ?>],
  6.         tagURL: '<?php echo $tag_url; ?>',
  7.         fileURL: '<?php echo $file_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].