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 ;
2021-04-15 23:30:12 +01:00
use App\Core\Cache ;
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 ;
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-09-20 17:02:35 +01:00
use App\Core\Security ;
2021-11-28 12:25:23 +00:00
use App\Entity\Activity ;
2021-09-18 03:22:27 +01:00
use App\Entity\Actor ;
2021-11-24 15:51:01 +00:00
use App\Entity\Language ;
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-08-19 19:18:33 +01:00
use App\Util\Exception\ClientException ;
2021-04-18 02:17:57 +01:00
use App\Util\Exception\InvalidFormException ;
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 ;
2021-12-04 12:58:27 +00:00
use Component\Attachment\Entity\ActorToAttachment ;
use Component\Attachment\Entity\Attachment ;
use Component\Attachment\Entity\AttachmentToNote ;
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 ;
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
*
* @ throws ClientException
* @ throws RedirectException
* @ throws ServerException
2020-11-06 19:47:15 +00:00
*/
2021-11-15 17:05:36 +00:00
public function onAppendRightPostingBlock ( Request $request , array & $res ) : bool
2020-08-14 16:46:08 +01:00
{
2021-09-17 21:51:55 +01:00
if (( $user = Common :: user ()) === null ) {
2020-11-06 19:47:15 +00:00
return Event :: next ;
2020-08-20 01:40:06 +01:00
}
2021-11-24 15:51:01 +00:00
$actor = $user -> getActor ();
2020-09-05 22:28:53 +01:00
$actor_id = $user -> getId ();
2021-11-24 15:51:01 +00:00
$to_tags = [];
$tags = Cache :: get (
2021-10-10 09:26:18 +01:00
" actor-circle- { $actor_id } " ,
2021-11-24 15:51:01 +00:00
fn () => DB :: dql ( 'select c.tag from App\Entity\ActorCircle c where c.tagger = :tagger' , [ 'tagger' => $actor_id ]),
2021-10-10 09:26:18 +01:00
);
2021-04-15 23:30:12 +01:00
foreach ( $tags as $t ) {
2021-11-24 15:51:01 +00:00
$t = $t [ 'tag' ];
2020-08-28 07:15:56 +01:00
$to_tags [ $t ] = $t ;
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 = [
'Plain Text' => 'text/plain' ,
];
2021-09-14 13:40:50 +01:00
Event :: handle ( 'PostingAvailableContentTypes' , [ & $available_content_types ]);
2021-09-09 03:46:30 +01:00
2021-11-15 17:05:36 +00:00
$context_actor = null ; // This is where we'd plug in the group in which the actor is posting, or whom they're replying to
2021-11-24 15:51:01 +00:00
$form_params = [
2021-11-17 17:14:15 +00:00
[ 'to' , ChoiceType :: class , [ 'label' => _m ( 'To:' ), 'multiple' => false , 'expanded' => false , 'choices' => $to_tags ]],
[ 'visibility' , ChoiceType :: class , [ 'label' => _m ( 'Visibility:' ), 'multiple' => false , 'expanded' => false , 'data' => 'public' , 'choices' => [ _m ( 'Public' ) => 'public' , _m ( 'Instance' ) => 'instance' , _m ( 'Private' ) => 'private' ]]],
[ 'content' , TextareaType :: class , [ 'label' => _m ( 'Content:' ), 'data' => $initial_content , 'attr' => [ 'placeholder' => _m ( $placeholder )], 'constraints' => [ new Length ([ 'max' => Common :: config ( 'site' , 'text_limit' )])]]],
[ 'attachments' , FileType :: class , [ 'label' => _m ( 'Attachments:' ), 'multiple' => true , 'required' => false , 'invalid_message' => _m ( 'Attachment not valid.' )]],
2021-12-08 14:28:58 +00:00
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-24 15:32:28 +01:00
];
2021-10-21 14:54:32 +01:00
2021-11-24 15:51:01 +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-11-24 15:51:01 +00:00
$data = $form -> getData ();
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 = [];
Event :: handle ( 'PostingHandleForm' , [ $request , $actor , $data , & $extra_args , $form_params , $form ]);
self :: storeLocalNote (
$user -> getActor (),
$data [ 'content' ],
$content_type ,
$data [ 'language' ],
$data [ 'attachments' ],
process_note_content_extra_args : $extra_args ,
);
2021-10-21 14:54:32 +01:00
throw new RedirectException ();
}
} catch ( FormSizeFileException $sizeFileException ) {
2021-10-24 15:32:28 +01:00
throw new FormSizeFileException ();
2021-10-21 14:54:32 +01:00
} catch ( InvalidFormException $invalidFormException ) {
2020-09-08 01:12:33 +01:00
throw new InvalidFormException ();
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
*
2021-12-04 12:58:27 +00:00
* @ param array $attachments Array of 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
2021-11-24 15:51:01 +00:00
*
* @ throws \App\Util\Exception\DuplicateFoundException
2021-09-18 03:44:02 +01:00
* @ throws ClientException
* @ throws ServerException
2021-11-24 15:51:01 +00:00
*
* @ return \App\Core\Entity | mixed
2021-09-18 03:44:02 +01:00
*/
2021-12-04 12:58:27 +00:00
public static function storeLocalNote (
Actor $actor ,
string $content ,
string $content_type ,
string $language ,
array $attachments = [],
array $processed_attachments = [],
array $process_note_content_extra_args = [],
) {
2021-09-14 13:40:50 +01:00
$rendered = null ;
2021-11-27 04:12:44 +00:00
$mentions = [];
2021-11-27 15:06:46 +00:00
Event :: handle ( 'RenderNoteContent' , [ $content , $content_type , & $rendered , $actor , $language , & $mentions ]);
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 ,
2021-12-04 19:58:00 +00:00
'language_id' => Language :: getByLocale ( $language ) -> getId (),
2021-11-24 15:51:01 +00:00
'is_local' => true ,
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-04 12:58:27 +00:00
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 ]));
}
}
2021-09-22 15:01:52 +01:00
2021-11-27 04:12:44 +00:00
$act = 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 (),
'is_local' => true ,
'source' => 'web' ,
2021-11-27 04:12:44 +00:00
]);
DB :: persist ( $act );
2021-09-22 15:01:52 +01:00
DB :: flush ();
2021-11-07 01:32:06 +00:00
2021-11-27 04:12:44 +00:00
$mentioned = [];
foreach ( $mentions as $mention ) {
foreach ( $mention [ 'mentioned' ] as $m ) {
$mentioned [] = $m -> getId ();
}
}
2021-11-28 12:25:23 +00:00
Event :: handle ( 'NewNotification' , [ $actor , $act , [ 'object' => $mentioned ], " { $actor -> getNickname () } created note { $note -> getUrl () } " ]);
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
$rendered = Security :: sanitize ( $content );
return Event :: stop ;
default :
return Event :: next ;
2021-09-14 13:40:50 +01:00
}
2020-09-10 21:35:57 +01:00
}
2021-04-27 21:53:59 +01:00
/**
* Get a unique representation of a file on disk
*
* This can be used in the future to deduplicate images by visual content
*/
2021-08-31 18:33:58 +01:00
public function onHashFile ( string $filename , ? string & $out_hash ) : bool
2021-04-27 21:53:59 +01:00
{
$out_hash = hash_file ( Attachment :: FILEHASH_ALGO , $filename );
return Event :: stop ;
}
2021-04-28 22:53:02 +01:00
/**
2021-08-31 18:33:58 +01:00
* Fill the list with allowed sizes for an attachment , to prevent potential DoS ' ing by requesting thousands of different thumbnail sizes
2021-04-28 22:53:02 +01:00
*/
2021-08-31 18:33:58 +01:00
public function onGetAllowedThumbnailSizes ( ? array & $sizes ) : bool
2021-04-28 22:53:02 +01:00
{
$sizes [] = [ 'width' => Common :: config ( 'thumbnail' , 'width' ), 'height' => Common :: config ( 'thumbnail' , 'height' )];
return Event :: next ;
}
2020-08-14 16:46:08 +01:00
}