2020-08-14 16:46:08 +01:00
< ? php
2021-11-24 15:51:01 +00:00
declare ( strict_types = 1 );
2021-10-10 09:26:18 +01:00
2020-08-14 16:46:08 +01:00
// {{{ License
2021-04-15 23:30:12 +01:00
2020-08-14 16:46:08 +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/>.
2021-04-15 23:30:12 +01:00
2020-08-14 16:46:08 +01:00
// }}}
namespace Component\Posting ;
2022-02-14 01:47:58 +00:00
use App\Core\ActorLocalRoles ;
2020-08-14 16:46:08 +01:00
use App\Core\DB\DB ;
use App\Core\Event ;
use App\Core\Form ;
2021-09-20 12:34:28 +01:00
use App\Core\GSFile ;
2022-02-16 06:56:59 +00:00
use App\Entity\NoteType ;
use Component\Notification\Entity\Attention ;
2021-11-24 15:51:01 +00:00
use function App\Core\I18n\_m ;
2021-04-18 02:17:57 +01:00
use App\Core\Modules\Component ;
2021-11-28 12:25:23 +00:00
use App\Core\Router\Router ;
2021-12-26 15:12:06 +00:00
use App\Core\VisibilityScope ;
2021-11-28 12:25:23 +00:00
use App\Entity\Activity ;
2021-09-18 03:22:27 +01:00
use App\Entity\Actor ;
2020-09-10 21:35:57 +01:00
use App\Entity\Note ;
2020-08-14 16:46:08 +01:00
use App\Util\Common ;
2021-12-26 15:12:06 +00:00
use App\Util\Exception\BugFoundException ;
2021-08-19 19:18:33 +01:00
use App\Util\Exception\ClientException ;
2021-12-19 17:43:43 +00:00
use App\Util\Exception\DuplicateFoundException ;
2020-09-05 22:28:53 +01:00
use App\Util\Exception\RedirectException ;
2021-08-31 18:33:58 +01:00
use App\Util\Exception\ServerException ;
2021-11-15 17:05:36 +00:00
use App\Util\Form\FormFields ;
2021-09-14 13:40:50 +01:00
use App\Util\Formatting ;
2022-01-12 17:12:26 +00:00
use App\Util\HTML ;
2021-12-04 12:58:27 +00:00
use Component\Attachment\Entity\ActorToAttachment ;
use Component\Attachment\Entity\AttachmentToNote ;
2021-12-19 17:43:43 +00:00
use Component\Conversation\Conversation ;
2021-12-26 09:48:16 +00:00
use Component\Language\Entity\Language ;
2021-12-25 11:23:25 +00:00
use Functional as F ;
2020-08-22 01:24:55 +01:00
use Symfony\Component\Form\Extension\Core\Type\ChoiceType ;
2020-08-20 01:40:06 +01:00
use Symfony\Component\Form\Extension\Core\Type\FileType ;
2020-08-14 16:46:08 +01:00
use Symfony\Component\Form\Extension\Core\Type\SubmitType ;
use Symfony\Component\Form\Extension\Core\Type\TextareaType ;
2021-10-24 15:32:28 +01:00
use Symfony\Component\HttpFoundation\File\Exception\FormSizeFileException ;
use Symfony\Component\HttpFoundation\File\UploadedFile ;
2021-11-15 17:05:36 +00:00
use Symfony\Component\HttpFoundation\Request ;
2022-01-03 20:35:26 +00:00
use Symfony\Component\Routing\Exception\ResourceNotFoundException ;
2021-10-24 15:32:28 +01:00
use Symfony\Component\Validator\Constraints\Length ;
2020-08-14 16:46:08 +01:00
2021-04-18 02:17:57 +01:00
class Posting extends Component
2020-08-14 16:46:08 +01:00
{
2020-11-06 19:47:15 +00:00
/**
* HTML render event handler responsible for adding and handling
* the result of adding the note submission form , only if a user is logged in
2021-08-31 18:33:58 +01:00
*
2022-01-26 20:01:37 +00:00
* @ throws BugFoundException
2021-08-31 18:33:58 +01:00
* @ throws ClientException
2022-01-26 20:01:37 +00:00
* @ throws DuplicateFoundException
2021-08-31 18:33:58 +01:00
* @ throws RedirectException
* @ throws ServerException
2020-11-06 19:47:15 +00:00
*/
2022-02-07 20:25:37 +00:00
public function onAddMainRightPanelBlock ( Request $request , array & $res ) : bool
2020-08-14 16:46:08 +01:00
{
2022-02-15 12:36:45 +00:00
if ( \is_null ( $user = Common :: user ()) || preg_match ( '(feed|conversation|group)' , $request -> get ( '_route' )) === 0 ) {
2020-11-06 19:47:15 +00:00
return Event :: next ;
2020-08-20 01:40:06 +01:00
}
2022-01-12 17:12:26 +00:00
$actor = $user -> getActor ();
2020-08-22 01:24:55 +01:00
2021-09-06 21:01:20 +01:00
$placeholder_strings = [ 'How are you feeling?' , 'Have something to share?' , 'How was your day?' ];
Event :: handle ( 'PostingPlaceHolderString' , [ & $placeholder_strings ]);
$placeholder = $placeholder_strings [ array_rand ( $placeholder_strings )];
2020-08-27 03:25:44 +01:00
2021-09-09 03:46:30 +01:00
$initial_content = '' ;
Event :: handle ( 'PostingInitialContent' , [ & $initial_content ]);
2021-10-24 15:32:28 +01:00
$available_content_types = [
2021-12-25 11:23:25 +00:00
_m ( 'Plain Text' ) => 'text/plain' ,
2021-10-24 15:32:28 +01:00
];
2021-09-14 13:40:50 +01:00
Event :: handle ( 'PostingAvailableContentTypes' , [ & $available_content_types ]);
2021-09-09 03:46:30 +01:00
2021-12-25 11:23:25 +00:00
$in_targets = [];
Event :: handle ( 'PostingFillTargetChoices' , [ $request , $actor , & $in_targets ]);
2021-12-19 17:43:43 +00:00
$context_actor = null ;
2021-12-25 11:23:25 +00:00
Event :: handle ( 'PostingGetContextActor' , [ $request , $actor , & $context_actor ]);
2021-12-19 17:43:43 +00:00
2021-12-25 11:23:25 +00:00
$form_params = [];
2021-12-26 19:08:56 +00:00
if ( ! empty ( $in_targets )) { // @phpstan-ignore-line
2022-02-11 15:31:22 +00:00
// Add "none" option to the first of choices
2022-02-15 12:36:45 +00:00
$in_targets = array_merge ([ _m ( 'Public' ) => 'public' ], $in_targets );
2022-02-11 15:31:22 +00:00
// Make the context actor the first In target option
2022-02-15 12:36:45 +00:00
if ( ! \is_null ( $context_actor )) {
2022-02-11 15:31:22 +00:00
foreach ( $in_targets as $it_nick => $it_id ) {
if ( $it_id === $context_actor -> getId ()) {
unset ( $in_targets [ $it_nick ]);
$in_targets = array_merge ([ $it_nick => $it_id ], $in_targets );
break ;
}
}
}
2021-12-25 11:23:25 +00:00
$form_params [] = [ 'in' , ChoiceType :: class , [ 'label' => _m ( 'In:' ), 'multiple' => false , 'expanded' => false , 'choices' => $in_targets ]];
2021-12-19 17:43:43 +00:00
}
2022-02-11 15:31:22 +00:00
$visibility_options = [
2021-12-26 17:31:53 +00:00
_m ( 'Public' ) => VisibilityScope :: EVERYWHERE -> value ,
_m ( 'Local' ) => VisibilityScope :: LOCAL -> value ,
_m ( 'Addressee' ) => VisibilityScope :: ADDRESSEE -> value ,
2022-02-11 15:31:22 +00:00
];
2022-02-15 12:36:45 +00:00
if ( ! \is_null ( $context_actor ) && $context_actor -> isGroup ()) {
2022-02-14 04:35:13 +00:00
if ( $actor -> canModerate ( $context_actor )) {
2022-02-14 01:47:58 +00:00
if ( $context_actor -> getRoles () & ActorLocalRoles :: PRIVATE_GROUP ) {
$visibility_options = array_merge ([ _m ( 'Group' ) => VisibilityScope :: GROUP -> value ], $visibility_options );
} else {
$visibility_options [ _m ( 'Group' )] = VisibilityScope :: GROUP -> value ;
}
}
2022-02-11 15:31:22 +00:00
}
2022-02-14 01:47:58 +00:00
$form_params [] = [ 'visibility' , ChoiceType :: class , [ 'label' => _m ( 'Visibility:' ), 'multiple' => false , 'expanded' => false , 'choices' => $visibility_options ]];
2022-02-11 15:31:22 +00:00
2021-12-25 11:23:25 +00:00
$form_params [] = [ 'content' , TextareaType :: class , [ 'label' => _m ( 'Content:' ), 'data' => $initial_content , 'attr' => [ 'placeholder' => _m ( $placeholder )], 'constraints' => [ new Length ([ 'max' => Common :: config ( 'site' , 'text_limit' )])]]];
$form_params [] = [ 'attachments' , FileType :: class , [ 'label' => _m ( 'Attachments:' ), 'multiple' => true , 'required' => false , 'invalid_message' => _m ( 'Attachment not valid.' )]];
2021-12-25 17:31:16 +00:00
$form_params [] = FormFields :: language ( $actor , $context_actor , label : _m ( 'Note language' ), help : _m ( 'The selected language will be federated and added as a lang attribute, preferred language can be set up in settings' ));
2021-10-21 14:54:32 +01:00
2021-12-26 15:12:06 +00:00
if ( \count ( $available_content_types ) > 1 ) {
2021-09-14 13:40:50 +01:00
$form_params [] = [ 'content_type' , ChoiceType :: class ,
2021-09-22 15:01:52 +01:00
[
2021-11-24 15:51:01 +00:00
'label' => _m ( 'Text format:' ), 'multiple' => false , 'expanded' => false ,
'data' => $available_content_types [ array_key_first ( $available_content_types )],
2021-09-22 15:01:52 +01:00
'choices' => $available_content_types ,
],
];
2021-09-09 03:46:30 +01:00
}
2021-12-04 12:58:27 +00:00
Event :: handle ( 'PostingAddFormEntries' , [ $request , $actor , & $form_params ]);
2021-10-24 15:32:28 +01:00
$form_params [] = [ 'post_note' , SubmitType :: class , [ 'label' => _m ( 'Post' )]];
2021-11-24 15:51:01 +00:00
$form = Form :: create ( $form_params );
2020-08-26 07:56:31 +01:00
2020-08-14 16:46:08 +01:00
$form -> handleRequest ( $request );
if ( $form -> isSubmitted ()) {
2021-10-21 14:54:32 +01:00
try {
if ( $form -> isValid ()) {
2021-12-16 11:08:53 +00:00
$data = $form -> getData ();
2022-01-03 20:35:26 +00:00
Event :: handle ( 'PostingModifyData' , [ $request , $actor , & $data , $form_params , $form ]);
2021-12-16 11:08:53 +00:00
if ( empty ( $data [ 'content' ]) && empty ( $data [ 'attachments' ])) {
// TODO Display error: At least one of `content` and `attachments` must be provided
2021-12-26 03:44:14 +00:00
throw new ClientException ( _m ( 'You must enter content or provide at least one attachment to post a note.' ));
}
2021-12-26 17:31:53 +00:00
if ( \is_null ( VisibilityScope :: tryFrom ( $data [ 'visibility' ]))) {
2021-12-26 03:44:14 +00:00
throw new ClientException ( _m ( 'You have selected an impossible visibility.' ));
2021-12-16 11:08:53 +00:00
}
2021-10-21 14:54:32 +01:00
$content_type = $data [ 'content_type' ] ? ? $available_content_types [ array_key_first ( $available_content_types )];
2021-12-04 12:58:27 +00:00
$extra_args = [];
2021-12-19 17:43:43 +00:00
Event :: handle ( 'AddExtraArgsToNoteContent' , [ $request , $actor , $data , & $extra_args , $form_params , $form ]);
2021-12-16 11:08:53 +00:00
2022-01-17 21:33:38 +00:00
if ( \array_key_exists ( 'in' , $data ) && $data [ 'in' ] !== 'public' ) {
$target = $data [ 'in' ];
}
2022-01-04 22:04:23 +00:00
2021-12-04 12:58:27 +00:00
self :: storeLocalNote (
2021-12-26 03:44:14 +00:00
actor : $user -> getActor (),
content : $data [ 'content' ],
content_type : $content_type ,
2022-01-04 22:04:23 +00:00
locale : $data [ 'language' ],
2021-12-26 17:31:53 +00:00
scope : VisibilityScope :: from ( $data [ 'visibility' ]),
2022-02-16 06:56:59 +00:00
targets : isset ( $target ) ? [ $target ] : [],
reply_to : $data [ 'reply_to_id' ],
2021-12-26 03:44:14 +00:00
attachments : $data [ 'attachments' ],
2021-12-04 12:58:27 +00:00
process_note_content_extra_args : $extra_args ,
);
2021-12-19 17:43:43 +00:00
2022-01-03 20:35:26 +00:00
try {
if ( $request -> query -> has ( 'from' )) {
$from = $request -> query -> get ( 'from' );
if ( str_contains ( $from , '#' )) {
[ $from , $fragment ] = explode ( '#' , $from );
}
Router :: match ( $from );
throw new RedirectException ( url : $from . ( isset ( $fragment ) ? '#' . $fragment : '' ));
}
} catch ( ResourceNotFoundException $e ) {
// continue
}
2021-10-21 14:54:32 +01:00
throw new RedirectException ();
}
2022-01-03 20:35:26 +00:00
} catch ( FormSizeFileException $e ) {
throw new ClientException ( _m ( 'Invalid file size given' ), previous : $e );
2020-08-14 16:46:08 +01:00
}
}
2021-11-15 17:05:36 +00:00
$res [ 'post_form' ] = $form -> createView ();
2020-08-14 16:46:08 +01:00
return Event :: next ;
}
2020-09-10 21:35:57 +01:00
2021-09-18 03:44:02 +01:00
/**
* Store the given note with $content and $attachments , created by
* $actor_id , possibly as a reply to note $reply_to and with flag
* $is_local . Sanitizes $content and $attachments
*
2022-02-16 06:56:59 +00:00
* @ param Actor $actor The Actor responsible for the creation of this Note
* @ param null | string $content The raw text content
* @ param string $content_type Indicating one of the various supported content format ( Plain Text , Markdown , LaTeX ... )
* @ param null | string $locale Note ' s written text language , set by the default Actor language or upon filling
* @ param null | VisibilityScope $scope The visibility of this Note
* @ param array $targets Actor | int [] : In Group / To Person or Bot , registers an attention between note and target
* @ param null | int | Note $reply_to The soon - to - be Note parent 's id, if it' s a Reply itself
* @ param array $attachments UploadedFile [] to be stored as GSFiles associated to this note
* @ param array $processed_attachments Array of [ Attachment , Attachment ' s name ][] to be associated to this $actor and Note
* @ param array $process_note_content_extra_args Extra arguments for the event ProcessNoteContent
* @ param bool $notify True if the newly created Note activity should be passed on as a Notification
* @ param null | string $rendered The Note ' s content post RenderNoteContent event , which sanitizes and processes the raw content sent
* @ param string $source The source of this Note
* @ return Note
2021-09-18 03:44:02 +01:00
* @ throws ClientException
2021-12-19 17:43:43 +00:00
* @ throws DuplicateFoundException
2021-09-18 03:44:02 +01:00
* @ throws ServerException
*/
2021-12-04 12:58:27 +00:00
public static function storeLocalNote (
2022-02-16 06:56:59 +00:00
Actor $actor ,
? string $content ,
string $content_type ,
? string $locale = null ,
2021-12-26 17:31:53 +00:00
? VisibilityScope $scope = null ,
2022-02-16 06:56:59 +00:00
array $targets = [],
null | int | Note $reply_to = null ,
array $attachments = [],
array $processed_attachments = [],
array $process_note_content_extra_args = [],
bool $notify = true ,
? string $rendered = null ,
string $source = 'web' ,
2021-12-26 03:44:14 +00:00
) : Note {
2021-12-26 17:31:53 +00:00
$scope ? ? = VisibilityScope :: EVERYWHERE ; // TODO: If site is private, default to LOCAL
2022-02-16 06:56:59 +00:00
$reply_to_id = is_null ( $reply_to ) ? null : ( is_int ( $reply_to ) ? $reply_to : $reply_to -> getId ());
2021-11-27 04:12:44 +00:00
$mentions = [];
2022-01-17 20:56:14 +00:00
if ( \is_null ( $rendered ) && ! empty ( $content )) {
2022-01-04 22:04:23 +00:00
Event :: handle ( 'RenderNoteContent' , [ $content , $content_type , & $rendered , $actor , $locale , & $mentions ]);
2021-12-16 11:08:53 +00:00
}
2021-07-22 13:02:09 +01:00
$note = Note :: create ([
2021-11-24 15:51:01 +00:00
'actor_id' => $actor -> getId (),
'content' => $content ,
2021-09-14 13:40:50 +01:00
'content_type' => $content_type ,
2021-11-24 15:51:01 +00:00
'rendered' => $rendered ,
2022-01-04 22:04:23 +00:00
'language_id' => ! \is_null ( $locale ) ? Language :: getByLocale ( $locale ) -> getId () : null ,
2021-11-24 15:51:01 +00:00
'is_local' => true ,
2021-12-26 15:12:06 +00:00
'scope' => $scope ,
2022-01-03 20:35:26 +00:00
'reply_to' => $reply_to_id ,
2022-01-17 20:56:14 +00:00
'source' => $source ,
2020-11-06 19:47:15 +00:00
]);
2021-09-18 03:44:02 +01:00
2021-10-24 15:32:28 +01:00
/** @var UploadedFile[] $attachments */
2021-09-18 03:44:02 +01:00
foreach ( $attachments as $f ) {
2021-11-24 15:51:01 +00:00
$filesize = $f -> getSize ();
2021-10-21 14:54:32 +01:00
$max_file_size = Common :: getUploadLimit ();
2021-09-18 03:44:02 +01:00
if ( $max_file_size < $filesize ) {
2021-10-24 15:32:28 +01:00
throw new ClientException ( _m ( 'No file may be larger than {quota} bytes and the file you sent was {size} bytes. '
2021-11-24 15:51:01 +00:00
. 'Try to upload a smaller version.' , [ 'quota' => $max_file_size , 'size' => $filesize ], ));
2021-09-18 03:44:02 +01:00
}
Event :: handle ( 'EnforceUserFileQuota' , [ $filesize , $actor -> getId ()]);
2021-09-22 15:01:52 +01:00
$processed_attachments [] = [ GSFile :: storeFileAsAttachment ( $f ), $f -> getClientOriginalName ()];
2021-09-18 03:44:02 +01:00
}
DB :: persist ( $note );
// Need file and note ids for the next step
2021-11-27 04:12:44 +00:00
$note -> setUrl ( Router :: url ( 'note_view' , [ 'id' => $note -> getId ()], Router :: ABSOLUTE_URL ));
2021-12-16 11:08:53 +00:00
if ( ! empty ( $content )) {
Event :: handle ( 'ProcessNoteContent' , [ $note , $content , $content_type , $process_note_content_extra_args ]);
}
2021-09-18 03:44:02 +01:00
2021-09-22 15:01:52 +01:00
if ( $processed_attachments !== []) {
2021-09-18 03:44:02 +01:00
foreach ( $processed_attachments as [ $a , $fname ]) {
2021-09-20 12:34:28 +01:00
if ( DB :: count ( 'actor_to_attachment' , $args = [ 'attachment_id' => $a -> getId (), 'actor_id' => $actor -> getId ()]) === 0 ) {
2021-09-18 03:44:02 +01:00
DB :: persist ( ActorToAttachment :: create ( $args ));
}
DB :: persist ( AttachmentToNote :: create ([ 'attachment_id' => $a -> getId (), 'note_id' => $note -> getId (), 'title' => $fname ]));
2022-01-17 20:56:14 +00:00
$a -> livesIncrementAndGet ();
2021-09-18 03:44:02 +01:00
}
}
2021-09-22 15:01:52 +01:00
2022-01-03 20:35:26 +00:00
Conversation :: assignLocalConversation ( $note , $reply_to_id );
2021-12-21 12:07:54 +00:00
$activity = Activity :: create ([
2021-11-28 12:25:23 +00:00
'actor_id' => $actor -> getId (),
'verb' => 'create' ,
2021-11-27 04:12:44 +00:00
'object_type' => 'note' ,
2021-11-28 12:25:23 +00:00
'object_id' => $note -> getId (),
2022-01-17 20:56:14 +00:00
'source' => $source ,
2021-11-27 04:12:44 +00:00
]);
2021-12-21 12:07:54 +00:00
DB :: persist ( $activity );
2022-01-17 20:56:14 +00:00
2022-02-16 06:56:59 +00:00
foreach ( $targets as $target ) {
2022-01-04 22:04:23 +00:00
$target = \is_int ( $target ) ? Actor :: getById ( $target ) : $target ;
2022-02-16 06:56:59 +00:00
DB :: persist ( Attention :: create ([ 'note_id' => $note -> getId (), 'target_id' => $target -> getId ()]));
2022-01-02 21:44:45 +00:00
$mentions [] = [
'mentioned' => [ $target ],
'type' => match ( $target -> getType ()) {
Actor :: PERSON => 'mention' ,
Actor :: GROUP => 'group' ,
default => throw new ClientException ( _m ( 'Unknown target type give in \'In\' field: {target}' , [ '{target}' => $target ? -> getNickname () ? ? '<null>' ])),
},
'text' => $target -> getNickname (),
];
2021-12-25 11:23:25 +00:00
}
2022-01-01 09:34:31 +00:00
$mention_ids = F\unique ( F\flat_map ( $mentions , fn ( array $m ) => F\map ( $m [ 'mentioned' ] ? ? [], fn ( Actor $a ) => $a -> getId ())));
2021-11-27 04:12:44 +00:00
2022-01-04 22:04:23 +00:00
// Flush before notification
2021-12-21 12:43:28 +00:00
DB :: flush ();
2021-12-28 17:49:46 +00:00
if ( $notify ) {
2022-01-03 20:35:26 +00:00
Event :: handle ( 'NewNotification' , [ $actor , $activity , [ 'object' => $mention_ids ], _m ( '{nickname} created a note {note_id}.' , [ '{nickname}' => $actor -> getNickname (), '{note_id}' => $activity -> getObjectId ()])]);
2021-12-28 17:49:46 +00:00
}
2021-11-27 04:12:44 +00:00
2021-11-07 01:32:06 +00:00
return $note ;
2021-09-14 13:40:50 +01:00
}
2021-08-14 16:47:45 +01:00
2021-11-27 15:06:46 +00:00
public function onRenderNoteContent ( string $content , string $content_type , ? string & $rendered , Actor $author , ? string $language = null , array & $mentions = [])
2021-09-14 13:40:50 +01:00
{
2021-09-20 17:02:35 +01:00
switch ( $content_type ) {
case 'text/plain' :
2021-11-28 12:25:23 +00:00
$rendered = Formatting :: renderPlainText ( $content , $language );
2021-11-27 04:12:44 +00:00
[ $rendered , $mentions ] = Formatting :: linkifyMentions ( $rendered , $author , $language );
2021-09-20 17:02:35 +01:00
return Event :: stop ;
case 'text/html' :
// TODO: It has to linkify and stuff as well
2022-01-12 17:12:26 +00:00
$rendered = HTML :: sanitize ( $content );
2021-09-20 17:02:35 +01:00
return Event :: stop ;
default :
return Event :: next ;
2021-09-14 13:40:50 +01:00
}
2020-09-10 21:35:57 +01:00
}
2020-08-14 16:46:08 +01:00
}