<?php

/**
 * A class that implements the Slony 1.0.x support plugin
 *
 * $Id: Slony.php,v 1.15 2007/10/02 21:36:35 ioguix Exp $
 */

include_once('./classes/plugins/Plugin.php');

class Slony extends Plugin {

	var $slony_version;
	var $slony_schema;
	var $slony_cluster;
	var $slony_owner;
	var $slony_comment;
	var $enabled = null;
	
	/**
	 * Constructor
	 */
	function Slony() {
		$this->Plugin('slony');
		$this->isEnabled();
	}

	/**
	 * Determines whether or not Slony is installed in the current
	 * database.
	 * @post Will populate version and schema fields, etc.
	 * @return True if Slony is installed, false otherwise.
	 */
	function isEnabled() {
		// Access cache
		if ($this->enabled !== null) return $this->enabled;
		else $this->enabled = false;
		
		global $data;
		
		// Slony needs schemas
		if (!$data->hasSchemas()) return false;
		
		// Check for the slonyversion() function and find the schema
		// it's in.  We put an order by and limit 1 in here to guarantee
		// only finding the first one, even if there are somehow two
		// Slony schemas.
		$sql = "SELECT pn.nspname AS schema, pu.usename AS owner, 
                    SUBSTRING(pn.nspname FROM 2) AS cluster,
                    pg_catalog.obj_description(pn.oid, 'pg_namespace') AS 
                    nspcomment
                    FROM pg_catalog.pg_proc pp, pg_catalog.pg_namespace pn, 
                    pg_catalog.pg_user pu 
					WHERE pp.pronamespace=pn.oid
					AND pn.nspowner = pu.usesysid
					AND pp.proname='slonyversion'
					AND pn.nspname LIKE '@_%' ESCAPE '@'  
					ORDER BY pn.nspname LIMIT 1";
		$rs = $data->selectSet($sql);
		if ($rs->recordCount() == 1) {
			$schema = $rs->fields['schema'];
			$this->slony_schema = $schema;
			$this->slony_owner = $rs->fields['owner'];
			$this->slony_comment = $rs->fields['nspcomment'];
			// Cluster name is schema minus "_" prefix.
			$this->slony_cluster = $rs->fields['cluster'];
			$data->fieldClean($schema);
			$sql = "SELECT \"{$schema}\".slonyversion() AS version";
			$version = $data->selectField($sql, 'version');
			if ($version === -1) return false;
			else {
				$this->slony_version = $version;
				$this->enabled = true;
				return true;
			}
		}
		else return false;
	}	

	// CLUSTERS

	/**
	 * Gets the clusters in this database
	 */
	function getClusters() {
		include_once('./classes/ArrayRecordSet.php');

		if ($this->isEnabled()) {
			$clusters = array(array('cluster' => $this->slony_cluster, 'comment' => $this->slony_comment));
		}
		else
			$clusters = array();

		return new ArrayRecordSet($clusters);
	}
	
	/**
	 * Gets a single cluster
	 */
	function getCluster() {
		global $data;

		$schema = $this->slony_schema;
		$data->fieldClean($schema);
		$data->clean($no_id);
		
		$sql = "SELECT no_id, no_comment, \"{$schema}\".slonyversion() AS version
					FROM \"{$schema}\".sl_local_node_id, \"{$schema}\".sl_node
					WHERE no_id=last_value";
		
		
		return $data->selectSet($sql);
	}

	/**
	 * Drops an entire cluster.
	 */
	function dropCluster() {
		global $data;
		
		$schema = $this->slony_schema;
		$data->fieldClean($schema);

		$sql = "SELECT \"{$schema}\".uninstallnode(); DROP SCHEMA \"{$schema}\" CASCADE";
		
		$status = $data->execute($sql);
		if ($status == 0) {
			$this->enabled = null;
			$enabled = $this->isEnabled();
		}
		return $status;
	}
	
	/**
	 * Helper function to get a file into a string and replace
	 * variables.
	 * @return The file contents, or FALSE on error.
	 */
	function _getFile($file, $cluster) {
		global $data,$misc;
		$schema = '_' . $cluster;
		$data->fieldClean($cluster);
		
		$server_info = $misc->getServerInfo();
		$path = $server_info['slony_sql'] . '/' . $file;
		
		// Check that we can access the file
		if (!file_exists($path) || !is_readable($path)) return false;

		$buffer = null;
		$handle = fopen($path, 'r');
		if ($handle === false) return false;
		while (!feof($handle)) {
		   $temp = fgets($handle, 4096);
		   $temp = str_replace('@CLUSTERNAME@', $cluster, $temp);
		   
		   $temp = str_replace('@NAMESPACE@', $schema, $temp);
		   $buffer .= $temp;
		}
		fclose($handle);
		
		return $buffer;
	}
	
	/**
	 * Initializes a new cluster
	 */
	function initCluster($name, $no_id, $no_comment) {
		global $data, $misc;
		
		// Prevent timeouts since cluster initialization can be slow
		if (!ini_get('safe_mode')) set_time_limit(0);

		$server_info = $misc->getServerInfo();
		
		if (!$data->isSuperUser($server_info['username'])) {
			return -10;
		}

		// Determine Slony compatibility version.
		if ($data->major_version == 7.3)
			$ver = '73';
		elseif ($data->major_version >= 7.4)
			$ver = '74';
		else {
			return -11;
		}
		
		$status = $data->beginTransaction();
		if ($status != 0) return -1;

		// Create the schema
		$status = $data->createSchema('_' . $name);
		if ($status != 0) {
			$data->rollbackTransaction();
			return -2;
		}

		$sql = $this->_getFile("xxid.v{$ver}.sql", $name);
		if ($sql === false) {
			$data->rollbackTransaction();
			return -6;
		}			
		$status = $data->execute($sql);
		if ($status != 0) {
			$data->rollbackTransaction();
			return -3;
		}
		
		$sql = $this->_getFile('slony1_base.sql', $name);
		if ($sql === false) {
			$data->rollbackTransaction();
			return -6;
		}			
		$status = $data->execute($sql);
		if ($status != 0) {
			$data->rollbackTransaction();
			return -3;
		}
/* THIS FILE IS EMPTY AND JUST CAUSES ERRORS
		$sql = $this->_getFile('slony1_base.v74.sql', $name);
		$status = $data->execute($sql);
		if ($status != 0) {
			$data->rollbackTransaction();
			return -3;
		}
*/
		$sql = $this->_getFile('slony1_funcs.sql', $name);
		if ($sql === false) {
			$data->rollbackTransaction();
			return -6;
		}			
		$status = $data->execute($sql);
		if ($status != 0) {
			$data->rollbackTransaction();
			return -3;
		}

		$sql = $this->_getFile("slony1_funcs.v{$ver}.sql", $name);
		if ($sql === false) {
			$data->rollbackTransaction();
			return -6;
		}			
		$status = $data->execute($sql);
		if ($status != 0) {
			$data->rollbackTransaction();
			return -3;
		}
		
		$this->enabled = null;
		$enabled = $this->isEnabled();
		if (!$enabled) {
			$data->rollbackTransaction();
			return -4;
		}
		
		$schema = $this->slony_schema;
		$data->fieldClean($schema);
		$data->clean($no_id);
		$data->clean($no_comment);

		$sql = "SELECT \"{$schema}\".initializelocalnode('{$no_id}', '{$no_comment}'); SELECT \"{$schema}\".enablenode('{$no_id}')";
		$status = $data->execute($sql);
		if ($status != 0) {
			$data->rollbackTransaction();
			return -5;
		}
		
		return $data->endTransaction();
	}
	
	// NODES

	/**
	 * Gets the nodes in this database
	 */
	function getNodes() {
		global $data;
		
		$schema = $this->slony_schema;
		$data->fieldClean($schema);
		
		// We use 10 seconds as the default check time since that is the
		// the default in Slony, and it gives no mechanism to look it up
		$sql = "SELECT no_id, no_active, no_comment, no_spool, ".
				"CASE WHEN st_lag_time > '10 seconds'::interval ". 
				"THEN 'outofsync' ELSE 'insync' END AS no_status ".
				"FROM \"{$schema}\".sl_node ".
				"LEFT JOIN \"{$schema}\".sl_status ON (no_id =st_received) ".
				"ORDER BY no_comment";

		return $data->selectSet($sql);
	}

	/**
	 * Gets a single node
	 */
	function getNode($no_id) {
		global $data;

		$schema = $this->slony_schema;
		$data->fieldClean($schema);
		$data->clean($no_id);
		
		$sql = "SELECT * FROM \"{$schema}\".sl_node WHERE no_id='{$no_id}'";
		
		return $data->selectSet($sql);
	}
	
	/**
	 * Creates a node
	 */
	function createNode($no_id, $no_comment) {
		global $data;

		$schema = $this->slony_schema;
		$data->fieldClean($schema);
		$data->clean($no_comment);
		$data->clean($no_id);
		
		if ($no_id != '')
			$sql = "SELECT \"{$schema}\".storenode('{$no_id}', '{$no_comment}')";
		else
			$sql = "SELECT \"{$schema}\".storenode((SELECT COALESCE(MAX(no_id), 0) + 1 FROM \"{$schema}\".sl_node), '{$no_comment}')";

		return $data->execute($sql);
	}

	/**
	 * Drops a node
	 */
	function dropNode($no_id) {
		global $data;

		$schema = $this->slony_schema;
		$data->fieldClean($schema);
		$data->clean($no_id);

		$sql = "SELECT \"{$schema}\".dropnode('{$no_id}')";

		return $data->execute($sql);
	}	
	
	// REPLICATION SETS
	
	/**
	 * Gets the replication sets in this database
	 */
	function getReplicationSets() {
		global $data;
		
		$schema = $this->slony_schema;
		$data->fieldClean($schema);
		
		$sql = "SELECT *, set_locked IS NOT NULL AS is_locked FROM \"{$schema}\".sl_set ORDER BY set_id";
		
		return $data->selectSet($sql);
	}

	/**
	 * Gets a particular replication set
	 */
	function getReplicationSet($set_id) {
		global $data;
		
		$schema = $this->slony_schema;
		$data->fieldClean($schema);
		$data->clean($set_id);
		
		$sql = "SELECT *, (SELECT COUNT(*) FROM \"{$schema}\".sl_subscribe ssub WHERE ssub.sub_set=ss.set_id) AS subscriptions,
					set_locked IS NOT NULL AS is_locked
					FROM \"{$schema}\".sl_set ss, \"{$schema}\".sl_node sn
					WHERE ss.set_origin=sn.no_id
					AND set_id='{$set_id}'";
		
		return $data->selectSet($sql);
	}

	/**
	 * Creates a set
	 */
	function createReplicationSet($set_id, $set_comment) {
		global $data;

		$schema = $this->slony_schema;
		$data->fieldClean($schema);
		$data->clean($set_comment);
		$data->clean($set_id);

		if ($set_id != '')
			$sql = "SELECT \"{$schema}\".storeset('{$set_id}', '{$set_comment}')";
		else
			$sql = "SELECT \"{$schema}\".storeset((SELECT COALESCE(MAX(set_id), 0) + 1 FROM \"{$schema}\".sl_set), '{$set_comment}')";

		return $data->execute($sql);
	}

	/**
	 * Drops a set
	 */
	function dropReplicationSet($set_id) {
		global $data;

		$schema = $this->slony_schema;
		$data->fieldClean($schema);
		$data->clean($set_id);

		$sql = "SELECT \"{$schema}\".dropset('{$set_id}')";

		return $data->execute($sql);
	}	

	/**
	 * Locks or unlocks a set
	 * @param boolean $lock True to lock, false to unlock
	 */
	function lockReplicationSet($set_id, $lock) {
		global $data;

		$schema = $this->slony_schema;
		$data->fieldClean($schema);
		$data->clean($set_id);

		if ($lock)
			$sql = "SELECT \"{$schema}\".lockset('{$set_id}')";
		else
			$sql = "SELECT \"{$schema}\".unlockset('{$set_id}')";		

		return $data->execute($sql);
	}	
		
	/**
	 * Merges two sets
	 */
	function mergeReplicationSet($set_id, $target) {
		global $data;

		$schema = $this->slony_schema;
		$data->fieldClean($schema);
		$data->clean($set_id);
		$data->clean($target);

		$sql = "SELECT \"{$schema}\".mergeset('{$target}', '{$set_id}')";

		return $data->execute($sql);
	}

	/**
	 * Moves a set to a new origin
	 */
	function moveReplicationSet($set_id, $new_origin) {
		global $data;

		$schema = $this->slony_schema;
		$data->fieldClean($schema);
		$data->clean($set_id);
		$data->clean($new_origin);

		$sql = "SELECT \"{$schema}\".moveset('{$set_id}', '{$new_origin}')";

		return $data->execute($sql);
	}

	/**
	 * Executes schema changing DDL set on nodes
	 */
	function executeReplicationSet($set_id, $script) {
		global $data;

		$schema = $this->slony_schema;
		$data->fieldClean($schema);
		$data->clean($set_id);
		$data->clean($script);

		$sql = "SELECT \"{$schema}\".ddlscript('{$set_id}', '{$script}')";

		return $data->execute($sql);
	}	
	
	// TABLES
	
	/**
	 * Return all tables in a replication set
	 * @param $set_id The ID of the replication set
	 * @return Tables in the replication set, sorted alphabetically 
	 */
	function getTables($set_id) {
		global $data;

		$schema = $this->slony_schema;
		$data->fieldClean($schema);
		$data->clean($set_id);		

		$sql = "SELECT st.tab_id, c.relname, n.nspname, n.nspname||'.'||c.relname AS qualname,
					pg_catalog.pg_get_userbyid(c.relowner) AS relowner, 
					reltuples::bigint";
		// Tablespace
		if ($data->hasTablespaces()) {
			$sql .= ", (SELECT spcname FROM pg_catalog.pg_tablespace pt WHERE pt.oid=c.reltablespace) AS tablespace";
		}
		$sql .= " FROM pg_catalog.pg_class c, \"{$schema}\".sl_table st, pg_catalog.pg_namespace n
					WHERE c.oid=st.tab_reloid
					AND c.relnamespace=n.oid
					AND st.tab_set='{$set_id}'
					ORDER BY n.nspname, c.relname";

		return $data->selectSet($sql);
	}

	/**
	 * Adds a table to a replication set
	 */
	function addTable($set_id, $tab_id, $nspname, $relname, $idxname, $comment, $storedtriggers) {
		global $data;

		$schema = $this->slony_schema;
		$data->fieldClean($schema);
		$data->clean($set_id);
		$data->clean($tab_id);
		$fqname = $nspname . '.' . $relname;
		$data->clean($fqname);
		$data->clean($nspname);
		$data->clean($relname);
		$data->clean($idxname);
		$data->clean($comment);

		$hastriggers = (sizeof($storedtriggers) > 0);
		if ($hastriggers) {
			// Begin a transaction
			$status = $data->beginTransaction();
			if ($status != 0) return -1;
		}

		if ($tab_id != '')
			$sql = "SELECT \"{$schema}\".setaddtable('{$set_id}', '{$tab_id}', '{$fqname}', '{$idxname}', '{$comment}')";
		else {
			$sql = "SELECT \"{$schema}\".setaddtable('{$set_id}', (SELECT COALESCE(MAX(tab_id), 0) + 1 FROM \"{$schema}\".sl_table), '{$fqname}', '{$idxname}', '{$comment}')";			
		}

		$status = $data->execute($sql);	
		if ($status != 0) {
			if ($hastriggers) $data->rollbackTransaction();
			return -3;
		}		

		// If we are storing triggers, we need to know the tab_id that was assigned to the table
		if ($tab_id == '' && $hastriggers) {
			$sql = "SELECT tab_id
						FROM \"{$schema}\".sl_table
						WHERE tab_set='{$set_id}'
						AND tab_reloid=(SELECT pc.oid FROM pg_catalog.pg_class pc, pg_namespace pn 
												WHERE pc.relnamespace=pn.oid AND pc.relname='{$relname}'
												AND pn.nspname='{$nspname}')";
			$tab_id = $data->selectField($sql, 'tab_id');
			if ($tab_id === -1) {
				$data->rollbackTransaction();
				return -4;
			}
		}
		
		// Store requested triggers
		if ($hastriggers) {
			foreach ($storedtriggers as $tgname) {
				$data->clean($tgname);
				$sql = "SELECT \"{$schema}\".storetrigger('{$tab_id}', '{$tgname}')";
				$status = $data->execute($sql);	
				if ($status != 0) {
					$data->rollbackTransaction();
					return -5;
				}		
			}				
		}

		if ($hastriggers)
			return $data->endTransaction();
		else
			return $status;
	}
		
	/**
	 * Drops a table from a replication set
	 */
	function dropTable($tab_id) {
		global $data;

		$schema = $this->slony_schema;
		$data->fieldClean($schema);
		$data->clean($tab_id);

		$sql = "SELECT \"{$schema}\".setdroptable('{$tab_id}')";

		return $data->execute($sql);
	}		

	/**
	 * Moves a table to another replication set
	 */
	function moveTable($tab_id, $new_set_id) {
		global $data;

		$schema = $this->slony_schema;
		$data->fieldClean($schema);
		$data->clean($tab_id);
		$data->clean($new_set_id);

		$sql = "SELECT \"{$schema}\".setmovetable('{$tab_id}', '{$new_set_id}')";

		return $data->execute($sql);
	}		

    /**
     * Return all tables we are not current in a replication set
     */
    function getNonRepTables()
    {
		global $data;
        /*
         * we cannot just query pg_tables as we want the OID of the table 
         * for the subquery against the slony table. We could match on
         * on schema name and table name, but the slony info isn't updated
         * if the user renames a table which is in a replication set
         */
        $sql = "SELECT c.relname, n.nspname, n.nspname||'.'||c.relname AS qualname, 
                    pg_get_userbyid(c.relowner) AS relowner
                    FROM pg_class c
                    LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
                    WHERE c.relkind = 'r' AND n.nspname NOT IN ('pg_catalog', 
                    'information_schema', 'pg_toast') AND
                    NOT(n.nspname = '_{$this->slony_cluster}' AND
                    relname LIKE 'sl_%') AND 
                    NOT EXISTS(SELECT 1 FROM _{$this->slony_cluster}.sl_table s
                    WHERE s.tab_reloid = c.oid)
                    ORDER BY n.nspname, c.relname";
        return $data->selectSet($sql);
    }

	// SEQUENCES
	
	/**
	 * Return all sequences in a replication set
	 * @param $set_id The ID of the replication set
	 * @return Sequences in the replication set, sorted alphabetically 
	 */
	function getSequences($set_id) {
		global $data;

		$schema = $this->slony_schema;
		$data->fieldClean($schema);
		$data->clean($set_id);		

		$sql = "SELECT ss.seq_id, c.relname AS seqname, n.nspname, n.nspname||'.'||c.relname AS qualname,
						pg_catalog.obj_description(c.oid, 'pg_class') AS seqcomment,
						pg_catalog.pg_get_userbyid(c.relowner) AS seqowner 
					FROM pg_catalog.pg_class c, \"{$schema}\".sl_sequence ss, pg_catalog.pg_namespace n
					WHERE c.oid=ss.seq_reloid
					AND c.relnamespace=n.oid
					AND ss.seq_set='{$set_id}'
					ORDER BY n.nspname, c.relname";

		return $data->selectSet($sql);
	}

	/**
	 * Adds a sequence to a replication set
	 */
	function addSequence($set_id, $seq_id, $fqname, $comment) {
		global $data;

		$schema = $this->slony_schema;
		$data->fieldClean($schema);
		$data->clean($set_id);
		$data->clean($seq_id);
		$data->clean($fqname);
		$data->clean($comment);

		if ($seq_id != '')
			$sql = "SELECT \"{$schema}\".setaddsequence('{$set_id}', '{$seq_id}', '{$fqname}', '{$comment}')";
		else
			$sql = "SELECT \"{$schema}\".setaddsequence('{$set_id}', (SELECT COALESCE(MAX(seq_id), 0) + 1 FROM \"{$schema}\".sl_sequence), '{$fqname}', '{$comment}')";

		return $data->execute($sql);	}		
		
	/**
	 * Drops a sequence from a replication set
	 */
	function dropSequence($seq_id) {
		global $data;

		$schema = $this->slony_schema;
		$data->fieldClean($schema);
		$data->clean($seq_id);

		$sql = "SELECT \"{$schema}\".setdropsequence('{$seq_id}')";

		return $data->execute($sql);
	}		

	/**
	 * Moves a sequence to another replication set
	 */
	function moveSequence($seq_id, $new_set_id) {
		global $data;

		$schema = $this->slony_schema;
		$data->fieldClean($schema);
		$data->clean($seq_id);
		$data->clean($new_set_id);

		$sql = "SELECT \"{$schema}\".setmovesequence('{$seq_id}', '{$new_set_id}')";

		return $data->execute($sql);
	}		
	
	// SUBSCRIPTIONS
	
	/**
	 * Gets all nodes subscribing to a set
	 * @param $set_id The ID of the replication set
	 * @return Nodes subscribing to this set
	 */
	function getSubscribedNodes($set_id) {
		global $data;
		
		$schema = $this->slony_schema;
		$data->fieldClean($schema);
		$data->clean($set_id);		
		
		$sql = "SELECT sn.*, ss.sub_set
					FROM \"{$schema}\".sl_subscribe ss, \"{$schema}\".sl_node sn
					WHERE ss.sub_set='{$set_id}'
					AND ss.sub_receiver = sn.no_id
					ORDER BY sn.no_comment";
		
		return $data->selectSet($sql);
	}

	/**
	 * Gets all nodes subscribing to a set
	 * @param $set_id The ID of the replication set
	 * @return Nodes subscribing to this set
	 */
	function getSubscription($set_id, $no_id) {
		global $data;
		
		$schema = $this->slony_schema;
		$data->fieldClean($schema);
		$data->clean($set_id);
		$data->clean($no_id);
		
		$sql = "SELECT ss.*, sn.no_comment AS receiver, sn2.no_comment AS provider
					FROM \"{$schema}\".sl_subscribe ss, \"{$schema}\".sl_node sn, \"{$schema}\".sl_node sn2
					WHERE ss.sub_set='{$set_id}'
					AND ss.sub_receiver = sn.no_id
					AND ss.sub_provider = sn2.no_id
					AND sn.no_id='{$no_id}'";
		
		return $data->selectSet($sql);
	}
	
	// NODES
	
	/**
	 * Gets node paths
	 */
	function getPaths($no_id) {
		global $data;

		$schema = $this->slony_schema;
		$data->fieldClean($schema);
		$data->clean($no_id);
		
		$sql = "SELECT * FROM \"{$schema}\".sl_path sp, \"{$schema}\".sl_node sn
					WHERE sp.pa_server=sn.no_id 
					AND sp.pa_client='{$no_id}'
					ORDER BY sn.no_comment";
		
		return $data->selectSet($sql);
	}

	/**
	 * Gets node path details
	 */
	function getPath($no_id, $path_id) {
		global $data;

		$schema = $this->slony_schema;
		$data->fieldClean($schema);
		$data->clean($no_id);
		$data->clean($path_id);
		
		$sql = "SELECT * FROM \"{$schema}\".sl_path sp, \"{$schema}\".sl_node sn
					WHERE sp.pa_server=sn.no_id 
					AND sp.pa_client='{$no_id}'
					AND sn.no_id='{$path_id}'";
		
		return $data->selectSet($sql);
	}

	/**
	 * Creates a path
	 */
	function createPath($no_id, $server, $conn, $retry) {
		global $data;

		$schema = $this->slony_schema;
		$data->fieldClean($schema);
		$data->clean($no_id);
		$data->clean($server);
		$data->clean($conn);
		$data->clean($retry);

		$sql = "SELECT \"{$schema}\".storepath('{$server}', '{$no_id}', '{$conn}', '{$retry}')";

		return $data->execute($sql);
	}

	/**
	 * Drops a path
	 */
	function dropPath($no_id, $path_id) {
		global $data;

		$schema = $this->slony_schema;
		$data->fieldClean($schema);
		$data->clean($no_id);
		$data->clean($path_id);

		$sql = "SELECT \"{$schema}\".droppath('{$path_id}', '{$no_id}')";

		return $data->execute($sql);
	}	
	
	// LISTENS
	
	/**
	 * Gets node listens
	 */
	function getListens($no_id) {
		global $data;

		$schema = $this->slony_schema;
		$data->fieldClean($schema);
		$data->clean($no_id);
		
		$sql = "SELECT * FROM \"{$schema}\".sl_listen sl, \"{$schema}\".sl_node sn
					WHERE sl.li_provider=sn.no_id 
					AND sl.li_receiver='{$no_id}'
					ORDER BY sn.no_comment";
		
		return $data->selectSet($sql);
	}

	/**
	 * Gets node listen details
	 */
	function getListen($no_id, $listen_id) {
		global $data;

		$schema = $this->slony_schema;
		$data->fieldClean($schema);
		$data->clean($no_id);
		$data->clean($listen_id);
		
		$sql = "SELECT sl.*, sn.*, sn2.no_comment AS origin FROM \"{$schema}\".sl_listen sl, \"{$schema}\".sl_node sn, \"{$schema}\".sl_node sn2
					WHERE sl.li_provider=sn.no_id 
					AND sl.li_receiver='{$no_id}'
					AND sn.no_id='{$listen_id}'
					AND sn2.no_id=sl.li_origin";
		
		return $data->selectSet($sql);
	}

	/**
	 * Creates a listen
	 */
	function createListen($no_id, $origin, $provider) {
		global $data;

		$schema = $this->slony_schema;
		$data->fieldClean($schema);
		$data->clean($no_id);
		$data->clean($origin);
		$data->clean($provider);

		$sql = "SELECT \"{$schema}\".storelisten('{$origin}', '{$provider}', '{$no_id}')";

		return $data->execute($sql);
	}

	/**
	 * Drops a listen
	 */
	function dropListen($no_id, $origin, $provider) {
		global $data;

		$schema = $this->slony_schema;
		$data->fieldClean($schema);
		$data->clean($no_id);
		$data->clean($origin);
		$data->clean($provider);

		$sql = "SELECT \"{$schema}\".droplisten('{$origin}', '{$provider}', '{$no_id}')";

		return $data->execute($sql);
	}	
	
	// ACTIONS
	
	

}

?>
