code:
<?php

// {{{ License

// 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/>.

// }}}

/**
 * Handle network public feed
 *
 * @package  GNUsocial
 * @category Controller
 *
 * @author    Hugo Sales <hugo@fc.up.pt>
 * @copyright 2020 Free Software Foundation, Inc http://www.fsf.org
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 */

namespace App\Controller;

use App\Core\Controller;
use App\Core\Form;
use function App\Core\I18n\_m;
use App\Util\Common;
use App\Util\Exceptiion\InvalidFormException;
use App\Util\Formatting;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\HttpFoundation\Request;

class AdminPanel extends Controller
{
    /**
     * Handler for the site admin panel section. Allows the
     * administrator to change various configuration options
     */
    public function site(Request $request)
    {
        // TODO CHECK PERMISSION
        $defaults = Common::getConfigDefaults();
        $options  = [];
        foreach ($defaults as $key => $inner) {
            $options[$key] = [];
            foreach (array_keys($inner) as $inner_key) {
                $options[_m($key)][_m($inner_key)] = "{$key}:{$inner_key}";
            }
        }

        $form = Form::create([
            ['setting', ChoiceType::class, ['label' => _m('Setting'), 'choices' => $options]],
            ['value',   TextType::class,   ['label' => _m('Value')]],
            ['save',    SubmitType::class, ['label' => _m('Set site setting')]],
        ]);

        $form->handleRequest($request);
        if ($form->isSubmitted()) {
            $data = $form->getData();
            if ($form->isValid() && array_key_exists('setting', $data)) {
                list($section, $setting) = explode(':', $data['setting']);
                $value                   = $data['value'];
                if (preg_match('/^[0-9]+$/', $value)) {
                    $value = (int) $value;
                } elseif (strstr($value, ',') === false) {
                    // empty, string
                } elseif (Formatting::toArray($value, $value)) {
                    // empty
                } elseif (preg_match('/true|false/i', $value)) {
                    $value = ($value == 'true');
                }

                $default = $defaults[$section][$setting];
                // Sanity check
                if (gettype($default) === gettype($value)) {
                    $old_value = Common::config($section, $setting);
                    Common::setConfig($section, $setting, $value);
                    return [
                        '_template' => 'config/admin.html.twig',
                        'form'      => $form->createView(),
                        'old_value' => Formatting::toString($old_value),
                        'default'   => Formatting::toString($default),
                    ];
                }
            } else {
                throw new InvalidFormException();
            }
        }

        return [
            '_template' => 'config/admin.html.twig',
            'form'      => $form->createView(),
        ];
    }
}


code:
<?php

// {{{ License

// 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/>.

// }}}

/**
 * Handle network public feed
 *
 * @package  GNUsocial
 * @category Controller
 *
 * @author    Hugo Sales <hugo@fc.up.pt>
 * @author    Eliseu Amaro <eliseu@fc.up.pt>
 * @copyright 2020 Free Software Foundation, Inc http://www.fsf.org
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 */

namespace App\Controller;

use App\Core\Controller;
use App\Core\DB\DB;
use function App\Core\I18n\_m;
use App\Core\NoteScope;
use App\Util\Common;
use App\Util\Exception\ClientException;
use Symfony\Component\HttpFoundation\Request;

class Network extends Controller
{
    // Can't have constanst inside herestring
    private $public_scope   = NoteScope::PUBLIC;
    private $instance_scope = NoteScope::PUBLIC | NoteScope::SITE;
    private $message_scope  = NoteScope::MESSAGE;
    private $follower_scope = NoteScope::PUBLIC | NoteScope::FOLLOWER;

    public function public(Request $request)
    {
        return [
            '_template' => 'network/public.html.twig',
            'notes'     => DB::sql('select * from note n ' .
                                   "where n.reply_to is null and (n.scope & {$this->instance_scope}) <> 0 " .
                                   'order by n.created DESC',
                                   ['n' => 'App\Entity\Note']),
        ];
    }

    public function home(Request $request, string $nickname)
    {
        $target = DB::findOneBy('gsactor', ['nickname' => $nickname]);
        if ($target == null) {
            throw new ClientException(_m('User {nickname} doesn\'t exist', ['{nickname}' => $nickname]));
        }

        $query = <<<END
        -- Select notes from:
        select note.* from note left join -- left join ensures all returned notes' ids are not null
        (
            -- Followed by target
            select n.id from note n inner join follow f on n.gsactor_id = f.followed
                where f.follower = :target_actor_id
            union all
            -- Replies to notes by target
            select n.id from note n inner join note nr on nr.id = nr.reply_to
            union all
            -- Notifications to target
            select a.activity_id from notification a inner join note n on a.activity_id = n.id
            union all
            -- Notes in groups target follows
            select gi.activity_id from group_inbox gi inner join group_member gm on gi.group_id = gm.group_id
                where gm.gsactor_id = :target_actor_id
        )
        as s on s.id = note.id
        where
            -- Remove direct messages
            note.scope <> {$this->message_scope}
        order by note.modified DESC
END;
        $notes = DB::sql($query, ['note' => 'App\Entity\Note'], ['target_actor_id' => $target->getId()]);

        return [
            '_template' => 'network/public.html.twig',
            'notes'     => $notes,
        ];
    }

    public function network(Request $request)
    {
        return [
            '_template' => 'network/public.html.twig',
            'notes'     => DB::dql('select n from App\Entity\Note n ' .
                                   "where n.scope = {$this->public_scope} " .
                                   'order by n.created DESC'
            ),
        ];
    }

    public function replies(Request $request)
    {
        $actor_id = Common::ensureLoggedIn()->getId();

        return [
            '_template' => 'network/public.html.twig',
            'notes'     => DB::dql('select n from App\Entity\Note n ' .
                                   'where n.reply_to is not null and n.gsactor_id = :id ' .
                                   'order by n.created DESC', ['id' => $actor_id]),
        ];
    }

    public function favourites(Request $request)
    {
        $actor_id = Common::ensureLoggedIn()->getId();

        return [
            '_template' => 'network/public.html.twig',
            'notes'     => DB::dql('select f from App\Entity\Favourite f ' .
                                   'where f.gsactor_id = :id ' .
                                   'order by f.created DESC', ['id' => $actor_id]),
        ];
    }
}

repo_name: ksuudilla/gnu

license: mit


code:
<?php

namespace App\Controller;

use App\Core\Controller;
use App\Core\DB\DB;
use App\Core\Form;
use function App\Core\I18n\_m;
use App\Entity\Follow;
use App\Entity\GSActor;
use App\Entity\LocalUser;
use App\Security\Authenticator;
use App\Security\EmailVerifier;
use app\Util\Common;
use App\Util\Nickname;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Guard\GuardAuthenticatorHandler;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank;

class Security extends Controller
{
    /**
     * Log a user in
     */
    public function login(AuthenticationUtils $authenticationUtils)
    {
        if ($this->getUser()) {
            return $this->redirectToRoute('main_all');
        }

        // get the login error if there is one
        $error = $authenticationUtils->getLastAuthenticationError();
        // last username entered by the user
        $last_username = $authenticationUtils->getLastUsername();

        return ['_template' => 'security/login.html.twig', 'last_username' => $last_username, 'error' => $error];
    }

    public function logout()
    {
        throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.');
    }

    /**
     * Register a user, making sure the nickname is not reserved and
     * possibly sending a confirmation email
     */
    public function register(Request $request,
                             EmailVerifier $email_verifier,
                             GuardAuthenticatorHandler $guard_handler,
                             Authenticator $authenticator)
    {
        $form = Form::create([
            ['nickname', TextType::class, [
                'label'       => _m('Nickname'),
                'constraints' => [new Length([
                    'min'        => 1,
                    'minMessage' => _m('Your password should be at least {{ limit }} characters long'),
                    'max'        => 64,
                    'maxMessage' => _m('Your password should be at most {{ limit }} characters long'), ]),
                ],
            ]],
            ['email', EmailType::class, ['label' => _m('Email')]],
            ['password', PasswordType::class, [
                'label'       => _m('Password'),
                'mapped'      => false,
                'constraints' => [
                    new NotBlank(['message' => _m('Please enter a password')]),
                    new Length(['min' => 6, 'minMessage' => _m('Your password should be at least {{ limit }} characters'), 'max' => 60]),
                ],
            ]],
            ['register', SubmitType::class, ['label' => _m('Register')]],
        ]);

        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            $data             = $form->getData();
            $data['password'] = $form->get('password')->getData();

            $valid_nickname = Nickname::isValid($data['nickname'], Nickname::CHECK_USED);
            if (!$valid_nickname) {
                throw new \Exception(_m('Invalid nickname'));
            }

            $actor = GSActor::create(['nickname' => $data['nickname']]);
            DB::persist($actor);
            DB::flush();
            $id   = $actor->getId();
            $user = LocalUser::create([
                'id'             => $id,
                'nickname'       => $data['nickname'],
                'outgoing_email' => $data['email'],
                'incoming_email' => $data['email'],
                'password'       => LocalUser::hashPassword($data['password']),
            ]);
            DB::persist($user);

            // generate a signed url and email it to the user
            if (Common::config('site', 'use_email')) {
                $email_verifier->sendEmailConfirmation(
                    'verify_email',
                    $user,
                    (new TemplatedEmail())
                    ->from(new Address(Common::config('site', 'email'), Common::config('site', 'nickname')))
                    ->to($user->getOutgoingEmail())
                    ->subject(_m('Please Confirm your Email'))
                    ->htmlTemplate('security/confirmation_email.html.twig')
                );
            } else {
                $user->setIsEmailVerified(true);
            }

            // Self follow
            $follow = Follow::create(['follower' => $id, 'followed' => $id]);
            DB::persist($follow);
            DB::flush();

            return $guard_handler->authenticateUserAndHandleSuccess(
                $user,
                $request,
                $authenticator,
                'main' // firewall name in security.yaml
            );
        }

        return [
            '_template'         => 'security/register.html.twig',
            'registration_form' => $form->createView(),
        ];
    }
}


code:
<?php

// {{{ License
// 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/>.
// }}}

namespace App\Controller;

use App\Core\Controller;
use Symfony\Component\HttpFoundation\Request;

class TemplateController extends Controller
{
    public function handle(Request $req)
    {
        return ['_template' => $req->get('template')];
    }
}


code:
<?php

// {{{ License

// 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/>.

// }}}

/**
 * Handle network public feed
 *
 * @package  GNUsocial
 * @category Controller
 *
 * @author    Hugo Sales <hugo@fc.up.pt>
 * @author    Eliseu Amaro <eliseu@fc.up.pt>
 * @copyright 2020 Free Software Foundation, Inc http://www.fsf.org
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 */

namespace App\Controller;

// {{{ Imports

use App\Core\DB\DB;
use App\Core\Event;
use App\Core\Form;
use function App\Core\I18n\_m;
use App\Core\Log;
use App\Entity\Avatar;
use App\Entity\File;
use App\Util\ClientException;
use App\Util\Common;
use App\Util\Form\ArrayTransformer;
use Component\Media\Media;
use Doctrine\DBAL\Types\Types;
use Exception;
use Functional as F;
use Misd\PhoneNumberBundle\Form\Type\PhoneNumberType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\LanguageType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\HttpFoundation\File\File as SymfonyFile;
use Symfony\Component\HttpFoundation\Request;

// }}} Imports

class UserPanel extends AbstractController
{
    /**
     * Local user personal information panel
     */
    public function personal_info(Request $request)
    {
        $user            = Common::user();
        $user            = $user->getActor();
        $extra           = ['self_tags' => $user->getSelfTags()];
        $form_definition = [
            ['nickname',  TextType::class,     ['label' => _m('Nickname'),  'required' => true,  'help' => _m('1-64 lowercase letters or numbers, no punctuation or spaces.')]],
            ['full_name', TextType::class,     ['label' => _m('Full Name'), 'required' => false, 'help' => _m('A full name is required, if empty it will be set to your nickname.')]],
            ['homepage',  TextType::class,     ['label' => _m('Homepage'),  'required' => false, 'help' => _m('URL of your homepage, blog, or profile on another site.')]],
            ['bio',       TextareaType::class, ['label' => _m('Bio'),       'required' => false, 'help' => _m('Describe yourself and your interests.')]],
            ['location',  TextType::class,     ['label' => _m('Location'),  'required' => false, 'help' => _m('Where you are, like "City, State (or Region), Country".')]],
            ['self_tags', TextType::class,     ['label' => _m('Self Tags'), 'required' => false, 'transformer' => ArrayTransformer::class, 'help' => _m('Tags for yourself (letters, numbers, -, ., and _), comma- or space-separated.')]],
            ['save',      SubmitType::class,   ['label' => _m('Save')]],
        ];
        $extra_step = function ($data, $extra_args) use ($user) { $user->setNickname($data['nickname']); };
        $form = Form::handle($form_definition, $request, $user, $extra, $extra_step, [['self_tags' => $extra['self_tags']]]);

        return ['_template' => 'settings/profile.html.twig', 'prof' => $form->createView()];
    }

    /**
     * Local user account information panel
     */
    public function account(Request $request)
    {
        $user            = Common::user();
        $form_definition = [
            ['outgoing_email',  TextType::class,        ['label' => _m('Outgoing email'), 'required' => true,  'help' => _m('Change the email we use to contact you')]],
            ['incoming_email',  TextType::class,        ['label' => _m('Incoming email'), 'required' => true,  'help' => _m('Change the email you use to contact us (for posting, for instance)')]],
            ['password',        TextType::class,        ['label' => _m('Password'),       'required' => false, 'help' => _m('Change your password'), 'attr' => ['placeholder' => '********']]],
            ['old_password',    TextType::class,        ['label' => _m('Old password'),   'required' => false, 'help' => _m('Enter your old password for verification'), 'attr' => ['placeholder' => '********']]],
            ['language',        LanguageType::class,    ['label' => _m('Language'),       'required' => false, 'help' => _m('Your preferred language')]],
            ['phone_number',    PhoneNumberType::class, ['label' => _m('Phone number'),   'required' => false, 'help' => _m('Your phone number'), 'data_class' => null]],
            ['save',            SubmitType::class,      ['label' => _m('Save')]],
        ];

        $form = Form::handle($form_definition, $request, $user);

        return ['_template' => 'settings/account.html.twig', 'acc' => $form->createView()];
    }

    /**
     * Local user avatar panel
     */
    public function avatar(Request $request)
    {
        $form = Form::create([
            ['avatar', FileType::class,   ['label' => _m('Avatar'), 'help' => _m('You can upload your personal avatar. The maximum file size is 2MB.')]],
            ['hidden', HiddenType::class, []],
            ['save',   SubmitType::class, ['label' => _m('Submit')]],
        ]);

        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            $data  = $form->getData();
            $sfile = null;
            if (isset($data['hidden'])) {
                // Cropped client side
                $matches = [];
                if (!empty(preg_match('/data:([^;]*)(;(base64))?,(.*)/', $data['hidden'], $matches))) {
                    list(, $mimetype_user, , $encoding_user, $data_user) = $matches;
                    if ($encoding_user == 'base64') {
                        $data_user = base64_decode($data_user);
                        $filename  = tempnam('/tmp/', 'avatar');
                        file_put_contents($filename, $data_user);
                        $sfile = new SymfonyFile($filename);
                    } else {
                        Log::info('Avatar upload got an invalid encoding, something\'s fishy and/or wrong');
                    }
                }
            } elseif (isset($data['avatar'])) {
                // Cropping failed (e.g. disabled js), have file as uploaded
                $sfile = $data['avatar'];
            } else {
                throw new ClientException('Invalid form');
            }
            $user     = Common::user();
            $actor_id = $user->getId();
            $file     = Media::validateAndStoreFile($sfile, Common::config('avatar', 'dir'), $title = null, $is_local = true, $use_unique = $actor_id);
            $old_file = null;
            $avatar   = DB::find('avatar', ['gsactor_id' => $actor_id]);
            // Must get old id before inserting another one
            if ($avatar != null) {
                $old_file = $avatar->delete();
            }
            DB::persist($file);
            // Can only get new id after inserting
            DB::flush();
            DB::persist(Avatar::create(['gsactor_id' => $actor_id, 'file_id' => $file->getId()]));
            DB::flush();
            // Only delete files if the commit went through
            if ($old_file != null) {
                @unlink($old_file);
            }
            Event::handle('DeleteCachedAvatar', [$user->getNickname()]);
        }

        return ['_template' => 'settings/avatar.html.twig', 'avatar' => $form->createView()];
    }

    /**
     * Local user notification settings tabbed panel
     */
    public function notifications(Request $request)
    {
        $schema    = DB::getConnection()->getSchemaManager();
        $platform  = $schema->getDatabasePlatform();
        $columns   = Common::arrayRemoveKeys($schema->listTableColumns('user_notification_prefs'), ['user_id', 'transport', 'created', 'modified']);
        $form_defs = ['placeholder' => []];
        foreach ($columns as $name => $col) {
            $type     = $col->getType();
            $val      = $type->convertToPHPValue($col->getDefault(), $platform);
            $type_str = lcfirst(substr((string) $type, 1));
            $label    = str_replace('_', ' ', ucfirst($name));

            $labels = [
                'target_actor_id' => 'Target Actors',
                'dm'              => 'DM',
            ];

            $help = [
                'target_actor_id'       => 'If specified, these settings apply only to these profiles (comma- or space-separated list)',
                'activity_by_followed'  => 'Notify me when someone I follow has new activity',
                'mention'               => 'Notify me when mentions me in a notice',
                'reply'                 => 'Notify me when someone replies to a notice made by me',
                'follow'                => 'Notify me when someone follows me or asks for permission to do so',
                'favorite'              => 'Notify me when someone favorites one of my notices',
                'nudge'                 => 'Notify me when someone nudges me',
                'dm'                    => 'Notify me when someone sends me a direct message',
                'post_on_status_change' => 'Post a notice when my status in this service changes',
                'enable_posting'        => 'Enable posting from this service',
            ];

            switch ($type_str) {
            case Types::BOOLEAN:
                $form_defs['placeholder'][$name] = [$name, CheckboxType::class, ['data' => $val, 'label' => _m($labels[$name] ?? $label), 'help' => _m($help[$name])]];
                break;
            case Types::INTEGER:
                if ($name == 'target_actor_id') {
                    $form_defs['placeholder'][$name] = ['target_actors', TextType::class, ['data' => $val, 'label' => _m($labels[$name]), 'help' => _m($help[$name])], 'transformer' => ActorArrayTransformer::class];
                }
                break;
            default:
                dd($type_str);
                throw new Exception("Structure of table user_notification_prefs changed in a way not accounted to in notification settings ({$name})", 500);
            }
        }

        Event::handle('AddNotificationTransport', [&$form_defs]);
        unset($form_defs['placeholder']);

        $tabbed_forms = [];
        foreach ($form_defs as $transport_name => $f) {
            $tabbed_forms[$transport_name] = Form::create($f);
        }

        $tabbed_forms = F\map($tabbed_forms, function ($f) { return $f->createView(); });
        return [
            '_template'    => 'settings/notifications.html.twig',
            'tabbed_forms' => $tabbed_forms,
        ];
    }
}


code:
<?php

// {{{ License

// 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/>.

// }}}

namespace App\Core;

use App\Util\Common;
use Functional as F;
use Redis;
use RedisCluster;
use SplFixedArray;
use Symfony\Component\Cache\Adapter;
use Symfony\Component\Cache\Adapter\ChainAdapter;

abstract class Cache
{
    protected static $pools;
    protected static $redis;

    /**
     * Configure a cache pool, with adapters taken from `ENV_VAR`.
     * We may want multiple of these in the future, but for now it seems
     * unnecessary
     */
    public static function setupCache()
    {
        self::$pools = [];
        self::$redis = [];

        $adapters = [];
        foreach (Common::config('cache', 'adapters') as $pool => $val) {
            self::$pools[$pool] = [];
            self::$redis[$pool] = [];
            foreach (explode(',', $val) as $dsn) {
                list($scheme, $rest) = explode('://', $dsn);
                $partial_to_dsn      = function ($r) use ($scheme) { return $scheme . '://' . $r; };
                switch ($scheme) {
                case 'redis':
                    // Redis can have multiple servers, but we want to take proper advantage of
                    // redis, not just as a key value store, but using it's datastructures
                    $dsns = explode(',', $rest);
                    if (count($dsns) === 1) {
                        $class = Redis::class;
                        $r     = new Redis();
                        if ($rest[0] != '/' && strstr($rest, ':') != false) {
                            list($host, $port) = explode(':', $rest);
                            $r->pconnect($host, $port);
                        } else {
                            $r->pconnect($rest);
                        }
                    } else {
                        $class = RedisCluster::class; // true for persistent connection
                        $r     = new RedisCluster(null, $dsns, null, null, true);
                        // Distribute reads randomly
                        $r->setOption($class::OPT_SLAVE_FAILOVER, $class::FAILOVER_DISTRIBUTE);
                    }
                    // Improved serializer
                    $r->setOption($class::OPT_SERIALIZER, $class::SERIALIZER_MSGPACK);
                    // Persistent connection
                    $r->setOption($class::OPT_TCP_KEEPALIVE, true);
                    // Use LZ4 for the improved decompression speed (while keeping an okay compression ratio)
                    $r->setOption($class::OPT_COMPRESSION, $class::COMPRESSION_LZ4);
                    self::$redis[$pool] = $r;
                    $adapters[$pool][]  = new Adapter\RedisAdapter($r);
                    break;
                case 'memcached':
                    // memcached can also have multiple servers
                    $dsns              = F\map(explode(',', $rest), $partial_to_dsn);
                    $adapters[$pool][] = new Adapter\MemcachedAdapter($dsns);
                    break;
                case 'filesystem':
                    $adapters[$pool][] = new Adapter\FilesystemAdapter($rest);
                    break;
                case 'apcu':
                    $adapters[$pool][] = new Adapter\ApcuAdapter();
                    break;
                case 'opcache':
                    $adapters[$pool][] = new Adapter\PhpArrayAdapter($rest, new FilesystemAdapter($rest . '.fallback'));
                    break;
                case 'doctrine':
                    $adapters[$pool][] = new Adapter\PdoAdapter($dsn);
                    break;
                default:
                    Log::error("Unknown or discouraged cache scheme '{$scheme}'");
                    return;
                }
            }

            if (count($adapters[$pool]) === 1) {
                self::$pools[$pool] = array_pop($adapters[$pool]);
            } else {
                self::$pools[$pool] = new ChainAdapter($adapters);
            }
        }
    }

    public static function set(string $key, mixed $value, string $pool = 'default')
    {
        // there's no set method, must be done this way
        return self::$pools[$pool]->get($key, function ($i) use ($value) { return $value; }, INF);
    }

    public static function get(string $key, callable $calculate, string $pool = 'default', float $beta = 1.0)
    {
        return self::$pools[$pool]->get($key, $calculate, $beta);
    }

    public static function delete(string $key, string $pool = 'default'): bool
    {
        return self::$pools[$pool]->delete($key);
    }

    /**
     * Retrieve a list from the cache, with a different implementation
     * for redis and others, trimming to $max_count if given
     */
    public static function getList(string $key, callable $calculate, string $pool = 'default', int $max_count = -1, float $beta = 1.0): array
    {
        if (isset(self::$redis[$pool])) {
            if (!($recompute = $beta === INF || !(self::$redis[$pool]->exists($key)))) {
                if (is_float($er = Common::config('cache', 'early_recompute'))) {
                    $recompute = (mt_rand() / mt_getrandmax() > $er);
                    Log::info('Item "{key}" elected for early recomputation', ['key' => $key]);
                } else {
                    if ($recompute = ($idletime = self::$redis[$pool]->object('idletime', $key) ?? false) && ($expiry = self::$redis[$pool]->ttl($key) ?? false) && $expiry <= $idletime / 1000 * $beta * log(random_int(1, PHP_INT_MAX) / PHP_INT_MAX)) {
                        Log::info('Item "{key}" elected for early recomputation {delta}s before its expiration', [
                            'key'   => $key,
                            'delta' => sprintf('%.1f', $expiry - microtime(true)),
                        ]);
                    }
                }
            }
            if ($recompute) {
                $save = true; // Pass by reference
                $res  = $calculate(null, $save);
                if ($save) {
                    self::$redis[$pool]->del($key);
                    self::$redis[$pool]->lPush($key, ...$res);
                }
            }
            self::$redis[$pool]->lTrim($key, 0, $max_count);
            return self::$redis[$pool]->lRange($key, 0, $max_count);
        } else {
            $keys = self::getKeyList($key, $max_count, $beta);
            $list = new SplFixedArray($keys->count());
            foreach ($keys as $k) {
                $list[] = self::get($k, $calculate, $pool, $beta);
            }
            return $list->toArray();
        }
    }

    /**
     * Push a value to the list, if not using redis, get, add to subkey and set
     */
    public static function pushList(string $key, mixed $value, string $pool = 'default', int $max_count = 64, float $beta = 1.0): void
    {
        if (isset(self::$redis[$pool])) {
            self::$redis[$pool]
                // doesn't need to be atomic, adding at one end, deleting at the other
                ->multi(Redis::PIPELINE)
                ->lPush($key, $value)
                // trim to $max_count, unless it's 0
                ->lTrim($key, 0, $max_count != 0 ? $max_count : -1)
                ->exec();
        } else {
            $keys = self::getKeyList($key, $max_count, $beta);
            $vkey = $key . ':' . count($keys);
            self::set($vkey, $value);
            $keys[] = $vkey;
            self::set($key, $keys);
        }
    }

    /**
     * Delete a whole list at $key, if not using redis, recurse into keys
     */
    public static function deleteList(string $key, string $pool = 'default'): bool
    {
        if (isset(self::$redis[$pool])) {
            return self::$redis[$pool]->del($key) == 1;
        } else {
            $keys = self::getKeyList($key, $max_count, $beta);
            if (!F\every($keys, function ($k) use ($pool) { return self::delete($k, $pool); })) {
                Log::warning("Some element of the list associated with {$key} was not deleted. There may be some memory leakage in the cache process");
            }
            return self::delete($key, $pool);
        }
    }

    /**
     * On non-Redis, get the list of keys that store a list at $key
     */
    private static function getKeyList(string $key, int $max_count, string $pool, float $beta): RingBuffer
    {
        // Get the current keys associated with a list. If the cache
        // is not primed, the function is called and returns an empty
        // ring buffer
        return self::get($key,
                         function (ItemInterface $i) use ($max_count) {
                             return new RingBuffer($max_count);
                         }, $pool, $beta);
    }
}


code:
<?php

// {{{ License
// 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/>.
// }}}

/**
 * Base class for controllers
 *
 * @package  GNUsocial
 * @category Controller
 *
 * @author    Hugo Sales <hugo@fc.up.pt>
 * @copyright 2020 Free Software Foundation, Inc http://www.fsf.org
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 */

namespace App\Core;

use App\Util\Common;
use App\Util\Exception\RedirectException;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\ControllerEvent;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Event\ViewEvent;
use Symfony\Component\HttpKernel\KernelEvents;

class Controller extends AbstractController implements EventSubscriberInterface
{
    private array $vars = [];

    public function __invoke(Request $request)
    {
        $class  = get_called_class();
        $method = 'on' . ucfirst(strtolower($request->getMethod()));
        if (method_exists($class, $method)) {
            return $class::$method($request, $this->vars);
        } else {
            return $class::handle($request, $this->vars);
        }
    }

    /**
     * Symfony event when it's searching for which controller to use
     */
    public function onKernelController(ControllerEvent $event)
    {
        $controller = $event->getController();
        $request    = $event->getRequest();

        $this->vars = ['controler' => $controller, 'request' => $request, 'have_user' => Common::user() !== null];
        Event::handle('StartTwigPopulateVars', [&$this->vars]);

        $event->stopPropagation();
        return $event;
    }

    /**
     * Symfony event when the controller result is not a Response object
     */
    public function onKernelView(ViewEvent $event)
    {
        $request  = $event->getRequest();
        $response = $event->getControllerResult();
        if (!is_array($response)) {
            return $event;
        }

        $this->vars = array_merge_recursive($this->vars, $response);
        Event::handle('EndTwigPopulateVars', [&$this->vars]);

        $template = $this->vars['_template'];
        unset($this->vars['_template'], $this->vars['request']);

        // Respond in the the most preffered acceptable content type
        $format = $request->getFormat($request->getAcceptableContentTypes()[0]);
        switch ($format) {
        case 'html':
            $event->setResponse($this->render($template, $this->vars));
            break;
        case 'json':
            $event->setResponse(new JsonResponse($this->vars));
            // no break
        default:
            throw new BadRequestHttpException('Unsupported format', null, 406);
        }

        return $event;
    }

    /**
     * Symfony event when the controller throws an exception
     */
    public function onKernelException(ExceptionEvent $event)
    {
        $except = $event->getThrowable();
        do {
            if ($except instanceof RedirectException) {
                if (($redir = $except->redirect_response) != null) {
                    $event->setResponse($redir);
                } else {
                    $event->setResponse(new RedirectResponse($event->getRequest()->getPathInfo()));
                }
            }
        } while ($except != null && ($except = $except->getPrevious()) != null);
        return $event;
    }

    public static function getSubscribedEvents()
    {
        return [
            KernelEvents::CONTROLLER => 'onKernelController',
            KernelEvents::EXCEPTION  => 'onKernelException',
            KernelEvents::VIEW       => 'onKernelView',
        ];
    }
}


code:
<?php

// {{{ License

// 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/>.

// }}}

/**
 * Doctrine entity manager static wrapper
 *
 * @package GNUsocial
 * @category DB
 *
 * @author    Hugo Sales <hugo@fc.up.pt>
 * @copyright 2020 Free Software Foundation, Inc http://www.fsf.org
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 */

namespace App\Core\DB;

use App\Util\Exception\NotFoundException;
use App\Util\Formatting;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\ExpressionBuilder;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Query;
use Doctrine\ORM\Query\ResultSetMappingBuilder;

abstract class DB
{
    private static ?EntityManagerInterface $em;
    public static function setManager($m): void
    {
        self::$em = $m;
    }

    /**
     * Perform a Doctrine Query Language query
     */
    public static function dql(string $query, array $params = [])
    {
        $q = new Query(self::$em);
        $q->setDQL($query);
        foreach ($params as $k => $v) {
            $q->setParameter($k, $v);
        }
        return $q->getResult();
    }

    /**
     * Perform a native, parameterized, SQL query. $entities is a map
     * from table aliases to class names. Replaces '{select}' in
     * $query with the appropriate select list
     */
    public static function sql(string $query, array $entities, array $params = [])
    {
        $rsm = new ResultSetMappingBuilder(self::$em);
        foreach ($entities as $alias => $entity) {
            $rsm->addRootEntityFromClassMetadata($entity, $alias);
        }
        $query = preg_replace('/{select}/', $rsm->generateSelectClause(), $query);
        $q     = self::$em->createNativeQuery($query, $rsm);
        foreach ($params as $k => $v) {
            $q->setParameter($k, $v);
        }
        return $q->getResult();
    }

    /**
     * A list of possible operations needed in self::buildExpression
     */
    private static array $find_by_ops = [
        'or', 'and', 'eq', 'neq', 'lt', 'lte',
        'gt', 'gte', 'is_null', 'in', 'not_in',
        'contains', 'member_of', 'starts_with', 'ends_with',
    ];

    /**
     * Build a Doctrine Criteria expression from the given $criteria.
     *
     * @see self::findBy for the syntax
     */
    private static function buildExpression(ExpressionBuilder $eb, array $criteria)
    {
        $expressions = [];
        foreach ($criteria as $op => $exp) {
            if ($op == 'or' || $op == 'and') {
                $method = "{$op}X";
                return $eb->{$method}(...self::buildExpression($eb, $exp));
            } elseif ($op == 'is_null') {
                $expressions[] = $eb->isNull($exp);
            } else {
                if (in_array($op, self::$find_by_ops)) {
                    $method        = Formatting::snakeCaseToCamelCase($op);
                    $expressions[] = $eb->{$method}(...$exp);
                } else {
                    $expressions[] = $eb->eq($op, $exp);
                }
            }
        }

        return $expressions;
    }

    /**
     * Query $table according to $criteria. If $criteria's keys are
     * one of self::$find_by_ops (and, or, etc), build a subexpression
     * with that operator and recurse. Examples of $criteria are
     * `['and' => ['lt' => ['foo' => 4], 'gte' => ['bar' => 2]]]` or
     * `['in' => ['foo', 'bar']]`
     */
    public static function findBy(string $table, array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
    {
        $criteria = array_change_key_case($criteria);
        $ops      = array_intersect(array_keys($criteria), self::$find_by_ops);
        $repo     = self::getRepository($table);
        if (empty($ops)) {
            return $repo->findBy($criteria, $orderBy, $limit, $offset);
        } else {
            $criteria = new Criteria(self::buildExpression(Criteria::expr(), $criteria), $orderBy, $offset, $limit);
            return $repo->matching($criteria)->toArray(); // Always work with array or it becomes really complicated
        }
    }

    /**
     * Return the first element of the result of @see self::findBy
     */
    public static function findOneBy(string $table, array $criteria, ?array $orderBy = null, ?int $offset = null)
    {
        $res = self::findBy($table, $criteria, $orderBy, 1, $offset);
        if (count($res) == 1) {
            return $res[0];
        } else {
            throw new NotFoundException("No value in table {$table} matches the requested criteria");
        }
    }

    /**
     * Intercept static function calls to allow refering to entities
     * without writing the namespace (which is deduced from the call
     * context)
     */
    public static function __callStatic(string $name, array $args)
    {
        // TODO Plugins
        // If the method is one of the following and the first argument doesn't look like a FQCN, add the prefix
        $pref = '\App\Entity\\';
        if (in_array($name, ['find', 'getReference', 'getPartialReference', 'getRepository'])
            && preg_match('/\\\\/', $args[0]) === 0
            && Formatting::startsWith($args[0], $pref) === false) {
            $args[0] = $pref . ucfirst(Formatting::snakeCaseToCamelCase($args[0]));
            $args[0] = preg_replace('/Gsactor/', 'GSActor', $args[0]);
        }

        return self::$em->{$name}(...$args);
    }
}


code:
<?php

// {{{ License
// 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/>.
// }}}

/**
 * Doctrine metadata driver which implements our old `schemaDef` interface
 *
 * @package GNUsocial
 * @category DB
 *
 * @author    Hugo Sales <hugo@fc.up.pt>
 * @copyright 2020 Free Software Foundation, Inc http://www.fsf.org
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 */

namespace App\Core\DB;

use Doctrine\Persistence\Mapping\ClassMetadata;
use Doctrine\Persistence\Mapping\Driver\StaticPHPDriver;
use Functional as F;

class SchemaDefDriver extends StaticPHPDriver
{
    /**
     * V2 DB type => Doctrine type
     */
    private const types = [
        'varchar'      => 'string',
        'char'         => 'string', // char is a fixed witdh varchar
        'int'          => 'integer',
        'serial'       => 'integer',
        'tinyint'      => 'smallint', // no portable tinyint
        'bigint'       => 'bigint',
        'bool'         => 'boolean',
        'numeric'      => 'decimal',
        'text'         => 'text',
        'datetime'     => 'datetime',
        'timestamp'    => 'datetime',
        'phone_number' => 'phone_number',
        // Unused in V2, but might start being used
        'date'        => 'date',
        'time'        => 'time',
        'datetimez'   => 'datetimez',
        'object'      => 'object',
        'array'       => 'array',
        'simplearray' => 'simplearray',
        'json_array'  => 'json_array',
        'float'       => 'float',
        'guid'        => 'guid',
        'blob'        => 'blob',
    ];

    /**
     * Fill in the database $metadata for $class_name
     *
     * @param string        $class_name
     * @param ClassMetadata $metadata
     */
    public function loadMetadataForClass($class_name, ClassMetadata $metadata)
    {
        $schema = $class_name::schemaDef();

        $metadata->setPrimaryTable([
            'name'              => $schema['name'],
            'indexes'           => self::kv_to_name_col($schema['indexes'] ?? []),
            'uniqueConstraints' => self::kv_to_name_col($schema['unique keys'] ?? []),
            'options'           => ['comment' => $schema['description'] ?? ''],
        ]);

        foreach ($schema['fields'] as $name => $opts) {
            // TODO
            // Convert old to new types
            $type = $name === 'date'
                  // Old date fields were stored as int, store as datetime/timestamp
                  ? 'datetime'
                  // For ints, prepend the size (smallint)
                  // The size field doesn't exist otherwise
                  :
                  self::types[($opts['size'] ?? '') . $opts['type']];

            $unique = null;
            foreach ($schema['unique keys'] ?? [] as $key => $uniq_arr) {
                if (in_array($name, $uniq_arr)) {
                    $unique = $key;
                    break;
                }
            }

            $default = $opts['default'] ?? null;

            $field = [
                // boolean, optional
                'id' => in_array($name, $schema['primary key']),
                // string
                'fieldName' => $name,
                // string
                'type' => $type,
                // stringn, optional
                'unique' => $unique,
                // String length, ignored if not a string
                // int, optional
                'length' => $opts['length'] ?? null,
                // boolean, optional
                'nullable' => !($opts['not null'] ?? false),
                // Numeric precision and scale, ignored if not a number
                // integer, optional
                'precision' => $opts['precision'] ?? null,
                // integer, optional
                'scale'   => $opts['scale'] ?? null,
                'options' => [
                    'comment'  => $opts['description'] ?? null,
                    'default'  => $default,
                    'unsigned' => $opts['unsigned'] ?? null,
                    // bool, optional
                    'fixed' => $opts['type'] === 'char',
                    // 'collation' => string, unused
                    // 'check', unused
                ],
                // 'columnDefinition', unused
            ];
            // The optional feilds from earlier were populated with null, remove them
            $field            = array_filter($field, F\not('is_null'));
            $field['options'] = array_filter($field['options'], F\not('is_null'));

            $metadata->mapField($field);
            if ($opts['type'] === 'serial') {
                $metadata->setIdGeneratorType($metadata::GENERATOR_TYPE_AUTO);
            }
        }
        // TODO foreign keys
    }

    /**
     * Override StaticPHPDriver's method,
     * we care about classes that have the method `schemaDef`,
     * instead of `loadMetadata`.
     *
     * @param string $class_name
     *
     * @return bool
     */
    public function isTransient($class_name)
    {
        return !method_exists($class_name, 'schemaDef');
    }

    /**
     * Convert [$key => $val] to ['name' => $key, 'columns' => $val]
     *
     * @param array $arr
     *
     * @return array
     */
    private static function kv_to_name_col(array $arr): array
    {
        $res = [];
        foreach ($arr as $name => $cols) {
            $res[] = ['name' => $name, 'columns' => $cols];
        }
        return $res;
    }
}


code:
<?php

// {{{ License

// 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/>.

// }}}

namespace App\Core;

use App\Core\DB\DB;
use App\Util\Formatting;
use DateTime;

/**
 * Base class to all entities, with some utilities
 */
abstract class Entity
{
    /**
     * Create an instance of the called class or fill in the
     * properties of $obj with the associative array $args. Doesn't
     * persist the result
     *
     * @param null|mixed $obj
     */
    public static function create(array $args, $obj = null)
    {
        $class           = get_called_class();
        $obj             = $obj ?: new $class();
        $args['created'] = $args['modified'] = new DateTime();
        foreach ($args as $prop => $val) {
            if (property_exists($class, $prop) && $val != null) {
                $set = 'set' . Formatting::snakeCaseToCamelCase($prop);
                $obj->{$set}($val);
            } else {
                Log::error("Property {$class}::{$prop} doesn't exist");
            }
        }
        return $obj;
    }

    /**
     * Create a new instance, but check for duplicates
     */
    public static function createOrUpdate(array $args, array $find_by)
    {
        $table = Formatting::camelCaseToSnakeCase(get_called_class());
        return self::create($args, DB::findOneBy($table, $find_by));
    }

    /**
     * Remove a given $obj or whatever is found by `DB::findBy(..., $args)`
     * from the database. Doesn't flush
     *
     * @param null|mixed $obj
     */
    public static function remove(array $args, $obj = null)
    {
        $class = '\\' . get_called_class();
        if ($obj == null) {
            $obj = DB::findBy($class, $args);
        }
        DB::remove($obj);
    }
}


code:
<?php

// {{{ License
// 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/>.
// }}}

/**
 * GNU social's event handler wrapper around Symfony's,
 * keeping our old interface, which is more convenient and just as powerful
 *
 * @package GNUsocial
 * @category Event
 *
 * @author    Hugo Sales <hugo@fc.up.pt>
 * @copyright 2020 Free Software Foundation, Inc http://www.fsf.org
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 */

namespace App\Core;

use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\GenericEvent;

abstract class Event
{
    /**
     * Constants to be returned from event handlers, for increased clarity
     *
     * bool stop - Stop other handlers from processing the event
     * bool next - Allow the other handlers to process the event
     */
    public const stop = false;
    public const next = true;

    private static EventDispatcherInterface $dispatcher;

    public static function setDispatcher(EventDispatcherInterface $dis): void
    {
        self::$dispatcher = $dis;
    }

    /**
     * Add an event handler
     *
     * To run some code at a particular point in GNU social processing.
     * Named events include receiving an XMPP message, adding a new notice,
     * or showing part of an HTML page.
     *
     * The arguments to the handler vary by the event. Handlers can return
     * two possible values: false means that the event has been replaced by
     * the handler completely, and no default processing should be done.
     * Non-false means successful handling, and that the default processing
     * should succeed. (Note that this only makes sense for some events.)
     *
     * Handlers can also abort processing by throwing an exception; these will
     * be caught by the closest code and displayed as errors.
     *
     * @param string   $name     Name of the event
     * @param callable $handler  Code to run
     * @param int      $priority Higher runs first
     * @param string   $ns
     *
     * @return void
     */
    public static function addHandler(string $name,
                                      callable $handler,
                                      int $priority = 0,
                                      string $ns = 'GNUsocial.'): void
    {
        self::$dispatcher->addListener(
            $ns . $name,
            function ($event, $event_name, $dispatcher) use ($handler) {
                // Old style of events (preferred)
                if ($event instanceof GenericEvent) {
                    if (call_user_func_array($handler, $event->getArguments()) == self::stop) {
                        $event->stopPropagation();
                    }
                    return $event;
                }
                // Symfony style of events
                call_user_func($handler, $event, $event_name, $dispatcher);

                return null;
            },
            $priority
        );
    }

    /**
     * Handle an event
     *
     * Events are any point in the code that we want to expose for admins
     * or third-party developers to use.
     *
     * We pass in an array of arguments (including references, for stuff
     * that can be changed), and each assigned handler gets run with those
     * arguments. Exceptions can be thrown to indicate an error.
     *
     * @param string $name Name of the event that's happening
     * @param array  $args Arguments for handlers
     * @param string $ns   Namspace for the event
     *
     * @return bool flag saying whether to continue processing, based
     *              on results of handlers.
     */
    public static function handle(string $name, array $args = [], string $ns = 'GNUsocial.'): bool
    {
        return !(self::$dispatcher->dispatch(new GenericEvent($name, $args), $ns . $name)->isPropagationStopped());
    }

    /**
     * Check to see if an event handler exists
     *
     * Look to see if there's any handler for a given event, or narrow
     * by providing the name of a specific plugin class.
     *
     * @param string $name   Name of the event to look for
     * @param string $plugin Optional name of the plugin class to look for
     *
     * @return bool flag saying whether such a handler exists
     */
    public static function hasHandler(string $name, ?string $plugin = null): bool
    {
        $listeners = self::$dispatcher->getListeners($name);
        if (isset($plugin)) {
            foreach ($listeners as $handler) {
                if (get_class($handler[0]) == $plugin) {
                    return true;
                }
            }
        } else {
            return !empty($listeners);
        }
        return false;
    }

    /**
     * Get the array of handlers for $name
     *
     * @param string $name Name of event
     *
     * @return array
     * @return array
     */
    public static function getHandlers(string $name): array
    {
        return self::$dispatcher->getListeners($name);
    }
}


code:
<?php

// {{{ License
// 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/>.
// }}}

/**
 * Handle network public feed
 *
 * @package  GNUsocial
 * @category Wrapper
 *
 * @author    Hugo Sales <hugo@fc.up.pt>
 * @copyright 2020 Free Software Foundation, Inc http://www.fsf.org
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 */

namespace App\Core;

use App\Core\DB\DB;
use App\Util\Formatting;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Form as SymfForm;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\Request;

abstract class Form
{
    private static ?FormFactoryInterface $form_factory;

    public static function setFactory($ff): void
    {
        self::$form_factory = $ff;
    }

    /**
     * Create a form with the given associative array $form as fields
     */
    public static function create(array $form,
                                  ?object $target = null,
                                  array $extra_data = [],
                                  string $type = 'Symfony\Component\Form\Extension\Core\Type\FormType',
                                  array $builder_options = []): SymfForm
    {
        $name = $form[array_key_last($form)][0];
        $fb   = self::$form_factory->createNamedBuilder($name, $type, array_merge($builder_options, ['translation_domain' => false]));
        foreach ($form as list($key, $class, $options)) {
            if ($target != null && empty($options['data']) && (strstr($key, 'password') == false) && $class != SubmitType::class) {
                if (isset($extra_data[$key])) {
                    $options['data'] = $extra_data[$key];
                } else {
                    $method = 'get' . ucfirst(Formatting::snakeCaseToCamelCase($key));
                    if (method_exists($target, $method)) {
                        $options['data'] = $target->{$method}();
                    }
                }
            }
            unset($options['hide']);
            if (isset($options['transformer'])) {
                $transformer = $options['transformer'];
                unset($options['transformer']);
            }
            $fb->add($key, $class, $options);
            if (isset($transformer)) {
                $fb->get($key)->addModelTransformer(new $transformer());
                unset($transformer);
            }
        }
        return $fb->getForm();
    }

    /**
     * Whether the given $field of $form has the `required` property
     * set, defaults to true
     */
    public static function isRequired(array $form, string $field): bool
    {
        return $form[$field][2]['required'] ?? true;
    }

    /**
     * Handle the full life cycle of a form. Creates it with @see
     * self::create and inserts the submitted values into the database
     */
    public static function handle(array $form_definition, Request $request, object $target, array $extra_args = [], ?callable $extra_step = null, array $create_args = [])
    {
        $form = self::create($form_definition, $target, ...$create_args);

        $form->handleRequest($request);
        if ($form->isSubmitted() && $form->isValid()) {
            $data = $form->getData();
            if ($target == null) {
                return $data;
            }
            unset($data['translation_domain'], $data['save']);
            foreach ($data as $key => $val) {
                $method = 'set' . ucfirst(Formatting::snakeCaseToCamelCase($key));
                if (method_exists($target, $method)) {
                    if (isset($extra_args[$key])) {
                        $target->{$method}($val, $extra_args[$key]);
                    } else {
                        $target->{$method}($val);
                    }
                }
            }
            if (isset($extra_step)) {
                $extra_step($data, $extra_args);
            }
            DB::flush();
        }
        return $form;
    }
}


code:
<?php

// {{{ License

// 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/>.

// }}}

/**
 * Main GNU social entry point
 *
 * @package   GNUsocial
 * @category  Framework
 *
 * StatusNet and GNU social 1
 *
 * @author    Refer to CREDITS.md
 * @copyright 2010 Free Software Foundation, Inc http://www.fsf.org
 *
 * GNU social 2
 * @author    Bruno Casteleiro <brunoccast@fc.up.pt>
 * @author    Diogo Cordeiro <diogo@fc.up.pt>
 *
 * GNU social 3
 * @author    Hugo Sales <hugo@fc.up.pt>
 * @copyright 2018-2020 Free Software Foundation, Inc http://www.fsf.org
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 */

namespace App\Core;

use App\Core\DB\DB;
use App\Core\I18n\I18n;
use App\Core\Queue\Queue;
use App\Core\Router\Router;
use App\Util\Common;
use Doctrine\ORM\EntityManagerInterface;
use HtmlSanitizer\SanitizerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Event\ConsoleCommandEvent;
use Symfony\Component\DependencyInjection\ParameterBag\ContainerBagInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Security as SSecurity;
use Symfony\Component\Security\Http\Util\TargetPathTrait;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\Translation\TranslatorInterface;

class GNUsocial implements EventSubscriberInterface
{
    use TargetPathTrait;

    protected bool                     $initialized = false;
    protected LoggerInterface          $logger;
    protected TranslatorInterface      $translator;
    protected EntityManagerInterface   $entity_manager;
    protected RouterInterface          $router;
    protected UrlGeneratorInterface    $url_generator;
    protected FormFactoryInterface     $form_factory;
    protected MessageBusInterface      $message_bus;
    protected EventDispatcherInterface $event_dispatcher;
    protected SessionInterface         $session;
    protected SSecurity                $security;
    protected ModuleManager            $module_manager;
    protected HttpClientInterface      $client;
    protected SanitizerInterface       $sanitizer;
    protected ContainerBagInterface    $config;

    /**
     * Symfony dependency injection gives us access to these services
     */
    public function __construct(LoggerInterface $logger,
                                TranslatorInterface $trans,
                                EntityManagerInterface $em,
                                RouterInterface $router,
                                UrlGeneratorInterface $url_gen,
                                FormFactoryInterface $ff,
                                MessageBusInterface $mb,
                                EventDispatcherInterface $ed,
                                SessionInterface $sess,
                                SSecurity $sec,
                                ModuleManager $mm,
                                HttpClientInterface $cl,
                                SanitizerInterface $san,
                                ContainerBagInterface $conf)
    {
        $this->logger           = $logger;
        $this->translator       = $trans;
        $this->entity_manager   = $em;
        $this->router           = $router;
        $this->url_generator    = $url_gen;
        $this->form_factory     = $ff;
        $this->message_bus      = $mb;
        $this->event_dispatcher = $ed;
        $this->session          = $sess;
        $this->security         = $sec;
        $this->module_manager   = $mm;
        $this->client           = $cl;
        $this->saniter          = $san;
        $this->config           = $conf;

        $this->initialize();
    }

    /**
     * Store these services to be accessed statically and load modules
     *
     * @param EventDispatcherInterface $event_dispatcher
     */
    public function initialize(): void
    {
        if (!$this->initialized) {
            Common::setupConfig($this->config);
            Log::setLogger($this->logger);
            Event::setDispatcher($this->event_dispatcher);
            I18n::setTranslator($this->translator);
            DB::setManager($this->entity_manager);
            Form::setFactory($this->form_factory);
            Queue::setMessageBus($this->message_bus);
            Security::setHelper($this->security, $this->saniter);
            Router::setRouter($this->router, $this->url_generator);
            HTTPClient::setClient($this->client);
            Cache::setupCache();

            // Events are proloaded on compilation, but set at runtime
            $this->module_manager->loadModules();

            $this->initialized = true;
        }
    }

    /**
     * Event very early on in the Symfony HTTP lifecycle, but after everyting is registered
     * where we get access to the event dispatcher
     *
     * @param RequestEvent             $event
     * @param string                   $event_name
     * @param EventDispatcherInterface $event_dispatcher
     *
     * @return RequestEvent
     */
    public function onKernelRequest(RequestEvent $event,
                                    string $event_name): RequestEvent
    {
        $request = $event->getRequest();

        // Save the target path, so we can redirect back after logging in
        if (!(!$event->isMasterRequest() || $request->isXmlHttpRequest() || in_array($request->attributes->get('_route'), ['login', 'register', 'logout']))) {
            $this->saveTargetPath($this->session, 'main', $request->getBaseUrl());
        }

        $this->initialize();
        return $event;
    }

    /**
     * Event after everything is initialized when using the `bin/console` command
     *
     * @param ConsoleCommandEvent      $event
     * @param string                   $event_name
     * @param EventDispatcherInterface $event_dispatcher
     *
     * @return ConsoleCommandEvent
     */
    public function onCommand(ConsoleCommandEvent $event,
                              string $event_name): ConsoleCommandEvent
    {
        $this->initialize();
        return $event;
    }

    /**
     * Tell Symfony which events we want to listen to, which Symfony detects and autowires
     * due to this implementing the `EventSubscriberInterface`
     */
    public static function getSubscribedEvents()
    {
        return [
            KernelEvents::REQUEST => 'onKernelRequest',
            'console.command'     => 'onCommand',
        ];
    }
}


code:
<?php

// {{{ License
// 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/>.
// }}}

namespace App\Core;

use Symfony\Contracts\HttpClient\HttpClientInterface;

abstract class HTTPClient
{
    private static ?Httpclientinterface $client;
    public static function setClient(HttpClientInterface $client)
    {
        self::$client = $client;
    }

    public static function __callStatic(string $name, array $args)
    {
        return self::$client->{$name}(...$args);
    }
}


code:
<?php

// {{{ License
// 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/>.
// }}}

/**
 * Utility functions for i18n
 *
 * @category  I18n
 * @package   GNU social
 *
 * @author    Matthew Gregg <matthew.gregg@gmail.com>
 * @author    Ciaran Gultnieks <ciaran@ciarang.com>
 * @author    Evan Prodromou <evan@status.net>
 * @author    Diogo Cordeiro <diogo@fc.up.pt>
 * @author    Hugo Sales <hugo@fc.up.pt>
 * @copyright 2010, 2018-2020 Free Software Foundation, Inc http://www.fsf.org
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 */

namespace App\Core\I18n;

use App\Util\Formatting;
use Symfony\Component\Translation\Exception\InvalidArgumentException;
use Symfony\Contracts\Translation\TranslatorInterface;

// Locale category constants are usually predefined, but may not be
// on some systems such as Win32.
$LC_CATEGORIES = [
    'LC_CTYPE',
    'LC_NUMERIC',
    'LC_TIME',
    'LC_COLLATE',
    'LC_MONETARY',
    'LC_MESSAGES',
    'LC_ALL',
];
foreach ($LC_CATEGORIES as $key => $name) {
    if (!defined($name)) {
        define($name, $key);
    }
}

abstract class I18n
{
    public static ?TranslatorInterface $translator = null;

    public static function setTranslator($trans): void
    {
        self::$translator = $trans;
    }

    /**
     * Looks for which plugin we've been called from to get the gettext domain;
     * if not in a plugin subdirectory, we'll use the default 'core+intl-icu'.
     *
     * @return string
     */
    public static function _mdomain(string $path): string
    {
        /*
          0 =>
          array
          'file' => string '/var/www/mublog/plugins/FeedSub/FeedSubPlugin.php' (length=49)
          'line' => int 77
          'function' => string '_m' (length=2)
          'args' =>
          array
          0 => &string 'Feeds' (length=5)
        */
        static $cached;
        if (!isset($cached[$path])) {
            $path          = Formatting::normalizePath($path);
            $cached[$path] = Formatting::pluginFromPath($path);
        }
        return $cached[$path] ?? 'core+intl-icu';
    }

    /**
     * Content negotiation for language codes
     *
     * @param string $http_accept_lang_header HTTP Accept-Language header
     *
     * @return string language code for best language match, false otherwise
     */
    public static function clientPreferredLanguage(string $http_accept_lang_header): string
    {
        $client_langs  = [];
        $all_languages = Common::config('site', 'languages');

        preg_match_all('"(((\S\S)-?(\S\S)?)(;q=([0-9.]+))?)\s*(,\s*|$)"',
                       strtolower($http_accept_lang_header), $http_langs);

        for ($i = 0; $i < count($http_langs); ++$i) {
            if (!empty($http_langs[2][$i])) {
                // if no q default to 1.0
                $client_langs[$http_langs[2][$i]] = ($http_langs[6][$i] ? (float) $http_langs[6][$i] : 1.0 - ($i * 0.01));
            }
            if (!empty($http_langs[3][$i]) && empty($client_langs[$http_langs[3][$i]])) {
                // if a catchall default 0.01 lower
                $client_langs[$http_langs[3][$i]] = ($http_langs[6][$i] ? (float) $http_langs[6][$i] - 0.01 : 0.99);
            }
        }
        // sort in descending q
        arsort($client_langs);

        foreach ($client_langs as $lang => $q) {
            if (isset($all_languages[$lang])) {
                return $all_languages[$lang]['lang'];
            }
        }
        return false;
    }

    /**
     * returns a simple code -> name mapping for languages
     *
     * @return array map of available languages by code to language name.
     */
    public static function getNiceLanguageList(): array
    {
        $nice_lang     = [];
        $all_languages = Common::config('site', 'languages');

        foreach ($all_languages as $lang) {
            $nice_lang[$lang['lang']] = $lang['name'];
        }
        return $nice_lang;
    }

    /**
     * Check whether a language is right-to-left
     *
     * @param string $lang_value language code of the language to check
     *
     * @return bool true if language is rtl
     */
    public static function isRtl(string $lang_value): bool
    {
        foreach (Common::config('site', 'languages') as $code => $info) {
            if ($lang_value == $info['lang']) {
                return $info['direction'] == 'rtl';
            }
        }
        throw new InvalidArgumentException('is_rtl function received an invalid lang to test. Lang was: ' . $lang_value);
    }

    /**
     * Get a list of all languages that are enabled in the default config
     *
     * @return array mapping of language codes to language info
     */
    public static function getAllLanguages(): array
    {
        return [
            'af'        => ['q' => 0.8, 'lang' => 'af', 'name' => 'Afrikaans', 'direction' => 'ltr'],
            'ar'        => ['q' => 0.8, 'lang' => 'ar', 'name' => 'Arabic', 'direction' => 'rtl'],
            'ast'       => ['q' => 1, 'lang' => 'ast', 'name' => 'Asturian', 'direction' => 'ltr'],
            'eu'        => ['q' => 1, 'lang' => 'eu', 'name' => 'Basque', 'direction' => 'ltr'],
            'be-tarask' => ['q' => 0.5, 'lang' => 'be-tarask', 'name' => 'Belarusian (Taraškievica orthography)', 'direction' => 'ltr'],
            'br'        => ['q' => 0.8, 'lang' => 'br', 'name' => 'Breton', 'direction' => 'ltr'],
            'bg'        => ['q' => 0.8, 'lang' => 'bg', 'name' => 'Bulgarian', 'direction' => 'ltr'],
            'my'        => ['q' => 1, 'lang' => 'my', 'name' => 'Burmese', 'direction' => 'ltr'],
            'ca'        => ['q' => 0.5, 'lang' => 'ca', 'name' => 'Catalan', 'direction' => 'ltr'],
            'zh-cn'     => ['q' => 0.9, 'lang' => 'zh_CN', 'name' => 'Chinese (Simplified)', 'direction' => 'ltr'],
            'zh-hant'   => ['q' => 0.2, 'lang' => 'zh_TW', 'name' => 'Chinese (Taiwanese)', 'direction' => 'ltr'],
            'ksh'       => ['q' => 1, 'lang' => 'ksh', 'name' => 'Colognian', 'direction' => 'ltr'],
            'cs'        => ['q' => 0.5, 'lang' => 'cs', 'name' => 'Czech', 'direction' => 'ltr'],
            'da'        => ['q' => 0.8, 'lang' => 'da', 'name' => 'Danish', 'direction' => 'ltr'],
            'nl'        => ['q' => 0.5, 'lang' => 'nl', 'name' => 'Dutch', 'direction' => 'ltr'],
            'arz'       => ['q' => 0.8, 'lang' => 'arz', 'name' => 'Egyptian Spoken Arabic', 'direction' => 'rtl'],
            'en'        => ['q' => 1, 'lang' => 'en', 'name' => 'English', 'direction' => 'ltr'],
            'en-us'     => ['q' => 1, 'lang' => 'en', 'name' => 'English (US)', 'direction' => 'ltr'],
            'en-gb'     => ['q' => 1, 'lang' => 'en_GB', 'name' => 'English (UK)', 'direction' => 'ltr'],
            'eo'        => ['q' => 0.8, 'lang' => 'eo', 'name' => 'Esperanto', 'direction' => 'ltr'],
            'fi'        => ['q' => 1, 'lang' => 'fi', 'name' => 'Finnish', 'direction' => 'ltr'],
            'fr'        => ['q' => 1, 'lang' => 'fr', 'name' => 'French', 'direction' => 'ltr'],
            'fr-fr'     => ['q' => 1, 'lang' => 'fr', 'name' => 'French (France)', 'direction' => 'ltr'],
            'fur'       => ['q' => 0.8, 'lang' => 'fur', 'name' => 'Friulian', 'direction' => 'ltr'],
            'gl'        => ['q' => 0.8, 'lang' => 'gl', 'name' => 'Galician', 'direction' => 'ltr'],
            'ka'        => ['q' => 0.8, 'lang' => 'ka', 'name' => 'Georgian', 'direction' => 'ltr'],
            'de'        => ['q' => 0.8, 'lang' => 'de', 'name' => 'German', 'direction' => 'ltr'],
            'el'        => ['q' => 0.1, 'lang' => 'el', 'name' => 'Greek', 'direction' => 'ltr'],
            'he'        => ['q' => 0.5, 'lang' => 'he', 'name' => 'Hebrew', 'direction' => 'rtl'],
            'hu'        => ['q' => 0.8, 'lang' => 'hu', 'name' => 'Hungarian', 'direction' => 'ltr'],
            'is'        => ['q' => 0.1, 'lang' => 'is', 'name' => 'Icelandic', 'direction' => 'ltr'],
            'id'        => ['q' => 1, 'lang' => 'id', 'name' => 'Indonesian', 'direction' => 'ltr'],
            'ia'        => ['q' => 0.8, 'lang' => 'ia', 'name' => 'Interlingua', 'direction' => 'ltr'],
            'ga'        => ['q' => 0.5, 'lang' => 'ga', 'name' => 'Irish', 'direction' => 'ltr'],
            'it'        => ['q' => 1, 'lang' => 'it', 'name' => 'Italian', 'direction' => 'ltr'],
            'ja'        => ['q' => 0.5, 'lang' => 'ja', 'name' => 'Japanese', 'direction' => 'ltr'],
            'ko'        => ['q' => 0.9, 'lang' => 'ko', 'name' => 'Korean', 'direction' => 'ltr'],
            'lv'        => ['q' => 1, 'lang' => 'lv', 'name' => 'Latvian', 'direction' => 'ltr'],
            'lt'        => ['q' => 1, 'lang' => 'lt', 'name' => 'Lithuanian', 'direction' => 'ltr'],
            'lb'        => ['q' => 1, 'lang' => 'lb', 'name' => 'Luxembourgish', 'direction' => 'ltr'],
            'mk'        => ['q' => 0.5, 'lang' => 'mk', 'name' => 'Macedonian', 'direction' => 'ltr'],
            'mg'        => ['q' => 1, 'lang' => 'mg', 'name' => 'Malagasy', 'direction' => 'ltr'],
            'ms'        => ['q' => 1, 'lang' => 'ms', 'name' => 'Malay', 'direction' => 'ltr'],
            'ml'        => ['q' => 0.5, 'lang' => 'ml', 'name' => 'Malayalam', 'direction' => 'ltr'],
            'ne'        => ['q' => 1, 'lang' => 'ne', 'name' => 'Nepali', 'direction' => 'ltr'],
            'nb'        => ['q' => 0.1, 'lang' => 'nb', 'name' => 'Norwegian (Bokmål)', 'direction' => 'ltr'],
            'no'        => ['q' => 0.1, 'lang' => 'nb', 'name' => 'Norwegian (Bokmål)', 'direction' => 'ltr'],
            'nn'        => ['q' => 1, 'lang' => 'nn', 'name' => 'Norwegian (Nynorsk)', 'direction' => 'ltr'],
            'fa'        => ['q' => 1, 'lang' => 'fa', 'name' => 'Persian', 'direction' => 'rtl'],
            'pl'        => ['q' => 0.5, 'lang' => 'pl', 'name' => 'Polish', 'direction' => 'ltr'],
            'pt'        => ['q' => 1, 'lang' => 'pt', 'name' => 'Portuguese', 'direction' => 'ltr'],
            'pt-br'     => ['q' => 0.9, 'lang' => 'pt_BR', 'name' => 'Brazilian Portuguese', 'direction' => 'ltr'],
            'ru'        => ['q' => 0.9, 'lang' => 'ru', 'name' => 'Russian', 'direction' => 'ltr'],
            'sr-ec'     => ['q' => 1, 'lang' => 'sr-ec', 'name' => 'Serbian', 'direction' => 'ltr'],
            'es'        => ['q' => 1, 'lang' => 'es', 'name' => 'Spanish', 'direction' => 'ltr'],
            'sv'        => ['q' => 0.8, 'lang' => 'sv', 'name' => 'Swedish', 'direction' => 'ltr'],
            'tl'        => ['q' => 0.8, 'lang' => 'tl', 'name' => 'Tagalog', 'direction' => 'ltr'],
            'ta'        => ['q' => 1, 'lang' => 'ta', 'name' => 'Tamil', 'direction' => 'ltr'],
            'te'        => ['q' => 0.3, 'lang' => 'te', 'name' => 'Telugu', 'direction' => 'ltr'],
            'tr'        => ['q' => 0.5, 'lang' => 'tr', 'name' => 'Turkish', 'direction' => 'ltr'],
            'uk'        => ['q' => 1, 'lang' => 'uk', 'name' => 'Ukrainian', 'direction' => 'ltr'],
            'hsb'       => ['q' => 0.8, 'lang' => 'hsb', 'name' => 'Upper Sorbian', 'direction' => 'ltr'],
            'ur'        => ['q' => 1, 'lang' => 'ur_PK', 'name' => 'Urdu (Pakistan)', 'direction' => 'rtl'],
            'vi'        => ['q' => 0.8, 'lang' => 'vi', 'name' => 'Vietnamese', 'direction' => 'ltr'],
        ];
    }

    /**
     * Format the given associative array $messages in the ICU
     * translation format, with the given $params. Allows for a
     * declarative use of the translation engine, for example
     * `formatICU(['she' => ['She has one foo', 'She has many foo'],
     *             'he'  => ['He has one foo', 'He has many foo']], ['she' => 1])`
     *
     * @see http://userguide.icu-project.org/formatparse/messages
     */
    public static function formatICU(array $messages, array $params): string
    {
        $res = '';
        foreach (array_slice($params, 0, 1, true) as $var => $type) {
            if (is_int($type)) {
                $pref = '=';
                $op   = 'plural';
            } elseif (is_string($type)) {
                $pref = '';
                $op   = 'select';
            } else {
                throw new Exception('Invalid variable type. (int|string) only');
            }

            $res = "{$var}, {$op}, ";
            $i   = 0;
            $cnt = count($messages) - 1;
            foreach ($messages as $val => $m) {
                if ($i !== $cnt) {
                    $res .= "{$pref}{$val}";
                } else {
                    $res .= 'other';
                }

                if (is_array($m)) {
                    $res .= ' {' . self::formatICU($m, array_slice($params, 1, null, true)) . '} ';
                } elseif (is_string($m)) {
                    $res .= " {{$m}} ";
                } else {
                    throw new Exception('Invalid message array');
                }
                ++$i;
            }
        }
        return "{{$res}}";
    }
}

/**
 * Wrapper for symfony translation with smart domain detection.
 *
 * If calling from a plugin, this function checks which plugin it was
 * being called from and uses that as text domain, which will have
 * been set up during plugin initialization.
 *
 * Also handles plurals and contexts depending on what parameters
 * are passed to it:
 *
 *    _m(string $msg)                                      -- simple message
 *    _m(string $ctx, string $msg)                         -- message with context
 *    _m(string|string[] $msg, array $params)              -- message
 *    _m(string $ctx, string|string[] $msg, array $params) -- combination of the previous two
 *
 * @throws InvalidArgumentException
 *
 * @return string
 *
 * @todo add parameters
 */
function _m(...$args): string
{
    // Get the file where this function was called from, reducing the
    // memory and performance inpact by not returning the arguments,
    // and only 2 frames (this and previous)
    $domain = I18n::_mdomain(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS)[0]['file'], 2);
    switch (count($args)) {
    case 1:
        // Empty parameters, simple message
        return I18n::$translator->trans($args[0], [], $domain);
    case 3:
        if (is_int($args[2])) {
            throw new Exception('Calling `_m()` with an explicit number is deprecated, ' .
                                'use an explicit parameter');
        }
        // Falthrough
        // no break
    case 2:
        if (is_array($args[0])) {
            $args[0] = I18n::formatICU($args[0], $args[1]);
        }

        if (is_string($args[0])) {
            $msg    = $args[0];
            $params = $args[1] ?? [];
            return I18n::$translator->trans($msg, $params, $domain);
        }
        // Fallthrough
        // no break
    default:
        throw new InvalidArgumentException('Bad parameters to `_m()`');
    }
}


code:
<?php

// {{{ License
// 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/>.
// }}}

/**
 * Extracts translation messages from PHP code.
 *
 * @package   GNUsocial
 * @category  I18n
 *
 * @author    Symfony project
 * @author    Michel Salib <michelsalib@hotmail.com>
 * @author    Fabien Potencier <fabien@symfony.com>
 * @copyright 2011-2019 Symfony project
 * @author    Hugo Sales <hugo@fc.up.pt>
 * @copyright 2020 Free Software Foundation, Inc http://www.fsf.org
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 */

namespace App\Core\I18n;

use App\Util\Formatting;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Translation\Extractor\AbstractFileExtractor;
use Symfony\Component\Translation\Extractor\ExtractorInterface;
use Symfony\Component\Translation\Extractor\PhpExtractor;
use Symfony\Component\Translation\Extractor\PhpStringTokenParser;
use Symfony\Component\Translation\MessageCatalogue;

class TransExtractor extends AbstractFileExtractor implements ExtractorInterface
{
    /**
     * The sequence that captures translation messages.
     *
     * @todo add support for all the cases we use
     *
     * @var array
     */
    protected $sequences = [
        // [
        //     '_m',
        //     '(',
        //     self::MESSAGE_TOKEN,
        //     ',',
        //     self::METHOD_ARGUMENTS_TOKEN,
        //     ',',
        //     self::DOMAIN_TOKEN,
        // ],
        [
            '_m',
            '(',
            self::MESSAGE_TOKEN,
        ],
        [
            // Special case: when we have calls to _m with a dynamic
            // value, we need to handle them seperately
            'function',
            '_m_dynamic',
            self::M_DYNAMIC,
        ],
    ];

    // TODO probably shouldn't be done this way
    // {{{Code from PhpExtractor
    // See vendor/symfony/translation/Extractor/PhpExtractor.php
    //
    const MESSAGE_TOKEN          = 300;
    const METHOD_ARGUMENTS_TOKEN = 1000;
    const DOMAIN_TOKEN           = 1001;
    const M_DYNAMIC              = 1002;

    /**
     * Prefix for new found message.
     *
     * @var string
     */
    private $prefix = '';

    /**
     * {@inheritdoc}
     */
    public function extract($resource, MessageCatalogue $catalog)
    {
        if (($dir = strstr($resource, '/Core/GNUsocial.php', true)) === false) {
            return;
        }

        $files = $this->extractFiles($dir);
        foreach ($files as $file) {
            $this->parseTokens(token_get_all(file_get_contents($file)), $catalog, $file);

            gc_mem_caches();
        }
    }

    /**
     * {@inheritdoc}
     */
    public function setPrefix(string $prefix)
    {
        $this->prefix = $prefix;
    }

    /**
     * Normalizes a token.
     *
     * @param mixed $token
     *
     * @return null|string
     */
    protected function normalizeToken($token)
    {
        if (isset($token[1]) && 'b"' !== $token) {
            return $token[1];
        }

        return $token;
    }

    /**
     * Seeks to a non-whitespace token.
     */
    private function seekToNextRelevantToken(\Iterator $tokenIterator)
    {
        for (; $tokenIterator->valid(); $tokenIterator->next()) {
            $t = $tokenIterator->current();
            if (T_WHITESPACE !== $t[0]) {
                break;
            }
        }
    }

    /**
     * {@inheritdoc}
     */
    private function skipMethodArgument(\Iterator $tokenIterator)
    {
        $openBraces = 0;

        for (; $tokenIterator->valid(); $tokenIterator->next()) {
            $t = $tokenIterator->current();

            if ('[' === $t[0] || '(' === $t[0]) {
                ++$openBraces;
            }

            if (']' === $t[0] || ')' === $t[0]) {
                --$openBraces;
            }

            if ((0 === $openBraces && ',' === $t[0]) || (-1 === $openBraces && ')' === $t[0])) {
                break;
            }
        }
    }

    /**
     * @throws \InvalidArgumentException
     *
     * @return bool
     *
     */
    protected function canBeExtracted(string $file)
    {
        return $this->isFile($file)
            && 'php' === pathinfo($file, PATHINFO_EXTENSION)
            && strstr($file, '/src/') !== false;
    }

    /**
     * {@inheritdoc}
     */
    protected function extractFromDirectory($directory)
    {
        $finder = new Finder();
        return $finder->files()->name('*.php')->in($directory);
    }

    /**
     * Extracts the message from the iterator while the tokens
     * match allowed message tokens.
     */
    private function getValue(\Iterator $tokenIterator)
    {
        $message  = '';
        $docToken = '';
        $docPart  = '';

        for (; $tokenIterator->valid(); $tokenIterator->next()) {
            $t = $tokenIterator->current();
            if ('.' === $t) {
                // Concatenate with next token
                continue;
            }
            if (!isset($t[1])) {
                break;
            }

            switch ($t[0]) {
                case T_START_HEREDOC:
                    $docToken = $t[1];
                    break;
                case T_ENCAPSED_AND_WHITESPACE:
                case T_CONSTANT_ENCAPSED_STRING:
                    if ('' === $docToken) {
                        $message .= PhpStringTokenParser::parse($t[1]);
                    } else {
                        $docPart = $t[1];
                    }
                    break;
                case T_END_HEREDOC:
                    $message .= PhpStringTokenParser::parseDocString($docToken, $docPart);
                    $docToken = '';
                    $docPart  = '';
                    break;
                case T_WHITESPACE:
                    break;
                default:
                    break 2;
            }
        }

        return $message;
    }

    // }}}

    /**
     * Extracts trans message from PHP tokens.
     */
    protected function parseTokens(array $tokens, MessageCatalogue $catalog, string $filename)
    {
        $tokenIterator = new \ArrayIterator($tokens);

        for ($key = 0; $key < $tokenIterator->count(); ++$key) {
            foreach ($this->sequences as $sequence) {
                $message = '';
                $domain  = I18n::_mdomain($filename);
                $tokenIterator->seek($key);

                foreach ($sequence as $sequenceKey => $item) {
                    $this->seekToNextRelevantToken($tokenIterator);

                    if ($this->normalizeToken($tokenIterator->current()) === $item) {
                        $tokenIterator->next();
                        continue;
                    } elseif (self::MESSAGE_TOKEN === $item) {
                        $message = $this->getValue($tokenIterator);

                        if (\count($sequence) === ($sequenceKey + 1)) {
                            break;
                        }
                    } elseif (self::METHOD_ARGUMENTS_TOKEN === $item) {
                        $this->skipMethodArgument($tokenIterator);
                    } elseif (self::DOMAIN_TOKEN === $item) {
                        $domainToken = $this->getValue($tokenIterator);
                        if ('' !== $domainToken) {
                            $domain = $domainToken;
                        }
                        break;
                    } elseif (self::M_DYNAMIC === $item) {
                        // Special case
                        self::storeDynamic($catalog, $filename);
                    } else {
                        break;
                    }
                }

                if ($message) {
                    self::store($catalog, $message, $domain, $filename, $tokens[$key][2]); // Line no.
                    break;
                }
            }
        }
    }

    /**
     * Store the $message in the message catalogue $mc
     */
    private function store(MessageCatalogue $mc, string $message,
                           string $domain, string $filename, ?int $line_no = null)
    {
        $mc->set($message, $this->prefix . $message, $domain);
        $metadata              = $mc->getMetadata($message, $domain) ?? [];
        $metadata['sources'][] = Formatting::normalizePath($filename) . (!empty($line_no) ? ":{$line_no}" : '');
        $mc->setMetadata($message, $metadata, $domain);
    }

    /**
     * Calls `::_m_dynamic` from the class defined in $filename and
     * stores the results in the catalogue. For cases when the
     * translation can't be done in a static (non-PHP) file
     */
    private function storeDynamic(MessageCatalogue $mc, string $filename)
    {
        require_once $filename;
        $class   = preg_replace('/.*\/([A-Za-z]*)\.php/', '\1', $filename);
        $classes = get_declared_classes();

        // Find FQCN of $class
        foreach ($classes as $c) {
            if (strstr($c, $class) !== false) {
                $class = $c;
                break;
            }
        }

        $messages = $class::_m_dynamic();
        $domain   = $messages['domain'];
        unset($messages['domain']);
        foreach ($messages as $m) {
            self::store($mc, $m, $domain, $filename);
        }
    }
}