Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
65.12% covered (warning)
65.12%
56 / 86
60.00% covered (warning)
60.00%
3 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
CookieConsentsTable
65.12% covered (warning)
65.12%
56 / 86
60.00% covered (warning)
60.00%
3 / 5
20.17
0.00% covered (danger)
0.00%
0 / 1
 initialize
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 validationDefault
100.00% covered (success)
100.00%
30 / 30
100.00% covered (success)
100.00%
1 / 1
1
 buildRules
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 createConsentCookie
94.12% covered (success)
94.12%
16 / 17
0.00% covered (danger)
0.00%
0 / 1
3.00
 getLatestConsent
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
56
1<?php
2declare(strict_types=1);
3
4namespace App\Model\Table;
5
6use App\Model\Entity\CookieConsent;
7use Cake\Http\Cookie\Cookie;
8use Cake\ORM\RulesChecker;
9use Cake\ORM\Table;
10use Cake\Validation\Validator;
11use DateTime;
12
13/**
14 * CookieConsents Model
15 *
16 * @property \App\Model\Table\UsersTable&\Cake\ORM\Association\BelongsTo $Users
17 * @method \App\Model\Entity\CookieConsent newEmptyEntity()
18 * @method \App\Model\Entity\CookieConsent newEntity(array $data, array $options = [])
19 * @method array<\App\Model\Entity\CookieConsent> newEntities(array $data, array $options = [])
20 * @method \App\Model\Entity\CookieConsent get(mixed $primaryKey, array|string $finder = 'all', \Psr\SimpleCache\CacheInterface|string|null $cache = null, \Closure|string|null $cacheKey = null, mixed ...$args)
21 * @method \App\Model\Entity\CookieConsent findOrCreate($search, ?callable $callback = null, array $options = [])
22 * @method \App\Model\Entity\CookieConsent patchEntity(\Cake\Datasource\EntityInterface $entity, array $data, array $options = [])
23 * @method array<\App\Model\Entity\CookieConsent> patchEntities(iterable $entities, array $data, array $options = [])
24 * @method \App\Model\Entity\CookieConsent|false save(\Cake\Datasource\EntityInterface $entity, array $options = [])
25 * @method \App\Model\Entity\CookieConsent saveOrFail(\Cake\Datasource\EntityInterface $entity, array $options = [])
26 * @method iterable<\App\Model\Entity\CookieConsent>|\Cake\Datasource\ResultSetInterface<\App\Model\Entity\CookieConsent>|false saveMany(iterable $entities, array $options = [])
27 * @method iterable<\App\Model\Entity\CookieConsent>|\Cake\Datasource\ResultSetInterface<\App\Model\Entity\CookieConsent> saveManyOrFail(iterable $entities, array $options = [])
28 * @method iterable<\App\Model\Entity\CookieConsent>|\Cake\Datasource\ResultSetInterface<\App\Model\Entity\CookieConsent>|false deleteMany(iterable $entities, array $options = [])
29 * @method iterable<\App\Model\Entity\CookieConsent>|\Cake\Datasource\ResultSetInterface<\App\Model\Entity\CookieConsent> deleteManyOrFail(iterable $entities, array $options = [])
30 * @mixin \Cake\ORM\Behavior\TimestampBehavior
31 */
32class CookieConsentsTable extends Table
33{
34    /**
35     * Initialize method
36     *
37     * @param array<string, mixed> $config The configuration for the Table.
38     * @return void
39     */
40    public function initialize(array $config): void
41    {
42        parent::initialize($config);
43
44        $this->setTable('cookie_consents');
45        $this->setDisplayField('ip_address');
46        $this->setPrimaryKey('id');
47
48        $this->addBehavior('Timestamp');
49
50        $this->belongsTo('Users', [
51            'foreignKey' => 'user_id',
52        ]);
53    }
54
55    /**
56     * Default validation rules.
57     *
58     * @param \Cake\Validation\Validator $validator Validator instance.
59     * @return \Cake\Validation\Validator
60     */
61    public function validationDefault(Validator $validator): Validator
62    {
63        $validator
64            ->uuid('user_id')
65            ->allowEmptyString('user_id');
66
67        $validator
68            ->scalar('session_id')
69            ->maxLength('session_id', 255)
70            ->allowEmptyString('session_id');
71
72        $validator
73            ->boolean('analytics_consent')
74            ->notEmptyString('analytics_consent');
75
76        $validator
77            ->boolean('functional_consent')
78            ->notEmptyString('functional_consent');
79
80        $validator
81            ->boolean('marketing_consent')
82            ->notEmptyString('marketing_consent');
83
84        $validator
85            ->boolean('essential_consent')
86            ->notEmptyString('essential_consent');
87
88        $validator
89            ->scalar('ip_address')
90            ->maxLength('ip_address', 45)
91            ->requirePresence('ip_address', 'create')
92            ->notEmptyString('ip_address');
93
94        $validator
95            ->scalar('user_agent')
96            ->maxLength('user_agent', 255)
97            ->requirePresence('user_agent', 'create')
98            ->notEmptyString('user_agent');
99
100        return $validator;
101    }
102
103    /**
104     * Returns a rules checker object that will be used for validating
105     * application integrity.
106     *
107     * @param \Cake\ORM\RulesChecker $rules The rules object to be modified.
108     * @return \Cake\ORM\RulesChecker
109     */
110    public function buildRules(RulesChecker $rules): RulesChecker
111    {
112        $rules->add($rules->existsIn(['user_id'], 'Users'), ['errorField' => 'user_id']);
113
114        return $rules;
115    }
116
117    /**
118     * Creates a consent cookie from a consent entity
119     * getLatestConsent($sessionId, $user->getIdentifier())
120     *
121     * @param \App\Model\Entity\CookieConsent $consent The consent entity to create cookie from
122     * @return \Cake\Http\Cookie\Cookie The configured cookie object
123     */
124    public function createConsentCookie(CookieConsent $consent): Cookie
125    {
126        $isHttps = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on';
127
128        $cookie = (new Cookie('consent_cookie'))
129            ->withValue(json_encode([
130                'user_id' => $consent->user_id,
131                'analytics_consent' => $consent->analytics_consent,
132                'functional_consent' => $consent->functional_consent,
133                'marketing_consent' => $consent->marketing_consent,
134                'essential_consent' => true,
135                'created' => time(),
136            ]))
137            ->withExpiry(new DateTime('+1 year'))
138            ->withPath('/')
139            ->withHttpOnly(true)
140            ->withSameSite('Lax');
141
142        // Only set Secure flag if we're actually on HTTPS
143        if ($isHttps) {
144            $cookie = $cookie->withSecure(true);
145        }
146
147        return $cookie;
148    }
149
150    /**
151     * Gets the latest consent record prioritizing user_id over session_id
152     *
153     * @param string|null $sessionId The session ID to search for
154     * @param string|null $userId The user ID to search for
155     * @return array|null Array of consent data or null if no match found
156     */
157    public function getLatestConsent(?string $sessionId = null, ?string $userId = null): ?array
158    {
159        $fields = [
160            'user_id',
161            'session_id',
162            'analytics_consent',
163            'functional_consent',
164            'marketing_consent',
165            'essential_consent',
166            'ip_address',
167            'user_agent',
168        ];
169
170        // If both parameters are null, return early
171        if ($sessionId === null && $userId === null) {
172            return null;
173        }
174
175        // First try to find by user_id if provided
176        if ($userId !== null) {
177            $result = $this->find()
178                ->select($fields)
179                ->where(['user_id' => $userId])
180                ->orderBy(['created' => 'DESC'])
181                ->first();
182
183            if ($result !== null) {
184                return $result->toArray();
185            }
186        }
187
188        // If no result found by user_id and session_id is provided, try finding by session_id
189        if ($sessionId !== null) {
190            $result = $this->find()
191                ->select($fields)
192                ->where(['session_id' => $sessionId])
193                ->orderBy(['created' => 'DESC'])
194                ->first();
195
196            if ($result !== null) {
197                return $result->toArray();
198            }
199        }
200
201        // No matching record found
202        return null;
203    }
204}