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/services/QuestionAttributeHelper.php
<?php

namespace LimeSurvey\Models\Services;

class QuestionAttributeHelper
{
    /**
     * Merges the 'base' attributes (ex: core question attributes) with the extended question attributes
     * (ex: question theme attributes). It also removes all attributes where extended attribute's inputType is
     * empty.
     * If an extended attribute's name cannot be determined, it's omitted.
     *
     * @param array $baseAttributes    the base set of attributes
     * @param array $extendedAttributes    the attributes to merge into the base set
     *
     * @return array the merged attributes
     */
    public function mergeQuestionAttributes($baseAttributes, $extendedAttributes)
    {
        $attributes = $baseAttributes;
        foreach ($extendedAttributes as $attribute) {
            // Omit the attribute if it has no name.
            // This shouldn't happen if sanitizeQuestionAttributes() is used.
            if (!isset($attribute['name'])) {
                continue;
            }

            $attributeName = $attribute['name'];
            $inputType = $attribute['inputtype'];
            // remove attribute if inputtype is empty
            if (empty($inputType)) {
                unset($attributes[$attributeName]);
            } else {
                $customAttribute = array_merge(
                    \QuestionAttribute::getDefaultSettings(),
                    $attribute
                );
                $attributes[$attributeName] = $customAttribute;
            }
        }
        return $attributes;
    }

    /**
     * Sanitizes an array of question attributes.
     * Current tasks:
     *  - makes sure that attributes have a name (removes them if name cannot be determined)
     *  - replaces empty arrays (generally resulting from empty xml nodes) with null.
     *
     * @param array $attributes the array of attributes to sanitize
     *
     * @return array<string,array> the array of sanitized attributes
     * @todo Pure function - move outside class?
     */
    public function sanitizeQuestionAttributes($attributes)
    {
        /** @var array<string,array> An array of sanitized question attributes */
        $sanitizedAttributes = [];
        foreach ($attributes as $key => $attribute) {
            // Make sure the attribute has a name.
            if (!is_numeric($key)) {
                $attribute['name'] = $key;
            } else {
                if (!isset($attribute['name'])) {
                    continue;
                }
            }

            // Replace empty arrays with null
            foreach ($attribute as $propertyName => $propertyValue) {
                if ($propertyValue === []) {
                    $attribute[$propertyName] = null;
                }
            }

            // Make sure "options" have the expected structure
            if (isset($attribute['options']['option']) && is_array($attribute['options']['option'])) {
                $attribute['options'] = $attribute['options']['option'];
            }

            $sanitizedAttributes[$attribute['name']] = $attribute;
        }
        return $sanitizedAttributes;
    }

    /**
     * Returns the received array of attributes filled with the values specified, taking into account the
     * 'i18n' property of the attributes.
     *
     * Both this and rewriteQuestionAttributeArray() are helper methods and accomplish quite similar tasks,
     * but the output is different: rewriteQuestionAttributeArray returns a name -> value array, while here
     * we return a complete definition map and the value as a piece of information mingled into it.
     *
     * @param array $attributes the attributes to be filled
     * @param array $attributeValues the values for the attributes
     * @param array $languages the languages to use for i18n attributes
     *
     * @return array the same source attributes with their corresponding values (when available)
     * @todo Pure function - move outside class?
     */
    public function fillAttributesWithValues($attributes, $attributeValues, $languages = [])
    {
        foreach ($attributes as $key => $attribute) {
            if ($attribute['i18n'] == false) {
                if (isset($attributeValues[$attribute['name']][''])) {
                    $attributes[$key]['value'] = $attributeValues[$attribute['name']][''];
                } else {
                    $attributes[$key]['value'] = $attribute['default'];
                }
                // Sanitize value in case it's saved as empty array
                if ($attributes[$key]['value'] === []) {
                    $attributes[$key]['value'] = null;
                }
            } else {
                foreach ($languages as $language) {
                    if (isset($attributeValues[$attribute['name']][$language])) {
                        $attributes[$key][$language]['value'] = $attributeValues[$attribute['name']][$language];
                    } else {
                        $attributes[$key][$language]['value'] = $attribute['default'];
                    }
                    // Sanitize value in case it's saved as empty array
                    if ($attributes[$key][$language]['value'] === []) {
                        $attributes[$key][$language]['value'] = null;
                    }
                }
            }
        }
        return $attributes;
    }

    /**
     * Receives an array of question attributes and groups them by category.
     * Used by advanced settings widget.
     *
     * @param array $attributes
     * @return array Grouped question attributes, with category as array key
     * @todo Pure function - move outside class?
     */
    public function groupAttributesByCategory($attributes)
    {
        $attributesByCategory = [];
        foreach ($attributes as $attribute) {
            $attributesByCategory[$attribute['category']][] = $attribute;
        }
        return $attributesByCategory;
    }

    /**
     * Filters an array of question attribute definitions by question type
     *
     * @param array $attributes    array of question attribute definitions to filter
     * @param string $questionType the question type that the attributes should apply to
     *
     * @return array    an array containing only the question attributes that match the specified question type
     */
    public function filterAttributesByQuestionType($attributes, $questionType)
    {
        $questionTypeAttributes = array_filter($attributes, function ($attribute) use ($questionType) {
            return $this->attributeAppliesToQuestionType($attribute, $questionType);
        });

        return $questionTypeAttributes;
    }

    /**
     * Check if question attribute applies to a specific question type
     *
     * @param array $attribute     question attribute definition
     * @param string $questionType the question type that the attribute should apply to
     *
     * @return bool     returns true if the question attribute applies to the specified question type
     */
    public function attributeAppliesToQuestionType($attribute, $questionType)
    {
        return isset($attribute['types']) && stripos((string) $attribute['types'], $questionType) !== false;
    }

    /**
     * Makes sure all the question attributes in an array have a category. If an attribute's
     * category is missing, it's filled with the specified category name.
     *
     * @param array $attributes    array of question attribute definitions
     * @param string $sCategoryName the category name to use if an attribute doesn't have one
     *
     * @return array    returns the array attributes with Category field complete
     */
    public function fillMissingCategory($attributes, $categoryName)
    {
        foreach ($attributes as &$attribute) {
            if (empty($attribute['category'])) {
                $attribute['category'] = $categoryName;
            }
        }
        return $attributes;
    }

    /**
     * This function returns an array of the attributes for the particular question
     * including their values set in the database
     *
     * @param \Question $question  The question object
     * @param string|null $language If you give a language then only the attributes for that language are returned
     * @param string|null $questionThemeOverride   Name of the question theme to use instead of the question's current theme
     * @param boolean $advancedOnly If set to true, only the advanced attributes will be returned
     * @return array
     */
    public function getQuestionAttributesWithValues($question, $language = null, $questionThemeOverride = null, $advancedOnly = false)
    {
        $questionAttributeFetcher = new \LimeSurvey\Models\Services\QuestionAttributeFetcher();
        $questionAttributeFetcher->setQuestion($question);
        $questionAttributeFetcher->setAdvancedOnly($advancedOnly);
        if (!empty($questionThemeOverride)) {
            $questionAttributeFetcher->setTheme($questionThemeOverride);
        }

        $questionAttributeDefinitions = $questionAttributeFetcher->fetch();
        $questionAttributesWithValues = $questionAttributeFetcher->populateValues($questionAttributeDefinitions, $language);

        return $questionAttributesWithValues;
    }

    /**
     * Comparison function for sorting question attributes by category with uasort().
     *
     * @param array<string,mixed> $a    First question attribute to compare
     * @param array<string,mixed> $b    Second question attribute to compare
     * @return int
     * @todo No state used, so no OOP needed, move to function at some point.
     */
    protected function categorySort($a, $b)
    {
        $categoryOrders = $this->getCategoryOrders();
        $orderA = $categoryOrders[$a['category']] ?? PHP_INT_MAX;
        $orderB = $categoryOrders[$b['category']] ?? PHP_INT_MAX;
        if ($orderA == $orderB) {
            $result = strnatcasecmp((string) $a['category'], (string) $b['category']);
            if ($result == 0) {
                $result = $a['sortorder'] - $b['sortorder'];
            }
        } else {
            $result = $orderA - $orderB;
        }
        return $result;
    }

    /**
     * Returns the array of categories with their assigned order.
     * The array doesn't contain all the posible categories, only those with an order assigned.
     *
     * @return array<string,int>
     */
    public function getCategoryOrders()
    {
        $orders = [
            'Logic' => 1,
            'Display' => 2,
            'Input' => 3,
            'Other' => 4,
            'Timer' => 5,
            'Statistics' => 6,
        ];
        return $orders;
    }

    /**
     * Sorts an array of question attributes by category.
     * Sorting is based on a predefined list of orders (see QuestionAtributeHelper::getCategoryOrders()).
     * Categories without a predefined order are considered less relevant.
     * Categories with the same order are sorted alphabetically.
     *
     * @param array<string,array> $attributes
     * @return array<string,array>
     */
    public function sortAttributesByCategory($attributes)
    {
        $attributesCopy = $attributes;
        uasort($attributesCopy, [$this, 'categorySort']);
        return $attributesCopy;
    }

    /**
     * Returns the user's default values for the specified question type
     * @param string $questionType
     * @return array<string,mixed>
     */
    public function getUserDefaultsForQuestionType($questionType)
    {
        $defaultValues = [];
        $userDefaultQuestionAttributes = \SettingsUser::getUserSettingValue('question_default_values_' . $questionType);
        if ($userDefaultQuestionAttributes !== null) {
            $defaultValuesByCategory = json_decode((string) $userDefaultQuestionAttributes, true);
            foreach ($defaultValuesByCategory as $attributes) {
                foreach ($attributes as $attribute => $value) {
                    if (!is_array($value)) {
                        $value = ['' => $value];
                    }
                    $defaultValues[$attribute] = $value;
                }
            }
        }
        return $defaultValues;
    }
}