<?php /**
 * 禅道API的entry类。
 * The entry class file of ZenTao API.
 *
 * @package framework
 *
 * The author disclaims copyright to this source code. In place of
 * a legal notice, here is a blessing:
 *
 *  May you do good and not evil.
 *  May you find forgiveness for yourself and forgive others.
 *  May you share freely, never taking more than you give.
 */
class entry extends baseEntry
{
    public function __construct()
    {
        parent::__construct();

        if($this->app->action == 'options') throw EndResponseException::create($this->send(204));

        if(!$this->loadModel('user')->isLogon()) throw EndResponseException::create($this->sendError(401, 'Unauthorized'));

        $this->dao = $this->loadModel('common')->dao;
    }
}

/**
 * 禅道API的baseEntry类。
 * The baseEntry class file of ZenTao API.
 *
 */
class baseEntry
{
    /**
     * 全局对象 $app。
     * The global $app object.
     *
     * @var object
     * @access public
     */
    public $app;

    /**
     * 语言项 $lang。
     * The global $app object.
     *
     * @var object
     * @access public
     */
    public $lang;

    /**
     * 提交的POST数据
     * The decoded request body.
     *
     * @var object
     * @access public
     */
    public $requestBody;

    /**
     * 构造方法。
     * The construct function.
     *
     * @access public
     * @return void
     */
    public function __construct()
    {
        global $app, $config, $lang;

        $this->app    = $app;
        $this->config = $config;
        $this->lang   = $lang;

        $this->parseRequestBody();
    }

    /**
     * 获取请求数据(POST PUT)
     * Get request data(POST or PUT)
     *
     * @param  string $key
     * @access public
     * @return mixed
     */
    public function request($key, $defaultValue = '')
    {
        if(isset($this->requestBody->$key)) return $this->requestBody->$key;
        return $defaultValue;
    }

    /**
     * 获取请求参数
     * Get request params.
     *
     * @param  string $key
     * @access public
     * @return mixed
     */
    public function param($key, $defaultValue = '')
    {
        if(isset($_GET[$key])) return $_GET[$key];
        return $defaultValue;
    }

    /**
     * 设置请求参数
     * Set request param.
     *
     * @param  string|array  $key   if is array, set params by its key-value pairs.
     * @access public
     * @return void
     */
    public function setParam($key, $value = null)
    {
        if(is_array($key))
        {
            foreach($key as $k => $v) $_GET[$k] = $v;
            return;
        }
        $_GET[$key] = $value;
    }

    /**
     * 解析请求数据
     * Parse body of request data.
     *
     * @access public
     * @return void
     */
    private function parseRequestBody()
    {
        $this->requestBody = new stdClass();

        if($this->app->action == 'post' or $this->app->action == 'put')
        {
            $requestBody = file_get_contents("php://input");
            if($requestBody) $this->requestBody = json_decode($requestBody);
        }
    }

    /**
     * HTTP状态码
     * HTTP status code
     *
     * @access public
     */
    public $statusCode = array(
        100 => "100 Continue",
        101 => "101 Switching Protocols",
        102 => "102 Processing",

        200 => "200 OK",
        201 => "201 Created",
        202 => "202 Accepted",
        203 => "203 Non-Authoritative Information",
        204 => "204 No Content",
        205 => "205 Reset Content",
        206 => "206 Partial Content",
        207 => "207 Multi-Status",

        300 => "300 Multiple Choices",
        301 => "301 Moved Permanently",
        302 => "302 Found",
        303 => "303 See Other",
        304 => "304 Not Modified",
        305 => "305 Use Proxy",
        307 => "307 Temporary Redirect",

        400 => "400 Bad Request",
        401 => "401 Authorization Required",
        402 => "402 Payment Required",
        403 => "403 Forbidden",
        404 => "404 Not Found",
        405 => "405 Method Not Allowed",
        406 => "406 Not Acceptable",
        407 => "407 Proxy Authentication Required",
        408 => "408 Request Time-out",
        409 => "409 Conflict",
        410 => "410 Gone",
        411 => "411 Length Required",
        412 => "412 Precondition Failed",
        413 => "413 Request Entity Too Large",
        414 => "414 Request-URI Too Large",
        415 => "415 Unsupported Media Type",
        416 => "416 Requested Range Not Satisfiable",
        417 => "417 Expectation Failed",
        422 => "422 Unprocessable Entity",
        423 => "423 Locked",
        424 => "424 Failed Dependency",
        426 => "426 Upgrade Required",
    );

    /**
     * 发送请求的响应数据
     * Send response data
     *
     * @param  int   $code
     * @access public
     * @return string
     */
    public function send($code, $data = '')
    {
        header("Access-Control-Allow-Origin: *");
        header("Access-Control-Allow-Credentials: true");
        header("Access-Control-Allow-Headers: Origin,X-Requested-With,Content-Type,Accept,Authorization,Token,Referer,User-Agent");
        header('Access-Control-Allow-Methods: GET,POST,PUT,DELETE,OPTIONS,PATCH');
        header("Content-type: application/json");
        header("HTTP/1.1 {$this->statusCode[$code]}");

        return !empty($data) ? json_encode($data, JSON_HEX_TAG) : '';
    }

    /**
     * 发送错误信息
     * Send error response
     *
     * @param  int    $code
     * @param  string|object $msg
     * @access public
     * @return string
     */
    public function sendError($code, $msg)
    {
        $response = new stdclass();
        $response->error = $msg;

        return $this->send($code, $response);
    }

    /**
     * 发送成功提示
     * Send success response
     *
     * @param  int    $code
     * @param  string $msg
     * @access public
     * @return string
     */
    public function sendSuccess($code, $msg)
    {
        $response = new stdclass();
        $response->message = $msg;

        return $this->send($code, $response);
    }

    /**
     * Send 400 response.
     *
     * @param  string message
     * @access public
     * @return string
     */
    public function send400($message = 'error')
    {
        return $this->sendError(400, $message);
    }

    /**
     * Send 404 response.
     *
     * @access public
     * @return string
     */
    public function send404()
    {
        return $this->sendError(404, '404 Not found');
    }

    /**
     * 加载禅道的控制器类
     * Load controller of zentaopms
     *
     * @param  string $moduleName
     * @param  string $methodName
     * @access public
     * @return object
     */
    public function loadController($moduleName, $methodName)
    {
        ob_start();

        global $app;
        if(!class_exists($moduleName) and !class_exists("my$moduleName"))
        {
            $app->setModuleName($moduleName);
            $app->setMethodName($methodName);
            $app->viewType = 'json';

            /* Check user permission. */
            $this->checkPriv();

            $app->setControlFile();
            $app->importControlFile();
        }

        /*
         * 设置control的类名。
         * Set the class name of the control.
         **/
        $className = class_exists("my$moduleName") ? "my$moduleName" : $moduleName;
        if(!class_exists($className)) $app->triggerError("the control $className not found", __FILE__, __LINE__, true);

        $controller = new $className();
        $controller->viewType = 'json';
        $app->control = $controller;

        return $controller;
    }

    /**
     * 加载指定模块的model文件。
     * Load the model file of one module.
     *
     * @param   string  $moduleName 模块名，如果为空，使用当前模块。The module name, if empty, use current module's name.
     * @param   string  $appName    The app name, if empty, use current app's name.
     * @access  public
     * @return  object|bool 如果没有model文件，返回false，否则返回model对象。If no model file, return false, else return the model object.
     */
    public function loadModel($moduleName = '', $appName = '')
    {
        if(empty($moduleName)) $moduleName = $this->app->moduleName;
        if(empty($appName))    $appName    = $this->app->appName;

        global $loadedModels;
        if(isset($loadedModels[$appName][$moduleName]))
        {
            $this->$moduleName = $loadedModels[$appName][$moduleName];
            $this->dao = $this->$moduleName->dao;
            return $this->$moduleName;
        }

        $modelFile = $this->app->setModelFile($moduleName, $appName);

        /**
         * 如果没有model文件，尝试加载config配置信息。
         * If no model file, try load config.
         */
        if(!helper::import($modelFile))
        {
            $this->app->loadModuleConfig($moduleName, $appName);
            $this->app->loadLang($moduleName, $appName);
            $this->dao = new dao($this->dao);
            return false;
        }

        /**
         * 如果没有扩展文件，model类名是$moduleName + 'model'，如果有扩展，还需要增加ext前缀。
         * If no extension file, model class name is $moduleName + 'model', else with 'ext' as the prefix.
         */
        $modelClass = class_exists('ext' . $appName . $moduleName. 'model') ? 'ext' . $appName . $moduleName . 'model' : $appName . $moduleName . 'model';
        if(!class_exists($modelClass))
        {
            $modelClass = class_exists('ext' . $moduleName. 'model') ? 'ext' . $moduleName . 'model' : $moduleName . 'model';
            if(!class_exists($modelClass)) $this->app->triggerError(" The model $modelClass not found", __FILE__, __LINE__, true);
        }

        /**
         * 初始化model对象，在control对象中可以通过$this->$moduleName来引用。同时将dao对象赋为control对象的成员变量，方便引用。
         * Init the model object thus you can try $this->$moduleName to access it. Also assign the $dao object as a member of control object.
         */
        $loadedModels[$appName][$moduleName] = new $modelClass($appName);
        $this->$moduleName = $loadedModels[$appName][$moduleName];
        $this->dao = $this->$moduleName->dao;

        return $this->$moduleName;
    }

    /**
     * 获取控制器执行返回的数据，在output缓存中.
     * Get controller data from output.
     *
     * @access  public
     * @return  object.
     */
    public function getData()
    {
        $output = helper::removeUTF8Bom(ob_get_clean());
        $output = json_decode((string) $output);
        if(isset($output->data) && !is_object($output->data)) $output->data   = json_decode((string) $output->data);
        if(!isset($output->status) && isset($output->result)) $output->status = $output->result;
        if(isset($output->load->alert))
        {
            $output->code    = 400;
            $output->status  = 'fail';
            $output->message = $output->load->alert;
            unset($output->load);
        }

        return $output;
    }

    /**
     * 添加$_POST全局变量.
     * Add data to $_POST.
     *
     * @param  string $key
     * @access public
     * @return void
     */
    public function setPost($key, $value)
    {
        $_POST[$key] = $value;
    }

    /**
     * 批量添加$_POST全局变量.
     * Batch set data to $_POST.
     *
     * @param  string $fields
     * @access public
     * @return void
     */
    public function batchSetPost($fields, $object = '')
    {
        $fields = explode(',', $fields);
        foreach($fields as $field)
        {
            /*
             * If the field exists in request body, use it.
             * Otherwise set default value from $object.
             */
            if(isset($this->requestBody->$field))
            {
                $value = $this->requestBody->$field;
            }
            else
            {
                if(!$object or !isset($object->$field)) continue;
                $value = $object->$field;
            }

            $this->setPost($field, $value);
        }
    }

    /**
     * 确保字段不能为空.
     * Make sure the fields is not empty.
     *
     * @param  string $fields
     * @access public
     * @return void
     */
    public function requireFields($fields)
    {
        $fields = explode(',', $fields);
        foreach($fields as $field)
        {
            if(!isset($_POST[$field]))
            {
                $module = $this->app->moduleName;
                $name   = $this->app->lang->$module->$field ?? $field;
                throw EndResponseException::create($this->sendError(400, sprintf($this->app->lang->error->notempty, $name)));
            }
        }
    }

    /**
     * 检查是否在后台启用了代号.
     * Check whether config->setCode are used in product,project,execution.
     *
     * @access public
     * @return bool
     */
    public function checkCodeUsed()
    {
        return isset($this->config->setCode) ? $this->config->setCode : 0;
    }

    /**
     * 格式化数据的字段类型.
     * Format fields of response data.
     *
     * @param  object|array $data
     * @param  string $fields
     * @access public
     * @return object|array
     */
    public function format($data, $fields)
    {
        if(is_array($data))
        {
            foreach($data as $object) $this->formatFields($object, $fields);
        }
        else
        {
            $this->formatFields($data, $fields);
        }

        return $data;
    }

    /**
     * 格式化对象的字段类型.
     * Format fields of object.
     *
     * @param  object $object
     * @param  string $fields
     * @access public
     * @return object
     */
    private function formatFields(&$object, $fields)
    {
        $fields = explode(',', $fields);

        foreach($fields as $field)
        {
            $field   = explode(':', $field);
            $key     = $field[0];
            $type    = $field[1];
            $isArray = false;

            if(!isset($object->$key)) continue;

            $pos = strpos($type, ']');
            if($pos !== false)
            {
                $isArray = true;
                $type    = substr($type, $pos + 1);
            }
            else if(str_contains($type, 'array'))
            {
                $isArray = true;
                $type    = 'object';
            }

            /* Format value. */
            if(!$isArray)
            {
                $object->$key = $this->cast(trim((string) $object->$key, ','), $type);
                continue;
            }

            /* Format array. */
            $value = array();
            if(is_array($object->$key) or is_object($object->$key))
            {
                foreach($object->$key as $v) $value[] = $this->cast($v, $type);
            }
            else
            {
                $vs = implode(',', $object->$key);
                foreach($vs as $v)
                {
                    if($v === '') continue;
                    $value[] = $this->cast($v, $type);
                }
            }
            $object->$key = $value;
        }
    }

    /**
     * Filter fields.
     *
     * @param  object $object
     * @param  string $allowable
     * @access public
     * @return object
     */
    public function filterFields($object, $allowable = '')
    {
        if(empty($allowable)) return $object;
        if(is_string($allowable)) $allowable = explode(',', $allowable);

        $filtered = new stdclass();
        foreach($allowable as $field)
        {
            $field = trim((string) $field);
            if(empty($field)) continue;
            if(!isset($object->$field)) continue;
            $filtered->$field = $object->$field;
        }

        return $filtered;
    }

    /**
     * Format user.
     *
     * @param  string       $account
     * @param  array|object $users
     * @access public
     * @return array
     */
    public function formatUser($account, $users)
    {
        $user = array();
        $user['account']  = $account;
        $user['realname'] = zget($users, $account);

        return $user;
    }

    /**
     * 类型转换.
     * Typecasting.
     *
     * @param  mixed  $vaule
     * @param  string $type
     * @access public
     * @return mixed
     */
    private function cast($value, $type)
    {
        switch($type)
        {
            case 'time':
                $timeFormat = $this->param('timeFormat', 'utc');
                if($timeFormat == 'utc')
                {
                    if(!$value or $value == '0000-00-00 00:00:00') return null;
                    return gmdate("Y-m-d\TH:i:s\Z", strtotime((string) $value));
                }
                return $value;
            case 'date':
                if(!$value or $value == '0000-00-00') return null;
                return $value;
            case 'bool':
                return !empty($value);
            case 'int':
                return (int) $value;
            case 'idList':
                $values = explode(',', (string) $value);
                if(empty($values)) return array();

                $idList = array();
                foreach($values as $val)
                {
                    if($val !== '') $idList[] = (int) $val;
                }
                return $idList;
            case 'stringList':
                $values = explode(',', (string) $value);
                if(empty($values)) return array();

                $stringList = array();
                foreach($values as $val)
                {
                    if($val !== '') $stringList[] = $val;
                }
                return $stringList;
            case 'array':
                $array = array();
                if(!empty($value)) foreach($value as $v) $array[] = $v;
                return $array;
            case 'user':
                if(empty($value)) return null;
                if(empty($this->users)) $this->users = $this->dao->select('id,account,avatar,realname')->from(TABLE_USER)->fetchAll('account');
                return zget($this->users, $value, null);
            case 'userList':
                $values = explode(',', (string) $value);
                if(empty($values)) return array();

                $userList = array();
                foreach($values as $val)
                {
                    $val = $this->cast($val, 'user');
                    if($val) $userList[] = $val;
                }
                return $userList;
            case 'decodeHtml':
                return htmlspecialchars_decode((string) $value);
            default:
                return $value;
        }
    }

    /**
     * 获取其他方法的执行结果。
     * Fetch result of other method.
     *
     * @param  string $entry
     * @param  string $method
     * @param  array  $params
     * @access public
     * @return mixed
     */
    public function fetch($entry, $method, $params = array())
    {
        include($this->app->appRoot . "api/{$this->app->version}/entries/" . strtolower($entry) . ".php");

        $entryName = $entry . 'Entry';
        $entry     = new $entryName();
        return call_user_func_array(array($entry, $method), $params);
    }

    /**
     * Check the user has permission to access this method, if not, return 403.
     *
     * @access public
     * @return void|string
     */
    public function checkPriv()
    {
        $module = $this->app->getModuleName();
        $method = $this->app->getMethodName();
        if($module and $method and !$this->loadModel('common')->isOpenMethod($module, $method) and !commonModel::hasPriv($module, $method))
        {
            die($this->send(403, array('error' => 'Access not allowed')));
        }
    }

    /**
     * Reset open app.
     *
     * @param  string  $tab
     * @access public
     * @return void
     */
    public function resetOpenApp($tab)
    {
        $_COOKIE['tab'] = $tab;
        $this->app->tab = $tab;
        $this->app->session->tab = $tab;
    }
}
