<?php
/**
 * The model file of doc module of ZenTaoPMS.
 *
 * @copyright   Copyright 2009-2023 禅道软件（青岛）有限公司(ZenTao Software (Qingdao) Co., Ltd. www.cnezsoft.com)
 * @license     ZPL(http://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
 * @author      Chunsheng Wang <chunsheng@cnezsoft.com>
 * @package     doc
 * @version     $Id: model.php 881 2010-06-22 06:50:32Z chencongzhi520 $
 * @link        https://www.zentao.net
 */

class docModel extends model
{

    /**
     * @var actionModel
     */
    public $action;

    // api doc type
    const DOC_TYPE_API  = 'api';
    const DOC_TYPE_REST = 'restapi';

    /**
     * Adjust the action clickable.
     *
     * @param  object $story
     * @param  string $action
     * @access public
     * @return bool
     * @param object $doc
     */
    public static function isClickable($doc, $action)
    {
        global $app;
        $action = strtolower($action);

        if($action == 'movedoc') return $doc->addedBy == $app->user->account;
        return true;
    }

    /**
     * 通过ID获取文档库信息。
     * Get library by id.
     *
     * @param  int         $libID
     * @access public
     * @return object|false
     */
    public function getLibByID($libID)
    {
        if(common::isTutorialMode()) return $this->loadModel('tutorial')->getDocLib();

        return $this->dao->findByID($libID)->from(TABLE_DOCLIB)->fetch();
    }

    /**
     * 获取api文档库。
     * Get api libraries.
     *
     * @param  int    $appendLib
     * @param  string $objectType nolink|product|project
     * @param  int    $objectID
     * @return array
     */
    public function getApiLibs($appendLib = 0, $objectType = '', $objectID = 0)
    {
        $libs = $this->dao->select('*')->from(TABLE_DOCLIB)
            ->where('deleted')->eq(0)
            ->andWhere('type')->eq('api')
            ->beginIF(!empty($objectType) && $objectID > 0 && $objectType != 'nolink')->andWhere($objectType)->eq($objectID)->fi()
            ->beginIF($objectType == 'nolink')
            ->andWhere('product')->eq(0)
            ->andWhere('project')->eq(0)
            ->andWhere('execution')->eq(0)
            ->fi()
            ->beginIF(!empty($appendLib))->orWhere('id')->eq($appendLib)->fi()
            ->orderBy('order_asc, id_asc')
            ->fetchAll('id');

        return array_filter($libs, array($this, 'checkPrivLib'));
    }

    /**
     * 获取文档库。
     * Get libraries.
     *
     * @param  string $type        all|includeDeleted|hasApi|product|project|execution|custom|mine
     * @param  string     $extra       withObject|notdoc
     * @param  int|string $appendLibs
     * @param  int        $objectID
     * @param  string     $excludeType product|project|execution|custom|mine
     * @access public
     * @return array
     */
    public function getLibs($type = '', $extra = '', $appendLibs = '', $objectID = 0, $excludeType = '')
    {
        $products   = $this->loadModel('product')->getPairs();
        $projects   = $this->loadModel('project')->getPairsByProgram(0, 'all', false, 'order_asc');
        $executions = $this->loadModel('execution')->getPairs(0, 'all', 'multiple,leaf');
        $waterfalls = array();
        if(empty($objectID) && $type == 'execution')
        {
            $waterfalls = $this->dao->select('t1.id,t2.name')->from(TABLE_EXECUTION)->alias('t1')
                ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project=t2.id')
                ->where('t1.type')->eq('stage')
                ->andWhere('t2.type')->eq('project')
                ->andWhere('t2.model')->eq('waterfall')
                ->andWhere('t1.deleted')->eq('0')
                ->andWhere('t2.deleted')->eq('0')
                ->fetchPairs('id', 'name');
        }

        $libPairs = $this->getLibPairs($type, $extra, $objectID, $excludeType, $products, $projects, $executions, $waterfalls);
        if(!empty($appendLibs))
        {
            $stmt = $this->dao->select('*')->from(TABLE_DOCLIB)->where('id')->in($appendLibs)->orderBy("`order`_asc, id_asc")->query();
            while($lib = $stmt->fetch())
            {
                if($lib->type == 'custom' && $lib->parent == 0) continue;
                if(!isset($libPairs[$lib->id]) && $this->checkPrivLib($lib, $extra)) $libPairs[$lib->id] = $lib->name;
            }
        }

        return $libPairs;
    }

    /**
     * 获取文档库键值对。
     * Get doc liberary pairs.
     *
     * @param  string $type        all|includeDeleted|hasApi|product|project|execution|custom|mine
     * @param  string $extra       withObject|notdoc
     * @param  int    $objectID
     * @param  string $excludeType product|project|execution|custom|mine
     * @param  array  $products
     * @param  array  $projects
     * @param  array  $executions
     * @param  array  $waterfalls
     * @access public
     * @return array
     */
    public function getLibPairs($type, $extra = '', $objectID = 0, $excludeType = '', $products = array(), $projects = array(), $executions = array(), $waterfalls = array())
    {
        if(in_array($type, array('all', 'includeDeleted', 'hasApi')))
        {
            $stmt = $this->dao->select('*')->from(TABLE_DOCLIB)
                ->where('vision')->eq($this->config->vision)
                ->beginIF($type == 'all')->andWhere('deleted')->eq(0)->fi()
                ->beginIF($type != 'hasApi')->andWhere('type')->ne('api')->fi()
                ->beginIF($excludeType)->andWhere('type')->notin($excludeType)->fi()
                ->orderBy('id_asc')
                ->query();
        }
        else
        {
            $stmt = $this->dao->select('*')->from(TABLE_DOCLIB)
                ->where('deleted')->eq(0)
                ->beginIF($type != 'product' or $this->config->vision != 'or')->andWhere('vision')->eq($this->config->vision)->fi()
                ->beginIF($type)->andWhere('type')->eq($type)->fi()
                ->beginIF(!$type)->andWhere('type')->ne('api')->fi()
                ->beginIF($objectID && strpos(',product,project,execution,', ",$type,") !== false)->andWhere($type)->eq($objectID)->fi()
                ->beginIF($type == 'custom' && !$objectID)->andWhere('parent')->ne(0)->fi()
                ->beginIF($type == 'custom' && $objectID)->andWhere('parent')->eq($objectID)->fi()
                ->orderBy('id_asc')
                ->query();
        }

        $libPairs = array();
        while($lib = $stmt->fetch())
        {
            if($lib->product != 0 && !isset($products[$lib->product])) continue;
            if($lib->execution != 0 && !isset($executions[$lib->execution])) continue;
            if($lib->project != 0 && !isset($projects[$lib->project]) && $lib->type == 'project') continue;
            if($this->checkPrivLib($lib, $extra))
            {
                if(strpos($extra, 'withObject') !== false)
                {
                    if($lib->product != 0) $lib->name = zget($products, $lib->product, '') . ' / ' . $lib->name;
                    if($lib->execution != 0)
                    {
                        $lib->name = zget($executions, $lib->execution, '') . ' / ' . $lib->name;
                        $lib->name = ltrim($lib->name, '/');
                        if(!empty($waterfalls[$lib->execution])) $lib->name = $waterfalls[$lib->execution] . ' / ' . $lib->name;
                        $lib->name = trim($lib->name, '/');
                    }
                    if($lib->project != 0)     $lib->name = zget($projects, $lib->project, '') . ' / ' . $lib->name;
                    if($lib->type == 'mine')   $lib->name = $this->lang->doc->person . ' / ' . $lib->name;
                    if($lib->type == 'custom')
                    {
                        if($lib->parent == 0) continue;
                        $lib->name = $this->lang->doc->team . ' / ' . $lib->name;
                    }
                }
                $libPairs[$lib->id] = $lib->name;
            }
        }
        return $libPairs;
    }

    /**
     * 获取有权限访问的文档库。
     * Get grant libs by doc.
     *
     * @access public
     * @return array
     */
    public function getPrivLibsByDoc()
    {
        static $libs;
        if($libs === null)
        {
            $libs = array();
            $stmt = $this->dao->select('lib,`groups`,users')->from(TABLE_DOC)->where('acl')->ne('open')->andWhere("(`groups` != '' or users != '')")->query();

            $account    = ",{$this->app->user->account},";
            $userGroups = $this->app->user->groups;
            while($lib = $stmt->fetch())
            {
                if(strpos(",$lib->users,", $account) !== false)
                {
                    $libs[$lib->lib] = $lib->lib;
                }
                elseif($lib->groups)
                {
                    foreach($userGroups as $groupID)
                    {
                        if(strpos(",$lib->groups,", ",$groupID,") !== false)
                        {
                            $libs[$lib->lib] = $lib->lib;
                            break;
                        }
                    }
                }
            }
        }
        return $libs;
    }

    /**
     * 创建一个文档库。
     * Create a lib.
     *
     * @param  object   $lib
     * @param  string   $libType wiki|api
     * @param  string   $type    api|project|product|execution|custom|mine
     * @access public
     * @return int|bool
     */
    public function createLib($lib, $type = '', $libType = '')
    {
        if(!empty($_POST['newSpace']))
        {
            if(!$lib->spaceName)
            {
                dao::$errors['spaceName'] = sprintf($this->lang->error->notempty, $this->lang->doc->space);
                return false;
            }

            $space = clone $lib;
            $space->name   = $space->spaceName;
            $space->parent = 0;
            $space->acl    = 'open';

            $spaceID = $this->docTao->doInsertLib($space);
            if(dao::isError()) return false;

            $lib->parent = $spaceID;
            $this->loadModel('action')->create('docspace', $spaceID, 'created');
        }
        elseif($lib->parent <= 0 && $type == 'custom')
        {
            dao::$errors['parent'] = sprintf($this->lang->error->notempty, $this->lang->doc->space);
            return false;
        }

        if($lib->execution) $lib->type = 'execution';
        if($lib->type == 'execution' && $lib->execution && !$lib->project)
        {
            $execution    = $this->loadModel('execution')->getByID((int)$lib->execution);
            $lib->project = $execution->project;
        }
        if($libType == 'api')
        {
            $lib->type = 'api';
            $this->checkApiLibName($lib, $type);
        }

        $libID = $this->docTao->doInsertLib($lib, $this->config->doc->createlib->requiredFields);
        if(dao::isError()) return false;
        return $libID;
    }

    /**
     * 创建一个API接口库。
     * Creat a api doc library.
     *
     * @param  object   $formData
     * @access public
     * @return bool|int
     */
    public function createApiLib($formData = null)
    {
        $this->app->loadLang('api');

        /* Replace doc library name. */
        $this->lang->doclib->name    = $this->lang->doclib->apiLibName;
        $this->lang->doclib->baseUrl = $this->lang->api->baseUrl;
        $this->lang->doclib->project = $this->lang->api->project;
        $this->lang->doclib->product = $this->lang->api->product;

        if($formData->libType == 'product') $this->config->api->createlib->requiredFields .= ',product';
        if($formData->libType == 'project') $this->config->api->createlib->requiredFields .= ',project';

        $this->checkApiLibName($formData, $formData->libType);
        if(dao::isError()) return false;

        $formData->type = static::DOC_TYPE_API;
        $this->dao->insert(TABLE_DOCLIB)->data($formData, 'libType')->autoCheck()
            ->batchCheck($this->config->api->createlib->requiredFields, 'notempty')
            ->exec();

        if(dao::isError()) return false;

        $libID = $this->dao->lastInsertID();
        $this->loadModel('action')->create('doclib', $libID, 'created');

        return $libID;
    }

    /**
     * 更新一个API接口库。
     * Update an api lib.
     *
     * @param  int         $id
     * @param  object      $formData
     * @access public
     * @return array|false
     */
    public function updateApiLib($id, $formData = null)
    {
        $oldLib = $this->getLibByID($id);

        /* Replace doc library name. */
        $this->app->loadLang('api');
        $this->lang->doclib->name    = $this->lang->doclib->apiLibName;
        $this->lang->doclib->baseUrl = $this->lang->api->baseUrl;
        $this->lang->doclib->project = $this->lang->api->project;
        $this->lang->doclib->product = $this->lang->api->product;

        $this->checkApiLibName($formData, $formData->type, $id);
        if(dao::isError()) return false;

        $formData->type = static::DOC_TYPE_API;
        $this->dao->update(TABLE_DOCLIB)->data($formData, 'type')->autoCheck()
            ->batchCheck($this->config->api->editlib->requiredFields, 'notempty')
            ->where('id')->eq($id)
            ->exec();
        if(dao::isError()) return false;

        $changes = common::createChanges($oldLib, $formData);
        if($changes)
        {
            $actionID = $this->loadModel('action')->create('docLib', $id, 'Edited');
            $this->action->logHistory($actionID, $changes);
        }
        return $changes;
    }

    /**
     * 更新一个文档库。
     * Update a doc lib.
     *
     * @param  int        $libID
     * @param  object     $lib
     * @access public
     * @return array|bool
     */
    public function updateLib($libID, $lib)
    {
        $oldLib = $this->getLibByID($libID);
        if($oldLib->type == 'project')
        {
            $libCreatedBy = $this->dao->select('*')->from(TABLE_ACTION)->where('objectType')->eq('doclib')->andWhere('objectID')->eq($libID)->andWhere('action')->eq('created')->fetch('actor');

            $openedBy = $this->dao->findById($oldLib->project)->from(TABLE_PROJECT)->fetch('openedBy');
            if($lib->acl == 'private' && $lib->acl == 'custom') $lib->users .= ',' . $libCreatedBy ? $libCreatedBy : $openedBy;
        }
        if($oldLib->acl != $lib->acl && $lib->acl == 'open') $lib->users = '';

        if($oldLib->type == 'api')
        {
            $type = 'nolink';
            if(!empty($oldLib->product)) $type = 'product';
            if(!empty($oldLib->project)) $type = 'project';
            $lib->product = $oldLib->product;
            $lib->project = $oldLib->project;
            $this->checkApiLibName($lib, $type, $libID);
        }

        $this->dao->update(TABLE_DOCLIB)->data($lib, 'space')->autoCheck()
            ->batchCheck($this->config->doc->editlib->requiredFields, 'notempty')
            ->where('id')->eq($libID)
            ->exec();

        $this->moveLib($libID, $lib);

        if(dao::isError()) return false;
        return common::createChanges($oldLib, $lib);
    }

    /**
     * 通过类型获取文档列表数据。
     * Get doc list data by browse type.
     *
     * @param  string $browseType all|bySearch|openedbyme|editedbyme|byediteddate|collectedbyme
     * @param  int    $queryID
     * @param  int    $moduleID
     * @param  string $sort
     * @param  object $pager
     * @access public
     * @return array
     */
    public function getDocsByBrowseType($browseType, $queryID, $moduleID, $sort, $pager = null)
    {
        if($browseType == 'all') return $this->getDocs(0, 0, $browseType, $sort, $pager);

        $allLibs          = $this->getLibs('all');
        $allLibIDList     = array_keys($allLibs);
        $hasPrivDocIdList = $this->getPrivDocs($allLibIDList, $moduleID);
        if($browseType == 'bySearch')
        {
            $docs = $this->getMyDocListBySearch($queryID, $hasPrivDocIdList, $allLibIDList, $sort, $pager);
        }
        elseif($browseType == "openedbyme")
        {
            $docs = $this->docTao->getOpenedDocs($hasPrivDocIdList, $sort, $pager);
        }
        elseif($browseType == 'editedbyme')
        {
            $docs = $this->docTao->getEditedDocs($sort, $pager);
        }
        elseif($browseType == 'byediteddate')
        {
            $docs = $this->docTao->getOrderedDocsByEditedDate($hasPrivDocIdList, $allLibIDList, $pager);
        }
        elseif($browseType == "collectedbyme")
        {
            $docs = $this->docTao->getCollectedDocs($hasPrivDocIdList, $sort, $pager);
        }

        if(empty($docs)) return array();
        if(!in_array($browseType, array('bySearch', 'openedbyme', 'editedbyme'))) return $this->processCollector($docs);

        $objects = array();
        list($objects['project'], $objects['execution'], $objects['product']) = $this->getObjectsByDoc(array_keys($docs));
        foreach($docs as $docID => $doc)
        {
            $doc->objectID   = zget($doc, $doc->objectType, 0);
            $doc->objectName = '';
            if(isset($objects[$doc->objectType]))
            {
                $doc->objectName = $objects[$doc->objectType][$doc->objectID];
            }
            else
            {
                if($doc->objectType == 'mine')   $doc->objectName = $this->lang->doc->person;
                if($doc->objectType == 'custom') $doc->objectName = $this->lang->doc->team;
            }
        }

        return $this->processCollector($docs);
    }

    /**
     * 获取搜索后的文档列表。
     * Get doc list by search.
     *
     * @param  int    $queryID
     * @param  array  $hasPrivDocIdList
     * @param  array  $allLibIDList
     * @param  string $sort
     * @param  object $pager
     * @access public
     * @return array
     */
    public function getMyDocListBySearch($queryID, $hasPrivDocIdList, $allLibIDList, $sort, $pager = null)
    {
        if($queryID)
        {
            $query = $this->loadModel('search')->getQuery($queryID);
            if($query)
            {
                $this->session->set('contributeDocQuery', $query->sql);
                $this->session->set('contributeDocForm', $query->form);
            }
            else
            {
                $this->session->set('contributeDocQuery', ' 1 = 1');
            }
        }
        else
        {
            if($this->session->contributeDocQuery == false) $this->session->set('contributeDocQuery', ' 1 = 1');
        }

        $query     = $this->getDocQuery($this->session->contributeDocQuery);
        $query     = preg_replace('/(`\w+`)/', 't1.$1', $query);
        $docIDList = $this->dao->select('objectID')->from(TABLE_ACTION)
            ->where('objectType')->eq('doc')
            ->andWhere('objectID')->in($hasPrivDocIdList)
            ->andWhere('actor')->eq($this->app->user->account)
            ->andWhere('action')->eq('edited')
            ->fetchAll('objectID');

        return $this->dao->select('t1.*, t2.name as libName, t2.type as objectType')->from(TABLE_DOC)->alias('t1')
            ->leftJoin(TABLE_DOCLIB)->alias('t2')->on("t1.lib=t2.id")
            ->where('t1.deleted')->eq(0)
            ->andWhere($query)
            ->andWhere('t1.lib')->in($allLibIDList)
            ->andWhere('t1.templateType')->eq('')
            ->andWhere('t1.vision')->in($this->config->vision)
            ->beginIF($this->config->doc->notArticleType)->andWhere('t1.type')->notIN($this->config->doc->notArticleType)->fi()
            ->andWhere('t1.addedBy', 1)->eq($this->app->user->account)
            ->orWhere('t1.id')->in(array_keys($docIDList))
            ->markRight(1)
            ->orderBy($sort)
            ->page($pager)
            ->fetchAll('id');
    }

    /**
     * 替换查询语句中的all。
     * Replace all in query.
     *
     * @param  string $query
     * @access public
     * @return string
     */
    public function getDocQuery($query)
    {
        $allLib = "`lib` = 'all'";
        if(strpos($query, $allLib) !== false)
        {
            $libPairs  = $this->loadModel('doc')->getLibs('all');
            $libIdList = empty($libPairs) ? 0 : implode(',', array_keys($libPairs));
            $query     = str_replace($allLib, '1', $query);
            $query     = $query . ' AND `lib` ' . helper::dbIN($libIdList);
        }

        $allProject = "`project` = 'all'";
        if(strpos($query, $allProject) !== false)
        {
            $projectPairs  = $this->loadModel('project')->getPairsByProgram();
            $projectIdList = empty($projectPairs) ? 0 : implode(',', array_keys($projectPairs));
            $query         = str_replace($allProject, '1', $query);
            $query         = $query . ' AND `project` in (' . $projectIdList . ')';
        }

        $allProduct = "`product` = 'all'";
        if(strpos($query, $allProduct) !== false)
        {
            $productPairs  = $this->loadModel('product')->getPairs();
            $productIdList = empty($productPairs) ? 0 : implode(',', array_keys($productPairs));
            $query         = str_replace($allProduct, '1', $query);
            $query         = $query . ' AND `product` ' . helper::dbIN($productIdList);
        }

        $allExecution = "`execution` = 'all'";
        if(strpos($query, $allExecution) !== false)
        {
            $executionPairs  = $this->loadModel('execution')->getPairs();
            $executionIdList = empty($executionPairs) ? 0 : implode(',', array_keys($executionPairs));
            $query           = str_replace($allExecution, '1', $query);
            $query           = $query . ' AND `execution` ' . helper::dbIN($executionIdList);
        }

        return $query;
    }

    /**
     * 通过文档ID获取文档所属产品、项目、执行。
     * Get projects, executions and products by docIdList.
     *
     * @param  array $docIdList
     * @access public
     * @return array
     */
    public function getObjectsByDoc($docIdList = array())
    {
        $projects = $executions = $products = array();
        if(empty($docIdList)) return array($projects, $executions, $products);

        $projects = $this->dao->select('t1.id, t1.name')->from(TABLE_PROJECT)->alias('t1')
            ->leftJoin(TABLE_DOC)->alias('t2')->on('t1.id=t2.project')
            ->where('t2.id')->in($docIdList)
            ->andWhere('t2.execution')->eq(0)
            ->fetchPairs();

        $executions = $this->dao->select('t1.id, t1.name')->from(TABLE_EXECUTION)->alias('t1')
            ->leftJoin(TABLE_DOC)->alias('t2')->on('t1.id=t2.execution')
            ->where('t2.id')->in($docIdList)
            ->fetchPairs();

        $products = $this->dao->select('t1.id, t1.name')->from(TABLE_PRODUCT)->alias('t1')
            ->leftJoin(TABLE_DOC)->alias('t2')->on('t1.id=t2.product')
            ->where('t2.id')->in($docIdList)
            ->fetchPairs();

        return array($projects, $executions, $products);
    }

    /**
     * 获取当前文档库下的文档列表数据。
     * Get doc list by libID.
     *
     * @param  int    $libID
     * @param  int    $moduleID
     * @param  string $browseType all|draft
     * @param  string $orderBy
     * @param  object $pager
     * @access public
     * @return array
     */
    public function getDocs($libID, $moduleID, $browseType, $orderBy, $pager = null)
    {
        if(common::isTutorialMode()) return $this->loadModel('tutorial')->getDocs();

        if(empty($libID) && $browseType != 'all') return array();

        $docIdList = $this->getPrivDocs($libID ? array($libID) : array(), $moduleID, 'children');
        $docs = $this->dao->select('*')->from(TABLE_DOC)
            ->where('deleted')->eq(0)
            ->andWhere('vision')->eq($this->config->vision)
            ->andWhere('templateType')->eq('')
            ->andWhere('id')->in($docIdList)
            ->beginIF($browseType == 'all')->andWhere("(status = 'normal' or (status = 'draft' and addedBy='{$this->app->user->account}'))")->fi()
            ->beginIF($browseType == 'draft')->andWhere('status')->eq('draft')->andWhere('addedBy')->eq($this->app->user->account)->fi()
            ->orderBy($orderBy)
            ->page($pager)
            ->fetchAll('id');

        $this->loadModel('common')->saveQueryCondition($this->dao->get(), 'doc', true);

        return $this->processCollector($docs);
    }

    /**
     * 获取我的空间下的文档列表数据。
     * Get mine list.
     *
     * @param  string $type       view|collect|createdby|editedby
     * @param  string $browseType all|draft|bysearch
     * @param  int    $queryID
     * @param  string $orderBy
     * @param  object $pager
     * @access public
     * @return array
     */
    public function getMineList($type, $browseType, $queryID = 0, $orderBy = 'id_desc', $pager = null)
    {
        $query = '';
        if($browseType == 'bysearch')
        {
            $query = $this->buildQuery($type, $queryID);
            $query = preg_replace('/(`\w+`)/', 't1.$1', $query);
        }
        if(in_array($type, array('view', 'collect', 'createdby', 'editedby'))) $docs = $this->getMySpaceDocs($type, $browseType, $query, $orderBy, $pager);

        $this->loadModel('tree');
        $objects = array();
        $modules = array();
        list($objects['project'], $objects['execution'], $objects['product']) = $this->getObjectsByDoc(array_keys($docs));
        foreach($docs as $docID => $doc)
        {
            if(!isset($modules[$doc->lib])) $modules[$doc->lib] = $this->tree->getOptionMenu((int)$doc->lib, 'doc', 0, 'all', 'nodeleted', 'all', ' > ');
            $doc->moduleName = zget($modules[$doc->lib], $doc->module);
            $doc->moduleName = ltrim($doc->moduleName, '/');

            $doc->objectID   = zget($doc, $doc->objectType, 0);
            $doc->objectName = '';
            if(isset($objects[$doc->objectType]))
            {
                $doc->objectName = $objects[$doc->objectType][$doc->objectID];
            }
            else
            {
                if($doc->objectType == 'mine')   $doc->objectName = $this->lang->doc->person;
                if($doc->objectType == 'custom') $doc->objectName = $this->lang->doc->team;
            }
        }

        return $this->processCollector($docs);
    }

    /**
     * 获取我的空间下的文档列表数据。
     * Get doc list under the my space.
     *
     * @param  string $type       view|collect|createdby|editedby
     * @param  string $browseType all|draft|bysearch
     * @param  string $orderBy
     * @param  string $query
     * @param  object $pager
     * @access public
     * @return array
     */
    public function getMySpaceDocs($type, $browseType, $query = '', $orderBy = 'id_desc', $pager = null)
    {
        if(!in_array($type, array('view', 'collect', 'createdby', 'editedby'))) return array();

        $allLibs          = $this->getLibs('all');
        $allLibIDList     = array_keys($allLibs);
        $hasPrivDocIdList = $this->getPrivDocs($allLibIDList);
        if(in_array($type, array('view', 'collect')))
        {
            $docSQL = $this->dao->select('MAX(id)')->from(TABLE_DOCACTION)
                ->where('action')->eq($type)
                ->andWhere('actor')->eq($this->app->user->account)
                ->groupBy('doc')
                ->get();

            $docs = $this->dao->select('t1.*,t2.date,t3.name as libName,t3.type as objectType')->from(TABLE_DOC)->alias('t1')
                ->leftJoin(TABLE_DOCACTION)->alias('t2')->on("t1.id=t2.doc")
                ->leftJoin(TABLE_DOCLIB)->alias('t3')->on("t1.lib=t3.id")
                ->where('t1.deleted')->eq(0)
                ->andWhere('t1.lib')->ne('')
                ->andWhere('t1.type')->in($this->config->doc->docTypes)
                ->andWhere('t1.vision')->eq($this->config->vision)
                ->andWhere('t2.id')->subIn($docSQL)
                ->beginIF(!common::hasPriv('doc', 'productSpace'))->andWhere('t3.type')->ne('product')->fi()
                ->beginIF(!common::hasPriv('doc', 'projectSpace'))->andWhere('t3.type')->notIN('project,execution')->fi()
                ->beginIF(!common::hasPriv('doc', 'teamSpace'))->andWhere('t3.type')->ne('custom')->fi()
                ->beginIF(in_array($browseType, array('all', 'bysearch')))->andWhere("(t1.status = 'normal' or (t1.status = 'draft' and t1.addedBy='{$this->app->user->account}'))")->fi()
                ->beginIF($browseType == 'draft')->andWhere('t1.status')->eq('draft')->andWhere('t1.addedBy')->eq($this->app->user->account)->fi()
                ->beginIF($browseType == 'bysearch')->andWhere($query)->fi()
                ->beginIF(!empty($hasPrivDocIdList))->andWhere('t1.id')->in($hasPrivDocIdList)->fi()
                ->orderBy($orderBy)
                ->page($pager, 't1.id')
                ->fetchAll('id');
        }
        else
        {
            $docIdList = $type == 'editedby' ? $this->docTao->getEditedDocIdList() : array();
            $docs = $this->dao->select('t1.*,t2.name as libName,t2.type as objectType')->from(TABLE_DOC)->alias('t1')->leftJoin(TABLE_DOCLIB)->alias('t2')->on("t1.lib=t2.id")
                ->where('t1.deleted')->eq(0)
                ->andWhere('t1.lib')->ne('')
                ->andWhere('t1.vision')->eq($this->config->vision)
                ->andWhere('t1.type')->in($this->config->doc->docTypes)
                ->beginIF($type == 'createdby')->andWhere('t1.addedBy')->eq($this->app->user->account)->fi()
                ->beginIF($type == 'editedby')->andWhere('t1.id')->in($docIdList)->fi()
                ->beginIF(!common::hasPriv('doc', 'productSpace'))->andWhere('t2.type')->ne('product')->fi()
                ->beginIF(!common::hasPriv('doc', 'projectSpace'))->andWhere('t2.type')->notIN('project,execution')->fi()
                ->beginIF(!common::hasPriv('doc', 'teamSpace'))->andWhere('t2.type')->ne('custom')->fi()
                ->beginIF($browseType == 'draft')->andWhere('t1.status')->eq('draft')->andWhere('t1.addedBy')->eq($this->app->user->account)->fi()
                ->beginIF($browseType == 'bysearch')->andWhere($query)->fi()
                ->beginIF(!empty($hasPrivDocIdList))->andWhere('t1.id')->in($hasPrivDocIdList)->fi()
                ->orderBy($orderBy)
                ->page($pager)
                ->fetchAll('id');
        }

        return $docs;
    }

    /**
     * 获取有权限查看的文档ID列表。
     * Get grant doc id list.
     *
     * @param  array  $libIdList
     * @param  int    $moduleID
     * @param  string $mode normal|all|chidren
     * @access public
     * @return array
     */
    public function getPrivDocs($libIdList = array(), $moduleID = 0, $mode = 'normal')
    {
        $modules = $moduleID && $mode == 'children' ? $this->loadModel('tree')->getAllChildID($moduleID) : $moduleID;

        /* 团队空间下的空间列出所有子库的文档。 */
        if(count($libIdList) == 1)
        {
            $libID = current($libIdList);
            $lib   = $this->getLibByID($libID);
            if($lib->type == 'custom' && $lib->parent == 0)
            {
                $libs = $this->dao->select('*')->from(TABLE_DOCLIB)->where('parent')->eq($libID)->andWhere('deleted')->eq('0')->fetchAll();
                foreach($libs as $subLib)
                {
                    if($this->checkPrivLib($subLib)) $libIdList[] = $subLib->id;
                }
            }
        }

        $stmt = $this->dao->select('*')->from(TABLE_DOC)
            ->where('vision')->eq($this->config->vision)
            ->andWhere('templateType')->eq('')
            ->beginIF(!empty($modules))->andWhere('module')->in($modules)->fi()
            ->beginIF($mode == 'normal')->andWhere('deleted')->eq(0)->fi()
            ->beginIF($this->config->doc->notArticleType)->andWhere('type')->notIN($this->config->doc->notArticleType)->fi()
            ->beginIF($libIdList)->andWhere('lib')->in($libIdList)->fi()
            ->query();

        $docIdList = array();
        while($doc = $stmt->fetch())
        {
            if($this->checkPrivDoc($doc)) $docIdList[$doc->id] = $doc->id;
        }
        return $docIdList;
    }

    /**
     * 通过ID获取文档信息。
     * Get doc info by id.
     *
     * @param  int          $docID
     * @param  int          $version
     * @param  bool         $setImgSize
     * @access public
     * @return object|false
     */
    public function getByID($docID, $version = 0, $setImgSize = false)
    {
        if(common::isTutorialMode()) return $this->loadModel('tutorial')->getDoc();

        $doc = $this->dao->select('*')->from(TABLE_DOC)
            ->where('id')->eq((int)$docID)
            ->andWhere('vision')->eq($this->config->vision)
            ->fetch();
        if(!$doc) return false;

        $docs    = $this->processCollector(array($doc->id => $doc));
        $doc     = $docs[$doc->id];
        $version = $version ? $version : $doc->version;

        $doc->releasedBy = $doc->releasedDate = '';
        if($doc->status == 'normal')
        {
            $releaseInfo = $this->dao->select('*')->from(TABLE_ACTION)
                ->where('objectType')->eq('doc')
                ->andWhere('objectID')->eq($docID)
                ->andWhere('action')->eq('releaseddoc')
                ->fetch();
            $doc->releasedBy   = $releaseInfo ? $releaseInfo->actor : $doc->addedBy;
            $doc->releasedDate = $releaseInfo ? $releaseInfo->date : $doc->addedDate;
        }

        return $this->processDoc($doc, (int)$version, $setImgSize);
    }

    /**
     * 处理文档数据。
     * Process doc data.
     *
     * @param  object $doc
     * @param  bool   $setImgSize
     * @access public
     * @return object
     * @param int $version
     */
    public function processDoc($doc, $version, $setImgSize = false)
    {
        /* When file change then version add one. */
        $files      = $this->loadModel('file')->getByObject('doc', $doc->id);
        $docFiles   = array();
        $docContent = $this->dao->select('*')->from(TABLE_DOCCONTENT)->where('doc')->eq($doc->id)->andWhere('version')->eq($version)->fetch();
        if($docContent)
        {
            foreach($files as $file)
            {
                $this->loadModel('file')->setFileWebAndRealPaths($file);
                if(strpos(",{$docContent->files},", ",{$file->id},") !== false) $docFiles[$file->id] = $file;
            }
        }

        /* Check file change. */
        if($version == $doc->version && ((empty($docContent->files) && $docFiles) || (!empty($docContent->files) && count(explode(',', trim($docContent->files, ','))) != count($docFiles))))
        {
            unset($docContent->id);
            $doc->version       += 1;
            $docContent->version = $doc->version;
            $docContent->files   = join(',', array_keys($docFiles));
            $this->dao->insert(TABLE_DOCCONTENT)->data($docContent)->exec();
            $this->dao->update(TABLE_DOC)->set('version')->eq($doc->version)->where('id')->eq($doc->id)->exec();
        }

        $doc->title          = isset($docContent->title) ? $docContent->title : $doc->title;
        $doc->digest         = isset($docContent->digest) ? $docContent->digest : '';
        $doc->content        = isset($docContent->content) ? $docContent->content : '';
        $doc->contentType    = isset($docContent->type) ? $docContent->type : '';
        $doc->contentVersion = isset($docContent->version) ? $docContent->version : $version;
        if($doc->type != 'url' && $doc->contentType != 'markdown') $doc = $this->loadModel('file')->replaceImgURL($doc, 'content,draft');
        if($setImgSize) $doc->content = $this->file->setImgSize($doc->content);
        $doc->files = $docFiles;

        $doc->productName = $doc->executionName = $doc->moduleName = '';
        if($doc->product)   $doc->productName   = $this->dao->findByID($doc->product)->from(TABLE_PRODUCT)->fetch('name');
        if($doc->execution) $doc->executionName = $this->dao->findByID($doc->execution)->from(TABLE_EXECUTION)->fetch('name');
        if($doc->module)    $doc->moduleName    = $this->dao->findByID($doc->module)->from(TABLE_MODULE)->fetch('name');
        if(!$doc->module && $doc->type == 'article' && $doc->parent) $doc->moduleName = $this->dao->findByID($doc->parent)->from(TABLE_DOC)->fetch('title');

        return $doc;
    }

    /**
     * 通过ID列表获取文档信息。
     * Get docs info by id list.
     *
     * @param  array  $docIdList
     * @access public
     * @return array
     */
    public function getByIdList($docIdList = array())
    {
        return $this->dao->select('*,t1.id as docID,t1.type as docType,t1.version as docVersion,t2.type as contentType')->from(TABLE_DOC)->alias('t1')
            ->leftJoin(TABLE_DOCCONTENT)->alias('t2')->on('t1.id=t2.doc and t1.version=t2.version')
            ->where('t1.id')->in($docIdList)
            ->andWhere('deleted')->eq(0)
            ->fetchAll('docID');
    }

    /**
     * 获取团队空间下的空间。
     * Get team spaces.
     *
     * @access public
     * @return array
     */
    public function getTeamSpaces()
    {
        if(common::isTutorialMode()) return $this->loadModel('tutorial')->getTeamSpaces();

        $objectLibs = $this->dao->select('*')->from(TABLE_DOCLIB)
            ->where('type')->eq('custom')
            ->andWhere('deleted')->eq(0)
            ->andWhere('parent')->eq(0)
            ->andWhere('vision')->eq($this->config->vision)
            ->fetchAll();

        $pairs = array();
        foreach($objectLibs as $lib)
        {
            if($this->checkPrivLib($lib)) $pairs[$lib->id] = $lib->name;
        }

        return $pairs;
    }

    /**
     * 创建独立的文档。
     * Create a seperate docs.
     *
     * @param  object             $doc
     * @access public
     * @return array|false|string
     */
    public function createSeperateDocs($doc)
    {
        if($doc->acl == 'open') $doc->users = $doc->groups = '';
        if(empty($doc->lib) && strpos((string)$doc->module, '_') !== false) list($doc->lib, $doc->module) = explode('_', $doc->module);
        if(empty($doc->lib)) return dao::$errors['lib'] = sprintf($this->lang->error->notempty, $this->lang->doc->lib);

        $files = $this->loadModel('file')->getUpload();
        if(empty($files) || isset($files['name'])) return dao::$errors['files'] = sprintf($this->lang->error->notempty, $this->lang->doc->uploadFile);

        $lib = $this->getLibByID($doc->lib);
        $doc = $this->loadModel('file')->processImgURL($doc, $this->config->doc->editor->create['id'], (string)$this->post->uid);

        $doc->product   = $lib->product;
        $doc->project   = $lib->project;
        $doc->execution = $lib->execution;

        $docContent          = new stdclass();
        $docContent->title   = $doc->title;
        $docContent->content = '';
        $docContent->type    = $doc->contentType;
        $docContent->digest  = '';
        $docContent->version = 1;
        unset($doc->contentType);

        return $this->insertSeperateDocs($doc, $docContent, $files);
    }

    /**
     * 批量插入独立的文档。
     * Insert seperate docs.
     *
     * @param  object     $doc
     * @param  object     $docContent
     * @param  array      $files
     * @access public
     * @return array|bool
     */
    public function insertSeperateDocs($doc, $docContent, $files)
    {
        $this->loadModel('file');

        $doc->draft = '';
        $docsAction = array();
        foreach($files as $file)
        {
            $title    = $file['title'];
            $position = strrpos($title, '.');
            if($position > 0) $title = substr($title, 0, $position);

            $doc->title = $title;
            $this->dao->insert(TABLE_DOC)->data($doc, 'content')->autoCheck()
                ->batchCheck($this->config->doc->create->requiredFields, 'notempty')
                ->exec();
            if(dao::isError()) return false;

            $docID = $this->dao->lastInsertID();
            $this->file->updateObjectID($this->post->uid, $docID, 'doc');
            $fileTitle = $this->file->saveAFile($file, 'doc', $docID);
            $docsAction[$docID] = $fileTitle;
            if(empty($fileTitle)) continue;
            if(dao::isError())
            {
                dao::$errors['message'][] = 'doc#' . ($file->title) . dao::getError(true);
                continue;
            }

            $docContent->doc     = $docID;
            $docContent->files   = ',' . $fileTitle->id;
            $docContent->title   = $title;
            $docContent->content = '';
            $docContent->type    = 'attachment';
            $docContent->digest  = '';
            $docContent->version = 1;
            $this->dao->insert(TABLE_DOCCONTENT)->data($docContent)->exec();
            $this->loadModel('score')->create('doc', 'create', $docID);
        }

        if(dao::isError()) return false;
        return array('status' => 'new', 'docsAction' => $docsAction, 'docType' => 'attachment', 'libID' => $doc->lib);
    }

    /**
     * 创建一个文档。
     * Create a doc.
     *
     * @param  object            $doc
     * @access public
     * @return array|bool|string
     */
    public function create($doc)
    {
        if($doc->acl == 'open') $doc->users = $doc->groups = '';
        if(empty($doc->lib) && strpos((string)$doc->module, '_') !== false) list($doc->lib, $doc->module) = explode('_', $doc->module);
        if(empty($doc->lib)) return dao::$errors['lib'] = sprintf($this->lang->error->notempty, $this->lang->doc->lib);

        $lib = $this->getLibByID($doc->lib);
        $doc = $this->loadModel('file')->processImgURL($doc, $this->config->doc->editor->create['id'], (string)$this->post->uid);
        $doc->product   = $lib->product;
        $doc->project   = $lib->project;
        $doc->execution = $lib->execution;

        $docContent          = new stdclass();
        $docContent->title   = $doc->title;
        $docContent->content = $doc->content;
        $docContent->type    = $doc->contentType;
        $docContent->digest  = '';
        $docContent->version = 1;
        unset($doc->contentType);

        $requiredFields = $this->config->doc->create->requiredFields;
        if($doc->status == 'draft') $requiredFields = 'title';
        if(strpos("url|word|ppt|excel", $doc->type) !== false) $requiredFields = trim(str_replace(",content,", ",", ",{$requiredFields},"), ',');

        $checkContent = strpos(",$requiredFields,", ',content,') !== false;
        if($checkContent && strpos("url|word|ppt|excel|", $lib->type) === false)
        {
            $requiredFields = trim(str_replace(',content,', ',', ",$requiredFields,"), ',');
            if(empty($docContent->content)) return dao::$errors['content'] = sprintf($this->lang->error->notempty, $this->lang->doc->content);
        }

        $files = $this->loadModel('file')->getUpload();
        if($doc->type == 'attachment' && (empty($files) || isset($files['name']))) return dao::$errors['files'] = sprintf($this->lang->error->notempty, $this->lang->doc->uploadFile);

        $doc->draft  = $docContent->content;
        $doc->vision = $this->config->vision;
        $this->dao->insert(TABLE_DOC)->data($doc, 'content')->autoCheck()
            ->batchCheck($requiredFields, 'notempty')
            ->exec();
        if(dao::isError()) return false;

        $docID = $this->dao->lastInsertID();

        $this->dao->update(TABLE_DOC)->set('`order`')->eq($docID)->where('id')->eq($docID)->exec();
        $this->file->updateObjectID($this->post->uid, $docID, 'doc');
        $files = $this->file->saveUpload('doc', $docID);

        $docContent->doc   = $docID;
        $docContent->files = join(',', array_keys($files));
        $this->dao->insert(TABLE_DOCCONTENT)->data($docContent)->exec();
        $this->loadModel('score')->create('doc', 'create', $docID);
        return array('status' => 'new', 'id' => $docID, 'files' => $files, 'docType' => $doc->type, 'libID' => $doc->lib);
    }

    /**
     * 编辑一个文档。
     * Update a doc.
     *
     * @param  int               $docID
     * @param  object            $doc
     * @access public
     * @return array|string|bool
     */
    public function update($docID, $doc)
    {
        $oldDoc = $this->dao->select('*')->from(TABLE_DOC)->where('id')->eq($docID)->fetch();
        list($doc, $oldDocContent) = $this->processDocForUpdate($oldDoc, $doc);
        if(dao::isError()) return false;

        $requiredFields = $doc->status == 'draft' ? 'title' : $this->config->doc->edit->requiredFields;
        if(strpos(",$requiredFields,", ',content,') !== false)
        {
            $requiredFields = trim(str_replace(',content,', ',', ",$requiredFields,"), ',');
            if(isset($doc->content) && empty($doc->content)) return dao::$errors['content'] = sprintf($this->lang->error->notempty, $this->lang->doc->content);
        }

        $files   = $this->file->saveUpload('doc', $docID);
        $changes = common::createChanges($oldDoc, $doc);
        $changed = $files ? true : false;
        foreach($changes as $change) if($change['field'] == 'content' || $change['field'] == 'title') $changed = true;

        if($changed)
        {
            $docContent          = new stdclass();
            $docContent->doc     = $oldDoc->id;
            $docContent->title   = $doc->title;
            $docContent->content = isset($doc->content) ? $doc->content : '';
            $docContent->files   = $oldDocContent->files;
            if($files) $docContent->files .= ',' . join(',', array_keys($files));
            $docContent->files = trim($docContent->files, ',');
            if(isset($doc->digest)) $docContent->digest = $doc->digest;

            if($oldDoc->status == 'draft') $this->dao->update(TABLE_DOCCONTENT)->data($docContent)->where('id')->eq($oldDocContent->id)->exec();
            if($oldDoc->status != 'draft')
            {
                $doc->version        = $oldDoc->version + 1;
                $docContent->version = $doc->version;
                $docContent->type    = $oldDocContent->type;
                $this->dao->replace(TABLE_DOCCONTENT)->data($docContent)->exec();
            }
        }

        unset($doc->contentType);
        $doc->draft = isset($doc->content) ? $doc->content : '';
        $this->dao->update(TABLE_DOC)->data($doc, 'content')
            ->autoCheck()
            ->batchCheck($requiredFields, 'notempty')
            ->where('id')->eq($docID)
            ->exec();

        if(dao::isError()) return false;
        $this->file->updateObjectID($this->post->uid, $docID, 'doc');
        return array('changes' => $changes, 'files' => $files);
    }

    /**
     * 为更新文档处理数据。
     * Process data for update a doc.
     *
     * @param  object $oldDoc
     * @param  object $doc
     * @access public
     * @return array
     */
    public function processDocForUpdate($oldDoc, $doc)
    {
        $editingDate = $oldDoc->editingDate ? json_decode($oldDoc->editingDate, true) : array();
        unset($editingDate[$this->app->user->account]);
        $doc->editingDate = json_encode($editingDate);

        if($doc->acl == 'open') $doc->users = $doc->groups = '';
        if($doc->type == 'chapter' && $doc->parent)
        {
            $parentDoc = $this->dao->select('*')->from(TABLE_DOC)->where('id')->eq((int)$doc->parent)->fetch();
            if(strpos($parentDoc->path, ",$oldDoc->id,") !== false) return dao::$errors['parent'] = $this->lang->doc->errorParentChapter;
        }

        $oldDocContent = $this->dao->select('*')->from(TABLE_DOCCONTENT)->where('doc')->eq($oldDoc->id)->andWhere('version')->eq($oldDoc->version)->fetch();
        if($oldDocContent)
        {
            $oldDoc->title       = $oldDocContent->title;
            $oldDoc->digest      = $oldDocContent->digest;
            $oldDoc->content     = $oldDocContent->content;
            $oldDoc->contentType = $oldDocContent->type;
        }

        $lib = !empty($doc->lib) ? $this->getLibByID($doc->lib) : '';
        $doc = $this->loadModel('file')->processImgURL($doc, $this->config->doc->editor->edit['id'], (string)$this->post->uid);

        if(!empty($lib))
        {
            $doc->product   = $lib->product;
            $doc->execution = $lib->execution;
        }

        return array($doc, $oldDocContent);
    }

    /**
     * 构造搜索条件。
     * Build search query.
     *
     * @param  string $type
     * @param  int    $queryID
     * @access public
     * @return string
     */
    public function buildQuery($type, $queryID = 0)
    {
        $queryName = $type . 'libDocQuery';
        $queryForm = $type . 'libDocForm';
        if($queryID)
        {
            $query = $this->loadModel('search')->getQuery($queryID);
            if($query)
            {
                $this->session->set($queryName, $query->sql);
                $this->session->set($queryForm, $query->form);
            }
            else
            {
                $this->session->set($queryName, ' 1 = 1');
            }
        }
        else
        {
            if($this->session->$queryName == false) $this->session->set($queryName, ' 1 = 1');
        }

        $query = $this->session->$queryName;
        if(strpos($query, "`lib` = 'all'") !== false) $query = str_replace("`lib` = 'all'", '1', $query);
        if(strpos($query, "`lib` = ''") !== false) $query = str_replace("`lib` = ''", '1', $query);
        return $query;
    }

    /**
     * 获取执行文档库的所属模块的键值对。
     * Gets the key-value pair of the module by execution ID.
     *
     * @access public
     * @return array
     */
    public function getExecutionModulePairs()
    {
        return $this->dao->select('t1.id,t1.name')->from(TABLE_MODULE)->alias('t1')
            ->leftJoin(TABLE_DOCLIB)->alias('t2')->on('t1.root = t2.id')
            ->where('t1.type')->eq('doc')
            ->andWhere('t2.execution')->ne('0')
            ->fetchPairs();
    }

    /**
     * 检查是否有权限访问文档库/文档。
     * Check priv for lib.
     *
     * @param  object|false $object
     * @param  string       $extra
     * @access public
     * @return bool
     */
    public function checkPrivLib($object, $extra = '')
    {
        if(empty($object)) return false;

        /* Only the creator can access the document library under my space. */
        if($object->type == 'mine' && $object->addedBy == $this->app->user->account) return true;
        if($this->app->user->admin && $object->type != 'mine') return true;
        if($object->acl == 'open') return true;

        /* When view a document, check whether current user has permission to access the document library. */
        $account = ',' . $this->app->user->account . ',';
        if(isset($object->addedBy) && $object->addedBy == $this->app->user->account) return true;
        if(isset($object->users) && strpos(",{$object->users},", $account) !== false) return true;

        if(!empty($object->groups))
        {
            $userGroups = $this->app->user->groups;
            foreach($userGroups as $groupID)
            {
                if(strpos(",$object->groups,", ",$groupID,") !== false) return true;
            }
        }

        $isProjectLib = $object->project && !$object->execution;
        if($isProjectLib && $object->acl == 'default' && $this->loadModel('project')->checkPriv($object->project)) return true;

        /* The user has permission to access the owning document library that can access the document. */
        if(strpos($extra, 'notdoc') !== false)
        {
            static $extraDocLibs;
            if($extraDocLibs === null) $extraDocLibs = $this->getPrivLibsByDoc();
            if(isset($extraDocLibs[$object->id])) return true;
        }

        /* If the acl is default, the document library cannot be accessed without object permission. */
        if($object->acl == 'default' && (!empty($object->product) || !empty($object->execution)))
        {
            $acls = $this->app->user->rights['acls'];
            if(!empty($object->product) && !empty($acls['products']) && !in_array($object->product, $acls['products'])) return false;
            if(!empty($object->execution) && !empty($acls['sprints']) && !in_array($object->execution, $acls['sprints'])) return false;
            if(!empty($object->execution)) return $this->loadModel('execution')->checkPriv($object->execution);
            if(!empty($object->product)) return $this->loadModel('product')->checkPriv($object->product);
        }

        return false;
    }

    /**
     * 检查文档权限。
     * Check privilege for the document.
     *
     * @param  object $doc
     * @access public
     * @return bool
     */
    public function checkPrivDoc($doc)
    {
        if(!isset($doc->lib)) return false;

        /* Asset document don't check privilege. */
        if((isset($doc->assetLibType) && $doc->assetLibType) || $doc->type == 'article') return true;

        /* My document are accessible only to the creator. */
        if($doc->status == 'draft' && $doc->addedBy != $this->app->user->account) return false;
        if($doc->status == 'normal' && $this->app->user->admin) return true;

        static $libs = array();
        if(!isset($libs[$doc->lib])) $libs[$doc->lib] = $this->getLibByID((int)$doc->lib);
        if(!$this->checkPrivLib($libs[$doc->lib])) return false;
        if(in_array($doc->acl, array('open', 'public'))) return true;

        /* Whitelist users can access private document. */
        $account = ",{$this->app->user->account},";
        if(isset($doc->addedBy) && $doc->addedBy == $this->app->user->account) return true;
        if(strpos(",$doc->users,", $account) !== false) return true;
        if($doc->groups)
        {
            foreach($this->app->user->groups as $groupID)
            {
                if(strpos(",$doc->groups,", ",$groupID,") !== false) return true;
            }
        }

        return false;
    }

    /**
     * 通过对象ID获取文档库。
     * Get libs by object.
     *
     * @param  string $type
     * @param  int    $objectID
     * @param  int    $appendLib
     * @access public
     * @return array
     */
    public function getLibsByObject($type, $objectID = 0, $appendLib = 0)
    {
        if(common::isTutorialMode()) return $this->loadModel('tutorial')->getDocLibs();

        if(!in_array($type, array('mine', 'custom', 'product', 'project', 'execution'))) return array();
        if(in_array($type, array('mine', 'custom')))
        {
            $objectLibs = $this->dao->select('*')->from(TABLE_DOCLIB)
                ->where('deleted')->eq(0)
                ->andWhere('vision')->eq($this->config->vision)
                ->andWhere('type')->eq($type)
                ->beginIF($type == 'custom')->andWhere('parent')->eq($objectID)->fi()
                ->beginIF($type == 'mine')->andWhere('addedBy')->eq($this->app->user->account)->fi()
                ->beginIF(!empty($appendLib))->orWhere('id')->eq($appendLib)->fi()
                ->orderBy('`order` asc, id_asc')
                ->fetchAll('id');
        }
        else
        {
            $objectLibs = $this->dao->select('*')->from(TABLE_DOCLIB)
                ->where('deleted')->eq(0)
                ->andWhere('vision')->eq($this->config->vision)
                ->andWhere($type)->eq($objectID)
                ->beginIF($type == 'project')->andWhere('type')->in('api,project')->fi()
                ->beginIF(!empty($appendLib))->orWhere('id')->eq($appendLib)->fi()
                ->orderBy('`order` asc, id_asc')
                ->fetchAll('id');

            $executionIDList = array();
            if($type == 'project') $executionIDList = $this->loadModel('execution')->getPairs($objectID, 'all', 'multiple,leaf');
            if($executionIDList)
            {
                $objectLibs += $this->dao->select('*')->from(TABLE_DOCLIB)
                    ->where('deleted')->eq(0)
                    ->andWhere('vision')->eq($this->config->vision)
                    ->andWhere('execution')->in(array_keys($executionIDList))
                    ->andWhere('type')->eq('execution')
                    ->beginIF(!empty($appendLib))->orWhere('id')->eq($appendLib)->fi()
                    ->orderBy('`order` asc, id_asc')
                    ->fetchAll('id');
            }
        }

        $libs = array();
        foreach($objectLibs as $lib)
        {
            if($this->checkPrivLib($lib)) $libs[$lib->id] = $lib;
        }

        $itemCounts = $this->statLibCounts(array_keys($libs));
        foreach($libs as $libID => $lib) $libs[$libID]->allCount = $itemCounts[$libID];
        return $libs;
    }

    /**
     * 获取已排序的对象数据。
     * Get ordered objects for doc.
     *
     * @param  string $objectType
     * @param  string $returnType nomerge|merge
     * @param  int    $append
     * @access public
     * @return array
     */
    public function getOrderedObjects($objectType = 'product', $returnType = 'merge', $append = 0)
    {
        $myObjects = $normalObjects = $closedObjects = array();
        if($objectType == 'product')
        {
            list($myObjects, $normalObjects, $closedObjects) = $this->getOrderedProducts($append);
        }
        elseif($objectType == 'project')
        {
            list($myObjects, $normalObjects, $closedObjects) = $this->getOrderedProjects($append);
        }
        elseif($objectType == 'execution')
        {
            list($myObjects, $normalObjects, $closedObjects) = $this->docTao->getOrderedExecutions($append);
        }

        if($returnType == 'nomerge') return array($myObjects, $normalObjects, $closedObjects);
        return $myObjects + $normalObjects + $closedObjects;
    }

    /**
     * 获取已排序的产品数据。
     * Get ordered products.
     *
     * @param  int    $append
     * @access public
     * @return array
     */
    public function getOrderedProducts($append = 0)
    {
        $myObjects = $normalObjects = $closedObjects = array();
        $products  = $this->loadModel('product')->getList();
        if($append && !isset($products[$append])) $products[$append] = $this->product->getByID($append);
        foreach($products as $id => $product)
        {
            if($product->status != 'closed' && $product->PO == $this->app->user->account)
            {
                $myObjects[$id] = $product->name;
            }
            elseif($product->status != 'closed' && !($product->PO == $this->app->user->account))
            {
                $normalObjects[$id] = $product->name;
            }
            elseif($product->status == 'closed')
            {
                $closedObjects[$id] = $product->name;
            }
        }

        return array($myObjects, $normalObjects, $closedObjects);
    }

    /**
     * 获取已排序的项目数据。
     * Get ordered projects.
     *
     * @param  int    $append
     * @access public
     * @return array
     */
    public function getOrderedProjects($append = 0)
    {
        $this->loadModel('program');

        /* Project permissions for DocLib whitelist. */
        $myObjects       = $normalObjects = $closedObjects = array();
        $orderedProjects = array();
        if($this->app->tab == 'doc')
        {
            $myObjects = $this->dao->select('t1.id, t1.name')->from(TABLE_PROJECT)->alias('t1')
                ->leftJoin(TABLE_DOCLIB)->alias('t2')->on('t2.project=t1.id')
                ->where("CONCAT(',', t2.users, ',')")->like("%,{$this->app->user->account},%")
                ->andWhere('t1.vision')->eq($this->config->vision)
                ->andWhere('t1.deleted')->eq(0)
                ->beginIF(!$this->app->user->admin)->andWhere('t1.id')->in($this->app->user->view->projects)->fi()
                ->fetchPairs();
        }
        $objects = $this->dao->select('*')->from(TABLE_PROJECT)
            ->where('type')->eq('project')
            ->andWhere('vision')->eq($this->config->vision)
            ->andWhere('deleted')->eq(0)
            ->beginIF(!$this->app->user->admin)->andWhere('id')->in($this->app->user->view->projects)->fi()
            ->beginIF($append)->orWhere('id')->eq($append)->fi()
            ->orderBy('order_asc')
            ->fetchAll('id');
        foreach($objects as $objectID => $object)
        {
            $object->parent             = $this->program->getTopByID($object->parent);
            $orderedProjects[$objectID] = $object;
            unset($objects[$object->id]);
        }

        foreach($orderedProjects as $id => $project)
        {
            if($project->status != 'done' && $project->status != 'closed' && $project->PM == $this->app->user->account)
            {
                $myObjects[$id] = $project->name;
            }
            elseif($project->status != 'done' && $project->status != 'closed' && !($project->PM == $this->app->user->account))
            {
                $normalObjects[$id] = $project->name;
            }
            elseif($project->status == 'done' || $project->status == 'closed')
            {
                $closedObjects[$id] = $project->name;
            }
        }
        return array($myObjects, $normalObjects, $closedObjects);
    }

    /**
     * 统计文档库下的模块和文档数量。
     * Stat module and document counts of lib.
     *
     * @param  array $idList
     * @access public
     * @return array
     */
    public function statLibCounts($idList)
    {
        if(empty($idList)) return array();

        $moduleCounts = $this->dao->select("`root`, count(id) as moduleCount")->from(TABLE_MODULE)
            ->where('type')->eq('doc')
            ->andWhere('root')->in($idList)
            ->andWhere('deleted')->eq(0)
            ->groupBy('root')
            ->fetchPairs();

        $docs = $this->dao->select("`id`,`addedBy`,`type`,`lib`,`acl`,`users`,`groups`,`status`")->from(TABLE_DOC)
            ->where('lib')->in($idList)
            ->andWhere('deleted')->eq(0)
            ->andWhere('module')->eq(0)
            ->fetchAll();

        $docCounts = array();
        foreach($docs as $doc)
        {
            if(!$this->checkPrivDoc($doc)) continue;

            if(!isset($docCounts[$doc->lib])) $docCounts[$doc->lib] = 0;
            $docCounts[$doc->lib] ++;
        }

        $itemCounts = array();
        foreach($idList as $libID)
        {
            $docCount           = isset($docCounts[$libID]) ? $docCounts[$libID] : 0;
            $moduleCount        = isset($moduleCounts[$libID]) ? $moduleCounts[$libID] : 0;
            $itemCounts[$libID] = $docCount + $moduleCount;
        }

        return $itemCounts;
    }

    /**
     * 获取文档库的附件。
     * Get lib files.
     *
     * @param  string      $type        product|project|execution
     * @param  int         $objectID
     * @param  string|bool $searchTitle
     * @param  string      $browseType
     * @param  int         $param
     * @param  string      $orderBy
     * @param  object      $pager
     * @access public
     * @return array
     */
    public function getLibFiles($type, $objectID, $browseType = '', $param = 0, $orderBy = 'id_desc', $pager = null)
    {
        if(!in_array($type, array('execution', 'project', 'product'))) return array();

        $searchModule = "{$type}DocFile";
        $queryID      = (int)$param;
        $this->loadModel('search')->setQuery($searchModule, $queryID);
        $docFileQuery = $this->session->{$searchModule . 'Query'};

        list($bugIdList, $testReportIdList, $caseIdList, $docIdList, $storyIdList, $planIdList, $releaseIdList, $issueIdList, $meetingIdList, $reviewIdList, $designIdList, $executionIdList, $taskIdList, $buildIdList, $testtaskIdList) = $this->getLinkedObjectData($type, $objectID);

        $files = $this->dao->select('*')->from(TABLE_FILE)
            ->where('size')->gt('0')
            ->andWhere('deleted')->eq('0')
            ->andWhere("(objectType = '$type' and objectID = $objectID)", true)
            ->orWhere("(objectType = 'doc' and objectID in ($docIdList))")
            ->orWhere("(objectType = 'bug' and objectID in ($bugIdList))")
            ->orWhere("(objectType = 'testreport' and objectID in ($testReportIdList))")
            ->orWhere("(objectType = 'testcase' and objectID in ($caseIdList))")
            ->beginIF($type == 'product')
            ->orWhere("(objectType in ('story','requirement', 'epic') and objectID in ($storyIdList))")
            ->orWhere("(objectType = 'release' and objectID in ($releaseIdList))")
            ->orWhere("(objectType = 'productplan' and objectID in ($planIdList))")
            ->fi()
            ->beginIF($type == 'project')
            ->orWhere("(objectType = 'execution' and objectID in ($executionIdList))")
            ->orWhere("(objectType = 'issue' and objectID in ($issueIdList))")
            ->orWhere("(objectType = 'review' and objectID in ($reviewIdList))")
            ->orWhere("(objectType = 'meeting' and objectID in ($meetingIdList))")
            ->orWhere("(objectType = 'design' and objectID in ($designIdList))")
            ->fi()
            ->beginIF($type == 'project' || $type == 'execution')
            ->orWhere("(objectType = 'task' and objectID in ($taskIdList))")
            ->orWhere("(objectType = 'build' and objectID in ($buildIdList))")
            ->orWhere("(objectType = 'testtask' and objectID in ($testtaskIdList))")
            ->beginIF($storyIdList)->orWhere("(objectType = 'story' and objectID in ($storyIdList))")->fi()
            ->fi()
            ->markRight(1)
            ->beginIF($browseType == 'bySearch')->andWhere("($docFileQuery)")->fi()
            ->orderBy($orderBy)
            ->page($pager)
            ->fetchAll('id');

        $this->loadModel('file');
        foreach($files as $fileID => $file) $this->file->setFileWebAndRealPaths($file);

        return $files;
    }

    /**
     * 获取关联产品/项目/执行的数据。
     * Get linked product/project/execution data.
     *
     * @param  string $type     product|project|execution
     * @param  int    $objectID
     * @access public
     * @return array
     */
    public function getLinkedObjectData($type, $objectID)
    {
        if(!in_array($type, array('execution', 'project', 'product'))) return array();

        $userView = $this->app->user->view->products;
        if($type == 'project')   $userView = $this->app->user->view->projects;
        if($type == 'execution') $userView = $this->app->user->view->sprints;

        $bugIdList = $testReportIdList = $caseIdList = $testtaskIdList = $storyIdList = $planIdList = $releaseIdList = $executionIdList = $taskIdList = $buildIdList = $issueIdList = $meetingIdList = $designIdList = $reviewIdList = 0;
        $bugPairs  = $this->dao->select('id')->from(TABLE_BUG)->where($type)->eq($objectID)->andWhere('deleted')->eq('0')->beginIF(!$this->app->user->admin)->andWhere($type)->in($userView)->fi()->fetchPairs('id');
        if(!empty($bugPairs)) $bugIdList = implode(',', $bugPairs);

        $testReportPairs = $this->dao->select('id')->from(TABLE_TESTREPORT)->where($type)->eq($objectID)->andWhere('deleted')->eq('0')->beginIF(!$this->app->user->admin)->andWhere($type)->in($userView)->fi()->fetchPairs('id');
        if(!empty($testReportPairs)) $testReportIdList = implode(',', $testReportPairs);

        $field     = $type == 'execution' ? 'project' : $type;
        $casePairs = $this->dao->select('`case`')->from(TABLE_PROJECTCASE)->where($field)->eq($objectID)->beginIF(!$this->app->user->admin)->andWhere($field)->in($userView)->fi()->fetchPairs('case');
        if(!empty($casePairs)) $caseIdList = implode(',', $casePairs);

        $docs = $this->dao->select('*')->from(TABLE_DOC)->where($type)->eq($objectID)->fetchAll('id');
        foreach($docs as $id => $doc)
        {
            if(!$this->checkPrivDoc($doc)) unset($docs[$id]);
        }
        $docIdList = empty($docs) ? 0 : $this->dao->select('id')->from(TABLE_DOC)->where($type)->eq($objectID)->andWhere('id')->in(array_keys($docs))->get();

        if($type == 'product')
        {
            list($storyIdList, $planIdList, $releasePairs, $casePairs) = $this->docTao->getLinkedProductData($objectID, $userView);
            if(!empty($releasePairs)) $releaseIdList = implode(',', $releasePairs);
            if(!empty($casePairs))    $caseIdList    = implode(',', $casePairs);
        }
        elseif($type == 'project')
        {
            list($storyIdList, $issueIdList, $meetingIdList, $reviewIdList, $designIdList, $executionIdList, $taskIdList, $buildIdList) = $this->getLinkedProjectData($objectID);
        }
        elseif($type == 'execution')
        {
            list($storyIdList, $taskIdList, $buildIdList, $testtaskIdList) = $this->getLinkedExecutionData($objectID);
        }

        return array($bugIdList, $testReportIdList, $caseIdList, $docIdList, $storyIdList, $planIdList, $releaseIdList, $issueIdList, $meetingIdList, $reviewIdList, $designIdList, $executionIdList, $taskIdList, $buildIdList, $testtaskIdList);
    }

    /**
     * 获取关联项目的数据。
     * Get linked project data.
     *
     * @param  int    $projectID
     * @access public
     * @return array
     */
    public function getLinkedProjectData($projectID)
    {
        $project     = $this->loadModel('project')->getByID($projectID);
        $storyIdList = $issueIdList = $meetingIdList = $reviewIdList = $designIdList = $executionIdList = $taskIdList = $buildIdList = 0;
        if($project && !$project->hasProduct)
        {
            $projectIDList = $this->dao->select('*')->from(TABLE_PROJECT)->where('id')->eq($projectID)->orWhere('project')->eq($projectID)->fetchPairs('id', 'id');
            $storyIdList   = $this->dao->select('story')->from(TABLE_PROJECTSTORY)->where('project')->in($projectIDList)->fetchPairs('story', 'story');
        }

        if(in_array($this->config->edition, array('max', 'ipd')))
        {
            $issueIdList = $this->dao->select('id')->from(TABLE_ISSUE)
                ->where('project')->eq($projectID)
                ->andWhere('deleted')->eq('0')
                ->beginIF(!$this->app->user->admin)->andWhere('project')->in($this->app->user->view->projects)->fi()
                ->get();
            $meetingIdList = $this->dao->select('id')->from(TABLE_MEETING)
                ->where('project')->eq($projectID)
                ->andWhere('deleted')->eq('0')
                ->beginIF(!$this->app->user->admin)->andWhere('project')->in($this->app->user->view->projects)->fi()
                ->get();
            $reviewIdList = $this->dao->select('id')->from(TABLE_REVIEW)
                ->where('project')->eq($projectID)
                ->andWhere('deleted')->eq('0')
                ->beginIF(!$this->app->user->admin)->andWhere('project')->in($this->app->user->view->projects)->fi()
                ->get();
        }

        $designIdList = $this->dao->select('id')->from(TABLE_DESIGN)
            ->where('project')->eq($projectID)
            ->andWhere('deleted')->eq('0')
            ->beginIF(!$this->app->user->admin)->andWhere('project')->in($this->app->user->view->projects)->fi()
            ->get();

        $executionIdList = $this->loadModel('execution')->getIdList($projectID);
        $taskPairs       = $this->dao->select('id')->from(TABLE_TASK)
            ->where('execution')->in($executionIdList)
            ->andWhere('deleted')->eq('0')
            ->beginIF(!$this->app->user->admin)->andWhere('execution')->in($this->app->user->view->sprints)->fi()
            ->fetchPairs('id');
        if(!empty($taskPairs)) $taskIdList = implode(',', $taskPairs);

        $buildPairs = $this->dao->select('id')->from(TABLE_BUILD)
            ->where('execution')->in($executionIdList)
            ->andWhere('deleted')->eq('0')
            ->beginIF(!$this->app->user->admin)->andWhere('execution')->in($this->app->user->view->sprints)->fi()
            ->fetchPairs('id');
        if(!empty($buildPairs)) $buildIdList = implode(',', $buildPairs);

        $executionIdList = $executionIdList ? join(',', $executionIdList) : 0;
        $storyIdList     = $storyIdList ? join(',', $storyIdList) : 0;
        return array($storyIdList, $issueIdList, $meetingIdList, $reviewIdList, $designIdList, $executionIdList, $taskIdList, $buildIdList);
    }

    /**
     * 获取关联执行的数据。
     * Get linked execution data.
     *
     * @param  int    $executionID
     * @access public
     * @return array
     */
    public function getLinkedExecutionData($executionID)
    {
        $storyIdList = $taskIdList = $buildIdList = $testtaskIdList = 0;
        $execution   = $this->loadModel('execution')->getByID($executionID);
        $project     = $execution ? $this->loadModel('project')->getByID((int)$execution->project) : '';

        if($project && !$project->hasProduct) $storyIdList = $this->dao->select('story')->from(TABLE_PROJECTSTORY)->where('project')->eq($executionID)->fetchPairs('story', 'story');
        $storyIdList = $storyIdList ? join(',', $storyIdList) : '';

        $taskPairs = $this->dao->select('id')->from(TABLE_TASK)
            ->where('execution')->eq($executionID)
            ->andWhere('deleted')->eq('0')
            ->beginIF(!$this->app->user->admin)->andWhere('execution')->in($this->app->user->view->sprints)->fi()
            ->fetchPairs('id');
        if(!empty($taskPairs)) $taskIdList = implode(',', $taskPairs);

        $buildPairs = $this->dao->select('id')->from(TABLE_BUILD)
            ->where('execution')->eq($executionID)
            ->andWhere('deleted')->eq('0')
            ->beginIF(!$this->app->user->admin)->andWhere('execution')->in($this->app->user->view->sprints)->fi()
            ->fetchPairs('id');
        if(!empty($buildPairs)) $buildIdList = implode(',', $buildPairs);

        $testtaskPairs = $this->dao->select('id')->from(TABLE_TESTTASK)
            ->where('execution')->eq($executionID)
            ->andWhere('deleted')->eq('0')
            ->beginIF(!$this->app->user->admin)->andWhere('execution')->in($this->app->user->view->sprints)->fi()
            ->fetchPairs('id');
        if(!empty($testtaskPairs)) $testtaskIdList = implode(',', $testtaskPairs);

        return array($storyIdList, $taskIdList, $buildIdList, $testtaskIdList);
    }

    /**
     * 获取附件的来源。
     * Get file source pairs.
     *
     * @param  array  $files
     * @access public
     * @return array
     */
    public function getFileSourcePairs($files)
    {
        $sourceList  = array();
        $sourcePairs = array();
        foreach($files as $file)
        {
            if(!isset($sourceList[$file->objectType])) $sourceList[$file->objectType] = array();
            $sourceList[$file->objectType][$file->objectID] = $file->objectID;
        }

        $this->app->loadConfig('action');
        foreach($sourceList as $type => $idList)
        {
            $table = zget($this->config->objectTables, $type, '');
            $field = zget($this->config->action->objectNameFields, $type, '');
            if(empty($table) || empty($field)) continue;

            $sourcePairs[$type] = $this->dao->select('id,' . $field)->from($table)->where('id')->in($idList)->fetchPairs('id', $field);
        }

        return $sourcePairs;
    }

    /**
     * 获取文件图标。
     * Get file icon.
     *
     * @param  array  $files
     * @access public
     * @return array
     */
    public function getFileIcon($files)
    {
        $this->app->loadConfig('file');

        $fileIcon = array();
        foreach($files as $file)
        {
            if(in_array($file->extension, $this->config->file->imageExtensions)) continue;

            switch($file->extension)
            {
                case in_array($file->extension, array('zip', 'tar', 'gz', 'bz2', 'rar')):
                    $iconClass = 'icon-file-archive';
                    break;
                case in_array($file->extension, array('csv', 'xls', 'xlsx')):
                    $iconClass = 'icon-file-excel';
                    break;
                case in_array($file->extension, array('doc', 'docx')):
                    $iconClass = 'icon-file-word';
                    break;
                case in_array($file->extension, array('ppt', 'pptx')):
                    $iconClass = 'icon-file-powerpoint';
                    break;
                case in_array($file->extension, array('pdf')):
                    $iconClass = 'icon-file-pdf';
                    break;
                case in_array($file->extension, array('mp3', 'ogg', 'wav')):
                    $iconClass = 'icon-file-audio';
                    break;
                case in_array($file->extension, array('avi', 'mp4', 'mov')):
                    $iconClass = 'icon-file-video';
                    break;
                case in_array($file->extension, array('txt', 'md')):
                    $iconClass = 'icon-file-text';
                    break;
                case in_array($file->extension, array('html', 'htm')):
                    $iconClass = 'icon-file-globe';
                    break;
                default:
                    $iconClass = 'icon-file';
                    break;
            }

            $fileIcon[$file->id] = "<i class='file-icon icon $iconClass'></i>";
        }

        return $fileIcon;
    }

    /**
     * 获取文档的树形结构。
     * Get doc tree.
     *
     * @param  int    $libID
     * @access public
     * @return array
     */
    public function getDocTree($libID)
    {
        $docTrees = $this->loadModel('tree')->getTreeStructure($libID, 'doc');
        array_unshift($docTrees, array('id' => 0, 'name' => '/', 'type' => 'doc', 'actions' => false, 'root' => $libID));
        foreach($docTrees as $i => $tree)
        {
            $tree          = (object)$tree;
            $docTrees[$i] = $this->buildDocNode($tree, $libID);
        }
        if(empty($docTrees[0]->children)) array_shift($docTrees);
        return $docTrees;
    }

    /**
     * 构造文档节点。
     * Build doc node.
     *
     * @param  object $node
     * @param  int    $libID
     * @access public
     * @return object
     */
    public function buildDocNode($node, $libID)
    {
        $node = (object)$node;
        static $docGroups;
        if(empty($docGroups))
        {
            $docs      = $this->dao->select('*')->from(TABLE_DOC)->where('lib')->eq($libID)->andWhere('deleted')->eq(0)->fetchAll();
            $docGroups = array();
            foreach($docs as $doc)
            {
                if($this->checkPrivDoc($doc)) $docGroups[$doc->module][$doc->id] = $doc;
            }
        }

        if(!empty($node->children)) foreach($node->children as $i => $child) $node->children[$i] = $this->buildDocNode($child, $libID);
        if(!isset($node->id)) $node->id = 0;

        $node->type = 'module';
        $docs       = isset($docGroups[$node->id]) ? $docGroups[$node->id] : array();
        $menu       = !empty($node->children) ? $node->children : array();
        if(!empty($docs))
        {
            $docItems = array();
            foreach($docs as $doc)
            {
                $docItem        = new stdclass();
                $docItem->type  = 'doc';
                $docItem->id    = $doc->id;
                $docItem->title = $doc->title;
                $docItem->acl   = $doc->acl;
                $docItem->url   = helper::createLink('doc', 'view', "doc=$doc->id");

                $buttons = '';
                if(common::hasPriv('doc', 'edit'))   $buttons .= html::a(helper::createLink('doc', 'edit',   "docID={$doc->id}"), "<i class='icon icon-edit'></i>", 'hiddenwin', "class='btn-icon' title='{$this->lang->doc->edit}'");
                if(common::hasPriv('doc', 'delete')) $buttons .= html::a(helper::createLink('doc', 'delete', "docID={$doc->id}"), "<i class='icon icon-remove'></i>", 'hiddenwin', "class='btn-icon' title='{$this->lang->doc->delete}'");
                $docItem->buttons = $buttons;
                $docItem->actions = false;
                $docItems[]       = $docItem;
            }

            /* Reorder children. The doc is top of menu. */
            if($menu) $docItems = array_merge($docItems, $menu);
            $node->children = $docItems;
        }

        $node->docsCount = isset($node->children) ? count($node->children) : 0;
        $node->actions   = false;
        return $node;
    }

    /**
     * 获取文档的统计信息。
     * Get statistic information.
     *
     * @access public
     * @return object
     */
    public function getStatisticInfo()
    {
        $today     = date('Y-m-d');
        $statistic = new stdclass();
        $statistic->totalDocs = $this->dao->select('COUNT(1) AS count')->from(TABLE_DOC)
            ->where('deleted')->eq('0')
            ->andWhere('type')->in($this->config->doc->docTypes)
            ->andWhere('vision')->eq($this->config->vision)
            ->fetch('count');

        $statistic->todayEditedDocs = $this->dao->select('count(DISTINCT objectID) as count')->from(TABLE_ACTION)->alias('t1')
            ->leftJoin(TABLE_DOC)->alias('t2')->on("t1.objectID=t2.id and t1.objectType='doc'")
            ->where('t1.objectType')->eq('doc')
            ->andWhere('t1.action')->eq('edited')
            ->andWhere('t1.actor')->eq($this->app->user->account)
            ->andWhere('t1.vision')->eq($this->config->vision)
            ->andWhere('LEFT(t1.date, 10)')->eq($today)
            ->andWhere('t2.deleted')->eq(0)
            ->andWhere('t2.type')->in($this->config->doc->docTypes)
            ->fetch('count');

        $statistic->myEditedDocs = $this->dao->select('count(DISTINCT t1.objectID) as count')->from(TABLE_ACTION)->alias('t1')
            ->leftJoin(TABLE_DOC)->alias('t2')->on("t1.objectID=t2.id and t1.objectType='doc'")
            ->where('t1.objectType')->eq('doc')
            ->andWhere('t1.action')->eq('edited')
            ->andWhere('t1.actor')->eq($this->app->user->account)
            ->andWhere('t1.vision')->eq($this->config->vision)
            ->andWhere('t2.lib')->ne('')
            ->andWhere('t2.deleted')->eq(0)
            ->andWhere('t2.type')->in($this->config->doc->docTypes)
            ->fetch('count');

        $myStatistic = $this->dao->select("COUNT(1) AS myDocs, SUM(views) as docViews, SUM(collects) as docCollects")->from(TABLE_DOC)
            ->where('addedBy')->eq($this->app->user->account)
            ->andWhere('type')->in($this->config->doc->docTypes)
            ->andWhere('deleted')->eq(0)
            ->andWhere('vision')->eq($this->config->vision)
            ->andWhere('lib')->ne('')
            ->fetch();

        $statistic->myDocs = $myStatistic->myDocs;
        $statistic->myDoc  = new stdclass();
        $statistic->myDoc->docViews    = $myStatistic->docViews;
        $statistic->myDoc->docCollects = $myStatistic->docCollects;
        return $statistic;
    }

    /**
     * 获取收信人和抄送人列表。
     * Get toList and ccList.
     *
     * @param  object     $doc
     * @access public
     * @return bool|array
     */
    public function getToAndCcList($doc)
    {
        /* Set toList and ccList. */
        $toList = '';
        $ccList = str_replace(' ', '', trim($doc->mailto, ','));

        if(empty($ccList)) return false;
        if(strpos($ccList, ',') === false)
        {
            $toList = $ccList;
            $ccList = '';
        }
        else
        {
            $commaPos = strpos($ccList, ',');
            $toList   = substr($ccList, 0, $commaPos);
            $ccList   = substr($ccList, $commaPos + 1);
        }
        return array($toList, $ccList);
    }

    /**
     * 获取下拉菜单的链接。
     * Get the dropmenu link.
     *
     * @param  string $type     product|project
     * @param  int    $objectID
     * @access public
     * @return string
     */
    public function getDropMenuLink($type, $objectID)
    {
        if(!in_array($type, array('product', 'project'))) return '';

        $currentModule = $this->app->rawModule;
        $currentMethod = $this->app->rawMethod;
        if($currentModule == 'api' && $currentMethod == 'index')
        {
            $currentModule = 'doc';
            $currentMethod = $type . 'Space';
        }

        return helper::createLink('doc', 'ajaxGetDropMenu', "objectType={$type}&objectID={$objectID}&module={$currentModule}&method={$currentMethod}");
    }

    /**
     * 统计当前页面上文件的数量和大小。
     * Count the number and size of files on the current page.
     *
     * @param  array  $files
     * @access public
     * @return string
     */
    public function summary($files)
    {
        $filesCount       = 0;
        $sizeCount        = 0;
        $extensionCount   = array();
        $extensionSummary = '';
        foreach($files as $file)
        {
            if(!isset($extensionCount[$file->extension])) $extensionCount[$file->extension] = 0;

            $sizeCount += $file->size;

            $filesCount++;
            $extensionCount[$file->extension] ++;
        }

        /* Unit conversion. */
        $i = 0;
        while ($sizeCount > 1024 and $i <= 4)
        {
            $sizeCount = $sizeCount / 1024;
            $i++;
        }
        $unitList  = array('B', 'K', 'M', 'G', 'T');
        $sizeCount = round($sizeCount, 1) . $unitList[$i];

        /* Summary of each type. */
        foreach($extensionCount as $extension => $count)
        {
            if(in_array($this->app->getClientLang(), array('zh-cn', 'zh-tw')))
            {
                $extensionSummary .= $extension . ' ' . $count . $this->lang->doc->ge . $this->lang->doc->point;
            }
            else
            {
                $extensionSummary .= $extension . ' ' . $this->lang->doc->ge . ' ' . $count . $this->lang->doc->point;
            }
        }
        $extensionSummary = rtrim($extensionSummary, $this->lang->doc->point);

        return sprintf($this->lang->doc->summary, $filesCount, $sizeCount, $extensionSummary);
    }

    /**
     * 设置文档的导航。
     * Set doc menu by type.
     *
     * @param  string       $type     mine|project|execution|product|custom
     * @param  int          $objectID
     * @param  int          $libID
     * @param  int          $appendLib
     * @access public
     * @return array|string
     */
    public function setMenuByType($type, $objectID, $libID, $appendLib = 0)
    {
        if(empty($type))
        {
            $doclib   = $this->getLibByID($libID);
            $type     = $doclib->type == 'execution' ? 'project' : $doclib->type;
            $objectID = isset($doclib->{$type}) ? $doclib->{$type} : 0;
        }
        $isExecution    = in_array($this->app->tab, array('project', 'doc')) && $type == 'execution';
        $type           = $isExecution ? 'project' : $type;
        $objectDropdown = array('text' => '', 'link' => '');
        $appendObject   = $objectID;
        if(in_array($type, array('project', 'product', 'execution')))
        {
            $object = $this->dao->select('id,name,status,deleted' . ($isExecution ? ',project' : ''))->from($this->config->objectTables[$type])->where('id')->eq($objectID)->fetch();
            $objectID = $isExecution ? (int)$object->project : $objectID;
            if(empty($object)) return helper::createLink($type, $type == 'project' && $this->config->vision != 'lite' ? 'createGuide' : 'create', $type == 'project' && $this->config->vision == 'lite' ? 'model=kanban' : '');

            $this->loadModel($type);
            $objects  = $this->getOrderedObjects($type, 'merge', $objectID);
            $objectID = method_exists($this->$type, 'saveState') ? $this->{$type}->saveState($objectID, $objects) : $this->{$type}->checkAccess($objectID, $objects);
            $libs     = $this->getLibsByObject($type, $objectID, $appendLib);
            if(($libID == 0 || !isset($libs[$libID])) && !empty($libs)) $libID = reset($libs)->id;
            if($this->app->tab != 'doc' && isset($libs[$libID]))
            {
                $objectDropdown['text'] = zget($libs[$libID], 'name', '');
            }
            else
            {
                $objectDropdown['text'] = zget($objects, $objectID, '');
                $objectDropdown['link'] = $this->getDropMenuLink($type, $appendObject);
            }
        }
        else
        {
            if($type == 'custom')
            {
                $spaces = $this->getTeamSpaces();
                if(empty($objectID)) $objectID = (int)key($spaces);
                if(!isset($spaces[$objectID])) $objectID = (int)key($spaces);
            }
            $libs = $this->getLibsByObject($type, $objectID, $appendLib);
            if(($libID == 0 || !isset($libs[$libID])) && !empty($libs)) $libID = reset($libs)->id;
            if(isset($libs[$libID])) $objectDropdown['text'] = zget($libs[$libID], 'name', '');
            if($type == 'custom')
            {
                $objectDropdown['text'] = zget($spaces, $objectID, '');
                $objectDropdown['link'] = helper::createLink('doc', 'ajaxGetSpaceMenu', "libID={$objectID}&module={$this->app->rawModule}&method={$this->app->rawMethod}");
            }

            $object     = new stdclass();
            $object->id = 0;
        }

        $tab = strpos(',my,doc,product,project,execution,', ",{$this->app->tab},") !== false ? $this->app->tab : 'doc';
        if($type == 'mine')   $type = 'my';
        if($type == 'custom') $type = 'team';
        if($tab == 'doc' && !common::hasPriv('doc', $type . 'Space')) return helper::createLink('user', 'deny', "module=doc&method={$type}Space");
        if($tab != 'doc' && method_exists($type . 'Model', 'setMenu'))
        {
            $this->loadModel($type)->setMenu($objectID);
        }
        elseif($tab == 'doc' && isset($this->lang->doc->menu->{$type}['alias']))
        {
            $this->lang->doc->menu->{$type}['alias'] .= ',' . $this->app->rawMethod;
        }
        return array($libs, $libID, $object, $objectID, $objectDropdown);
    }

    /**
     * 通过搜索获取文档列表数据。
     * Get docs by search.
     *
     * @param  string $type     product|project|execution|custom|book
     * @param  int    $objectID
     * @param  int    $libID
     * @param  int    $queryID
     * @param  string $orderBy
     * @param  object $pager
     * @access public
     * @return array
     */
    public function getDocsBySearch($type, $objectID, $libID, $queryID, $orderBy = 'id_desc', $pager = null)
    {
        $query     = $this->buildQuery($type, $queryID);
        $libs      = $this->getLibsByObject($type, $objectID);
        $docIdList = $this->getPrivDocs(array_keys($libs));
        $docs      = $this->dao->select('*')->from(TABLE_DOC)
            ->where('deleted')->eq(0)
            ->andWhere($query)
            ->andWhere('lib')->in(array_keys($libs))
            ->andWhere('vision')->eq($this->config->vision)
            ->beginIF($this->config->doc->notArticleType)->andWhere('type')->notIN($this->config->doc->notArticleType)->fi()
            ->beginIF(!empty($docIdList))->andWhere('id')->in($docIdList)->fi()
            ->andWhere("(status = 'normal' or (status = 'draft' and addedBy='{$this->app->user->account}'))")
            ->orderBy($orderBy)
            ->page($pager)
            ->fetchAll('id');

        $this->loadModel('common')->saveQueryCondition($this->dao->get(), 'doc', true);

        return $this->processCollector($docs);
    }

    /**
     * Get module tree.
     *
     * @param  int    $rootID
     * @param  int    $moduleID
     * @param  string $type doc|api
     * @param  int    $parent
     * @param  array  $modules
     * @access public
     * @return array
     */
    public function getModuleTree($rootID, $moduleID = 0, $type = 'doc', $parent = 0, $modules = array(), $docID = 0)
    {
        if(is_array($modules) and empty($modules))
        {
            $modules = $this->dao->select('*')->from(TABLE_MODULE)
                ->where('root')->eq((int)$rootID)
                ->andWhere('type')->eq($type)
                ->andWhere('deleted')->eq(0)
                ->orderBy('grade desc, `order`')
                ->fetchAll('id');
        }

        $moduleTree = array();
        foreach($modules as $module)
        {
            if($module->parent != $parent) continue;
            unset($modules[$module->id]);

            $item = new stdclass();
            $item->id         = $module->id;
            $item->name       = $module->name;
            $item->objectType = $module->type;
            $item->type       = 'module';
            $item->libID      = $rootID;
            $item->active     = $module->id == $moduleID ? 1 : 0;
            $item->order      = $module->order;
            $item->children   = $this->getModuleTree($rootID, $moduleID, $type, $module->id, $modules, $docID);
            $showDoc = $this->loadModel('setting')->getItem('owner=' . $this->app->user->account . '&module=doc&key=showDoc');
            $showDoc = $showDoc === '0' ? 0 : 1;
            if($this->app->rawMethod == 'view' and $module->type != 'api' and $showDoc)
            {
                $docIDList = $this->getPrivDocs(array($rootID), $module->id);
                $docs      = $this->dao->select('*, title as name')->from(TABLE_DOC)
                    ->where('id')->in($docIDList)
                    ->andWhere('deleted')->eq(0)
                    ->fetchAll('id');
                if(!empty($docs))
                {
                    $docs = array_values($docs);
                    foreach($docs as $doc) $doc->active = $doc->id == $docID ? 1 : 0;
                    $item->children = array_merge($docs, $item->children);
                }
            }

            $moduleTree[$module->id] = $item;
        }

        return array_values($moduleTree);
    }

    /**
     * 获取文档库的树形结构。
     * Get lib tree.
     *
     * @param  int    $libID
     * @param  array  $libs
     * @param  string $type       mine|product|project|execution|api|custom
     * @param  int    $moduleID
     * @param  int    $objectID
     * @param  string $browseType bysearch|byrelease
     * @param  int    $param
     * @param  int    $docID
     * @access public
     * @return array
     */
    public function getLibTree($libID, $libs, $type, $moduleID, $objectID = 0, $browseType = '', $param = 0, $docID = 0)
    {
        if(common::isTutorialMode()) return $this->loadModel('tutorial')->getLibTree();

        $type = $this->app->tab == 'doc' && $type == 'execution' ? 'project' : $type;
        list($libTree, $apiLibs, $apiLibIDList) = $this->getObjectTree($libID, $libs, $type, $moduleID, $objectID, strtolower($browseType), $param, $docID);
        $libTree = $this->processObjectTree($libTree, $type, $libID, $objectID, $apiLibs, $apiLibIDList);

        if($type != 'project') $libTree = array_values($libTree[$type]);
        if($type == 'mine')
        {
            $children    = $libTree;
            $libType     = isset($this->app->rawParams['type']) ? $this->app->rawParams['type'] : '';
            $libType     = $this->app->rawMethod == 'view' ? 'mine' : $libType;
            $mineMethods = array('mine' => 'myLib', 'view' => 'myView', 'collect' => 'myCollection', 'createdBy' => 'myCreation', 'editedBy' => 'myEdited');
            $libTree     = array();
            foreach($mineMethods as $type => $mineMethod)
            {
                if($mineMethod != 'myLib' && !common::hasPriv('doc', $mineMethod)) continue;

                $myItem = new stdclass();
                $myItem->id         = 0;
                $myItem->name       = $type == 'mine' ? '' : $this->lang->doc->{$mineMethod};
                $myItem->type       = $type;
                $myItem->objectType = 'doc';
                $myItem->objectID   = 0;
                $myItem->hasAction  = false;
                $myItem->active     = strtolower($libType) == strtolower($type) ? 1 : 0;
                if($type == 'mine') $myItem->children = $children;

                $libTree[] = $myItem;
            }
        }

        return $libTree;
    }

    /**
     * 获取产品、项目、执行文档库的树形结构。
     * Get a tree structure of the product, project, and execution document library.
     *
     * @param  int    $libID
     * @param  array  $libs
     * @param  string $type       mine|product|project|execution|api|custom
     * @param  int    $moduleID
     * @param  int    $objectID
     * @param  string $browseType bysearch|byrelease
     * @param  int    $docID
     * @param  int    $param
     * @access public
     * @return array
     */
    public function getObjectTree($libID, $libs, $type, $moduleID = 0, $objectID = 0, $browseType = '', $param = 0, $docID = 0)
    {
        if($type == 'project')
        {
            $executionLibs = array();
            foreach($libs as $lib)
            {
                if($lib->type != 'execution') continue;
                $executionLibs[$lib->execution][$lib->id] = $lib;
            }

            $executionPairs = $this->dao->select('id,name')->from(TABLE_EXECUTION)->where('id')->in(array_keys($executionLibs))->fetchPairs();
        }

        $release = $browseType == 'byrelease' && $param ? $this->loadModel('api')->getRelease(0, 'byId', $param) : null;
        $libTree = array($type => array());
        $apiLibs = $apiLibIDList = array();
        foreach($libs as $lib)
        {
            $item = $this->buildLibItem($libID, $lib, $type, $moduleID, $objectID, $browseType, $docID, $release);
            if(($type == 'project' && $lib->type != 'execution') || $type != 'project')
            {
                if($item->type == 'docLib') $libTree[$lib->type][$lib->id] = $item;
                if($item->type == 'apiLib') $apiLibs[$lib->id] = $item;
            }
            else
            {
                $executionID = $lib->execution;
                if(empty($libTree['execution'][$executionID]))
                {
                    $execution = new stdclass();
                    $execution->id         = $executionID;
                    $execution->name       = zget($executionPairs, $executionID);
                    $execution->type       = 'execution';
                    $execution->active     = $item->active;
                    $execution->hasAction  = false;
                    $execution->objectType = 'execution';
                    $execution->children   = array();
                    if(count($executionLibs[$executionID]) == 1)
                    {
                        $execution->id        = $item->id;
                        $execution->type      = 'docLib';
                        $execution->hasAction = true;
                        $execution->children  = $item->children;
                    }

                    $libTree['execution'][$executionID] = $execution;
                    if(count($executionLibs[$executionID]) == 1) continue;
                }

                $libTree['execution'][$executionID]->active = $item->active ? 1 : $libTree['execution'][$executionID]->active;
                $libTree['execution'][$executionID]->children[] = $item;
            }

            if($item->type == 'apiLib') $apiLibIDList[] = $lib->id;
        }

        return array($libTree, $apiLibs, $apiLibIDList);
    }

    /**
     * 构建文档库树形结构的节点。
     * Build a node of the tree structure of the document library.
     *
     * @param  int              $libID
     * @param  object           $lib
     * @param  string           $type       mine|product|project|execution|api|custom
     * @param  int              $moduleID
     * @param  int              $objectID
     * @param  string           $browseType bysearch|byrelease
     * @param  int              $docID
     * @param  object|null|bool $release
     * @access public
     * @return object
     */
    public function buildLibItem($libID, $lib, $type, $moduleID = 0, $objectID = 0, $browseType = '', $docID = 0, $release = null)
    {
        $releaseModule = array();
        if($release && $release->lib == $lib->id)
        {
            foreach($release->snap['modules'] as $module) $releaseModule[$module['id']] = (object)$module;
        }

        $item = new stdclass();
        $item->id         = $lib->id;
        $item->type       = $lib->type == 'api' ? 'apiLib' : 'docLib';
        $item->name       = $lib->name;
        $item->parent     = isset($lib->parent) ? $lib->parent : '';
        $item->order      = $lib->order;
        $item->main       = zget($lib, 'main', 0);
        $item->objectType = $type;
        $item->objectID   = $objectID;
        $item->addedBy    = $lib->addedBy;
        $item->active     = $lib->id == $libID && $browseType != 'bysearch' ? 1 : 0;
        $item->children   = $this->getModuleTree($lib->id, $moduleID, $lib->type == 'api' ? 'api' : 'doc', 0, $releaseModule, $docID);

        $showDoc = $this->loadModel('setting')->getItem('owner=' . $this->app->user->account . '&module=doc&key=showDoc');
        $showDoc = $showDoc === '0' ? 0 : 1;
        if($this->app->rawMethod == 'view' && $lib->type != 'apiLib' && $showDoc && !($lib->type == 'custom' && $lib->parent == 0))
        {
            $docIDList = $this->getPrivDocs(array($lib->id));
            $docs      = $this->dao->select('*, title as name')->from(TABLE_DOC)
                ->where('id')->in($docIDList)
                ->andWhere("(status = 'normal' or (status = 'draft' && addedBy='{$this->app->user->account}'))")
                ->andWhere('deleted')->eq(0)
                ->andWhere('module')->eq(0)
                ->fetchAll('id');

            if(!empty($docs))
            {
                $docs = array_values($docs);
                foreach($docs as $doc) $doc->active = $doc->id == $docID ? 1 : 0;
                $item->children = array_merge($docs, $item->children);
            }
        }

        return $item;
    }

    /**
     * 处理产品、项目、执行的文档库树形结构。
     * Process the tree structure of the document library of product, project, and execution.
     *
     * @param  array  $libTree
     * @param  array  $apiLibs
     * @param  array  $apiLibIDList
     * @param  string $type         mine|product|project|execution|api|custom
     * @param  int    $libID
     * @param  int    $objectID
     * @access public
     * @return array
     */
    public function processObjectTree($libTree, $type, $libID = 0, $objectID = 0, $apiLibs = array(), $apiLibIDList = array())
    {
        if(in_array($type, array('product', 'project', 'execution')))
        {
            $libTree[$type] = array_merge($libTree[$type], $apiLibs);
            if($type == 'project' && !empty($libTree['execution'])) $libTree['execution'] = array_values($libTree['execution']);

            $annex = new stdclass();
            $annex->id         = 'annex';
            $annex->name       = $this->lang->doclib->files;
            $annex->type       = 'annex';
            $annex->objectType = $type;
            $annex->objectID   = $objectID;
            $annex->active     = empty($libID) ? 1 : 0;
            if($type == 'project')
            {
                $newLibTree = array();
                foreach($libTree as $key => $libs)
                {
                    $newLibTree[$key] = $libs;
                    if($key == 'project') $newLibTree['annex'][''] = $annex;
                }

                $libTree = $newLibTree;
            }
            else
            {
                $libTree[$type][''] = $annex;
            }
        }
        elseif($type == 'api')
        {
            $libTree[$type] = array_merge($libTree[$type], $apiLibs);
        }

        /* Add release for api. */
        $releases = $this->loadModel('api')->getReleaseByQuery($apiLibIDList);
        foreach($libTree as &$libList)
        {
            foreach($libList as &$lib)
            {
                if(!is_object($lib) || $lib->type != 'apiLib') continue;

                $lib->versions = array();
                foreach($releases as $index => $release)
                {
                    if($lib->id != $release->lib) continue;

                    $lib->versions[] = $release;
                    unset($releases[$index]);
                }

                if(empty($lib->versions)) continue;

                /* Set default version. */
                $defaultVersion = new stdclass();
                $defaultVersion->id      = 0;
                $defaultVersion->version = $this->lang->build->common;
                $lib->versions = array_merge(array($defaultVersion), $lib->versions);
            }
        }
        return $libTree;
    }

    /**
     * 检查接口库名称。
     * Check api library name.
     *
     * @param  object $lib
     * @param  string $libType product|project
     * @param  int    $libID
     * @access public
     * @return bool
     */
    public function checkApiLibName($lib, $libType, $libID = 0)
    {
        $sameNames = $this->dao->select('*')
            ->from(TABLE_DOCLIB)
            ->where('`product`')->eq($lib->product)
            ->andWhere('`project`')->eq($lib->project)
            ->andWhere('`name`')->eq($lib->name)
            ->andWhere('`type`')->eq('api')
            ->beginIF(!empty($libID))->andWhere('`id`')->ne($libID)->fi()
            ->fetchAll();

        if(count($sameNames) > 0 && $libType == 'product') dao::$errors['name'] = $this->lang->doclib->apiNameUnique[$libType] . sprintf($this->lang->error->unique, $this->lang->doclib->name, $lib->name);
        if(count($sameNames) > 0 && $libType == 'project') dao::$errors['name'] = $this->lang->doclib->apiNameUnique[$libType] . sprintf($this->lang->error->unique, $this->lang->doclib->name, $lib->name);
        if(count($sameNames) > 0 && $libType == 'nolink')  dao::$errors['name'] = $this->lang->doclib->apiNameUnique[$libType] . sprintf($this->lang->error->unique, $this->lang->doclib->name, $lib->name);

        return !dao::isError();
    }

    /**
     * 创建一个操作。
     * Create an action.
     *
     * @param  int    $docID
     * @param  string $action   view|collect
     * @param  string $account
     * @access public
     * @return int|bool
     */
    public function createAction($docID, $action, $account = '')
    {
        if(empty($docID)) return false;

        $docStatus = $this->dao->select('status')->from(TABLE_DOC)->where('id')->eq($docID)->fetch('status');

        if(empty($account))$account = $this->app->user->account;
        if($action == 'collect') $this->dao->delete()->from(TABLE_DOCACTION)->where('doc')->eq($docID)->andWhere('action')->eq('collect')->andWhere('actor')->eq($account)->exec();

        if($action == 'view')
        {
            $lastView = $this->dao->select('date')->from(TABLE_DOCACTION)->where('doc')->eq($docID)->andWhere('action')->eq('view')->andWhere('actor')->eq($account)->orderBy('id_desc')->fetch('date');
            if($lastView and (time() - strtotime($lastView) < 4)) return false;
        }

        $data  = new stdclass();
        $data->doc    = $docID;
        $data->action = $action;
        $data->actor  = $account;
        $data->date   = helper::now();
        $this->dao->insert(TABLE_DOCACTION)->data($data)->autoCheck()->exec();
        if(dao::isError()) return false;

        $actionID = $this->dao->lastInsertID();
        if($action == 'view' && $docStatus == 'normal') $this->dao->update(TABLE_DOC)->set('views = views + 1')->where('id')->eq($docID)->exec();
        if($action == 'collect')
        {
            $collectCount = $this->dao->select('COUNT(1) AS count')->from(TABLE_DOCACTION)->where('doc')->eq($docID)->andWhere('action')->eq('collect')->fetch('count');
            $this->dao->update(TABLE_DOC)->set('collects')->eq($collectCount)->where('id')->eq($docID)->exec();
        }

        return $actionID;
    }

    /**
     * 获取文档的所有操作信息。
     * Get action by doc ID.
     *
     * @param  int          $docID
     * @param  string       $action    view|collect
     * @param  string       $account
     * @access public
     * @return object|false
     */
    public function getActionByObject($docID, $action, $account = '')
    {
        if(empty($account)) $account = $this->app->user->account;
        return $this->dao->select('*')->from(TABLE_DOCACTION)->where('doc')->eq($docID)->andWhere('action')->eq($action)->andWhere('actor')->eq($account)->fetch();
    }

    /**
     * 删除一个动作。
     * Delete an action.
     *
     * @param  int    $actionID
     * @access public
     * @return bool
     */
    public function deleteAction($actionID)
    {
        $action = $this->dao->select('*')->from(TABLE_DOCACTION)->where('id')->eq($actionID)->fetch();
        if(!$action) return false;

        $this->dao->delete()->from(TABLE_DOCACTION)->where('id')->eq($actionID)->exec();
        if($action->action == 'collect')
        {
            $collectCount = $this->dao->select('COUNT(1) AS count')->from(TABLE_DOCACTION)->where('doc')->eq($action->doc)->andWhere('action')->eq('collect')->fetch('count');
            $this->dao->update(TABLE_DOC)->set('collects')->eq($collectCount)->where('id')->eq($action->doc)->exec();
        }
        return !dao::isError();
    }

    /**
     * 处理文档的收藏者信息。
     * Process collector to account.
     *
     * @param  array    $docs
     * @access public
     * @return array
     */
    public function processCollector($docs)
    {
        $actionGroup = $this->dao->select('*')->from(TABLE_DOCACTION)->where('doc')->in(array_keys($docs))->andWhere('action')->eq('collect')->fetchGroup('doc', 'actor');
        foreach($docs as $docID => $doc)
        {
            $doc->collector = '';
            if(isset($actionGroup[$docID])) $doc->collector = ',' . implode(',', array_keys($actionGroup[$docID])) . ',';
        }
        return $docs;
    }

    /**
     * 检查文档是否正在被其他人编辑。
     * Check other editing.
     *
     * @param  int    $docID
     * @access public
     * @return bool
     */
    public function checkOtherEditing($docID)
    {
        $now     = time();
        $account = $this->app->user->account;
        $docID   = (int)$docID;
        $doc     = $this->dao->select('id,editingDate')->from(TABLE_DOC)->where('id')->eq($docID)->fetch();
        if(empty($doc)) return false;

        $editingDate  = $doc->editingDate ? json_decode($doc->editingDate, true) : array();
        $otherEditing = false;
        foreach($editingDate as $editingAccount => $timestamp)
        {
            if($editingAccount != $account && ($now - $timestamp) <= $this->config->doc->saveDraftInterval)
            {
                $otherEditing = true;
                break;
            }
        }

        $editingDate[$account] = $now;
        $this->dao->update(TABLE_DOC)->set('editingDate')->eq(json_encode($editingDate))->where('id')->eq($docID)->exec();

        return $otherEditing;
    }

    /**
     * 获取文档动态。
     * Get document dynamic.
     *
     * @param  object $pager
     * @access public
     * @return array
     */
    public function getDynamic($pager = null)
    {
        $allLibs          = $this->getLibs('hasApi');
        $hasPrivDocIdList = $this->getPrivDocs(array(), 0, 'all');
        $apiList          = $this->loadModel('api')->getPrivApis();

        $actions = $this->dao->select('*')->from(TABLE_ACTION)
            ->where('vision')->eq($this->config->vision)
            ->andWhere('((objectType')->eq('doclib')
            ->andWhere('objectID')->in(array_keys($allLibs))
            ->markRight(1)
            ->orWhere('(objectType')->eq('doc')
            ->andWhere('objectID')->in($hasPrivDocIdList)
            ->markRight(1)
            ->orWhere('(objectType')->eq('api')
            ->andWhere('objectID')->in(array_keys($apiList))
            ->markRight(2)
            ->orderBy('date_desc,id_asc')
            ->page($pager)
            ->fetchAll();

        return $this->loadModel('action')->transformActions($actions);
    }

    /**
     * 将当前用户从文档的正在编辑者列表中移除。
     * Removes the current user from the list of people editing the document.
     *
     * @param  object|bool $doc
     * @access public
     * @return bool
     */
    public function removeEditing($doc)
    {
        if(empty($doc->id) || empty($doc->editingDate)) return false;

        $account     = $this->app->user->account;
        $editingDate = json_decode($doc->editingDate, true);
        if(!isset($editingDate[$account])) return false;

        unset($editingDate[$account]);
        $this->dao->update(TABLE_DOC)->set('editingDate')->eq(json_encode($editingDate))->where('id')->eq($doc->id)->exec();

        return !dao::isError();
    }

    /**
     * 获取编辑过文档的用户列表。
     * Get editors of a document.
     *
     * @param  int    $docID
     * @access public
     * @return array
     */
    public function getEditors($docID = 0)
    {
        if(!$docID) return array();
        $actions = $this->dao->select('*')->from(TABLE_ACTION)
            ->where('objectType')->eq('doc')
            ->andWhere('objectID')->eq($docID)
            ->andWhere('action')->in('edited')
            ->orderBy('date_desc')
            ->fetchAll('id');

        $editors = array();
        foreach($actions as $action)
        {
            $editor = new stdclass();
            $editor->account = $action->actor;
            $editor->date    = $action->date;

            $editors[] = $editor;
        }

        return $editors;
    }

    /**
     * 构造搜索表单。
     * Build search form.
     *
     * @param int $libID
     * @param  array   $libs
     * @param  int     $queryID
     * @param  string  $actionURL
     * @param  string  $type       mine|product|project|execution|custom
     * @access public
     * @return void
     */
    public function buildSearchForm($libID, $libs, $queryID, $actionURL, $type)
    {
        $this->loadModel('product');
        if($this->app->rawMethod == 'contribute')
        {
            $this->config->doc->search['module'] = 'contributeDoc';
            $this->config->doc->search['params']['project']['values']   = $this->loadModel('project')->getPairsByProgram(0, 'all', false, 'order_asc') + array('all' => $this->lang->doc->allProjects);
            $this->config->doc->search['params']['execution']['values'] = $this->loadModel('execution')->getPairs(0, 'all', 'multiple,leaf,noprefix,withobject') + array('all' => $this->lang->doc->allExecutions);
            $this->config->doc->search['params']['lib']['values']       = $this->loadModel('doc')->getLibs('all', 'withObject') + array('all' => $this->lang->doclib->all);
            $this->config->doc->search['params']['product']['values']   = $this->product->getPairs() + array('all' => $this->lang->doc->allProduct);

            unset($this->config->doc->search['fields']['module'], $this->config->doc->search['params']['module']);
        }
        else
        {
            if(!isset($libs[$libID])) $libs[$libID] = $this->getLibByID($libID);

            $libPairs  = array();
            $queryName = $type . 'libDoc';
            foreach($libs as $lib)
            {
                if(empty($lib)) continue;
                if($lib->type == 'api') continue;
                $libPairs[$lib->id] = $lib->name;
            }

            if($type == 'project')
            {
                $this->config->doc->search['params']['execution']['values'] = $this->loadModel('execution')->getPairs((int)$this->session->project, 'all', 'multiple,leaf,noprefix') + array('all' => $this->lang->doc->allExecutions);
            }
            else
            {
                if($type == 'mine' || $type == 'createdby')
                {
                    unset($this->config->doc->search['fields']['addedBy'], $this->config->doc->search['params']['addedBy']);
                    if($type == 'mine') unset($this->config->doc->search['fields']['editedBy'], $this->config->doc->search['params']['editedBy']);
                }
                unset($this->config->doc->search['fields']['execution'], $this->config->doc->search['params']['execution']);
            }

            if(in_array($type, array('view', 'collect', 'createdby', 'editedby'))) $libPairs = $this->getLibs('all', 'withObject');

            $this->config->doc->search['module'] = $queryName;
            $this->config->doc->search['params']['lib']['values'] = $libPairs + array('all' => $this->lang->doclib->all);
            unset($this->config->doc->search['fields']['product'], $this->config->doc->search['params']['product']);
            unset($this->config->doc->search['fields']['module'], $this->config->doc->search['params']['module']);
        }

        unset($this->config->doc->search['params']['status']['values']['']);
        $this->config->doc->search['actionURL'] = $actionURL;
        $this->config->doc->search['queryID']   = $queryID;

        $this->loadModel('search')->setSearchParams($this->config->doc->search);
    }

    /**
     * 删除附件。
     * Delete files.
     *
     * @param  array  $idList
     * @access public
     * @return bool
     */
    public function deleteFiles($idList)
    {
        if(empty($idList)) return true;

        $this->dao->update(TABLE_FILE)->set('deleted')->eq('1')->where('id')->in($idList)->exec();
        return !dao::isError();
    }

    /**
     * 通过ID获取产品/项目/执行的信息。
     * Get product/project/execution by ID.
     *
     * @param  string       $type     product|project|execution
     * @param  int          $objectID
     * @access public
     * @return object|false
     */
    public function getObjectByID($type, $objectID)
    {
        $table = zget($this->config->objectTables, $type, '');
        if(!$table) return false;

        return $this->dao->select('*')->from($table)->where('id')->eq($objectID)->fetch();
    }

    /**
     * 更新文档库顺序。
     * Update doclib order.
     *
     * @param  int    $catalogID
     * @param  int    $order
     * @access public
     * @return bool
     * @param int $id
     */
    public function updateDoclibOrder($id, $order)
    {
        $this->dao->update(TABLE_DOCLIB)->set('`order`')->eq($order)->where('id')->eq($id)->exec();

        return !dao::isError();
    }

    /**
     * 更新文档顺序。
     * Update doc order.
     *
     * @param  array $sortedIdList
     * @access public
     * @return void
     */
    public function updateDocOrder($sortedIdList)
    {
        /* Remove programID. */
        $sortedIdList = array_values(array_filter(array_map(function($id){return (is_numeric($id) and $id > 0) ? $id : null;}, $sortedIdList)));
        if(empty($sortedIdList)) return;

        $docs = $this->dao->select('`order`, id')->from(TABLE_DOC)->where('id')->in($sortedIdList)->orderBy('order_asc')->fetchPairs('order', 'id');

        /* Update order by sorted id list. */
        foreach($docs as $order => $id)
        {
            $newID = array_shift($sortedIdList);
            if($id == $newID) continue;
            $this->dao->update(TABLE_DOC)->set('`order`')->eq($order)->where('id')->eq($newID)->exec();
        }
    }

    /**
     * 更新目录顺序。
     * Update catalog order.
     *
     * @param  int    $catalogID
     * @param  int    $order
     * @param  string $order
     * @access public
     * @return bool
     * @param string $type
     */
    public function updateOrder($catalogID, $order, $type = 'doc')
    {
        $this->dao->update(TABLE_MODULE)->set('`order`')->eq($order)->where('id')->eq($catalogID)->andWhere('type')->eq($type)->exec();

        return !dao::isError();
    }

    /**
     * 更新文档中的附件信息。
     * Update doc file.
     *
     * @param  int    $docID
     * @param  int    $fileID
     * @access public
     * @return bool
     */
    public function updateDocFile($docID, $fileID)
    {
        $docContent = $this->dao->select('t1.*')->from(TABLE_DOCCONTENT)->alias('t1')
            ->leftJoin(TABLE_DOC)->alias('t2')->on('t1.doc=t2.id and t1.version=t2.version')
            ->where('t2.id')->eq($docID)
            ->fetch();

        unset($docContent->id);
        $docContent->files    = trim(str_replace(",{$fileID},", ',', ",{$docContent->files},"), ',');
        $docContent->version += 1;
        $this->dao->insert(TABLE_DOCCONTENT)->data($docContent)->exec();
        $this->dao->update(TABLE_DOC)->set('version')->eq($docContent->version)->where('id')->eq($docID)->exec();

        return !dao::isError();
    }

    /**
     * 移动文档库。
     * Move library.
     *
     * @param  int    $docID
     * @param  object $data
     * @access public
     * @return bool
     * @param int $libID
     */
    public function moveLib($libID, $data)
    {
        if(empty($libID) || empty($data->space)) return false;
        if(is_numeric($data->space))
        {
            $data->type   = 'custom';
            $data->parent = $data->space;
        }
        elseif($data->space == 'mine')
        {
            $data->type    = 'mine';
            $data->parent  = 0;
            $data->addedBy = $this->app->user->account;
        }
        else
        {
            return false;
        }

        $lib     = $this->getLibByID($libID);
        $changes = common::createChanges($lib, $data);
        if(empty($changes)) return false;

        unset($data->space);
        $this->dao->update(TABLE_DOCLIB)->data($data)->where('id')->eq($libID)->exec();

        $actionID = $this->loadModel('action')->create('docLib', $libID, 'Moved', '', json_encode(array('from' => $lib->type == 'mine' ? 'mine' : $lib->parent, 'to' => $data->type == 'mine' ? 'mine' : $data->parent)));
        $this->action->logHistory($actionID, $changes);

        /* 从团队空间移动到我的空间，需要保留其他人创建的文档在团队空间。 */
        if($data->type == 'mine') $this->reserveOthersDoc($lib);

        return true;
    }

    /**
     * 迁移文档库时保留其他人创建的文档。
     * Reserve other people's documents when migrating document libraries.
     *
     * @param  object $lib
     * @access public
     * @return void
     */
    public function reserveOthersDoc($lib)
    {
        $othersDocList = $this->dao->select('id, module')->from(TABLE_DOC)
            ->where('lib')->eq($lib->id)
            ->andWhere('addedBy')->ne($this->app->user->account)
            ->andWhere('deleted')->eq('0')
            ->fetchPairs();

        if($othersDocList)
        {
            unset($lib->id);
            $this->dao->insert(TABLE_DOCLIB)->data($lib)->exec();
            $newLibID = $this->dao->lastInsertID();

            $this->dao->update(TABLE_DOC)->set('lib')->eq($newLibID)->where('id')->in(array_keys($othersDocList))->exec();

            $modulePairs  = $this->dao->select('id, path')->from(TABLE_MODULE)->where('id')->in($othersDocList)->andWhere('deleted')->eq('0')->fetchPairs();
            $moduleIdList = array();
            foreach($modulePairs as $path)
            {
                $paths = explode(',', trim($path, ','));
                foreach($paths as $id)
                {
                    if($id > 0) $moduleIdList[$id] = $id;
                }
            }

            $mapList = array();
            $idMap   = array();
            $modules = $this->dao->select('*')->from(TABLE_MODULE)->where('id')->in($moduleIdList)->andWhere('deleted')->eq('0')->fetchAll('id');
            foreach($modules as $module)
            {
                $oldID = $module->id;
                unset($module->id);
                $module->root = $newLibID;
                $this->dao->insert(TABLE_MODULE)->data($module)->exec();

                $newID = $this->dao->lastInsertID();

                $module->id    = $newID;
                $mapList[]     = $module;
                $idMap[$oldID] = $newID;

                $this->dao->update(TABLE_DOC)->set('module')->eq($newID)->where('module')->eq($oldID)->andWhere('lib')->eq($newLibID)->exec();
            }

            foreach($mapList as $module)
            {
                $parent = $module->parent ? $idMap[$module->parent] : 0;
                $paths  = explode(',', trim($module->path, ','));
                foreach($paths as $key => $path)
                {
                    if($path) $paths[$key] = $idMap[$path];
                }
                $path = implode(',', $paths);
                $this->dao->update(TABLE_MODULE)->set('parent')->eq($parent)->set('path')->eq($path)->where('id')->eq($module->id)->exec();
            }
        }
    }

    /**
     * 判断文档库下是否有其他人创建的文档。
     * Check if there are other documents created under the document library.
     *
     * @param  object $lib
     * @access public
     * @return bool
     */
    public function hasOthersDoc($lib)
    {
        if($lib->type != 'custom') return false;

        $docID = $this->dao->select('id')->from(TABLE_DOC)
            ->where('vision')->eq($this->config->vision)
            ->andWhere('templateType')->eq('')
            ->andWhere('deleted')->eq(0)
            ->andWhere('lib')->eq($lib->id)
            ->andWhere('addedBy')->ne($this->app->user->account)
            ->fetch('id');

        return !empty($docID);
    }
}
