<?php
/*
 * PiAnoS (Picture Annotation System), A fingerprint examiner training tool
 * Copyright (C) 2011-2012 University of Lausanne, Switzerland
 * https://ips-labs.unil.ch/pianos
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see http://www.gnu.org/licenses/.
 */
require_once ( "backend/AppDB.php" );
class AppDB_TE extends AppDB
{
    function __construct()
    {
        parent::__construct();
    }
    
    // Exercise management
    public function getExercise( $id, $withProgress = false, $withAssignments = false )
    {
        if( $_SESSION[ 'user' ][ 'isadmin' ] )
        {
            $check1 = '';
            $check2 = '';
        } else
        {
            $check1 = ' AND checkACL(folder.id)';
            $check2 = ' AND person.id = ' . ( int ) $_SESSION[ 'user' ][ 'id' ];
        }
        $res = $this->query( "
        	SELECT
        		exercise.id,
        		exercise.name,
        		exercise.description,
        		exercise.mark,
        		imgmark.name as namemark,
        		imgprint.name as nameprint,
        		exercise.print,
        		person.id AS ownerid,
        		person.name AS ownername,
        		folder.id AS folderid
        	
        	FROM
        		exercise
        		
        	LEFT OUTER JOIN
        		folder
        		ON folder.id = exercise.folder
        	
        	LEFT OUTER JOIN
        		person
        		ON folder.personalof = person.id
        	
        	LEFT OUTER JOIN
        		imagev AS imgmark
        		ON exercise.mark = imgmark.id
        		
        	LEFT OUTER JOIN
        		imagev as imgprint
        		ON exercise.print = imgprint.id
        		
        	" . $check2 . "
        	
        	WHERE
        		exercise.id = " . ( int ) $id . " " . $check1 . ";"
        );
        
        if( count( $res ) != 1 )
            throw new Exception( "Access denied" );
        
        if( $withProgress )
        {
            $progress = $this->getExerciseProgress( $id );
            $res[ 0 ][ 'progress' ] = $progress;
            
            if( ! $_SESSION[ 'user' ][ 'isadmin' ] && Application::get( 'users_can_reuse_analysis' ) )
            {
                $res[ 0 ][ 'reuse_analysis' ] = array ();
                if( $progress[ 'status' ] <= 1 )
                {
                    // The option is made available only if the Analysis is not submitted yet. We leave the
                    // possibility to start the analysis, and say "oh crap, I forgot to copy the data from
                    // a previous exercise, let's go back and do it now". But if the analysis was submitted,
                    // the game is over, and we can't overwrite our analysis by another one. This is why we
                    // check for status <= 1 (see getAssignmentStatus() in database/pianos4-te-functions.sql)
                    
                    // I could maybe do this using a single query, but I'm too lazy AND such a specialized
                    // feature doesn't deserve an optimization for the moment.
                    
                    // 1) Collect all candidates exercises - that is, exercises the user has access to that
                    // have the same mark image as the current exercise.
                    $sql = "SELECT exercise.id, exercise.name
							FROM exercise
								JOIN folder ON folder.id = exercise.folder
							WHERE exercise.mark = " . ( int ) $res[ 0 ][ 'mark' ] . "
								AND exercise.id <> " . ( int ) $res[ 0 ][ 'id' ] . "
								AND checkACL(folder.id);";
                    $exs = $this->query( $sql );
                    
                    // 2) Examine the status of the user in each of these exercises, and collect
                    // the candidates.
                    $candidates = array ();
                    foreach( $exs as $ex )
                    {
                        $p = $this->getExerciseProgress( $ex[ 'id' ] );
                        if( $p[ 'status' ] >= 2 )
                        {
                            // This exercise is available as a source of analysis only if the user has
                            // at least submitted the analysis phase.
                            $c = $ex;
                            $c[ 'progress' ] = $p;
                            $candidates[] = $c;
                        }
                    }
                    
                    $res[ 0 ][ 'reuse_analysis' ] = $candidates;
                }
            }
        }
        
        if( $withAssignments )
        {
            $this->checkAdmin();
            $res[ 0 ][ 'assignments' ] = $this->getExerciseAssignments( $id );
        }
        
        return $res[ 0 ];
    }
    public function getExerciseCloneData( $id, $aid = NULL )
    {
        if( $_SESSION[ 'user' ][ 'isadmin' ] )
        {
            $check1 = '';
            $check2 = '';
            $progress = $this->getAssignmentProgress( $aid );
        } else
        {
            $progress = $this->getExerciseProgress( $id );
            $check1 = ' AND checkACL(folder.id)';
            $check2 = ' AND person.id = ' . ( int ) $_SESSION[ 'user' ][ 'id' ];
        }
        
        $res = $this->query( "SELECT exercise.id,exercise.name,exercise.description,exercise.mark,exercise.print,person.id AS ownerid,person.name AS ownername,folder.id AS folderid FROM exercise LEFT OUTER JOIN folder ON folder.id = exercise.folder LEFT OUTER JOIN person ON folder.personalof = person.id " . $check2 . " WHERE exercise.id = " . ( int ) $id . " " . $check1 . ";" );
        if( count( $res ) != 1 )
            throw new Exception( "Access denied" );
        
        $res[ 0 ][ 'progress' ] = $progress;
        
        return $res[ 0 ];
    }
    public function createExercise( $name, $description, $folder, $mark, $print )
    {
        if( ! $_SESSION[ 'user' ][ 'isadmin' ] )
        {
            // Users can only create exercises in their personal folder
            $res = $this->query( "SELECT personalof = " . ( int ) $_SESSION[ 'user' ][ 'id' ] . " AS check FROM folder WHERE id = " . ( int ) $folder . ";" );
            if( count( $res ) != 1 || $res[ 0 ][ 'check' ] != 't' )
                throw new Exception( "Access denied" );
        }
        
        $res = $this->query( "INSERT INTO exercise (name, description, folder, mark, print) 
						VALUES (E'" . pg_escape_string( $name ) . "', E'" . pg_escape_string( $description ) . "',
							" . ( int ) $folder . ", " . ( int ) $mark . ", " . ( int ) $print . ") RETURNING id;" );
        
        return ( int ) $res[ 0 ][ 'id' ];
    }
    public function cloneExercise( $aid, $name, $description, $print, $folder = NULL )
    {
        if( ! $_SESSION[ 'user' ][ 'isadmin' ] )
        {
            // Users can only create exercises in their personal folder
            $res = $this->query( "SELECT id FROM folder WHERE personalof = " . ( int ) $_SESSION[ 'user' ][ 'id' ] . ";" );
            if( count( $res ) != 1 )
                throw new Exception( "Access denied" );
            $folder = ( int ) $res[ 0 ][ 'id' ];
        }
        
        $trtoken = $this->beginTransactionIf();
        
        // The provided assignment gives us all the source information:
        // 1) the exercise it is bound to is copied (to a new exercise)
        $ex = $this->query( "SELECT exercise.* FROM exercise,assignment,object WHERE assignment.id = object.id AND assignment.id = " . ( int ) $aid . " AND object.creator = " . ( int ) $_SESSION[ 'user' ][ 'id' ] . " AND exercise.id = assignment.exercise;" );
        if( count( $ex ) != 1 )
            throw new Exception( "Access denied" );
        $ex = $ex[ 0 ];
        $newexid = $this->createExercise( $name, $description, $folder, $ex[ 'mark' ], $print );
        
        // 2) Create a new assignment for this exercise/user
        $newaid = $this->getOrCreateAssignment( $newexid );
        
        // 3) the submitted analysis results it contains are copied (to a new assignment + new analysis results)
        $this->_copyAnalysis( $aid, $newaid );
        
        // Finally, commit everything.
        $this->commitTransactionIf( $trtoken );
        
        return $newexid;
    }
    
    // Reuse analysis from assignment '$aid' and store this as a submitted analysis result
    // of exercise '$exid'
    // Returns the assignment ID that was used (created if needed).
    public function reuseAnalysis( $exid, $aid )
    {
        if( ! Application::get( 'users_can_reuse_analysis' ) )
            throw new Exception( "Invalid parameters" );
        
        if( ! $_SESSION[ 'user' ][ 'isadmin' ] )
        {
            // Users can only clone an analysis they own
            $res = $this->query( "SELECT assignment.* FROM assignment JOIN object ON assignment.id=object.id WHERE assignment.id=" . ( int ) $aid . " AND creator = " . ( int ) $_SESSION[ 'user' ][ 'id' ] . ";" );
            if( count( $res ) != 1 )
                throw new Exception( "Access denied" );
            $srcEID = ( int ) $res[ 0 ][ 'exercise' ];
            // Both exercises (the one we are copying from and the one we are copying to) must be accessible to the user
            $res = $this->query( "SELECT checkACL((SELECT folder FROM exercise WHERE id=" . $srcEID . ")) AND checkACL((SELECT folder FROM exercise WHERE id=" . ( int ) $exid . ")) AS foo;" );
            if( $res[ 0 ][ 'foo' ] !== 't' )
                throw new Exception( "Access denied" );
        }
        
        $trtoken = $this->beginTransactionIf();
        
        $dstAID = $this->getOrCreateAssignment( $exid );
        $this->_copyAnalysis( $aid, $dstAID );
        
        $this->commitTransactionIf( $trtoken );
        
        return $dstAID;
    }
    private function _copyAnalysis( $srcAID, $dstAID )
    {
        // NOTE: private function, no security checks at all. Do not make public (unless you
        // wrap it with calling code that does appropriate checking).
        $trtoken = $this->beginTransactionIf();
        $results = $this->query( "SELECT result.* FROM result,resulttype,resultstep WHERE result.type = resulttype.id AND resulttype.short = 'VALIDATED' AND result.step = resultstep.id AND resultstep.short = 'ANALYSIS' AND result.assignment = " . ( int ) $srcAID . ";" );
        if( count( $results ) != 1 )
            throw new Exception( "Invalid parameters" );
        $results = $results[ 0 ];
        // Decode the results from JSON-string storage. This is needed (and ugly) because
        // we're going to call saveResults(), which expect PHP data in input (and not strings)
        // We could also "manually" insert the results, but the number of operation saveResults()
        // does is simply too much (and complicated to imitate properly) so we're better off
        // pretending we're the user submitting the results.
        // TODO: provide two code paths to the saveResults() function, which should be made
        // internal (private). So current saveResults() should simply call the new private
        // implementation, and we would also call this private implementation here.
        $data = json_decode( $results[ 'data' ], true );
        // Copy the analysis results into the new assignment
        $this->saveResults( $dstAID, 'VALIDATED', 'ANALYSIS', $data[ 'mark' ], NULL, $data[ 'observations' ] );
        $this->commitTransactionIf( $trtoken );
    }
    public function deleteExercise( $id )
    {
        $this->checkAdmin();
        
        $trtoken = $this->beginTransactionIf();
        
        $res = $this->query( "SELECT id FROM assignment WHERE exercise = " . ( int ) $id . ";" );
        foreach( $res as $row )
        {
            $this->deleteAssignment( $row[ 'id' ] );
        }
        
        $res = $this->query( "DELETE FROM security WHERE object = " . ( int ) $id . ";" );
        $res = $this->query( "DELETE FROM exercise WHERE id = " . ( int ) $id . ";" );
        $res = $this->query( "DELETE FROM object WHERE id = " . ( int ) $id . ";" );
        $this->commitTransactionIf( $trtoken );
    }
    public function getExerciseDataSummary( $id )
    {
        $this->checkAdmin();
        
        $res = $this->query( "SELECT count(assignment.*) FROM assignment WHERE assignment.exercise = " . ( int ) $id . ";" );
        
        return array (
                'assignments' => ( int ) $res[ 0 ][ 'count' ] 
        );
    }
    
    // TODO: merge this function and the getUserAssignments in a single one, the difference
    // being that here we fetch the user data (name, id and groups). And of course that this
    // function selects assignments by exercise, whereas getUserAssignments does so by
    // user, obviously.
    // Otherwise, the SQL code and the post-processing a too similar to be two totally
    // different beasts.
    public function getExerciseAssignments( $id, $extended = false, $anonymousreview = 0, $multiCaseViewer = NULL )
    {
        $this->checkAdmin();
        
        $obstypes = array (
                'conclusion' => 'conclusions', 
                'mark_value' => 'mark_values', 
                'decision' => 'decisions', 
                'decision_ex' => 'decisions_ex' 
        );
        if( $extended )
        {
            $view = 'resultvex2';
            $obstypes[ 'mark_degradation' ] = 'mark_degradations';
            $obstypes[ 'mark_details' ] = 'mark_details';
            $obstypes[ 'comparison_need_better_known' ] = 'comparison_need_better_knowns';
            $obstypes[ 'comparison_l3details' ] = 'comparison_l3details';
            $obstypes[ 'comparison_qualquant' ] = 'comparison_qualquants';
        } else
        {
            $view = 'resultvex';
        }
        
        $innerSelect = '';
        $groupby = '';
        foreach( $obstypes as $k => $v )
        {
            $innerSelect .= ",\narray_agg(" . $k . ") AS " . $v;
            $groupby .= ", arv." . $v;
        }
        
        $multiCaseViewerWhere = "";
        if( $multiCaseViewer != NULL )
        {
            $multiCaseViewerAids = explode( ",", $multiCaseViewer );
            
            foreach( $multiCaseViewerAids as  $multiCaseViewerAid )
            {
                $multiCaseViewerWhere .= " OR assignment.exercise = " . ( int ) $multiCaseViewerAid;
            }
        }
        
        $res = $this->query( "
			SELECT arv.*,
				object.createdon,
				object.updatedon,
				person.name AS person,
				person.id AS personid,
				array_to_string(array_agg(pgroup.name),',') AS pgroups,
				getAssignmentStatus(arv.id) AS status
			FROM pgroup,membership,person,object,
					(SELECT id,
						ispreview,
						count(id) AS numresults,
						array_agg(step) AS ressteps,
						array_agg(type) AS restypes,
						array_agg(updatedon) AS resupdates
						" . $innerSelect . "
						FROM
							(SELECT assignment.id,
								assignment.ispreview,
								resultvex.*
							FROM assignment
								LEFT OUTER JOIN " . $view . " AS resultvex ON resultvex.assignment = assignment.id
							WHERE ( assignment.exercise = " . ( int ) $id . " " . $multiCaseViewerWhere . " )
							ORDER BY assignment.id,resultvex.stepid,resultvex.typeid) AS rv
						GROUP BY id,ispreview
					) AS arv
			WHERE person.id = object.creator
				AND object.id = arv.id
				AND pgroup.id = membership.pgroup
				AND membership.person = person.id
			GROUP BY arv.id, arv.ispreview, arv.numresults, arv.ressteps, arv.restypes, arv.resupdates " . $groupby . ",
				object.createdon, object.updatedon, person.name, person.id, status
			ORDER BY person.name,arv.id;
		" );
        
        foreach( $res as &$row )
        {
            $row[ 'progress' ] = $this->translateAssignmentStatus( ( int ) $row[ 'status' ] );
            unset( $row[ 'status' ] );
            $row[ 'user' ] = array (
                    'name' => ( ( int ) $anonymousreview ) ? 'anonymous_' . $row[ 'id' ]: $row[ 'person' ],
                    'id' => ( int ) $row[ 'personid' ], 
                    'groups' => $row[ 'pgroups' ] 
            );
            unset( $row[ 'person' ] );
            unset( $row[ 'personid' ] );
            unset( $row[ 'pgroups' ] );
            $row[ 'ispreview' ] = $row[ 'ispreview' ] === 't';
            $row[ 'numresults' ] = ( int ) $row[ 'numresults' ];
            if( $row[ 'numresults' ] == 0 )
            {
                $row[ 'restypes' ] = NULL;
                $row[ 'ressteps' ] = NULL;
                $row[ 'resupdates' ] = NULL;
                $row[ 'conclusions' ] = NULL;
                $row[ 'mark_values' ] = NULL;
                $row[ 'decisions' ] = NULL;
                $row[ 'decisions_ex' ] = NULL;
                if( $extended )
                {
                    $row[ 'mark_degradations' ] = NULL;
                    $row[ 'mark_details' ] = NULL;
                    $row[ 'comparison_need_better_knowns' ] = NULL;
                    $row[ 'comparison_l3details' ] = NULL;
                    $row[ 'comparison_qualquants' ] = NULL;
                }
            } else
            {
                $row[ 'restypes' ] = $this->stringSqlArrayToPhp( $row[ 'restypes' ] );
                $row[ 'ressteps' ] = $this->stringSqlArrayToPhp( $row[ 'ressteps' ] );
                $row[ 'resupdates' ] = $this->stringSqlArrayToPhp( $row[ 'resupdates' ] );
                $row[ 'conclusions' ] = $this->stringSqlArrayToPhp( $row[ 'conclusions' ] );
                $row[ 'mark_values' ] = $this->stringSqlArrayToPhp( $row[ 'mark_values' ] );
                $row[ 'decisions' ] = $this->stringSqlArrayToPhp( $row[ 'decisions' ] );
                $row[ 'decisions_ex' ] = $this->stringSqlArrayToPhp( $row[ 'decisions_ex' ] );
                if( $extended )
                {
                    $row[ 'mark_degradations' ] = $this->stringSqlArrayToPhp( $row[ 'mark_degradations' ] );
                    $row[ 'mark_details' ] = $this->stringSqlArrayToPhp( $row[ 'mark_details' ] );
                    $row[ 'comparison_need_better_knowns' ] = $this->stringSqlArrayToPhp( $row[ 'comparison_need_better_knowns' ] );
                    $row[ 'comparison_l3details' ] = $this->stringSqlArrayToPhp( $row[ 'comparison_l3details' ] );
                    $row[ 'comparison_qualquants' ] = $this->stringSqlArrayToPhp( $row[ 'comparison_qualquants' ] );
                }
            }
        }
        
        return $res;
    }
    
    // TODO: merge this function and the getExerciseAssignments in a single one
    public function getUserAssignments( $id )
    {
        $this->checkAdmin();
        
        $res = $this->query( "
			SELECT arv.*,
				object.createdon,
				object.updatedon,
				exercise.id AS exid,
				exercise.name AS exname,
				getAssignmentStatus(arv.id) AS status
			FROM exercise,object,
					(SELECT id,
						exercise,
						ispreview,
						count(id) AS numresults,
						array_agg(step) AS ressteps,
						array_agg(type) AS restypes,
						array_agg(updatedon) AS resupdates,
						array_agg(conclusion) AS conclusions,
						array_agg(mark_value) AS mark_values,
						array_agg(decision) AS decisions
						FROM
							(SELECT assignment.id,
								assignment.exercise,
								assignment.ispreview,
								resultvex.*
							FROM object,assignment
								LEFT OUTER JOIN resultvex ON resultvex.assignment = assignment.id
							WHERE object.id = assignment.id
								AND object.creator = " . ( int ) $id . "
							ORDER BY assignment.id,resultvex.stepid,resultvex.typeid) AS rv
						GROUP BY id,exercise,ispreview
					) AS arv
			WHERE arv.exercise = exercise.id
				AND object.id = arv.id
			ORDER BY exercise.name,arv.id;
		" );
        
        foreach( $res as &$row )
        {
            $row[ 'progress' ] = $this->translateAssignmentStatus( ( int ) $row[ 'status' ] );
            unset( $row[ 'status' ] );
            $row[ 'ispreview' ] = $row[ 'ispreview' ] === 't';
            $row[ 'numresults' ] = ( int ) $row[ 'numresults' ];
            if( $row[ 'numresults' ] == 0 )
            {
                $row[ 'restypes' ] = NULL;
                $row[ 'ressteps' ] = NULL;
                $row[ 'resupdates' ] = NULL;
                $row[ 'conclusions' ] = NULL;
                $row[ 'mark_values' ] = NULL;
                $row[ 'decisions' ] = NULL;
            } else
            {
                $row[ 'restypes' ] = $this->stringSqlArrayToPhp( $row[ 'restypes' ] );
                $row[ 'ressteps' ] = $this->stringSqlArrayToPhp( $row[ 'ressteps' ] );
                $row[ 'resupdates' ] = $this->stringSqlArrayToPhp( $row[ 'resupdates' ] );
                $row[ 'conclusions' ] = $this->stringSqlArrayToPhp( $row[ 'conclusions' ] );
                $row[ 'mark_values' ] = $this->stringSqlArrayToPhp( $row[ 'mark_values' ] );
                $row[ 'decisions' ] = $this->stringSqlArrayToPhp( $row[ 'decisions' ] );
            }
        }
        
        return $res;
    }
    public function saveExercise( $id, $name, $description, $mark, $print )
    {
        $this->beginTransaction();
        
        if( ! $_SESSION[ 'user' ][ 'isadmin' ] )
        {
            $res = $this->query( "SELECT checkACL(folder.id) FROM folder,exercise WHERE exercise.id=" . ( int ) $id . " AND exercise.folder = folder.id AND folder.personalof = " . ( int ) $_SESSION[ 'user' ][ 'id' ] . ";" );
            if( count( $res ) == 0 || $res[ 0 ][ 'checkacl' ] != 't' )
                throw new Exception( "Access denied" );
        }
        
        $this->query( "UPDATE exercise SET name = E'" . pg_escape_string( $name ) . "', description = E'" . pg_escape_string( $description ) . "', mark = " . ( int ) $mark . ", print = " . ( int ) $print . " WHERE id = " . ( int ) $id . ";" );
        
        $this->commitTransaction();
    }
    public function deleteExercisePreviews( $id )
    {
        $this->checkAdmin();
        
        $this->beginTransaction();
        
        $results = $this->query( "SELECT result.assignment,result.exstep FROM result,assignment WHERE result.assignment=assignment.id AND assignment.ispreview IS true AND assignment.exercise = " . ( int ) $id );
        foreach( $results as $result )
        {
            $this->query( "SELECT deleteDocResults(" . ( int ) $result[ 'assignment' ] . "," . ( int ) $result[ 'exstep' ] . ");" );
            $this->query( "DELETE FROM result WHERE assignment = " . ( int ) $result[ 'assignment' ] . " AND exstep = " . ( int ) $result[ 'exstep' ] . ";" );
            $this->query( "DELETE FROM assignment WHERE id = " . ( int ) $result[ 'assignment' ] . ";" );
            $this->query( "DELETE FROM object WHERE id = " . ( int ) $result[ 'assignment' ] . ";" );
        }
        
        $this->commitTransaction();
    }
    public function getExerciseNeighbours( $id, $window = 1 )
    {
        $this->checkAdmin();
        
        $res = $this->query( "SELECT * FROM exercise WHERE id=" . ( int ) $id . ";" );
        if( count( $res ) !== 1 )
            throw new Exception( "Access denied" );
        $ex = $res[ 0 ];
        $e_exname = pg_escape_string( $ex[ 'name' ] );
        $before = $this->query( "SELECT * FROM exercise WHERE name < E'" . $e_exname . "' AND folder = " . ( int ) $ex[ 'folder' ] . " ORDER BY name DESC LIMIT " . ( int ) $window . ";" );
        $after = $this->query( "SELECT * FROM exercise WHERE name > E'" . $e_exname . "' AND folder = " . ( int ) $ex[ 'folder' ] . " ORDER BY name ASC LIMIT " . ( int ) $window . ";" );
        return array (
                $before, 
                $ex, 
                $after 
        );
    }
    
    // Images management
    public function addImage( $type, $name, $mimetype, $info, $resolution, $width, $height, $data )
    {
        $trtoken = $this->beginTransactionIf();
        
        $res = $this->query( "INSERT INTO image(type,name,mimetype,info,resolution,width,height,data) VALUES (
			(SELECT id FROM imagetype WHERE short = E'" . pg_escape_string( $type ) . "'),
			E'" . pg_escape_string( $name ) . "',
			E'" . pg_escape_string( $mimetype ) . "',
			E'" . pg_escape_string( $info ) . "',
			" . ( int ) $resolution . "," . ( int ) $width . "," . ( int ) $height . ",
			E'" . pg_escape_bytea( $data ) . "'
		) RETURNING id;" );
        $imageID = ( int ) $res[ 0 ][ 'id' ];
        
        if( ! $_SESSION[ 'user' ][ 'isadmin' ] )
        {
            $this->query( "INSERT INTO security(object,trustee) VALUES (" . $imageID . "," . ( int ) $_SESSION[ 'user' ][ 'id' ] . ");" );
        }
        
        $this->commitTransactionIf( $trtoken );
        
        return $imageID;
    }
    
    public function changeResolution( $id, $newResolution )
    {
        $this->checkAdmin();
        
        $trtoken = $this->beginTransactionIf();
        
        $res = $this->query( "UPDATE image SET resolution = " . ( int ) pg_escape_string( $newResolution ) . " WHERE id = " . ( int ) pg_escape_string( $id ) . ";" );
        
        $this->commitTransactionIf( $trtoken );
        
        return $res;
    }
    
    // Assignments
    public function getOrCreateExercisePreview( $id )
    {
        $this->checkAdmin();
        
        $res = $this->query( "SELECT assignment.id FROM assignment,object WHERE assignment.id=object.id AND assignment.exercise = " . ( int ) $id . " AND object.creator = " . $_SESSION[ 'user' ][ 'id' ] . " AND assignment.isPreview IS true;" );
        if( count( $res ) > 0 )
        {
            return $res[ 0 ][ 'id' ];
        } else
        {
            $res = $this->query( "INSERT INTO assignment(exercise,ispreview) VALUES (" . ( int ) $id . ",true) RETURNING id;" );
            if( count( $res ) != 1 )
                throw new Exception( "Access denied" );
            return $res[ 0 ][ 'id' ];
        }
    }
    public function tryGetAssignment( $id )
    {
        $res = $this->query( "SELECT assignment.id FROM assignment,object WHERE assignment.id=object.id AND assignment.exercise = " . ( int ) $id . " AND object.creator = " . $_SESSION[ 'user' ][ 'id' ] . " AND assignment.isPreview IS false;" );
        if( count( $res ) > 0 )
            return ( int ) $res[ 0 ][ 'id' ];
        return NULL;
    }
    public function getOrCreateAssignment( $id )
    {
        $aid = $this->tryGetAssignment( $id );
        if( $aid === NULL )
        {
            $res = $this->query( "INSERT INTO assignment(exercise,ispreview) VALUES (" . ( int ) $id . "," . ( $_SESSION[ 'user' ][ 'isadmin' ] ? 'true': 'false' ) . ") RETURNING id;" );
            if( count( $res ) != 1 )
                throw new Exception( "Access denied" );
            $aid = ( int ) $res[ 0 ][ 'id' ];
        }
        return $aid;
    }
    public function getAssignment( $id )
    {
        $sql = "SELECT assignment.* FROM assignment,object WHERE assignment.id=object.id AND assignment.id = " . ( int ) $id;
        if( ! $_SESSION[ 'user' ][ 'isadmin' ] )
            $sql .= " AND object.creator = " . ( int ) $_SESSION[ 'user' ][ 'id' ];
        $sql .= ";";
        $res = $this->query( $sql );
        if( count( $res ) != 1 )
            throw new Exception( "Access denied" );
        return $res[ 0 ];
    }
    public function deleteAssignment( $aid )
    {
        $this->checkAdmin();
        
        $token = $this->beginTransactionIf();
        
        $res = $this->query( "DELETE FROM minutia WHERE assignment = " . ( int ) $aid . ";" );
        $res = $this->query( "DELETE FROM observation WHERE assignment = " . ( int ) $aid . ";" );
        $res = $this->query( "DELETE FROM annotation WHERE assignment = " . ( int ) $aid . ";" );
        $res = $this->query( "DELETE FROM result WHERE assignment = " . ( int ) $aid . ";" );
        $res = $this->query( "DELETE FROM resulthistory WHERE assignment = " . ( int ) $aid . ";" );
        $res = $this->query( "DELETE FROM assignment WHERE id = " . ( int ) $aid . ";" );
        $res = $this->query( "DELETE FROM object WHERE id = " . ( int ) $aid . ";" );
        
        $this->commitTransactionIf( $token );
    }
    
    // Folders
    public function getFolders( $countExercises = false, $showPersonal = false, $folderID = NULL )
    {
        $select = 'folder.id, folder.name';
        $from = 'folder';
        $group = '';
        $where = '';
        if( $countExercises )
        {
            $select .= ', count( exercise.* ) + count( F.* ) AS numexercisesandfolders';
            $from .= ' LEFT OUTER JOIN exercise ON folder.id = exercise.folder';
            $from .= ' LEFT OUTER JOIN folder as F ON folder.id = F.childof';
            $group .= ' GROUP BY folder.id, folder.name';
        }
        if( ! $_SESSION[ 'user' ][ 'isadmin' ] )
        {
            $select .= ",folder.personalof=" . ( int ) $_SESSION[ 'user' ][ 'id' ] . ' AS own';
            $where = "WHERE checkACL(folder.id," . $_SESSION[ 'user' ][ 'id' ] . ")";
            if( $group )
                $group .= ',own';
        } else if( ! $showPersonal )
        {
            $where = "WHERE folder.personalof IS NULL";
        }
        
        if( Application::get('subfolders') )
        {
	        if( $where == '' )
	        	$where = 'WHERE';
	        else
	        	$where .= ' and';
	        	
        	
        	if( $folderID != NULL )
	        	$where .= " folder.childof =". $folderID;
	        else
        		$where .= " folder.childof IS NULL";
        }        
        
        $res = $this->query( "SELECT " . $select . " FROM " . $from . " " . $where . " " . $group . " ORDER BY folder.name ASC;" );
        
        if( ! $_SESSION[ 'user' ][ 'isadmin' ] )
        {
            foreach( $res as &$row )
            {
                $row[ 'own' ] = $row[ 'own' ] == 't';
                if( $row[ 'own' ] )
                    $row[ 'name' ] = 'My personal folder';
            }
        }
        foreach( $res as &$row )
        	$row[ 'type' ] = 'folder';
        
        return $res;
    }
    
    public function getFolderExercises( $folderID = NULL, $progress = false )
    {
    	$sql = "
    		SELECT 
				exercise.id, 
				exercise.name, 
				folder.name as folder, 
				person.id as userid,
				person.name as username
			FROM 
				public.exercise, 
				public.object, 
				public.folder, 
				public.person
			WHERE 
				exercise.id = object.id AND
				exercise.folder = folder.id AND
				object.creator = person.id
				" . ( $_SESSION[ 'user' ][ 'isadmin' ] ? "": " AND checkACL( folder.id," . ( int ) $_SESSION[ 'user' ][ 'id' ] . " )" ) . "
				" . ( $folderID ? 'AND folder.id = ' . ( int ) $folderID: '' ) . "
			ORDER BY
				name ASC
			;";
    	
    	$res = $this->query( $sql );
    	
        if( $progress )
        {
            foreach( $res as &$ex )
                $ex[ 'progress' ] = $this->getExerciseProgress( $ex[ 'id' ] );
        }
        
        foreach( $res as &$ex )
        	$ex[ 'type' ] = 'exercise';
        
        return $res;
    }
    
    public function getAllExercises()
    {
        if( $_SESSION[ 'user' ][ 'isadmin' ] )
        {
            $sql = "
                SELECT
                	folder.name as foldername,
                    folder.id as folderid,
                	exercise.name as exname,
                	exercise.id as exid
                	
                FROM exercise, folder
                WHERE exercise.folder = folder.id
                ORDER BY foldername, exname";
                
            $res = $this->query( $sql );
            
            return $res;
            
        } else {
            throw new Exception( "Access denied" );
        }
    }
    
    public function getFolderExercisesAndSubFolders( $countExercises, $showPersonal, $folderID )
    {
    	$ex = $this->getFolderExercises( $folderID );
    	
    	if( Application::get('subfolders') )
    	{
    		$sub = $this->getFolders( $countExercises, $showPersonal, $folderID );
    		return array_merge( $sub, $ex );
    	} else {
    		return $ex;
    	}
    }
    
    public function getFolder( $id )
    {
        $res = $this->query( "SELECT folder.*,object.* FROM folder,object WHERE folder.id=object.id AND checkACL(folder.id) AND folder.id = " . ( int ) $id . ";" );
        if( count( $res ) != 1 )
            throw new Exception( "Access denied" );
        $folder = $res[ 0 ];
        
        $res = $this->query( "SELECT exercise.*,object.* FROM exercise,object WHERE exercise.id=object.id AND exercise.folder = " . ( int ) $id . ";" );
        $folder[ 'exercises' ] = $res;
        
        if( $_SESSION[ 'user' ][ 'isadmin' ] )
        {
            $res = $this->query( "SELECT security.trustee FROM security,pgroup WHERE security.trustee = pgroup.id AND security.object = " . ( int ) $id . ";" );
            $folder[ 'acl' ] = array ();
            foreach( $res as $pgroup )
                $folder[ 'acl' ][] = ( int ) $pgroup[ 'trustee' ];
        }
        
        return $folder;
    }
    
    public function createFolder( $name, $childof = NULL )
    {
        $this->checkAdmin();
        
        if( $childof == NULL )
        	$res = $this->query( "INSERT INTO folder (name) VALUES (E'" . pg_escape_string( $name ) . "') RETURNING id;" );
        else
        	$res = $this->query( "INSERT INTO folder (name,childof) VALUES (E'" . pg_escape_string( $name ) . "', E'" . pg_escape_string( $childof ) . "') RETURNING id;" );
        
        return ( int ) $res[ 0 ][ 'id' ];
    }
    public function setFolderName( $id, $name )
    {
        $this->checkAdmin();
        
        $res = $this->query( "UPDATE folder SET name = E'" . pg_escape_string( $name ) . "' WHERE id = " . ( int ) $id . " RETURNING id;" );
        return ( int ) $res[ 0 ][ 'id' ];
    }
    public function deleteFolder( $id )
    {
        $this->checkAdmin();
        
        $this->beginTransaction();
        
        $res = $this->query( "SELECT id FROM exercise WHERE folder = " . ( int ) $id . ";" );
        foreach( $res as $row )
            $this->deleteExercise( ( int ) $row[ 'id' ] );
        
        $res = $this->query( "DELETE FROM security WHERE object = " . ( int ) $id . ";" );
        $res = $this->query( "DELETE FROM folder WHERE id = " . ( int ) $id . ";" );
        $res = $this->query( "DELETE FROM object WHERE id = " . ( int ) $id . ";" );
        
        $this->commitTransaction();
    }
    public function getFolderDataSummary( $id )
    {
        $this->checkAdmin();
        
        $res = $this->query( "SELECT exercise.id,exercise.name,count(assignment.*) AS numassignments FROM exercise LEFT OUTER JOIN assignment ON exercise.id = assignment.exercise WHERE folder = " . ( int ) $id . " GROUP BY exercise.id,exercise.name;" );
        
        $numTotalAssignments = 0;
        foreach( $res as $row )
            $numTotalAssignments += ( int ) $row[ 'numassignments' ];
        
        return array (
                'assignments' => $numTotalAssignments, 
                'exercises' => count( $res ), 
                'all' => $res 
        );
    }
    public function saveFolder( $id, $name, $groups = array() )
    {
        $this->checkAdmin();
        
        $this->beginTransaction();
        $res = $this->query( "UPDATE folder SET name = E'" . pg_escape_string( $name ) . "' WHERE id = " . ( int ) $id . " RETURNING id;" );
        if( count( $res ) != 1 )
            throw new Exception( "Access denied" );
        $this->query( "DELETE FROM security USING pgroup WHERE security.object = " . ( int ) $id . " AND security.trustee = pgroup.id AND pgroup.tutors IS false;" );
        foreach( $groups as $group )
            $this->query( "INSERT INTO security(object,trustee) VALUES (" . ( int ) $id . ",(SELECT id FROM pgroup WHERE id = " . ( int ) $group . " AND tutors IS false));" ); // The inner SELECT makes sure we are inserting only regular groups here, not users nor tutor groups.
        $this->commitTransaction();
        return $id;
    }
    public function setFolderACL( $id, $acl )
    {
        $this->checkAdmin();
        
        $this->beginTransaction();
        $this->query( "DELETE FROM security WHERE object = " . ( int ) $id . " AND trustee IN (SELECT id FROM trustee WHERE isgroup IS true);" );
        foreach( $acl as $ace )
            $this->query( "INSERT INTO security(object,trustee) VALUES (" . ( int ) $id . "," . ( int ) $ace . ");" );
        $this->commitTransaction();
    }
    public function duplicateFolder( $id, $name )
    {
        $this->checkAdmin();
        $trtoken = $this->beginTransactionIf();
        
        $exercises = $this->query( "SELECT * FROM exercise WHERE folder = " . ( int ) $id . ";" );
        
        $res = $this->query( "INSERT INTO folder(name) VALUES (E'" . pg_escape_string( $name ) . "') RETURNING id;" );
        $newID = ( int ) $res[ 0 ][ 'id' ];
        
        foreach( $exercises as $ex )
        {
            $this->query( "INSERT INTO exercise(name,description,folder,mark,print) VALUES (E'" . pg_escape_string( $ex[ 'name' ] ) . "',E'" . pg_escape_string( $ex[ 'description' ] ) . "'," . $newID . "," . ( int ) $ex[ 'mark' ] . "," . ( int ) $ex[ 'print' ] . ");" );
        }
        
        $this->commitTransactionIf( $trtoken );
        
        return $newID;
    }
    
    // Images
    public function getImageInfo( $id )
    {
        $res = $this->query( "SELECT image.id, imagetype.short AS type, image.name, image.mimetype, 
									image.info, image.resolution, image.width, image.height, image.thumb,
									(SELECT count(*) FROM image AS image2 WHERE image2.thumb = image.id) > 0 AS isThumb,
									object.createdon, extract(epoch from object.createdon) AS createdon_unix, 
									object.updatedon, extract(epoch from object.updatedon) AS updatedon_unix,
									object.creator
			FROM image,imagetype,object
			WHERE image.type = imagetype.id
				AND image.id = object.id
				AND image.id = " . ( int ) $id . ";" );
        
        if( count( $res ) != 1 )
            throw new Exception( "Access denied" );
        
        $res[ 0 ][ 'isthumb' ] = $res[ 0 ][ 'isthumb' ] === 't';
        
        return $res[ 0 ];
    }
    public function getImageLibrary( $filter = NULL, $type = NULL, $extended = false )
    {
        if( $type )
            $where = "imagev.typeid = (SELECT id FROM imagetype WHERE short = E'" . pg_escape_string( $type ) . "')";
        else
            $where = "imagev.typeid <> (SELECT id FROM imagetype WHERE short = E'THUMBNAIL')";
        
        if( $filter )
            $where .= " AND imagev.name LIKE '%" . pg_escape_string( $filter ) . "%'";
        
        if( ! $_SESSION[ 'user' ][ 'isadmin' ] )
            $where .= " AND checkImageReadACL(imagev.id," . ( int ) $_SESSION[ 'user' ][ 'id' ] . ")";
        
        $columns = "imagev.id,imagev.typeid,imagev.type,imagev.name,imagev.mimetype,imagev.info,imagev.resolution,imagev.width,imagev.height";
        if( $extended )
        {
            $select = $columns . ",person.name AS owner, object.createdon, array_agg(ex.id) AS ex_ids, array_agg(ex.name) AS ex_names, array_agg(CASE WHEN ex.mark<>imagev.id AND ex.print<>imagev.id THEN 'none' WHEN ex.mark=imagev.id AND ex.print<>imagev.id THEN 'mark' WHEN ex.mark<>imagev.id AND ex.print=imagev.id THEN 'print' ELSE 'both' END) AS ex_sources";
            $from = "person,object,imagev LEFT OUTER JOIN exercise AS ex ON (ex.mark = imagev.id OR ex.print = imagev.id)";
            $where .= " AND person.id = object.creator AND object.id = imagev.id";
            $group = "GROUP BY " . $columns . ",owner,createdon";
        } else
        {
            $select = $columns;
            $group = "";
            $from = "imagev";
        }
        $sql = "SELECT " . $select . " FROM " . $from . " WHERE " . $where . " " . $group . " ORDER BY name ASC;";
        $res = $this->query( $sql );
        if( $extended )
        {
            foreach( $res as &$row )
            {
                $row[ 'ex_ids' ] = $row[ 'ex_ids' ] === '{NULL}' ? array (): $this->intSqlArrayToPhp( $row[ 'ex_ids' ] );
                $row[ 'ex_names' ] = $row[ 'ex_names' ] === '{NULL}' ? array (): $this->stringSqlArrayToPhp( $row[ 'ex_names' ] );
                $row[ 'ex_sources' ] = $row[ 'ex_sources' ] === '{NULL}' ? array (): $this->stringSqlArrayToPhp( $row[ 'ex_sources' ] );
            }
        }
        
        return $res;
    }
    public function deleteImage( $id, $cascade = false )
    {
        $this->checkAdmin();
        
        $trtoken = $this->beginTransactionIf();
        
        $res = $this->query( "SELECT id FROM exercise WHERE mark = " . ( int ) $id . " OR print = " . ( int ) $id . " GROUP BY id;" );
        if( count( $res ) !== 0 && ! $cascade )
            throw new Exception( "Access denied" );
        
        foreach( $res as $ex )
        {
            $this->deleteExercise( $ex[ 'id' ] );
        }
        
        $res = $this->query( "DELETE FROM security WHERE object = " . ( int ) $id . ";" );
        $res = $this->query( "DELETE FROM image WHERE id = " . ( int ) $id . ";" );
        $res = $this->query( "DELETE FROM object WHERE id = " . ( int ) $id . ";" );
        
        $this->commitTransactionIf( $trtoken );
    }
    
    // Results
    public function getExerciseProgress( $id )
    {
    	$sql = "SELECT assignment.id 
			FROM assignment, object 
			WHERE object.id = assignment.id 
			AND assignment.exercise = " . ( int ) $id ."
			" . ( $_SESSION[ 'user' ][ 'isadmin' ] ? "": " AND object.creator = " . ( int ) $_SESSION[ 'user' ][ 'id' ] ) . "
			ORDER BY object.createdon 
			DESC LIMIT 1;";
    	
        $res = $this->query( $sql );
        if( count( $res ) != 1 )
            return array (
                    'status' => - 1, 
                    'label' => 'Not started' 
            );
        else
            return $this->getAssignmentProgress( $res[ 0 ][ 'id' ] );
    }
    public function getAssignmentProgress( $aid )
    {
        try
        {
            $res = $this->query( "SELECT getAssignmentStatus(" . ( int ) $aid . ");" );
        } catch( Exception $ex )
        {
            throw new Exception( "Access denied" );
        }
        $status = ( int ) $res[ 0 ][ 'getassignmentstatus' ];
        $ret = $this->translateAssignmentStatus( $status );
        $ret[ 'aid' ] = ( int ) $aid;
        return $ret;
    }
    private function translateAssignmentStatus( $status )
    {
        switch( $status )
        {
            default:
            case 0:
                $label = 'Exercise started';
                $step = 'ANALYSIS';
                break;
            case 1:
                $label = 'Analysis started';
                $step = 'ANALYSIS';
                break;
            case 2:
                $label = 'Analysis submitted';
                $step = 'ANALYSIS';
                break;
            case 3:
                $label = 'Comparison started';
                $step = 'COMPARISON';
                break;
            case 4:
                $label = 'Comparison submitted';
                $step = 'COMPARISON';
                break;
            case 5:
                $label = 'Evaluation started';
                $step = 'EVALUATION';
                break;
            case 6:
                $label = 'Evaluation submitted';
                $step = 'EVALUATION';
                break;
        }
        return array (
                'status' => $status, 
                'label' => $label, 
                'step' => $step 
        );
    }
    public function getAssignmentSubmissionTime( $aid, $step )
    {
        if( is_string( $step ) )
            $subq = "(SELECT id FROM resultstep WHERE short = E'" . pg_escape_string( $step ) . "')";
        else
            $subq = ( int ) $step;
        
        $query = "SELECT updatedon 
			FROM result
			WHERE assignment = " . ( int ) $aid . " 
			AND type = (SELECT id FROM resulttype WHERE short = 'VALIDATED')
			AND step = " . $subq . ";";
        
        $res = $this->query( $query );
        if( count( $res ) != 1 )
            throw new Exception( "Database integrity error" );
        return $res[ 0 ][ 'updatedon' ];
    }
    private function getResultTypes()
    {
        $res = $this->query( "SELECT * FROM resulttype ORDER BY id ASC;" );
        $ret = array ();
        foreach( $res as $item )
            $ret[ $item[ 'short' ] ] = ( int ) $item[ 'id' ];
        return $ret;
    }
    private function getResultSteps()
    {
        $res = $this->query( "SELECT * FROM resultstep ORDER BY id ASC;" );
        $ret = array ();
        foreach( $res as $item )
            $ret[ $item[ 'short' ] ] = ( int ) $item[ 'id' ];
        return $ret;
    }
    private function getNextResultStep( $step )
    {
        if( is_string( $step ) )
            $res = $this->query( "SELECT * FROM resultstep WHERE id = (SELECT id FROM resultstep WHERE short = E'" . pg_escape_string( $step ) . "') + 1;" );
        else
            $res = $this->query( "SELECT * FROM resultstep WHERE id = " . ( ( int ) $step + 1 ) . ";" );
        if( count( $res ) == 1 )
            return $res[ 0 ];
        else
            return NULL;
    }
    public function saveResults( $aid, $type, $step, $mark = NULL, $print = NULL, $observations = NULL )
    {
        $rtypes = $this->getResultTypes();
        $rsteps = $this->getResultSteps();
        
        if( ! array_key_exists( $type, $rtypes ) )
            throw new Exception( "Invalid parameters" );
        $rtype = $rtypes[ $type ];
        
        if( ! array_key_exists( $step, $rsteps ) )
            throw new Exception( "Invalid parameters" );
        $rstep = $rsteps[ $step ];
        
        $trtoken = $this->beginTransactionIf();
        
        // First thing to check: whether the user has access to the assignment
        $owner = $this->query( "SELECT creator FROM object WHERE id = " . ( int ) $aid . ";" );
        if( count( $owner ) !== 1 || ( int ) $owner[ 0 ][ 'creator' ] !== $_SESSION[ 'user' ][ 'id' ] )
            throw new Exception( "Access denied" );
        
        $data = array ();
        if( $mark )
            $data[ 'mark' ] = $mark;
        if( $print )
            $data[ 'print' ] = $print;
        if( $observations )
            $data[ 'observations' ] = $observations;
        
        $serialized = json_encode( $data );
        $e_serialized = pg_escape_string( $serialized );
        
        $results = array ();
        
        $res = $this->query( "SELECT count(*) FROM result WHERE assignment = " . ( int ) $aid . " AND type = " . $rtype . " AND step = " . $rstep . ";" );
        if( ( int ) $res[ 0 ][ 'count' ] !== 1 )
        {
            $res = $this->query( "INSERT INTO result(assignment,type,step,data) VALUES (" . ( int ) $aid . "," . $rtype . "," . $rstep . ",E'" . $e_serialized . "');" );
            $results[] = array (
                    ( int ) $aid, 
                    $rtype, 
                    $rstep, 
                    $data 
            );
            
            if( $type == 'VALIDATED' )
            {
                // Make a copy of the result set and store it as the initial state for the next step
                // (but only do it if it makes sense - i.e. if there actually is a next step)
                $nextStep = $this->getNextResultStep( $rstep );
                if( $nextStep !== NULL )
                {
                    // Strip the observations from the copy - as observations during one step are
                    // not supposed to be repeated in the next one.
                    $dataCopy = array ();
                    if( $mark )
                        $dataCopy[ 'mark' ] = $mark;
                    if( $print )
                        $dataCopy[ 'print' ] = $print;
                    $dataCopyEncoded = json_encode( $dataCopy );
                    
                    $res = $this->query( "INSERT INTO result(assignment,type,step,data) VALUES (" . ( int ) $aid . "," . $rtypes[ 'CURRENT' ] . "," . ( int ) $nextStep[ 'id' ] . ",E'" . pg_escape_string( $dataCopyEncoded ) . "');" );
                    $results[] = array (
                            ( int ) $aid, 
                            $rtypes[ 'CURRENT' ], 
                            ( int ) $nextStep[ 'id' ], 
                            $dataCopy 
                    );
                }
            }
        } else
        {
            // Second check: one cannot update a VALIDATED result (this check is also enforced at the DB level,
            // but the "Access denied" message seems more appropriate for the end user - who should never see
            // it unless she is fiddling with the API).
            if( $type == 'VALIDATED' )
                throw new Exception( "Access denied" );
            
            $this->query( "UPDATE result SET data = E'" . $e_serialized . "', updatedon = now() WHERE assignment = " . $aid . " AND type = " . $rtype . " AND step = " . $rstep . ";" );
            $results[] = array (
                    ( int ) $aid, 
                    $rtype, 
                    $rstep, 
                    $data 
            );
        }
        
        // The result set itself is a JSON object, whose format is loosely defined. That is, it *must* contain
        // a number of informations, and it *may* contain more info, depending on the various plugins that are
        // running on the client side (e.g. additionnal tools that may provide additionnal features). PiAnoS is
        // designed to make at least accessible to the SQL queries the following data:
        // - all annotations (mandatory, not NULL): ID, type ('minutia', 'ridge', ...), date created, date modified
        // - all annotations (optional, may be NULL): pairing ID, subtype (e.g. 'quality', 'wcs', ...), value (arbitrary value whose interpretation depends on the subtype)
        // - minutiae: X,Y,Theta,Type
        // - ridges/areas: type, list of points, color, opacity, width
        $this->updateResultsData( $results );
        
        if( Application::get( 'archive_results' ) )
        {
            // Archive the data in the history (the history does not have a PK, hence all results are
            // continuously added to it). Remember to purge it some day !
            $this->query( "INSERT INTO resulthistory(assignment,type,step,data) VALUES (" . ( int ) $aid . "," . $rtype . "," . $rstep . ",E'" . $e_serialized . "');" );
        }
        
        $this->commitTransactionIf( $trtoken );
    }
    private function getMinutiaTypes()
    {
        $res = $this->query( "SELECT * FROM minutiatype ORDER BY id ASC;" );
        $ret = array ();
        foreach( $res as $item )
            $ret[ $item[ 'name' ] ] = ( int ) $item[ 'id' ];
        return $ret;
    }
    private function updateResultsData( $results )
    {
        foreach( $results as $result )
        {
            $aid = $result[ 0 ];
            $rtype = $result[ 1 ];
            $rstep = $result[ 2 ];
            $data = $result[ 3 ];
            
            $this->query( "DELETE FROM minutia WHERE assignment = " . $aid . " AND restype = " . $rtype . " AND resstep = " . $rstep . ";" );
            $this->query( "DELETE FROM annotation WHERE assignment = " . $aid . " AND restype = " . $rtype . " AND resstep = " . $rstep . ";" );
            $this->query( "DELETE FROM observation WHERE assignment = " . $aid . " AND restype = " . $rtype . " AND resstep = " . $rstep . ";" );
            
            if( array_key_exists( 'observations', $data ) )
            {
                foreach( $data[ 'observations' ] as $obsType => $obsValue )
                {
                    $this->query( "INSERT INTO observation(assignment,restype,resstep,type,value) VALUES (" . $aid . "," . $rtype . "," . $rstep . ",(SELECT id FROM observationtype WHERE name = E'" . pg_escape_string( $obsType ) . "'),E'" . pg_escape_string( json_encode( $obsValue ) ) . "');" );
                }
            }
            
            $imgtypes = array ();
            if( array_key_exists( 'mark', $data ) )
                $imgtypes[ 'MARK' ] = $data[ 'mark' ];
            if( array_key_exists( 'print', $data ) )
                $imgtypes[ 'PRINT' ] = $data[ 'print' ];
            
            foreach( $imgtypes as $imgTypeShort => $imgData )
            {
                $res = $this->query( "SELECT id FROM imagetype WHERE short = E'" . pg_escape_string( $imgTypeShort ) . "';" );
                $imgTypeID = ( int ) $res[ 0 ][ 'id' ];
                
                $pkfields = array (
                        'assignment', 
                        'restype', 
                        'resstep' 
                );
                $pkvalues = array (
                        $aid, 
                        $rtype, 
                        $rstep 
                );
                
                foreach( $imgData as $aTypeNamePlural => $annotations )
                {
                    if( $aTypeNamePlural == 'image' )
                        continue;
                    
                    $basefields = array_merge( $pkfields, array (
                            'image' 
                    ) );
                    $basevalues = array_merge( $pkvalues, array (
                            $imgTypeID 
                    ) );
                    
                    // FIXME!
                    // This is *very* ugly: the front-end sends the annotations as arrays depending on the type
                    // i.e. 'minutiae' => array_of_minutiae, 'ridges' => array_of_ridges, etc.
                    // Each annotation does *not* include the actual type - which can be inferred from the name
                    // of the key (e.g. things in the 'minutiae' array have the type 'minutia').
                    // This is heavily broken because extensibility is basically impossible. It is also coupled
                    // with the problem of finding the type of an annotation in the Javascript code - the allAnnotations
                    // array vs. the annotations.minutiae, annotations.ridges, etc. thing.
                    // This needs refactoring. In the meantime, we infer the type from the plural term. Additionnal
                    // annotation types will not be recognized - not too much trouble for the moment.
                    switch( $aTypeNamePlural )
                    {
                        case 'minutiae':
                            $aTypeName = 'minutia';
                            break;
                        case 'ridges':
                            $aTypeName = 'ridge';
                            break;
                        case 'areas':
                            $aTypeName = 'area';
                            break;
                        case 'comments':
                            $aTypeName = 'comment';
                            break;
                        default:
                            $aTypeName = $aTypeNamePlural;
                    }
                    $res = $this->query( "SELECT getAnnotationType(E'" . pg_escape_string( $aTypeName ) . "') AS id;" );
                    $aTypeID = ( int ) $res[ 0 ][ 'id' ];
                    
                    foreach( $annotations as $id => $values )
                    {
                        $afields = array (
                                'id', 
                                'type', 
                                'createdon', 
                                'updatedon' 
                        );
                        $avalues = array (
                                ( int ) $id, 
                                $aTypeID, 
                                "E'" . pg_escape_string( $values[ 'created' ] ) . "'", 
                                "E'" . pg_escape_string( $values[ 'modified' ] ) . "'" 
                        );
                        
                        if( array_key_exists( 'pairing', $values ) )
                        {
                            $afields[] = 'pairing';
                            $avalues[] = ( int ) $values[ 'pairing' ];
                        }
                        if( array_key_exists( 'type', $values ) && $aTypeName != 'minutia' )
                        {
                            // Don't write the subtype for minutiae ('bifurcation', etc. is normalized through
                            // the minutiatype table and the 'type' field of the minutia table
                            $afields[] = 'subtype';
                            $avalues[] = "E'" . pg_escape_string( $values[ 'type' ] ) . "'";
                        }
                        if( array_key_exists( 'value', $values ) )
                        {
                            $afields[] = 'value';
                            $avalues[] = ( int ) $values[ 'value' ];
                        }
                        // Pack the group and target group into a 32-bit value, using the
                        // low-order 16 bits for the group, and the high-order bits for the
                        // target group. BTW, yes, the group IDs are limited to 1-65535.
                        $groups = 0;
                        if( array_key_exists( 'group', $values ) )
                            $groups += 0x0000ffff & ( int ) $values[ 'group' ];
                        if( array_key_exists( 'target_group', $values ) )
                            $groups += 0xffff0000 & ( ( int ) $values[ 'target_group' ] << 16 );
                        $afields[] = 'groups';
                        $avalues[] = $groups;
                        
                        $sqlfields = implode( ',', array_merge( $basefields, $afields ) );
                        $sqlvalues = implode( ',', array_merge( $basevalues, $avalues ) );
                        
                        $this->query( "INSERT INTO annotation(" . $sqlfields . ") VALUES (" . $sqlvalues . ");" );
                    }
                }
                
                if( array_key_exists( 'minutiae', $imgData ) )
                {
                    $mtypes = $this->getMinutiaTypes();
                    foreach( $imgData[ 'minutiae' ] as $id => $values )
                    {
                        $mfields = array (
                                'id', 
                                'x', 
                                'y', 
                                'theta', 
                                'type' 
                        );
                        $mvalues = array (
                                ( int ) $id, 
                                ( double ) $values[ 'pos' ][ 0 ], 
                                ( double ) $values[ 'pos' ][ 1 ], 
                                ( double ) $values[ 'angle' ], 
                                $mtypes[ $values[ 'type' ] ] 
                        );
                        
                        $sqlfields = implode( ',', array_merge( $pkfields, $mfields ) );
                        $sqlvalues = implode( ',', array_merge( $pkvalues, $mvalues ) );
                        
                        $this->query( "INSERT INTO minutia(" . $sqlfields . ") VALUES (" . $sqlvalues . ");" );
                    }
                }
            }
        }
    }
    public function updateAllResults( $numItems = 100 )
    {
        if( ! _DEBUG )
            throw new Exception( "Invalid parameters" );
        
        $this->checkAdmin();
        
        $this->beginTransaction();
        
        $alla = $this->query( "SELECT assignment.id,count(result.*) AS numres FROM assignment,result WHERE result.assignment = assignment.id GROUP BY assignment.id HAVING count(result.*) > 0;" );
        $ret = array ();
        $itemCount = 0;
        foreach( $alla as $arow )
        {
            $aid = ( int ) $arow[ 'id' ];
            $res = $this->query( "SELECT result.type,result.step,count(annotation.*) AS numannot,count(observation.*) AS numobs
			FROM result
				LEFT OUTER JOIN annotation ON result.assignment = annotation.assignment 
					AND result.step = annotation.resstep 
					AND result.type = annotation.restype
				LEFT OUTER JOIN observation ON result.assignment = observation.assignment 
					AND result.step = observation.resstep 
					AND result.type = observation.restype
			WHERE result.assignment = " . $aid . "
			GROUP BY result.type,result.step;" );
            foreach( $res as $row )
            {
                $numAnnot = ( int ) $row[ 'numannot' ];
                $numObs = ( int ) $row[ 'numobs' ];
                if( $numAnnot === 0 && $numObs === 0 )
                {
                    $step = ( int ) $row[ 'step' ];
                    $type = ( int ) $row[ 'type' ];
                    $datares = $this->query( "SELECT data FROM result WHERE assignment = " . $aid . " AND step = " . $step . " AND type = " . $type . ";" );
                    if( count( $datares ) !== 1 )
                        throw new Exception( "Assertion failed" );
                    $data = json_decode( $datares[ 0 ][ 'data' ], true );
                    $this->updateResultsData( array (
                            array (
                                    $aid, 
                                    $type, 
                                    $step, 
                                    $data 
                            ) 
                    ) );
                    $ret[] = array (
                            $aid, 
                            $type, 
                            $step 
                    );
                    if( $itemCount ++  >= $numItems )
                        break;
                }
            }
            if( $itemCount >= $numItems )
                break;
        }
        
        $this->commitTransaction();
        
        return $ret;
    }
    
    /*
     * public function hasResults($aid, $exstep)
     * {
     * $results = $this->query("SELECT * FROM result WHERE assignment = ".(int)$aid." AND exstep = ".(int)$exstep.";");
     * return count($results) > 0;
     * }
     */
    public function getResults( $aid, $type, $step, $anonymousreview = 0, $multiCaseViewer = NULL )
    {
        // xdebug_break();
        // First thing to check: whether the user has access to the assignment
        if( ! $_SESSION[ 'user' ][ 'isadmin' ] )
        {
            $owner = $this->query( "SELECT object.creator FROM object,assignment WHERE object.id=assignment.id AND object.id = " . ( int ) $aid . ";" );
            if( count( $owner ) !== 1 || ( int ) $owner[ 0 ][ 'creator' ] !== $_SESSION[ 'user' ][ 'id' ] )
                throw new Exception( "Access denied" );
        }
        
        $rtypes = $this->getResultTypes();
        $rtypes[ 'LATEST' ] = - 1;
        $rsteps = $this->getResultSteps();
        
        if( ! array_key_exists( $type, $rtypes ) )
            throw new Exception( "Invalid parameters" );
        
        if( ! array_key_exists( $step, $rsteps ) )
            throw new Exception( "Invalid parameters" );
        $rstep = $rsteps[ $step ];
        
        if( $_SESSION[ 'user' ][ 'isadmin' ] && ( ( int ) $aid < 0 || true ) )
        {
            // If we're logged as admin, it is valid to receive a negative number as assignment ID.
            // It means "all assignments for the exercise -$aid"
            
            $multiCaseViewerWhere = "";
            if( $multiCaseViewer != NULL )
            {
                $multiCaseViewerAids = explode( ",", $multiCaseViewer );
                
                foreach( $multiCaseViewerAids as  $multiCaseViewerAid )
                {
                    $multiCaseViewerWhere .= " OR assignment.exercise = " . ( int ) $multiCaseViewerAid;
                }
            }
            
            $needMany = true;
            $limit = _DEBUG && is_int( Application::get( 'debug_max_results' ) ) ? ' LIMIT ' . Application::get( 'debug_max_results' ): '';
            $assignments = $this->query( "
			SELECT id,userid,username,groupnames,getAssignmentStatus(id) AS status FROM (
				SELECT object.id, person.id AS userid, person.name AS username, array_to_string(array_agg(pgroup.name),',') AS groupnames
				FROM assignment, object, person, membership, pgroup
				WHERE assignment.id = object.id
					AND object.creator = person.id
					AND assignment.ispreview IS false 
					AND ( assignment.exercise = " . - ( int ) $aid . " " . $multiCaseViewerWhere . " )
					AND membership.person = person.id
					AND membership.pgroup = pgroup.id
				GROUP BY object.id, object.updatedon, person.id, person.name
				ORDER BY object.updatedon DESC
			) AS foo
			ORDER BY username ASC " . $limit . ";" );
            // We'll need this value later on
            $analysisStepID = $rsteps[ 'ANALYSIS' ];
        } else
        {
            $needMany = false;
            $assignments = array (
                    array (
                            'id' => $aid 
                    ) 
            );
        }
        
        $ret = array ();
        $numAssignments = count( $assignments );
        foreach( $assignments as $item )
        {
            $rtype = NULL;
            if( $type == 'LATEST' )
            {
                // LATEST is not an actual result type - rather, we use a query to find which
                // result (of CURRENT, AUTOSAVE and VALIDATED) has been stored more recently.
                $res = $this->query( "
					SELECT result.*,resulttype.short,object.createdon 
					FROM result,resulttype,object
					WHERE result.assignment = " . ( int ) $item[ 'id' ] . "
						AND result.type = resulttype.id
						AND result.step = " . $rstep . "
						AND object.id = result.assignment
					ORDER BY result.updatedon DESC, object.createdon DESC
					LIMIT 1;" );
                if( count( $res ) == 0 )
                {
                    // There is no latest result - so there are no results at all
                    // (exercise was started, but nothing was saved). This condition
                    // is checked later (if ($rtype===NULL))
                    $rtype = NULL;
                } else
                {
                    // Use the result type we've found
                    $rtype = ( int ) $res[ 0 ][ 'type' ];
                }
            }
            
            if( $rtype === NULL )
            {
                // If $rtype is NULL at this point, we default to CURRENT so that the query below
                // works. The result set will be empty, indeed.
                $rtype = ( int ) $rtypes[ 'CURRENT' ];
            }
            
            $res = $this->query( "SELECT * FROM result WHERE assignment = " . ( int ) $item[ 'id' ] . " AND type = " . $rtype . " AND step = " . $rstep . ";" );
            if( count( $res ) !== 1 )
            {
                // No results at all
                $data = array ();
            } else
            {
                $result = $res[ 0 ];
                $data = json_decode( $result[ 'data' ], true );
            }
            
            if( ! $needMany )
                return $data;
                
                // CODE BELOW DOES NOT RUN IN SINGLE USER MODE
                
            // Attach additional data relative to the user (the annotation themselves contain the userid
                // but not the name)
            $data[ 'user' ] = array (
                    'id' => ( int ) $item[ 'userid' ], 
                    'name' => ( ( int ) $anonymousreview ) ? 'anonymous_' . $item[ 'id' ]: $item[ 'username' ], 
                    'groups' => $item[ 'groupnames' ] 
            );
            
            // Additional data in comparison mode
            
            $data[ 'progress' ] = array (
                    'status' => $item[ 'status' ] 
            );
            if( $rstep > $analysisStepID && $item[ 'status' ] >= 2 )
            {
                // Data for the correct display of "yellow" minutiae: add the timestamp
                // of the submission of the analysis (the GUI will compare this to the timestamp
                // of the minutiae)
                $data[ 'progress' ][ 'analysis_submitted_on' ] = $this->getAssignmentSubmissionTime( $item[ 'id' ], $analysisStepID );
                
                // Conclusions of the analysis - these are not present in the JSON result, which
                // contains only the results for the current step (in the 'observations' field)
                $res = $this->query( "SELECT value,name FROM observationv WHERE assignment=" . ( int ) $item[ 'id' ] . " AND restype=" . $rtypes[ 'VALIDATED' ] . " AND resstep=" . $rsteps[ 'ANALYSIS' ] . ";" );
                $oarr = array ();
                foreach( $res as $obs )
                    $oarr[ $obs[ 'name' ] ] = json_decode( $obs[ 'value' ], true );
                $data[ 'analysis' ] = $oarr;
            }
            
            $ret[] = $data;
        }
        
        return $ret;
    }
    public function deleteAllResults( $aid )
    {
        $this->checkAdmin();
        
        $results = $this->query( "SELECT * FROM result WHERE assignment = " . ( int ) $aid . ";" );
        // foreach ($reults as $result)
    }
    
    // Statistics
    public function search( $what )
    {
        $this->checkAdmin();
        if( strlen( $what ) == 0 )
            throw new Exception( "Invalid parameters" );
        $res = $this->query( "
		SELECT ts_rank(ftsv.fts, query) AS rank, ftsv.type, ftsv.id, ftsv.name, ftsv.desc
		FROM ftsv, plainto_tsquery(E'" . pg_escape_string( $what ) . "%') AS query 
		WHERE ftsv.fts @@ query
		ORDER BY rank DESC;" );
        return $res;
    }
    public function getAllConclusions( $exid, $step = NULL, $type = NULL, $anonymousreview = NULL, $multiCaseViewer = NULL )
    {
        $this->checkAdmin();
        
        return $this->getExerciseAssignments( $exid, true, $anonymousreview, $multiCaseViewer );
        
        $columns = array (
                'object.creator AS uid', 
                'observationtype.name', 
                'observation.value' 
        );
        $from = array (
                'object', 
                'assignment', 
                'observationtype', 
                'observation' 
        );
        
        $multiCaseViewerWhere = "";
        if( $multiCaseViewer != NULL )
        {
            $multiCaseViewerAids = explode( ",", $multiCaseViewer );
            
            foreach( $multiCaseViewerAids as  $multiCaseViewerAid )
            {
                $multiCaseViewerWhere .= " OR assignment.exercise = " . ( int ) $multiCaseViewerAid;
            }
        }
        
        $where = "object.id = assignment.id
				AND ( assignment.exercise = " . ( int ) $exid . " " . $multiCaseViewerWhere . ")
				AND assignment.id = observation.assignment
				AND observation.type = observationtype.id";
        
        if( $step )
        {
            if( is_string( $step ) )
            {
                $where .= " AND resultstep.id = observation.resstep AND resultstep.short ILIKE E'" . pg_escape_string( $step ) . "'";
                $from[] = 'resultstep';
            } else
                $where .= " AND observation.resstep = " . ( int ) $step;
            
            $columns[] = 'observation.resstep AS step';
        }
        
        if( $type )
        {
            if( is_string( $type ) )
                $where .= " AND observationtype.name = E'" . pg_escape_string( $type ) . "'";
            else
                $where .= " AND observation.type = " . ( int ) $type;
        }
        
        $sql = "SELECT " . implode( $columns, ',' ) . "
			FROM " . implode( $from, ', ' ) . "
			WHERE " . $where . ";";
        
        throw new Exception( $sql );
        $res = $this->query( $sql );
        return $res;
    }
    
    // User views
    public function getHomeData()
    {
        $res = $this->query( "
        	SELECT
        		folder.id,
        		'My personal folder' AS name,
        		array_agg(ex.id) AS exids,
        		array_agg(ex.name) AS exnames
        	
        	FROM
        		folder
        	
        	LEFT OUTER JOIN
        		(
        			SELECT
        				id,
        				name,
        				folder
        			FROM
        				exercise
        			ORDER BY
        				name ASC
        		) AS ex
        		
        	ON
        		ex.folder = folder.id
        		
        	WHERE
        		folder.personalof = " . ( int ) $_SESSION[ 'user' ][ 'id' ] . "
        	
        	GROUP BY
        		folder.id;
        	" );
        
        if( count( $res ) > 1 )
            throw new Exception( "Database integrity error" );
        
        if( count( $res ) )
        {
            $folder = $res[ 0 ];
            $folder[ 'exids' ] = $folder[ 'exids' ] == '{NULL}' ? array (): $this->intSqlArrayToPhp( $folder[ 'exids' ] );
            $folder[ 'exnames' ] = $folder[ 'exnames' ] == '{NULL}' ? array (): $this->stringSqlArrayToPhp( $folder[ 'exnames' ] );
            $folder[ 'name' ] = 'My personal folder';
        } else
            $folder = NULL;
        
        $ret = array (
                'progress' => $this->getFolderExercises( NULL, true ), 
                'folder' => $folder 
        );
        return $ret;
    }
    public function createPersonalFolder( $id = NULL )
    {
        $uid = $_SESSION[ 'user' ][ 'isadmin' ] ? ( int ) $id: ( int ) $_SESSION[ 'user' ][ 'id' ];
        $this->beginTransaction();
        $res = $this->query( "INSERT INTO folder (name,personalof) VALUES ((SELECT name FROM person WHERE id = " . $uid . ")||E'\'s folder'," . $uid . ") RETURNING id;" );
        $fid = ( int ) $res[ 0 ][ 'id' ];
        $this->query( "SELECT applyACL(" . $fid . ",ARRAY[]::integer[], ARRAY[" . $uid . "]);" );
        $this->commitTransaction();
    }
    public function getSuitabilityApproach( $userid = NULL )
    {
        if( $_SESSION[ 'user' ][ 'isadmin' ] )
        {
            $userid = $userid ? ( int ) $userid: $_SESSION[ 'user' ][ 'id' ];
        } else
        {
            $userid = ( int ) $_SESSION[ 'user' ][ 'id' ];
        }
        
        $res = $this->query( "SELECT suitability_approach AS approach FROM person WHERE id = " . $userid . ";" );
        if( count( $res ) != 1 )
            throw new Exception( "Access denied" );
        $val = ( int ) $res[ 0 ][ 'approach' ];
        if( $val != 1 && $val != 2 )
            throw new Exception( "Database integrity error" );
        return $val;
    }
    public function isMarkOfNoValue( $assignmentID )
    {
        if( ! $_SESSION[ 'user' ][ 'isadmin' ] )
        {
            $res = $this->query( "SELECT creator FROM object WHERE id = " . ( int ) $assignmentID . ";" );
            if( ( int ) $res[ 0 ][ 'creator' ] !== ( int ) $_SESSION[ 'user' ][ 'id' ] )
                throw new Exception( "Access denied" );
        }
        $res = $this->query( "SELECT value FROM observation WHERE assignment = " . ( int ) $assignmentID . " AND restype = (SELECT ID FROM resulttype WHERE short = 'VALIDATED') AND resstep = (SELECT ID FROM resultstep WHERE short = 'ANALYSIS') AND type = (SELECT ID FROM observationtype WHERE name = 'mark-value');" );
        if( count( $res ) != 1 )
            throw new Exception( "Database integrity error" );
        $val = ( int ) $res[ 0 ][ 'value' ];
        if( $val !== 0 && $val !== 1 && $val !== 2 && $val !== 3 )
            throw new Exception( "Database integrity error" );
        return $val === 0;
    }
    
    /*
     *  Admin view
     */
    public function getUserList()
    {
    	if( ! $_SESSION[ 'user' ][ 'isadmin' ] )
    	{
    		throw new Exception( "Access denied" );
    	} else {
	    	$res = $this->query( "
				SELECT 
					(CASE WHEN pgroup.tutors THEN 'Tutor' ELSE 'User' END) as tutors, 
					person.id, 
					person.name
				FROM 
					public.pgroup, 
					public.person, 
					public.membership
				WHERE 
					person.id = membership.person AND
					membership.pgroup = pgroup.id AND
	    			person.active = true
	    		
	    		ORDER BY person.name ASC;
				"
	    	);
	    	
	    	return $res;
    	}
    }
    
    // Messaging subsystem
    public function getHomeMessages()
    {
        // TODO: this should be removed and message filtering implemented according to user rights.
        $this->checkAdmin();
        
        $res = $this->query( "SELECT * FROM messagev WHERE type='home' AND visibility IN ('tutors','all') ORDER BY createdon DESC;" );
        return $res;
    }
    public function getMessageConstants()
    {
        $this->checkAdmin();
        $r1 = $this->query( "SELECT * FROM messagetype ORDER BY id ASC;" );
        $r2 = $this->query( "SELECT * FROM messagevisibility ORDER BY id ASC;" );
        return array (
                $r1, 
                $r2 
        );
    }
    public function getAllMessages()
    {
        $this->checkAdmin();
        $res = $this->query( "SELECT * FROM messagev ORDER BY createdon ASC;" );
        return $res;
    }
    public function getMessage( $id )
    {
        $this->checkAdmin();
        $res = $this->query( "SELECT * FROM messagev WHERE id=" . ( int ) $id . ";" );
        if( count( $res ) !== 1 )
            throw new Exception( "Access denied" );
        return $res[ 0 ];
    }
    public function deleteMessage( $id )
    {
        $this->checkAdmin();
        $trtoken = $this->beginTransactionIf();
        $this->query( "DELETE FROM message WHERE id=" . ( int ) $id . ";" );
        $this->query( "DELETE FROM object WHERE id=" . ( int ) $id . ";" );
        $this->commitTransactionIf( $trtoken );
    }
    public function addMessage( $title, $type, $visibility, $value )
    {
        $this->checkAdmin();
        $res = $this->query( "INSERT INTO message(title,type,visibility,value) VALUES (E'" . pg_escape_string( $title ) . "', " . ( int ) $type . ", " . ( int ) $visibility . ", E'" . pg_escape_string( $value ) . "') RETURNING id;" );
        return ( int ) $res[ 0 ][ 'id' ];
    }
    public function updateMessage( $id, $title, $type, $visibility, $value )
    {
        $this->checkAdmin();
        $res = $this->query( "UPDATE message SET title=E'" . pg_escape_string( $title ) . "', type=" . ( int ) $type . ", visibility=" . ( int ) $visibility . ", value=E'" . pg_escape_string( $value ) . "' WHERE id=" . ( int ) $id . ";" );
    }
}
;
