1 | <?php |
||
2 | |||
3 | namespace Frontend\Modules\Profiles\Engine; |
||
4 | |||
5 | use Common\Uri as CommonUri; |
||
6 | use Frontend\Core\Engine\Model as FrontendModel; |
||
7 | use Frontend\Core\Engine\Navigation as FrontendNavigation; |
||
8 | use Frontend\Modules\Profiles\Engine\Authentication as FrontendProfilesAuthentication; |
||
9 | use Frontend\Modules\Profiles\Engine\Model as FrontendProfilesModel; |
||
10 | use Frontend\Modules\Profiles\Engine\Profile as FrontendProfilesProfile; |
||
11 | |||
12 | /** |
||
13 | * In this file we store all generic functions that we will be using with profiles. |
||
14 | */ |
||
15 | class Model |
||
16 | { |
||
17 | const MAX_DISPLAY_NAME_CHANGES = 2; |
||
18 | |||
19 | /** |
||
20 | * Avatars cache |
||
21 | * |
||
22 | * @var array |
||
23 | */ |
||
24 | private static $avatars = []; |
||
25 | |||
26 | public static function maxDisplayNameChanges(): int |
||
27 | { |
||
28 | return FrontendModel::get('fork.settings')->get('Profiles', 'max_display_name_changes', self::MAX_DISPLAY_NAME_CHANGES); |
||
29 | } |
||
30 | |||
31 | public static function displayNameCanStillBeChanged(Profile $profile): bool |
||
32 | { |
||
33 | if (!FrontendModel::get('fork.settings')->get('Profiles', 'limit_display_name_changes', false)) { |
||
34 | return true; |
||
35 | } |
||
36 | |||
37 | return (int) FrontendProfilesModel::getSetting($profile->getId(), 'display_name_changes') < |
||
38 | self::maxDisplayNameChanges(); |
||
39 | } |
||
40 | |||
41 | public static function deleteSetting(int $profileId, string $name): int |
||
42 | { |
||
43 | return (int) FrontendModel::getContainer()->get('database')->delete( |
||
44 | 'profiles_settings', |
||
45 | 'profile_id = ? AND name = ?', |
||
46 | [$profileId, $name] |
||
47 | ); |
||
48 | } |
||
49 | |||
50 | public static function existsByEmail(string $email, int $excludedId = null): bool |
||
51 | { |
||
52 | $where = 'p.email = :email'; |
||
53 | $parameters = ['email' => $email]; |
||
54 | |||
55 | if ($excludedId !== null) { |
||
56 | $where .= ' AND p.id != :excludedId'; |
||
57 | $parameters['excludedId'] = $excludedId; |
||
58 | } |
||
59 | |||
60 | return (bool) FrontendModel::getContainer()->get('database')->getVar( |
||
61 | 'SELECT 1 |
||
62 | FROM profiles AS p |
||
63 | WHERE ' . $where . ' LIMIT 1', |
||
64 | $parameters |
||
65 | ); |
||
66 | } |
||
67 | |||
68 | public static function existsDisplayName(string $displayName, int $excludedId = null): bool |
||
69 | { |
||
70 | $where = 'p.display_name = :displayName'; |
||
71 | $parameters = ['displayName' => $displayName]; |
||
72 | |||
73 | if ($excludedId !== null) { |
||
74 | $where .= ' AND p.id != :excludedId'; |
||
75 | $parameters['excludedId'] = $excludedId; |
||
76 | } |
||
77 | |||
78 | return (bool) FrontendModel::getContainer()->get('database')->getVar( |
||
79 | 'SELECT 1 |
||
80 | FROM profiles AS p |
||
81 | WHERE ' . $where . ' LIMIT 1', |
||
82 | $parameters |
||
83 | ); |
||
84 | } |
||
85 | |||
86 | public static function get(int $profileId): FrontendProfilesProfile |
||
87 | { |
||
88 | return new FrontendProfilesProfile($profileId); |
||
89 | } |
||
90 | |||
91 | /** |
||
92 | * @param int $id The id for the profile we want to get the avatar from. |
||
93 | * @param string $email The email from the user we can use for gravatar. |
||
94 | * @param string $size The resolution you want to use. Default: 240x240 pixels. |
||
95 | * |
||
96 | * @return string $avatar The absolute path to the avatar. |
||
97 | */ |
||
98 | public static function getAvatar(int $id, string $email = null, string $size = '240x240'): string |
||
99 | { |
||
100 | // return avatar from cache |
||
101 | if (isset(self::$avatars[$id])) { |
||
102 | return self::$avatars[$id]; |
||
103 | } |
||
104 | |||
105 | // define avatar path |
||
106 | $avatarPath = FRONTEND_FILES_URL . '/Profiles/Avatars/' . $size . '/'; |
||
107 | |||
108 | // get user |
||
109 | $user = self::get($id); |
||
110 | |||
111 | // if no email is given |
||
112 | if (empty($email)) { |
||
113 | // redefine email |
||
114 | $email = $user->getEmail(); |
||
115 | } |
||
116 | |||
117 | // define avatar |
||
118 | $avatar = $user->getSetting('avatar'); |
||
119 | |||
120 | // no custom avatar defined, get gravatar if allowed |
||
121 | if (empty($avatar) && FrontendModel::get('fork.settings')->get('Profiles', 'allow_gravatar', true)) { |
||
122 | // define hash |
||
123 | $hash = md5(mb_strtolower(trim('d' . $email))); |
||
124 | |||
125 | // define avatar url |
||
126 | $avatar = 'https://www.gravatar.com/avatar/' . $hash; |
||
127 | |||
128 | // when email not exists, it has to show our custom no-avatar image |
||
129 | $avatar .= '?d=' . rawurlencode(SITE_URL . $avatarPath) . 'no-avatar.gif'; |
||
130 | } elseif (empty($avatar)) { |
||
131 | // define avatar as not found |
||
132 | $avatar = SITE_URL . $avatarPath . 'no-avatar.gif'; |
||
133 | } else { |
||
134 | // define custom avatar path |
||
135 | $avatar = $avatarPath . $avatar; |
||
136 | } |
||
137 | |||
138 | // set avatar in cache |
||
139 | self::$avatars[$id] = $avatar; |
||
140 | |||
141 | // return avatar image path |
||
142 | return $avatar; |
||
143 | } |
||
144 | |||
145 | /** |
||
146 | * Encrypt the password with PHP password_hash function. |
||
147 | * |
||
148 | * @param string $password |
||
149 | * |
||
150 | * @return string |
||
151 | */ |
||
152 | public static function encryptPassword(string $password): string |
||
153 | { |
||
154 | return password_hash($password, PASSWORD_DEFAULT); |
||
155 | } |
||
156 | |||
157 | /** |
||
158 | * Verify the password with PHP password_verify function. |
||
159 | * |
||
160 | * @param string $email |
||
161 | * @param string $password |
||
162 | * |
||
163 | * @return bool |
||
164 | */ |
||
165 | public static function verifyPassword(string $email, string $password): bool |
||
166 | { |
||
167 | $encryptedPassword = self::getEncryptedPassword($email); |
||
168 | |||
169 | return password_verify($password, $encryptedPassword); |
||
0 ignored issues
–
show
Bug
introduced
by
Loading history...
|
|||
170 | } |
||
171 | |||
172 | /** |
||
173 | * @param string $string |
||
174 | * @param string $salt |
||
175 | * |
||
176 | * @return string |
||
177 | */ |
||
178 | public static function getEncryptedString(string $string, string $salt): string |
||
179 | { |
||
180 | return md5(sha1(md5($string)) . sha1(md5($salt))); |
||
181 | } |
||
182 | |||
183 | public static function getIdByEmail(string $email): int |
||
184 | { |
||
185 | return (int) FrontendModel::getContainer()->get('database')->getVar( |
||
186 | 'SELECT p.id FROM profiles AS p WHERE p.email = ?', |
||
187 | |||
188 | ); |
||
189 | } |
||
190 | |||
191 | /** |
||
192 | * @param string $name Setting name. |
||
193 | * @param mixed $value Value of the setting. |
||
194 | * |
||
195 | * @return int |
||
196 | */ |
||
197 | public static function getIdBySetting(string $name, $value): int |
||
198 | { |
||
199 | return (int) FrontendModel::getContainer()->get('database')->getVar( |
||
200 | 'SELECT ps.profile_id |
||
201 | FROM profiles_settings AS ps |
||
202 | WHERE ps.name = ? AND ps.value = ?', |
||
203 | [$name, serialize($value)] |
||
204 | ); |
||
205 | } |
||
206 | |||
207 | /** |
||
208 | * @param int $length Length of random string. |
||
209 | * @param bool $numeric Use numeric characters. |
||
210 | * @param bool $lowercase Use alphanumeric lowercase characters. |
||
211 | * @param bool $uppercase Use alphanumeric uppercase characters. |
||
212 | * @param bool $special Use special characters. |
||
213 | * |
||
214 | * @return string |
||
215 | */ |
||
216 | public static function getRandomString( |
||
217 | int $length = 15, |
||
218 | bool $numeric = true, |
||
219 | bool $lowercase = true, |
||
220 | bool $uppercase = true, |
||
221 | bool $special = true |
||
222 | ): string { |
||
223 | // init |
||
224 | $characters = ''; |
||
225 | $string = ''; |
||
226 | $charset = FrontendModel::getContainer()->getParameter('kernel.charset'); |
||
227 | |||
228 | // possible characters |
||
229 | if ($numeric) { |
||
230 | $characters .= '1234567890'; |
||
231 | } |
||
232 | if ($lowercase) { |
||
233 | $characters .= 'abcdefghijklmnopqrstuvwxyz'; |
||
234 | } |
||
235 | if ($uppercase) { |
||
236 | $characters .= 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; |
||
237 | } |
||
238 | if ($special) { |
||
239 | $characters .= '-_.:;,?!@#&=)([]{}*+%$'; |
||
240 | } |
||
241 | |||
242 | // get random characters |
||
243 | for ($i = 0; $i < $length; ++$i) { |
||
244 | // random index |
||
245 | $index = mt_rand(0, mb_strlen($characters) - 1); |
||
246 | |||
247 | // add character to salt |
||
248 | $string .= mb_substr($characters, $index, 1, $charset); |
||
249 | } |
||
250 | |||
251 | return $string; |
||
252 | } |
||
253 | |||
254 | /** |
||
255 | * Get a setting for a profile. |
||
256 | * |
||
257 | * @param int $id Profile id. |
||
258 | * @param string $name Setting name. |
||
259 | * |
||
260 | * @return mixed |
||
261 | */ |
||
262 | public static function getSetting(int $id, string $name) |
||
263 | { |
||
264 | return unserialize( |
||
265 | (string) FrontendModel::getContainer()->get('database')->getVar( |
||
266 | 'SELECT ps.value |
||
267 | FROM profiles_settings AS ps |
||
268 | WHERE ps.profile_id = ? AND ps.name = ?', |
||
269 | [$id, $name] |
||
270 | ) |
||
271 | ); |
||
272 | } |
||
273 | |||
274 | public static function getSettings(int $profileId): array |
||
275 | { |
||
276 | // get settings |
||
277 | $settings = (array) FrontendModel::getContainer()->get('database')->getPairs( |
||
278 | 'SELECT ps.name, ps.value |
||
279 | FROM profiles_settings AS ps |
||
280 | WHERE ps.profile_id = ?', |
||
281 | $profileId |
||
282 | ); |
||
283 | |||
284 | // unserialize values |
||
285 | foreach ($settings as &$value) { |
||
286 | $value = unserialize($value, ['allowed_classes' => false]); |
||
287 | } |
||
288 | |||
289 | // return |
||
290 | return $settings; |
||
291 | } |
||
292 | |||
293 | /** |
||
294 | * Retrieve a unique URL for a profile based on the display name. |
||
295 | * |
||
296 | * @param string $displayName The display name to base on. |
||
297 | * @param int $excludedId The id of the profile to ignore. |
||
298 | * |
||
299 | * @return string |
||
300 | */ |
||
301 | public static function getUrl(string $displayName, int $excludedId = null): string |
||
302 | { |
||
303 | // decode special chars |
||
304 | $displayName = \SpoonFilter::htmlspecialcharsDecode($displayName); |
||
305 | |||
306 | // urlise |
||
307 | $url = CommonUri::getUrl($displayName); |
||
308 | |||
309 | // get database |
||
310 | $database = FrontendModel::getContainer()->get('database'); |
||
311 | |||
312 | // new item |
||
313 | if ($excludedId === null) { |
||
314 | // get number of profiles with this URL |
||
315 | $number = (int) $database->getVar( |
||
316 | 'SELECT 1 |
||
317 | FROM profiles AS p |
||
318 | WHERE p.url = ? |
||
319 | LIMIT 1', |
||
320 | (string) $url |
||
321 | ); |
||
322 | |||
323 | // already exists |
||
324 | if ($number !== 0) { |
||
325 | // add number |
||
326 | $url = FrontendModel::addNumber($url); |
||
327 | |||
328 | // try again |
||
329 | return self::getUrl($url); |
||
330 | } |
||
331 | |||
332 | return $url; |
||
333 | } |
||
334 | |||
335 | // current profile should be excluded |
||
336 | // get number of profiles with this URL |
||
337 | $number = (int) $database->getVar( |
||
338 | 'SELECT 1 |
||
339 | FROM profiles AS p |
||
340 | WHERE p.url = ? AND p.id != ? |
||
341 | LIMIT 1', |
||
342 | [$url, $excludedId] |
||
343 | ); |
||
344 | |||
345 | // already exists |
||
346 | if ($number !== 0) { |
||
347 | // add number |
||
348 | $url = FrontendModel::addNumber($url); |
||
349 | |||
350 | // try again |
||
351 | return self::getUrl($url, $excludedId); |
||
352 | } |
||
353 | |||
354 | return $url; |
||
355 | } |
||
356 | |||
357 | public static function insert(array $profile): int |
||
358 | { |
||
359 | return (int) FrontendModel::getContainer()->get('database')->insert('profiles', $profile); |
||
360 | } |
||
361 | |||
362 | /** |
||
363 | * Parse the general profiles info into the template. |
||
364 | */ |
||
365 | public static function parse(): void |
||
366 | { |
||
367 | // get the template |
||
368 | $tpl = FrontendModel::getContainer()->get('templating'); |
||
369 | |||
370 | // logged in |
||
371 | if (FrontendProfilesAuthentication::isLoggedIn()) { |
||
372 | // get profile |
||
373 | $profile = FrontendProfilesAuthentication::getProfile(); |
||
374 | |||
375 | // display name set? |
||
376 | if ($profile->getDisplayName() != '') { |
||
377 | $tpl->assign('profileDisplayName', $profile->getDisplayName()); |
||
378 | } else { |
||
379 | // no display name -> use email |
||
380 | $tpl->assign('profileDisplayName', $profile->getEmail()); |
||
381 | } |
||
382 | |||
383 | // show logged in |
||
384 | $tpl->assign('isLoggedIn', true); |
||
385 | } |
||
386 | |||
387 | // ignore these urls in the query string |
||
388 | $ignoreUrls = [ |
||
389 | FrontendNavigation::getUrlForBlock('Profiles', 'Login'), |
||
390 | FrontendNavigation::getUrlForBlock('Profiles', 'Register'), |
||
391 | FrontendNavigation::getUrlForBlock('Profiles', 'ForgotPassword'), |
||
392 | ]; |
||
393 | |||
394 | // query string |
||
395 | $queryString = FrontendModel::getRequest()->query->has('queryString') |
||
396 | ? SITE_URL . '/' . urldecode(FrontendModel::getRequest()->query->get('queryString')) |
||
397 | : SITE_URL . FrontendModel::get('url')->getQueryString(); |
||
398 | |||
399 | // check all ignore urls |
||
400 | foreach ($ignoreUrls as $url) { |
||
401 | // query string contains a boeboe url |
||
402 | if (mb_stripos($queryString, $url) !== false) { |
||
403 | $queryString = ''; |
||
404 | break; |
||
405 | } |
||
406 | } |
||
407 | |||
408 | // no need to add this if its empty |
||
409 | $queryString = ($queryString !== '') ? '?queryString=' . rawurlencode($queryString) : ''; |
||
410 | |||
411 | // useful urls |
||
412 | $tpl->assign('loginUrl', FrontendNavigation::getUrlForBlock('Profiles', 'Login') . $queryString); |
||
413 | $tpl->assign('registerUrl', FrontendNavigation::getUrlForBlock('Profiles', 'Register')); |
||
414 | $tpl->assign('forgotPasswordUrl', FrontendNavigation::getUrlForBlock('Profiles', 'ForgotPassword')); |
||
415 | } |
||
416 | |||
417 | /** |
||
418 | * Insert or update a single profile setting. |
||
419 | * |
||
420 | * @param int $id Profile id. |
||
421 | * @param string $name Setting name. |
||
422 | * @param mixed $value New setting value. |
||
423 | */ |
||
424 | public static function setSetting(int $id, string $name, $value): void |
||
425 | { |
||
426 | // insert or update |
||
427 | FrontendModel::getContainer()->get('database')->execute( |
||
428 | 'INSERT INTO profiles_settings(profile_id, name, value) |
||
429 | VALUES(?, ?, ?) |
||
430 | ON DUPLICATE KEY UPDATE value = ?', |
||
431 | [$id, $name, serialize($value), serialize($value)] |
||
432 | ); |
||
433 | } |
||
434 | |||
435 | /** |
||
436 | * Insert or update multiple profile settings. |
||
437 | * |
||
438 | * @param int $id Profile id. |
||
439 | * @param array $values Settings in key=>value form. |
||
440 | */ |
||
441 | public static function setSettings(int $id, array $values): void |
||
442 | { |
||
443 | // build parameters |
||
444 | $parameters = []; |
||
445 | foreach ($values as $key => $value) { |
||
446 | $parameters[] = $id; |
||
447 | $parameters[] = $key; |
||
448 | $parameters[] = serialize($value); |
||
449 | } |
||
450 | |||
451 | // build the query |
||
452 | $query = 'INSERT INTO profiles_settings(profile_id, name, value) |
||
453 | VALUES'; |
||
454 | $query .= rtrim(str_repeat('(?, ?, ?), ', count($values)), ', ') . ' '; |
||
455 | $query .= 'ON DUPLICATE KEY UPDATE value = VALUES(value)'; |
||
456 | |||
457 | FrontendModel::getContainer()->get('database')->execute($query, $parameters); |
||
458 | } |
||
459 | |||
460 | /** |
||
461 | * @param int $id The profile id. |
||
462 | * @param array $values The values to update. |
||
463 | * |
||
464 | * @return int |
||
465 | */ |
||
466 | public static function update(int $id, array $values): int |
||
467 | { |
||
468 | return (int) FrontendModel::getContainer()->get('database')->update('profiles', $values, 'id = ?', $id); |
||
469 | } |
||
470 | |||
471 | /** |
||
472 | * Get encrypted password for an email. |
||
473 | * |
||
474 | * @param string $email |
||
475 | * |
||
476 | * @return null|string |
||
477 | */ |
||
478 | public static function getEncryptedPassword(string $email): ?string |
||
479 | { |
||
480 | return FrontendModel::get('database')->getVar( |
||
481 | 'SELECT password |
||
482 | FROM profiles |
||
483 | WHERE email = :email', |
||
484 | ['email' => $email] |
||
485 | ); |
||
486 | } |
||
487 | } |
||
488 |