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 ;
2022-02-27 09:42:59 +09:00
use App\Core\Cache ;
2020-08-15 00:46:08 +09:00
use App\Core\DB\DB ;
use App\Core\Event ;
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 ;
2022-03-05 00:16:19 +09:00
use App\Core\Router\RouteLoader ;
2021-11-28 21:25:23 +09:00
use App\Core\Router\Router ;
2021-12-27 00:12:06 +09:00
use App\Core\VisibilityScope ;
2021-11-28 21:25:23 +09:00
use App\Entity\Activity ;
2021-09-18 11:22:27 +09:00
use App\Entity\Actor ;
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-12-27 00:12:06 +09:00
use App\Util\Exception\BugFoundException ;
2021-08-20 03:18:33 +09:00
use App\Util\Exception\ClientException ;
2021-12-20 02:43:43 +09:00
use App\Util\Exception\DuplicateFoundException ;
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-09-14 21:40:50 +09:00
use App\Util\Formatting ;
2022-01-13 02:12:26 +09:00
use App\Util\HTML ;
2021-12-04 21:58:27 +09:00
use Component\Attachment\Entity\ActorToAttachment ;
use Component\Attachment\Entity\AttachmentToNote ;
2021-12-20 02:43:43 +09:00
use Component\Conversation\Conversation ;
2021-12-26 18:48:16 +09:00
use Component\Language\Entity\Language ;
2022-02-20 03:08:05 +09:00
use Component\Notification\Entity\Attention ;
2021-12-25 20:23:25 +09:00
use Functional as F ;
2021-10-24 23:32:28 +09:00
use Symfony\Component\HttpFoundation\File\UploadedFile ;
2021-11-16 02:05:36 +09:00
use Symfony\Component\HttpFoundation\Request ;
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
{
2022-03-05 00:16:19 +09:00
public const route = 'posting_form_action' ;
public function onAddRoute ( RouteLoader $r ) : bool
{
$r -> connect ( self :: route , '/form/posting' , Controller\Posting :: class );
return Event :: next ;
}
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
*
2022-01-27 05:01:37 +09:00
* @ throws BugFoundException
2021-09-01 02:33:58 +09:00
* @ throws ClientException
2022-01-27 05:01:37 +09:00
* @ throws DuplicateFoundException
2021-09-01 02:33:58 +09:00
* @ throws RedirectException
* @ throws ServerException
2020-11-07 04:47:15 +09:00
*/
2022-02-08 05:25:37 +09:00
public function onAddMainRightPanelBlock ( Request $request , array & $res ) : bool
2020-08-15 00:46:08 +09:00
{
2022-02-17 06:17:13 +09:00
if ( \is_null ( $user = Common :: user ()) || preg_match ( '(feed|conversation|group|view)' , $request -> get ( '_route' )) === 0 ) {
2020-11-07 04:47:15 +09:00
return Event :: next ;
2020-08-20 09:40:06 +09:00
}
2022-03-05 00:16:19 +09:00
$res [ 'post_form' ] = Form\Posting :: create ( $request ) -> createView ();
2020-08-15 00:46:08 +09:00
return Event :: next ;
}
2020-09-11 05:35:57 +09:00
2022-02-16 15:58:11 +09:00
/**
* @ throws ClientException
2022-02-20 03:08:05 +09:00
* @ throws DuplicateFoundException
2022-02-16 15:58:11 +09:00
* @ throws ServerException
*/
public static function storeLocalPage (
2022-02-20 03:08:05 +09:00
Actor $actor ,
? string $content ,
string $content_type ,
? string $locale = null ,
2022-02-16 15:58:11 +09:00
? VisibilityScope $scope = null ,
2022-02-20 03:08:05 +09:00
array $targets = [],
null | int | Note $reply_to = null ,
array $attachments = [],
array $processed_attachments = [],
array $process_note_content_extra_args = [],
bool $flush_and_notify = true ,
? string $rendered = null ,
string $source = 'web' ,
? string $title = null ,
2022-02-17 04:35:27 +09:00
) : array {
[ $activity , $note , $attention_ids ] = self :: storeLocalNote (
2022-02-16 15:58:11 +09:00
actor : $actor ,
content : $content ,
content_type : $content_type ,
locale : $locale ,
scope : $scope ,
targets : $targets ,
reply_to : $reply_to ,
attachments : $attachments ,
processed_attachments : $processed_attachments ,
process_note_content_extra_args : $process_note_content_extra_args ,
2022-02-17 04:35:27 +09:00
flush_and_notify : false ,
2022-02-16 15:58:11 +09:00
rendered : $rendered ,
2022-02-20 03:08:05 +09:00
source : $source ,
2022-02-16 15:58:11 +09:00
);
2022-02-27 11:04:48 +09:00
$note -> setType ( 'page' );
2022-02-18 03:45:30 +09:00
$note -> setTitle ( $title );
2022-02-17 04:35:27 +09:00
if ( $flush_and_notify ) {
// Flush before notification
DB :: flush ();
Event :: handle ( 'NewNotification' , [ $actor , $activity , [ 'object' => $attention_ids ], _m ( '{nickname} created a page {note_id}.' , [ '{nickname}' => $actor -> getNickname (), '{note_id}' => $activity -> getObjectId ()])]);
}
return [ $activity , $note , $attention_ids ];
2022-02-16 15:58:11 +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
*
2022-02-20 03:08:05 +09: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 $flush_and_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
*
2021-09-18 11:44:02 +09:00
* @ throws ClientException
2021-12-20 02:43:43 +09:00
* @ throws DuplicateFoundException
2021-09-18 11:44:02 +09:00
* @ throws ServerException
2022-02-20 03:08:05 +09:00
*
* @ return array [ Activity , Note , int []] Activity , Note , Attention Ids
2021-09-18 11:44:02 +09:00
*/
2021-12-04 21:58:27 +09:00
public static function storeLocalNote (
2022-02-20 03:08:05 +09:00
Actor $actor ,
? string $content ,
string $content_type ,
? string $locale = null ,
2021-12-27 02:31:53 +09:00
? VisibilityScope $scope = null ,
2022-02-20 03:08:05 +09:00
array $targets = [],
null | int | Note $reply_to = null ,
array $attachments = [],
array $processed_attachments = [],
array $process_note_content_extra_args = [],
bool $flush_and_notify = true ,
? string $rendered = null ,
string $source = 'web' ,
2022-02-17 04:35:27 +09:00
) : array {
2021-12-27 02:31:53 +09:00
$scope ? ? = VisibilityScope :: EVERYWHERE ; // TODO: If site is private, default to LOCAL
2022-02-20 03:08:05 +09:00
$reply_to_id = \is_null ( $reply_to ) ? null : ( \is_int ( $reply_to ) ? $reply_to : $reply_to -> getId ());
$mentions = [];
2022-01-18 05:56:14 +09:00
if ( \is_null ( $rendered ) && ! empty ( $content )) {
2022-01-05 07:04:23 +09:00
Event :: handle ( 'RenderNoteContent' , [ $content , $content_type , & $rendered , $actor , $locale , & $mentions ]);
2021-12-16 20:08:53 +09:00
}
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 ,
2022-01-05 07:04:23 +09:00
'language_id' => ! \is_null ( $locale ) ? Language :: getByLocale ( $locale ) -> getId () : null ,
2021-11-25 00:51:01 +09:00
'is_local' => true ,
2021-12-27 00:12:06 +09:00
'scope' => $scope ,
2022-01-04 05:35:26 +09:00
'reply_to' => $reply_to_id ,
2022-01-18 05:56:14 +09:00
'source' => $source ,
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 );
2022-02-20 03:08:05 +09:00
Conversation :: assignLocalConversation ( $note , $reply_to_id );
2021-09-18 11:44:02 +09:00
2022-02-27 09:42:59 +09:00
// Update replies cache
if ( ! \is_null ( $reply_to_id )) {
Cache :: incr ( Note :: cacheKeys ( $reply_to_id )[ 'replies-count' ]);
2022-03-01 20:19:47 +09:00
// Not having them cached doesn't mean replies don't exist, but don't push it to the
// list, as that means they need to be refetched, or some would be missed
2022-02-27 09:42:59 +09:00
if ( Cache :: exists ( Note :: cacheKeys ( $reply_to_id )[ 'replies' ])) {
Cache :: listPushRight ( Note :: cacheKeys ( $reply_to_id )[ 'replies' ], $note );
}
}
2021-09-18 11:44:02 +09:00
// 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
2022-03-08 02:08:55 +09:00
// These are note attachments now, and not just attachments, ensure these relations are ensured
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 ]) {
2022-03-08 02:08:55 +09:00
// Most attachments should already be associated with its author, but maybe it didn't make sense
//for this attachment, or it's simply a repost of an attachment by a different actor
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-12-21 21:07:54 +09:00
$activity = 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 (),
2022-01-18 05:56:14 +09:00
'source' => $source ,
2021-11-27 13:12:44 +09:00
]);
2021-12-21 21:07:54 +09:00
DB :: persist ( $activity );
2022-01-18 05:56:14 +09:00
2022-02-25 05:20:05 +09:00
$attention_ids = [];
2022-02-16 15:56:59 +09:00
foreach ( $targets as $target ) {
2022-02-25 05:20:05 +09:00
$target_id = \is_int ( $target ) ? $target : $target -> getId ();
DB :: persist ( Attention :: create ([ 'note_id' => $note -> getId (), 'target_id' => $target_id ]));
$attention_ids [ $target_id ] = true ;
2021-12-25 20:23:25 +09:00
}
2022-02-25 05:20:05 +09:00
$attention_ids = array_keys ( $attention_ids );
2021-12-29 02:49:46 +09:00
2022-02-17 04:35:27 +09:00
if ( $flush_and_notify ) {
// Flush before notification
DB :: flush ();
2022-02-25 05:20:05 +09:00
Event :: handle ( 'NewNotification' , [
$actor ,
$activity ,
[
'note-attention' => $attention_ids ,
2022-02-27 09:42:59 +09:00
'object' => F\unique ( F\flat_map ( $mentions , fn ( array $m ) => F\map ( $m [ 'mentioned' ] ? ? [], fn ( Actor $a ) => $a -> getId ()))),
2022-02-25 05:20:05 +09:00
],
_m ( '{nickname} created a note {note_id}.' , [
'{nickname}' => $actor -> getNickname (),
'{note_id}' => $activity -> getObjectId (),
]),
]);
2021-12-29 02:49:46 +09:00
}
2021-11-27 13:12:44 +09:00
2022-02-17 04:35:27 +09:00
return [ $activity , $note , $attention_ids ];
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
2022-01-13 02:12:26 +09:00
$rendered = HTML :: sanitize ( $content );
2021-09-21 01:02:35 +09:00
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
}