humhub / legal

3 stars 8 forks source link

Enh: Check User Birthday field #67

Closed ArchBlood closed 2 months ago

ArchBlood commented 9 months ago

Here's what this P/R provides;

  1. Null Check: Before performing any operations, we ensure that the $user object and its profile property are not null. This prevents errors when trying to access properties of null objects, which could occur if the user is not logged in or if their profile information is missing.

  2. Age Calculation: If the user object and its profile are not null, we proceed with calculating the user's age. We retrieve the user's birthdate from their profile and calculate their age based on the current year and their birth year.

  3. Minimum Age Comparison: We compare the user's age with the minimum age requirement specified by the $module->getMinimumAge() method. If the user's age is less than the minimum age requirement, this indicates that they are underage.

  4. Logout: If the user's age is less than the minimum age requirement, we log the user out. This action ensures that underage users are not allowed to continue using the system. Logging the user out prevents them from accessing restricted content or performing actions that require them to be of legal age.

  5. Age Confirmation Check: If the user's age meets the minimum age requirement, we proceed with the regular age confirmation check. This involves checking whether the user has accepted the age confirmation, which is typically done through the checkbox mechanism during registration or account setup.

  6. Return Value: The method returns true if the user's age meets the minimum age requirement and they have not yet confirmed their age. This indicates that the age confirmation step should be displayed to the user. If the user's age is below the minimum age requirement, the method logs them out and returns false, preventing further access to the system.

In summary, this updated showAgeCheck() method ensures that underage users are logged out automatically and prevents them from accessing restricted content or performing actions that require them to be of legal age.

ArchBlood commented 9 months ago

Example

Someone enters their birth year as 2015 then their age is 8 and then they're marked as underage, which then performs the logout action. Screenshot_1

When said user tries logging back in, they're force logged out again.

ArchBlood commented 9 months ago

What I've currently not found a way around for is if the birthday field isn't set to required and at registration, also currently when creating an account the logout action does not trigger if both of these have a checkmark during finalization of the registration and instead allows for the login action to continue, so this will need fixed before it can be merged.

ArchBlood commented 9 months ago

Hmm, could maybe the legal module inject an AgeValidator for the Birthday field in the profile and make sure that no too young users are created?

Wouldn't that also work if the birthday field is activated in ShowOnRegistration?

I could look into this, although I'm not much of a fan of injections, because if another module were to also be created that injects into the field then there would be a conflict between both injections, so if we were to do this then we'd have to tread carefully doing so. :thinking:

ArchBlood commented 9 months ago

Thinking about it, it would be much easier to implement if there were a core FieldValidator class that we'd be able to extend to do this.

ArchBlood commented 9 months ago

Here's an example for the requested AgeValidator class;

<?php

namespace humhub\modules\legal\validators;

use DateTime;
use Yii;
use yii\base\Model;
use yii\validators\Validator;

/**
 * AgeValidator validates that the given value represents an age greater than or equal to a specified minimum age.
 */
class AgeValidator extends Validator
{
    /**
     * Initializes the validator.
     */
    public function init()
    {
        parent::init();
        if ($this->message === null) {
            $this->message = Yii::t('LegalModule.base', 'You must be at least {age} years old.', ['age' => Yii::$app->getModule('legal')->getMinimumAge()]);
        }
    }

    /**
     * Validates the age of the user based on the given attribute value.
     *
     * @param \yii\base\Model $model the data model being validated
     * @param string $attribute the name of the attribute to be validated
     */
    public function validateAttribute($model, $attribute)
    {
        // Ensure we are validating the birthday attribute during user registration
        if ($model instanceof \humhub\modules\user\models\forms\Registration) {
            $birthday = $this->user->profile->birthday;

            if ($birthday !== null && $birthday != '' && $birthday->format('Y') != '0000') {
                $minimumAge = Yii::$app->getModule('legal')->getMinimumAge();
                $minimumAgeDate = new DateTime("-{$minimumAge} years");
                $today = new DateTime();
                $age = $today->diff($birthday)->y;

                if ($age < $minimumAge) {
                    $this->addError($model, $attribute, $this->message, ['age' => $minimumAge]);
                }
            }
        }
    }

    /**
     * Attaches the validator to the Birthday field of the model.
     *
     * @param \yii\base\Model $model the model to attach the validator to
     */
    public static function attachToBirthday($model)
    {
        $validator = new self();
        $validator->validateAttribute($model, 'birthday');
    }
}

Please make any necessary modifications if needed.

luke- commented 9 months ago

Couldn't we use the BEFORE_VALIDATE event of the Profile model and do this additional validation?

ArchBlood commented 9 months ago

Couldn't we use the BEFORE_VALIDATE event of the Profile model and do this additional validation?

Could you provide some documentation for this?

luke- commented 9 months ago

Here is an example: https://docs.humhub.org/docs/develop/modules-event-handler/#model-validation

ArchBlood commented 9 months ago

Here is an example: https://docs.humhub.org/docs/develop/modules-event-handler/#model-validation

So knowing this, maybe something along the lines of this?

Events.php Examples

/**
 * Performs age validation before the registration form is validated.
 *
 * @param \yii\base\Event $event the event parameter
 * @throws \Exception if an error occurs while performing age validation
 */
public static function onBeforeValidate($event)
{
    $registrationForm = $event->sender;
    $profile = $registrationForm->models['Profile'];

    // Check if $profile is loaded and the birthday attribute is set
    if ($profile !== null && isset($profile->birthday)) {
        // Option 1: Access birthday directly from profile
        $birthday = $profile->birthday;
    } else {
        // Option 2: Access birthday through user->profile
        $birthday = Yii::$app->user->profile->birthday;
    }

    // Perform age validation using $birthday
    $minimumAge = Yii::$app->getModule('legal')->getMinimumAge();

    // Check if birthday is set and has a year
    if ($birthday !== null && $birthday != '' && $birthday->format('Y') != '0000') {
        $minimumAgeDate = new DateTime("-{$minimumAge} years");
        $today = new DateTime();
        $age = $today->diff($birthday)->y;

        if ($age < $minimumAge) {
            // Add an error if age is less than the minimum age requirement
            $registrationForm->addError('profile', Yii::t('LegalModule.base', 'You must be at least {age} years old.', ['age' => $minimumAge]));
        }
    }
}

config.php Example

<?php /** @noinspection MissedFieldInspection */

use humhub\components\Controller;
use humhub\modules\content\widgets\richtext\ProsemirrorRichText;
use humhub\modules\user\models\forms\Registration;
use humhub\widgets\FooterMenu;
use humhub\widgets\LayoutAddons;
use humhub\modules\user\models\Profile;

/**
 * @link https://www.humhub.org/
 * @copyright Copyright (c) 2018 HumHub GmbH & Co. KG
 * @license https://www.humhub.com/licences
 */

return [
    'id' => 'legal',
    'class' => 'humhub\modules\legal\Module',
    'namespace' => 'humhub\modules\legal',
    'events' => [
        ['class' => FooterMenu::class, 'event' => FooterMenu::EVENT_INIT, 'callback' => ['humhub\modules\legal\Events', 'onFooterMenuInit']],
        ['class' => LayoutAddons::class, 'event' => LayoutAddons::EVENT_INIT, 'callback' => ['humhub\modules\legal\Events', 'onLayoutAddonInit']],
        ['class' => Registration::class, 'event' => Registration::EVENT_BEFORE_RENDER, 'callback' => ['humhub\modules\legal\Events', 'onRegistrationFormRender']],
        ['class' => Registration::class, 'event' => Registration::EVENT_AFTER_INIT, 'callback' => ['humhub\modules\legal\Events', 'onRegistrationFormInit']],
        ['class' => Registration::class, 'event' => Registration::EVENT_AFTER_REGISTRATION, 'callback' => ['humhub\modules\legal\Events', 'onRegistrationAfterRegistration']],
        ['class' => Controller::class, 'event' => Controller::EVENT_BEFORE_ACTION, 'callback' => ['humhub\modules\legal\Events', 'onBeforeControllerAction']],
        ['class' => ProsemirrorRichText::class, 'event' => ProsemirrorRichText::EVENT_AFTER_RUN, 'callback' => ['humhub\modules\legal\Events', 'onAfterRunRichText']],
        ['class' => Profile::class, 'event' => Profile::EVENT_BEFORE_VALIDATE, 'callback' => ['humhub\modules\legal\Events', 'onBeforeValidate']],
    ]
];
luke- commented 9 months ago

Haven't had time for a review yet. But it looks good.

It might be better to put the validation in a separate validator class + an option to activate this validator.

ArchBlood commented 9 months ago

Haven't had time for a review yet. But it looks good.

It might be better to put the validation in a separate validator class + an option to activate this validator.

Just to confirm, you want the EVENT_BEFORE_VALIDTION and a validator class;

It should use the validator class for the main parts of the code and be called in onBeforeValidate() instead of using the check within it? I think it would be a good idea and make it more maintainable this way, and I wouldn't have to change much of the provided code, so in theory I can use both https://github.com/humhub-contrib/legal/pull/67#issuecomment-1961141972 & https://github.com/humhub-contrib/legal/pull/67#issuecomment-1961249324 with minor refactoring.

Would you also want an option to enable/disable the validator, similar to the age requirement option from the configuration settings?

ArchBlood commented 9 months ago

I'm not sure if I'm a fan of this method, but it should be better than just logging out the user;

AgeValidator.php Example

Here we validate the user's age, if they don't meet the age requirement then the account is disabled instead of doing a forced logout action.

<?php

namespace humhub\modules\legal\validators;

use DateTime;
use Yii;
use yii\validators\Validator;

/**
 * AgeValidator validates that the given value represents an age greater than or equal to a specified minimum age.
 */
class AgeValidator extends Validator
{
    /**
     * Validates the age of the user based on the given attribute value.
     *
     * @param \yii\base\Model $model the data model being validated
     * @param string $attribute the name of the attribute to be validated
     */
    public function validateAttribute($model, $attribute)
    {
        $value = $model->$attribute;

        // Ensure the value represents a valid date
        if ($value instanceof DateTime) {
            // Get minimum age from the module
            $minimumAge = Yii::$app->getModule('legal')->getMinimumAge();

            // Calculate the age
            $today = new DateTime();
            $age = $today->diff($value)->y;

            // Check if the age meets the minimum requirement
            if ($age < $minimumAge) {
                // Set error message
                $message = Yii::t('LegalModule.base', 'You must be at least {age} years old.', ['age' => $minimumAge]);

                // Add error to the model attribute
                $model->addError($attribute, $message);

                // Check if the user account should be disabled
                if ($minimumAge > 0) {
                    $user = $model->user;
                    $user->status = User::STATUS_DISABLED;
                    $user->save();
                }
            }
        }
    }
}

Events.php Example

Here we make the appropriate calls to the validator within onBeforeValidate()

public static function onBeforeValidate($event)
{
    // Get the registration form
    $registrationForm = $event->sender;

    // Check for minimum
    $minimumAge = Yii::$app->getModule('legal')->getMinimumAge();
    if ($minimumAge > 0) {
        // Validate the user's age
        $ageValidator = new AgeValidator();
        $ageValidator->validateAttribute($registrationForm, 'birthday');
    }
}

config.php Example

Nothing changed from https://github.com/humhub-contrib/legal/pull/67#issuecomment-1961249324.

<?php /** @noinspection MissedFieldInspection */

use humhub\components\Controller;
use humhub\modules\content\widgets\richtext\ProsemirrorRichText;
use humhub\modules\user\models\forms\Registration;
use humhub\widgets\FooterMenu;
use humhub\widgets\LayoutAddons;
use humhub\modules\user\models\Profile;

/**
 * @link https://www.humhub.org/
 * @copyright Copyright (c) 2018 HumHub GmbH & Co. KG
 * @license https://www.humhub.com/licences
 */

return [
    'id' => 'legal',
    'class' => 'humhub\modules\legal\Module',
    'namespace' => 'humhub\modules\legal',
    'events' => [
        ['class' => FooterMenu::class, 'event' => FooterMenu::EVENT_INIT, 'callback' => ['humhub\modules\legal\Events', 'onFooterMenuInit']],
        ['class' => LayoutAddons::class, 'event' => LayoutAddons::EVENT_INIT, 'callback' => ['humhub\modules\legal\Events', 'onLayoutAddonInit']],
        ['class' => Registration::class, 'event' => Registration::EVENT_BEFORE_RENDER, 'callback' => ['humhub\modules\legal\Events', 'onRegistrationFormRender']],
        ['class' => Registration::class, 'event' => Registration::EVENT_AFTER_INIT, 'callback' => ['humhub\modules\legal\Events', 'onRegistrationFormInit']],
        ['class' => Registration::class, 'event' => Registration::EVENT_AFTER_REGISTRATION, 'callback' => ['humhub\modules\legal\Events', 'onRegistrationAfterRegistration']],
        ['class' => Controller::class, 'event' => Controller::EVENT_BEFORE_ACTION, 'callback' => ['humhub\modules\legal\Events', 'onBeforeControllerAction']],
        ['class' => ProsemirrorRichText::class, 'event' => ProsemirrorRichText::EVENT_AFTER_RUN, 'callback' => ['humhub\modules\legal\Events', 'onAfterRunRichText']],
        ['class' => Profile::class, 'event' => Profile::EVENT_BEFORE_VALIDATE, 'callback' => ['humhub\modules\legal\Events', 'onBeforeValidate']],
    ]
];
luke- commented 9 months ago

I like this approach.

Question would be, do we need an option to enable this behavior? And maybe unit tests for the AgeValidator would be good.

ArchBlood commented 9 months ago

I like this approach.

Question would be, do we need an option to enable this behavior? And maybe unit tests for the AgeValidator would be good.

Well, to answer the question about an option for enabling the validator, I don't think we'd need to due to the minimum age already having this. As for the unit tests, I don't have much experience with this so I believe this would have to be someone other than me doing it. 🤔

luke- commented 9 months ago

image

@Semir1212 What do you think regarding the field hint?

Semir1212 commented 9 months ago

@luke- "Show checkbox" is not clear enough. Hint is fine.

luke- commented 9 months ago

Why are you closing the PR. Shouldn't we enforce a minimum age in the profile field?

ArchBlood commented 9 months ago

Why are you closing the PR. Shouldn't we enforce a minimum age in the profile field?

For some reason when renaming a branch it closes and deletes the original branch.

ArchBlood commented 8 months ago

@luke- can a branch be made for this P/R?

ArchBlood commented 2 months ago

I've created a new P/R for a proof-of-concept for this functionality, please use #89 for new reference.