HEX
Server: Apache
System: Linux WWW 6.1.0-40-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.153-1 (2025-09-20) x86_64
User: web11 (1011)
PHP: 8.2.29
Disabled: NONE
Upload Files
File: /var/www/apklausos/application/models/LSActiveRecord.php
<?php

/*
 * LimeSurvey
 * Copyright (C) 2013 The LimeSurvey Project Team / Carsten Schmitz
 * All rights reserved.
 * License: GNU/GPL License v2 or later, see LICENSE.php
 * LimeSurvey is free software. This version may have been modified pursuant
 * to the GNU General Public License, and as distributed it includes or
 * is derivative of works licensed under the GNU General Public License or
 * other free or open source software licenses.
 * See COPYRIGHT.php for copyright notices and details.
 *
  *     Extensions to the CActiveRecord class
 */

/**
 * @method PluginEvent dispatchPluginModelEvent(string $sEventName, CDbCriteria $criteria = null, array $eventParams = [])
 */
class LSActiveRecord extends CActiveRecord
{
    /** @var string[] Array of attributes that should be XSS filtered on mass updates */
    protected $xssFilterAttributes = [];

    public $bEncryption = false;

    /**
     * Lists the behaviors of this model
     *
     * Below is a list of all behaviors we register:
     * @return array
     * @see PluginEventBehavior
     * @see CTimestampBehavior
     */
    public function behaviors()
    {
        $aBehaviors = [];
        $sCreateFieldName = ($this->hasAttribute('created') ? 'created' : null);
        $sUpdateFieldName = ($this->hasAttribute('modified') ? 'modified' : null);
        $sDriverName = Yii::app()->db->getDriverName();
        if ($sDriverName == 'sqlsrv' || $sDriverName == 'dblib') {
            $sTimestampExpression = new CDbExpression('GETDATE()');
        } else {
            $sTimestampExpression = new CDbExpression('NOW()');
        }
        $aBehaviors['CTimestampBehavior'] = [
            'class'               => 'zii.behaviors.CTimestampBehavior',
            'createAttribute'     => $sCreateFieldName,
            'updateAttribute'     => $sUpdateFieldName,
            'timestampExpression' => $sTimestampExpression
        ];
        // Some tables might not exist/not be up to date during a database upgrade so in that case disconnect plugin events
        if (!Yii::app()->getConfig('Updating')) {
            $aBehaviors['PluginEventBehavior'] = [
                'class' => 'application.models.behaviors.PluginEventBehavior'
            ];
        }
        return $aBehaviors;
    }

    /**
     * Modified version that default to do the same as the original, but allows via a
     * third parameter to retrieve the result as array instead of active records. This
     * solves a joining problem. Usage via findAllAsArray method
     *
     * Performs the actual DB query and populates the AR objects with the query result.
     * This method is mainly internally used by other AR query methods.
     * @param CDbCriteria $criteria the query criteria
     * @param boolean $all whether to return all data
     * @param bool $asAR
     * @return mixed the AR objects populated with the query result
     * @since 1.1.7
     */
    protected function query($criteria, $all = false, $asAR = true)
    {
        if ($asAR === true) {
            return parent::query($criteria, $all);
        } else {
            $this->beforeFind();
            $this->applyScopes($criteria);
            if (!$all) {
                $criteria->limit = 1;
            }

            $command = $this->getCommandBuilder()->createFindCommand($this->getTableSchema(), $criteria);
            //For debug, this command will get you the generated sql:
            //echo $command->getText();

            return $all ? $command->queryAll() : $command->queryRow();
        }
    }

    /**
     * Finds all active records satisfying the specified condition but returns them as array
     *
     * See {@link find()} for detailed explanation about $condition and $params.
     * @param mixed $condition query condition or criteria.
     * @param array $params parameters to be bound to an SQL statement.
     * @return array list of active records satisfying the specified condition. An empty array is returned if none is found.
     */
    public function findAllAsArray($condition = '', $params = [])
    {
        Yii::trace(get_class($this) . '.findAll()', 'system.db.ar.CActiveRecord');
        $criteria = $this->getCommandBuilder()->createCriteria($condition, $params);
        return $this->query($criteria, true, false); //Notice the third parameter 'false'
    }


    /**
     * Return the max value for a field
     *
     * This is a convenience method, that uses the primary key of the model to
     * retrieve the highest value.
     *
     * @param string $field The field that contains the Id, when null primary key is used if it is a single field
     * @param boolean $forceRefresh Don't use value from static cache but always requery the database
     * @return false|int
     * @throws Exception
     */
    public function getMaxId($field = null, $forceRefresh = false)
    {
        static $maxIds = [];

        if (is_null($field)) {
            $primaryKey = $this->getMetaData()->tableSchema->primaryKey;
            if (is_string($primaryKey)) {
                $field = $primaryKey;
            } else {
                // Composite key, throw a warning to the programmer
                throw new Exception(sprintf('Table %s has a composite primary key, please explicitly state what field you need the max value for.', $this->tableName()));
            }
        }

        if ($forceRefresh || !array_key_exists($field, $maxIds)) {
            $maxId = $this->dbConnection->createCommand()
                ->select('MAX(' . $this->dbConnection->quoteColumnName($field) . ')')
                ->from($this->tableName())
                ->queryScalar();

            // Save so we can reuse in the same request
            $maxIds[$field] = $maxId;
        }

        return $maxIds[$field];
    }

    /**
     * Return the min value for a field
     *
     * This is a convenience method, that uses the primary key of the model to
     * retrieve the highest value.
     *
     * @param string $field The field that contains the Id, when null primary key is used if it is a single field
     * @param boolean $forceRefresh Don't use value from static cache but always requery the database
     * @return false|int
     * @throws Exception
     */
    public function getMinId($field = null, $forceRefresh = false)
    {
        static $minIds = [];

        if (is_null($field)) {
            $primaryKey = $this->getMetaData()->tableSchema->primaryKey;
            if (is_string($primaryKey)) {
                $field = $primaryKey;
            } else {
                // Composite key, throw a warning to the programmer
                throw new Exception(sprintf('Table %s has a composite primary key, please explicitly state what field you need the min value for.', $this->tableName()));
            }
        }

        if ($forceRefresh || !array_key_exists($field, $minIds)) {
            $minId = $this->dbConnection->createCommand()
                ->select('MIN(' . $this->dbConnection->quoteColumnName($field) . ')')
                ->from($this->tableName())
                ->queryScalar();

            // Save so we can reuse in the same request
            $minIds[$field] = $minId;
        }

        return $minIds[$field];
    }

    /**
     * @param array $attributes list of attribute values (indexed by attribute names) that the active records should match.
     * An attribute value can be an array which will be used to generate an IN condition.
     * @param string $condition query condition or criteria.
     * @param array $params parameters to be bound to an SQL statement.
     * @return integer number of rows affected by the execution.
     * @todo This should also be moved to the behavior at some point.
     * This method overrides the parent in order to raise PluginEvents for Bulk delete operations.
     *
     * Filter Criteria are wrapped into a CDBCriteria instance so we have a single instance responsible for holding the filter criteria
     * to be passed to the PluginEvent,
     * this also enables us to pass the fully configured CDBCriteria instead of the original Parameters.
     *
     * See {@link find()} for detailed explanation about $condition and $params.
     */
    public function deleteAllByAttributes($attributes, $condition = '', $params = [])
    {
        $builder = $this->getCommandBuilder();
        $table = $this->getTableSchema();
        $criteria = $builder->createColumnCriteria($table, $attributes, $condition, $params);
        $modelEventName = get_class($this);
        $eventParams = [];
        if (is_subclass_of($this, 'Dynamic')) {
            /** @scrutinizer ignore-call since we test if exist by subclass */
            $eventParams['dynamicId'] = $this->getDynamicId();
            $modelEventName = get_parent_class($this);
        }
        $this->dispatchPluginModelEvent('before' . $modelEventName . 'DeleteMany', $criteria, $eventParams);
        $this->dispatchPluginModelEvent('beforeModelDeleteMany', $criteria, $eventParams);
        return parent::deleteAllByAttributes([], $criteria, []);
    }

    /**
     * Updates records with the specified condition.
     * XSS filtering is enforced for attributes listed in model's $xssFilterAttributes property.
     * See {@link find()} for detailed explanation about $condition and $params.
     * Note, the attributes are not checked for safety and no validation is done.
     * @param array $attributes list of attributes (name=>$value) to be updated
     * @param mixed $condition query condition or criteria.
     * @param array $params parameters to be bound to an SQL statement.
     * @return integer the number of rows being updated
     */
    public function updateAll($attributes, $condition = '', $params = array())
    {
        if (!empty($this->xssFilterAttributes)) {
            $validator = new LSYii_Validators();
            if ($validator->xssfilter) {
                $attributeNames = array_keys($attributes);
                $attributesToFilter = array_intersect($attributeNames, $this->xssFilterAttributes);
                foreach ($attributesToFilter as $attribute) {
                    $attributes[$attribute] = $validator->xssFilter($attributes[$attribute]);
                }
            }
        }

        return parent::updateAll($attributes, $condition, $params);
    }

    /**
     * Overriding of Yii's findByAttributes method to provide encrypted attribute value search
     * @param array $attributes list of attribute values (indexed by attribute names) that the active record should match.
     * An attribute value can be an array which will be used to generate an IN condition.
     * @param mixed $condition query condition or criteria.
     * @param array $params parameters to be bound to an SQL statement.
     * @return static|null the record found. Null if none is found.
     */
    public function findByAttributes($attributes, $condition = '', $params = [])
    {
        $attributes = $this->encryptAttributeValues($attributes);
        return parent::findByAttributes($attributes, $condition, $params);
    }

    /**
     * Overriding of Yii's findAllByAttributes method to provide encrypted attribute value search
     * @param array $attributes list of attribute values (indexed by attribute names) that the active records should match.
     * An attribute value can be an array which will be used to generate an IN condition.
     * @param mixed $condition query condition or criteria.
     * @param array $params parameters to be bound to an SQL statement.
     * @return static[] the records found. An empty array is returned if none is found.
     */
    public function findAllByAttributes($attributes, $condition = '', $params = [])
    {
        $attributes = $this->encryptAttributeValues($attributes);
        return parent::findAllByAttributes($attributes, $condition, $params);
    }

    /**
     * @param int $iSurveyId
     * @param string $sClassName
     * @return array
     * TODO: Should be split into seperate functions in the appropiate model or helper class
     * TODO: Make an interface for records that support encryption.
     */
    public function getAllEncryptedAttributes($iSurveyId, $sClassName)
    {
        $aAttributes = [];
        if ($sClassName == 'ParticipantAttribute') {
            // participants attributes
            $aAttributes[] = 'value';
        } elseif ($sClassName == 'Participant') {
            // participants
            $aTokenAttributes = Participant::getParticipantsEncryptionOptions();
            if ($aTokenAttributes['enabled'] = 'Y') {
                foreach ($aTokenAttributes['columns'] as $attribute => $oColumn) {
                    if ($oColumn == 'Y') {
                        $aAttributes[] = $attribute;
                    }
                }
            }
        } elseif ($iSurveyId > 0 && ($sClassName == 'TokenDynamic' || $sClassName == 'Token_' . $iSurveyId || $sClassName == 'Token')) {
            //core token attributes
            $oSurvey = Survey::model()->findByPk($iSurveyId);
            $aTokenAttributes = $oSurvey->getTokenEncryptionOptions();
            if ($aTokenAttributes['enabled'] = 'Y') {
                foreach ($aTokenAttributes['columns'] as $attribute => $oColumn) {
                    if ($oColumn == 'Y') {
                        $aAttributes[] = $attribute;
                    }
                }
            }
            // custom token attributes
            $aCustomAttributes = $oSurvey->tokenAttributes;
            foreach ($aCustomAttributes as $attribute => $value) {
                if ($value['encrypted'] == 'Y') {
                    $aAttributes[] = $attribute;
                }
            }
        } elseif ($sClassName == 'SurveyDynamic' || $sClassName == 'Response_' . $iSurveyId) {
            // response attributes
            $aAttributes = Response::getEncryptedAttributes($iSurveyId);
        }

        return $aAttributes;
    }

    /**
     * Attribute values are encrypted ( if needed )to be used for searching purposes
     * @param array $attributes list of attribute values (indexed by attribute names) that the active records should match.
     * An attribute value can be an array which will be used to generate an IN condition.
     * @return array attributes array with encrypted atrribute values is returned
     */
    public function encryptAttributeValues($attributes = null, $bEncryptedOnly = false, $bReplaceValues = true)
    {
        // load sodium library
        $sodium = Yii::app()->sodium;

        if (method_exists($this, 'getSurveyId')) {
            $iSurveyId = $this->getSurveyId();
        } else {
            $iSurveyId = 0;
        }
        $class = get_class($this);
        $encryptedAttributes = $this->getAllEncryptedAttributes($iSurveyId, $class);
        foreach ($attributes as $key => $attribute) {
            if (in_array($key, $encryptedAttributes)) {
                if ($bReplaceValues) {
                    $attributes[$key] = $sodium->encrypt($attributes[$key]);
                }
            } else {
                if ($bEncryptedOnly) {
                    unset($attributes[$key]);
                }
            }
        }
        return $attributes;
    }

    /**
     * Decrypt values from database
     * @param string $sValueSingle String value which needs to be decrypted
     */
    public function decrypt($value = '')
    {
        // if $sValueSingle is provided, it would decrypt
        if (!empty($value)) {
            // load sodium library
            $sodium = Yii::app()->sodium;

            return $sodium->decrypt($value);
        } else {
            // decrypt attributes
            $this->decryptEncryptAttributes('decrypt');

            return $this;
        }
    }


    /**
     * Decrypt single value
     * @param string $value String value which needs to be decrypted
     * @return string the decrypted string
     */
    public static function decryptSingle($value = ''): string
    {
        // if $value is provided, it would decrypt
        if (!empty($value)) {
            // load sodium library
            $sodium = Yii::app()->sodium;
            return $sodium->decrypt($value);
        }
        return '';
    }

    /**
     * Decrypt single value
     * @param string $value String value which needs to be decrypted
     * @return string the decrypted string
     */
    public static function decryptSingleOld($value = ''): string
    {
        static $sodium = null;
        if (!isset($sodium)) {
            // load sodium library
            $sodium = Yii::app()->sodiumOld;
        }
        // if $value is provided, it would decrypt
        if (isset($value) && $value !== '') {
            try {
                return $sodium->decrypt($value);
            } catch (throwable $e) {
                // if decryption with oldDecrypt fails try it with new decryption
                try {
                    return LSActiveRecord::decryptSingle($value);
                } catch (throwable $e) {
                    // if decryption with new decryption fails just return the current value
                    // this should not happen
                    return $value;
                }
            }
        }
        return '';
    }


    /**
     * Enrypt single value
     * @param string $value String value which needs to be encrypted
     */
    public static function encryptSingle($value = '')
    {
        // if $value is provided, it would decrypt
        if (isset($value) && $value !== "") {
            // load sodium library
            $sodium = Yii::app()->sodium;
            return $sodium->encrypt($value);
        }
    }


    /**
     * Encrypt values
     */
    public function encrypt()
    {
        // encrypt attributes
        $this->decryptEncryptAttributes('encrypt');

        return $this;
    }


    /**
     * Encrypt values before saving to the database
     */
    public function encryptSave($runValidation = false)
    {
        // run validation on attribute values before encryption take place, it is impossible to validate encrypted values
        if ($runValidation) {
            if (!$this->validate()) {
                return false;
            }
        }

        // encrypt attributes
        $this->decryptEncryptAttributes('encrypt');
        // call save() method  without validation, validation is already done ( if needed )
        return $this->save(false);
    }

    /**
     * Encrypt/decrypt values
     */
    public function decryptEncryptAttributes($action = 'decrypt')
    {
        // load sodium library
        $sodium = Yii::app()->sodium;

        $class = get_class($this);
        // TODO: Use OOP polymorphism instead of switching on class names.
        if ($class === 'ParticipantAttribute') {
            $aParticipantAttributes = CHtml::listData(ParticipantAttributeName::model()->findAll(["select" => "attribute_id", "condition" => "encrypted = 'Y' and core_attribute <> 'Y'"]), 'attribute_id', '');
            if (array_key_exists($this->attribute_id, $aParticipantAttributes)) {
                $this->value = $sodium->$action($this->value);
            }
        } else {
            $attributes = $this->encryptAttributeValues($this->attributes, true, false);
            $LEM = LimeExpressionManager::singleton();
            $updatedValues = $LEM->getUpdatedValues();
            foreach ($attributes as $key => $attribute) {
                if ($action === 'decrypt' && array_key_exists($key, $updatedValues)) {
                    continue;
                }
                $this->$key = $sodium->$action($attribute);
            }
        }
    }

    /**
     * Function to show encryption symbol in gridview attribute header if value ois encrypted
     * @param int $surveyId
     * @param string $className
     * @param string $attributeName
     * @return string
     * @throws CException
     */
    public function setEncryptedAttributeLabel(int $surveyId, string $className, string $attributeName)
    {
        $encryptedAttributes = $this->getAllEncryptedAttributes($surveyId, $className);
        $encryptionNotice = gT("This field is encrypted and can only be searched by exact match. Please enter the exact value you are looking for.");
        if (isset($encryptedAttributes)) {
            if (in_array($attributeName, $encryptedAttributes)) {
                return ' <span  data-bs-toggle="tooltip" title="' . $encryptionNotice . '" class="ri-key-2-fill text-success"></span>';
            }
        }
    }
}