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 #89

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
Open
11 changes: 11 additions & 0 deletions Events.php
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,17 @@ public static function onAccountSettingsMenuInit($event)
}
}

public static function onBeforeValidate($event)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@luke- @yurabakhtin your thoughts on using the following?

How this version works is if the checkbox Show age verification and the Minimum age field aren't set it will uncheck the required checkboxes in the Birthday field otherwise when the checkbox and field in the Legal module are set it enables these requirements. If this solution isn't like then we'd have to find a way to have a conditional migration which currently I don't have the time to build.

public static function onBeforeValidate($event)
{
    $registrationForm = $event->sender;
    $minimumAge = Yii::$app->getModule('legal')->getMinimumAge();

    // Find the ProfileField for 'birthday'
    $profileField = \humhub\modules\user\models\ProfileField::findOne(['internal_name' => 'birthday']);

    if ($profileField !== null) {
        // Handle when minimum age is set and valid
        if ($minimumAge > 0) {
            // If minimum age is set, force "Required" and "Show at registration" checkboxes
            $profileField->required = 1;
            $profileField->show_at_registration = 1;

            // Validate the minimum age for the birthday field
            $ageValidator = new validators\AgeValidator(['minimumAge' => $minimumAge]);
            $ageValidator->validateAttribute($registrationForm, 'birthday');
        } 
        
        // Handle when minimum age is 0 or negative
        if ($minimumAge <= 0) {
            // If minimum age is 0 or negative, uncheck the "Required" and "Show at registration" checkboxes
            $profileField->required = 0;
            $profileField->show_at_registration = 0;
        }

        // Save the changes to the ProfileField
        if (!$profileField->save(false)) {
            Yii::error('Failed to update profile field settings for birthday.', 'legal');
        }
    }
}

Copy link
Contributor

Choose a reason for hiding this comment

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

@ArchBlood I agree that before apply the AgeValidator we should check the profile field birthday exists in DB.

But I don't agree that we should update the profile field birthday on the event Profile::EVENT_BEFORE_VALIDATE. I mean it is not a correct place for doing such changes in the profile field, because the event is called when any user updates own profile.

If we really need to do the changes then it would be better to do this at the time when the legal module form is updated, i.e. when the settings "Show age verification X" and "Minimum age" are updated.

But I am not sure that the Legal module should do it, because it looks like a silent updating that may be unexpected for admin. If the updating will be noticed somehow for admin with some warning message, probably under the settings on the legal module config form.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@yurabakhtin I'm not sure how we should approach this to be honest that is why I suggested the above option, if you have any ideas on the approach we should take I'm all eyes and ears, otherwise I'm left scratching my head on this one, as without the two checkboxes within the Birthday field being checked this P/R won't work and we'd have to start from scratch.

@luke- any ideas on this topic?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Hmm, I have no idea.

But couldn't we just inject an AgeValidator when the registration is displayed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm, I have no idea.

But couldn't we just inject an AgeValidator when the registration is displayed?

I'm not exactly sure here, as I'm reviewing the code EVENT_BEFORE_VALIDATE doesn't allow for this, or not to the extent that we need it to or want it to. 🤔

https://github.com/yiisoft/yii2/blob/3c75ff1043cdfc3c0c78ad8a4b477b5894223a5a/framework/base/Model.php#L375-L382

Maybe what we should think about using is EVENT_BEFORE_INSERT? But again this wouldn't really match up with a validation class as far as I'm seeing it.
https://github.com/yiisoft/yii2/blob/3c75ff1043cdfc3c0c78ad8a4b477b5894223a5a/framework/db/BaseActiveRecord.php#L957-L980

Another option would be updating HumHub's ActiveRecord class with a custom validation event, I already see a EVENT_APPEND_RULES being used.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In this way we'd do as following;

Events Example

    public static function onAppendRegistrationRules($event)
    {
        /** @var ActiveRecord $model */
        $model = $event->sender;
        
        $minimumAge = Yii::$app->getModule('legal')->getMinimumAge();

        if ($minimumAge > 0) {
            $model->addRule('birthday', AgeValidator::class, ['minimumAge' => $minimumAge]);
        }
    }

config.php Example

'events' => [
    [
        'class' => Registration::class,
        'event' => ActiveRecord::EVENT_APPEND_RULES,
        'callback' => [Events::class, 'onAppendRegistrationRules']
    ],
    // ... other event handlers ...
],

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ok, seems there is no way to $model->addRule(). So the current approach is ok.

Is there anything not working as expected? Maybe we can make this validation optional?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Other than what @yurabakhtin has pointed out I see no issues, as for optional currently it should be only triggered when Yii::$app->getModule('legal')->getMinimumAge() is both set and enabled unless I am misunderstanding and you wish for an additional option?

{
$registrationForm = $event->sender;
$minimumAge = Yii::$app->getModule('legal')->getMinimumAge();

if ($minimumAge > 0) {
$ageValidator = new validators\AgeValidator(['minimumAge' => $minimumAge]);
$ageValidator->validateAttribute($registrationForm, 'birthday');
}
}

/**
* Callback on daily cron job run
*/
Expand Down
2 changes: 2 additions & 0 deletions config.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use humhub\modules\content\widgets\richtext\ProsemirrorRichText;
use humhub\modules\user\models\forms\Registration;
use humhub\modules\user\widgets\AccountSettingsMenu;
use humhub\modules\user\models\Profile;
use humhub\widgets\FooterMenu;
use humhub\widgets\LayoutAddons;

Expand All @@ -30,5 +31,6 @@
['class' => ProsemirrorRichText::class, 'event' => ProsemirrorRichText::EVENT_AFTER_RUN, 'callback' => ['humhub\modules\legal\Events', 'onAfterRunRichText']],
['class' => AccountSettingsMenu::class, 'event' => AccountSettingsMenu::EVENT_INIT, 'callback' => ['humhub\modules\legal\Events', 'onAccountSettingsMenuInit']],
['class' => CronController::class, 'event' => CronController::EVENT_ON_DAILY_RUN, 'callback' => ['humhub\modules\legal\Events', 'onCronDailyRun']],
['class' => Profile::class, 'event' => Profile::EVENT_BEFORE_VALIDATE, 'callback' => ['humhub\modules\legal\Events', 'onBeforeValidate']],
],
];
1 change: 1 addition & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Changelog
1.4.3 (Unreleased)
--------------------------
- Fix #85: Fix downloading of large user export data file
- Enh: Check User Birthday field
- Enh #90: Use PHP CS Fixer

1.4.2 (September 13, 2024)
Expand Down
44 changes: 44 additions & 0 deletions tests/codeception/acceptance/LegalCest.php
Original file line number Diff line number Diff line change
Expand Up @@ -136,4 +136,48 @@ public function testAgeVerification(AcceptanceTester $I)
$I->amUser1(true);
$I->dontSee($title, '.panel-heading');
}

public function testAgeValidation(AcceptanceTester $I)
{
$I->wantTo('test age validation during registration and profile update');
$minimumAge = 18;

$I->amAdmin();
$I->amGoingTo('enable age verification');
$I->enableAgeVerification($minimumAge);

// Test registration with valid age
$I->amGoingTo('test registration with valid age');
$I->amOnRoute('/user/registration');
$I->fillField('Registration[username]', 'validAgeUser');
$I->fillField('Registration[email]', 'validage@example.com');
$I->fillField('Registration[password]', 'ValidPassword123');
$I->fillField('Registration[birthday]', date('Y-m-d', strtotime("-{$minimumAge} years -1 day")));
$I->click('Register');
$I->dontSee('You must be at least ' . $minimumAge . ' years old.');

// Test registration with invalid age
$I->amGoingTo('test registration with invalid age');
$I->amOnRoute('/user/registration');
$I->fillField('Registration[username]', 'invalidAgeUser');
$I->fillField('Registration[email]', 'invalidage@example.com');
$I->fillField('Registration[password]', 'InvalidPassword123');
$I->fillField('Registration[birthday]', date('Y-m-d', strtotime("-{$minimumAge} years +1 day")));
$I->click('Register');
$I->see('You must be at least ' . $minimumAge . ' years old.');

// Test profile update with invalid age
$I->amGoingTo('test profile update with invalid age');
$I->amUser1(true);
$I->amOnRoute('/user/account/edit');
$I->fillField('Profile[birthday]', date('Y-m-d', strtotime("-{$minimumAge} years +1 day")));
$I->click('Save');
$I->see('You must be at least ' . $minimumAge . ' years old.');

// Test profile update with valid age
$I->amGoingTo('test profile update with valid age');
$I->fillField('Profile[birthday]', date('Y-m-d', strtotime("-{$minimumAge} years -1 day")));
$I->click('Save');
$I->dontSee('You must be at least ' . $minimumAge . ' years old.');
}
}
57 changes: 57 additions & 0 deletions validators/AgeValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

namespace humhub\modules\legal\validators;

use DateTime;
use Yii;
use yii\validators\Validator;
use humhub\modules\user\models\User;

/**
* AgeValidator validates that the given value represents an age greater than or equal to a specified minimum age.
*/
class AgeValidator extends Validator
{
/**
* @var int The minimum age required
*/
public $minimumAge;

/**
* @inheritdoc
*/
public function init()
{
parent::init();
if ($this->minimumAge === null) {
$this->minimumAge = 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)
{
$value = $model->$attribute;
if (!$value instanceof DateTime) {
try {
$value = new DateTime($value);
} catch (\Exception $e) {
$this->addError($model, $attribute, Yii::t('LegalModule.base', 'Invalid date format.'));
return;
}
}

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

if ($age < $this->minimumAge) {
$message = Yii::t('LegalModule.base', 'You must be at least {age} years old.', ['age' => $this->minimumAge]);
$this->addError($model, $attribute, $message);
}
}
}