The official, canonical postActiv repository. http://www.postactiv.com

profilesettings.php 25KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549
  1. <?php
  2. // !TODO: I WRITE HTML, REFACTOR FOR SMARTY
  3. /* ============================================================================
  4. * Title: ProfileSettings
  5. * Change profile settings
  6. *
  7. * postActiv:
  8. * the micro-blogging software
  9. *
  10. * Copyright:
  11. * Copyright (C) 2016-2018, Maiyannah Bishop
  12. *
  13. * Derived from code copyright various sources:
  14. * o GNU Social (C) 2013-2016, Free Software Foundation, Inc
  15. * o StatusNet (C) 2008-2012, StatusNet, Inc
  16. * ----------------------------------------------------------------------------
  17. * License:
  18. * This program is free software: you can redistribute it and/or modify
  19. * it under the terms of the GNU Affero General Public License as published by
  20. * the Free Software Foundation, either version 3 of the License, or
  21. * (at your option) any later version.
  22. *
  23. * This program is distributed in the hope that it will be useful,
  24. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  25. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  26. * GNU Affero General Public License for more details.
  27. *
  28. * You should have received a copy of the GNU Affero General Public License
  29. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  30. *
  31. * <https://www.gnu.org/licenses/agpl.html>
  32. * ----------------------------------------------------------------------------
  33. * About:
  34. * Change profile settings
  35. *
  36. * PHP version:
  37. * Tested with PHP 7.0
  38. * ----------------------------------------------------------------------------
  39. * File Authors:
  40. * o Evan Prodromou
  41. * o Mike Cochrane <mikec@mikenz.geek.nz>
  42. * o Matthew Gregg <matthew.gregg@gmail.com>
  43. * o Zach Copley
  44. * o Robin Millette <robin@millette.info>
  45. * o Sarven Capadisli
  46. * o Craig Andrews <candrews@integralblue.com>
  47. * o Brion Vibber <brion@pobox.com>
  48. * o Siebrand Mazeland <s.mazeland@xs4all.nl>
  49. * o Sashi Gowda <connect2shashi@gmail.com>
  50. * o Mikael Nordfeldth <mmn@hethane.se>
  51. * o Maiyannah Bishop <maiyannah.bishop@postactiv.com>
  52. *
  53. * Web:
  54. * o postActiv <http://www.postactiv.com>
  55. * o GNU social <https://www.gnu.org/s/social/>
  56. * ============================================================================
  57. */
  58. // This file is formatted so that it provides useful documentation output in
  59. // NaturalDocs. Please be considerate of this before changing formatting.
  60. if (!defined('POSTACTIV')) { exit(1); }
  61. /**
  62. * Change profile settings
  63. */
  64. class ProfilesettingsAction extends SettingsAction
  65. {
  66. /**
  67. * Title of the page
  68. *
  69. * @return string Title of the page
  70. */
  71. function title()
  72. {
  73. // TRANS: Page title for profile settings.
  74. return _('Profile settings');
  75. }
  76. /**
  77. * Instructions for use
  78. *
  79. * @return instructions for use
  80. */
  81. function getInstructions()
  82. {
  83. // TRANS: Usage instructions for profile settings.
  84. return _('You can update your personal profile info here '.
  85. 'so people know more about you.');
  86. }
  87. function showScripts()
  88. {
  89. parent::showScripts();
  90. $this->autofocus('fullname');
  91. }
  92. /**
  93. * Content area of the page
  94. *
  95. * Shows a form for uploading an avatar.
  96. *
  97. * @return void
  98. */
  99. function showContent()
  100. {
  101. $user = $this->scoped->getUser();
  102. $this->elementStart('form', array('method' => 'post',
  103. 'id' => 'form_settings_profile',
  104. 'class' => 'form_settings',
  105. 'action' => common_local_url('profilesettings')));
  106. $this->elementStart('fieldset');
  107. // TRANS: Profile settings form legend.
  108. $this->element('legend', null, _('Profile information'));
  109. $this->hidden('token', common_session_token());
  110. // too much common patterns here... abstractable?
  111. $this->elementStart('ul', 'form_data');
  112. if (Event::handle('StartProfileFormData', array($this))) {
  113. $this->elementStart('li');
  114. // TRANS: Field label in form for profile settings.
  115. $this->input('nickname', _('Nickname'),
  116. $this->trimmed('nickname') ?: $this->scoped->getNickname(),
  117. // TRANS: Tooltip for field label in form for profile settings.
  118. _('1-64 lowercase letters or numbers, no punctuation or spaces.'),
  119. null, false, // "name" (will be set to id), then "required"
  120. !common_config('profile', 'changenick')
  121. ? array('disabled' => 'disabled', 'placeholder' => null)
  122. : array('placeholder' => null));
  123. $this->elementEnd('li');
  124. $this->elementStart('li');
  125. // TRANS: Field label in form for profile settings.
  126. $this->input('fullname', _('Full name'),
  127. $this->trimmed('fullname') ?: $this->scoped->getFullname(),
  128. // TRANS: Instructions for full name text field on profile settings
  129. _('A full name is required, if empty it will be set to your nickname.'),
  130. null, true);
  131. $this->elementEnd('li');
  132. $this->elementStart('li');
  133. // TRANS: Field label in form for profile settings.
  134. $this->input('homepage', _('Homepage'),
  135. $this->trimmed('homepage') ?: $this->scoped->getHomepage(),
  136. // TRANS: Tooltip for field label in form for profile settings.
  137. _('URL of your homepage, blog, or profile on another site.'));
  138. $this->elementEnd('li');
  139. $this->elementStart('li');
  140. $maxBio = Profile::maxBio();
  141. if ($maxBio > 0) {
  142. // TRANS: Tooltip for field label in form for profile settings. Plural
  143. // TRANS: is decided by the number of characters available for the
  144. // TRANS: biography (%d).
  145. $bioInstr = sprintf(_m('Describe yourself and your interests in %d character.',
  146. 'Describe yourself and your interests in %d characters.',
  147. $maxBio),
  148. $maxBio);
  149. } else {
  150. // TRANS: Tooltip for field label in form for profile settings.
  151. $bioInstr = _('Describe yourself and your interests.');
  152. }
  153. // TRANS: Text area label in form for profile settings where users can provide
  154. // TRANS: their biography.
  155. $this->textarea('bio', _('Bio'),
  156. $this->trimmed('bio') ?: $this->scoped->getDescription(),
  157. $bioInstr);
  158. $this->elementEnd('li');
  159. $this->elementStart('li');
  160. $this->input('xmpp', _('XMPP'),
  161. $this->trimmed('xmpp') ?: $this->scoped->getXmpp(),
  162. // TRANS: Your XMPP address
  163. _('Your XMPP address.'),
  164. null, false);
  165. $this->elementEnd('li');
  166. $this->elementStart('li');
  167. $this->textarea('gpgpubkey', _('PGP/GPG'),
  168. $this->trimmed('gpgpubkey') ?: $this->scoped->getGpgPubKey(),
  169. // TRANS: Your XMPP address
  170. _('Your PGP/GPG public key'));
  171. $this->elementEnd('li');
  172. $this->elementStart('li');
  173. $this->input('toxid', _('Tox ID'),
  174. $this->trimmed('toxid') ?: $this->scoped->getToxId(),
  175. // TRANS: Your Tox ID
  176. _('Your Tox ID'),
  177. null, false);
  178. $this->elementEnd('li');
  179. $this->elementStart('li');
  180. $this->input('matrix', _('Matrix address'),
  181. $this->trimmed('matrix') ?: $this->scoped->getMatrix(),
  182. // TRANS: Your Matrix address
  183. _('Your Matrix address'),
  184. null, false);
  185. $this->elementEnd('li');
  186. $this->elementStart('li');
  187. $this->input('donateurl', _('Donations link'),
  188. $this->trimmed('donateurl') ?: $this->scoped->getDonateUrl(),
  189. // TRANS: Donations link
  190. _('Donations link'),
  191. null, false);
  192. $this->elementEnd('li');
  193. $this->elementStart('li');
  194. // TRANS: Field label in form for profile settings.
  195. $this->input('location', _('Location'),
  196. $this->trimmed('location') ?: $this->scoped->location,
  197. // TRANS: Tooltip for field label in form for profile settings.
  198. _('Where you are, like "City, State (or Region), Country".'));
  199. $this->elementEnd('li');
  200. if (common_config('location', 'share') == 'user') {
  201. $this->elementStart('li');
  202. // TRANS: Checkbox label in form for profile settings.
  203. $this->checkbox('sharelocation', _('Share my current location when posting notices'),
  204. ($this->arg('sharelocation')) ?
  205. $this->boolean('sharelocation') : $this->scoped->shareLocation());
  206. $this->elementEnd('li');
  207. }
  208. Event::handle('EndProfileFormData', array($this));
  209. $this->elementStart('li');
  210. // TRANS: Field label in form for profile settings.
  211. $this->input('tags', _('Tags'),
  212. $this->trimmed('tags') ?: implode(' ', Profile_tag::getSelfTagsArray($this->scoped)),
  213. // TRANS: Tooltip for field label in form for profile settings.
  214. _('Tags for yourself (letters, numbers, -, ., and _), comma- or space- separated.'));
  215. $this->elementEnd('li');
  216. $this->elementStart('li');
  217. $language = common_language();
  218. // TRANS: Dropdownlist label in form for profile settings.
  219. $this->dropdown('language', _('Language'),
  220. // TRANS: Tooltip for dropdown list label in form for profile settings.
  221. get_nice_language_list(), _('Preferred language.'),
  222. false, $language);
  223. $this->elementEnd('li');
  224. $timezone = common_timezone();
  225. $timezones = array();
  226. foreach(DateTimeZone::listIdentifiers() as $k => $v) {
  227. $timezones[$v] = $v;
  228. }
  229. $this->elementStart('li');
  230. // TRANS: Dropdownlist label in form for profile settings.
  231. $this->dropdown('timezone', _('Timezone'),
  232. // TRANS: Tooltip for dropdown list label in form for profile settings.
  233. $timezones, _('What timezone are you normally in?'),
  234. true, $timezone);
  235. $this->elementEnd('li');
  236. $this->elementStart('li');
  237. $this->checkbox('autosubscribe',
  238. // TRANS: Checkbox label in form for profile settings.
  239. _('Automatically subscribe to whoever '.
  240. 'subscribes to me (best for non-humans)'),
  241. ($this->arg('autosubscribe')) ?
  242. $this->boolean('autosubscribe') : $user->autosubscribe);
  243. $this->elementEnd('li');
  244. $this->elementStart('li');
  245. $this->dropdown('subscribe_policy',
  246. // TRANS: Dropdown field label on profile settings, for what policies to apply when someone else tries to subscribe to your updates.
  247. _('Subscription policy'),
  248. // TRANS: Dropdown field option for following policy.
  249. array(User::SUBSCRIBE_POLICY_OPEN => _('Let anyone follow me'),
  250. // TRANS: Dropdown field option for following policy.
  251. User::SUBSCRIBE_POLICY_MODERATE => _('Ask me first')),
  252. // TRANS: Dropdown field title on group edit form.
  253. _('Whether other users need your permission to follow your updates.'),
  254. false,
  255. (empty($user->subscribe_policy)) ? User::SUBSCRIBE_POLICY_OPEN : $user->subscribe_policy);
  256. $this->elementEnd('li');
  257. }
  258. if (common_config('profile', 'allowprivate') || $user->private_stream) {
  259. $this->elementStart('li');
  260. $this->checkbox('private_stream',
  261. // TRANS: Checkbox label in profile settings.
  262. _('Make updates visible only to my followers'),
  263. ($this->arg('private_stream')) ?
  264. $this->boolean('private_stream') : $user->private_stream);
  265. $this->elementEnd('li');
  266. }
  267. $this->elementEnd('ul');
  268. // TRANS: Button to save input in profile settings.
  269. $this->submit('save', _m('BUTTON','Save'));
  270. $this->elementEnd('fieldset');
  271. $this->elementEnd('form');
  272. }
  273. /**
  274. * Handle a post
  275. *
  276. * Validate input and save changes. Reload the form with a success
  277. * or error message.
  278. *
  279. * @return void
  280. */
  281. protected function doPost()
  282. {
  283. if (Event::handle('StartProfileSaveForm', array($this))) {
  284. // $nickname will only be set if this changenick value is true.
  285. if (common_config('profile', 'changenick') == true) {
  286. try {
  287. $nickname = Nickname::normalize($this->trimmed('nickname'), true);
  288. } catch (NicknameTakenException $e) {
  289. // Abort only if the nickname is occupied by _another_ local user profile
  290. if (!$this->scoped->sameAs($e->profile)) {
  291. throw $e;
  292. }
  293. // Since the variable wasn't set before the exception was thrown, let's run
  294. // the normalize sequence again, but without in-use check this time.
  295. $nickname = Nickname::normalize($this->trimmed('nickname'));
  296. }
  297. }
  298. $fullname = $this->trimmed('fullname');
  299. $homepage = $this->trimmed('homepage');
  300. $bio = $this->trimmed('bio');
  301. $xmpp = $this->trimmed('xmpp');
  302. $gpgpubkey = $this->trimmed('gpgpubkey');
  303. $toxid = $this->trimmed('toxid');
  304. $matrix = $this->trimmed('matrix');
  305. $donateurl = $this->trimmed('donateurl');
  306. $location = $this->trimmed('location');
  307. $autosubscribe = $this->booleanintstring('autosubscribe');
  308. $subscribe_policy = $this->trimmed('subscribe_policy');
  309. $language = $this->trimmed('language');
  310. $timezone = $this->trimmed('timezone');
  311. $tagstring = $this->trimmed('tags');
  312. // Some validation
  313. if (!is_null($homepage) && (strlen($homepage) > 0) &&
  314. !common_valid_http_url($homepage)) {
  315. // TRANS: Validation error in form for profile settings.
  316. throw new ClientException(_('Homepage is not a valid URL.'));
  317. } else if (!is_null($fullname) && mb_strlen($fullname) > 191) {
  318. // TRANS: Validation error in form for profile settings.
  319. throw new ClientException(_('Full name is too long (maximum 191 characters).'));
  320. } else if (Profile::bioTooLong($bio)) {
  321. // TRANS: Validation error in form for profile settings.
  322. // TRANS: Plural form is used based on the maximum number of allowed
  323. // TRANS: characters for the biography (%d).
  324. throw new ClientException(sprintf(_m('Bio is too long (maximum %d character).',
  325. 'Bio is too long (maximum %d characters).',
  326. Profile::maxBio()),
  327. Profile::maxBio()));
  328. } else if (!is_null($location) && mb_strlen($location) > 191) {
  329. // TRANS: Validation error in form for profile settings.
  330. throw new ClientException(_('Location is too long (maximum 191 characters).'));
  331. } else if (is_null($timezone) || !in_array($timezone, DateTimeZone::listIdentifiers())) {
  332. // TRANS: Validation error in form for profile settings.
  333. throw new ClientException(_('Timezone not selected.'));
  334. } else if (!is_null($language) && strlen($language) > 50) {
  335. // TRANS: Validation error in form for profile settings.
  336. throw new ClientException(_('Language is too long (maximum 50 characters).'));
  337. }
  338. $tags = array();
  339. $tag_priv = array();
  340. if (is_string($tagstring) && strlen($tagstring) > 0) {
  341. $tags = preg_split('/[\s,]+/', $tagstring);
  342. foreach ($tags as &$tag) {
  343. $private = @$tag[0] === '.';
  344. $tag = common_canonical_tag($tag);
  345. if (!common_valid_profile_tag($tag)) {
  346. // TRANS: Validation error in form for profile settings.
  347. // TRANS: %s is an invalid tag.
  348. throw new ClientException(sprintf(_('Invalid tag: "%s".'), $tag));
  349. }
  350. $tag_priv[$tag] = $private;
  351. }
  352. }
  353. $user = $this->scoped->getUser();
  354. $user->query('BEGIN');
  355. // Only allow setting private_stream if site policy allows it
  356. // (or user already _has_ a private stream, then you can unset it)
  357. if (common_config('profile', 'allowprivate') || $user->private_stream) {
  358. $private_stream = $this->booleanintstring('private_stream');
  359. } else {
  360. // if not allowed, we set to the existing value
  361. $private_stream = $user->private_stream;
  362. }
  363. // $user->nickname is updated through Profile->update();
  364. // XXX: XOR
  365. if (($user->autosubscribe ^ $autosubscribe)
  366. || ($user->private_stream ^ $private_stream)
  367. || $user->timezone != $timezone
  368. || $user->language != $language
  369. || $user->subscribe_policy != $subscribe_policy) {
  370. $original = clone($user);
  371. $user->autosubscribe = $autosubscribe;
  372. $user->language = $language;
  373. $user->private_stream = $private_stream;
  374. $user->subscribe_policy = $subscribe_policy;
  375. $user->timezone = $timezone;
  376. $result = $user->update($original);
  377. if ($result === false) {
  378. common_log_db_error($user, 'UPDATE', __FILE__);
  379. $user->query('ROLLBACK');
  380. // TRANS: Server error thrown when user profile settings could not be updated to
  381. // TRANS: automatically subscribe to any subscriber.
  382. throw new ServerException(_('Could not update user for autosubscribe or subscribe_policy.'));
  383. }
  384. // Re-initialize language environment if it changed
  385. common_init_language();
  386. }
  387. $original = clone($this->scoped);
  388. if (common_config('profile', 'changenick') == true && $this->scoped->getNickname() !== $nickname) {
  389. assert(Nickname::normalize($nickname)===$nickname);
  390. common_debug("Changing user nickname from '{$this->scoped->getNickname()}' to '{$nickname}'.");
  391. $this->scoped->nickname = $nickname;
  392. $this->scoped->profileurl = common_profile_url($this->scoped->getNickname());
  393. }
  394. $this->scoped->fullname = (mb_strlen($fullname)>0 ? $fullname : $this->scoped->nickname);
  395. $this->scoped->homepage = $homepage;
  396. $this->scoped->bio = $bio;
  397. $this->scoped->xmpp = $xmpp;
  398. $this->scoped->gpgpubkey = $gpgpubkey;
  399. $this->scoped->toxid = $toxid;
  400. $this->scoped->matrix = $matrix;
  401. $this->scoped->donateurl = $donateurl;
  402. $this->scoped->location = $location;
  403. $loc = Location::fromName($location);
  404. if (empty($loc)) {
  405. $this->scoped->lat = null;
  406. $this->scoped->lon = null;
  407. $this->scoped->location_id = null;
  408. $this->scoped->location_ns = null;
  409. } else {
  410. $this->scoped->lat = $loc->lat;
  411. $this->scoped->lon = $loc->lon;
  412. $this->scoped->location_id = $loc->location_id;
  413. $this->scoped->location_ns = $loc->location_ns;
  414. }
  415. if (common_config('location', 'share') == 'user') {
  416. $exists = false;
  417. $prefs = User_location_prefs::getKV('user_id', $this->scoped->getID());
  418. if (empty($prefs)) {
  419. $prefs = new User_location_prefs();
  420. $prefs->user_id = $this->scoped->getID();
  421. $prefs->created = common_sql_now();
  422. } else {
  423. $exists = true;
  424. $orig = clone($prefs);
  425. }
  426. $prefs->share_location = $this->booleanintstring('sharelocation');
  427. if ($exists) {
  428. $result = $prefs->update($orig);
  429. } else {
  430. $result = $prefs->insert();
  431. }
  432. if ($result === false) {
  433. common_log_db_error($prefs, ($exists) ? 'UPDATE' : 'INSERT', __FILE__);
  434. $user->query('ROLLBACK');
  435. // TRANS: Server error thrown when user profile location preference settings could not be updated.
  436. throw new ServerException(_('Could not save location prefs.'));
  437. }
  438. }
  439. common_debug('Old profile: ' . common_log_objstring($original), __FILE__);
  440. common_debug('New profile: ' . common_log_objstring($this->scoped), __FILE__);
  441. $result = $this->scoped->update($original);
  442. if ($result === false) {
  443. common_log_db_error($this->scoped, 'UPDATE', __FILE__);
  444. $user->query('ROLLBACK');
  445. // TRANS: Server error thrown when user profile settings could not be saved.
  446. throw new ServerException(_('Could not save profile.'));
  447. }
  448. // Set the user tags
  449. $result = Profile_tag::setSelfTags($this->scoped, $tags, $tag_priv);
  450. $user->query('COMMIT');
  451. Event::handle('EndProfileSaveForm', array($this));
  452. // TRANS: Confirmation shown when user profile settings are saved.
  453. return _('Settings saved.');
  454. }
  455. }
  456. function showAside() {
  457. $this->elementStart('div', array('id' => 'aside_primary',
  458. 'class' => 'aside'));
  459. $this->elementStart('div', array('id' => 'account_actions',
  460. 'class' => 'section'));
  461. $this->elementStart('ul');
  462. if (Event::handle('StartProfileSettingsActions', array($this))) {
  463. if ($this->scoped->hasRight(Right::BACKUPACCOUNT)) {
  464. $this->elementStart('li');
  465. $this->element('a',
  466. array('href' => common_local_url('backupaccount')),
  467. // TRANS: Option in profile settings to create a backup of the account of the currently logged in user.
  468. _('Backup account'));
  469. $this->elementEnd('li');
  470. }
  471. if ($this->scoped->hasRight(Right::DELETEACCOUNT)) {
  472. $this->elementStart('li');
  473. $this->element('a',
  474. array('href' => common_local_url('deleteaccount')),
  475. // TRANS: Option in profile settings to delete the account of the currently logged in user.
  476. _('Delete account'));
  477. $this->elementEnd('li');
  478. }
  479. if ($this->scoped->hasRight(Right::RESTOREACCOUNT)) {
  480. $this->elementStart('li');
  481. $this->element('a',
  482. array('href' => common_local_url('restoreaccount')),
  483. // TRANS: Option in profile settings to restore the account of the currently logged in user from a backup.
  484. _('Restore account'));
  485. $this->elementEnd('li');
  486. }
  487. Event::handle('EndProfileSettingsActions', array($this));
  488. }
  489. $this->elementEnd('ul');
  490. $this->elementEnd('div');
  491. $this->elementEnd('div');
  492. }
  493. }
  494. ?>