2020-06-21 23:56:26 +01:00
< ? php
2021-12-04 19:58:00 +00:00
declare ( strict_types = 1 );
2021-10-10 09:26:18 +01:00
2020-06-21 23:56:26 +01:00
// {{{ License
2020-08-14 23:37:45 +01:00
2020-06-21 23:56:26 +01:00
// This file is part of GNU social - https://www.gnu.org/software/social
//
// GNU social is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// GNU social is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with GNU social. If not, see <http://www.gnu.org/licenses/>.
2020-08-14 23:37:45 +01:00
2020-06-21 23:56:26 +01:00
// }}}
/**
* Handle network public feed
*
* @ package GNUsocial
* @ category Controller
*
2021-02-19 23:29:43 +00:00
* @ author Hugo Sales < hugo @ hsal . es >
2020-06-30 17:35:38 +01:00
* @ author Eliseu Amaro < eliseu @ fc . up . pt >
2021-02-19 23:29:43 +00:00
* @ copyright 2020 - 2021 Free Software Foundation , Inc http :// www . fsf . org
2020-06-21 23:56:26 +01:00
* @ license https :// www . gnu . org / licenses / agpl . html GNU AGPL v3 or later
*/
namespace App\Controller ;
2020-07-30 23:47:47 +01:00
// {{{ Imports
2021-11-15 17:00:58 +00:00
use App\Core\Cache ;
2021-11-15 13:34:34 +00:00
use App\Core\Controller ;
2020-07-30 23:47:47 +01:00
use App\Core\DB\DB ;
use App\Core\Event ;
2020-06-26 23:44:53 +01:00
use App\Core\Form ;
2021-12-04 19:58:00 +00:00
use function App\Core\I18n\_m ;
2021-10-21 13:44:30 +01:00
use App\Core\Log ;
2021-11-15 17:00:58 +00:00
use App\Entity\ActorLanguage ;
2021-11-25 23:08:30 +00:00
use App\Entity\Language ;
2020-07-25 18:56:38 +01:00
use App\Util\Common ;
2021-08-04 21:11:01 +01:00
use App\Util\Exception\AuthenticationException ;
2021-12-03 02:51:41 +00:00
use App\Util\Exception\NicknameEmptyException ;
use App\Util\Exception\NicknameInvalidException ;
use App\Util\Exception\NicknameNotAllowedException ;
use App\Util\Exception\NicknameTakenException ;
use App\Util\Exception\NicknameTooLongException ;
use App\Util\Exception\NoLoggedInUser ;
2021-11-15 17:00:58 +00:00
use App\Util\Exception\RedirectException ;
2021-08-04 21:11:01 +01:00
use App\Util\Exception\ServerException ;
2021-09-06 19:49:03 +01:00
use App\Util\Form\ActorArrayTransformer ;
2020-07-27 04:48:35 +01:00
use App\Util\Form\ArrayTransformer ;
2021-08-06 12:12:10 +01:00
use App\Util\Form\FormFields ;
use App\Util\Formatting ;
2021-11-27 04:11:35 +00:00
use Component\Notification\Entity\UserNotificationPrefs ;
2020-07-30 23:47:47 +01:00
use Doctrine\DBAL\Types\Types ;
use Exception ;
use Functional as F ;
2020-07-26 01:02:20 +01:00
use Misd\PhoneNumberBundle\Form\Type\PhoneNumberType ;
2020-07-21 22:02:39 +01:00
use Symfony\Component\Form\Extension\Core\Type\CheckboxType ;
2021-11-15 17:00:58 +00:00
use Symfony\Component\Form\Extension\Core\Type\IntegerType ;
2021-12-03 02:51:41 +00:00
use Symfony\Component\Form\Extension\Core\Type\PasswordType ;
2020-06-21 23:56:26 +01:00
use Symfony\Component\Form\Extension\Core\Type\SubmitType ;
2020-07-20 22:05:10 +01:00
use Symfony\Component\Form\Extension\Core\Type\TextareaType ;
2020-06-21 23:56:26 +01:00
use Symfony\Component\Form\Extension\Core\Type\TextType ;
2021-12-03 02:51:41 +00:00
use Symfony\Component\Form\FormInterface ;
2021-11-25 23:08:30 +00:00
use Symfony\Component\Form\SubmitButton ;
2020-06-21 23:56:26 +01:00
use Symfony\Component\HttpFoundation\Request ;
2020-07-30 23:47:47 +01:00
// }}} Imports
2021-11-15 13:34:34 +00:00
class UserPanel extends Controller
2020-06-21 23:56:26 +01:00
{
2021-07-29 17:29:25 +01:00
/**
* Return main settings page forms
*
* @ throws Exception
*/
2021-11-15 17:00:58 +00:00
public function allSettings ( Request $request ) : array
2021-07-29 17:29:25 +01:00
{
2021-12-03 02:51:41 +00:00
$personal_form = $this -> personalInfo ( $request );
2021-12-04 19:58:00 +00:00
$email_form = $this -> email ( $request );
2021-12-03 02:51:41 +00:00
$password_form = $this -> password ( $request );
$language_form = $this -> language ( $request );
2021-08-04 21:11:01 +01:00
$notifications_form_array = $this -> notifications ( $request );
2021-07-29 17:29:25 +01:00
2021-08-04 21:11:01 +01:00
return [
2021-12-04 19:58:00 +00:00
'_template' => 'settings/base.html.twig' ,
'profile' => $personal_form -> createView (),
'email' => $email_form -> createView (),
'password' => $password_form -> createView (),
'language' => $language_form -> createView (),
2021-08-04 21:11:01 +01:00
'tabbed_forms_notify' => $notifications_form_array ,
2021-12-04 19:58:00 +00:00
'open_details_query' => $this -> string ( 'open' ),
2021-07-29 17:29:25 +01:00
];
}
2020-11-06 19:47:15 +00:00
/**
2021-12-03 02:51:41 +00:00
* Change email settings form
*
* @ throws NoLoggedInUser
* @ throws ServerException
2020-11-06 19:47:15 +00:00
*/
2021-12-03 02:51:41 +00:00
public function email ( Request $request ) : FormInterface
2020-06-21 23:56:26 +01:00
{
2021-12-03 02:51:41 +00:00
$user = Common :: ensureLoggedIn ();
// TODO Add support missing settings
$form = Form :: create ([
[ 'outgoing_email' , TextType :: class , [ 'label' => _m ( 'Outgoing email' ), 'required' => false , 'help' => _m ( 'Change the email we use to contact you' )]],
[ 'incoming_email' , TextType :: class , [ 'label' => _m ( 'Incoming email' ), 'required' => false , 'help' => _m ( 'Change the email you use to contact us (for posting, for instance)' )]],
[ 'save_email' , SubmitType :: class , [ 'label' => _m ( 'Save email info' )]],
]);
$form -> handleRequest ( $request );
if ( $form -> isSubmitted () && $form -> isValid ()) {
$data = $form -> getData ();
foreach ( $data as $key => $val ) {
$method = 'set' . ucfirst ( Formatting :: snakeCaseToCamelCase ( $key ));
if ( method_exists ( $user , $method )) {
$user -> { $method }( $val );
}
2021-12-02 21:26:17 +00:00
}
2021-12-03 02:51:41 +00:00
DB :: flush ();
}
return $form ;
2020-06-21 23:56:26 +01:00
}
2020-07-21 22:02:39 +01:00
2020-11-06 19:47:15 +00:00
/**
2021-12-03 02:51:41 +00:00
* Change password form
*
* @ throws AuthenticationException
* @ throws NoLoggedInUser
* @ throws ServerException
2020-11-06 19:47:15 +00:00
*/
2021-12-03 02:51:41 +00:00
public function password ( Request $request ) : FormInterface
2020-07-21 22:02:39 +01:00
{
2021-11-15 17:00:58 +00:00
$user = Common :: ensureLoggedIn ();
2021-08-04 21:11:01 +01:00
// TODO Add support missing settings
2021-11-15 17:00:58 +00:00
2021-08-04 21:11:01 +01:00
$form = Form :: create ([
2021-12-03 02:51:41 +00:00
[ 'old_password' , PasswordType :: class , [ 'label' => _m ( 'Old password' ), 'required' => true , 'help' => _m ( 'Enter your old password for verification' ), 'attr' => [ 'placeholder' => '********' ]]],
FormFields :: repeated_password ([ 'required' => true ]),
[ 'save_password' , SubmitType :: class , [ 'label' => _m ( 'Save new password' )]],
2021-08-04 21:11:01 +01:00
]);
$form -> handleRequest ( $request );
if ( $form -> isSubmitted () && $form -> isValid ()) {
$data = $form -> getData ();
2021-12-04 19:58:00 +00:00
if ( ! \is_null ( $data [ 'old_password' ])) {
2021-08-04 21:11:01 +01:00
$data [ 'password' ] = $form -> get ( 'password' ) -> getData ();
if ( ! ( $user -> changePassword ( $data [ 'old_password' ], $data [ 'password' ]))) {
throw new AuthenticationException ( _m ( 'The provided password is incorrect' ));
}
}
unset ( $data [ 'old_password' ], $data [ 'password' ]);
2021-12-03 02:51:41 +00:00
foreach ( $data as $key => $val ) {
$method = 'set' . ucfirst ( Formatting :: snakeCaseToCamelCase ( $key ));
if ( method_exists ( $user , $method )) {
$user -> { $method }( $val );
}
}
DB :: flush ();
}
return $form ;
}
/**
2021-12-04 19:58:00 +00:00
* @ throws NoLoggedInUser
2021-12-03 02:51:41 +00:00
* @ throws RedirectException
* @ throws ServerException
*/
public function language ( Request $request ) : FormInterface
{
$user = Common :: ensureLoggedIn ();
// TODO Add support missing settings
$form = Form :: create ([
FormFields :: language ( $user -> getActor (), context_actor : null , label : _m ( 'Languages' ), help : _m ( 'The languages you understand, so you can see primarily content in those' ), multiple : true , required : false , use_short_display : false ),
[ 'save_languages' , SubmitType :: class , [ 'label' => _m ( 'Proceed to order selected languages' )]],
]);
$form -> handleRequest ( $request );
if ( $form -> isSubmitted () && $form -> isValid ()) {
$data = $form -> getData ();
2021-11-15 17:00:58 +00:00
if ( ! \is_null ( $data [ 'languages' ])) {
$selected_langs = DB :: findBy ( 'language' , [ 'locale' => $data [ 'languages' ]]);
$existing_langs = DB :: dql (
'select l from language l join actor_language al with l.id = al.language_id where al.actor_id = :actor_id' ,
[ 'actor_id' => $user -> getId ()],
);
2021-12-03 02:51:41 +00:00
2021-11-15 17:00:58 +00:00
$new_langs = array_udiff ( $selected_langs , $existing_langs , fn ( $l , $r ) => $l -> getId () <=> $r -> getId ());
$removing_langs = array_udiff ( $existing_langs , $selected_langs , fn ( $l , $r ) => $l -> getId () <=> $r -> getId ());
foreach ( $new_langs as $l ) {
DB :: persist ( ActorLanguage :: create ([ 'actor_id' => $user -> getId (), 'language_id' => $l -> getId (), 'ordering' => 0 ]));
}
2021-12-03 02:51:41 +00:00
2021-11-15 17:00:58 +00:00
if ( ! empty ( $removing_langs )) {
$actor_langs_to_remove = DB :: findBy ( 'actor_language' , [ 'actor_id' => $user -> getId (), 'language_id' => F\map ( $removing_langs , fn ( $l ) => $l -> getId ())]);
foreach ( $actor_langs_to_remove as $lang ) {
DB :: remove ( $lang );
}
}
2021-12-03 02:51:41 +00:00
2021-11-15 17:00:58 +00:00
Cache :: delete ( ActorLanguage :: collectionCacheKey ( $user ));
DB :: flush ();
ActorLanguage :: normalizeOrdering ( $user ); // In case the user doesn't submit the other page
unset ( $data [ 'languages' ]);
throw new RedirectException ( 'settings_sort_languages' , [ '_fragment' => null ]); // TODO doesn't clear fragment
}
2021-12-03 02:51:41 +00:00
}
2021-07-29 17:29:25 +01:00
return $form ;
2020-07-21 22:02:39 +01:00
}
2020-07-22 21:58:23 +01:00
2021-11-25 23:16:04 +00:00
/**
2021-12-03 02:51:41 +00:00
* Local user personal information panel
*
* @ throws NicknameEmptyException
* @ throws NicknameInvalidException
* @ throws NicknameNotAllowedException
* @ throws NicknameTakenException
* @ throws NicknameTooLongException
* @ throws NoLoggedInUser
* @ throws ServerException
2021-11-25 23:16:04 +00:00
*/
2021-12-03 02:51:41 +00:00
public function personalInfo ( Request $request ) : mixed
2021-11-15 17:00:58 +00:00
{
2021-12-03 02:51:41 +00:00
// Ensure the user is logged in and retrieve Actor object for given user
2021-12-04 19:58:00 +00:00
$user = Common :: ensureLoggedIn ();
2021-12-03 02:51:41 +00:00
$actor = $user -> getActor ();
2021-11-15 17:00:58 +00:00
2021-12-03 02:51:41 +00:00
// Used in Form::handle as an array $extra_args
[ $_ , $actor_tags ] = $actor -> getSelfTags ();
2021-12-04 19:58:00 +00:00
$extra = [ 'self_tags' => $actor_tags ];
2021-12-03 02:51:41 +00:00
// Defining the various form fields
$form_definition = [
[ 'nickname' , TextType :: class , [ 'label' => _m ( 'Nickname' ), 'required' => true , 'help' => _m ( '1-64 lowercase letters or numbers, no punctuation or spaces.' )]],
[ 'full_name' , TextType :: class , [ 'label' => _m ( 'Full Name' ), 'required' => false , 'help' => _m ( 'A full name is required, if empty it will be set to your nickname.' )]],
[ 'homepage' , TextType :: class , [ 'label' => _m ( 'Homepage' ), 'required' => false , 'help' => _m ( 'URL of your homepage, blog, or profile on another site.' )]],
[ 'bio' , TextareaType :: class , [ 'label' => _m ( 'Bio' ), 'required' => false , 'help' => _m ( 'Describe yourself and your interests.' )]],
[ 'phone_number' , PhoneNumberType :: class , [ 'label' => _m ( 'Phone number' ), 'required' => false , 'help' => _m ( 'Your phone number' ), 'data_class' => null ]],
[ 'location' , TextType :: class , [ 'label' => _m ( 'Location' ), 'required' => false , 'help' => _m ( 'Where you are, like "City, State (or Region), Country".' )]],
[ 'self_tags' , TextType :: class , [ 'label' => _m ( 'Self Tags' ), 'required' => false , 'help' => _m ( 'Tags for yourself (letters, numbers, -, ., and _), comma- or space-separated.' ), 'transformer' => ArrayTransformer :: class ]],
[ 'save_personal_info' , SubmitType :: class , [ 'label' => _m ( 'Save personal info' )]],
2021-11-15 17:00:58 +00:00
];
2021-12-03 02:51:41 +00:00
// Setting nickname normalised and setting actor cache
$extra_step = function ( $data , $extra_args ) use ( $user , $actor ) {
$user -> setNicknameSanitizedAndCached ( $data [ 'nickname' ], $actor -> getId ());
};
2021-12-05 17:50:15 +00:00
2021-12-03 02:51:41 +00:00
return Form :: handle ( $form_definition , $request , $actor , $extra , $extra_step , [[ 'self_tags' => $extra [ 'self_tags' ]]]);
2021-11-15 17:00:58 +00:00
}
2020-11-06 19:47:15 +00:00
/**
* Local user notification settings tabbed panel
*/
2021-12-03 02:51:41 +00:00
public function notifications ( Request $request ) : array
2020-07-22 21:58:23 +01:00
{
2021-12-04 19:58:00 +00:00
$user = Common :: ensureLoggedIn ();
$schema = DB :: getConnection () -> getSchemaManager ();
$platform = $schema -> getDatabasePlatform ();
$columns = Common :: arrayRemoveKeys ( $schema -> listTableColumns ( 'user_notification_prefs' ), [ 'user_id' , 'transport' , 'created' , 'modified' ]);
2020-07-30 23:47:47 +01:00
$form_defs = [ 'placeholder' => []];
foreach ( $columns as $name => $col ) {
2021-12-04 19:58:00 +00:00
$type = $col -> getType ();
$val = $type -> convertToPHPValue ( $col -> getDefault (), $platform );
2021-11-15 13:37:29 +00:00
$type_str = $type -> getName ();
2021-12-04 19:58:00 +00:00
$label = str_replace ( '_' , ' ' , ucfirst ( $name ));
2020-08-06 00:05:06 +01:00
$labels = [
2021-09-18 03:22:27 +01:00
'target_actor_id' => 'Target Actors' ,
2021-12-04 19:58:00 +00:00
'dm' => 'DM' ,
2020-08-06 00:05:06 +01:00
];
$help = [
2021-12-04 19:58:00 +00:00
'target_actor_id' => 'If specified, these settings apply only to these profiles (comma- or space-separated list)' ,
2021-11-08 13:44:35 +00:00
'activity_by_subscribed' => 'Notify me when someone I subscribed has new activity' ,
2021-12-04 19:58:00 +00:00
'mention' => 'Notify me when mentions me in a notice' ,
'reply' => 'Notify me when someone replies to a notice made by me' ,
'subscription' => 'Notify me when someone subscribes to me or asks for permission to do so' ,
'favorite' => 'Notify me when someone favorites one of my notices' ,
'nudge' => 'Notify me when someone nudges me' ,
'dm' => 'Notify me when someone sends me a direct message' ,
'post_on_status_change' => 'Post a notice when my status in this service changes' ,
'enable_posting' => 'Enable posting from this service' ,
2020-08-06 00:05:06 +01:00
];
2020-08-05 17:31:39 +01:00
switch ( $type_str ) {
2021-12-03 02:51:41 +00:00
case Types :: BOOLEAN :
$form_defs [ 'placeholder' ][ $name ] = [ $name , CheckboxType :: class , [ 'data' => $val , 'required' => false , 'label' => _m ( $labels [ $name ] ? ? $label ), 'help' => _m ( $help [ $name ])]];
break ;
case Types :: INTEGER :
if ( $name == 'target_actor_id' ) {
$form_defs [ 'placeholder' ][ $name ] = [ $name , TextType :: class , [ 'data' => $val , 'required' => false , 'label' => _m ( $labels [ $name ]), 'help' => _m ( $help [ $name ])], 'transformer' => ActorArrayTransformer :: class ];
}
break ;
default :
// @codeCoverageIgnoreStart
Log :: critical ( " Structure of table user_notification_prefs changed in a way not accounted to in notification settings ( { $name } ): " . $type_str );
throw new ServerException ( _m ( 'Internal server error' ));
2021-08-04 21:11:01 +01:00
// @codeCoverageIgnoreEnd
2020-07-30 23:47:47 +01:00
}
}
2021-12-04 19:58:00 +00:00
$form_defs [ 'placeholder' ][ 'save' ] = fn ( string $transport , string $form_name ) => [ $form_name , SubmitType :: class ,
[ 'label' => _m ( 'Save notification settings for {transport}' , [ 'transport' => $transport ])], ];
2021-08-07 19:24:11 +01:00
2020-10-19 19:22:59 +01:00
Event :: handle ( 'AddNotificationTransport' , [ & $form_defs ]);
2020-07-30 23:47:47 +01:00
unset ( $form_defs [ 'placeholder' ]);
$tabbed_forms = [];
foreach ( $form_defs as $transport_name => $f ) {
2021-08-07 19:24:11 +01:00
unset ( $f [ 'save' ]);
2021-12-04 19:58:00 +00:00
$form = Form :: create ( $f );
2021-08-07 19:24:11 +01:00
$tabbed_forms [ $transport_name ] = $form ;
$form -> handleRequest ( $request );
if ( $form -> isSubmitted () && $form -> isValid ()) {
$data = $form -> getData ();
2021-08-07 19:25:10 +01:00
unset ( $data [ 'translation_domain' ]);
2021-08-04 21:11:01 +01:00
try {
[ $ent , $is_update ] = UserNotificationPrefs :: createOrUpdate (
array_merge ([ 'user_id' => $user -> getId (), 'transport' => $transport_name ], $data ),
2021-10-10 09:26:18 +01:00
find_by_keys : [ 'user_id' , 'transport' ],
2021-08-04 21:11:01 +01:00
);
if ( ! $is_update ) {
DB :: persist ( $ent );
}
DB :: flush ();
// @codeCoverageIgnoreStart
2021-10-10 09:26:18 +01:00
} catch ( Exception $e ) {
2021-08-04 21:11:01 +01:00
// Somehow, the exception doesn't bubble up in phpunit
2021-10-21 13:44:30 +01:00
// dd($data, $e);
2021-08-04 21:11:01 +01:00
// @codeCoverageIgnoreEnd
2021-10-21 13:44:30 +01:00
Log :: critical ( 'Exception at ' . $e -> getFile () . ':' . $e -> getLine () . ': ' . $e -> getMessage ());
2021-08-07 19:25:10 +01:00
}
2021-08-07 19:24:11 +01:00
}
2020-07-30 23:47:47 +01:00
}
2021-12-04 19:58:00 +00:00
$tabbed_forms = F\map ( $tabbed_forms , fn ( $f ) => $f -> createView ());
2021-07-29 17:29:25 +01:00
return $tabbed_forms ;
2020-07-22 21:58:23 +01:00
}
2021-12-03 02:51:41 +00:00
/**
* Controller for defining the ordering of a users ' languages
*
* @ throws NoLoggedInUser
* @ throws RedirectException
* @ throws ServerException
*/
public function sortLanguages ( Request $request ) : array
{
$user = Common :: ensureLoggedIn ();
$langs = DB :: dql ( 'select l.locale, l.long_display, al.ordering from language l join actor_language al with l.id = al.language_id where al.actor_id = :id order by al.ordering ASC' , [ 'id' => $user -> getId ()]);
$form_entries = [];
foreach ( $langs as $l ) {
$form_entries [] = [ $l [ 'locale' ], IntegerType :: class , [ 'label' => _m ( $l [ 'long_display' ]), 'data' => $l [ 'ordering' ]]];
}
$form_entries [] = [ 'save_language_order' , SubmitType :: class , []];
$form_entries [] = [ 'go_back' , SubmitType :: class , [ 'label' => _m ( 'Return to settings page' )]];
2021-12-04 19:58:00 +00:00
$form = Form :: create ( $form_entries );
2021-12-03 02:51:41 +00:00
$form -> handleRequest ( $request );
if ( $form -> isSubmitted () && $form -> isValid ()) {
/** @var SubmitButton $button */
2021-12-04 19:58:00 +00:00
$button = $form -> get ( 'go_back' );
2021-12-03 02:51:41 +00:00
$go_back = $button -> isClicked ();
2021-12-04 19:58:00 +00:00
$data = $form -> getData ();
2021-12-03 02:51:41 +00:00
asort ( $data ); // Sort by the order value
$data = array_keys ( $data ); // This keeps the order and gives us a unique number for each
foreach ( $data as $order => $locale ) {
2021-12-04 19:58:00 +00:00
$lang = Language :: getByLocale ( $locale );
2021-12-03 02:51:41 +00:00
$actor_lang = DB :: getReference ( 'actor_language' , [ 'actor_id' => $user -> getId (), 'language_id' => $lang -> getId ()]);
$actor_lang -> setOrdering ( $order + 1 );
}
DB :: flush ();
if ( ! $go_back ) {
// Stay on same page, but force update and prevent resubmission
throw new RedirectException ( 'settings_sort_languages' );
} else {
throw new RedirectException ( 'settings' , [ 'open' => 'account' , '_fragment' => 'save_account_info_languages' ]);
}
}
return [
'_template' => 'settings/sort_languages.html.twig' ,
2021-12-04 19:58:00 +00:00
'form' => $form -> createView (),
2021-12-03 02:51:41 +00:00
];
}
2020-07-23 15:08:31 +01:00
}