• SupportSolved
  • every day there are some discussions only have title, no content

I use an account to make a test in my flare.

when I only set title, then post discussion, there is an error, like this picture.

the discussion has been posted. but in latest discussions there is no this discussion.

in newest discussions, i can see it, like the picture above

I found an error in the log

[2024-04-25 09:52:38] flarum.ERROR: TypeError: Flarum\Mentions\Formatter\UnparsePostMentions::__invoke(): Argument #2 ($xml) must be of type string, null given, called in /home/webapps/flarum/vendor/flarum/core/src/Foundation/ContainerUtil.php on line 30 and defined in /home/webapps/flarum/vendor/flarum/mentions/src/Formatter/UnparsePostMentions.php:34

  • luceos replied to this.
  • Disregard my now-deleted reply. I found the issue.

    To reproduce you need only FoF Filter and any extension that implements an unparsing callback like Mentions. Then try posting with an account without the bypassFoFFilter permission.

    The problem isn't really caused by anything FoF Filter does. Really it's a Flarum issue.

    The code I referenced earlier, that sets null on the content attribute when passed an empty string actually always runs, then the Saving event is dispatched, and only after that does the Flarum validator runs.

    The error is thrown as soon as $post->content is invoked in the Saving event, because CommentPost::getContentAttribute passes the string|null content to the unparser, which expects string only.

    Even if the issue was fixed in Flarum, there are still potential issues I see with FoF Filter:

    Empty lines in the list of words don't seem to be skipped and they result in a censor rule that will match every time (//i).

    The email for flagged content is sent synchronously inside the Saving event, before the post is saved to the database, meaning it will still go out even if another extension throws a validation error from a different event lister or from the PostValidator itself.

    And finally, the use of preg_replace_callback in CheckPost::checkContent seems a bit unnecessary because the replaced result is not used for anything. But it's still a valid way to loop over a regex. But then in the callback there's if ($matches) which will always be true, in fact if it wasn't, the next line $matches[0] would throw. If using a loop or throwable, the loop could be exited as soon as $isExplicit becomes true since the code doesn't perform any other check.

    Flarum core: 1.8.5
    PHP version: 8.1.27
    MySQL version: 10.4.31-MariaDB-1:10.4.31+mariaubu1804-log
    Loaded extensions: Core, date, libxml, openssl, pcre, zlib, dom, enchant, FFI, filter, gmp, hash, json, pcntl, Reflection, SPL, session, standard, mysqlnd, PDO, readline, xml, xmlreader, xmlwriter, bcmath, bz2, calendar, ctype, curl, mbstring, fileinfo, ftp, gd, gettext, iconv, igbinary, imap, intl, exif, memcache, memcached, msgpack, mysqli, pdo_mysql, pdo_sqlite, Phar, posix, pspell, realpath_turbo, redis, shmop, SimpleXML, soap, sockets, sodium, sqlite3, swoole, sysvmsg, sysvsem, sysvshm, tidy, timezonedb, tokenizer, xmlrpc, xsl, zip, Zend OPcache
    +--------------------------------+------------+--------+
    | Flarum Extensions | | |
    +--------------------------------+------------+--------+
    | ID | Version | Commit |
    +--------------------------------+------------+--------+
    | flarum-flags | v1.8.0 | |
    | flarum-tags | v1.8.0 | |
    | flarum-approval | v1.8.0 | |
    | flarum-suspend | v1.8.1 | |
    | flarum-mentions | v1.8.2 | |
    | flarum-subscriptions | v1.8.0 | |
    | fof-user-directory | 1.3.2 | |
    | fof-follow-tags | 1.2.2 | |
    | flarum-likes | v1.8.0 | |
    | afrux-forum-widgets-core | v0.1.7 | |
    | v17development-seo | v1.8.1 | |
    | fof-upload | 1.5.2 | |
    | fof-terms | 1.3.0 | |
    | fof-sitemap | 2.2.0 | |
    | fof-share-social | 1.1.2 | |
    | fof-polls | 2.1.2 | |
    | fof-links | 1.2.0 | |
    | fof-filter | 1.1.3 | |
    | fof-disposable-emails | 1.0.0 | |
    | fof-byobu | 1.3.6 | |
    | fof-best-answer | 1.4.2 | |
    | flarum-sticky | v1.8.0 | |
    | flarum-statistics | v1.8.0 | |
    | flarum-markdown | v1.8.0 | |
    | flarum-lock | v1.8.0 | |
    | flarum-lang-english | v1.8.0 | |
    | flarum-lang-chinese-simplified | dev-master | |
    | flarum-emoji | v1.8.0 | |
    | flarum-bbcode | v1.8.0 | |
    | datlechin-bbcode-hide-content | v0.1.5 | |
    | darkle-fancybox | 1.1.3 | |
    | clarkwinkelmann-mailing | 1.1.0 | |
    | askvortsov-moderator-warnings | v0.6.3 | |
    | antoinefr-money | v1.3.1 | |
    | afrux-news-widget | v0.1.1 | |
    +--------------------------------+------------+--------+

    meihuak the discussion has been posted. but in latest discussions there is no this discussion.

    in newest discussions, i can see it, like the picture above

    So this is the issue? I'm a bit confused so trying to get things cleared up.

    What change did you make to Flarum to allow empty post content? This should not be allowed by default and I don't see which extension in your list might do that.

    The underlying issue is a bit of an unknown behavior situation in Flarum. Flarum does not allow empty content value. But if it receives an empty value, it will set the column to null. At the same time, the post parser expects all posts of type comment to have a string content value. Meaning it can never work with empty content, but it's also not allowed so this situation is never encountered.

    So whatever code was written to allow an empty body should either force-store the empty content as string, or modify the signature of all extenders and implementations of those extenders in extensions to accept nullable comment content. The latter being nearly impossible, only the first solution is feasible.

    I see a lot more code has been rewritten in the upcoming 2.x release to require a string everywhere. Yet the CommentPost class still saves an empty content to null in the development branch. This should probably be changed, just for consistency even if Flarum still won't allow empty content by default. Because as we see here, if any null value ends up in the database (for a comment post type), it will break a lot of things.

    So, the next step to fix the issue on your forum is identifying which extension or local change allows empty post content. The red message should say The content field is required when you try to post.

      clarkwinkelmann exclude these extensions, I only changed the "Custom Header". but I remove the code in it and try to post a discussion without content, it happens again

      how can I change this word to tell the members that they must write a post to create a discussion

        meihuak you can use Linguist.

        But again, there should be a validation error. One of your extension or a local extender or file edit must have removed it.

        Got this tested on freeflarum (newly created)
        Flarum:1.8.
        PHP:8.3.6
        MySQL:11.1.4-MariaDB

        I only enabled fof-filter and was able to reproduce the error (all other extensions are left as default)

        Full log:

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

        Noted that if the user is able to Bypass word filter(s), for example in this case is the admin:


        The validation error shows up normally:

        Disregard my now-deleted reply. I found the issue.

        To reproduce you need only FoF Filter and any extension that implements an unparsing callback like Mentions. Then try posting with an account without the bypassFoFFilter permission.

        The problem isn't really caused by anything FoF Filter does. Really it's a Flarum issue.

        The code I referenced earlier, that sets null on the content attribute when passed an empty string actually always runs, then the Saving event is dispatched, and only after that does the Flarum validator runs.

        The error is thrown as soon as $post->content is invoked in the Saving event, because CommentPost::getContentAttribute passes the string|null content to the unparser, which expects string only.

        Even if the issue was fixed in Flarum, there are still potential issues I see with FoF Filter:

        Empty lines in the list of words don't seem to be skipped and they result in a censor rule that will match every time (//i).

        The email for flagged content is sent synchronously inside the Saving event, before the post is saved to the database, meaning it will still go out even if another extension throws a validation error from a different event lister or from the PostValidator itself.

        And finally, the use of preg_replace_callback in CheckPost::checkContent seems a bit unnecessary because the replaced result is not used for anything. But it's still a valid way to loop over a regex. But then in the callback there's if ($matches) which will always be true, in fact if it wasn't, the next line $matches[0] would throw. If using a loop or throwable, the loop could be exited as soon as $isExplicit becomes true since the code doesn't perform any other check.

          clarkwinkelmann The error is thrown as soon as $post->content is invoked in the Saving event, because CommentPost::getContentAttribute passes the string|null content to the unparser, which expects string only.

          I assumed this was the underlying issue, but looking at the rest of your post more concerns have popped up. Thank for your the in-depth research 👏

          13 days later

          when I use admin account to test, only write title, don't write content, flarum tell me "The content field is required."

          it is correct.

          so I think if there are some error in my permission setting?

            meihuak as I showed in my post above, it's actually a Flarum bug that will have to be fixed in Flarum.

            The only workaround I can think of would be to register an additional event listener, make sure it runs before Mentions and in that listener, manually throw a validation exception if the content is null (reading the raw value on the model, since the content accessor would cause the bug). But this can't be done with a local extender because of the priority. Another option only for Flarum 1.8 (would not work on the 2.x branch because the error would trigger upstream) would be to create a no-op unparser callback, have it accept any parameter, and throw the exception if null. This one could be inserted in front of the Mentions unparser using a service provider even if registered from the local extenders.