diff options
| -rw-r--r-- | app/class/globals.php | 81 | ||||
| -rw-r--r-- | app/class/image.php | 70 | ||||
| -rw-r--r-- | app/class/object.class.php | 88 | ||||
| -rw-r--r-- | app/df.php | 102 | ||||
| -rw-r--r-- | app/dynmic/bgs/.gitignore | 3 | ||||
| -rw-r--r-- | app/dynmic/heads/.gitignore | 3 | ||||
| -rw-r--r-- | schema.sql | 3 | 
7 files changed, 348 insertions, 2 deletions
| diff --git a/app/class/globals.php b/app/class/globals.php index 8bfa029..615efa6 100644 --- a/app/class/globals.php +++ b/app/class/globals.php @@ -20,6 +20,19 @@  define("__VERSION__", "v0.0");  /* + * These global variables are arrays of strings logged by Scrott business + * logic to report errors, warnings, or informational responses to the + * user in cases where an exception doesn't need to be thrown. + */ +define("ERROR", "errorlist"); +define("WARNING", "warninglist"); +define("NOTICE", "noticelist"); + +$_SCROTT[ERROR] = array(); +$_SCROTT[WARNING] = array(); +$_SCROTT[NOTICE] = array(); + +/*   * Get the application root path.  This is an absolute path on the server.   */  function ar() : string @@ -63,4 +76,72 @@ function require_https() : void          redirect("https://" . $_SERVER['SERVER_NAME'] . ap());  } +/* + * Check for errors, warnings, or notices + */ +function isError(string $level) : bool +{ +    global $_SCROTT; +    return count($_SCROTT[$level]) > 0; +} + +/* + * Log an error, warning, or notice + */ +function logError(string $level, string $error) : void +{ +    global $_SCROTT; +    $_SCROTT[$level][] = $error; +} + +/* + * Get an array of all errors, warnings, or notices + */ +function getErrors(string $level) : array +{ +    global $_SCROTT; +    return $_SCROTT[$level]; +} + +/* + * Save an uploaded file and impose some constraints on supplied + * data.  Caller can optionally pass some strings by reference to + * be given the supplied file's original name and mime-type. + * Maxsize is in bytes.  If this function returns false, the + * appropriate error will be logged. + */ +function saveFile(array $file, string $path, int $maxsize, ?array $allowedMime = NULL, +    ?string &$origName = NULL, ?string &$origMime = NULL) : bool +{ +    if ($file['error'] > 0) +    { +        if ($file['error'] != UPLOAD_ERR_NO_FILE) +            logError(ERROR, "An unknown error occurred"); + +        return false; +    } + +    if ($file['size'] > $maxsize) +    { +        logError(ERROR, "File must be no larger than " . $maxsize . " bytes"); +        return false; +    } + +    if (is_array($allowedMime) && array_search($file['type'], $allowedMime) === false) +    { +        logError(ERROR, "File type is not supported"); +        return false; +    } + +    if (!move_uploaded_file($file['tmp_name'], $path)) +    { +        logError(ERROR, "Error saving uploaded file"); +        return false; +    } + +    $origName = $file['name']; +    $origMime = $file['type']; +    return true; +} +  ?> diff --git a/app/class/image.php b/app/class/image.php new file mode 100644 index 0000000..6b73cae --- /dev/null +++ b/app/class/image.php @@ -0,0 +1,70 @@ +<?php + +/* + * SCROTT IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR + * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * For more information, please refer to UNLICENSE + */ + +/* + * This file defines custom image manipulation routines for Scrott.  All + * are available in the global namespace. + */ + +/* + * Mappings from image MIME types to PHP open/write functions + */ +$_IMG_OPEN_FUNCS['image/jpeg'] = "imagecreatefromjpeg"; +$_IMG_OPEN_FUNCS['image/jpg']  = "imagecreatefromjpeg"; +$_IMG_OPEN_FUNCS['image/png']  = "imagecreatefrompng"; + +$_IMG_WRITE_FUNCS['image/jpeg'] = "imagejpeg"; +$_IMG_WRITE_FUNCS['image/jpg']  = "imagejpeg"; +$_IMG_WRITE_FUNCS['image/png']  = "imagepng"; + +/* + * Open the given image and crop it, such that the result is a square + * image whose side length is equal to the smaller of the original + * dimensions and whose area is centered on the area of the original + * image.  The resulting image is written to the same path as provided, + * overwriting the image. + */ +function imageSquareCrop(string $uri) : bool +{ +    global $_IMG_OPEN_FUNCS; +    global $_IMG_WRITE_FUNCS; + +    $mime = mime_content_type($uri); +    $img = $_IMG_OPEN_FUNCS[$mime]($uri); + +    if ($img === false) +        return false; + +    $wid = imagesx($img); +    $hei = imagesy($img); + +    $rec = array(); +    $rec['width']  = min($wid, $hei); +    $rec['height'] = $rec['width']; +    $rec['y'] = ($hei / 2) - ($rec['height'] / 2); +    $rec['x'] = ($wid / 2) - ($rec['width'] / 2); + +    $cropped = imagecrop($img, $rec); +    imagedestroy($img); + +    if ($cropped === false) +        return false; + +    $ret = $_IMG_WRITE_FUNCS[$mime]($cropped, $uri); +    imagedestroy($cropped); + +    return $ret; +} + +?> diff --git a/app/class/object.class.php b/app/class/object.class.php index 14ab891..7c80b5b 100644 --- a/app/class/object.class.php +++ b/app/class/object.class.php @@ -13,6 +13,7 @@   */  require_once "class/table.class.php"; +require_once "class/image.php";  /*   * This is a generic database object.  This is a supertype of all Scrott @@ -21,6 +22,17 @@ require_once "class/table.class.php";  class object extends table  {      /* +     * Constants used for uploading images +     */ +    public const HEAD_MAXSIZE = 1048576; // 1Mb +    public const BG_MAXSIZE = 1048576; // 1Mb +    public const IMAGE_MIME = array( +        "image/jpeg", +        "image/jpg", +        "image/png", +    ); + +    /*       * Constructor       */      public function __construct(?string $guid = NULL) @@ -149,6 +161,82 @@ class object extends table          database::query($query);          return true;      } + +    /* +     * Get the URL to the head image resource for this object +     */ +    public function getHeadImg() : string +    { +        return ar() . "/df.php?d=heads&f=" . $this->guid; +    } + +    /* +     * Set the head image for this object, overwriting any existing +     * image.  $image should be an uploaded file to PHP, still +     * unhandled. +     */ +    public function setHeadImg(array $image) : bool +    { +        $path = "dynmic/heads/" . $this->guid; + +        if (!saveFile($image, $path, self::HEAD_MAXSIZE, self::IMAGE_MIME)) +            return false; + +        if (!imageSquareCrop($path)) +        { +            $this->rmHeadImg(); +            return false; +        } + +        return true; +    } + +    /* +     * Remove the head image for this object.  This deletes the image +     * on disk. +     */ +    public function rmHeadImg() : bool +    { +        if (!is_file("dynmic/heads/" . $this->guid)) +            return true; + +        return unlink("dynmic/heads/" . $this->guid); +    } + +    /* +     * Get the URL to the background image resource for this +     * object.  If no image is set, NULL is returned. +     */ +    public function getBgImg() : ?string +    { +        if (!is_file("dynmic/bgs/" . $this->guid)) +            return NULL; + +        return ar() . "/df.php?d=bgs&f=" . $this->guid; +    } + +    /* +     * Set the background image for this object, overwriting any +     * existing image.  $image should be an uploaded file to PHP, +     * still unhandled. +     */ +    public function setBgImg(array $image) : bool +    { +        $path = "dynmic/bgs/" . $this->guid; +        return saveFile($image, $path, self::BG_MAXSIZE, self::IMAGE_MIME); +    } + +    /* +     * Remove the background image for this object.  This deletes +     * the image on disk. +     */ +    public function rmBgImg() : bool +    { +        if (!is_file("dynmic/bgs/" . $this->guid)) +            return true; + +        return unlink("dynmic/bgs/" . $this->guid); +    }  }  ?> diff --git a/app/df.php b/app/df.php new file mode 100644 index 0000000..a425d57 --- /dev/null +++ b/app/df.php @@ -0,0 +1,102 @@ +<?php + +/* + * SCROTT IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR + * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * For more information, please refer to UNLICENSE + */ + +require_once "class/user.class.php"; + +/* + * This file is a proxy script for fetching resources from the /dynmic + * directory.  This script enforces access-control on HTTP objects such + * as images and flat files which are supplied by users. + * + * Example request: + * https://yourdomain.com/scrott/df.php?d=heads&f=a4bf903a + * + * In cases of error or lack of access privilege, this script will + * produce no output and fail silently. + */ + +/* + * Serve the resource at the given URI in response to the current + * request.  When finished, this function will exit PHP and terminate + * this script. + */ +function serveResource(string $uri) : void +{ +    $f = fopen($uri, "rb"); + +    if (!$f) +        exit; + +    header("Content-type: " . mime_content_type($uri)); +    header("Content-length: " . filesize($uri)); +    fpassthru($f); +    fclose($f); + +    exit; +} + +/* + * Check the current user's permissions.  User must have access + * rights for the file's object, unless that object is a user + * object and $allowHeadUser is set to true. + */ +function checkPermissions(string $guid, bool $allowHeadUser = false) : bool +{ +    if (!($user = user::getCurrent())) +        return false; + +    $obj = new object($guid); + +    if ($allowHeadUser && $obj->objtype == "user") +        return true; + +    return $user->canAccess($obj); +} + +/* + * Respond to users' requests for dynamic files + */ +function main(string $dir, string $guid) : void +{ +    try +    { +        if (basename($guid) != $guid || $guid == "") +            return; + +        if (!checkPermissions($guid, $dir == "heads")) +            return; + +        switch ($dir) +        { +            case "heads": +                if (file_exists("dynmic/heads/" . $guid)) +                    serveResource("dynmic/heads/" . $guid); +                else +                    serveResource("static/img/null.jpg"); +                break; + +            case "bgs": +                serveResource("dynmic/bgs/" . $guid); +                break; +        } +    } +    catch (Exception $e) +    { +        /* fail silently */ +    } +} + +main($_REQUEST['d'], $_REQUEST['f']); + +?> diff --git a/app/dynmic/bgs/.gitignore b/app/dynmic/bgs/.gitignore new file mode 100644 index 0000000..e38384e --- /dev/null +++ b/app/dynmic/bgs/.gitignore @@ -0,0 +1,3 @@ +# Track empty directory +* +!.gitignore diff --git a/app/dynmic/heads/.gitignore b/app/dynmic/heads/.gitignore new file mode 100644 index 0000000..e38384e --- /dev/null +++ b/app/dynmic/heads/.gitignore @@ -0,0 +1,3 @@ +# Track empty directory +* +!.gitignore @@ -196,8 +196,7 @@ CREATE TABLE mesgs (      guid        varchar(8)          NOT NULL,      author      varchar(8)          NOT NULL,      mesg        text                NOT NULL    DEFAULT '', -    attachName  varchar(64)         NOT NULL    DEFAULT '', -    attachMime  varchar(64)         NOT NULL    DEFAULT '', +    attachment  varchar(64)         NOT NULL,      PRIMARY KEY (guid)  ); | 
