2
\$\begingroup\$

I'm learning PHP and 'web' development (having an offline programming background).

I plan to setup a simple storage server API that I'll adress with a javascript application.

The following code :

  • does not consider authentification
  • does provide file upload/deletion, folder listing

I got some inspiration from the Dropbox REST API for error values and action names.

<?php
/*
the 'API' provided is :

* ?action=list_folder&path=</some/path>
* ?action=permanently_delete&path=</some/path>
* ?action=upload&path=</some/path>

The provided </some/path> values are joined to 'ROOT_FOLDER' constant.

Can't create folders or download files.

Needs PHP 5.5+ for `finally` clauses.
*/

error_reporting(0);

define('ROOT_FOLDER', 'files/' );                // IMPORTANT(nico) '/' at the end

//============================================================================

header('Access-Control-Allow-Origin: *');
header('Cache-Control: no-cache, must-revalidate');

$result = null;

try {

  $action = _filesystem_checked_GET('action');

  switch ($action) {

  case 'list_folder':
    $path = _filesystem_checked_GET('path');
    $result = _filesystem_list_folder($path);
    break;

  case 'permanently_delete':
    $path = _filesystem_checked_GET('path');
    $result = _filesystem_permanently_delete($path);
    break;

  case 'upload':
    $path = _filesystem_checked_GET('path');
    if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
      throw new _filesystem_HTTPException('Bad Request', 400, 'POST expected');
    }
    $result = _filesystem_upload($path);
    break;

  default:
    throw new _filesystem_HTTPException('Bad Request', 400, 'Unknown action');
  }
}
catch (_filesystem_HTTPException $e) {
  $e->setErrorHeader();
  $result = ['error'=> ['.tag'=> 'other'], 'error_summary'=> $e->content];
}
catch (Exception $e) {
  header("HTTP/1.1 500 Internal error");
  $result = ['error'=> ['.tag'=> 'other'], 'error_summary'=> $e->getMessage()];
}

header('Content-type:application/json;charset=utf-8');
echo json_encode($result);

//============================================================================

function _filesystem_upload($path) {

  $abspath = _filesystem_abspath($path);

  $temppath = tempnam(dirname($abspath), 'temp');
  if (!$temppath) {
    throw new _filesystem_HTTPException("`tempnam` failed", 500);
  }

  try {

    $dst = fopen($temppath, "wb");
    $src = fopen("php://input", "r"); // POST raw data

    try {

      if (!$src || !$dst) {
        throw new _filesystem_HTTPException("Could not create file", 500);
      }
      // copy streams
      while ($data = fread($src, 1024))
      {
        if ($data === FALSE) {
          throw new _filesystem_HTTPException("Could not read source data", 500);     // FIXME(nico) endpoint error ?
        }
        $written = fwrite($dst, $data, 1024);
        if ($written != strlen($written)) {
          throw new _filesystem_HTTPException("Could not write to file", 500);
        }
      }

    }
    finally {
      fclose($src);
      fclose($dst);
    }

    // finalize destination file
    if (!rename($temppath, $abspath)) {
      throw new _filesystem_HTTPException("Could not finalize file", 500);
    }

  }
  finally {

    if (file_exists($temppath)) {
      unlink($temppath);
    }
  }

  $name = basename($abspath);
  $result = _filesystem_metadata($name, $abspath);
  return $result;
}

//----------------------------------------------------------------------------

function _filesystem_permanently_delete($path) {

  $abspath = _filesystem_abspath($path);
  if (unlink($abspath)) {         // FIXME(nico) can trigger a warning, check file_exists first, and improve error reporting

    return null;
  }
  else {

    return ['error'=> ['.tag'=> 'other'], 'error_summary'=> "Could not unlink file"];
  }
}

//----------------------------------------------------------------------------

function _filesystem_list_folder($path) {

  $abspath = _filesystem_abspath($path);
  $names = array_diff(scandir($abspath), array('..', '.'));
  $result = [];
  foreach ($names as $name) {
    $path = _filesystem_path_join($abspath, $name);

    $size = filesize($path);
    $server_modified = date(DATE_ISO8601, filemtime($path));
    $tag = null;
    if (is_dir($path)) { $tag='folder'; }
    elseif (is_file($path)) { $tag='file'; }

    $metadata = _filesystem_metadata($name, $path);
    if ($metadata['.tag'] != null) {
      // NOTE(nico) do not include info on 'undefined' filesystem items
      $result[] = $metadata;
    }
  }
  return [ 'entries'=> $result, 'has_more'=> false ];
}

//============================================================================

function _filesystem_metadata($name, $path) {

  $size = filesize($path);
  $server_modified = date(DATE_ISO8601, filemtime($path));
  $tag = null;
  if (is_dir($path)) { $tag='folder'; }
  elseif (is_file($path)) { $tag='file'; }

  return [ ".tag"=>$tag, 'name'=>$name, 'server_modified'=>$server_modified, 'size'=>$size ];
}

function _filesystem_abspath($path) {

  return ROOT_FOLDER . $path;       // FIXME(nico) security check, path should be absolute starting with '/'
}

function _filesystem_path_join($root, $path) {

  return $root . $path;            // FIXME(nico) check '/' & stuff
}

function _filesystem_checked_GET($varname) {
  if (!isset($_GET[$varname])) {
    throw new _filesystem_HTTPException('Bad Request', 400, 'Missing parameter `' . $varname . '`');
  }
  return $_GET[$varname];
}

class _filesystem_HTTPException extends Exception {
  public $content = null;
  public function __construct($message = null, $code = 501, $content = null) {
    parent::__construct($message, $code);
    $this->content = $content;
  }
  public function setErrorHeader() {
    header("HTTP/1.1 " . $this->code . ' ' . $this->getMessage());
  }
}

//============================================================================

?>
\$\endgroup\$
1
  • \$\begingroup\$ FWIW, this code fails badly on Windows when filenames have accents. \$\endgroup\$ – rotoglup Feb 1 '18 at 14:47
2
\$\begingroup\$

There are a lot of points to review here :

  1. Choose a coding style, and stick to it (strings with ' or ", spaces, braces, casing...)
  2. Use files, to put things apart and avoid headaches
  3. Use classes, to make code clearer and reusable
  4. Do not expose everything at the root of your access point
  5. Do not rely on comments to make it work (i.e. "'/' at the end" can be enforced by code)
  6. Name your classes, functions and variables with explicit and clean and readable names.
  7. Reuse code
  8. Shorten your code with early returns (guards) and short functions

As a whole review, I do not believe doing what you want to do to be a good idea, you may be implementing a security breach on your server. You should consider using a database for storage or learn real fast about security.

That being said, the code below may not fully work (because I didn't test it) but should give you a good insight. A good practice from now on would be to code and run some unit tests.

web/index.php

<?php
/*
the 'API' provided is :
* ?action=list_folder&path=</some/path>
* ?action=permanently_delete&path=</some/path>
* ?action=upload&path=</some/path>

The provided </some/path> values are joined to 'ROOT_FOLDER' class constant.

Can't create folders or download files.

Needs PHP 5.5+ for `finally` clauses.
 */

error_reporting(0);
require_once('../lib/StorageAPI/Controler.php');
require_once('../lib/StorageAPI/Exception.php');
StorageAPIControler::run('files/'); // IMPORTANT(nico) '/' at the end

lib/StorageAPI/Controler.php

<?php
class StorageAPIControler {
    public static $root_folder;

    public static function run($root_folder) {
        self::$root_folder = $root_folder;

        try {
            $action = self::readGET('action');
            $method = 'api_'.$action;
            if(!method_exists(__CLASS__,$method))
                $method = 'api_default';
            $result = self::$method();
        }
        catch (StorageAPIException $storage_api_exception) {
            $result = $storage_api_exception->getResult();
        }
        catch (Exception $exception) {
            $storage_api_exception = new StorageAPIException('Internal Server Error - '.$exception->getMessage(), 500);
            $result = $storage_api_exception->getResult();
        }

        self::output($result);
    }
    public static function output($result) {
        if(!isset($result['http_status_code']))
            $result['http_status_code'] = 200;
        if(!isset($result['http_status_message']))
            $result['http_status_message'] = 'OK';

        header('Access-Control-Allow-Origin: *');
        header('Cache-Control: no-cache, must-revalidate');
        header('Content-Type:application/json; charset=utf-8');
        echo json_encode($result);
    }

    public static function api_list_folder() {
        $path = self::readGET('path');
        $entries = self::list_folder($path);
        return [ 'entries'=> $result, 'has_more'=> false ];
    }
    public static function list_folder($path) {
        $abspath = self::abspath($path);
        $names = array_diff(scandir($abspath), array('..', '.'));
        $result = [];
        foreach ($names as $name) {
            $path = self::path_join($abspath, $name);

            $metadata = self::metadata($name, $path);
            if ($metadata['.tag'] != null) {
                // NOTE(nico) do not include info on 'undefined' filesystem items
                $result[] = $metadata;
            }
        }
        return $entries;
    }

    public static function api_permanently_delete() {
        $path = self::readGET('path');
        $status = self::permanently_delete($path);
        return []; // TODO
    }
    public static function permanently_delete($path) {
        $abspath = self::abspath($path);
        if (unlink($abspath)) {         // FIXME(nico) can trigger a warning, check file_exists first, and improve error reporting
            return true;
        }
        throw new StorageAPIException('Could not unlink file');
    }

    public static function api_upload() {
        $path = self::readGET('path');
        $result = self::upload($path);
    }
    public static function upload($path) {
        if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
            throw new StorageAPIException('Bad Request - POST expected', 400);
        }

        $abspath = self::abspath($path);

        $temppath = tempnam(dirname($abspath), 'temp');
        if (!$temppath) {
            throw new StorageAPIException('"tempnam" failed', 500);
        }

        try {
            $dst = fopen($temppath, 'wb');
            $src = fopen('php://input', 'r'); // POST raw data

            try {
                if (!$src || !$dst) {
                    throw new StorageAPIException('Could not create file', 500);
                }
                // copy streams
                while ($data = fread($src, 1024))
                {
                    if ($data === false) {
                        throw new StorageAPIException('Could not read source data', 500);     // FIXME(nico) endpoint error ?
                    }
                    $written = fwrite($dst, $data, 1024);
                    if ($written != strlen($written)) {
                        throw new StorageAPIException('Could not write to file', 500);
                    }
                }

            }
            finally {
                fclose($src);
                fclose($dst);
            }

            // finalize destination file
            if (!rename($temppath, $abspath)) {
                throw new StorageAPIException('Could not finalize file', 500);
            }
        }
        finally {
            if (file_exists($temppath)) {
                unlink($temppath);
            }
        }

        $name = basename($abspath);
        $result = self::metadata($name, $abspath);
        return $result;
    }

    public static function api_default() {
        throw new StorageAPIException('Bad Request - Unknown action', 400);
    }

    public static function readGET($varname) {
        if (!isset($_GET[$varname])) {
            throw new StorageAPIException('Bad Request - Missing parameter "'.$varname.'"', 400);
        }
        return $_GET[$varname];
    }
    public static function metadata($name, $path) {
        $size = filesize($path);
        $server_modified = date(DATE_ISO8601, filemtime($path));
        $tag = null;
        if (is_dir($path)) {
            $tag = 'folder';
        }
        elseif (is_file($path)) {
            $tag = 'file';
        }
        return [ '.tag'=>$tag, 'name'=>$name, 'server_modified'=>$server_modified, 'size'=>$size];
    }
    public static function abspath($path) {
        return self::$ROOT_FOLDER . $path;       // FIXME(nico) security check, path should be absolute starting with '/'
    }
    public static function path_join($root, $path) {
        return $root . $path;            // FIXME(nico) check '/' & stuff
    }
}

lib/StorageAPI/Exception.php

<?php
class StorageAPIException extends Exception {
    public function __construct($message = null, $code = 501) {
        parent::__construct($message, $code);
    }
    public function getResult() {
        return ['error'=>['.tag'=>'other'], 'http_status_code'=>$this->getCode(), 'http_status_message'=>$this->getMessage()];
    }
}
\$\endgroup\$
3
  • \$\begingroup\$ Thanks for your feedback, I'd rather not introduce a class for pure functions, I see that PHP supports namespace to avoid name collisions, what do you think of it ? Maybe not used often ? \$\endgroup\$ – rotoglup Jan 25 '18 at 12:19
  • \$\begingroup\$ namespace is a valid pattern, but I don't like the PHP implementation new \MyNamespace\MyClass(), so I don't use it. I believe that since in your case you don't use external librairies you don't need to use namespaces. Anyway classes are the way to go to have a clean code, that will be easily readable and maintenable. I kept it simple to not lose you too much towards your original code, but you can add more classes : controler, helper, filesystem, ... I.E if you want to give the choice of the output format (JSON/XML/...) you should have renderer, rendererJson, renderXml... \$\endgroup\$ – Geompse Jan 25 '18 at 15:50
  • \$\begingroup\$ Most projects starts as one simple script file and ends as a large application. \$\endgroup\$ – Geompse Jan 25 '18 at 15:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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