Relatively bigger change, mtree replaced by m2tree.

Change in database structure, reinstallation needed!
This commit is contained in:
tomas 2005-02-20 22:01:07 +00:00
parent 84ab36151f
commit 08938d4ed4
7 changed files with 1185 additions and 748 deletions

View File

@ -23,11 +23,14 @@
Author : $Author: tomas $
Version : $Revision: 1.9 $
Version : $Revision: 1.10 $
Location : $Source: /home/paul/cvs2svn-livesupport/newcvsrepo/livesupport/modules/alib/var/alib.php,v $
------------------------------------------------------------------------------*/
require_once 'subj.php';
define('USE_ALIB_CLASSES', TRUE);
define('ALIBERR_NOTLOGGED', 30);
define('ALIBERR_NOTEXISTS', 31);
@ -37,7 +40,7 @@ define('ALIBERR_NOTEXISTS', 31);
* authentication/authorization class
*
* @author $Author: tomas $
* @version $Revision: 1.9 $
* @version $Revision: 1.10 $
* @see Subjects
* @see GreenBox
*/
@ -182,68 +185,89 @@ class Alib extends Subjects{
}
/**
* Check if specified subject have permission to specified action
* on specified object - huh ;)<br>
* One of the most important method in this class hierarchy ...
* Check if specified subject have permission to specified action
* on specified object
*
* @param sid int
* @param action string
* @param oid int OPT
* @return boolean/err
* Look for sequence of correnponding permissions and order it by
* relevence, then test the most relevant for result.
* High relevence have direct permission (directly for specified subject
* and object. Relevance order is done by level distance in the object
* tree, level distance in subjects (user/group system).
* Similar way is used for permissions related to object classes.
* But class-related permissions have lower priority then
* object-tree-related.
* Support for object classes can be disabled by USE_ALIB_CLASSES const.
*
* @param sid int, subject id (user or group id)
* @param action string, from set defined in config
* @param oid int, object id, optional (default: root node)
* @return boolean/err
*/
function checkPerm($sid, $action, $oid=NULL)
{
if(!is_numeric($sid)) return FALSE;
if(is_null($oid)) $oid = $this->getObjId($this->RootNode);
if(is_null($oid) or $oid=='') $oid = $this->getObjId($this->RootNode);
if(PEAR::isError($oid)) return $oid;
if(!is_numeric($oid)) return FALSE;
// query elements
$q_flds = "m.level as S_lvl, p.subj, s.login, action, p.type, p.obj";
$q_from = "{$this->subjTable} s, {$this->permTable} p";
$q_join = "LEFT JOIN {$this->smembTable} m ON p.subj=m.gid ";
// query construction
// shortcuts:
// p: permTable,
// s: subjTable, m smembTable,
// t: treeTable ts: structTable,
// c: classTable, cm: cmembTable
// main query elements:
$q_flds = "m.level , p.subj, s.login, action, p.type, p.obj";
$q_from = "{$this->permTable} p ";
// joins for solving users/groups:
$q_join = "LEFT JOIN {$this->subjTable} s ON s.id=p.subj ";
$q_join .= "LEFT JOIN {$this->smembTable} m ON m.gid=p.subj ";
$q_cond = "p.action in('_all', '$action') AND
(m.uid=$sid OR p.subj=$sid) AND s.id=p.subj";
(s.id=$sid OR m.uid=$sid) ";
// coalesce -1 for higher priority of nongroup rows:
// action DESC order for lower priority of '_all':
$q_ordb = "ORDER BY coalesce(m.level,-1), action DESC, p.type DESC";
$q_flds0 = $q_flds;
$q_from0 = $q_from;
$q_join0 = $q_join;
$q_cond0 = $q_cond;
$q_ordb0 = $q_ordb;
// joins for solving object tree:
$q_flds .= ", t.name, ts.level as tlevel";
$q_join .= "LEFT JOIN {$this->treeTable} t ON t.id=p.obj ";
$q_join .= "LEFT JOIN {$this->structTable} ts ON ts.parid=p.obj ";
$q_cond .= " AND (t.id=$oid OR ts.objid=$oid)";
// action DESC order is hack for lower priority of '_all':
$q_ordb = "ORDER BY S_lvl, action DESC, p.type DESC";
$qc0 = $q_cond;
// test if object is class:
$iscls = $this->isClass($oid);
if(PEAR::isError($iscls)) return $iscls;
if($iscls){
$q_from .= ", {$this->classTable} c";
$q_cond .= " AND c.id=p.obj AND c.id=$oid";
}else{
// obj is normal node => path search => retrieve L/R values for it:
$r1 = $this->dbc->getRow("SELECT lft, rgt, level
FROM {$this->treeTable} WHERE id=$oid");
if(is_null($r1))
return PEAR::raiseError("Alib::checkPerm: object not exists ($oid)",
ALIBERR_NOTEXISTS, PEAR_ERROR_RETURN
);
if(PEAR::isError($r1)) return($r1);
// fetch all path to oid + join with perms
$q_flds .= ", t.name, ({$r1['level']}-t.level)as T_lvl";
$q_from = "{$this->treeTable} t, ".$q_from;
$q_cond .= " AND t.id=p.obj AND t.lft<={$r1['lft']} AND
t.rgt>={$r1['rgt']}";
// action DESC order is hack for lower priority of '_all':
$q_ordb = "ORDER BY T_lvl, S_lvl, action DESC, p.type DESC";
}
$query="SELECT $q_flds FROM $q_from $q_join WHERE $q_cond $q_ordb";
$r2 = $this->dbc->getAll($query);
if(PEAR::isError($r2)) return($r2);
if(!$iscls && !(is_array($r2) && count($r2)>0)){
// no perm found, search in classes:
$q_from = "{$this->cmembTable} cm, ".$q_from;
$q_cond = $qc0.
" AND t.lft<={$r1['lft']} AND t.rgt>={$r1['rgt']}".
" AND cm.cid=p.obj AND cm.objid=t.id";
$query="SELECT $q_flds FROM $q_from $q_join WHERE $q_cond $q_ordb";
$r2 = $this->dbc->getAll($query);
if(PEAR::isError($r2)) return($r2);
}
$q_ordb = "ORDER BY coalesce(ts.level,0), m.level, action DESC, p.type DESC";
// query by tree:
$query1 = "SELECT $q_flds FROM $q_from $q_join WHERE $q_cond $q_ordb";
$r1 = $this->dbc->getAll($query1);
if(PEAR::isError($r1)) return($r1);
// if there is row with type='A' on the top => permit
return (is_array($r2) && count($r2)>0 && $r2[0]['type']=='A');
$AllowedByTree =
(is_array($r1) && count($r1)>0 && $r1[0]['type']=='A');
$DeniedByTree =
(is_array($r1) && count($r1)>0 && $r1[0]['type']=='D');
if(!USE_ALIB_CLASSES) return $AllowedbyTree;
// joins for solving object classes:
$q_flds = $q_flds0.", c.cname ";
$q_join = $q_join0."LEFT JOIN {$this->classTable} c ON c.id=p.obj ";
$q_join .= "LEFT JOIN {$this->cmembTable} cm ON cm.cid=p.obj ";
$q_cond = $q_cond0." AND (c.id=$oid OR cm.objid=$oid)";
$q_ordb = $q_ordb0;
// query by class:
$query2 = "SELECT $q_flds FROM $q_from $q_join WHERE $q_cond $q_ordb";
$r2 = $this->dbc->getAll($query2);
if(PEAR::isError($r2)) return($r2);
$AllowedByClass =
(is_array($r2) && count($r2)>0 && $r2[0]['type']=='A');
// not used now:
// $DeniedByClass =
// (is_array($r2) && count($r2)>0 && $r2[0]['type']=='D');
$res = ($AllowedByTree || (!$DeniedByTree && $AllowedByClass));
# echo"<pre>\nsid=$sid, action=$action, oid=$oid\n"; var_dump($r1); echo"\n---\n$query1\n---\n\n"; var_dump($r2); echo"\n---\n$query2\n---\n\n"; exit;
return $res;
}
/* ---------------------------------------------------------- object tree */
@ -392,9 +416,17 @@ class Alib extends Subjects{
*/
function dumpPerms($indstr=' ', $ind='')
{
$r = $ind.join(', ', array_map(
create_function('$v', 'return "{$v[\'action\']}/{$v[\'type\']}";'),
$this->dbc->getAll("SELECT action, type FROM {$this->permTable}")
$arr = $this->dbc->getAll("
SELECT s.login, p.action, p.type
FROM {$this->permTable} p, {$this->subjTable} s
WHERE s.id=p.subj
ORDER BY p.permid
");
if(PEAR::isError($arr)) return $arr;
$r = $ind.join(', ', array_map(create_function('$v',
'return "{$v[\'login\']}/{$v[\'action\']}/{$v[\'type\']}";'
),
$arr
))."\n";
return $r;
}
@ -421,24 +453,27 @@ class Alib extends Subjects{
$t =& $this->tdata['tree'];
$c =& $this->tdata['classes'];
$s =& $this->tdata['subjects'];
$o[] = $this->addPerm($s[0], '_all', $t[0], 'A');
$o[] = $this->addPerm($s[1], '_all', $t[4], 'A');
$o[] = $this->addPerm($s[1], '_all', $t[7], 'D');
# $o[] = $this->addPerm($s[2], 'addChilds', $t[6], 'A');
$o[] = $this->addPerm($s[2], 'read', $t[5], 'A');
$o[] = $this->addPerm($s[2], 'edit', $t[6], 'A');
$o[] = $this->addPerm($s[3], 'read', $c[0], 'A');
$o[] = $this->addPerm($s[4], 'editPerms', $c[1], 'A');
$o[] = $this->addPerm($s[4], 'editPerms', $t[7], 'D');
$o[] = $this->addPerm($s[1], 'addChilds', $t[3], 'A');
$o[] = $this->addPerm($s[1], 'addChilds', $t[1], 'A');
$o[] = $this->addPerm($s[5], 'addChilds', $t[3], 'A');
$o[] = $this->addPerm($s[5], 'addChilds', $t[1], 'A');
$o[] = $this->addPerm($s[6], 'addChilds', $t[3], 'A');
$o[] = $this->addPerm($s[6], 'addChilds', $t[1], 'A');
$o[] = $this->addPerm($s[7], 'addChilds', $t[3], 'A');
$o[] = $this->addPerm($s[7], 'addChilds', $t[1], 'A');
$this->dbc->setErrorHandling(PEAR_ERROR_PRINT);
$perms = array(
array($s['root'], '_all', $t['root'], 'A'),
array($s['test1'], '_all', $t['pa'], 'A'),
array($s['test1'], 'read', $t['s2b'], 'D'),
array($s['test2'], 'addChilds', $t['pa'], 'D'),
array($s['test2'], 'read', $t['i2'], 'A'),
array($s['test2'], 'edit', $t['s1a'], 'A'),
array($s['test1'], 'addChilds', $t['s2a'], 'D'),
array($s['test1'], 'addChilds', $t['s2c'], 'D'),
array($s['gr2'], 'addChilds', $t['i2'], 'A'),
array($s['test3'], '_all', $t['t1'], 'D'),
);
if(USE_ALIB_CLASSES){
$perms[] = array($s['test3'], 'read', $c['cl_sa'], 'D');
$perms[] = array($s['test4'], 'editPerms', $c['cl2'], 'A');
}
foreach($perms as $p){
$o[] = $r = $this->addPerm($p[0], $p[1], $p[2], $p[3]);
if(PEAR::isError($r)) return $r;
}
$this->tdata['perms'] = $o;
}
@ -451,24 +486,38 @@ class Alib extends Subjects{
{
if(PEAR::isError($p = parent::test())) return $p;
$this->deleteData();
$this->testData();
$this->test_correct = "_all/A, _all/A, _all/D, read/A, edit/A, read/A,".
" editPerms/A, editPerms/D, addChilds/A, addChilds/A, addChilds/A,".
" addChilds/A, addChilds/A, addChilds/A, addChilds/A, addChilds/A".
"\nno, yes\n";
$this->test_dump = $this->dumpPerms().
$r = $this->testData();
if(PEAR::isError($r)) return $r;
$this->test_correct = "root/_all/A, test1/_all/A, test1/read/D,".
" test2/addChilds/D, test2/read/A, test2/edit/A,".
" test1/addChilds/D, test1/addChilds/D, gr2/addChilds/A,".
" test3/_all/D";
if(USE_ALIB_CLASSES){
$this->test_correct .= ", test3/read/D, test4/editPerms/A";
}
$this->test_correct .= "\nno, yes\n";
$r = $this->dumpPerms();
if(PEAR::isError($r)) return $r;
$this->test_dump = $r.
($this->checkPerm(
$this->tdata['subjects'][1], 'edit', $this->tdata['tree'][7]
$this->tdata['subjects']['test1'], 'read',
$this->tdata['tree']['t1']
)? 'yes':'no').", ".
($this->checkPerm(
$this->tdata['subjects'][2], 'read', $this->tdata['tree'][5]
$this->tdata['subjects']['test1'], 'addChilds',
$this->tdata['tree']['i2']
)? 'yes':'no')."\n"
;
$this->removePerm($this->tdata['perms'][1]);
$this->removePerm($this->tdata['perms'][3]);
$this->test_correct .= "_all/A, _all/D, edit/A, read/A, editPerms/A,".
" editPerms/D, addChilds/A, addChilds/A, addChilds/A, addChilds/A,".
" addChilds/A, addChilds/A, addChilds/A, addChilds/A\n";
$this->test_correct .= "root/_all/A, test1/read/D,".
" test2/read/A, test2/edit/A,".
" test1/addChilds/D, test1/addChilds/D, gr2/addChilds/A,".
" test3/_all/D";
if(USE_ALIB_CLASSES){
$this->test_correct .= ", test3/read/D, test4/editPerms/A";
}
$this->test_correct .= "\n";
$this->test_dump .= $this->dumpPerms();
$this->deleteData();
if($this->test_dump==$this->test_correct)

View File

@ -23,11 +23,11 @@
Author : $Author: tomas $
Version : $Revision: 1.4 $
Version : $Revision: 1.5 $
Location : $Source: /home/paul/cvs2svn-livesupport/newcvsrepo/livesupport/modules/alib/var/class.php,v $
------------------------------------------------------------------------------*/
require_once "mtree.php";
require_once "m2tree.php";
/**
* ObjClass class
@ -35,11 +35,11 @@ require_once "mtree.php";
* class for 'object classes' handling - i.e. groups of object in tree
*
* @author $Author: tomas $
* @version $Revision: 1.4 $
* @version $Revision: 1.5 $
* @see Mtree
* @see Subj
*/
class ObjClasses extends Mtree{
class ObjClasses extends M2tree{
var $classTable;
var $cmembTable;
/**
@ -51,7 +51,7 @@ class ObjClasses extends Mtree{
*/
function ObjClasses(&$dbc, $config)
{
parent::MTree($dbc, $config);
parent::M2Tree($dbc, $config);
$this->classTable = $config['tblNamePrefix'].'classes';
$this->cmembTable = $config['tblNamePrefix'].'cmemb';
}
@ -174,7 +174,7 @@ class ObjClasses extends Mtree{
function getClassName($id)
{
return $this->dbc->getOne(
$query = "SELECT cname FROM {$this->classTable}WHERE id=$id");
$query = "SELECT cname FROM {$this->classTable} WHERE id=$id");
}
/**
@ -244,7 +244,7 @@ class ObjClasses extends Mtree{
{
$this->dbc->query("DELETE FROM {$this->cmembTable}");
$this->dbc->query("DELETE FROM {$this->classTable}");
parent::deleteData();
parent::reset();
}
/**
* Insert test data
@ -253,10 +253,12 @@ class ObjClasses extends Mtree{
function testData()
{
parent::testData();
$o[] = $this->addClass('Sections b');
$o[] = $this->addClass('Class 2');
$this->addObj2Class($o[1], $this->tdata['tree'][4]);
$this->addObj2Class($o[1], $this->tdata['tree'][9]);
$o['cl_sa'] = $this->addClass('Sections a');
$o['cl2'] = $this->addClass('Class 2');
$this->addObj2Class($o['cl_sa'], $this->tdata['tree']['s1a']);
$this->addObj2Class($o['cl_sa'], $this->tdata['tree']['s2a']);
$this->addObj2Class($o['cl2'], $this->tdata['tree']['t1']);
$this->addObj2Class($o['cl2'], $this->tdata['tree']['pb']);
$this->tdata['classes'] = $o;
}
@ -269,11 +271,11 @@ class ObjClasses extends Mtree{
if(PEAR::isError($p = parent::test())) return $p;
$this->deleteData();
$this->testData();
$this->test_correct = "Sections b (0), Class 2 (2)\n";
$this->test_correct = "Sections a (2), Class 2 (2)\n";
$this->test_dump = $this->dumpClasses();
$this->removeClass('Sections b');
$this->removeObjFromClass($this->tdata['tree'][4],
$this->tdata['classes'][1]);
$this->removeClass('Sections a');
$this->removeObjFromClass($this->tdata['tree']['pb'],
$this->tdata['classes']['cl2']);
$this->test_correct .= "Class 2 (1)\n";
$this->test_dump .= $this->dumpClasses();
$this->deleteData();

View File

@ -0,0 +1,736 @@
<?
define('ALIBERR_MTREE', 10);
/**
* M2tree class
*
* class for tree hierarchy stored in db
*
* example config: example/conf.php<br>
* example minimal config:
* <pre><code>
* $config = array(
* 'dsn' => array( // data source definition
* 'username' => DBUSER,
* 'password' => DBPASSWORD,
* 'hostspec' => 'localhost',
* 'phptype' => 'pgsql',
* 'database' => DBNAME
* ),
* 'tblNamePrefix' => 'al_',
* 'RootNode' =>'RootNode',
* );
* </code></pre>
* @author $Author: tomas $
* @version $Revision: 1.1 $
* @see ObjClasses
* Original author Tom Hlava
*/
class M2tree{
/**
* Database object container
*/
var $dbc;
/**
* Configuration tree
*/
var $config;
/**
* Tree table name
*/
var $treeTable;
/**
* Structure table name
*/
var $structTable;
/**
* Root node name
*/
var $rootNodeName;
/**
* Constructor
*
* @param dbc object
* @param config array
* @return this
*/
function M2tree(&$dbc, $config)
{
$this->dbc =& $dbc;
$this->config = $config;
$this->treeTable = $config['tblNamePrefix'].'tree';
$this->structTable = $config['tblNamePrefix'].'struct';
$this->rootNodeName = $config['RootNode'];
}
/* ======================================================= public methods */
/**
* Add new object of specified type to the tree under specified parent
* node
*
* @param name string, mnemonic name for new object
* @param type string, type of new object
* @param parid int, optional, parent id
* @return int/err - new id of inserted object or PEAR::error
*/
function addObj($name, $type, $parid=NULL)
{
if($name=='' || $type=='') return $this->dbc->raiseError(
"M2tree::addObj: Wrong name or type", ALIBERR_MTREE
);
if(is_null($parid)) $parid = $this->getRootNode();
// changing name if the same is in the dest. folder:
for( ;
$xid = $this->getObjId($name, $parid),
!is_null($xid) && !$this->dbc->isError($xid);
$name .= "_"
);
if($this->dbc->isError($xid)) return $xid;
// insert new object record:
$this->dbc->query("BEGIN");
$oid = $this->dbc->nextId("{$this->treeTable}_id_seq");
if($this->dbc->isError($oid)) return $this->_dbRollback($oid);
$r = $this->dbc->query("
INSERT INTO {$this->treeTable} (id, name, type)
VALUES ($oid, '$name', '$type')
");
if($this->dbc->isError($r)) return $this->_dbRollback($r);
$dataArr = array();
// build data ($dataArr) for INSERT of structure records:
for($p=$parid, $l=1; !is_null($p); $p=$this->getParent($p), $l++){
$rid = $this->dbc->nextId("{$this->structTable}_id_seq");
if($this->dbc->isError($rid)) return $this->_dbRollback($rid);
$dataArr[] = array($rid, $oid, $p, $l);
}
// build and prepare INSERT command automatically:
$pr = $this->dbc->autoPrepare($this->structTable,
array('rid', 'objid', 'parid', 'level'), DB_AUTOQUERY_INSERT);
if($this->dbc->isError($pr)) return $this->_dbRollback($pr);
// execute INSERT command for $dataArr:
$r = $this->dbc->executeMultiple($pr, $dataArr);
if($this->dbc->isError($r)) return $this->_dbRollback($r);
$r = $this->dbc->query("COMMIT");
if(PEAR::isError($r)) return $this->_dbRollback($r);
return $oid;
}
/**
* Remove specified object
*
* @param oid int, object id to remove
* @return boolean/err - TRUE or PEAR::error
*/
function removeObj($oid)
{
if($oid == $this->getRootNode()){
return $this->dbc->raiseError(
"M2tree::removeObj: Can't remove root"
);
}
$dir = $this->getDir($oid);
if($this->dbc->isError($dir)) return $dir;
foreach($dir as $k=>$ch){
$r = $this->removeObj($ch['id']);
if($this->dbc->isError($r)) return $r;
}
$r = $this->dbc->query("
DELETE FROM {$this->treeTable}
WHERE id=$oid
");
if($this->dbc->isError($r)) return $r;
/* done by automatic reference trigger:
$r = $this->dbc->query("
DELETE FROM {$this->structTable}
WHERE objid=$oid
");
if($this->dbc->isError($r)) return $r;
*/
return TRUE;
}
/**
* Create copy of specified object and insert copy to new position
* recursively
*
* @param oid int, source object id
* @param newParid int, destination parent id
* @param after null, dummy argument for back-compatibility
* @return int/err - new id of inserted object or PEAR::error
*/
function copyObj($oid, $newParid, $after=NULL)
{
if(TRUE === ($r = $this->isChildOf($newParid, $oid, TRUE))){
return $this->dbc->raiseError(
"M2tree::copyObj: Can't copy into itself"
);
}
if($this->dbc->isError($r)) return $r;
// get name:
$name = $this->getObjName($oid);
if($this->dbc->isError($name)) return $name;
// get parent id:
$parid = $this->getParent($oid);
if($this->dbc->isError($parid)) return $parid;
if($parid == $newParid) $name .= "_copy";
// get type:
$type = $this->getObjType($oid);
if($this->dbc->isError($type)) return $type;
// look for children:
$dir = $this->getDir($oid, $flds='id');
if($this->dbc->isError($dir)) return $dir;
// insert aktual object:
$nid = $this->addObj($name, $type, $newParid);
if($this->dbc->isError($nid)) return $nid;
// if no children:
if(is_null($dir)) return $nid;
// optionally insert children recursively:
foreach($dir as $k=>$item){
$r = $this->copyObj($item['id'], $nid);
if($this->dbc->isError($r)) return $r;
}
return $nid;
}
/**
* Move subtree to another node without removing/adding
*
* @param oid int
* @param newParid int
* @param after null, dummy argument for back-compatibility
* @return boolean/err
*/
function moveObj($oid, $newParid, $after=NULL)
{
if(TRUE === (
$r = $this->isChildOf($newParid, $oid, TRUE)
|| $oid == $newParid
)){
return $this->dbc->raiseError(
"M2tree::moveObj: Can't move into itself"
);
}
if($this->dbc->isError($r)) return $r;
// get name:
$name0 = $name = $this->getObjName($oid);
if($this->dbc->isError($name)) return $name;
$this->dbc->query("BEGIN");
// cut it from source:
$r = $this->_cutSubtree($oid);
if($this->dbc->isError($r)) return $this->_dbRollback($r);
// changing name if the same is in the dest. folder:
for( ;
$xid = $this->getObjId($name, $newParid),
!is_null($xid) && !$this->dbc->isError($xid);
$name .= "_"
);
if($this->dbc->isError($xid)) return $this->_dbRollback($xid);
if($name != $name0){
$r = $this->renameObj($oid, $name);
if($this->dbc->isError($r)) return $this->_dbRollback($r);
}
// paste it to dest.:
$r = $this->_pasteSubtree($oid, $newParid);
if($this->dbc->isError($r)) return $this->_dbRollback($r);
$r = $this->dbc->query("COMMIT");
if(PEAR::isError($r)) return $this->_dbRollback($r);
return TRUE;
}
/**
* Rename of specified object
*
* @param oid int, object id to rename
* @param newName string, new name
* @return boolean/err - True or PEAR::error
*/
function renameObj($oid, $newName)
{
// get parent id:
$parid = $this->getParent($oid);
if($this->dbc->isError($parid)) return $parid;
// changing name if the same is in the folder:
for( ;
$xid = $this->getObjId($newName, $parid),
!is_null($xid) && !$this->dbc->isError($xid);
$newName .= "_"
);
if($this->dbc->isError($xid)) return $xid;
$r = $this->dbc->query("
UPDATE {$this->treeTable}
SET name='$newName'
WHERE id=$oid
");
if($this->dbc->isError($r)) return $r;
return TRUE;
}
/* --------------------------------------------------------- info methods */
/**
* Search for child id by name in sibling set
*
* @param name string, searched name
* @param parId int, optional, parent id (default is root node)
* @return int/null/err - child id (if found) or null or PEAR::error
*/
function getObjId($name, $parId=NULL)
{
if($name=='' && is_null($parId)) $name = $this->rootNodeName;
$parcond = (is_null($parId) ? "parid is null" : "parid='$parId'");
$r = $this->dbc->getOne("
SELECT id FROM {$this->treeTable} t
LEFT JOIN {$this->structTable} s ON id=objid
WHERE name='$name' AND $parcond"
);
if($this->dbc->isError($r)) return $r;
return $r;
}
/**
* Get one value for object by id (default: get name)
*
* @param oid int
* @param fld string, optional, requested field (default: name)
* @return string/err
*/
function getObjName($oid, $fld='name')
{
$r = $this->dbc->getOne("
SELECT $fld FROM {$this->treeTable}
WHERE id=$oid
");
return $r;
}
/**
* Get object type by id
*
* @param oid int
* @return string/err
*/
function getObjType($oid)
{
return $this->getObjName($oid, 'type');
}
/**
* Get parent id
*
* @param oid int
* @return int/err
*/
function getParent($oid)
{
$r = $this->dbc->getOne("
SELECT parid FROM {$this->structTable}
WHERE objid=$oid AND level=1
");
return $r;
}
/**
* Get array of nodes in object's path from root node
*
* @param oid int
* @param flds string, optional
* @return array/err
*/
function getPath($oid, $flds='id', $withSelf=TRUE)
{
$path = $this->dbc->getAll("
SELECT $flds
FROM {$this->treeTable}
LEFT JOIN {$this->structTable} s ON id=parid
WHERE objid=$oid
ORDER BY coalesce(level, 0) DESC
");
if($this->dbc->isError($path)) return $path;
if($withSelf){
$r = $this->dbc->getRow("
SELECT $flds FROM {$this->treeTable}
WHERE id=$oid
");
if($this->dbc->isError($r)) return $r;
}
array_push($path, $r);
return $path;
}
/**
* Get array of childnodes
*
* @param oid int
* @param flds string, optional, comma separated list of requested fields
* @param order string, optional, fieldname for order by clause
* @return array/err
*/
function getDir($oid, $flds='id', $order='name')
{
$r = $this->dbc->getAll("
SELECT $flds
FROM {$this->treeTable}
INNER JOIN {$this->structTable} ON id=objid AND level=1
WHERE parid=$oid
ORDER BY $order
");
return $r;
}
/**
* Get level of object relatively to specified root
*
* @param oid int, object id
* @param flds string, list of field names for select
* (optional - default: 'level')
* @param rootId int, root for relative levels
* (optional - default: NULL - use root of whole tree)
* @return hash-array with field name/value pairs
*/
function getObjLevel($oid, $flds='level', $rootId=NULL)
{
if(is_null($rootId)) $rootId = $this->getRootNode();
$re = $this->dbc->getRow("
SELECT $flds
FROM {$this->treeTable}
LEFT JOIN {$this->structTable} s ON id=objid AND parid=$rootId
WHERE id=$oid
");
$re['level'] = intval($re['level']);
return $re;
}
/**
* Get subtree of specified node
*
* @param oid int, optional, default: root node
* @param withRoot boolean, optional, include/exclude specified node
* @param rootId int, root for relative levels, optional
* @return array/err
*/
function getSubTree($oid=NULL, $withRoot=FALSE, $rootId=NULL)
{
if(is_null($oid)) $oid = $this->getRootNode();
if(is_null($rootId)) $rootId = $oid;
$r = array();
if($withRoot){
$r[] = $re = $this->getObjLevel($oid, 'id, name, level', $rootId);
}
if($this->dbc->isError($re)) return $re;
$dirarr = $this->getDir($oid, 'id, level');
if($this->dbc->isError($dirarr)) return $dirarr;
foreach($dirarr as $k=>$snod)
{
$re = $this->getObjLevel($snod['id'], 'id, name, level', $rootId);
if($this->dbc->isError($re)) return $re;
# $re['level'] = intval($re['level'])+1;
$r[] = $re;
$r = array_merge($r,
$this->getSubTree($snod['id'], FALSE, $rootId));
}
return $r;
}
/**
* Returns true if first object if child of second one
*
* @param oid int, object id of tested object
* @param parid int, object id of parent
* @param indirect boolean, test indirect or only direct relation
* @return boolean
*/
function isChildOf($oid, $parid, $indirect=FALSE)
{
if(!$indirect){
$paridD = $this->getParent($oid);
if($this->dbc->isError($paridD)) return $paridD;
return ($paridD == $parid);
}
$path = $this->getPath($oid, 'id', FALSE);
if($this->dbc->isError($path)) return $path;
$res = FALSE;
foreach($path as $k=>$item){ if($item['id'] == $parid) $res = TRUE; }
return $res;
}
/**
* Get id of root node
*
* @return int/err
*/
function getRootNode()
{
return $this->getObjId($this->rootNodeName);
}
/**
* Get all objects in the tree as array of hashes
*
* @return array/err
*/
function getAllObjects()
{
return $this->dbc->getAll(
"SELECT * FROM {$this->treeTable}"
);
}
/* ------------------------ info methods related to application structure */
/* (this part should be redefined in extended class to allow
* defining/modifying/using application structure)
* (only very simple structure definition - in $config - supported now)
*/
/**
* Get child types allowed by application definition
*
* @param type string
* @return array
*/
function getAllowedChildTypes($type)
{
return $this->config['objtypes'][$type];
}
/* ==================================================== "private" methods */
/**
* Cut subtree of specified object from tree.
* Preserve subtree structure.
*
* @param oid int, object id
* @return boolean
*/
function _cutSubtree($oid)
{
$lvl = $this->getObjLevel($oid);
if($this->dbc->isError($lvl)) return $lvl;
$lvl = $lvl['level'];
// release downside structure
$r = $this->dbc->query("
DELETE FROM {$this->structTable}
WHERE rid IN (
SELECT s3.rid FROM {$this->structTable} s1
INNER JOIN {$this->structTable} s2 ON s1.objid=s2.objid
INNER JOIN {$this->structTable} s3 ON s3.objid=s1.objid
WHERE (s1.parid=$oid OR s1.objid=$oid)
AND s2.parid=1 AND s3.level>(s2.level-$lvl)
)
");
if($this->dbc->isError($r)) return $r;
return TRUE;
}
/**
* Paste subtree previously cut by _cutSubtree method into main tree
*
* @param oid int, object id
* @param newParid int, destination object id
* @return boolean
*/
function _pasteSubtree($oid, $newParid)
{
$dataArr = array();
// build data ($dataArr) for INSERT:
foreach($this->getSubTree($oid, TRUE) as $o){
$l=intval($o['level'])+1;
for($p=$newParid; !is_null($p); $p=$this->getParent($p), $l++){
$rid = $this->dbc->nextId("{$this->structTable}_id_seq");
if($this->dbc->isError($rid)) return $rid;
$dataArr[] = array($rid, $o['id'], $p, $l);
}
}
// build and prepare INSERT command automatically:
$pr = $this->dbc->autoPrepare($this->structTable,
array('rid', 'objid', 'parid', 'level'), DB_AUTOQUERY_INSERT);
if($this->dbc->isError($pr)) return $pr;
// execute INSERT command for $dataArr:
$r = $this->dbc->executeMultiple($pr, $dataArr);
if($this->dbc->isError($r)) return $r;
return TRUE;
}
/**
* Do SQL rollback and return PEAR::error
*
* @param r object/string, error object or error message
* @return err
*/
function _dbRollback($r)
{
$this->dbc->query("ROLLBACK");
if($this->dbc->isError($r)) return $r;
elseif(is_string($r)){
$msg = basename(__FILE__)."::".get_class($this).": $r";
}else{
$msg = basename(__FILE__)."::".get_class($this).": unknown error";
}
return $this->dbc->raiseError($msg, ALIBERR_MTREE, PEAR_ERROR_RETURN);
}
/* ==================================================== auxiliary methods */
/**
* Human readable dump of subtree - for debug
*
* @param oid int, start object id
* @param indstr string, indentation string
* @param ind string, aktual indentation
* @return string
*/
function dumpTree($oid=NULL, $indstr=' ', $ind='',
$format='{name}({id})', $withRoot=TRUE)
{
$r='';
foreach($st = $this->getSubTree($oid, $withRoot) as $o){
if($this->dbc->isError($st)) return $st;
$r .= $ind.str_repeat($indstr, $o['level']).
preg_replace(array('|\{name\}|', '|\{id\}|'),
array($o['name'], $o['id']), $format).
"\n";
}
return $r;
}
/**
* Create tables + initialize root node
*
*/
function install()
{
$this->dbc->query("BEGIN");
if(PEAR::isError($r)) return $r;
$r = $this->dbc->query("CREATE TABLE {$this->treeTable} (
id int not null PRIMARY KEY,
name varchar(255) not null default'',
-- parid int,
type varchar(255) not null default'',
param varchar(255)
)");
if($this->dbc->isError($r)) return $r;
$r = $this->dbc->createSequence("{$this->treeTable}_id_seq");
if($this->dbc->isError($r)) return $r;
$r = $this->dbc->query("CREATE UNIQUE INDEX {$this->treeTable}_id_idx
ON {$this->treeTable} (id)");
if($this->dbc->isError($r)) return $r;
$r = $this->dbc->query("CREATE INDEX {$this->treeTable}_name_idx
ON {$this->treeTable} (name)");
if($this->dbc->isError($r)) return $r;
$r = $this->dbc->query("CREATE TABLE {$this->structTable} (
rid int not null PRIMARY KEY,
objid int not null REFERENCES {$this->treeTable} ON DELETE CASCADE,
parid int not null REFERENCES {$this->treeTable} ON DELETE CASCADE,
level int
)");
if($this->dbc->isError($r)) return $r;
$r = $this->dbc->createSequence("{$this->structTable}_id_seq");
if($this->dbc->isError($r)) return $r;
$r = $this->dbc->query("CREATE UNIQUE INDEX {$this->structTable}_rid_idx
ON {$this->structTable} (rid)");
if($this->dbc->isError($r)) return $r;
$r = $this->dbc->query("CREATE INDEX {$this->structTable}_objid_idx
ON {$this->structTable} (objid)");
if($this->dbc->isError($r)) return $r;
$r = $this->dbc->query("CREATE INDEX {$this->structTable}_parid_idx
ON {$this->structTable} (parid)");
if($this->dbc->isError($r)) return $r;
$r = $this->dbc->query("CREATE INDEX {$this->structTable}_level_idx
ON {$this->structTable} (level)");
if($this->dbc->isError($r)) return $r;
$r = $this->dbc->query("
CREATE UNIQUE INDEX {$this->structTable}_objid_level_idx
ON {$this->structTable} (objid, level)
");
if($this->dbc->isError($r)) return $r;
$r = $this->dbc->query("
CREATE UNIQUE INDEX {$this->structTable}_objid_parid_idx
ON {$this->structTable} (objid, parid)
");
if($this->dbc->isError($r)) return $r;
$oid = $this->dbc->nextId("{$this->treeTable}_id_seq");
if($this->dbc->isError($oid)) return $oid;
$r = $this->dbc->query("
INSERT INTO {$this->treeTable}
(id, name, type)
VALUES
($oid, '{$this->rootNodeName}', 'RootNode')
");
if($this->dbc->isError($r)) return $r;
$r = $this->dbc->query("COMMIT");
if(PEAR::isError($r)) return $r;
}
/**
* Drop all tables and sequencies
*
*/
function uninstall()
{
$this->dbc->query("DROP TABLE {$this->structTable}");
$this->dbc->dropSequence("{$this->structTable}_id_seq");
$this->dbc->query("DROP TABLE {$this->treeTable}");
$this->dbc->dropSequence("{$this->treeTable}_id_seq");
}
/**
* Uninstall and install
*
*/
function reinstall()
{
$this->uninstall();
$this->install();
}
/**
* Clean up tree - delete all except root node
*
*/
function reset()
{
$rid = $this->getRootNode();
if($this->dbc->isError($rid)) return $rid;
$r = $this->dbc->query("DELETE FROM {$this->structTable}");
if($this->dbc->isError($r)) return $r;
$r = $this->dbc->query("DELETE FROM {$this->treeTable} WHERE id<>$rid");
if($this->dbc->isError($r)) return $r;
}
/**
* Insert test data to the tree.
* Only for compatibility with previous mtree - will be removed.
*
* @return array
*/
function test()
{
require_once"m2treeTest.php";
$mt = &new M2treeTest($this->dbc, $this->config);
$r = $mt->_test();
return $r;
}
/**
* Insert test data to the tree.
* Only for compatibility with previous mtree - will be removed.
*
* @return array
*/
function testData()
{
$o['root'] = $this->getRootNode();
$o['pa'] = $this->addObj('Publication A', 'Publication', $o['root']);
$o['i1'] = $this->addObj('Issue 1', 'Issue', $o['pa']);
$o['s1a'] = $this->addObj('Section a', 'Section', $o['i1']);
$o['s1b'] = $this->addObj('Section b', 'Section', $o['i1']);
$o['i2'] = $this->addObj('Issue 2', 'Issue', $o['pa']);
$o['s2a'] = $this->addObj('Section a', 'Section', $o['i2']);
$o['s2b'] = $this->addObj('Section b', 'Section', $o['i2']);
$o['t1'] = $this->addObj('Title', 'Title', $o['s2b']);
$o['s2c'] = $this->addObj('Section c', 'Section', $o['i2']);
$o['pb'] = $this->addObj('Publication B', 'Publication', $o['root']);
$this->tdata['tree'] = $o;
}
}
?>

View File

@ -0,0 +1,210 @@
<?php
require_once"m2tree.php";
class M2treeTest extends M2tree{
function _test_addObj()
{
for($i=1; $i<=3; $i++){
$r = $this->addObj("Publication$i", "Publication");
if($this->dbc->isError($r)) return $r;
$this->_t["p$i"] = $r;
}
for($i=1; $i<=3; $i++){
$r = $this->addObj("Issue$i", "Issue",
$this->_t[$i<=2 ? 'p1' : 'p2']);
if($this->dbc->isError($r)) return $r;
$this->_t["i$i"] = $r;
}
for($i=1; $i<=4; $i++){
$r = $this->addObj("Section$i", "Section",
$this->_t[$i<=3 ? 'i1' : 'i3']);
if($this->dbc->isError($r)) return $r;
$this->_t["s$i"] = $r;
}
$r = $this->addObj("Par1", "Par", $this->_t["s2"]);
if($this->dbc->isError($r)) return $r;
$this->_t["r1"] = $r;
}
function _test_check($title, $expected, $returned)
{
if($expected != $returned){
return $this->dbc->raiseError(
"m2tree::$title FAILED:\n".
" ###expected:\n$expected\n ---\n".
" ###returned:\n$returned\n ---\n"
);
}
return "# ".get_class($this)."::$title: OK\n";
}
function _test()
{
echo "# M2tree test:\n";
// addObj/dumpTree test:
$r = $this->_test_addObj();
if($this->dbc->isError($r)) return $r;
$expected = "RootNode
Publication1
Issue1
Section1
Section2
Par1
Section3
Issue2
Publication2
Issue3
Section4
Publication3
";
$returned = $this->dumpTree(NULL, ' ', '', '{name}');
if($this->dbc->isError($returned)) return $returned;
$r = $this->_test_check('addObj/dumpTree', $expected, $returned);
if($this->dbc->isError($r)) return $r; else echo $r;
// shaking test:
$nid = $this->copyObj($this->_t['s2'], $this->_t['s4']);
if($this->dbc->isError($nid)) return $nid;
$r = $this->removeObj($this->_t['s2']);
if($this->dbc->isError($r)) return $r;
$r = $this->moveObj($nid, $this->_t['i1']);
if($this->dbc->isError($r)) return $r;
$returned = $this->dumpTree(NULL, ' ', '', '{name}');
if($this->dbc->isError($returned)) return $returned;
$r = $this->_test_check('shaking test', $expected, $returned);
if($this->dbc->isError($r)) return $r; else echo $r;
// removeObj test:
$r = $this->removeObj($this->_t['p2']);
if($this->dbc->isError($r)) return $r;
$expected = "RootNode
Publication1
Issue1
Section1
Section2
Par1
Section3
Issue2
Publication3
";
$returned = $this->dumpTree(NULL, ' ', '', '{name}');
$r = $this->_test_check('removeObj', $expected, $returned);
if($this->dbc->isError($r)) return $r; else echo $r;
// renameObj/getObjName test:
$original = $this->getObjName($this->_t['i2']);
if($this->dbc->isError($original)) return $original;
$changed = 'Issue2_changed';
$expected = $original.$changed;
$r = $this->renameObj($this->_t['i2'], $changed);
if($this->dbc->isError($r)) return $r;
$r = $this->getObjName($this->_t['i2']);
if($this->dbc->isError($r)) return $r;
$returned = $r;
$r = $this->renameObj($this->_t['i2'], $original);
if($this->dbc->isError($r)) return $r;
$r = $this->getObjName($this->_t['i2']);
$returned = $r.$returned;
if($this->dbc->isError($r)) return $r;
$r = $this->_test_check('renameObj/getObjName', $expected, $returned);
if($this->dbc->isError($r)) return $r; else echo $r;
// getPath test:
$expected = "RootNode, Publication1, Issue1, Section3";
$r = $this->getPath($this->_t['s3'], 'name');
$returned = join(', ', array_map(create_function('$it', 'return $it["name"];'), $r));
$r = $this->_test_check('getPath', $expected, $returned);
if($this->dbc->isError($r)) return $r; else echo $r;
// getObjType test:
$expected = 'Issue';
$returned = $this->getObjType($this->_t['i2']);
$r = $this->_test_check('getObjType', $expected, $returned);
if($this->dbc->isError($r)) return $r; else echo $r;
// getParent test:
$expected = $this->_t['p1'];
$returned = $this->getParent($this->_t['i2']);
$r = $this->_test_check('getParent', $expected, $returned);
if($this->dbc->isError($r)) return $r; else echo $r;
// getDir test:
$expected = "Issue1, Issue2";
$r = $this->getDir($this->_t['p1'], 'name');
$returned = join(', ', array_map(create_function('$it', 'return $it["name"];'), $r));
$r = $this->_test_check('getDir', $expected, $returned);
if($this->dbc->isError($r)) return $r; else echo $r;
// getObjId test:
$expected = $this->_t['i2'];
$returned = $this->getObjId('Issue2', $this->_t['p1']);
$r = $this->_test_check('getObjId', $expected, $returned);
if($this->dbc->isError($r)) return $r; else echo $r;
// getObjLevel test:
$expected = 2;
$r = $this->getObjLevel($this->_t['i2']);
if($this->dbc->isError($r)) return $r;
$returned = $r['level'];
$r = $this->_test_check('getObjLevel', $expected, $returned);
if($this->dbc->isError($r)) return $r; else echo $r;
// copyObj test:
$expected = "RootNode
Publication1
Issue1
Section1
Section2
Par1
Section3
Issue2
Publication3
Issue1
Section1
Section2
Par1
Section3
";
$nid = $this->copyObj($this->_t['i1'], $this->_t['p3']);
if($this->dbc->isError($nid)) return $nid;
$returned = $this->dumpTree(NULL, ' ', '', '{name}');
$r = $this->_test_check('copyObj', $expected, $returned);
if($this->dbc->isError($r)) return $r; else echo $r;
$this->removeObj($nid);
// moveObj test:
$expected = "RootNode
Publication1
Issue2
Publication3
Issue1
Section1
Section2
Par1
Section3
";
$r = $this->moveObj($this->_t['i1'], $this->_t['p3']);
if($this->dbc->isError($r)) return $r;
$returned = $this->dumpTree(NULL, ' ', '', '{name}');
$r = $this->_test_check('moveObj', $expected, $returned);
if($this->dbc->isError($r)) return $r; else echo $r;
// _cutSubtree test:
// _pasteSubtree test:
echo $this->dumpTree();
// reset test:
$expected = "RootNode\n";
$r = $this->reset();
if($this->dbc->isError($r)) return $r;
$returned = $this->dumpTree(NULL, ' ', '', '{name}');
$r = $this->_test_check('reset', $expected, $returned);
if($this->dbc->isError($r)) return $r; else echo $r;
echo "# OK\n";
return TRUE;
}
}
?>

View File

@ -0,0 +1,63 @@
<?php
require_once 'DB.php';
require_once './m2treeTest.php';
require_once"./conf.php";
PEAR::setErrorHandling(PEAR_ERROR_DIE);
PEAR::setErrorHandling(PEAR_ERROR_CALLBACK, 'errCallback');
function errCallback($err)
{
if(assert_options(ASSERT_ACTIVE)==1) return;
echo "<pre>\n";
echo "request: "; print_r($_REQUEST);
echo "\ngm:\n".$err->getMessage()."\nui:\n".$err->getUserInfo()."\n";
echo "<hr>BackTrace:\n";
print_r($err->backtrace);
echo "</pre>\n";
exit;
}
$dbc = DB::connect($config['dsn'], TRUE);
$dbc->setFetchMode(DB_FETCHMODE_ASSOC);
$m2 = &new M2treeTest($dbc, $config);
#$m2->uninstall();
#exit;
#$r = $m2->install(); if($dbc->isError($r)){ echo $r->getMessage()."\n".$r->getUserInfo()."\n"; exit; }
$m2->reset();
#$r = $m2->_test_addObj(); if($dbc->isError($r)){ echo $r->getMessage()."\n".$r->getUserInfo()."\n"; exit; }
$r = $m2->_test(); if($dbc->isError($r)){ echo $r->getMessage()."\n".$r->getUserInfo()."\n"; exit; }
/*
$parid = $m2->_t['s1'];
for($i=1; $i<=20; $i++){
$r = $m2->addObj("X$i", "XX", $parid);
if($m2->dbc->isError($r)) return $r;
$parid = $r;
//$m2->_t["p$i"] = $r;
}
$r = $m2->dumpTree(); echo "$r\n";
*/
#$r = $m2->getSubTree($m2->_t['i1'], TRUE); var_dump($r);
#$r = $m2->getPath($m2->_t['r1'], 'id, name, level'); var_dump($r);
#$r = $m2->getPath($m2->_t['r1'], 'id, name, level', TRUE); var_dump($r);
/*
foreach($m2->getAllObjects() as $k=>$obj){
$r = $m2->isChildOf($m2->_t['r1'], $obj['id'], TRUE);
echo "{$obj['name']}: $r\n";
}
*/
#$r = $m2->getDir($m2->_t['i1'], 'id, name, level'); var_dump($r);
#$r = $m2->getPath($m2->_t['s3'], 'name'); var_dump($r);
#$r = $m2->addObj("Issue1", "XX", $m2->_t["s4"]); var_dump($r);
#$r = $m2->moveObj($m2->_t['i1'], $m2->_t['s4']); var_dump($r);
#$r = $m2->copyObj($m2->_t['i1'], $m2->_t['s4']); var_dump($r);
#$r = $m2->removeObj($m2->_t['p2']); var_dump($r);
#$r = $m2->renameObj($m2->_t['s1'], 'Section2'); var_dump($r);
#$r = $m2->renameObj($m2->_t['s3'], 'Section2'); var_dump($r);
$r = $m2->dumpTree(); echo "$r\n";
?>

View File

@ -1,627 +0,0 @@
<?php
/*------------------------------------------------------------------------------
Copyright (c) 2004 Media Development Loan Fund
This file is part of the LiveSupport project.
http://livesupport.campware.org/
To report bugs, send an e-mail to bugs@campware.org
LiveSupport is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
LiveSupport is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with LiveSupport; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Author : $Author: tomas $
Version : $Revision: 1.5 $
Location : $Source: /home/paul/cvs2svn-livesupport/newcvsrepo/livesupport/modules/alib/var/Attic/mtree.php,v $
------------------------------------------------------------------------------*/
define('ALIBERR_MTREE', 10);
/**
* Mtree class
*
* class for tree hierarchy stored in db
*
* example config: example/conf.php<br>
* example minimal config:
* <pre><code>
* $config = array(
* 'dsn' => array( // data source definition
* 'username' => DBUSER,
* 'password' => DBPASSWORD,
* 'hostspec' => 'localhost',
* 'phptype' => 'pgsql',
* 'database' => DBNAME
* ),
* 'tblNamePrefix' => 'al_',
* 'RootNode' =>'RootNode',
* );
* </code></pre>
* @author $Author: tomas $
* @version $Revision: 1.5 $
* @see ObjClasses
*/
class Mtree{
var $dbc;
var $config;
var $treeTable;
var $rootNodeName;
/** Constructor
*
* @param dbc object
* @param config array
* @return this
*/
function Mtree(&$dbc, $config)
{
$this->dbc =& $dbc;
$this->config = $config;
$this->treeTable = $config['tblNamePrefix'].'tree';
$this->rootNodeName = $config['RootNode'];
}
/* ======================================================= public methods */
/**
* Add new object of specified type to the tree under specified parent
* node as last child or after specified sibling
*
* @param name string
* @param type string
* @param parid int, optional, parent id
* @param aftid int, optional, after id
* @return int/err - new id of inserted object or PEAR::error
*/
function addObj($name, $type, $parid=1, $aftid=NULL)
{
if($name=='' || $type=='') return PEAR::raiseError(
"Mtree::addObj: Wrong name or type", ALIBERR_MTREE
);
if(!is_numeric($parid)) return PEAR::raiseError(
"Mtree::addObj: Wrong parid ($parid)", ALIBERR_MTREE
);
if(!is_numeric($aftid) && !is_null($aftid)) return PEAR::raiseError(
"Mtree::addObj: Wrong aftid ($aftid)", ALIBERR_MTREE
);
$this->dbc->query("BEGIN");
$r = $this->dbc->query("LOCK TABLE {$this->treeTable}");
if(PEAR::isError($r)) return $r;
// position resolving:
if(is_null($aftid)){ // add object as last child
$after = $this->dbc->getOne("
SELECT max(rgt) FROM {$this->treeTable} WHERE parid='$parid'
");
}else{ // use 'aftid'
$after = $this->dbc->getOne("
SELECT ".($aftid == $parid ? 'lft' : 'rgt')."
FROM {$this->treeTable} WHERE id='$aftid'");
}
if(PEAR::isError($after)) return $this->_dbRollback($after);
if(is_null($after)){ // position not specified - add as first child
$after = $this->dbc->getOne("
SELECT lft FROM {$this->treeTable} WHERE id='$parid'
");
}
if(PEAR::isError($after)) return $this->_dbRollback($after);
$after = intval($after);
// tree level resolving:
$level = $this->dbc->getOne("SELECT level FROM {$this->treeTable}
WHERE id='$parid'");
if(is_null($level))
return $this->_dbRollback('addObj: parent does not exist');
if(PEAR::isError($level)) return $this->_dbRollback($level);
$id = $this->dbc->nextId("{$this->treeTable}_id_seq");
if(PEAR::isError($id)) return $this->_dbRollback($id);
// creating space in rgt/lft sequencies:
$r = $this->dbc->query("UPDATE {$this->treeTable} SET rgt=rgt+2
WHERE rgt>$after");
if(PEAR::isError($r)) return $this->_dbRollback($r);
$r = $this->dbc->query("UPDATE {$this->treeTable} SET lft=lft+2
WHERE lft>$after");
if(PEAR::isError($r)) return $this->_dbRollback($r);
// inserting object:
$r = $this->dbc->query("
INSERT INTO {$this->treeTable}
(id, name, type, parid, level, lft, rgt)
VALUES
('$id', '$name', '$type', $parid,
".($level+1).", ".($after+1).", ".($after+2)."
)
");
if(PEAR::isError($r)) return $this->_dbRollback($r);
$r = $this->dbc->query("COMMIT");
if(PEAR::isError($r)) return $this->_dbRollback($r);
return $id;
}
/**
* Create copy of specified object and insert copy to new position
*
* @param id int, source object id
* @param newParid int, destination parent id
* @param after int, optional, destinantion after id
* @return int/err - new id of inserted object or PEAR::error
*/
function copyObj($id, $newParid, $after=NULL)
{
$o = $this->dbc->getRow("SELECT * FROM {$this->treeTable}
WHERE id='$id'");
if(PEAR::isError($o)) return $o;
$parid = $this->getParent($id);
if($newParid == $parid) $o['name'] .= "_copy";
$nid = $this->addObj(
$o['name'], $o['type'], $newParid, $after, $o['param']
);
return $nid;
}
/**
* Rename of specified object
*
* @param id int, object id to rename
* @param newName string, new name
* @return boolean/err - True or PEAR::error
*/
function renameObj($id, $newName)
{
$r = $this->dbc->query("UPDATE {$this->treeTable} SET name='$newName'
WHERE id='$id'");
if(PEAR::isError($r)) return $r;
return TRUE;
}
/**
* Remove of specified object
*
* @param id int, object id to remove
* @return boolean/err - TRUE or PEAR::error
*/
function removeObj($id)
{
$dirarr = $this->getDir($id); if(PEAR::isError($dirarr)) return $dirarr;
foreach($dirarr as $k=>$snod)
{
$this->removeObj($snod['id']);
}
$this->dbc->query("BEGIN");
$r = $this->dbc->query("LOCK TABLE {$this->treeTable}");
if(PEAR::isError($r)) return $r;
$rgt = $this->dbc->getOne("SELECT rgt FROM {$this->treeTable}
WHERE id='$id'");
if(is_null($rgt))
return $this->_dbRollback('removeObj: object not exists');
// deleting object:
$r = $this->dbc->query("DELETE FROM {$this->treeTable} WHERE id='$id'");
if(PEAR::isError($r)) return $this->_dbRollback($r);
// closing the space in rgt/lft sequencies:
$r = $this->dbc->query("UPDATE {$this->treeTable} SET rgt=rgt-2
WHERE rgt>$rgt");
if(PEAR::isError($r)) return $this->_dbRollback($r);
$r = $this->dbc->query("UPDATE {$this->treeTable} SET lft=lft-2
WHERE lft>$rgt");
if(PEAR::isError($r)) return $this->_dbRollback($r);
$r = $this->dbc->query("COMMIT");
if(PEAR::isError($r)) return $this->_dbRollback($r);
return TRUE;
}
/* --------------------------------------------------------- info methods */
/**
* Search for child id by name
*
* @param name string, searched name
* @param parId int, optional, parent id (default is root node)
* @return int/null/err - child id (if found) or null or PEAR::error
*/
function getObjId($name, $parId=NULL)
{
if($name=='' && is_null($parId)) $name = $this->rootNodeName;
$r = $this->dbc->getOne(
"SELECT id FROM {$this->treeTable}
WHERE name='$name' and ".(is_null($parId) ? "parid is null" : "parid='$parId'")
);
if(PEAR::isError($r)) return $r;
return $r;
}
/**
* Get one value for object (default: get name) by id
*
* @param oid int
* @param fld string, optional, requested field (default: name)
* @return string/err
*/
function getObjName($oid, $fld='name')
{
return $this->dbc->getOne("SELECT $fld FROM {$this->treeTable}
WHERE id='$oid'");
}
/**
* Get object type by id
*
* @param oid int
* @return string/err
*/
function getObjType($oid)
{
return $this->getObjName($oid, 'type');
}
/**
* Get parent id
*
* @param oid int
* @return int/err
*/
function getParent($oid)
{
return $this->getObjName($oid, 'parid');
}
/**
* Get array of nodes in object's path from root node
*
* @param id int
* @param flds string, optional
* @return array/err
*/
function getPath($id, $flds='id')
{
$this->dbc->query("BEGIN");
$a = $this->dbc->getRow("SELECT name, lft, rgt FROM {$this->treeTable}
WHERE id='$id'");
$res = $this->dbc->getAll("
SELECT $flds FROM {$this->treeTable}
WHERE lft<={$a['lft']} AND rgt>={$a['rgt']}
ORDER by lft
");
$this->dbc->query("COMMIT");
return $res;
}
/**
* Get array of childnodes
*
* @param id int
* @param flds string, optional, comma separated list of requested fields
* @param order string, optional, fieldname for order by clause
* @return array/err
*/
function getDir($id, $flds='id', $order='lft')
{
return $this->dbc->getAll("
SELECT $flds FROM {$this->treeTable}
WHERE parid='$id' ORDER BY $order
");
}
/**
* Get subtree of specified node
*
* @param id int, optional, default: root node
* @param withRoot boolean, optional, include/exclude specified node
* @return array/err
*/
function getSubTree($id=NULL, $withRoot=FALSE)
{
if(is_null($id)) $id = $this->getRootNode();
$r = array();
if($withRoot) $r[] = $re = $this->dbc->getRow(
"SELECT id, name, level FROM {$this->treeTable} WHERE id='$id'"
);
if(PEAR::isError($r)) return $r;
$dirarr = $this->getDir($id); if(PEAR::isError($dirarr)) return $dirarr;
foreach($dirarr as $k=>$snod)
{
$r[] = $re = $this->dbc->getRow("SELECT id, name, level
FROM {$this->treeTable} WHERE id={$snod['id']}");
if(PEAR::isError($re)) return $re;
$r = array_merge($r, $this->getSubTree($snod['id']));
}
return $r;
}
/**
* Get id of root node
*
* @return int/err
*/
function getRootNode()
{
return $this->getObjId($this->rootNodeName);
}
/**
* Get all objects in the tree
*
* @return array/err
*/
function getAllObjects()
{
return $this->dbc->getAll(
"SELECT * FROM {$this->treeTable} ORDER BY lft"
);
}
/* ------------------------ info methods related to application structure */
/* (this part should be added/rewritten to allow defining/modifying/using
* application structure)
* (only very simple structure definition - in $config - supported now)
*/
/**
* Get child types allowed by application definition
*
* @param type string
* @return array
*/
function getAllowedChildTypes($type)
{
return $this->config['objtypes'][$type];
}
/* ==================================================== "private" methods */
/**
* Do SQL rollback and return PEAR::error
*
* @param r object/string
* @return err
*/
function _dbRollback($r)
{
$this->dbc->query("ROLLBACK");
if(PEAR::isError($r)) return $r;
elseif(is_string($r)) return PEAR::raiseError(
"ERROR: ".get_class($this).": $r", ALIBERR_MTREE, PEAR_ERROR_RETURN
);
else return PEAR::raiseError(
"ERROR: ".get_class($this).": unknown error",
ALIBERR_MTREE, PEAR_ERROR_RETURN
);
}
/**
* Move subtree to another node without removing/adding
* Little bit complicated - sorry - it probably should be simlified ... ;)
*
* @param id int
* @param newParid int
* @param after int
* @return boolean/err
*/
function _relocateSubtree($id, $newParid, $after=NULL)
{
$this->dbc->query("BEGIN");
$r = $this->dbc->query("LOCK TABLE {$this->treeTable}");
if(PEAR::isError($r)) return $r;
// obtain values for source node:
$a1 = $this->dbc->getRow("SELECT lft, rgt, level FROM {$this->treeTable}
WHERE id='$id'");
if(is_null($a1))
return $this->_dbRollback('_relocateSubtree: object not exists');
extract($a1);
// values for destination node:
$a2 = $this->dbc->getRow("SELECT rgt, level FROM {$this->treeTable}
WHERE id='$newParid'");
if(is_null($a2))return $this->_dbRollback(
'_relocateSubtree: new parent not exists'
);
$nprgt = $a2['rgt']; $newLevel = $a2['level'];
// calculate differencies:
if(is_null($after)) $after = $nprgt-1;
$dif1 = $rgt-$lft+1;
$dif2 = $after-$lft+1;
$dif3 = $newLevel-$level+1;
// relocate the object"
$r = $this->dbc->query(
"UPDATE {$this->treeTable} SET parid='$newParid' WHERE id='$id'");
if(PEAR::isError($r)) return $this->_dbRollback($r);
if($after>$rgt){
// relocate subtree to the right:
$r = $this->dbc->query(
"UPDATE {$this->treeTable} SET rgt=rgt+$dif1 WHERE rgt>$after");
if(PEAR::isError($r)) return $this->_dbRollback($r);
$r = $this->dbc->query(
"UPDATE {$this->treeTable} SET lft=lft+$dif1 WHERE lft>$after");
if(PEAR::isError($r)) return $this->_dbRollback($r);
$r = $this->dbc->query("UPDATE {$this->treeTable}
SET lft=lft+$dif2, rgt=rgt+$dif2, level=level+$dif3
WHERE lft>=$lft AND rgt <=$rgt");
if(PEAR::isError($r)) return $this->_dbRollback($r);
$r = $this->dbc->query(
"UPDATE {$this->treeTable} SET rgt=rgt-$dif1 WHERE rgt>$rgt");
if(PEAR::isError($r)) return $this->_dbRollback($r);
$r = $this->dbc->query(
"UPDATE {$this->treeTable} SET lft=lft-$dif1 WHERE lft>$rgt");
if(PEAR::isError($r)) return $this->_dbRollback($r);
}else{
// relocate subtree to the left:
$r = $this->dbc->query(
"UPDATE {$this->treeTable} SET rgt=rgt+$dif1 WHERE rgt>$after");
if(PEAR::isError($r)) return $this->_dbRollback($r);
$r = $this->dbc->query(
"UPDATE {$this->treeTable} SET lft=lft+$dif1 WHERE lft>$after");
if(PEAR::isError($r)) return $this->_dbRollback($r);
$r = $this->dbc->query("UPDATE {$this->treeTable}
SET lft=lft+$dif2-$dif1, rgt=rgt+$dif2-$dif1, level=level+$dif3
WHERE lft>=$lft+$dif1 AND rgt <=$rgt+$dif1");
if(PEAR::isError($r)) return $this->_dbRollback($r);
$r = $this->dbc->query("UPDATE {$this->treeTable} SET rgt=rgt-$dif1
WHERE rgt>$rgt+$dif1");
if(PEAR::isError($r)) return $this->_dbRollback($r);
$r = $this->dbc->query("UPDATE {$this->treeTable} SET lft=lft-$dif1
WHERE lft>$rgt+$dif1");
if(PEAR::isError($r)) return $this->_dbRollback($r);
}
$r = $this->dbc->query("COMMIT");
if(PEAR::isError($r)) return $this->_dbRollback($r);
return TRUE;
}
/**
* Recursive copyObj - copy of whole subtree
*
* @param id int
* @param newParid int
* @param after int
* @return array
*/
function _copySubtree($id, $newParid, $after=NULL)
{
$nid = $this->copyObj($id, $newParid, $after);
if(PEAR::isError($nid)) return $nid;
$dirarr = $this->getDir($id); if(PEAR::isError($dirarr)) return $dirarr;
foreach($dirarr as $k=>$snod)
{
$r = $this->_copySubtree($snod['id'], $nid);
if(PEAR::isError($r)) return $r;
}
return TRUE;
}
/* =============================================== test and debug methods */
/**
* Human readable dump of subtree - for debug
*
* @param id int
* @param indstr string, indentation string
* @param ind string, aktual indentation
* @return string
*/
function dumpTree($id=NULL, $indstr=' ', $ind='',
$format='{name}', $withRoot=TRUE)
{
$r='';
foreach($this->getSubTree($id, $withRoot) as $o)
$r .= str_repeat($indstr, intval($o['level'])).
preg_replace(array('|\{name\}|', '|\{id\}|'),
array($o['name'], $o['id']), $format).
"\n";
return $r;
}
/**
* Delete all nodes except the root
*
*/
function deleteData()
{
$this->dbc->query("DELETE FROM {$this->treeTable}
WHERE parid is not null");
}
/**
* Insert test data to the tree
*
* @return array
*/
function testData()
{
$o[] = $rootId = $this->getRootNode();
$o[] = $p1 = $this->addObj('Publication A', 'Publication', $rootId); //1
$o[] = $p2 = $this->addObj('Publication B', 'Publication', $rootId); //2
$o[] = $i1 = $this->addObj('Issue 1', 'Issue', $p1); //3
$o[] = $i2 = $this->addObj('Issue 2', 'Issue', $p1); //4
$o[] = $s1 = $this->addObj('Section a', 'Section', $i2);
$o[] = $s2 = $this->addObj('Section b', 'Section', $i2); //6
$o[] = $s3 = $this->addObj('Section c', 'Section', $i2);
$o[] = $t1 = $this->addObj('Title', 'Title', $s2);
$o[] = $s4 = $this->addObj('Section a', 'Section', $i1);
$o[] = $s5 = $this->addObj('Section b', 'Section', $i1);
$this->tdata['tree'] = $o;
}
/**
* Make basic test
*
*/
function test()
{
$this->deleteData();
$this->testData();
$rootId = $this->getRootNode();
$this->test_correct ="RootNode\n Publication A\n Issue 1\n".
" Section a\n Section b\n Issue 2\n".
" Section a\n Section b\n".
" Title\n Section c\n Publication B\n".
"RootNode\n";
$this->test_dump = $this->dumpTree();
$this->removeObj($this->tdata['tree'][1]);
$this->removeObj($this->tdata['tree'][2]);
$this->test_dump .= $this->dumpTree();
$this->deleteData();
if($this->test_dump == $this->test_correct){
$this->test_log.="tree: OK\n"; return TRUE;
}else return PEAR::raiseError('Mtree::test:', 1, PEAR_ERROR_DIE, '%s'.
"<pre>\ncorrect:\n.{$this->test_correct}.\n".
"dump:\n.{$this->test_dump}.\n</pre>\n");
}
/**
* Create tables + initialize
*
*/
function install()
{
$this->dbc->query("CREATE TABLE {$this->treeTable} (
id int not null PRIMARY KEY,
name varchar(255) not null default'',
parid int,
lft int,
rgt int,
level int,
type varchar(255) not null default'',
param varchar(255)
)");
$this->dbc->query("CREATE UNIQUE INDEX {$this->treeTable}_id_idx
ON {$this->treeTable} (id)");
$this->dbc->query("CREATE INDEX {$this->treeTable}_name_idx
ON {$this->treeTable} (name)");
$this->dbc->createSequence("{$this->treeTable}_id_seq");
$id = $this->dbc->nextId("{$this->treeTable}_id_seq");
$this->dbc->query("INSERT INTO {$this->treeTable}
(id, name, parid, level, lft, rgt, type)
VALUES
($id, '{$this->rootNodeName}', NULL, 0, 1, 2, 'RootNode')");
}
/**
* Drop tables etc.
*
*/
function uninstall()
{
$this->dbc->query("DROP TABLE {$this->treeTable}");
$this->dbc->dropSequence("{$this->treeTable}_id_seq");
}
/**
* Uninstall and install
*
*/
function reinstall()
{
$this->uninstall();
$this->install();
}
}
?>

View File

@ -23,7 +23,7 @@
Author : $Author: tomas $
Version : $Revision: 1.5 $
Version : $Revision: 1.6 $
Location : $Source: /home/paul/cvs2svn-livesupport/newcvsrepo/livesupport/modules/alib/var/subj.php,v $
------------------------------------------------------------------------------*/
@ -39,7 +39,7 @@ define('ALIBERR_BADSMEMB', 21);
* (allow adding users to groups or groups to groups)
*
* @author $Author: tomas $
* @version $Revision: 1.5 $
* @version $Revision: 1.6 $
* @see ObjClasses
* @see Alib
*/
@ -71,6 +71,9 @@ class Subjects extends ObjClasses{
*/
function addSubj($login, $pass=NULL)
{
if(!$login) return $this->dbc->raiseError(
get_class($this)."::addSubj: empty login"
);
$id = $this->dbc->nextId("{$this->subjTable}_id_seq");
if(PEAR::isError($id)) return $id;
$r = $this->dbc->query("
@ -465,21 +468,22 @@ class Subjects extends ObjClasses{
function testData()
{
parent::testData();
$o[] = $this->addSubj('root', 'q');
$o[] = $this->addSubj('test1', 'a');
$o[] = $this->addSubj('test2', 'a');
$o[] = $this->addSubj('test3', 'a');
$o[] = $this->addSubj('test4', 'a');
$o[] = $this->addSubj('gr1');
$o[] = $this->addSubj('gr2');
# $this->addSubj2Gr('test1', 'gr1');
$this->addSubj2Gr('test2', 'gr1');
$this->addSubj2Gr('test3', 'gr1');
$this->addSubj2Gr('test4', 'gr2');
$this->addSubj2Gr('gr2', 'gr1');
$o[] = $this->addSubj('gr3');
$o['root'] = $this->addSubj('root', 'q');
$o['test1'] = $this->addSubj('test1', 'a');
$o['test2'] = $this->addSubj('test2', 'a');
$o['test3'] = $this->addSubj('test3', 'a');
$o['test4'] = $this->addSubj('test4', 'a');
$o['test5'] = $this->addSubj('test5', 'a');
$o['gr1'] = $this->addSubj('gr1');
$o['gr2'] = $this->addSubj('gr2');
$o['gr3'] = $this->addSubj('gr3');
$o['gr4'] = $this->addSubj('gr4');
$this->addSubj2Gr('test1', 'gr1');
$this->addSubj2Gr('test2', 'gr2');
$this->addSubj2Gr('test3', 'gr3');
$this->addSubj2Gr('test1', 'gr3');
$this->addSubj2Gr('test4', 'gr4');
$this->addSubj2Gr('test5', 'gr1');
$this->addSubj2Gr('gr4', 'gr3');
$this->addSubj2Gr('gr3', 'gr2');
return $this->tdata['subjects'] = $o;
}
@ -494,14 +498,14 @@ class Subjects extends ObjClasses{
$this->deleteData();
$this->testData();
$this->test_correct = "root(0), test1(0), test2(0), test3(0),".
" test4(0), gr1(3), gr2(2), gr3(2)\n";
" test4(0), test5(0), gr1(2), gr2(2), gr3(2), gr4(1)\n";
$this->test_dump = $this->dumpSubjects();
$this->removeSubj('test1');
$this->removeSubj('test3');
$this->removeSubjFromGr('test2', 'gr1');
$this->removeSubjFromGr('test5', 'gr1');
$this->removeSubjFromGr('gr3', 'gr2');
$this->test_correct .= "root(0), test2(0), test4(0), gr1(1),".
" gr2(1), gr3(0)\n";
$this->test_correct .= "root(0), test2(0), test4(0), test5(0),".
" gr1(0), gr2(1), gr3(1), gr4(1)\n";
$this->test_dump .= $this->dumpSubjects();
$this->deleteData();
if($this->test_dump == $this->test_correct)