2020-08-15 00:46:08 +09:00
< ? php
2021-11-25 00:51:01 +09:00
declare ( strict_types = 1 );
2021-10-10 17:26:18 +09:00
2020-08-15 00:46:08 +09:00
// {{{ License
2021-04-16 07:30:12 +09:00
2020-08-15 00:46:08 +09: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-16 07:30:12 +09:00
2020-08-15 00:46:08 +09:00
// }}}
namespace Component\Posting ;
2021-04-16 07:30:12 +09:00
use App\Core\Cache ;
2020-08-15 00:46:08 +09:00
use App\Core\DB\DB ;
use App\Core\Event ;
use App\Core\Form ;
2021-09-20 20:34:28 +09:00
use App\Core\GSFile ;
2021-11-25 00:51:01 +09:00
use function App\Core\I18n\_m ;
2021-04-18 10:17:57 +09:00
use App\Core\Modules\Component ;
2021-11-28 21:25:23 +09:00
use App\Core\Router\Router ;
2021-09-21 01:02:35 +09:00
use App\Core\Security ;
2021-11-28 21:25:23 +09:00
use App\Entity\Activity ;
2021-09-18 11:22:27 +09:00
use App\Entity\Actor ;
2021-11-25 00:51:01 +09:00
use App\Entity\Language ;
2020-09-11 05:35:57 +09:00
use App\Entity\Note ;
2020-08-15 00:46:08 +09:00
use App\Util\Common ;
2021-08-20 03:18:33 +09:00
use App\Util\Exception\ClientException ;
2020-09-06 06:28:53 +09:00
use App\Util\Exception\RedirectException ;
2021-09-01 02:33:58 +09:00
use App\Util\Exception\ServerException ;
2021-11-16 02:05:36 +09:00
use App\Util\Form\FormFields ;
2021-09-14 21:40:50 +09:00
use App\Util\Formatting ;
2021-12-04 21:58:27 +09:00
use Component\Attachment\Entity\ActorToAttachment ;
use Component\Attachment\Entity\Attachment ;
use Component\Attachment\Entity\AttachmentToNote ;
2020-08-22 09:24:55 +09:00
use Symfony\Component\Form\Extension\Core\Type\ChoiceType ;
2020-08-20 09:40:06 +09:00
use Symfony\Component\Form\Extension\Core\Type\FileType ;
2020-08-15 00:46:08 +09:00
use Symfony\Component\Form\Extension\Core\Type\SubmitType ;
use Symfony\Component\Form\Extension\Core\Type\TextareaType ;
2021-10-24 23:32:28 +09:00
use Symfony\Component\HttpFoundation\File\Exception\FormSizeFileException ;
use Symfony\Component\HttpFoundation\File\UploadedFile ;
2021-11-16 02:05:36 +09:00
use Symfony\Component\HttpFoundation\Request ;
2021-10-24 23:32:28 +09:00
use Symfony\Component\Validator\Constraints\Length ;
2020-08-15 00:46:08 +09:00
2021-04-18 10:17:57 +09:00
class Posting extends Component
2020-08-15 00:46:08 +09:00
{
2020-11-07 04:47:15 +09: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-09-01 02:33:58 +09:00
*
* @ throws ClientException
* @ throws RedirectException
* @ throws ServerException
2020-11-07 04:47:15 +09:00
*/
2021-11-16 02:05:36 +09:00
public function onAppendRightPostingBlock ( Request $request , array & $res ) : bool
2020-08-15 00:46:08 +09:00
{
2021-09-18 05:51:55 +09:00
if (( $user = Common :: user ()) === null ) {
2020-11-07 04:47:15 +09:00
return Event :: next ;
2020-08-20 09:40:06 +09:00
}
2021-11-25 00:51:01 +09:00
$actor = $user -> getActor ();
2020-09-06 06:28:53 +09:00
$actor_id = $user -> getId ();
2021-11-25 00:51:01 +09:00
$to_tags = [];
$tags = Cache :: get (
2021-10-10 17:26:18 +09:00
" actor-circle- { $actor_id } " ,
2021-11-25 00:51:01 +09:00
fn () => DB :: dql ( 'select c.tag from App\Entity\ActorCircle c where c.tagger = :tagger' , [ 'tagger' => $actor_id ]),
2021-10-10 17:26:18 +09:00
);
2021-04-16 07:30:12 +09:00
foreach ( $tags as $t ) {
2021-11-25 00:51:01 +09:00
$t = $t [ 'tag' ];
2020-08-28 15:15:56 +09:00
$to_tags [ $t ] = $t ;
2020-08-22 09:24:55 +09:00
}
2021-09-07 05:01:20 +09: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 11:25:44 +09:00
2021-09-09 11:46:30 +09:00
$initial_content = '' ;
Event :: handle ( 'PostingInitialContent' , [ & $initial_content ]);
2021-10-24 23:32:28 +09:00
$available_content_types = [
'Plain Text' => 'text/plain' ,
];
2021-09-14 21:40:50 +09:00
Event :: handle ( 'PostingAvailableContentTypes' , [ & $available_content_types ]);
2021-09-09 11:46:30 +09:00
2021-11-16 02:05:36 +09: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-25 00:51:01 +09:00
$form_params = [
2021-11-18 02:14:15 +09: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 23:28:58 +09: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 23:32:28 +09:00
];
2021-10-21 22:54:32 +09:00
2021-11-25 00:51:01 +09:00
if ( \count ( $available_content_types ) > 1 ) {
2021-09-14 21:40:50 +09:00
$form_params [] = [ 'content_type' , ChoiceType :: class ,
2021-09-22 23:01:52 +09:00
[
2021-11-25 00:51:01 +09:00
'label' => _m ( 'Text format:' ), 'multiple' => false , 'expanded' => false ,
'data' => $available_content_types [ array_key_first ( $available_content_types )],
2021-09-22 23:01:52 +09:00
'choices' => $available_content_types ,
],
];
2021-09-09 11:46:30 +09:00
}
2021-12-04 21:58:27 +09:00
Event :: handle ( 'PostingAddFormEntries' , [ $request , $actor , & $form_params ]);
2021-10-24 23:32:28 +09:00
$form_params [] = [ 'post_note' , SubmitType :: class , [ 'label' => _m ( 'Post' )]];
2021-11-25 00:51:01 +09:00
$form = Form :: create ( $form_params );
2020-08-26 15:56:31 +09:00
2020-08-15 00:46:08 +09:00
$form -> handleRequest ( $request );
if ( $form -> isSubmitted ()) {
2021-10-21 22:54:32 +09:00
try {
if ( $form -> isValid ()) {
2021-12-16 20:08:53 +09:00
$data = $form -> getData ();
if ( empty ( $data [ 'content' ]) && empty ( $data [ 'attachments' ])) {
// TODO Display error: At least one of `content` and `attachments` must be provided
throw new ClientException ( _m ( 'You must enter content or provide at least one attachment to post a note' ));
}
2021-10-21 22:54:32 +09:00
$content_type = $data [ 'content_type' ] ? ? $available_content_types [ array_key_first ( $available_content_types )];
2021-12-04 21:58:27 +09:00
$extra_args = [];
Event :: handle ( 'PostingHandleForm' , [ $request , $actor , $data , & $extra_args , $form_params , $form ]);
2021-12-16 20:08:53 +09:00
2021-12-04 21:58:27 +09:00
self :: storeLocalNote (
$user -> getActor (),
$data [ 'content' ],
$content_type ,
$data [ 'language' ],
$data [ 'attachments' ],
process_note_content_extra_args : $extra_args ,
);
2021-10-21 22:54:32 +09:00
throw new RedirectException ();
}
} catch ( FormSizeFileException $sizeFileException ) {
2021-10-24 23:32:28 +09:00
throw new FormSizeFileException ();
2020-08-15 00:46:08 +09:00
}
}
2021-11-16 02:05:36 +09:00
$res [ 'post_form' ] = $form -> createView ();
2020-08-15 00:46:08 +09:00
return Event :: next ;
}
2020-09-11 05:35:57 +09:00
2021-09-18 11:44:02 +09: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 21:58:27 +09: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-25 00:51:01 +09:00
*
* @ throws \App\Util\Exception\DuplicateFoundException
2021-09-18 11:44:02 +09:00
* @ throws ClientException
* @ throws ServerException
2021-11-25 00:51:01 +09:00
*
* @ return \App\Core\Entity | mixed
2021-09-18 11:44:02 +09:00
*/
2021-12-04 21:58:27 +09:00
public static function storeLocalNote (
Actor $actor ,
2021-12-16 20:08:53 +09:00
? string $content ,
2021-12-04 21:58:27 +09:00
string $content_type ,
2021-12-10 12:59:23 +09:00
? string $language = null ,
2021-12-04 21:58:27 +09:00
array $attachments = [],
array $processed_attachments = [],
array $process_note_content_extra_args = [],
) {
2021-09-14 21:40:50 +09:00
$rendered = null ;
2021-11-27 13:12:44 +09:00
$mentions = [];
2021-12-16 20:08:53 +09:00
if ( ! empty ( $content )) {
Event :: handle ( 'RenderNoteContent' , [ $content , $content_type , & $rendered , $actor , $language , & $mentions ]);
}
2021-07-22 21:02:09 +09:00
$note = Note :: create ([
2021-11-25 00:51:01 +09:00
'actor_id' => $actor -> getId (),
'content' => $content ,
2021-09-14 21:40:50 +09:00
'content_type' => $content_type ,
2021-11-25 00:51:01 +09:00
'rendered' => $rendered ,
2021-12-16 20:08:53 +09:00
'language_id' => ! \is_null ( $language ) ? Language :: getByLocale ( $language ) -> getId () : null ,
2021-11-25 00:51:01 +09:00
'is_local' => true ,
2020-11-07 04:47:15 +09:00
]);
2021-09-18 11:44:02 +09:00
2021-10-24 23:32:28 +09:00
/** @var UploadedFile[] $attachments */
2021-09-18 11:44:02 +09:00
foreach ( $attachments as $f ) {
2021-11-25 00:51:01 +09:00
$filesize = $f -> getSize ();
2021-10-21 22:54:32 +09:00
$max_file_size = Common :: getUploadLimit ();
2021-09-18 11:44:02 +09:00
if ( $max_file_size < $filesize ) {
2021-10-24 23:32:28 +09:00
throw new ClientException ( _m ( 'No file may be larger than {quota} bytes and the file you sent was {size} bytes. '
2021-11-25 00:51:01 +09:00
. 'Try to upload a smaller version.' , [ 'quota' => $max_file_size , 'size' => $filesize ], ));
2021-09-18 11:44:02 +09:00
}
Event :: handle ( 'EnforceUserFileQuota' , [ $filesize , $actor -> getId ()]);
2021-09-22 23:01:52 +09:00
$processed_attachments [] = [ GSFile :: storeFileAsAttachment ( $f ), $f -> getClientOriginalName ()];
2021-09-18 11:44:02 +09:00
}
DB :: persist ( $note );
// Need file and note ids for the next step
2021-11-27 13:12:44 +09:00
$note -> setUrl ( Router :: url ( 'note_view' , [ 'id' => $note -> getId ()], Router :: ABSOLUTE_URL ));
2021-12-16 20:08:53 +09:00
if ( ! empty ( $content )) {
Event :: handle ( 'ProcessNoteContent' , [ $note , $content , $content_type , $process_note_content_extra_args ]);
}
2021-09-18 11:44:02 +09:00
2021-09-22 23:01:52 +09:00
if ( $processed_attachments !== []) {
2021-09-18 11:44:02 +09:00
foreach ( $processed_attachments as [ $a , $fname ]) {
2021-09-20 20:34:28 +09:00
if ( DB :: count ( 'actor_to_attachment' , $args = [ 'attachment_id' => $a -> getId (), 'actor_id' => $actor -> getId ()]) === 0 ) {
2021-09-18 11:44:02 +09:00
DB :: persist ( ActorToAttachment :: create ( $args ));
}
DB :: persist ( AttachmentToNote :: create ([ 'attachment_id' => $a -> getId (), 'note_id' => $note -> getId (), 'title' => $fname ]));
}
}
2021-09-22 23:01:52 +09:00
2021-11-27 13:12:44 +09:00
$act = Activity :: create ([
2021-11-28 21:25:23 +09:00
'actor_id' => $actor -> getId (),
'verb' => 'create' ,
2021-11-27 13:12:44 +09:00
'object_type' => 'note' ,
2021-11-28 21:25:23 +09:00
'object_id' => $note -> getId (),
'source' => 'web' ,
2021-11-27 13:12:44 +09:00
]);
DB :: persist ( $act );
2021-09-22 23:01:52 +09:00
DB :: flush ();
2021-11-07 10:32:06 +09:00
2021-11-27 13:12:44 +09:00
$mentioned = [];
foreach ( $mentions as $mention ) {
foreach ( $mention [ 'mentioned' ] as $m ) {
$mentioned [] = $m -> getId ();
}
}
2021-11-28 21:25:23 +09:00
Event :: handle ( 'NewNotification' , [ $actor , $act , [ 'object' => $mentioned ], " { $actor -> getNickname () } created note { $note -> getUrl () } " ]);
2021-11-27 13:12:44 +09:00
2021-11-07 10:32:06 +09:00
return $note ;
2021-09-14 21:40:50 +09:00
}
2021-08-15 00:47:45 +09:00
2021-11-28 00:06:46 +09:00
public function onRenderNoteContent ( string $content , string $content_type , ? string & $rendered , Actor $author , ? string $language = null , array & $mentions = [])
2021-09-14 21:40:50 +09:00
{
2021-09-21 01:02:35 +09:00
switch ( $content_type ) {
case 'text/plain' :
2021-11-28 21:25:23 +09:00
$rendered = Formatting :: renderPlainText ( $content , $language );
2021-11-27 13:12:44 +09:00
[ $rendered , $mentions ] = Formatting :: linkifyMentions ( $rendered , $author , $language );
2021-09-21 01:02:35 +09: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 21:40:50 +09:00
}
2020-09-11 05:35:57 +09:00
}
2020-08-15 00:46:08 +09:00
}