<?php
    class AuditLog extends \LimeSurvey\PluginManager\PluginBase {

        protected $storage = 'DbStorage';
        static protected $description = 'Core: Create an audit log of changes';
        static protected $name = 'auditlog';

        protected $settings = array(
            'AuditLog_Log_UserSave' => array(
                'type' => 'checkbox',
                'label' => 'Log if a user was modified or created',
                'default' => '1',
            ),
            'AuditLog_Log_UserLogin' => array(
                'type' => 'checkbox',
                'label' => 'Log if a user has logged in successfully',
                'default' => '1',
            ),
            'AuditLog_Log_UserLogout' => array(
                'type' => 'checkbox',
                'label' =>  'Log if user has logged out',                                                
                'default' => '1',
            ),
            'AuditLog_Log_UserFailedLoginAttempt' => array(
                'type' => 'checkbox',
                'label' => 'Log if a user login has failed',
                'default' => '1',
            ),
            'AuditLog_Log_UserDelete' => array(
                'type' => 'checkbox',
                'label' => 'Log if a user was deleted',
                'default' => '1',
            ),
            'AuditLog_Log_DataEntryCreate' => array(
                'type' => 'checkbox',
                'label' => 'Log if a survey admin creates a response',
                'default' => '1',
            ),
            'AuditLog_Log_DataEntryUpdate' => array(
                'type' => 'checkbox',
                'label' => 'Log if a survey admin modifies a response',
                'default' => '1',
            ),
            'AuditLog_Log_DataEntryDelete' => array(
                'type' => 'checkbox',
                'label' => 'Log if a survey admin delete a response',
                'default' => '1',
            ),
            'AuditLog_Log_DataEntryImport' => array(
                'type' => 'checkbox',
                'label' => 'Log if a survey admin imports responses',
                'default' => '1',
            ),
            'AuditLog_Log_TokenSave' => array(
                'type' => 'checkbox',
                'label' => 'Log if a survey participant was modified or created',
                'default' => '1',
            ),
            'AuditLog_Log_TokenDelete' => array(
                'type' => 'checkbox',
                'label' => 'Log if a survey participant was deleted',
                'default' => '1',
            ),
            'AuditLog_Log_ParticipantSave' => array(
                'type' => 'checkbox',
                'label' => 'Log if a central database participant was modified or created',
                'default' => '1',
            ),
            'AuditLog_Log_ParticipantDelete' => array(
                'type' => 'checkbox',
                'label' => 'Log if a central database participant was deleted',
                'default' => '1',
            ),
            'AuditLog_Log_UserPermissionsChanged' => array(
                'type' => 'checkbox',
                'label' => 'Log if a user permissions changes',
                'default' => '1',
            ),
            'AuditLog_Log_SurveySettings' => array(
                'type' => 'checkbox',
                'label' => 'Log if a user changes survey settings',
                'default' => '1',
            ),
        );


        public function init() {
            $this->subscribe('beforeSurveySettings');
            $this->subscribe('newSurveySettings');
            $this->subscribe('beforeSurveySettingsSave');
            $this->subscribe('beforeActivate');
            $this->subscribe('beforeUserSave');
            $this->subscribe('beforeUserDelete');
            $this->subscribe('beforePermissionSetSave');
            $this->subscribe('beforeDataEntryCreate');
            $this->subscribe('beforeDataEntryUpdate');
            $this->subscribe('beforeDataEntryDelete');
            $this->subscribe('beforeDataEntryImport');
            $this->subscribe('beforeTokenSave');
            $this->subscribe('beforeTokenDelete');
            $this->subscribe('beforeParticipantSave');
            $this->subscribe('beforeParticipantDelete');
            $this->subscribe('beforeLogout');
            $this->subscribe('afterSuccessfulLogin');
            $this->subscribe('afterFailedLoginAttempt');
        }

        /**
        * check for setting for a single operation event, login user, save or delete
        * @return boolean
        */
        private function checkSetting($settingName) {
            $pluginsettings = $this->getPluginSettings(true);
            // Logging will done if setted to true
            return $pluginsettings[$settingName]['current'] == 1;
        }


        /**
        * User logout to the audit log
        * @return unknown_type
        */
        public function beforeLogout()
        {
            if (!$this->checkSetting('AuditLog_Log_UserLogout')) {
                return;
            }
            $oUser = $this->api->getCurrentUser();
            if ($oUser != false)
            {
                $iUserID = $oUser->uid;
                $oAutoLog = $this->api->newModel($this, 'log');
                $oAutoLog->uid=$iUserID;
                $oAutoLog->entity='user';
                $oAutoLog->entityid=$iUserID;
                $oAutoLog->action='beforeLogout';
                $oAutoLog->save();
            }
        }

        /**
        * Successfull login to the audit log
        * @return unknown_type
        */
        public function afterSuccessfulLogin()
        {
            if (!$this->checkSetting('AuditLog_Log_UserLogin')) {
                return;
            }

            $iUserID=$this->api->getCurrentUser()->uid;
            $oAutoLog = $this->api->newModel($this, 'log');
            $oAutoLog->uid=$iUserID;
            $oAutoLog->entity='user';
            $oAutoLog->entityid=$iUserID;
            $oAutoLog->action='afterSuccessfulLogin';
            $oAutoLog->save();
        }

        /**
        * Failed login attempt to the audit log
        * @return unknown_type
        */
        public function afterFailedLoginAttempt()
        {
            if (!$this->checkSetting('AuditLog_Log_UserFailedLoginAttempt')) {
                return;
            }
            $event = $this->getEvent();
            $identity = $event->get('identity');
            $oAutoLog = $this->api->newModel($this, 'log');
            $oAutoLog->entity='user';
            $oAutoLog->action='afterFailedLoginAttempt';
            $aUsername['username'] = $identity->username;
            $oAutoLog->newvalues = json_encode($aUsername);
            $oAutoLog->save();
        }

        /**
        * Saves permissions changes to the audit log
        */
        public function beforePermissionSetSave()
        {

            if (!$this->checkSetting('AuditLog_Log_UserPermissionsChanged')) {
                return;
            }

            $event = $this->getEvent();
            $aNewPermissions=$event->get('aNewPermissions');
            $iSurveyID=$event->get('iSurveyID');
            $iUserID=$event->get('iUserID');
            $oCurrentUser=$this->api->getCurrentUser();
            $oOldPermission=$this->api->getPermissionSet($iUserID, $iSurveyID, 'survey');
            $sAction='update';   // Permissions are in general only updated (either you have a permission or you don't)

            if (count(array_diff_assoc_recursive($aNewPermissions,$oOldPermission)))
            {
                $oAutoLog = $this->api->newModel($this, 'log');
                $oAutoLog->uid=$oCurrentUser->uid;
                $oAutoLog->entity='permission';
                $oAutoLog->entityid=$iSurveyID;
                $oAutoLog->action=$sAction;
                $oAutoLog->oldvalues=json_encode(array_diff_assoc_recursive($oOldPermission,$aNewPermissions));
                $oAutoLog->newvalues=json_encode(array_diff_assoc_recursive($aNewPermissions,$oOldPermission));
                $oAutoLog->fields=implode(',',array_keys(array_diff_assoc_recursive($aNewPermissions,$oOldPermission)));
                $oAutoLog->save();
            }
        }

        /**
        * Function catches if a response was created
        * @return unknown_type
        */
        public function beforeDataEntryCreate()
        {
            $event = $this->getEvent();
            $iSurveyID=$event->get('iSurveyID');
            if (!$this->checkSetting('AuditLog_Log_DataEntryCreate') || !$this->get('auditing', 'Survey', $iSurveyID, true)) {
                return;
            }

            $oCurrentUser = $this->api->getCurrentUser();
            $currentUID = $oCurrentUser ? $oCurrentUser->uid : null;

            $aValues = $event->get('oModel')->getAttributes();
            if (count($aValues)){
                $oAutoLog = $this->api->newModel($this, 'log');
                $oAutoLog->uid=$currentUID;
                $oAutoLog->entity='survey_' . $iSurveyID;
                $oAutoLog->action="create";
                $oAutoLog->newvalues=json_encode($aValues);
                $oAutoLog->save();
            }
        }

        /**
        * Function catches if a response was modified
        * @return unknown_type
        */
        public function beforeDataEntryUpdate()
        {
            $event = $this->getEvent();
            $iSurveyID=$event->get('iSurveyID');
            if (!$this->checkSetting('AuditLog_Log_DataEntryUpdate') || !$this->get('auditing', 'Survey', $iSurveyID, true)) {
                return;
            }

            $oCurrentUser = $this->api->getCurrentUser();
            $currentUID = $oCurrentUser ? $oCurrentUser->uid : null;
            $oldvalues= $this->api->getResponse($iSurveyID, $event->get('iResponseID'), false);

            $aDiffOld = array();
            $aDiffNew = array();
            foreach ($oldvalues->attributes as $aFieldName => $sValue) {
                if (App()->request->getPost($aFieldName) !== null) {
                    $oldValue = $sValue;
                    $newValue = App()->request->getPost($aFieldName);
                    if ($oldValue != $newValue) {
                        $aDiffOld[$aFieldName] = $oldValue;
                        $aDiffNew[$aFieldName] = $newValue;
                    }
                }
            }

            if (count($aDiffOld)){
                $oAutoLog = $this->api->newModel($this, 'log');
                $oAutoLog->uid=$currentUID;
                $oAutoLog->entity='survey_' . $iSurveyID;
                $oAutoLog->action="update";
                $oAutoLog->entityid=$event->get('iResponseID');
                $oAutoLog->oldvalues=json_encode($aDiffOld);
                $oAutoLog->newvalues=json_encode($aDiffNew);
                $oAutoLog->fields=implode(',',array_keys($aDiffOld));
                $oAutoLog->save();
            }
        }

        /**
        * Function catches if a response was deleted
        * @return unknown_type
        */
        public function beforeDataEntryDelete()
        {
            $event = $this->getEvent();
            $iSurveyID=$event->get('iSurveyID');
            if (!$this->checkSetting('AuditLog_Log_DataEntryDelete') || !$this->get('auditing', 'Survey', $iSurveyID, true)) {
                return;
            }

            $oCurrentUser = $this->api->getCurrentUser();
            $currentUID = $oCurrentUser ? $oCurrentUser->uid : null;
            $oldvalues = $this->api->getResponse($iSurveyID, $event->get('iResponseID'), true);

            $oAutoLog = $this->api->newModel($this, 'log');
            $oAutoLog->uid=$currentUID;
            $oAutoLog->entity='survey_' . $iSurveyID;
            $oAutoLog->action="delete";
            $oAutoLog->entityid=$event->get('iResponseID');
            $oAutoLog->oldvalues=json_encode($oldvalues);
            $oAutoLog->save();
        }

        /**
        * Log import responses
        * @return unknown_type
        */
        public function beforeDataEntryImport()
        {
            $event = $this->getEvent();
            $iSurveyID=$event->get('iSurveyID');
            if (!$this->checkSetting('AuditLog_Log_DataEntryImport') || !$this->get('auditing', 'Survey', $iSurveyID, true)) {
                return;
            }

            $oCurrentUser = $this->api->getCurrentUser();
            $currentUID = $oCurrentUser ? $oCurrentUser->uid : null;

            $oModel = $this->getEvent()->get('oModel');
            $aValues = $oModel->getAttributes();
            if (count($aValues)){
                $oAutoLog = $this->api->newModel($this, 'log');
                $oAutoLog->uid=$currentUID;
                $oAutoLog->entity='survey_' . $iSurveyID;
                $oAutoLog->action="import";
                $oAutoLog->newvalues=json_encode($aValues);
                $oAutoLog->fields=implode(',',array_keys($aValues));
                $oAutoLog->save();
            }
        }

        /**
        * Function catches if a participant of a particular survey was modified or created
        * All data is saved - only the password hash is anonymized for security reasons
        */
        public function beforeTokenSave()
        {
            $event = $this->getEvent();
            $iSurveyID=$event->get('iSurveyID');
            if (!$this->checkSetting('AuditLog_Log_TokenSave') || !$this->get('auditing', 'Survey', $iSurveyID, false)) {
                return;
            }

            $oNewParticipant=$this->getEvent()->get('model');
            $oCurrentUser = $this->api->getCurrentUser();
            $currentUID = $oCurrentUser ? $oCurrentUser->uid : null;
            if ($oNewParticipant->isNewRecord)
            {
                $sAction = 'create';
                $oldvalues = array();
            }
            else
            {
                $sAction = 'update';
                $oldvalues= $this->api->getTokenById($iSurveyID, $oNewParticipant->tid)->getAttributes();
            }

            $newValues=$oNewParticipant->getAttributes();

            if (count(array_diff_assoc($newValues,$oldvalues))){
                $oAutoLog = $this->api->newModel($this, 'log');
                $oAutoLog->uid=$currentUID;
                $oAutoLog->entity='token_' . $iSurveyID;
                $oAutoLog->action=$sAction;
                $oAutoLog->entityid=$newValues['tid'];
                $oAutoLog->oldvalues=json_encode(array_diff_assoc($oldvalues,$newValues));
                $oAutoLog->newvalues=json_encode(array_diff_assoc($newValues,$oldvalues));
                $oAutoLog->fields=implode(',',array_keys(array_diff_assoc($newValues,$oldvalues)));
                $oAutoLog->save();
            }
        }

        /**
        * Function catches if a participant of a particular survey was modified or created
        * All data is saved - only the password hash is anonymized for security reasons
        */
        public function beforeTokenDelete()
        {
            $event = $this->getEvent();
            $iSurveyID=$event->get('iSurveyID');
            if (!$this->checkSetting('AuditLog_Log_TokenDelete') || !$this->get('auditing', 'Survey', $iSurveyID, true)) {
                return;
            }

            $sTokenIds=$this->getEvent()->get('sTokenIds');
            $aTokenIds = explode(',', $sTokenIds);
            $oCurrentUser=$this->api->getCurrentUser();

            foreach ($aTokenIds as $tokenId)
            {
                $token = Token::model($iSurveyID)->find('tid=' . $tokenId);

                if (!is_null($token))
                {
                    $aValues=$token->getAttributes();
                    $oAutoLog = $this->api->newModel($this, 'log');
                    $oAutoLog->uid=$oCurrentUser->uid;
                    $oAutoLog->entity='token';
                    $oAutoLog->action='delete';
                    $oAutoLog->entityid=$aValues['participant_id'];
                    $oAutoLog->oldvalues=json_encode($aValues);
                    $oAutoLog->fields=implode(',',array_keys($aValues));
                    $oAutoLog->save();
                }
            }
        }

        /**
        * Function catches if a central database participant was modified or created
        * All data is saved - only the password hash is anonymized for security reasons
        */
        public function beforeParticipantSave()
        {
            if (!$this->checkSetting('AuditLog_Log_ParticipantSave')) {
                return;
            }
            $oNewParticipant=$this->getEvent()->get('model');
            if ($oNewParticipant->isNewRecord)
            {
                $sAction = 'create';
                $aOldValues = array();
            }
            else
            {
                $sAction = 'update';
                $aOldValues=$this->api->getParticipant($oNewParticipant->participant_id)->getAttributes();
            }
            $oCurrentUser=$this->api->getCurrentUser();
            $aNewValues=$oNewParticipant->getAttributes();
            if (count(array_diff_assoc($aNewValues,$aOldValues)))
            {
                $oAutoLog = $this->api->newModel($this, 'log');
                $oAutoLog->uid=$oCurrentUser->uid;
                $oAutoLog->entity='participant';
                $oAutoLog->action=$sAction;
                $oAutoLog->entityid=$aNewValues['participant_id'];
                $oAutoLog->oldvalues=json_encode(array_diff_assoc($aOldValues,$aNewValues));
                $oAutoLog->newvalues=json_encode(array_diff_assoc($aNewValues,$aOldValues));
                $oAutoLog->fields=implode(',',array_keys(array_diff_assoc($aNewValues,$aOldValues)));
                $oAutoLog->save();
            }
        }

        /**
        * Function catches if a central database participant was modified or created
        * All data is saved - only the password hash is anonymized for security reasons
        */
        public function beforeParticipantDelete()
        {
            if (!$this->checkSetting('AuditLog_Log_ParticipantDelete')) {
                return;
            }
            $oNewParticipant=$this->getEvent()->get('model');
            $oCurrentUser=$this->api->getCurrentUser();

            $aValues=$oNewParticipant->getAttributes();

            $oAutoLog = $this->api->newModel($this, 'log');
            $oAutoLog->uid=$oCurrentUser->uid;
            $oAutoLog->entity='participant';
            $oAutoLog->action='delete';
            $oAutoLog->entityid=$aValues['participant_id'];
            $oAutoLog->oldvalues=json_encode($aValues);
            $oAutoLog->fields=implode(',',array_keys($aValues));
            $oAutoLog->save();
        }

        /**
        * Function catches if a user was modified or created
        * All data is saved - only the password hash is anonymized for security reasons
        */
        public function beforeUserSave()
        {

            if (!$this->checkSetting('AuditLog_Log_UserSave')) {
                return;
            }
            $oUserData=$this->getEvent()->get('model');

            $oCurrentUser=$this->api->getCurrentUser();

            $aNewValues=$oUserData->getAttributes();
            if (!isset($oUserData->uid))
            {
                $sAction='create';
                $aOldValues=array();
                // Indicate the password has changed but assign fake hash
                $aNewValues['password']='*MASKED*PASSWORD*';
            }
            else
            {
                $oOldUser=$this->api->getUser($oUserData->uid);
                $sAction='update';
                $aOldValues=$oOldUser->getAttributes();

                // Postgres delivers bytea fields as streams
                if (gettype($aOldValues['password'])=='resource')
                {
                    $aOldValues['password'] = stream_get_contents($aOldValues['password']);
                }
                // If the password has changed then indicate that it has changed but assign fake hashes
                if ($aNewValues['password']!=$aOldValues['password'])
                {
                    $aOldValues['password']='*MASKED*OLD*PASSWORD*';
                    $aNewValues['password']='*MASKED*NEW*PASSWORD*';
                }
            }

            if (count(array_diff_assoc($aNewValues,$aOldValues)))
            {
                $oAutoLog = $this->api->newModel($this, 'log');
                if ($oCurrentUser) {
                    $oAutoLog->uid=$oCurrentUser->uid;
                }
                else {
                    $oAutoLog->uid='Automatic creation';
                }
                $oAutoLog->entity='user';
                if ($sAction=='update') $oAutoLog->entityid=$oOldUser['uid'];
                $oAutoLog->action=$sAction;
                $oAutoLog->oldvalues=json_encode(array_diff_assoc($aOldValues,$aNewValues));
                $oAutoLog->newvalues=json_encode(array_diff_assoc($aNewValues,$aOldValues));
                $oAutoLog->fields=implode(',',array_keys(array_diff_assoc($aNewValues,$aOldValues)));
                $oAutoLog->save();
            }
        }

        /**
        * Function catches if a user was deleted
        * All data is saved - only the password hash is anonymized for security reasons
        */
        public function beforeUserDelete()
        {
            if (!$this->checkSetting('AuditLog_Log_UserDelete')) {
                return;
            }

            $oUserData=$this->getEvent()->get('model');
            $oCurrentUser=$this->api->getCurrentUser();
            $oOldUser=$this->api->getUser($oUserData->uid);
            if ($oOldUser)
            {
                $aOldValues=$oOldUser->getAttributes();
                unset($aOldValues['password']);
                $oAutoLog = $this->api->newModel($this, 'log');
                $oAutoLog->uid=$oCurrentUser->uid;
                $oAutoLog->entity='user';
                $oAutoLog->entityid=$oOldUser['uid'];
                $oAutoLog->action='delete';
                $oAutoLog->oldvalues=json_encode($aOldValues);
                $oAutoLog->fields=implode(',',array_keys($aOldValues));
                $oAutoLog->save();
            }
        }

        public function beforeActivate()
        {
            if (!$this->api->tableExists($this, 'log'))
            {
                $this->api->createTable($this, 'log', array('id'=>'pk',
                    'created'=>'datetime',
                    'uid'=>'string',
                    'entity'=>'string',
                    'entityid'=>'string',
                    'action'=>'string',
                    'fields'=>'text',
                    'oldvalues'=>'text',
                    'newvalues'=>'text'));
            }
        }

        /**
        * This event is fired by the administration panel to gather extra settings
        * available for a survey.
        * The plugin should return setting meta data.
        */
        public function beforeSurveySettings()
        {
            $pluginsettings = $this->getPluginSettings(true);

            $event = $this->getEvent();
            $event->set("surveysettings.{$this->id}", array(
                'name' => get_class($this),
                'settings' => array(
                    'auditing' => array(
                        'type' => 'select',
                        'options'=>array(0=>'No',
                            1=>'Yes'),
                        'default' => 1,
                        'tab' => 'notification', // @todo: Setting no used yet
                        'category' => 'Auditing for person-related data', // @todo: Setting no used yet
                        'label' => 'Audit log for this survey:',
                        'current' => $this->get('auditing', 'Survey', $event->get('survey'))
                    )
                )
            ));
        }

        public function newSurveySettings()
        {
            $event = $this->getEvent();
            foreach ($event->get('settings') as $name => $value)
            {
                    $this->set($name, $value, 'Survey', $event->get('survey'));
            }
        }

        public function beforeSurveySettingsSave()
        {
            $event = $this->getEvent();
            $oModifiedSurvey = $event->get('modifiedSurvey');
            $iSurveyID = $oModifiedSurvey->sid;
            if (!$this->checkSetting('AuditLog_Log_SurveySettings') || !$this->get('auditing', 'Survey', $iSurveyID, false)) {
                return;
            }

            $oCurrentUser=$this->api->getCurrentUser();
            if (!is_null($oModifiedSurvey)) {
                $newAttributes = $oModifiedSurvey->getAttributes();
                $oldSurvey=Survey::model()->find('sid = :sid', array(':sid' => $iSurveyID));

                $oldAttributes= $oldSurvey->getAttributes();
                $diff = array_diff_assoc($newAttributes, $oldAttributes);
                if (count($diff)>0){
                    $oAutoLog = $this->api->newModel($this, 'log');
                    $oAutoLog->uid=$oCurrentUser->uid;
                    $oAutoLog->entity='survey';
                    $oAutoLog->entityid=$iSurveyID;
                    $oAutoLog->action='update';
                    $oAutoLog->oldvalues=json_encode(array_diff_assoc($oldAttributes, $newAttributes));
                    $oAutoLog->newvalues=json_encode($diff);
                    #$oAutoLog->fields=json_encode($diff);
                    $oAutoLog->fields=implode(',',array_keys($diff));
                    $oAutoLog->save();
                }
            }
        }
    }
