flarum / framework

Simple forum software for building great communities.
http://flarum.org/
6.23k stars 830 forks source link

Reading $post->content in saving listener throws error if content is empty #3983

Open clarkwinkelmann opened 2 months ago

clarkwinkelmann commented 2 months ago

Current Behavior

Trying to read $post->content on a CommentPost inside Flarum\Post\Event\Saving will throw a 500 error if the user submitted an empty content value.

The problem would also happen when trying to read that attribute anytime if an extension allowed a comment post to be saved with an empty post, which Flarum doesn't allow by default.

I did my tests on Flarum 1.8 but I am certain the problem is still present in the 2.x branch because the code hasn't changed.

[2024-04-29 22:15:38] flarum.ERROR: TypeError: Flarum\Mentions\Formatter\UnparsePostMentions::__invoke(): Argument #2 ($xml) must be of type string, null given, called in /home/clark/Projects/flarum-1.8/vendor/flarum/core/src/Foundation/ContainerUtil.php on line 30 and defined in /home/clark/Projects/flarum-1.8/vendor/flarum/mentions/src/Formatter/UnparsePostMentions.php:34
Stack trace:
#0 /home/clark/Projects/flarum-1.8/vendor/flarum/core/src/Foundation/ContainerUtil.php(30): Flarum\Mentions\Formatter\UnparsePostMentions->__invoke()
#1 /home/clark/Projects/flarum-1.8/vendor/flarum/core/src/Formatter/Formatter.php(139): Flarum\Foundation\ContainerUtil::Flarum\Foundation\{closure}()
#2 /home/clark/Projects/flarum-1.8/vendor/flarum/core/src/Post/CommentPost.php(134): Flarum\Formatter\Formatter->unparse()
#3 /home/clark/Projects/flarum-1.8/vendor/illuminate/database/Eloquent/Concerns/HasAttributes.php(620): Flarum\Post\CommentPost->getContentAttribute()
#4 /home/clark/Projects/flarum-1.8/vendor/illuminate/database/Eloquent/Concerns/HasAttributes.php(1991): Illuminate\Database\Eloquent\Model->mutateAttribute()
#5 /home/clark/Projects/flarum-1.8/vendor/illuminate/database/Eloquent/Concerns/HasAttributes.php(451): Illuminate\Database\Eloquent\Model->transformModelValue()
#6 /home/clark/Projects/flarum-1.8/vendor/illuminate/database/Eloquent/Concerns/HasAttributes.php(430): Illuminate\Database\Eloquent\Model->getAttributeValue()
#7 /home/clark/Projects/flarum-1.8/vendor/flarum/core/src/Database/AbstractModel.php(135): Illuminate\Database\Eloquent\Model->getAttribute()
#8 /home/clark/Projects/flarum-1.8/vendor/illuminate/database/Eloquent/Model.php(2029): Flarum\Database\AbstractModel->getAttribute()
#9 /home/clark/Projects/flarum-1.8/extend.php(17): Illuminate\Database\Eloquent\Model->__get()
#10 /home/clark/Projects/flarum-1.8/vendor/illuminate/events/Dispatcher.php(404): Flarum\Foundation\Site::{closure}()
#11 /home/clark/Projects/flarum-1.8/vendor/illuminate/events/Dispatcher.php(249): Illuminate\Events\Dispatcher->Illuminate\Events\{closure}()
#12 /home/clark/Projects/flarum-1.8/vendor/flarum/core/src/Post/Command/PostReplyHandler.php(97): Illuminate\Events\Dispatcher->dispatch()
#13 /home/clark/Projects/flarum-1.8/vendor/illuminate/bus/Dispatcher.php(122): Flarum\Post\Command\PostReplyHandler->handle()
#14 /home/clark/Projects/flarum-1.8/vendor/illuminate/pipeline/Pipeline.php(128): Illuminate\Bus\Dispatcher->Illuminate\Bus\{closure}()
#15 /home/clark/Projects/flarum-1.8/vendor/illuminate/pipeline/Pipeline.php(103): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}()
#16 /home/clark/Projects/flarum-1.8/vendor/illuminate/bus/Dispatcher.php(132): Illuminate\Pipeline\Pipeline->then()
#17 /home/clark/Projects/flarum-1.8/vendor/illuminate/bus/Dispatcher.php(78): Illuminate\Bus\Dispatcher->dispatchNow()
#18 /home/clark/Projects/flarum-1.8/vendor/flarum/core/src/Api/Controller/CreatePostController.php(62): Illuminate\Bus\Dispatcher->dispatch()
#19 /home/clark/Projects/flarum-1.8/vendor/flarum/core/src/Api/Controller/AbstractSerializeController.php(116): Flarum\Api\Controller\CreatePostController->data()
#20 /home/clark/Projects/flarum-1.8/vendor/flarum/core/src/Api/Controller/AbstractCreateController.php(22): Flarum\Api\Controller\AbstractSerializeController->handle()
#21 /home/clark/Projects/flarum-1.8/vendor/flarum/core/src/Http/RouteHandlerFactory.php(41): Flarum\Api\Controller\AbstractCreateController->handle()
#22 /home/clark/Projects/flarum-1.8/vendor/flarum/core/src/Http/Middleware/ExecuteRoute.php(27): Flarum\Http\RouteHandlerFactory->Flarum\Http\{closure}()
#23 /home/clark/Projects/flarum-1.8/vendor/laminas/laminas-stratigility/src/Next.php(49): Flarum\Http\Middleware\ExecuteRoute->process()
#24 /home/clark/Projects/flarum-1.8/workbench/flarum-ext-audit-free/src/Middlewares/SetLoggerActor.php(28): Laminas\Stratigility\Next->handle()
#25 /home/clark/Projects/flarum-1.8/vendor/laminas/laminas-stratigility/src/Next.php(49): Kilowhat\Audit\Middlewares\SetLoggerActor->process()
#26 /home/clark/Projects/flarum-1.8/vendor/flarum/core/src/Api/Middleware/ThrottleApi.php(33): Laminas\Stratigility\Next->handle()
#27 /home/clark/Projects/flarum-1.8/vendor/laminas/laminas-stratigility/src/Next.php(49): Flarum\Api\Middleware\ThrottleApi->process()
#28 /home/clark/Projects/flarum-1.8/vendor/flarum/core/src/Http/Middleware/CheckCsrfToken.php(44): Laminas\Stratigility\Next->handle()
#29 /home/clark/Projects/flarum-1.8/vendor/laminas/laminas-stratigility/src/Next.php(49): Flarum\Http\Middleware\CheckCsrfToken->process()
#30 /home/clark/Projects/flarum-1.8/vendor/flarum/core/src/Http/Middleware/ResolveRoute.php(69): Laminas\Stratigility\Next->handle()
#31 /home/clark/Projects/flarum-1.8/vendor/laminas/laminas-stratigility/src/Next.php(49): Flarum\Http\Middleware\ResolveRoute->process()
#32 /home/clark/Projects/flarum-1.8/vendor/flarum/core/src/Http/Middleware/SetLocale.php(51): Laminas\Stratigility\Next->handle()
#33 /home/clark/Projects/flarum-1.8/vendor/laminas/laminas-stratigility/src/Next.php(49): Flarum\Http\Middleware\SetLocale->process()
#34 /home/clark/Projects/flarum-1.8/vendor/flarum/core/src/Http/Middleware/AuthenticateWithHeader.php(58): Laminas\Stratigility\Next->handle()
#35 /home/clark/Projects/flarum-1.8/vendor/laminas/laminas-stratigility/src/Next.php(49): Flarum\Http\Middleware\AuthenticateWithHeader->process()
#36 /home/clark/Projects/flarum-1.8/vendor/flarum/core/src/Http/Middleware/AuthenticateWithSession.php(31): Laminas\Stratigility\Next->handle()
#37 /home/clark/Projects/flarum-1.8/vendor/laminas/laminas-stratigility/src/Next.php(49): Flarum\Http\Middleware\AuthenticateWithSession->process()
#38 /home/clark/Projects/flarum-1.8/vendor/flarum/core/src/Http/Middleware/RememberFromCookie.php(52): Laminas\Stratigility\Next->handle()
#39 /home/clark/Projects/flarum-1.8/vendor/laminas/laminas-stratigility/src/Next.php(49): Flarum\Http\Middleware\RememberFromCookie->process()
#40 /home/clark/Projects/flarum-1.8/vendor/flarum/core/src/Http/Middleware/StartSession.php(61): Laminas\Stratigility\Next->handle()
#41 /home/clark/Projects/flarum-1.8/vendor/laminas/laminas-stratigility/src/Next.php(49): Flarum\Http\Middleware\StartSession->process()
#42 /home/clark/Projects/flarum-1.8/vendor/flarum/core/src/Api/Middleware/FakeHttpMethods.php(29): Laminas\Stratigility\Next->handle()
#43 /home/clark/Projects/flarum-1.8/vendor/laminas/laminas-stratigility/src/Next.php(49): Flarum\Api\Middleware\FakeHttpMethods->process()
#44 /home/clark/Projects/flarum-1.8/vendor/flarum/core/src/Http/Middleware/ParseJsonBody.php(28): Laminas\Stratigility\Next->handle()
#45 /home/clark/Projects/flarum-1.8/vendor/laminas/laminas-stratigility/src/Next.php(49): Flarum\Http\Middleware\ParseJsonBody->process()
#46 /home/clark/Projects/flarum-1.8/vendor/flarum/core/src/Http/Middleware/HandleErrors.php(57): Laminas\Stratigility\Next->handle()
#47 /home/clark/Projects/flarum-1.8/vendor/laminas/laminas-stratigility/src/Next.php(49): Flarum\Http\Middleware\HandleErrors->process()
#48 /home/clark/Projects/flarum-1.8/vendor/flarum/core/src/Http/Middleware/InjectActorReference.php(25): Laminas\Stratigility\Next->handle()
#49 /home/clark/Projects/flarum-1.8/vendor/laminas/laminas-stratigility/src/Next.php(49): Flarum\Http\Middleware\InjectActorReference->process()
#50 /home/clark/Projects/flarum-1.8/vendor/laminas/laminas-stratigility/src/MiddlewarePipe.php(75): Laminas\Stratigility\Next->handle()
#51 /home/clark/Projects/flarum-1.8/vendor/middlewares/request-handler/src/RequestHandler.php(84): Laminas\Stratigility\MiddlewarePipe->process()
#52 /home/clark/Projects/flarum-1.8/vendor/laminas/laminas-stratigility/src/Next.php(49): Middlewares\RequestHandler->process()
#53 /home/clark/Projects/flarum-1.8/vendor/middlewares/base-path-router/src/BasePathRouter.php(101): Laminas\Stratigility\Next->handle()
#54 /home/clark/Projects/flarum-1.8/vendor/laminas/laminas-stratigility/src/Next.php(49): Middlewares\BasePathRouter->process()
#55 /home/clark/Projects/flarum-1.8/vendor/laminas/laminas-stratigility/src/Middleware/OriginalMessages.php(36): Laminas\Stratigility\Next->handle()
#56 /home/clark/Projects/flarum-1.8/vendor/laminas/laminas-stratigility/src/Next.php(49): Laminas\Stratigility\Middleware\OriginalMessages->process()
#57 /home/clark/Projects/flarum-1.8/vendor/middlewares/base-path/src/BasePath.php(73): Laminas\Stratigility\Next->handle()
#58 /home/clark/Projects/flarum-1.8/vendor/laminas/laminas-stratigility/src/Next.php(49): Middlewares\BasePath->process()
#59 /home/clark/Projects/flarum-1.8/vendor/flarum/core/src/Http/Middleware/ProcessIp.php(24): Laminas\Stratigility\Next->handle()
#60 /home/clark/Projects/flarum-1.8/vendor/laminas/laminas-stratigility/src/Next.php(49): Flarum\Http\Middleware\ProcessIp->process()
#61 /home/clark/Projects/flarum-1.8/vendor/laminas/laminas-stratigility/src/MiddlewarePipe.php(75): Laminas\Stratigility\Next->handle()
#62 /home/clark/Projects/flarum-1.8/vendor/laminas/laminas-stratigility/src/MiddlewarePipe.php(64): Laminas\Stratigility\MiddlewarePipe->process()
#63 /home/clark/Projects/flarum-1.8/vendor/laminas/laminas-httphandlerrunner/src/RequestHandlerRunner.php(73): Laminas\Stratigility\MiddlewarePipe->handle()
#64 /home/clark/Projects/flarum-1.8/vendor/flarum/core/src/Http/Server.php(45): Laminas\HttpHandlerRunner\RequestHandlerRunner->run()
#65 /home/clark/Projects/flarum-1.8/public/index.php(26): Flarum\Http\Server->listen()
#66 {main}  

Steps to Reproduce

Enable any extension that registers a formatter unparsing callback, like Mentions.

Add local extender, or use in an extension:

<?php

use Flarum\Extend;

return [
    // Register extenders here to customize your forum!
    (new Extend\Event())
        ->listen(\Flarum\Post\Event\Saving::class, function ($event) {
            // Just need to call the magic getter method, no need to do anything with the result
            $event->post->content;
        }),
];

Try creating discussion or reply without any body. See error.

Expected Behavior

$post->content should return null or an empty string

Screenshots

No response

Environment

Output of php flarum info

Output of "php flarum info", run this in terminal in your Flarum directory.

Possible Solution

Just like CommentPost::setContentAttribute() skips the formatter and sets the value to null directly

https://github.com/flarum/framework/blob/270188b5b01c55c30b481eeb565ff5be05143572/framework/core/src/Post/CommentPost.php#L113-L115

CommentPost::getContentAttribute() should also skip the formatter if the value is null and return it directly

https://github.com/flarum/framework/blob/270188b5b01c55c30b481eeb565ff5be05143572/framework/core/src/Post/CommentPost.php#L103-L105

The error trace will be different between Flarum 1.8 and 2.x, because in 1.8.5, Formatter::unparse() has no type check for $xml.

Because of that, the type error is only thrown when encountering the first unparse callback that type-hints its parameters according to the extender documentation, like Mentions does.

https://github.com/flarum/framework/blob/2a693db1b65cf7b0f7e76cbaa695a5e65bd947f6/framework/core/src/Formatter/Formatter.php#L136

https://github.com/flarum/framework/blob/2a693db1b65cf7b0f7e76cbaa695a5e65bd947f6/extensions/mentions/src/Formatter/UnparsePostMentions.php#L34

In the 2.x branch, a string $xml has been added, so it would error right here when called with null, even if there are no unparsing callbacks registered.

https://github.com/flarum/framework/blob/270188b5b01c55c30b481eeb565ff5be05143572/framework/core/src/Formatter/Formatter.php#L97

I'd like to take this opportunity to suggest always parsing content, even empty strings. I don't have any concrete use case, but I feel like it would be more logical to always treat a comment post content as string. It would ensure any logic registered through TextFormatter runs on all posts by default. Extensions that don't want this behavior would have an easier time saving null in the database in the event listener rather than having to call the formatter themselves on the possibly empty content. I think this would require changes to the validator because since it looks at the XML which would now always have a value.

In any case, the getter needs to not error on null values because it's a valid value that can be set in the database.

Additional Context

Reported and discussed at https://discuss.flarum.org/d/34454-every-day-there-are-some-discussions-only-have-title-no-content

FoF Filter issue about its impact https://github.com/FriendsOfFlarum/filter/issues/52

EDIT: adding this as a footnote here as I don't have time to create a separate issue, but the $value ? /*...*/ : null condition also means you can't post 0 as the body, which is definitely not desirable behaviour.