Issues (281)

Branch: master

src/Frontend/Modules/Profiles/Engine/Model.php (1 issue)

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
It seems like $encryptedPassword can also be of type null; however, parameter $hash of password_verify() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

169
        return password_verify($password, /** @scrutinizer ignore-type */ $encryptedPassword);
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
            $email
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