<?php

/*
 * File: ratatoeskr/sys/pluginpackage.php
 * Handle plugin packages easily.
 *
 * License:
 * This file is part of Ratatöskr.
 * Unlike the other parts of Ratatöskr, *this* file ist *not* licensed under the
 * MIT / X11 License, but under the WTFPL, to make it even easier to use this
 * file in other projects.
 * See "ratatoeskr/licenses/wtfpl" for more information.
 */

/*
 * Function: dir2array
 * Pack a directory into an array.
 *
 * Parameters:
 *  $dir - The directory to pack.
 *
 * Returns:
 *  Associative array. Keys are filenames, values are either the file's content as a string or another array, if it's a directory.
 */
function dir2array($dir)
{
    $rv = [];
    foreach (scandir($dir) as $fn) {
        if (($fn == ".") or ($fn == "..")) {
            continue;
        }
        $fn_new = $dir . "/" . $fn;
        if (is_dir($fn_new)) {
            $rv[$fn] = dir2array($fn_new);
        } elseif (is_file($fn_new)) {
            $rv[$fn] = file_get_contents($fn_new);
        }
    }
    return $rv;
}

/*
 * Function: array2dir
 * Unpack an array into a directory.
 *
 * Parameters:
 *  $a   - Array to unpack.
 *  $dir - Directory to unpack to.
 */
function array2dir($a, $dir)
{
    if (!is_dir($dir)) {
        mkdir($dir);
    }

    foreach ($a as $k => $v) {
        $k = "$dir/$k";
        if (is_array($v)) {
            array2dir($v, $k);
        } else {
            file_put_contents($k, $v);
        }
    }
}

function validate_url($u)
{
    return preg_match("/^http[s]{0,1}:\\/\\/.*$/", $u) != 0;
}
function validate_arraydir($a)
{
    if (!is_array($a)) {
        return false;
    }
    foreach ($a as $k=>$v) {
        if (!is_string($k)) {
            return false;
        }
        if (is_array($v) and (!validate_arraydir($v))) {
            return false;
        } elseif (!is_string($v)) {
            return false;
        }
    }
    return true;
}

/*
 * Class: InvalidPackage
 * An Exception that <PluginPackage>'s function can throw, if the package is invalid.
 */
class InvalidPackage extends Exception
{
}

/*
 * Class: PluginPackage
 * A plugin package representation.
 */
class PluginPackage
{
    public static $magic = "R7RPLGPACKV001";

    /*
     * Variables: Mandatory values
     *
     * $code              - The plugin code
     * $classname         - The name of the plugins main class
     * $name              - Name of the plugin (must be at least one character, allowed chars: a-z A-Z 0-9 - _)
     * $author            - The author of the plugin (preferably in the format: Name<mail@address>)
     * $versiontext       - A text to describe the current version, something like "1.1 Beta"
     * $versioncount      - A number for this version, should be increased with every release
     * $api               - The used API version
     * $short_description - A short description.
     */
    public $code              = null;
    public $classname         = null;
    public $name              = null;
    public $author            = null;
    public $versiontext       = null;
    public $versioncount      = null;
    public $api               = null;
    public $short_description = null;

    /*
     * Variables: Optional values
     *
     * $updatepath - A URL that points to a update information resource (serialize'd array("current-version" => VERSIONCOUNT, "dl-path" => DOWNLOAD PATH); will get overwritten/set by the default repository software.
     * $web        - A URL to the webpage for the plugin. If left empty, the default repository software will set this to the description page of your plugin.
     * $license    - The license text of your plugin.
     * $help       - A help / manual (formatted in HTML) for your plugin.
     * $custompub  - <dir2array> 'd directory that contains custom public(i.e. can later be accessed from the web) data.
     * $custompriv - <dir2array> 'd directory that contains custom private data.
     * $tpls       - <dir2array> 'd directory containing custom STE templates.
     */
    public $updatepath = null;
    public $web        = null;
    public $license    = null;
    public $help       = null;
    public $custompub  = null;
    public $custompriv = null;
    public $tpls       = null;

    /*
     * Function: validate
     * Validate, if the variables are set correctly.
     * Will throw an <InvalidPackage> exception if invalid.
     */
    public function validate()
    {
        if (!is_string($this->code)) {
            throw new InvalidPackage("Invalid code value.");
        }
        if (!is_string($this->classname)) {
            throw new InvalidPackage("Invalid classname value.");
        }
        if (preg_match("/^[a-zA-Z0-9_\\-]+$/", $this->name) == 0) {
            throw new InvalidPackage("Invalid name value (must be at least 1 character, accepted chars: a-z A-Z 0-9 - _).");
        }
        if (!is_string($this->author)) {
            throw new InvalidPackage("Invalid author value.");
        }
        if (!is_string($this->versiontext)) {
            throw new InvalidPackage("Invalid versiontext value.");
        }
        if (!is_numeric($this->versioncount)) {
            throw new InvalidPackage("Invalid versioncount value. Must be a number.");
        }
        if (!is_numeric($this->api)) {
            throw new InvalidPackage("Invalid api value. Must be a number.");
        }
        if (!is_string($this->short_description)) {
            throw new InvalidPackage("Invalid short_description value.");
        }

        if ((!empty($this->updatepath)) and (!validate_url($this->updatepath))) {
            throw new InvalidPackage("Invalid updatepath value. Must be an URL. " .$this->updatepath);
        }
        if ((!empty($this->web)) and (!validate_url($this->web))) {
            throw new InvalidPackage("Invalid web value. Must be an URL.");
        }
        if (($this->license !== null) and (!is_string($this->license))) {
            throw new InvalidPackage("Invalid license value.");
        }
        if (($this->help !== null) and (!is_string($this->help))) {
            throw new InvalidPackage("Invalid help value.");
        }
        if (($this->custompub !== null) and (!validate_arraydir($this->custompub))) {
            throw new InvalidPackage("Invalid custompub value.");
        }
        if (($this->custompriv !== null) and (!validate_arraydir($this->custompriv))) {
            throw new InvalidPackage("Invalid custompriv value.");
        }
        if (($this->tpls !== null) and (!validate_arraydir($this->tpls))) {
            throw new InvalidPackage("Invalid tpls value.");
        }
        return true;
    }

    /*
     * Function: load
     * Load a plugin package from binary data.
     *
     * Parameters:
     *  $plugin_raw - The raw package to load.
     *
     * Returns:
     *  The <PluginPackage> object.
     *
     * Throws:
     *  <InvalidPackage> if package is invalid.
     */
    public static function load($plugin_raw)
    {
        /* Read and compare magic number */
        $magic = substr($plugin_raw, 0, strlen(self::$magic));
        if ($magic != self::$magic) {
            throw new InvalidPackage("Wrong magic number");
        }

        /* Read sha1sum and uncompress serialized plugin, then compare the hash */
        $sha1sum   = substr($plugin_raw, strlen(self::$magic), 20);
        $pluginser = gzuncompress(substr($plugin_raw, strlen(self::$magic) + 20));
        if (sha1($pluginser, true) != $sha1sum) {
            throw new InvalidPackage("Wrong SHA1 hash");
        }

        $plugin = @unserialize($pluginser);
        if (!($plugin instanceof self)) {
            throw new InvalidPackage("Not the correct class or not unserializeable.");
        }

        $plugin->validate();

        return $plugin;
    }

    /*
     * Function: save
     * Save the plugin.
     *
     * Returns:
     *  A binary plugin package.
     *
     * Throws:
     *  <InvalidPackage> if package is invalid.
     */
    public function save()
    {
        $this->validate();
        $ser = serialize($this);
        return self::$magic . sha1($ser, true) . gzcompress($ser, 9);
    }

    /*
     * Function: extract_meta
     * Get just the metadata of this package.
     *
     * Returns:
     *  A <PluginPackageMeta> object.
     */
    public function extract_meta()
    {
        $meta = new PluginPackageMeta();

        $meta->name              = $this->name;
        $meta->author            = $this->author;
        $meta->versiontext       = $this->versiontext;
        $meta->versioncount      = $this->versioncount;
        $meta->api               = $this->api;
        $meta->short_description = $this->short_description;
        $meta->updatepath        = $this->updatepath;
        $meta->web               = $this->web;
        $meta->license           = $this->license;

        return $meta;
    }
}

/*
 * Class: PluginPackageMeta
 * Only the metadata of a <PluginPackage>.
 */
class PluginPackageMeta
{
    /*
     * Variables: Mandatory values
     *
     * $name              - Name of the plugin (must be at least one character, allowed chars: a-z A-Z 0-9 - _)
     * $author            - The author of the plugin (preferably in the format: Name<mail@address>)
     * $versiontext       - A text to describe the current version, something like "1.1 Beta"
     * $versioncount      - A number for this version, should be increased with every release
     * $api               - The used API version
     * $short_description - A short description.
     */
    public $name              = null;
    public $author            = null;
    public $versiontext       = null;
    public $versioncount      = null;
    public $api               = null;
    public $short_description = null;

    /*
     * Variables: Optional values
     *
     * $updatepath - A URL that points to a update information resource (serialize'd array("current-version" => VERSIONCOUNT, "dl-path" => DOWNLOAD PATH); will get overwritten/set by the default repository software.
     * $web        - A URL to the webpage for the plugin. If left empty, the default repository software will set this to the description page of your plugin.
     * $license    - The license text of your plugin.
     */
    public $updatepath = null;
    public $web        = null;
    public $license    = null;
}