PageRenderTime 57ms CodeModel.GetById 20ms RepoModel.GetById 1ms app.codeStats 0ms

/src/Frontend/Modules/Profiles/Engine/Model.php

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