Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enh: Check User Birthday field #67

Closed
wants to merge 0 commits into from
Closed

Conversation

ArchBlood
Copy link
Contributor

@ArchBlood ArchBlood commented Feb 22, 2024

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 ArchBlood marked this pull request as draft February 22, 2024 07:16
@ArchBlood
Copy link
Contributor Author

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
Copy link
Contributor Author

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.

Copy link
Collaborator

@luke- luke- left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, these AI-generated summaries take more time to read than the PR itself. I would prefer a short compact sentence describing the purpose of the PR than AI blah blah blah :-)

In general, I don't know whether it makes sense to simply lock out users with an early/incorrect date of birth.

I'm currently not sure how we can best implement this check.

// Check if the user's age meets the minimum age requirement
if ($userAge < $minimumAge) {
// Perform logout if the age check fails
Yii::$app->user->logout();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am unsure whether we should simply log out here, e.g. a user could have entered an incorrect date of birth. We would then log them out without further notice.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could attempt a redirect or display a error message requesting to contact an Administrator for more information? The problem is the birthday field can always be manipulated when it comes to adding a date because the user must click the checkbox field which they see the age beforehand no?

// Check if the user and their profile property are not null before accessing them
if ($module->showAgeCheck() && $this->user !== null && $this->user->profile !== null) {
// Calculate the user's age
$birthday = $this->user->profile->birthday;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could move this code into a seperate private method?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can attempt do to this if required, would you want the call to also be made within this function through a static method or not?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think a static method will be necessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking closer, we'll also have to do something within this class as well else it doesn't really do anything for EVENT_BEFORE_VALIDATE as the current #67 (comment) throws an error calling for Profile::models which doesn't exist. 🤔

@luke- luke- marked this pull request as ready for review February 23, 2024 09:27
@luke- luke- marked this pull request as draft February 23, 2024 09:28
Copy link
Collaborator

@luke- luke- left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

@ArchBlood
Copy link
Contributor Author

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. 🤔

@ArchBlood
Copy link
Contributor Author

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
Copy link
Contributor Author

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-
Copy link
Collaborator

luke- commented Feb 23, 2024

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

@ArchBlood
Copy link
Contributor Author

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-
Copy link
Collaborator

luke- commented Feb 23, 2024

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

@ArchBlood
Copy link
Contributor Author

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-
Copy link
Collaborator

luke- commented Feb 23, 2024

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
Copy link
Contributor Author

ArchBlood commented Feb 23, 2024

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 #67 (comment) & #67 (comment) 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
Copy link
Contributor Author

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 #67 (comment).

<?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-
Copy link
Collaborator

luke- commented Feb 24, 2024

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
Copy link
Contributor Author

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-
Copy link
Collaborator

luke- commented Feb 24, 2024

image

@Semir1212 What do you think regarding the field hint?

@Semir1212
Copy link

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

@ArchBlood ArchBlood closed this Feb 28, 2024
@ArchBlood ArchBlood deleted the patch-1 branch February 28, 2024 16:05
@luke-
Copy link
Collaborator

luke- commented Feb 28, 2024

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

@ArchBlood ArchBlood restored the patch-1 branch February 28, 2024 16:21
@ArchBlood
Copy link
Contributor Author

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 ArchBlood reopened this Feb 28, 2024
@ArchBlood
Copy link
Contributor Author

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

@ArchBlood
Copy link
Contributor Author

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants