<?php
/**
 * ZenTaoPHP的dao和sql类。
 * The dao and sql class file of ZenTaoPHP 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.
 */

helper::import(dirname(dirname(__FILE__)) . '/base/dao/dao.class.php');
/**
 * DAO类。
 * DAO, data access object.
 *
 * @package framework
 */
class dao extends baseDAO
{
    /**
     * 检查字段是否满足条件。
     * Check a filed is satisfied with the check rule.
     * 
     * @param  string $fieldName    the field to check
     * @param  string $funcName     the check rule
     * @param  string $condition     the condition
     * @access public
     * @return object the dao object self.
     */
    public function check($fieldName, $funcName, $condition = '')
    {
        /* 
         * 如果没数据中没有该字段，直接返回。
         * If no this field in the data, return.
         **/
        if(!isset($this->sqlobj->data->$fieldName)) return $this;

        /* 设置字段值。 */
        /* Set the field label and value. */
        global $lang, $config, $app;
        $table = strtolower(substr($this->table, strrpos($this->table, '_') + 1));
        $table = str_replace('`', '', $table);

        $fieldLabel = isset($lang->$table->$fieldName) ? $lang->$table->$fieldName : $fieldName;
        $value = isset($this->sqlobj->data->$fieldName) ? $this->sqlobj->data->$fieldName : null;

        /* 
         * 检查唯一性。
         * Check unique.
         **/
        if($funcName == 'unique')
        {
            $args = func_get_args();
            $sql  = "SELECT COUNT(*) AS count FROM $this->table WHERE `$fieldName` = " . $this->sqlobj->quote($value); 
            if($condition) $sql .= ' AND ' . $condition;
            try
            {
                $row = $this->dbh->query($sql)->fetch();
                if($row->count != 0) $this->logError($funcName, $fieldName, $fieldLabel, array($value));
            }
            catch (PDOException $e) 
            {
                $this->sqlError($e);
            }
        }
        else
        {
            /* 
             * 创建参数。
             * Create the params.
             **/
            $funcArgs = func_get_args();
            unset($funcArgs[0]);
            unset($funcArgs[1]);

            for($i = 0; $i < VALIDATER::MAX_ARGS; $i ++)
            {
                ${"arg$i"} = isset($funcArgs[$i + 2]) ? $funcArgs[$i + 2] : null;
            }

            $checkFunc = 'check' . $funcName;
            if(validater::$checkFunc($value, $arg0, $arg1, $arg2) === false)
            {
                $this->logError($funcName, $fieldName, $fieldLabel, $funcArgs);
            }
        }

        return $this;
    }

    /**
     * 获取查询记录条数。
     * The count method, call sql::select() and from().
     * use as $this->dao->select()->from(TABLE_BUG)->where()->count();
     *
     * @param  string $distinctField
     * @access public
     * @return void
     */
    public function count($distinctField = '')
    {
        /* 获得SELECT，FROM的位置，使用count(*)替换其字段。 */
        /* Get the SELECT, FROM position, thus get the fields, replace it by count(*). */
        $sql        = $this->get();
        $selectPOS  = strpos($sql, 'SELECT') + strlen('SELECT');
        $fromPOS    = strpos($sql, 'FROM');
        $fields     = substr($sql, $selectPOS, $fromPOS - $selectPOS);
        $countField = $distinctField ? 'distinct ' . $distinctField : '*';
        $sql        = str_replace($fields, " COUNT($countField) AS recTotal ", substr($sql, 0, $fromPOS)) . substr($sql, $fromPOS);

        /*
         * 去掉SQL语句中group, order和limit之后的部分。
         * Remove the part after group, order and limit.
         **/
        $subLength = strlen($sql);
        $groupPOS  = strripos($sql, 'group by');
        $orderPOS  = strripos($sql, 'order by');
        $limitPOS  = strripos($sql, 'limit');
        if($limitPOS) $subLength = $limitPOS;
        if($orderPOS) $subLength = $orderPOS;
        if($groupPOS) $subLength = $groupPOS;
        $sql = substr($sql, 0, $subLength);
        self::$querys[] = $sql;

        /*
         * 获取记录数。
         * Get the records count.
         **/
        try
        {
            $row = $this->dbh->query($sql)->fetch(PDO::FETCH_OBJ);
        }
        catch (PDOException $e)
        {
            $this->sqlError($e);
        }

        return is_object($row) ? $row->recTotal : 0;
    }

    /**
     * 设置需要更新或插入的数据。
     * Set the data to update or insert.
     * 
     * @param  object $data  the data object or array
     * @access public
     * @return object the dao object self.
     */
    public function data($data, $skipFields = '')
    {
        if(!is_object($data)) $data = (object)$data;

        /* Check current module is buildin workflow. */
        if(defined('RUN_MODE') && RUN_MODE == 'front' && isset($this->config->workflow->buildin->modules))
        {
            $currentModule = $this->app->rawModule;
            foreach($this->config->workflow->buildin->modules as $app => $appModules)
            {
                if(isset($appModules[$currentModule]))
                {
                    $currentMainTable = $appModules[$currentModule]['table'];
                    break;
                }
            }

            if(isset($currentMainTable))
            {
                if($currentMainTable == $this->table)
                {
                    $data = $this->processData($data);
                }
                else
                {
                    $workflowFields = array();
                    $stmt = $this->dbh->query("SELECT `field`,`type` FROM " . TABLE_WORKFLOWFIELD . " WHERE `module` = '{$currentModule}' AND `buildin` = '0'");
                    while($row = $stmt->fetch())
                    {
                        $workflowFields[$row->field] = $row->type;
                    }

                    $fields = $this->getFieldsType();
                    foreach($data as $field => $value)
                    {
                        if(!isset($fields[$field]) && isset($workflowFields[$field])) unset($data->$field);
                    }
                }
            }
        }

        $skipFields .= ',uid';
        return parent::data($data, $skipFields);
    }

    /**
     * Process workflow data
     *
     * @param  object  $data
     * @access public
     * @return object
     */
    public function processData($data)
    {
        global $app;

        if(!defined('RUN_MODE') || RUN_MODE != 'front' || !$app->isBiz) return $data;

        $module = $app->getModuleName();
        $method = $app->getMethodName();

        $action = $this->dbh->query("SELECT * FROM " . TABLE_WORKFLOWACTION . " WHERE `module` = '{$module}' AND `action` = '{$method}' AND `buildin` = '1' AND `extensionType` = 'extend'")->fetch(PDO::FETCH_OBJ);
        if(!$action) return $data;

        $fields = $this->dbh->query("SELECT t2.name,t2.rules,t2.control,t2.field,t2.type,t2.options,t1.layoutRules FROM " . TABLE_WORKFLOWLAYOUT . " AS t1 LEFT JOIN " . TABLE_WORKFLOWFIELD . " AS t2 ON t1.module = t2.module WHERE t1.module = '{$module}' AND t1.field = t2.field AND t1.action = '{$method}' AND t1.readonly = '0'")->fetchAll();
        if(!$fields) return $data;

        $this->app->loadModuleConfig('flow');
        $this->app->loadModuleConfig('workflowfield', 'flow');
        foreach($fields as $field)
        {
            if(isset($data->{$field->field}))
            {
                if($field->options && is_string($field->options) && strpos(',user,dept,', ",$field->options,") !== false)
                {
                    if(!is_array($data->{$field->field})) $data->{$field->field} = explode(',', $data->{$field->field});
                    foreach($data->{$field->field} as $key => $value)
                    {
                        $data->{$field->field}[$key] = $this->getParamRealValue($value);
                    }
                }

                /* If data value is array, implode by comma. */
                if(is_array($data->{$field->field}))
                {
                    $dataValue = array_values(array_unique(array_filter($data->{$field->field})));
                    asort($dataValue);
                    $data->{$field->field} = implode(',', $dataValue);
                }
            }
            else
            {
                if(strpos(',radio,checkbox,multi-select,', ",$field->control,") !== false) $data->{$field->field} = '';
            }
        }

        foreach($data as $field => $value)
        {
            if(in_array($value, $this->config->flow->variables)) $data->$field = $this->getParamRealValue($value);
        }

        return $data;
    }

    /**
     * Get param real value
     *
     * @param  string $param
     * @access public
     * @return string
     */
    public function getParamRealValue($param)
    {
        if(empty($this->deptManager))
        {
            $dept    = zget($this->app->user, 'dept', '');
            $manager = $this->dbh->query("SELECT moderators FROM " . TABLE_CATEGORY . " WHERE `type` = 'dept' AND `id` = '{$dept}'")->fetch(PDO::FETCH_OBJ);
            $this->deptManager = $manager ? trim($manager->moderators, ',') : '';
        }

        switch((string)$param)
        {
            case 'today'       : return date('Y-m-d');
            case 'now'         :
            case 'currentTime' : return date('Y-m-d H:i:s');
            case 'actor'       :
            case 'currentUser' : return $this->app->user->account;
            case 'currentDept' : return $this->app->user->dept ? $this->app->user->dept : $param;
            case 'deptManager' : return $this->deptManager ? $this->deptManager : $param;
            default            : return $param;
        }
    }

    /**
     * Check workFlow field rule.
     * 
     * @access public
     * @return object the dao object self.
     */
    public function checkFlow()
    {
        global $app;

        if(!defined('RUN_MODE') || RUN_MODE != 'front' || !$app->isBiz) return $this;

        $module = $app->getModuleName();
        $method = $app->getMethodName();

        $flowAction = $this->dbh->query("SELECT * FROM " . TABLE_WORKFLOWACTION . " WHERE `module` = '{$module}' AND `action` = '{$method}' AND `buildin` = '1' AND `extensionType` = 'extend'")->fetch(PDO::FETCH_OBJ);
        if(!$flowAction) return $this;

        $flowFields = $this->dbh->query("SELECT t2.name,t2.rules,t2.control,t2.field,t1.layoutRules FROM " . TABLE_WORKFLOWLAYOUT . " AS t1 LEFT JOIN " . TABLE_WORKFLOWFIELD . " AS t2 ON t1.module = t2.module AND t1.field = t2.field WHERE t1.module = '{$module}' AND t1.action = '{$method}' AND t1.readonly = '0'")->fetchAll();
        if(!$flowFields) return $this;

        $rules    = array();
        $rawRules = $this->dbh->query("SELECT * FROM " . TABLE_WORKFLOWRULE)->fetchAll();
        foreach($rawRules as $rule) $rules[$rule->id] = $rule;

        foreach($flowFields as $key => $field)
        {
            if(!$field)
            {
                unset($flowFields[$key]);
                continue;
            }

            $ruleIDs = explode(',', trim($field->rules, ',') . ',' . trim($field->layoutRules, ','));
            $ruleIDs = array_unique($ruleIDs);

            $fieldRules = array();
            foreach($ruleIDs as $ruleID)
            {
                if(!$ruleID || !isset($rules[$ruleID])) continue;

                $fieldRules[] = $rules[$ruleID];
            }

            $field->ruleData = $fieldRules;
            $field->rules    = join(',', $ruleIDs);
        }

        return $this->checkExtend($flowFields);
    }

    /**
     * 检查工作流扩展字段 
     * check workflow extend field 
     * 
     * @param  array $fields 
     * @access public
     * @return object the dao object self
     */
    public function checkExtend($fields)
    {
        global $lang;

        if(!$fields) return $this;
        foreach($fields as $field)
        {
            /* If the field don't have rule, don't check it. */
            if(empty($field->rules)) continue;

            if($field->control == 'file')
            {
                foreach($field->ruleData as $rule)
                {
                    if(empty($rule)) continue;
                    if($rule->type != 'system' || $rule->rule != 'notempty') continue;

                    $files = !empty($_FILES[$field->field]) ? $_FILES[$field->field] : '';
                    if(empty($files)) dao::$errors[$field->field][] = sprintf($lang->error->notempty, $field->name);
                    break;
                }
                continue;
            }

            /* Check rules of fields. */
            foreach($field->ruleData as $rule)
            {
                if(empty($rule)) continue;
                if(!isset($this->sqlobj->data->{$field->field})) continue;

                if($rule->type == 'system')
                {
                    if($rule->rule == 'unique')
                    {
                        if(empty($this->sqlobj->data->{$field->field})) continue;
                        $condition = isset($this->sqlobj->data->id) ? '`id` != ' . $this->sqlobj->data->id : '';
                        $this->check($field->field, $rule->rule, $condition);
                    }
                    else
                    {
                        if(is_numeric($this->sqlobj->data->{$field->field}) && $this->sqlobj->data->{$field->field} == 0 && $rule->rule == 'notempty' && !in_array($field->control, array('select', 'multi-select'))) continue;
                        $this->check($field->field, $rule->rule);
                    }
                }
                elseif($rule->type == 'regex')
                {
                    $this->check($field->field, 'reg', $rule->rule);
                }
            }
        }

        return $this;
    }
}

/**
 * SQL类。
 * The SQL class.
 *
 * @package framework
 */
class sql extends baseSQL
{
    public $checkGroupBy = true;

    /**
     * Set Check groupby.
     * 
     * @param  bool    $open 
     * @access public
     * @return void
     */
    public function setCheckGroupBy($open = true)
    {
        $this->checkGroupBy = $open;
    }

    /**
     * 创建ORDER BY部分。
     * Create the order by part.
     * 
     * @param  string $order 
     * @access public
     * @return object the sql object.
     */
    public function orderBy($order)
    {
        if(stripos($order, 'convert(') !== false)
        {
            if($this->inCondition and !$this->conditionIsTrue) return $this;

            $order = str_replace(array('|', '', '_'), ' ', $order);
            $this->sql .= ' ' . DAO::ORDERBY . " $order";
            return $this;
        }
        else
        {
            return parent::orderBy($order);
        }
    }

    /**
     * 创建GROUP BY部分。
     * Create the groupby part.
     *
     * @param  string $groupBy
     * @access public
     * @return object the sql object.
     */
    public function groupBy($groupBy)
    {
        if($this->inCondition and !$this->conditionIsTrue) return $this;
        if($this->checkGroupBy and !preg_match('/^\w+[a-zA-Z0-9_`.,\s]+$/', $groupBy))
        {
            $groupBy = htmlspecialchars($groupBy);
            die("Group is bad query, The group is $groupBy");
        }
        $this->sql .= ' ' . DAO::GROUPBY . " $groupBy";
        return $this;
    }
}
