<?php
define('ALIBERR_NOTGR', 20);
define('ALIBERR_BADSMEMB', 21);

/**
 * Subj class
 *
 * users + groups
 * with "linearized recursive membership" ;)
 *   (allow adding users to groups or groups to groups)
 *
 * @package Campcaster
 * @subpackage Alib
 * @copyright 2010 Sourcefabric O.P.S.
 * @license http://www.gnu.org/licenses/gpl.txt
 * @see ObjClasses
 * @see Alib
 */
class Subjects {

    /* ======================================================= public methods */

    /**
     * Add new subject (a.k.a. "user")
     *
     * @param string $p_login
     * @param string $p_pass
     * @param string $p_realname
     * @param boolean $p_passenc
     * 		password already encrypted if true
     * @return int|PEAR_Error
     */
    public static function AddSubj($p_login, $p_pass=NULL, $p_realname='', $p_passenc=FALSE)
    {
        global $CC_CONFIG, $CC_DBC;
        if (!$p_login) {
            return $CC_DBC->raiseError("Subjects::AddSubj: empty login");
        }
        $id = $CC_DBC->nextId($CC_CONFIG['subjSequence']);
        if (PEAR::isError($id)) {
            return $id;
        }
        if (!is_null($p_pass) && !$p_passenc) {
            $p_pass = md5($p_pass);
        }
        $sql = "INSERT INTO ".$CC_CONFIG['subjTable']." (id, login, pass, type, realname)"
            ." VALUES ($id, '$p_login', ".
                (is_null($p_pass) ? "'!', 'G'" : "'$p_pass', 'U'").",
                '$p_realname')";
        $r = $CC_DBC->query($sql);
        if (PEAR::isError($r)) {
            return $r;
        }
        return $id;
    }


    /**
     * Remove subject by uid or by login
     *
     * @param string $login
     * @param int $uid
     * @return boolean|PEAR_Error
     */
    public static function RemoveSubj($login, $uid=NULL)
    {
        global $CC_CONFIG, $CC_DBC;
        if (is_null($uid)) {
            $uid = Subjects::GetSubjId($login);
        }
        if (PEAR::isError($uid)) {
            return $uid;
        }
        $sql = "DELETE FROM ".$CC_CONFIG['smembTable']
            ." WHERE (uid='$uid' OR gid='$uid') AND mid is null";
        $r = $CC_DBC->query($sql);
        if (PEAR::isError($r)) {
            return $r;
        }
        $sql2 = "DELETE FROM ".$CC_CONFIG['subjTable']
            ." WHERE login='$login'";
        $r = $CC_DBC->query($sql2);
        if (PEAR::isError($r)) {
            return $r;
        }
        return Subjects::_rebuildRels();
    } // fn removeSubj


    /**
     * Check login and password
     *
     * @param string $login
     * @param string $pass
     * 		optional
     * @return boolean|int|PEAR_Error
     */
    public static function Authenticate($login, $pass='')
    {
        global $CC_CONFIG, $CC_DBC;
        $cpass = md5($pass);
        $sql = "SELECT id FROM ".$CC_CONFIG['subjTable']
            ." WHERE login='$login' AND pass='$cpass' AND type='U'";
        $id = $CC_DBC->getOne($sql);
        if (PEAR::isError($id)) {
            return $id;
        }
        return (is_null($id) ? FALSE : $id);
    } // fn authenticate


    /**
     * Set lastlogin or lastfail timestamp
     *
     * @param string $login
     * @param boolean $failed
     * 		true=> set lastfail, false=> set lastlogin
     * @return boolean|int|PEAR_Error
     */
    public static function SetTimeStamp($login, $failed=FALSE)
    {
        global $CC_CONFIG, $CC_DBC;
        $fld = ($failed ? 'lastfail' : 'lastlogin');
        $sql = "UPDATE ".$CC_CONFIG['subjTable']." SET $fld=now()"
            ." WHERE login='$login'";
        $r = $CC_DBC->query($sql);
        if (PEAR::isError($r)) {
            return $r;
        }
        return TRUE;
    } // fn setTimeStamp


    /**
     * Change user password
     *
     * @param string $login
     * @param string $oldpass
     * 		old password (optional for 'superuser mode')
     * @param string $pass
     * 		optional
     * @param boolean $passenc
     * 		optional, password already encrypted if true
     * @return boolean|PEAR_Error
     */
    public static function Passwd($login, $oldpass=null, $pass='', $passenc=FALSE)
    {
        global $CC_CONFIG, $CC_DBC;
        if (!$passenc) {
            $cpass = md5($pass);
        } else {
            $cpass = $pass;
        }
        if (!is_null($oldpass)) {
            $oldcpass = md5($oldpass);
            $oldpCond = "AND pass='$oldcpass'";
        } else {
            $oldpCond = '';
        }
        $sql = "UPDATE ".$CC_CONFIG['subjTable']." SET pass='$cpass'"
            ." WHERE login='$login' $oldpCond AND type='U'";
        $r = $CC_DBC->query($sql);
        if (PEAR::isError($r)) {
            return $r;
        }
        return TRUE;
    } // fn passwd


    /* --------------------------------------------------------------- groups */

    /**
     * Add {login} and direct/indirect members to {gname} and to groups,
     * where {gname} is [in]direct member
     *
     * @param string $login
     * @param string $gname
     * @return int|PEAR_Error
     */
    public static function AddSubjectToGroup($login, $gname)
    {
        $uid = Subjects::GetSubjId($login);
        if (PEAR::isError($uid)) {
            return $uid;
        }
        $gid = Subjects::GetSubjId($gname);
        if (PEAR::isError($gid)) {
            return $gid;
        }
        $isgr = Subjects::IsGroup($gid);
        if (PEAR::isError($isgr)) {
            return $isgr;
        }
        if (!$isgr) {
            return PEAR::raiseError("Subjects::addSubj2Gr: Not a group ($gname)", ALIBERR_NOTGR);
        }
        // add subject and all [in]direct members to group $gname:
        $mid = Subjects::_plainAddSubjectToGroup($uid, $gid);
        if (PEAR::isError($mid)) {
            return $mid;
        }
        // add it to all groups where $gname is [in]direct member:
        $marr = Subjects::_listRMemb($gid);
        if (PEAR::isError($marr)) {
            return $marr;
        }
        foreach ($marr as $k => $v) {
            $r = Subjects::_plainAddSubjectToGroup(
                $uid, $v['gid'], intval($v['level'])+1, $v['id']);
            if (PEAR::isError($r)) {
                return $r;
            }
        }
        return $mid;
    } // fn addSubj2Gr


    /**
     * Remove subject from group
     *
     * @param string $login
     * @param string $gname
     * @return boolean|PEAR_Error
     */
    public static function RemoveSubjectFromGroup($login, $gname)
    {
        global $CC_CONFIG, $CC_DBC;
        $uid = Subjects::GetSubjId($login);
        if (PEAR::isError($uid)) {
            return $uid;
        }
        $gid = Subjects::GetSubjId($gname);
        if (PEAR::isError($gid)) {
            return $gid;
        }
        $sql = "SELECT id FROM ".$CC_CONFIG['smembTable']
            ." WHERE uid='$uid' AND gid='$gid' AND mid is null";
        $mid = $CC_DBC->getOne($sql);
        if (is_null($mid)) {
            return FALSE;
        }
        if (PEAR::isError($mid)) {
            return $mid;
        }
        // remove it:
        $r = Subjects::_removeMemb($mid);
        if (PEAR::isError($r)) {
            return $r;
        }
        // and rebuild indirect memberships:
        $r = Subjects::_rebuildRels();
        if (PEAR::isError($r)) {
            return $r;
        }
        return TRUE;
    } // fn removeSubjFromGr


    /* --------------------------------------------------------- info methods */

    /**
     * Get subject id from login
     *
     * @param string $login
     * @return int|PEAR_Error
     */
    public static function GetSubjId($login)
    {
        global $CC_CONFIG;
        global $CC_DBC;
        $sql = "SELECT id FROM ".$CC_CONFIG['subjTable']
            ." WHERE login='$login'";
        return $CC_DBC->getOne($sql);
    } // fn getSubjId


    /**
     * Get subject name (login) from id
     *
     * @param int $id
     * @param string $fld
     * @return string|PEAR_Error
     */
    public static function GetSubjName($id, $fld='login')
    {
        global $CC_CONFIG;
        global $CC_DBC;
        $sql = "SELECT $fld FROM ".$CC_CONFIG['subjTable']
            ." WHERE id='$id'";
        return $CC_DBC->getOne($sql);
    } // fn getSubjName


    /**
     * Get one subject from the table.
     *
     * @param string $p_fieldValue
     * @param string $p_fieldName
     * @return array
     */
    public static function GetSubject($p_fieldValue, $p_fieldName='login')
    {
        global $CC_CONFIG, $CC_DBC;
        if (!in_array($p_fieldName, array("login", "id"))) {
            return null;
        }
        $escapedValue = pg_escape_string($p_fieldValue);
        $sql = "SELECT * FROM ".$CC_CONFIG['subjTable']
            ." WHERE $p_fieldName='$escapedValue'";
        $row = $CC_DBC->GetRow($sql);
        return $row;
    }


    /**
     * Get all subjects
     *
     * @param string $flds
     * @return array|PEAR_Error
     */
    public static function GetSubjects($flds='id, login')
    {
        global $CC_CONFIG, $CC_DBC;
        $sql = "SELECT $flds FROM ".$CC_CONFIG['subjTable'];
        return $CC_DBC->getAll($sql);
    } // fn getSubjects


    /**
     * Get subjects with count of direct members
     *
     * @return array|PEAR_Error
     */
    public static function GetSubjectsWCnt()
    {
        global $CC_CONFIG, $CC_DBC;
        $sql = "SELECT count(m.uid)as cnt, s.id, s.login, s.type"
            ." FROM ".$CC_CONFIG['subjTable']." s"
            ." LEFT JOIN ".$CC_CONFIG['smembTable']." m ON m.gid=s.id"
            ." WHERE m.mid is null"
            ." GROUP BY s.id, s.login, s.type"
            ." ORDER BY s.id";
        return $CC_DBC->getAll($sql);
    } // fn getSubjectsWCnt


    /**
     * Return true if subject is a group
     *
     * @param int $gid
     * @return boolean|PEAR_Error
     */
    public static function IsGroup($gid)
    {
        global $CC_CONFIG, $CC_DBC;
        if (empty($gid)) {
            return FALSE;
        }
        $sql = "SELECT type FROM ".$CC_CONFIG['subjTable']
            ." WHERE id='$gid'";
        $r = $CC_DBC->getOne($sql);
        if (PEAR::isError($r)) {
            return $r;
        }
        return ($r === 'G');
    } // fn isGroup


    /**
     * List direct members of group
     *
     * @param int $gid
     * @return array|PEAR_Error
     */
    public static function ListGroup($gid)
    {
        global $CC_CONFIG, $CC_DBC;
        $sql = "SELECT s.id, s.login, s.type"
            ." FROM ".$CC_CONFIG['smembTable']." m, ".$CC_CONFIG['subjTable']." s"
            ." WHERE m.uid=s.id AND m.mid is null AND m.gid='$gid'";
        return $CC_DBC->getAll($sql);
    } // fn listGroup


    /**
     * Return true if uid is [id]direct member of gid
     *
     * @param int $uid
     * 		local user id
     * @param int $gid
     * 		local group id
     * @return boolean
     */
    public static function IsMemberOf($uid, $gid)
    {
        global $CC_CONFIG, $CC_DBC;
        $sql = "SELECT count(*)as cnt"
            ." FROM ".$CC_CONFIG['smembTable']
            ." WHERE uid='$uid' AND gid='$gid'";
        $res = $CC_DBC->getOne($sql);
        if (PEAR::isError($res)) {
            return $res;
        }
        return (intval($res) > 0);
    } // fn isMemberOf


    /* ==================================================== "private" methods */

    /**
     * Create membership record
     *
     * @param int $uid
     * @param int $gid
     * @param int $level
     * @param int $mid
     * @return int|PEAR_Error
     */
    private static function _addMemb($uid, $gid, $level=0, $mid='null')
    {
        global $CC_CONFIG, $CC_DBC;
        if ($uid == $gid) {
            return PEAR::raiseError("Subjects::_addMemb: uid==gid ($uid)", ALIBERR_BADSMEMB);
        }
        $sql = "SELECT id, level, mid FROM ".$CC_CONFIG['smembTable']
            ." WHERE uid='$uid' AND gid='$gid' ORDER BY level ASC";
        $a = $CC_DBC->getAll($sql);
        if (PEAR::isError($a)) {
            return $a;
        }
        if (count($a) > 0) {
            $a0 = $a[0];
            $id = $a0['id'];
            if ($level < intval($a0['level'])){
                $sql2 = "UPDATE ".$CC_CONFIG['smembTable']
                    ." SET level='$level', mid=$mid WHERE id='{$a0['id']}'";
                $r = $CC_DBC->query($sql2);
                if (PEAR::isError($r)) {
                    return $r;
                }
            }
        } else {
            $id = $CC_DBC->nextId($CC_CONFIG['smembSequence']);
            if (PEAR::isError($id)) {
                return $id;
            }
            $sql3 = "INSERT INTO ".$CC_CONFIG['smembTable']." (id, uid, gid, level, mid)"
                ." VALUES ($id, $uid, $gid, $level, $mid)";
            $r = $CC_DBC->query($sql3);
            if (PEAR::isError($r)) {
                return $r;
            }
        }
        return $id;
    } // fn _addMemb


    /**
     * Remove membership record
     *
     * @param int $mid
     * @return null|PEAR_Error
     */
    private static function _removeMemb($mid)
    {
        global $CC_CONFIG, $CC_DBC;
        $sql = "DELETE FROM ".$CC_CONFIG['smembTable']
            ." WHERE id='$mid'";
        return $CC_DBC->query($sql);
    } // fn _removeMemb


    /**
     * List [in]direct members of group
     *
     * @param int $gid
     * @param int $uid
     * @return array|PEAR_Error
     */
    private static function _listMemb($gid, $uid=NULL)
    {
        global $CC_CONFIG, $CC_DBC;
        $sql = "SELECT id, uid, level FROM ".$CC_CONFIG['smembTable']
            ." WHERE gid='$gid'".(is_null($uid) ? '' : " AND uid='$uid'");
        return $CC_DBC->getAll($sql);
    } // fn _listMemb


    /**
     * List groups where uid is [in]direct member
     *
     * @param int $gid
     * @param int $uid
     * @return array|PEAR_Error
     */
    private static function _listRMemb($uid, $gid=NULL)
    {
        global $CC_CONFIG, $CC_DBC;
        $sql = "SELECT id, gid, level FROM ".$CC_CONFIG['smembTable']
            ." WHERE uid='$uid'".(is_null($gid) ? '' : " AND gid='$gid'");
        return $CC_DBC->getAll($sql);
    } // fn listRMemb


    /**
     * Add uid and its [in]direct members to gid
     *
     * @param int $uid
     * @param int $gid
     * @param int $level
     * @param int $rmid
     * @return int|PEAR_Error
     */
    private static function _plainAddSubjectToGroup($uid, $gid, $level=0, $rmid='null')
    {
        $mid = Subjects::_addMemb($uid, $gid, $level, $rmid);
        if (PEAR::isError($mid)) {
            return $mid;
        }
        $marr = Subjects::_listMemb($uid);
        if (PEAR::isError($marr)) {
            return $marr;
        }
        foreach ($marr as $k => $v) {
            $r = Subjects::_addMemb(
                $v['uid'], $gid, intval($v['level'])+$level+1, $mid
            );
            if (PEAR::isError($r)) {
                return $r;
            }
        }
        return $mid;
    }


    /**
     * Rebuild indirect membership records<br>
     * it's probably more complicated to do removing without rebuild ...
     *
     * @return true|PEAR_Error
     */
    private static function _rebuildRels()
    {
        global $CC_CONFIG, $CC_DBC;
        $CC_DBC->query("BEGIN");
        $r = $CC_DBC->query("LOCK TABLE ".$CC_CONFIG['smembTable']);
        if (PEAR::isError($r)) {
            return $r;
        }
        $sql = "DELETE FROM ".$CC_CONFIG['smembTable']
            ." WHERE mid is not null";
        $r = $CC_DBC->query($sql);
        if (PEAR::isError($r)) {
            return $r;
        }
        $arr = $CC_DBC->getAll("SELECT uid, gid FROM ".$CC_CONFIG['smembTable']);
                            //  WHERE mid is null
        if (PEAR::isError($arr)) {
            return $arr;
        }
        foreach ($arr as $it) {
            $marr = Subjects::_listRMemb($it['gid']);
            if (PEAR::isError($marr)) {
                return $marr;
            }
            foreach ($marr as $k => $v) {
                $r = Subjects::_plainAddSubjectToGroup(
                    $it['uid'], $v['gid'], intval($v['level'])+1, $v['id']
                );
                if (PEAR::isError($r)) {
                    return $r;
                }
            }
        }
        $r = $CC_DBC->query("COMMIT");
        if (PEAR::isError($r)) {
            return $r;
        }
        return TRUE;
    } // fn _rebuildRels


    /* =============================================== test and debug methods */

    /**
     * Dump subjects for debug
     *
     * @param string $indstr
     * 		indentation string
     * @param string $ind
     * 		actual indentation
     * @return string
     */
    public static function DumpSubjects($indstr='    ', $ind='')
    {
        $r = $ind.join(', ', array_map(
            create_function('$v', 'return "{$v[\'login\']}({$v[\'cnt\']})";'),
            Subjects::GetSubjectsWCnt()
        ))."\n";
        return $r;
    } // fn dumpSubjects


    /**
     * Delete all subjects and membership records
     *
     * @return void
     */
    public static function DeleteData()
    {
        global $CC_CONFIG, $CC_DBC;
        $CC_DBC->query("DELETE FROM ".$CC_CONFIG['subjTable']);
        $CC_DBC->query("DELETE FROM ".$CC_CONFIG['smembTable']);
        //ObjClasses::DeleteData();
    } // fn deleteData


    /**
     * Insert test data
     *
     * @return array
     */
    public function TestData()
    {
//        $tdata = ObjClasses::TestData();
//        $o['root'] = Subjects::AddSubj('root', 'q');
//        $o['test1'] = Subjects::AddSubj('test1', 'a');
//        $o['test2'] = Subjects::AddSubj('test2', 'a');
//        $o['test3'] = Subjects::AddSubj('test3', 'a');
//        $o['test4'] = Subjects::AddSubj('test4', 'a');
//        $o['test5'] = Subjects::AddSubj('test5', 'a');
//        $o['gr1'] = Subjects::AddSubj('gr1');
//        $o['gr2'] = Subjects::AddSubj('gr2');
//        $o['gr3'] = Subjects::AddSubj('gr3');
//        $o['gr4'] = Subjects::AddSubj('gr4');
//        Subjects::AddSubjectToGroup('test1', 'gr1');
//        Subjects::AddSubjectToGroup('test2', 'gr2');
//        Subjects::AddSubjectToGroup('test3', 'gr3');
//        Subjects::AddSubjectToGroup('test4', 'gr4');
//        Subjects::AddSubjectToGroup('test5', 'gr1');
//        Subjects::AddSubjectToGroup('gr4', 'gr3');
//        Subjects::AddSubjectToGroup('gr3', 'gr2');
//        $tdata['subjects'] = $o;
//        return $tdata;
    } // fn TestData


    /**
     * Make basic test
     *
     */
    public static function Test()
    {
//        $p = ObjClasses::Test();
//        if (PEAR::isError($p)) {
//            return $p;
//        }
//        Subjects::DeleteData();
//        Subjects::TestData();
//        $test_correct = "root(0), test1(0), test2(0), test3(0),".
//            " test4(0), test5(0), gr1(2), gr2(2), gr3(2), gr4(1)\n";
//        $test_dump = Subjects::DumpSubjects();
//        Subjects::RemoveSubj('test1');
//        Subjects::RemoveSubj('test3');
//        Subjects::RemoveSubjectFromGroup('test5', 'gr1');
//        Subjects::RemoveSubjectFromGroup('gr3', 'gr2');
//        $test_correct .= "root(0), test2(0), test4(0), test5(0),".
//            " gr1(0), gr2(1), gr3(1), gr4(1)\n";
//        $test_dump .= Subjects::DumpSubjects();
//        Subjects::DeleteData();
//        if ($test_dump == $test_correct) {
//            $test_log .= "subj: OK\n";
//            return TRUE;
//        } else {
//            return PEAR::raiseError(
//                'Subjects::test:', 1, PEAR_ERROR_DIE, '%s'.
//                "<pre>\ncorrect:\n{$test_correct}\n".
//                "dump:\n{$test_dump}\n</pre>\n");
//        }
    } // fn test

} // class Subjects
?>