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 Component\Media;

use App\Core\Cache;
use App\Core\DB\DB;
use function App\Core\I18n\_m;
use App\Core\Log;
use App\Entity\Avatar;
use App\Entity\File;
use App\Util\Common;
use App\Util\Exception\ClientException;
use Component\Media\Exception\NoAvatarException;
use Exception;
use Symfony\Component\Asset\Package;
use Symfony\Component\Asset\VersionStrategy\EmptyVersionStrategy;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\File\File as SymfonyFile;
use Symfony\Component\HttpFoundation\HeaderUtils;
use Symfony\Component\HttpFoundation\Response;

abstract class Utils
{
    /**
     * Perform file validation (checks and normalization) and store the given file
     */
    public static function validateAndStoreFile(SymfonyFile $sfile,
                                                string $dest_dir,
                                                ?string $title = null,
                                                bool $is_local = true,
                                                ?int $actor_id = null): File
    {
        // The following properly gets the mimetype with `file` or other
        // available methods, so should be safe
        $hash = hash_file(File::FILEHASH_ALGO, $sfile->getPathname());
        $file = File::create([
            'file_hash' => $hash,
            'actor_id'  => $actor_id,
            'mimetype'  => $sfile->getMimeType(),
            'title'     => $title ?: _m('Untitled attachment'),
            'is_local'  => $is_local,
        ]);
        $sfile->move($dest_dir, $hash);
        // TODO Normalize file types
        return $file;
    }

    /**
     * Include $filepath in the response, for viewing or downloading.
     *
     * @throws ServerException
     */
    public static function sendFile(string $filepath, string $mimetype, ?string $output_filename, string $disposition = 'inline'): Response
    {
        $response = new BinaryFileResponse(
            $filepath,
            Response::HTTP_OK,
            [
                'Content-Description' => 'File Transfer',
                'Content-Type'        => $mimetype,
                'Content-Disposition' => HeaderUtils::makeDisposition($disposition, $output_filename ?: _m('untitled')),
                'Cache-Control'       => 'public',
            ],
            $public = true,
            $disposition = null,
            $add_etag = true,
            $add_last_modified = true
        );
        if (Common::config('site', 'x_static_delivery')) {
            $response->trustXSendfileTypeHeader();
        }
        return $response;
    }

    /**
     * Throw a client exception if the cache key $id doesn't contain
     * exactly one entry
     *
     * @param mixed $except
     * @param mixed $id
     */
    private static function error($except, $id, array $res)
    {
        switch (count($res)) {
        case 0:
            throw new $except();
        case 1:
            return $res[0];
        default:
            Log::error('Media query returned more than one result for identifier: \"' . $id . '\"');
            throw new ClientException(_m('Internal server error'));
        }
    }

    /**
     * Get the file info by id
     *
     * Returns the file's hash, mimetype and title
     */
    public static function getFileInfo(int $id)
    {
        return self::error(NoSuchFileException::class,
                           $id,
                           Cache::get("file-info-{$id}",
                                      function () use ($id) {
                                          return DB::dql('select f.file_hash, f.mimetype, f.title ' .
                                                         'from App\\Entity\\File f ' .
                                                         'where f.id = :id',
                                                         ['id' => $id]);
                                      }));
    }

    // ----- Attachment ------

    /**
     * Get the attachment file info by id
     *
     * Returns the attachment file's hash, mimetype, title and path
     */
    public static function getAttachmentFileInfo(int $id): array
    {
        $res              = self::getFileInfo($id);
        $res['file_path'] = Common::config('attachments', 'dir') . $res['file_hash'];
        return $res;
    }

    // ----- Avatar ------

    /**
     * Get the avatar associated with the given nickname
     */
    public static function getAvatar(?string $nickname = null): Avatar
    {
        $nickname = $nickname ?: Common::userNickname();
        return self::error(NoAvatarException::class,
                           $nickname,
                           Cache::get("avatar-{$nickname}",
                                      function () use ($nickname) {
                                          return DB::dql('select a from App\\Entity\\Avatar a ' .
                                                         'join App\Entity\GSActor g with a.gsactor_id = g.id ' .
                                                         'where g.nickname = :nickname',
                                                         ['nickname' => $nickname]);
                                      }));
    }

    /**
     * Get the cached avatar associated with the given nickname, or the current user if not given
     */
    public static function getAvatarUrl(?string $nickname = null): string
    {
        $nickname = $nickname ?: Common::userNickname();
        return Cache::get("avatar-url-{$nickname}", function () use ($nickname) {
            try {
                return self::getAvatar($nickname)->getUrl();
            } catch (NoAvatarException $e) {
            }
            $package = new Package(new EmptyVersionStrategy());
            return $package->getUrl(Common::config('avatar', 'default'));
        });
    }

    /**
     * Get the cached avatar file info associated with the given nickname
     *
     * Returns the avatar file's hash, mimetype, title and path.
     * Ensures exactly one cached value exists
     */
    public static function getAvatarFileInfo(string $nickname): array
    {
        try {
            $res = self::error(NoAvatarException::class,
                               $nickname,
                               Cache::get("avatar-file-info-{$nickname}",
                                          function () use ($nickname) {
                                              return DB::dql('select f.file_hash, f.mimetype, f.title ' .
                                                             'from App\\Entity\\File f ' .
                                                             'join App\\Entity\\Avatar a with f.id = a.file_id ' .
                                                             'join App\\Entity\\GSActor g with g.id = a.gsactor_id ' .
                                                             'where g.nickname = :nickname',
                                                             ['nickname' => $nickname]);
                                          }));
            $res['file_path'] = Avatar::getFilePathStatic($res['file_hash']);
            return $res;
        } catch (Exception $e) {
            $filepath = INSTALLDIR . '/public/assets/default-avatar.svg';
            return ['file_path' => $filepath, 'mimetype' => 'image/svg+xml', 'title' => null];
        }
    }
}


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 Component\Posting;

use App\Core\DB\DB;
use App\Core\Event;
use App\Core\Form;
use function App\Core\I18n\_m;
use App\Core\Module;
use App\Core\Security;
use App\Entity\FileToNote;
use App\Entity\Note;
use App\Util\Common;
use App\Util\Exceptiion\InvalidFormException;
use App\Util\Exception\RedirectException;
use Component\Media\Media;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;

class Posting extends Module
{
    /**
     * HTML render event handler responsible for adding and handling
     * the result of adding the note submission form, only if a user is logged in
     */
    public function onStartTwigPopulateVars(array &$vars): bool
    {
        if (($user = Common::user()) == null) {
            return Event::next;
        }

        $actor_id = $user->getId();
        $to_tags  = [];
        foreach (DB::dql('select c.tag from App\Entity\GSActorCircle c where c.tagger = :tagger', ['tagger' => $actor_id]) as $t) {
            $t           = $t['tag'];
            $to_tags[$t] = $t;
        }

        $placeholder_string = ['How are you feeling?', 'Have something to share?', 'How was your day?'];
        $rand_key           = array_rand($placeholder_string);

        $request = $vars['request'];
        $form    = Form::create([
            ['content',     TextareaType::class, ['label' => ' ', 'data' => '', 'attr' => ['placeholder' => _m($placeholder_string[$rand_key])]]],
            ['attachments', FileType::class,     ['label' => ' ', 'data' => null, 'multiple' => true, 'required' => false]],
            ['visibility',  ChoiceType::class,   ['label' => _m('Visibility:'), 'expanded' => true, 'choices' => [_m('Public') => 'public', _m('Instance') => 'instance', _m('Private') => 'private']]],
            ['to',          ChoiceType::class,   ['label' => _m('To:'), 'multiple' => true, 'expanded' => true, 'choices' => $to_tags]],
            ['post',        SubmitType::class,   ['label' => _m('Post')]],
        ]);

        $form->handleRequest($request);
        if ($form->isSubmitted()) {
            $data = $form->getData();
            if ($form->isValid()) {
                self::storeNote($actor_id, $data['content'], $data['attachments'], $is_local = true);
                throw new RedirectException();
            } else {
                throw new InvalidFormException();
            }
        }

        $vars['post_form'] = $form->createView();

        return Event::next;
    }

    /**
     * Store the given note with $content and $attachments, created by
     * $actor_id, possibly as a reply to note $reply_to and with flag
     * $is_local. Sanitizes $content and $attachments
     */
    public static function storeNote(int $actor_id, string $content, array $attachments, bool $is_local, ?int $reply_to = null, ?int $repeat_of = null)
    {
        $note = Note::create([
            'gsactor_id' => $actor_id,
            'content'    => Security::sanitize($content),
            'is_local'   => $is_local,
            'reply_to'   => $reply_to,
            'repeat_of'  => $repeat_of,
        ]);
        $files = [];
        foreach ($attachments as $f) {
            $nf = Media::validateAndStoreFile($f, Common::config('attachments', 'dir'),
                                              Security::sanitize($title = $f->getClientOriginalName()),
                                              $is_local = true, $actor_id);
            $files[] = $nf;
            DB::persist($nf);
        }
        DB::persist($note);
        // Need file and note ids for the next step
        DB::flush();
        if ($attachments != []) {
            foreach ($files as $f) {
                DB::persist(FileToNote::create(['file_id' => $f->getId(), 'note_id' => $note->getId()]));
            }
            DB::flush();
        }
    }
}

repo_name: ksuudilla/gnu

license: mit


gnu/social

type: project

description: Free software social networking platform.

dist

App\: src/

Plugin\: plugins/

Component\: components/

App\Tests\: tests/

cache:clear: symfony-cmd

assets:install %PUBLIC_DIR%: symfony-cmd


_readme: This file locks the dependencies of your project to a known state

_readme: Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies

alchemy/resource-component

type: git

type: zip

type: library

Alchemy\Resource\: src/

description: Resource component

keywords: alchemy resource uri

alchemy/zippy

ext-rar: To read/write Rar files

ext-tar: To read/write Tar files

ext-zip: To read/write Zip files

guzzle/guzzle: To read archives and files from remote sources (Guzzle 3)

guzzlehttp/guzzle: To read archives and files from remote sources (Guzzle 6)

Alchemy\Zippy\: src/

description: Zippy, the archive manager companion

keywords: bzip compression tar zip

beberlei/assert

Assert\: lib/Assert

Benjamin Eberlei

role: Lead Developer

Richard Quadling

role: Collaborator

description: Thin assertion library for input validation in business models.

keywords: assert assertion validation

composer/package-versions-deprecated

type: composer-plugin

class: PackageVersions\Installer

PackageVersions\: src/PackageVersions

description: Composer plugin that provides efficient querying for installed package versions (no runtime IO)

type: custom

type: github

type: tidelift

doctrine/annotations

Doctrine\Common\Annotations\: lib/Doctrine/Common/Annotations

description: Docblock Annotations Parser

keywords: annotations docblock parser

doctrine/cache

alcaeus/mongo-php-adapter: Required to use legacy MongoDB driver

Doctrine\Common\Cache\: lib/Doctrine/Common/Cache

description: PHP Doctrine Cache library is a popular cache implementation that supports many different drivers such as redis, memcache, apc, mongodb and others.

keywords: abstraction apcu cache caching couchdb memcached php redis xcache

type: patreon

doctrine/collections

Doctrine\Common\Collections\: lib/Doctrine/Common/Collections

description: PHP Doctrine Collections library that adds additional functionality on top of PHP arrays.

keywords: array collections iterators php

doctrine/common

Doctrine\Common\: lib/Doctrine/Common

description: PHP Doctrine Common project is a library that provides additional functionality that other Doctrine projects depend on such as better reflection support, persistence interfaces, proxies, event system and much more.

keywords: common doctrine php

doctrine/dbal

symfony/console: For helpful console commands such as SQL execution and import of files.

Doctrine\DBAL\: lib/Doctrine/DBAL

description: Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.

keywords: abstraction database db2 dbal mariadb mssql mysql oci8 oracle pdo pgsql postgresql queryobject sasql sql sqlanywhere sqlite sqlserver sqlsrv

doctrine/doctrine-bundle

doctrine/orm: The Doctrine ORM integration is optional in the bundle.

symfony/web-profiler-bundle: To use the data collector.

type: symfony-bundle

description: Symfony DoctrineBundle

keywords: database dbal orm persistence

doctrine/doctrine-migrations-bundle

description: Symfony DoctrineMigrationsBundle

keywords: dbal migrations schema

doctrine/event-manager

description: The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.

keywords:
event
event dispatcher
event manager
event system
events

doctrine/inflector

Doctrine\Common\Inflector\: lib/Doctrine/Common/Inflector

Doctrine\Inflector\: lib/Doctrine/Inflector

description: PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.

keywords: inflection inflector lowercase manipulation php plural singular strings uppercase words

doctrine/instantiator

Doctrine\Instantiator\: src/Doctrine/Instantiator/

description: A small, lightweight utility to instantiate objects in PHP without invoking their constructors

keywords: constructor instantiate

doctrine/lexer

description: PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.

doctrine/migrations

doctrine/sql-formatter: Allows to generate formatted SQL with the diff command.

symfony/yaml: Allows the use of yaml for migration configuration files.

description: PHP Doctrine Migrations project offer additional functionality on top of the database abstraction layer (DBAL) for versioning your database schema and easily deploying changes to it. It is a very easy to use and a powerful tool.

doctrine/orm

symfony/yaml: If you want to use YAML Metadata Mapping Driver

description: Object-Relational-Mapper for PHP

doctrine/persistence

description: The Doctrine Persistence project is a set of shared interfaces and functionality that the different Doctrine object mappers share.

doctrine/sql-formatter

description: a PHP SQL highlighting library

egulias/email-validator

ext-intl: PHP Internationalization Libraries are required to use the SpoofChecking validation

description: A library for validating emails against several RFCs

keywords: email emailvalidation emailvalidator validation validator

erusev/parsedown

description: Parser for Markdown.

giggsey/libphonenumber-for-php

description: PHP Port of Google's libphonenumber

keywords: geocoding geolocation libphonenumber mobile phonenumber validation

giggsey/locale

description: Locale functions required by libphonenumber-for-php

laminas/laminas-code

doctrine/annotations: Doctrine\Common\Annotations >=1.0 for annotation features

description: Extensions to the PHP Reflection API, static code scanning, and code generation

laminas/laminas-eventmanager

laminas/laminas-stdlib: ^2.7.3 || ^3.0, to use the FilterChain feature

description: Trigger and listen to events within a PHP application

laminas/laminas-zendframework-bridge

description: Alias legacy ZF class names to Laminas Project equivalents.

league/uri-parser

league/uri-schemes: Allow validating and normalizing URI parsing results

description: userland URI parser RFC 3986 compliant

lstrojny/functional-php

description: Functional primitives for PHP

description: An HTML5 parser and serializer.

keywords: HTML5 dom html parser querypath serializer xml

monolog/monolog

aws/aws-sdk-php: Allow sending log messages to AWS services like DynamoDB

doctrine/couchdb: Allow sending log messages to a CouchDB server

elasticsearch/elasticsearch: Allow sending log messages to an Elasticsearch server via official client

ext-amqp: Allow sending log messages to an AMQP server (1.0+ required)

ext-mbstring: Allow to work properly with unicode symbols

ext-mongodb: Allow sending log messages to a MongoDB server (via driver)

graylog2/gelf-php: Allow sending log messages to a GrayLog2 server

mongodb/mongodb: Allow sending log messages to a MongoDB server (via library)

php-amqplib/php-amqplib: Allow sending log messages to an AMQP server using php-amqplib

php-console/php-console: Allow sending log messages to Google Chrome

ruflin/elastica: Allow sending log messages to an Elastic Search server

description: Sends your logs to files, sockets, inboxes, databases and various web services

ocramius/proxy-manager

laminas/laminas-json: To have the JsonRpc adapter (Remote Object feature)

laminas/laminas-soap: To have the Soap adapter (Remote Object feature)

laminas/laminas-xmlrpc: To have the XmlRpc adapter (Remote Object feature)

ocramius/generated-hydrator: To have very fast object to array to object conversion for ghost objects

description: A library providing utilities to generate, instantiate and generally operate with Object Proxies

keywords:
aop
lazy loading
proxy
proxy pattern
service proxies

odolbeau/phone-number-bundle

symfony/serializer: Serialize/deserialize phone numbers using Symfony library

description: Integrates libphonenumber into your Symfony application

keywords:
bundle
libphonenumber
phone-number
phonenumber
telephone number

php-ds/php-ds

ext-ds: to improve performance and reduce memory usage

phpdocumentor/reflection-common

description: Common reflection classes used by phpdocumentor to reflect the code structure

keywords:
FQSEN
phpDocumentor
phpdoc
reflection
static analysis

phpdocumentor/reflection-docblock

description: With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.

phpdocumentor/type-resolver

description: A PSR-5 based resolver of Class names, Types and Structural Element Names

psr/cache

description: Common interface for caching libraries

psr/container

description: Common Container Interface (PHP FIG PSR-11)

keywords: PSR-11 container container-interface container-interop psr

psr/event-dispatcher

description: Standard interfaces for event handling.

psr/link

description: Common interfaces for HTTP links

psr/log

description: Common interface for logging libraries

sensio/framework-extra-bundle

description: This bundle provides a way to configure your controllers with annotations

someonewithpc/memcached-polyfill

description: No-implementation memcached extension polyfill

someonewithpc/redis-polyfill

description: No-implementation redis extension polyfill

symfony/amqp-messenger

description: Symfony AMQP extension Messenger Bridge

symfony/asset

description: Symfony Asset Component

symfony/cache

description: Symfony Cache component with PSR-6, PSR-16, and tags

symfony/cache-contracts

description: Generic abstractions related to caching

keywords: abstractions contracts decoupling interfaces interoperability standards

symfony/config

description: Symfony Config Component

symfony/console

description: Symfony Console Component

symfony/dependency-injection

symfony/expression-language: For using expressions in service container configuration

symfony/finder: For using double-star glob patterns or when GLOB_BRACE portability is required

symfony/proxy-manager-bridge: Generate service proxies to lazy load them

description: Symfony DependencyInjection Component

symfony/deprecation-contracts

description: A generic function and convention to trigger deprecation notices

symfony/doctrine-bridge

description: Symfony Doctrine Bridge

symfony/doctrine-messenger

description: Symfony Doctrine Messenger Bridge

symfony/dotenv

description: Registers environment variables from a .env file

symfony/error-handler

description: Symfony ErrorHandler Component

symfony/event-dispatcher

description: Symfony EventDispatcher Component

symfony/event-dispatcher-contracts

description: Generic abstractions related to dispatching event

symfony/expression-language

description: Symfony ExpressionLanguage Component

symfony/filesystem

description: Symfony Filesystem Component

symfony/finder

description: Symfony Finder Component

symfony/flex

description: Composer plugin for Symfony

symfony/form

symfony/security-csrf: For protecting forms against CSRF attacks.

description: Symfony Form Component

symfony/framework-bundle

ext-apcu: For best performance of the system caches

symfony/web-link: For using web links, features such as preloading, prefetching or prerendering

symfony/yaml: For using the debug:config and lint:yaml commands

description: Symfony FrameworkBundle

symfony/http-client

description: Symfony HttpClient component

symfony/http-client-contracts

description: Generic abstractions related to HTTP clients

symfony/http-foundation

description: Symfony HttpFoundation Component

symfony/http-kernel

description: Symfony HttpKernel Component

symfony/intl

ext-intl: to use the component with locales other than "en"

description: A PHP replacement layer for the C intl extension that includes additional data from the ICU library.

keywords: i18n icu internationalization intl l10n localization

symfony/mailer

description: Symfony Mailer Component

symfony/messenger

enqueue/messenger-adapter: For using the php-enqueue library as a transport.

description: Symfony Messenger Component

symfony/mime

description: A library to manipulate MIME messages

symfony/monolog-bridge

symfony/console: For the possibility to show log messages in console commands depending on verbosity settings.

symfony/http-kernel: For using the debugging handlers together with the response life cycle of the HTTP kernel.

symfony/var-dumper: For using the debugging handlers like the console handler or the log server handler.

description: Symfony Monolog Bridge

symfony/monolog-bundle

description: Symfony MonologBundle

symfony/notifier

description: A library to notify messages

symfony/options-resolver

description: Symfony OptionsResolver Component

symfony/orm-pack

description: A pack for the Doctrine ORM

symfony/polyfill-intl-grapheme

description: Symfony polyfill for intl's grapheme_* functions

keywords: compatibility grapheme intl polyfill portable shim

symfony/polyfill-intl-icu

description: Symfony polyfill for intl's ICU-related data and classes

keywords: compatibility icu intl polyfill portable shim

symfony/polyfill-intl-idn

description: Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions

keywords: compatibility idn intl polyfill portable shim

symfony/polyfill-intl-normalizer

description: Symfony polyfill for intl's Normalizer class and related functions

keywords: compatibility intl normalizer polyfill portable shim

symfony/polyfill-mbstring

description: Symfony polyfill for the Mbstring extension

keywords: compatibility mbstring polyfill portable shim

description: Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions

description: Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions

symfony/process

description: Symfony Process Component

symfony/property-access

description: Symfony PropertyAccess Component

keywords:
access
array
extraction
index
injection
object
property
property path
reflection

symfony/property-info

description: Symfony Property Info Component

keywords: doctrine phpdoc property symfony type validator

symfony/redis-messenger

description: Symfony Redis extension Messenger Bridge

symfony/routing

symfony/config: For using the all-in-one router or any loader

description: Symfony Routing Component

symfony/security-bundle

description: Symfony SecurityBundle

symfony/security-core

description: Symfony Security Component - Core Library

symfony/security-csrf

symfony/http-foundation: For using the class SessionTokenStorage.

description: Symfony Security Component - CSRF Library

symfony/security-guard

description: Symfony Security Component - Guard

symfony/security-http

symfony/routing: For using the HttpUtils class to create sub-requests, redirect the user, and match URLs

symfony/security-csrf: For using tokens to protect authentication/logout attempts

description: Symfony Security Component - HTTP Integration

symfony/serializer

doctrine/annotations: For using the annotation mapping. You will also need doctrine/cache.

doctrine/cache: For using the default cached annotation reader and metadata cache.

symfony/mime: For using a MIME type guesser within the DataUriNormalizer.

symfony/yaml: For using the default YAML mapping loader.

description: Symfony Serializer Component

symfony/serializer-pack

description: A pack for the Symfony serializer

symfony/service-contracts

description: Generic abstractions related to writing services

symfony/stopwatch

description: Symfony Stopwatch Component

symfony/string

description: Symfony String component

symfony/translation

description: Symfony Translation Component

symfony/translation-contracts

description: Generic abstractions related to translation

symfony/twig-bridge

description: Symfony Twig Bridge

symfony/twig-bundle

description: Symfony TwigBundle

symfony/twig-pack

description: A Twig pack for Symfony projects

symfony/validator

doctrine/cache: For using the default cached annotation reader.

symfony/expression-language: For using the Expression validator and the ExpressionLanguageSyntax constraints

symfony/property-access: For accessing properties within comparison constraints

symfony/property-info: To automatically add NotNull and Type constraints

description: Symfony Validator Component

symfony/var-dumper

ext-iconv: To convert non-UTF-8 strings to UTF-8 (or symfony/polyfill-iconv in case ext-iconv cannot be used).

symfony/console: To use the ServerDumpCommand and/or the bin/var-dump-server script

description: Symfony mechanism for exploring and dumping PHP variables

symfony/var-exporter

description: A blend of var_export() + serialize() to turn any serializable data structure to plain PHP code

keywords: clone construct export hydrate instantiate serialize

symfony/web-link

description: Symfony WebLink Component

keywords: dns-prefetch http http2 link performance prefetch preload prerender psr13 push

symfony/yaml

symfony/console: For validating YAML files using the lint command

description: Symfony Yaml Component

symfonycasts/verify-email-bundle

description: Simple, stylish Email Verification for Symfony

tgalopin/html-sanitizer

description: Sanitize untrustworthy HTML user input

tgalopin/html-sanitizer-bundle

description: Symfony Bundle for https://github.com/tgalopin/html-sanitizer

twig/extra-bundle

description: A Symfony bundle for extra Twig extensions

twig/markdown-extra

description: A Twig extension for Markdown

twig/twig

description: Twig, the flexible, fast, and secure template language for PHP

webimpress/safe-writer

description: Tool to write files safely, to avoid race conditions

keywords:
concurrent write
file writer
race condition
safe writer
webimpress

webmozart/assert

description: Assertions to validate method input/output with nice error messages.

composer/semver

description: Semver library that offers utilities, version constraint parsing and validation.

composer/xdebug-handler

description: Restarts a process without Xdebug.

friendsofphp/php-cs-fixer

symfony/polyfill-mbstring: When enabling `ext-mbstring` is not possible.

description: A tool to automatically fix PHP code style

niels-de-blaauw/php-doc-check

description: Command line tool that identifies code that needs documentation.

nikic/php-parser

description: A PHP parser written in PHP

php-cs-fixer/diff

description: sebastian/diff v2 backport support for PHP5.6

symfony/browser-kit

description: Symfony BrowserKit Component

symfony/css-selector

description: Symfony CssSelector Component

symfony/debug-bundle

symfony/dependency-injection: For using as a service from the container

description: Symfony DebugBundle

symfony/debug-pack

description: A debug pack for Symfony projects

symfony/dom-crawler

description: Symfony DomCrawler Component

symfony/maker-bundle

description: Symfony Maker helps you create empty commands, controllers, form classes, tests and more so you can forget about writing boilerplate code.

keywords:
code generator
generator
scaffold
scaffolding

symfony/phpunit-bridge

symfony/error-handler: For tracking deprecated interfaces usages at runtime with DebugClassLoader

description: Symfony PHPUnit Bridge

symfony/profiler-pack

description: A pack for the Symfony web profiler

symfony/test-pack

description: A pack for functional and end-to-end testing within a Symfony app

symfony/web-profiler-bundle

description: Symfony WebProfilerBundle

ulrichsg/getopt-php

description: Command line arguments parser for PHP 5.4 - 7.3

wp-cli/php-cli-tools

description: Console utilities for PHP


code:
<?php

use App\Core\ModuleManager;
use Symfony\Component\Dotenv\Dotenv;

$loader = require dirname(__DIR__) . '/vendor/autoload.php';

// Load cached env vars if the .env.local.php file exists
// Run "composer dump-env prod" to create it (requires symfony/flex >=1.2)
if (\is_array($env = @include dirname(__DIR__) . '/.env.local.php') && (!isset($env['APP_ENV']) || ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? $env['APP_ENV']) === $env['APP_ENV'])) {
    foreach ($env as $k => $v) {
        $_ENV[$k] = $_ENV[$k] ?? (isset($_SERVER[$k]) && 0 !== strpos($k, 'HTTP_') ? $_SERVER[$k] : $v);
    }
} elseif (!\class_exists(Dotenv::class)) {
    throw new RuntimeException('Please run "composer require symfony/dotenv" to load the ".env" files configuring the application.');
} else {
    // load all the .env files
    (new Dotenv())->loadEnv(\dirname(__DIR__) . '/.env');
}

$_SERVER += $_ENV;
$_SERVER['APP_ENV']   = $_ENV['APP_ENV']   = ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? null) ?: 'dev';
$_SERVER['APP_DEBUG'] = $_SERVER['APP_DEBUG'] ?? $_ENV['APP_DEBUG'] ?? 'prod' !== $_SERVER['APP_ENV'];
$_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = (int) $_SERVER['APP_DEBUG'] || filter_var($_SERVER['APP_DEBUG'], FILTER_VALIDATE_BOOLEAN) ? '1' : '0';

ModuleManager::setLoader($loader);


code:
<?php

return [
    Symfony\Bundle\FrameworkBundle\FrameworkBundle::class                => ['all' => true],
    Symfony\Bundle\TwigBundle\TwigBundle::class                          => ['all' => true],
    Twig\Extra\TwigExtraBundle\TwigExtraBundle::class                    => ['all' => true],
    Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class            => ['dev' => true, 'test' => true],
    Symfony\Bundle\MonologBundle\MonologBundle::class                    => ['all' => true],
    Symfony\Bundle\DebugBundle\DebugBundle::class                        => ['dev' => true, 'test' => true],
    Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true],
    Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class                 => ['all' => true],
    Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class     => ['all' => true],
    Symfony\Bundle\SecurityBundle\SecurityBundle::class                  => ['all' => true],
    Symfony\Bundle\MakerBundle\MakerBundle::class                        => ['dev' => true],
    SymfonyCasts\Bundle\VerifyEmail\SymfonyCastsVerifyEmailBundle::class => ['all' => true],
    Misd\PhoneNumberBundle\MisdPhoneNumberBundle::class                  => ['all' => true],
    HtmlSanitizer\Bundle\HtmlSanitizerBundle::class                      => ['all' => true],
];


code:
<?php

require_once 'bootstrap.php';

use App\Kernel;
use Doctrine\ORM\Tools\Console\ConsoleRunner;

$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);
$kernel->boot();
$entityManager = $kernel->getContainer()->get('doctrine.orm.default_entity_manager');

return ConsoleRunner::createHelperSet($entityManager);


code:
framework:
    cache:
        # Unique name of your app: used to compute stable namespaces for cache keys.
        #prefix_seed: your_vendor_name/app_name

        # The "app" cache stores to the filesystem by default.
        # The data in this cache should persist between deploys.
        # Other options include:

        # Redis
        #app: cache.adapter.redis
        #default_redis_provider: redis://localhost

        # APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues)
        #app: cache.adapter.apcu

        # Namespaced pools use the above "app" backend by default
        #pools:
            #my.dedicated.cache: null


code:
debug:
    # Forwards VarDumper Data clones to a centralized server allowing to inspect dumps on CLI or in your browser.
    # See the "server:dump" command to start a new server.
    dump_destination: "tcp://%env(VAR_DUMPER_SERVER)%"


code:
doctrine:
    dbal:
        profiling_collect_backtrace: true


code:
services:
    EasyCorp\EasyLog\EasyLogHandler:
        public: false
        arguments: ['%kernel.logs_dir%/%kernel.environment%.log']

#// FIXME: How to add this configuration automatically without messing up with the monolog configuration?
#monolog:
#    handlers:
#        buffered:
#            type:     buffer
#            handler:  easylog
#            channels: ['!event']
#            level:    debug
#        easylog:
#            type: service
#            id:   EasyCorp\EasyLog\EasyLogHandler


code:
monolog:
    handlers:
        main:
            type: stream
            path: "%kernel.logs_dir%/%kernel.environment%.log"
            level: debug
            channels: ["!event"]
        # uncomment to get logging in your browser
        # you may have to allow bigger header sizes in your Web server configuration
        #firephp:
        #    type: firephp
        #    level: info
        #chromephp:
        #    type: chromephp
        #    level: info
        console:
            type: console
            process_psr_3_messages: false
            channels: ["!event", "!doctrine", "!console"]


code:
web_profiler:
    toolbar: true
    intercept_redirects: false

framework:
    profiler: { only_exceptions: false }


code:
doctrine:
    dbal:
        url: '%env(resolve:DATABASE_URL)%'
        charset: UTF8
        schema_filter: ~^(?!rememberme_token)~ # Ignore these in migrations
    orm:
        auto_generate_proxy_classes: true
        naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
        auto_mapping: true
        # Defined in the compiler pass to use SchemaDefDriver
        # mappings:
        #     App:
        #         is_bundle: false
        #         type: staticphp
        #         dir: '%kernel.project_dir%/src/Entity'
        #         prefix: 'App\Entity'
        #         alias: App


code:
doctrine_migrations:
    migrations_paths:
        # namespace is arbitrary but should be different from App\Migrations
        # as migrations classes should NOT be autoloaded
        'DoctrineMigrations': '%kernel.project_dir%/migrations'


code:
framework:
    secret: '%env(APP_SECRET)%'
    csrf_protection: true
    #http_method_override: true

    # Enables session support. Note that the session will ONLY be started if you read or write from it.
    # Remove or comment this section to explicitly disable session support.
    session:
        handler_id: null
        cookie_secure: auto
        cookie_samesite: lax

    #esi: true
    #fragments: true
    php_errors:
        log: true


code:
html_sanitizer:
    default_sanitizer: 'default'
    sanitizers:
        default:
            # Read https://github.com/tgalopin/html-sanitizer/blob/master/docs/1-getting-started.md#extensions
            # to learn more about which extensions you would like to enable.
            extensions:
                - 'basic'
                # - 'list'
                # - 'table'
                # - 'image'
                # - 'code'
                # - 'iframe'
                # - 'extra'

            # Read https://github.com/tgalopin/html-sanitizer/blob/master/docs/3-configuration-reference.md
            # to discover all the available options for each extension.


code:
framework:
    mailer:
        dsn: '%env(MAILER_DSN)%'


code:
framework:
    messenger:
        failure_transport: failed
        transports:
            failed: 'doctrine://default?queue_name=failed'
            high: '%env(MESSENGER_TRANSPORT_DSN_HIGH)%'
            low: '%env(MESSENGER_TRANSPORT_DSN_LOW)%'

        routing:
            'App\Core\Queue\MessageHigh': high
            'App\Core\Queue\MessageLow': low


code:
doctrine:
    dbal:
        types:
            phone_number: Misd\PhoneNumberBundle\Doctrine\DBAL\Types\PhoneNumberType


code:
framework:
    notifier:
        #chatter_transports:
        #    slack: '%env(SLACK_DSN)%'
        #    telegram: '%env(TELEGRAM_DSN)%'
        #texter_transports:
        #    twilio: '%env(TWILIO_DSN)%'
        #    nexmo: '%env(NEXMO_DSN)%'
        channel_policy:
            # use chat/slack, chat/telegram, sms/twilio or sms/nexmo
            urgent: ['email']
            high: ['email']
            medium: ['email']
            low: ['email']
        admin_recipients:
            - { email: admin@example.com }


code:
# As of Symfony 5.1, deprecations are logged in the dedicated "deprecation" channel when it exists
monolog:
   channels: [deprecation]
   handlers:
       deprecation:
           type: stream
           channels: [deprecation]
           path: "%kernel.logs_dir%/%kernel.environment%.deprecations.log"


code:
doctrine:
    orm:
        auto_generate_proxy_classes: false
        metadata_cache_driver:
            type: pool
            pool: doctrine.system_cache_pool
        query_cache_driver:
            type: pool
            pool: doctrine.system_cache_pool
        result_cache_driver:
            type: pool
            pool: doctrine.result_cache_pool

framework:
    cache:
        pools:
            doctrine.result_cache_pool:
                adapter: cache.app
            doctrine.system_cache_pool:
                adapter: cache.system


code:
monolog:
    handlers:
        main:
            type: fingers_crossed
            action_level: error
            handler: nested
            excluded_http_codes: [404, 405]
            buffer_size: 50 # How many messages should be saved? Prevent memory leaks
        nested:
            type: stream
            path: "%kernel.logs_dir%/%kernel.environment%.log"
            level: debug
        console:
            type: console
            process_psr_3_messages: false
            channels: ["!event", "!doctrine"]
        deprecation:
            type: stream
            path: "%kernel.logs_dir%/%kernel.environment%.deprecations.log"
        deprecation_filter:
            type: filter
            handler: deprecation
            max_level: info
            channels: ["php"]


code:
framework:
    router:
        strict_requirements: null


code:
framework:
    router:
        utf8: true


code:
security:
    # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
    providers:
        users:
            entity:
                class: 'App\Entity\LocalUser'
                property: 'nickname'
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            anonymous: true
            lazy: true
            provider: users
            guard:
                authenticators:
                    - App\Security\Authenticator
            logout:
                path: logout
                # where to redirect after logout
                target: main_all

            remember_me:
                secret: '%kernel.secret%'
                secure: true
                httponly: '%remember_me_httponly%'
                samesite: '%remember_me_samesite%'
                token_provider: 'Symfony\Bridge\Doctrine\Security\RememberMe\DoctrineTokenProvider'

            # activate different ways to authenticate
            # https://symfony.com/doc/current/security.html#firewalls-authentication

            # https://symfony.com/doc/current/security/impersonating_user.html
            # switch_user: true

    # Easy way to control access for large sections of your site
    # Note: Only the *first* access control that matches will be used
    access_control:
        - { path: ^/admin, roles: ROLE_ADMIN }
        - { path: ^/settings, roles: ROLE_USER }


code:
sensio_framework_extra:
    router:
        annotations: false


code:
framework:
    test: true
    session:
        storage_id: session.storage.mock_file


code:
monolog:
    handlers:
        main:
            type: fingers_crossed
            action_level: error
            handler: nested
            excluded_http_codes: [404, 405]
            channels: ["!event"]
        nested:
            type: stream
            path: "%kernel.logs_dir%/%kernel.environment%.log"
            level: debug


code:
twig:
    strict_variables: true


code:
framework:
    validation:
        not_compromised_password: false


code:
web_profiler:
    toolbar: false
    intercept_redirects: false

framework:
    profiler: { collect: false }


code:
framework:
    default_locale: en
    translator:
        default_path: '%kernel.project_dir%/translations'
        fallbacks:
            - en


code:
twig:
    paths:
        '%kernel.project_dir%/templates': default_path
        "%kernel.project_dir%/public": public_path


code:
framework:
    validation:
        email_validation_mode: html5

        # Enables validator auto-mapping support.
        # For instance, basic validation constraints will be inferred from Doctrine's metadata.
        #auto_mapping:
        #    App\Entity\: []


code:
<?php

use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;

return function (RoutingConfigurator $routes) {
    $routes->import('.', 'GNUsocial');
};


code:
controllers:
    resource: ../../src/Controller/
    type: annotation

kernel:
    resource: ../../src/Kernel.php
    type: annotation


code:
_errors:
    resource: '@FrameworkBundle/Resources/config/routing/errors.xml'
    prefix: /_error


code:
web_profiler_wdt:
    resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml'
    prefix: /_wdt

web_profiler_profiler:
    resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml'
    prefix: /_profiler


code:
# This file is the entry point to configure your own services.
# Files in the packages/ subdirectory configure your dependencies.

parameters:

services:
    # default configuration for services in *this* file
    _defaults:
        autowire: true      # Automatically injects dependencies in your services.
        autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.

    # makes classes in src/ available to be used as services
    # this creates a service per class whose id is the fully-qualified class name
    App\:
        resource: '../src/*'
        exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php,Routes}'

    # controllers are imported separately to make sure services can be injected
    # as action arguments even if you don't extend any base controller class
    App\Controller\:
        resource: '../src/Controller'
        tags: ['controller.service_arguments']

    App\Core\Router\RouteLoader:
        tags: ['routing.loader']

    # Wrapper arround Doctrine's StaticPHP metadata driver
    app.core.schemadef_driver:
        class: App\Core\DB\SchemaDefDriver
        arguments:
            - '%kernel.project_dir%/src/Entity'

    App\Core\I18n\TransExtractor:
        tags:
            - { name: 'translation.extractor', alias: social }

    App\Core\Queue\MessageHandler:
      tags: ['messenger.message_handler']

    Symfony\Bridge\Doctrine\Security\RememberMe\DoctrineTokenProvider: ~

    Plugin\:
      resource: '../plugins/*'
      exclude: '../plugins/*/{scripts,classes,lib,actions,locale,doc}'
      tags: ['controller.service_arguments']

    Component\:
      resource: '../components/*'
      exclude: '../components/*/{scripts,classes,lib,actions,locale,doc}'
      tags: ['controller.service_arguments']


code:
version: '3.3'

services:
  nginx:
    image: nginx:alpine
    depends_on:
      - php
    restart: always
    tty: false
    ports:
      - 80:80
      - 443:443
    volumes:
      # Nginx
      - ./docker/nginx/nginx.conf:/var/nginx/social.conf
      - ./docker/nginx/domain.sh:/var/nginx/domain.sh
      # Certbot
      - ./docker/certbot/www:/var/www/certbot
      - ./docker/certbot/.files:/etc/letsencrypt
      # Social
      - ./public:/var/www/social/public
    env_file:
      - ./docker/bootstrap/bootstrap.env
      - ./docker/db/db.env
    command: /bin/sh -c '/var/nginx/domain.sh;
                         while :; do
                             sleep 6h & wait $${!};
                             nginx -s reload;
                         done &
                         nginx -g "daemon off;"'

  certbot:
    image: certbot/certbot
    depends_on:
      - nginx
    # Check for certificate renewal every 12h as
    # recomnended by Let's Encryot
    entrypoint: /bin/sh -c 'trap exit TERM;
                            while :; do
                                certbot renew > /dev/null;
                                sleep 12h & wait $${!};
                            done'
    volumes:
      - ./docker/certbot/www:/var/www/certbot
      - ./docker/certbot/.files:/etc/letsencrypt

  php:
    build: docker/php
    depends_on:
      - db
    restart: always
    tty: true
    ports:
      - 9000:9000
    volumes:
      # Entrypoint
      - ./docker/php/entrypoint.sh:/entrypoint.sh
      - ./docker/db/wait_for_db.sh:/wait_for_db.sh
      - ./docker/social/install.sh:/var/entrypoint.d/social_install.sh
      # Main files
      - .:/var/www/social
    env_file:
      - ./docker/social/social.env
      - ./docker/db/db.env
    command: /entrypoint.sh

  db:
    image: postgres:alpine
    restart: always
    tty: false
    ports:
      - 5432:5432
    environment:
      - PGDATA=/var/lib/postgres/data
    env_file:
      - ./docker/db/db.env
    volumes:
      - database:/var/lib/postgres/data

  redis:
    image: redis:alpine
    restart: always
    tty: false
    ports:
      - 6379:6379

volumes:
  database:


code:
FROM nginx:alpine

RUN echo "Installing bootstrap utils"

RUN apk add curl certbot openssl > /dev/null

RUN echo '                                  \
server {                                    \
    listen [::]:80;                         \
    listen 80;                              \
    server_name %hostname%;                 \
    location /.well-known/acme-challenge/ { \
        root /var/www/certbot;              \
    }                                       \
}                                           \
' > /etc/nginx/conf.d/challenge.conf

path: docker/bootstrap/Dockerfile


code:
#!/bin/sh

. bootstrap.env

sed -ri "s/%hostname%/${domain}/" /etc/nginx/conf.d/challenge.conf

nginx

rsa_key_size=4096
certbot_path="/var/www/certbot"
lets_path="/etc/letsencrypt"

echo "Starting bootstrap"

if [ ! -e "$lets_path/live//options-ssl-nginx.conf" ] ||  [ ! -e "$lets_path/live/ssl-dhparams.pem" ]
then

  echo "### Downloading recommended TLS parameters ..."
  mkdir -p "${lets_path}/live/${domain_root}"

  curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf >"$lets_path/options-ssl-nginx.conf"
  curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot/certbot/ssl-dhparams.pem >"$lets_path/ssl-dhparams.pem"

  if [ ${signed} -eq 0 ]
  then
    echo "### Creating self signed certificate for ${domain_root} ..."
    openssl req -x509 -nodes -newkey rsa:$rsa_key_size -days 365 \
      -keyout "${lets_path}/live/${domain_root}/privkey.pem" \
      -out "${lets_path}/live/${domain_root}/fullchain.pem" -subj "/CN=${domain_root}"

  else
    echo "### Creating dummy certificate for ${domain_root} ..."
    openssl req -x509 -nodes -newkey rsa:1024 -days 1 \
      -keyout "${lets_path}/live/${domain_root}/privkey.pem" \
      -out "${lets_path}/live/${domain_root}/fullchain.pem" -subj '/CN=localhost'

    nginx -s reload

    rm -Rf "${lets_path}/live/${domain_root}"
    rm -Rf "${lets_path}/archive/${domain_root}"
    rm -Rf "${lets_path}/renewal/${domain_root}.conf"

    echo "### Requesting Let's Encrypt certificate for ${domain_root} ..."
    # Format domain_args with the cartesian product of `domain_root` and `subdomains`

    if [ "${domain_root}" = "${domain}" ]; then domain_arg="-d ${domain_root}"; else domain_arg="-d ${domain_root} -d ${domain}"; fi

    # Ask Let's Encrypt to create certificates, if challenge passed
    certbot certonly --webroot -w "${certbot_path}" \
            --email "${email}" \
            ${domain_arg} \
            --non-interactive \
            --rsa-key-size "${rsa_key_size}" \
            --agree-tos \
            --force-renewal
  fi

else
  echo "Certificate related files exists, exiting"
fi


code:
version: "3.3"

services:
  bootstrap:
    build: .
    volumes:
      - ../certbot/www:/var/www/certbot
      - ../certbot/.files:/etc/letsencrypt
      - ./bootstrap.sh:/bootstrap.sh
      - ./bootstrap.env:/bootstrap.env
    ports:
      - 80:80
    env_file:
      - bootstrap.env
    entrypoint: /bootstrap.sh


code:
#!/bin/sh

case $DBMS in
    "postgres")
        CMD="pg_isready -hdb -q -Upostgres"
        ;;
    "mariadb")
        CMD="mysqladmin ping --silent -hdb -uroot -p${MYSQL_ROOT_PASSWORD}"
        ;;
    *)
        exit 1
esac

while ! $CMD;
do
    echo "Waiting for DB..."
    sleep 3
done


code:
#!/bin/sh

cat /var/nginx/social.conf | \
    sed -r "s/%hostname%/$domain/g; s/%hostname_root%/$domain_root/g" > \
        /etc/nginx/conf.d/social.conf


code:
server {

    listen [::]:80;
    listen 80;

    server_name %hostname%;

    # redirect all traffic to HTTPS
    rewrite ^ https://$host$request_uri? permanent;
}

server {

    listen [::]:443 ssl http2;
    listen 443 ssl http2;

    ssl_certificate /etc/letsencrypt/live/%hostname_root%/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/%hostname_root%/privkey.pem;

    # Let's Encrypt best practices
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    root /var/www/social/public;

    # Server name
    server_name %hostname%;

    # Index
    index index.php;

    # X-Accel/X-Sendfile. Still needs to be enabled in the config
    location /file {
        internal;
        root /var/www/social;
    }

    # PHP
    location ~ ^/(index|install)\.php(/.*)?$ {
        include fastcgi_params;

        fastcgi_split_path_info ^(.+?\.php)(/.*)$;
        set $path_info $fastcgi_path_info;
        try_files $fastcgi_script_name =404;

        fastcgi_pass php:9000;
        fastcgi_index index.php;

        fastcgi_param PATH_INFO $path_info;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }

    # Don't allow any PHP file other than index.php to be executed
    # This will ensure that nor config.php nor plugin files with eventual hardcoded security information are downloadable
    # And this is better than allowing php files to be executed in case of forgotten `if (!defined('GNUSOCIAL')) { exit(1); }`
    location ~ \.php$ {
        deny all;
    }

    # Location
    location / {
        try_files $uri $uri/ @index_handler;
    }

    # Fancy URLs
    error_page 404 @index_handler;
    location @index_handler {
        rewrite ^(.*)$ /index.php?p=$1 last;
    }

    # Restrict access that is unnecessary anyway
    location ~ /\.(ht|git) {
        deny all;
    }

#
# Hardening (optional)
#
    add_header Strict-Transport-Security "max-age=15768000; preload;";
    add_header X-Content-Type-Options nosniff;
    add_header Referrer-Policy strict-origin-when-cross-origin;
    add_header Content-Security-Policy "default-src 'self' 'unsafe-inline'; frame-ancestors 'self'; form-action 'self'; style-src 'self' 'unsafe-inline'; img-src * blob: data:;";
    add_header X-Permitted-Cross-Domain-Policies none;
    add_header X-Robots-Tag all; # Not really hardening, just here for strictness purposes

    client_max_body_size 15M;
    client_body_buffer_size 128k;
    gzip_vary on;

    location ~* \.(?:css|js|woff|svg|gif|png|webp|ttf|ico|jpe?g)$ {
        gzip on;
        gzip_comp_level 4;
        add_header Cache-Control "public";
        expires 30d;
        access_log off;
        log_not_found off;
    }
}


code:
FROM php:fpm-alpine

RUN apk update && apk add git autoconf make file gettext-dev icu-dev zlib-dev libpng-dev gmp-dev \
        mariadb-dev mariadb-client postgresql-dev postgresql-client composer > /dev/null

ARG exts=" bcmath exif gd gettext gmp intl mysqli opcache pdo pdo_mysql mysqli pdo_pgsql pgsql"

RUN apk add --virtual .phpize-deps $PHPIZE_DEPS \
        && cd /tmp && git clone https://github.com/krakjoe/apcu && cd apcu && phpize && ./configure --enable-apcu && make install \
        && cd /tmp && git clone https://github.com/php-ds/ext-ds && cd ext-ds && phpize && ./configure && make install \
        && cd /tmp && git clone https://github.com/msgpack/msgpack-php && cd msgpack-php && phpize  && ./configure && make install \
        && cd /tmp && git clone https://github.com/lz4/lz4 && cd lz4 && make install \
        && cd /tmp && git clone https://github.com/phpredis/phpredis && cd phpredis && phpize  && ./configure --enable-redis-msgpack --enable-redis-lz4 --with-liblz4=yes && make install \
        && rm -rf /usr/share/php7 \
        && rm -rf /tmp/* \
        && apk del .phpize-deps > /dev/null

RUN docker-php-ext-install ${exts} && docker-php-ext-enable ds msgpack redis apcu

WORKDIR /var/www/social

path: docker/php/Dockerfile


code:
#!/bin/sh

/wait_for_db.sh

echo "Got response from DB"

for script in /var/entrypoint.d/*.sh; do
    $script
done

exec php-fpm


code:
#!/bin/sh

case "${DBMS}" in
    'postgres')
        PGPASSWORD="${POSTGRES_PASSWORD}" psql -ltq -Upostgres -hdb | \
            cut -d '|' -f1 | grep -Fwq "${SOCIAL_DB}"
        DB_EXISTS=$?
        ;;
    'mariadb')
        mysqlcheck -cqs -uroot -p"${MYSQL_ROOT_PASSWORD}" -hdb social 2> /dev/null
        DB_EXISTS=$?
        exit 1
        ;;
    *)
        echo "Unknown DBMS"
        exit 1
esac

if [ ${DB_EXISTS} -ne 0 ]; then

    echo "Installing GNU social"
    echo "Installing composer dependencies"

    cd /var/www/social || exit 1

    composer install

    chmod g+w -R .
    chown -R :www-data .

    php bin/console doctrine:database:create || exit 1
    php bin/console doctrine:schema:create   || exit 1

    echo "GNU social is installed"
else
    echo "GNU social is already installed"
fi


code:
<?xml version="1.0" encoding="UTF-8"?>

<!-- https://phpunit.readthedocs.io/en/latest/configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="bin/.phpunit/phpunit.xsd"
         backupGlobals="false"
         colors="true"
         bootstrap="tests/bootstrap.php"
>
    <php>
        <ini name="error_reporting" value="-1" />
        <server name="APP_ENV" value="test" force="true" />
        <server name="SHELL_VERBOSITY" value="-1" />
        <server name="SYMFONY_PHPUNIT_REMOVE" value="" />
        <server name="SYMFONY_PHPUNIT_VERSION" value="7.5" />
    </php>

    <testsuites>
        <testsuite name="Project Test Suite">
            <directory>tests</directory>
        </testsuite>
    </testsuites>

    <filter>
        <whitelist processUncoveredFilesFromWhitelist="true">
            <directory suffix=".php">src</directory>
        </whitelist>
    </filter>

    <listeners>
        <listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener" />
    </listeners>
</phpunit>


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

// }}}

/**
 * ActivityPub implementation for GNU social
 *
 * @package   GNUsocial
 *
 * @author    Diogo Cordeiro <diogo@fc.up.pt>
 * @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
 *
 * @see      http://www.gnu.org/software/social/
 */

namespace Plugin\ActivityPub;

use App\Core\Module;

// // Import plugin libs
// foreach (glob(__DIR__ . DIRECTORY_SEPARATOR . 'lib' . DIRECTORY_SEPARATOR . '*.php') as $filename) {
//     require_once $filename;
// }
// // Import plugin models
// foreach (glob(__DIR__ . DIRECTORY_SEPARATOR . 'lib' . DIRECTORY_SEPARATOR . 'models' . DIRECTORY_SEPARATOR . '*.php') as $filename) {
//     require_once $filename;
// }

/**
 * Adds ActivityPub support to GNU social when enabled
 *
 * @category  Plugin
 * @package   GNUsocial
 *
 * @author    Diogo Cordeiro <diogo@fc.up.pt>
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 */
class ActivityPub extends Module
{
    const PLUGIN_VERSION = '0.4.0alpha0';
    // So that this isn't hardcoded everywhere
    const ACTIVITYPUB_PUBLIC_TO = [
        'https://www.w3.org/ns/activitystreams#Public',
        'Public',
        'as:Public',
    ];

    const ACTIVITYPUB_HTTP_CLIENT_HEADERS = [
        'Accept: application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
        'User-Agent: GNUsocialBot ' . GNUSOCIAL_VERSION . ' - https://gnusocial.network',
    ];

    /**
     * Returns a Actor's URI from its local $profile
     * Works both for local and remote users.
     * This is a discovery event but it seems more logical to have it separated.
     * This ensures that Profile->getUri() will always return the intended for a remote AP profile.
     *
     * @param Profile $profile Actor's local profile
     * @param string  &$uri    I/O Actor's URI
     *
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     *
     * @return bool event hook
     */
    public function onStartGetProfileUri(Profile $profile, &$uri): bool
    {
        $aprofile = Activitypub_profile::getKV('profile_id', $profile->id);
        if ($aprofile instanceof Activitypub_profile) {
            $uri = $aprofile->getUri();
            return false;
        }
        return true;
    }

    /**
     * Returns a notice from its URL.
     *
     * @param string $url         Notice's URL
     * @param bool   $grab_online whether to try online grabbing, defaults to true
     *
     * @throws Exception This function or provides a Notice, null, or fails with exception
     *
     * @return null|Notice The Notice object
     *
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public static function grab_notice_from_url(string $url, bool $grab_online = true): ?Notice
    {
        // Offline Grabbing
        try {
            // Look for a known remote notice
            return Notice::getByUri($url);
        } catch (Exception $e) {
            // Look for a local notice (unfortunately GNU social doesn't
            // provide this functionality natively)
            try {
                $candidate = Notice::getByID((int) substr($url, (strlen(common_local_url('apNotice', ['id' => 0])) - 1)));
                if (common_local_url('apNotice', ['id' => $candidate->getID()]) === $url) { // Sanity check
                    return $candidate;
                } else {
                    common_debug('ActivityPubPlugin Notice Grabber: ' . $candidate->getUrl() . ' is different of ' . $url);
                }
            } catch (Exception $e) {
                common_debug('ActivityPubPlugin Notice Grabber: failed to find: ' . $url . ' offline.');
            }
        }

        if ($grab_online) {
            // Online Grabbing
            $client   = new HTTPClient();
            $response = $client->get($url, ACTIVITYPUB_HTTP_CLIENT_HEADERS);
            $object   = json_decode($response->getBody(), true);
            if (Activitypub_notice::validate_note($object)) {
                // Okay, we've found a valid note object!
                // Now we need to find the Actor who authored it
                // The right way would be to grab attributed to and check its outbox
                // But that would be outright inefficient
                // Hence, let's just compare the domain names...
                if (isset($object['attributedTo'])) {
                    $acclaimed_actor_profile = ActivityPub_explorer::get_profile_from_url($object['attributedTo']);
                } elseif (isset($object['actor'])) {
                    $acclaimed_actor_profile = ActivityPub_explorer::get_profile_from_url($object['actor']);
                } else {
                    throw new Exception("A notice can't be created without an actor.");
                }
                if (parse_url($acclaimed_actor_profile->getUri(), PHP_URL_HOST) == parse_url($object['id'], PHP_URL_HOST)) {
                    return Activitypub_notice::create_notice($object, $acclaimed_actor_profile);
                } else {
                    throw new Exception("The acclaimed actor didn't create this note.");
                }
            } else {
                throw new Exception('Valid ActivityPub Notice object but unsupported by GNU social.');
            }
        }

        common_debug('ActivityPubPlugin Notice Grabber: failed to find: ' . $url);
        return null;
    }

    /**
     * Route/Reroute urls
     *
     * @param URLMapper $m
     *
     * @throws Exception
     *
     * @return void
     */
    public function onRouterInitialized(URLMapper $m)
    {
        $acceptHeaders = [
            'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' => 0,
            'application/activity+json'                                            => 1,
            'application/json'                                                     => 2,
            'application/ld+json'                                                  => 3,
        ];

        $m->connect(
            'user/:id',
            ['action' => 'apActorProfile'],
            ['id'     => '[0-9]+'],
            true,
            $acceptHeaders
        );

        $m->connect(
            ':nickname',
            ['action'   => 'apActorProfile'],
            ['nickname' => Nickname::DISPLAY_FMT],
            true,
            $acceptHeaders
        );

        $m->connect(
            ':nickname/',
            ['action'   => 'apActorProfile'],
            ['nickname' => Nickname::DISPLAY_FMT],
            true,
            $acceptHeaders
        );

        $m->connect(
            'notice/:id',
            ['action' => 'apNotice'],
            ['id'     => '[0-9]+'],
            true,
            $acceptHeaders
        );

        $m->connect(
            'user/:id/liked.json',
            ['action' => 'apActorLiked'],
            ['id'     => '[0-9]+']
        );

        $m->connect(
            'user/:id/followers.json',
            ['action' => 'apActorFollowers'],
            ['id'     => '[0-9]+']
        );

        $m->connect(
            'user/:id/following.json',
            ['action' => 'apActorFollowing'],
            ['id'     => '[0-9]+']
        );

        $m->connect(
            'user/:id/inbox.json',
            ['action' => 'apInbox'],
            ['id'     => '[0-9]+']
        );

        $m->connect(
            'user/:id/outbox.json',
            ['action' => 'apActorOutbox'],
            ['id'     => '[0-9]+']
        );

        $m->connect(
            'inbox.json',
            ['action' => 'apInbox']
        );
    }

    /**
     * Plugin version information
     *
     * @param array $versions
     *
     * @return bool hook true
     */
    public function onPluginVersion(array &$versions): bool
    {
        $versions[] = [
            'name'     => 'ActivityPub',
            'version'  => self::PLUGIN_VERSION,
            'author'   => 'Diogo Cordeiro',
            'homepage' => 'https://notabug.org/diogo/gnu-social/src/nightly/plugins/ActivityPub',
            // TRANS: Plugin description.
            'rawdescription' => _m('Follow people across social networks that implement ' .
            'ActivityPub.'),
        ];
        return true;
    }

    /**
     * Set up queue handlers for required interactions
     *
     * @param QueueManager $qm
     *
     * @return bool event hook return
     */
    public function onEndInitializeQueueManager(QueueManager $qm): bool
    {
        // Notice distribution
        $qm->connect('activitypub', 'ActivityPubQueueHandler');
        return true;
    }

    /**
     * Enqueue saved notices for distribution
     *
     * @param Notice $notice      notice to be distributed
     * @param array  &$transports list of transports to queue for
     *
     * @return bool event hook return
     */
    public function onStartEnqueueNotice(Notice $notice, array &$transports): bool
    {
        try {
            $id = $notice->getID();

            if ($id > 0) {
                $transports[] = 'activitypub';
                $this->log(LOG_INFO, "Notice:{$id} queued for distribution");
            }
        } catch (Exception $e) {
            $this->log(LOG_ERR, 'Invalid notice, not queueing for distribution');
        }

        return true;
    }

    /**
     * Update notice before saving.
     * We'll use this as a hack to maintain replies to unlisted/followers-only
     * notices away from the public timelines.
     *
     * @param Notice &$notice notice to be saved
     *
     * @return bool event hook return
     */
    public function onStartNoticeSave(Notice &$notice): bool
    {
        if ($notice->reply_to) {
            try {
                $parent   = $notice->getParent();
                $is_local = (int) $parent->is_local;

                // if we're replying unlisted/followers-only notices received by AP
                // or replying to replies of such notices, then we make sure to set
                // the correct type flag.
                if (($parent->source === 'ActivityPub' && $is_local === Notice::GATEWAY) || ($parent->source === 'web' && $is_local === Notice::LOCAL_NONPUBLIC)) {
                    $this->log(LOG_INFO, 'Enforcing type flag LOCAL_NONPUBLIC for new notice');
                    $notice->is_local = Notice::LOCAL_NONPUBLIC;
                }
            } catch (NoParentNoticeException $e) {
                // This is not a reply to something (has no parent)
            }
        }

        return true;
    }

    /**
     * Add AP-subscriptions for private messaging
     *
     * @param User  $current     current logged user
     * @param array &$recipients
     *
     * @return void
     */
    public function onFillDirectMessageRecipients(User $current, array &$recipients): void
    {
        try {
            $subs = Activitypub_profile::getSubscribed($current->getProfile());
            foreach ($subs as $sub) {
                if (!$sub->isLocal()) { // AP plugin adds AP users
                    try {
                        $value              = 'profile:' . $sub->getID();
                        $recipients[$value] = substr($sub->getAcctUri(), 5) . " [{$sub->getBestName()}]";
                    } catch (ProfileNoAcctUriException $e) {
                        $recipients[$value] = '[?@?] ' . $e->profile->getBestName();
                    }
                }
            }
        } catch (NoResultException $e) {
            // let it go
        }
    }

    /**
     * Validate AP-recipients for profile page message action addition
     *
     * @param Profile $recipient
     *
     * @return bool hook return value
     */
    public function onDirectMessageProfilePageActions(Profile $recipient): bool
    {
        $to = Activitypub_profile::getKV('profile_id', $recipient->getID());
        if ($to instanceof Activitypub_profile) {
            return false; // we can validate this profile, signal it
        }

        return true;
    }

    /**
     * Mark an ap_profile object for deletion
     *
     * @param Profile profile being deleted
     * @param array &$related objects with same profile_id to be deleted
     *
     * @return void
     */
    public function onProfileDeleteRelated(Profile $profile, array &$related): void
    {
        $related[] = 'Activitypub_profile';
        $related[] = 'Activitypub_rsa';

        // pending_follow_requests doesn't have a profile_id column,
        // so we must handle it manually
        $follow = new Activitypub_pending_follow_requests(null, $profile->getID());

        if ($follow->find()) {
            while ($follow->fetch()) {
                $follow->delete();
            }
        }
    }

    /**
     * Plugin Nodeinfo information
     *
     * @param array $protocols
     *
     * @return bool hook true
     */
    public function onNodeInfoProtocols(array &$protocols)
    {
        $protocols[] = 'activitypub';
        return true;
    }

    /**
     * Adds an indicator on Remote ActivityPub profiles.
     *
     * @param HTMLOutputter $out
     * @param Profile       $profile
     *
     * @throws Exception
     *
     * @return bool hook return value
     *
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public function onEndShowAccountProfileBlock(HTMLOutputter $out, Profile $profile)
    {
        if ($profile->isLocal()) {
            return true;
        }

        $aprofile = Activitypub_profile::getKV('profile_id', $profile->getID());
        if (!$aprofile instanceof Activitypub_profile) {
            // Not a remote ActivityPub_profile! Maybe some other network
            // that has imported a non-local user (e.g.: OStatus)?
            return true;
        }

        $out->elementStart('dl', 'entity_tags activitypub_profile');
        $out->element('dt', null, 'ActivityPub');
        $out->element('dd', null, _m('Remote Profile'));
        $out->elementEnd('dl');

        return true;
    }

    /**
     * Hack the notice search-box and try to grab remote profiles or notices.
     *
     * Note that, on successful grabbing, this function will redirect to the
     * new profile/notice, so URL searching is directly affected. A good solution
     * for this is to store the URLs in the notice text without the https/http
     * prefixes. This would change the queries for URL searching and therefore we
     * could do both search and grab.
     *
     * @param string $query search query
     *
     * @return bool hook
     *
     * @author Bruno Casteleiro <up201505347@fc.up.pt>
     */
    public function onStartNoticeSearch(string $query): bool
    {
        if (!common_logged_in()) {
            // early return: Only allow logged users to import/search for remote actors or notes
            return true;
        }

        if (preg_match('!^((?:\w+\.)*\w+@(?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+)$!', $query)) { // WebFinger ID found!
            // Try to grab remote actor
            $aprofile = self::pull_remote_profile($query);
            if ($aprofile instanceof Activitypub_profile) {
                $url = common_local_url('userbyid', ['id' => $aprofile->getID()], null, null, false);
                common_redirect($url, 303);
                return false;
            }
        } elseif (filter_var($query, FILTER_VALIDATE_URL)) { // URL found!
            // Is this an ActivityPub notice?

            // If we already know it, just return
            try {
                $notice = self::grab_notice_from_url($query, false); // Only check locally
                if ($notice instanceof Notice) {
                    return true;
                }
            } catch (Exception $e) {
                // We will next try online
            }

            // Otherwise, try to grab it
            try {
                $notice = self::grab_notice_from_url($query); // Unfortunately we will be trying locally again
                if ($notice instanceof Notice) {
                    $url = common_local_url('shownotice', ['notice' => $notice->getID()]);
                    common_redirect($url, 303);
                }
            } catch (Exception $e) {
                // We will next check if this URL is an actor
            }

            // Is this an ActivityPub actor?

            // If we already know it, just return
            try {
                $explorer = new Activitypub_explorer();
                $profile  = $explorer->lookup($query, false)[0]; // Only check locally
                if ($profile instanceof Profile) {
                    return true;
                }
            } catch (Exception $e) {
                // We will next try online
            }

            // Try to grab remote actor
            try {
                if (!isset($explorer)) {
                    $explorer = new Activitypub_explorer();
                }
                $profile = $explorer->lookup($query)[0]; // Unfortunately we will be trying locally again
                if ($profile instanceof Profile) {
                    $url = common_local_url('userbyid', ['id' => $profile->getID()], null, null, false);
                    common_redirect($url, 303);
                    return true;
                }
            } catch (Exception $e) {
                // Let the search run naturally
            }
        }
        return true;
    }

    /**
     * Make sure necessary tables are filled out.
     *
     * @return bool hook true
     */
    public function onCheckSchema()
    {
        $schema = Schema::get();
        $schema->ensureTable('activitypub_profile', Activitypub_profile::schemaDef());
        $schema->ensureTable('activitypub_rsa', Activitypub_rsa::schemaDef());
        $schema->ensureTable('activitypub_pending_follow_requests', Activitypub_pending_follow_requests::schemaDef());
        return true;
    }

    // WebFinger Events

    /**
     * Get remote user's ActivityPub_profile via a identifier
     *
     * @param string $arg A remote user identifier
     *
     * @return null|Activitypub_profile Valid profile in success | null otherwise
     *
     * @author GNU social
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public static function pull_remote_profile($arg)
    {
        if (preg_match('!^((?:\w+\.)*\w+@(?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+)$!', $arg)) {
            // webfinger lookup
            try {
                return Activitypub_profile::ensure_webfinger($arg);
            } catch (Exception $e) {
                common_log(LOG_ERR, 'Webfinger lookup failed for ' .
                                                $arg . ': ' . $e->getMessage());
            }
        }

        // Look for profile URLs, with or without scheme:
        $urls = [];
        if (preg_match('!^https?://((?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+(?:/\w+)+)$!', $arg)) {
            $urls[] = $arg;
        }
        if (preg_match('!^((?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+(?:/\w+)+)$!', $arg)) {
            $schemes = ['http', 'https'];
            foreach ($schemes as $scheme) {
                $urls[] = "{$scheme}://{$arg}";
            }
        }

        foreach ($urls as $url) {
            try {
                return Activitypub_profile::fromUri($url);
            } catch (Exception $e) {
                common_log(LOG_ERR, 'Profile lookup failed for ' .
                                                $arg . ': ' . $e->getMessage());
            }
        }
        return null;
    }

    /**
     * Webfinger matches: @user@example.com or even @user--one.george_orwell@1984.biz
     *
     * @author GNU social
     *
     * @param string $text       The text from which to extract webfinger IDs
     * @param string $preMention Character(s) that signals a mention ('@', '!'...)
     *
     * @return array The matching IDs (without $preMention) and each respective position in the given string.
     */
    public static function extractWebfingerIds($text, $preMention = '@')
    {
        $wmatches = [];
        $result   = preg_match_all(
            '/(?<!\S)' . preg_quote($preMention, '/') . '(' . Nickname::WEBFINGER_FMT . ')/',
            $text,
            $wmatches,
            PREG_OFFSET_CAPTURE
        );
        if ($result === false) {
            common_log(LOG_ERR, __METHOD__ . ': Error parsing webfinger IDs from text (preg_last_error==' . preg_last_error() . ').');
            return [];
        } elseif ($n_matches = count($wmatches)) {
            common_debug(sprintf('Found %d matches for WebFinger IDs: %s', $n_matches, _ve($wmatches)));
        }
        return $wmatches[1];
    }

    /**
     * Profile URL matches: @example.com/mublog/user
     *
     * @author GNU social
     *
     * @param string $text       The text from which to extract URL mentions
     * @param string $preMention Character(s) that signals a mention ('@', '!'...)
     *
     * @return array The matching URLs (without @ or acct:) and each respective position in the given string.
     */
    public static function extractUrlMentions($text, $preMention = '@')
    {
        $wmatches = [];
        // In the regexp below we need to match / _before_ URL_REGEX_VALID_PATH_CHARS because it otherwise gets merged
        // with the TLD before (but / is in URL_REGEX_VALID_PATH_CHARS anyway, it's just its positioning that is important)
        $result = preg_match_all(
            '/(?:^|\s+)' . preg_quote($preMention, '/') . '(' . URL_REGEX_DOMAIN_NAME . '(?:\/[' . URL_REGEX_VALID_PATH_CHARS . ']*)*)/',
            $text,
            $wmatches,
            PREG_OFFSET_CAPTURE
        );
        if ($result === false) {
            common_log(LOG_ERR, __METHOD__ . ': Error parsing profile URL mentions from text (preg_last_error==' . preg_last_error() . ').');
            return [];
        } elseif (count($wmatches)) {
            common_debug(sprintf('Found %d matches for profile URL mentions: %s', count($wmatches), _ve($wmatches)));
        }
        return $wmatches[1];
    }

    /**
     * Add activity+json mimetype on WebFinger
     *
     * @param XML_XRD            $xrd
     * @param Managed_DataObject $object
     *
     * @throws Exception
     *
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public function onEndWebFingerProfileLinks(XML_XRD $xrd, Managed_DataObject $object)
    {
        if ($object->isPerson()) {
            $link = new XML_XRD_Element_Link(
                'self',
                $object->getProfile()->getUri(),
                'application/activity+json'
            );
            $xrd->links[] = clone $link;
        }
    }

    /**
     * Find any explicit remote mentions. Accepted forms:
     *   Webfinger: @user@example.com
     *   Profile link:
     *
     * @param Profile $sender
     * @param string  $text   input markup text
     * @param $mentions
     *
     * @throws InvalidUrlException
     *
     * @return bool hook return value
     *
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     * @example.com/mublog/user
     *
     * @author GNU social
     */
    public function onEndFindMentions(Profile $sender, $text, &$mentions)
    {
        $matches = [];

        foreach (self::extractWebfingerIds($text, '@') as $wmatch) {
            list($target, $pos) = $wmatch;
            $this->log(LOG_INFO, "Checking webfinger person '{$target}'");
            $profile = null;
            try {
                $aprofile = Activitypub_profile::ensure_webfinger($target);
                $profile  = $aprofile->local_profile();
            } catch (Exception $e) {
                $this->log(LOG_ERR, 'Webfinger check failed: ' . $e->getMessage());
                continue;
            }
            assert($profile instanceof Profile);

            $displayName = !empty($profile->nickname) && mb_strlen($profile->nickname) < mb_strlen($target)
                        ? $profile->getNickname()   // TODO: we could do getBestName() or getFullname() here
                        : $target;
            $url = $profile->getUri();
            if (!common_valid_http_url($url)) {
                $url = $profile->getUrl();
            }
            $matches[$pos] = ['mentioned' => [$profile],
                'type'                    => 'mention',
                'text'                    => $displayName,
                'position'                => $pos,
                'length'                  => mb_strlen($target),
                'url'                     => $url, ];
        }

        foreach (self::extractUrlMentions($text) as $wmatch) {
            list($target, $pos) = $wmatch;
            $schemes            = ['https', 'http'];
            foreach ($schemes as $scheme) {
                $url = "{$scheme}://{$target}";
                $this->log(LOG_INFO, "Checking profile address '{$url}'");
                try {
                    $aprofile    = Activitypub_profile::fromUri($url);
                    $profile     = $aprofile->local_profile();
                    $displayName = !empty($profile->nickname) && mb_strlen($profile->nickname) < mb_strlen($target) ?
                                        $profile->nickname : $target;
                    $matches[$pos] = ['mentioned' => [$profile],
                        'type'                    => 'mention',
                        'text'                    => $displayName,
                        'position'                => $pos,
                        'length'                  => mb_strlen($target),
                        'url'                     => $profile->getUrl(), ];
                    break;
                } catch (Exception $e) {
                    $this->log(LOG_ERR, 'Profile check failed: ' . $e->getMessage());
                }
            }
        }

        foreach ($mentions as $i => $other) {
            // If we share a common prefix with a local user, override it!
            $pos = $other['position'];
            if (isset($matches[$pos])) {
                $mentions[$i] = $matches[$pos];
                unset($matches[$pos]);
            }
        }
        foreach ($matches as $mention) {
            $mentions[] = $mention;
        }

        return true;
    }

    /**
     * Allow remote profile references to be used in commands:
     *   sub update@status.net
     *   whois evan@identi.ca
     *   reply http://identi.ca/evan hey what's up
     *
     * @param Command $command
     * @param string  $arg
     * @param Profile &$profile
     *
     * @return bool hook return code
     *
     * @author GNU social
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public function onStartCommandGetProfile($command, $arg, &$profile)
    {
        try {
            $aprofile = $this->pull_remote_profile($arg);
            $profile  = $aprofile->local_profile();
        } catch (Exception $e) {
            // No remote ActivityPub profile found
            return true;
        }

        return false;
    }

    // Discovery Events

    /**
     * Profile from URI.
     *
     * @author GNU social
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     *
     * @param string  $uri
     * @param Profile &$profile in/out param: Profile got from URI
     *
     * @return mixed hook return code
     */
    public function onStartGetProfileFromURI($uri, &$profile)
    {
        try {
            $profile = Activitypub_explorer::get_profile_from_url($uri);
            return false;
        } catch (Exception $e) {
            return true; // It's not an ActivityPub profile as far as we know, continue event handling
        }
    }

    /**
     * Try to grab and store the remote profile by the given uri
     *
     * @param string  $uri
     * @param Profile &$profile
     *
     * @return bool
     */
    public function onRemoteFollowPullProfile(string $uri, ?Profile &$profile): bool
    {
        try {
            $aprofile = $this->pull_remote_profile($uri);
            if ($aprofile instanceof Activitypub_profile) {
                $profile = $aprofile->local_profile();
            }
        } catch (Exception $e) {
            // No remote ActivityPub profile found
            return true;
        }

        return is_null($profile);
    }

    // Delivery Events

    /**
     * Having established a remote subscription, send a notification to the
     * remote ActivityPub profile's endpoint.
     *
     * @param Profile $profile subscriber
     * @param Profile $other   subscribee
     *
     * @throws HTTP_Request2_Exception
     *
     * @return bool return value
     *
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public function onStartSubscribe(Profile $profile, Profile $other)
    {
        if (!$profile->isLocal()) {
            return true;
        }

        $other = Activitypub_profile::getKV('profile_id', $other->getID());
        if (!$other instanceof Activitypub_profile) {
            return true;
        }

        $postman = new Activitypub_postman($profile, [$other]);
        $postman->follow();

        return true;
    }

    /**
     * Notify remote server on unsubscribe.
     *
     * @param Profile $profile
     * @param Profile $other
     *
     * @throws HTTP_Request2_Exception
     *
     * @return bool return value
     *
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public function onStartUnsubscribe(Profile $profile, Profile $other)
    {
        if (!$profile->isLocal()) {
            return true;
        }

        $other = Activitypub_profile::getKV('profile_id', $other->getID());
        if (!$other instanceof Activitypub_profile) {
            return true;
        }

        $postman = new Activitypub_postman($profile, [$other]);
        $postman->undo_follow();

        return true;
    }

    /**
     * Notify remote followers when a user gets deleted
     *
     * @param Action $action
     * @param User   $user   user being deleted
     */
    public function onEndDeleteUser(Action $action, User $user): void
    {
        $postman = new Activitypub_postman($user->getProfile());
        $postman->delete_profile();
    }

    /**
     * Federate private message
     *
     * @param Notice $message
     *
     * @return void
     */
    public function onSendDirectMessage(Notice $message): void
    {
        $from = $message->getProfile();
        if (!$from->isLocal()) {
            // nothing to do
            return;
        }

        $to = Activitypub_profile::from_profile_collection(
            $message->getAttentionProfiles()
        );

        if (!empty($to)) {
            $postman = new Activitypub_postman($from, $to);
            $postman->create_direct_note($message);
        }
    }

    /**
     * Override the "from ActivityPub" bit in notice lists to link to the
     * original post and show the domain it came from.
     *
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     *
     * @param $notice
     * @param $name
     * @param $url
     * @param $title
     *
     * @throws Exception
     *
     * @return mixed hook return code
     */
    public function onStartNoticeSourceLink($notice, &$name, &$url, &$title)
    {
        // If we don't handle this, keep the event handler going
        if (!in_array($notice->source, ['ActivityPub', 'share'])) {
            return true;
        }

        try {
            $url = $notice->getUrl();
            // If getUrl() throws exception, $url is never set

            $bits   = parse_url($url);
            $domain = $bits['host'];
            if (substr($domain, 0, 4) == 'www.') {
                $name = substr($domain, 4);
            } else {
                $name = $domain;
            }

            // TRANS: Title. %s is a domain name.
            $title = sprintf(_m('Sent from %s via ActivityPub'), $domain);

            // Abort event handler, we have a name and URL!
            return false;
        } catch (InvalidUrlException $e) {
            // This just means we don't have the notice source data
            return true;
        }
    }
}

/**
 * Plugin return handler
 */
class ActivityPubReturn
{
    /**
     * Return a valid answer
     *
     * @param string $res
     * @param int    $code Status Code
     *
     * @return void
     *
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public static function answer($res = '', $code = 202)
    {
        http_response_code($code);
        header('Content-Type: application/activity+json');
        echo json_encode($res, JSON_UNESCAPED_SLASHES | (isset($_GET['pretty']) ? JSON_PRETTY_PRINT : null));
        exit;
    }

    /**
     * Return an error
     *
     * @param string $m
     * @param int    $code Status Code
     *
     * @return void
     *
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public static function error($m, $code = 400)
    {
        http_response_code($code);
        header('Content-Type: application/activity+json');
        $res[] = Activitypub_error::error_message_to_array($m);
        echo json_encode($res, JSON_UNESCAPED_SLASHES);
        exit;
    }
}


code:
# Contributing

When contributing to this repository, please first discuss the change you wish to make via issue,
email, or any other method with the owners of this repository before making a change.

Please note we have a code of conduct, please follow it in all your interactions with the project.

# Coding Style
- We follow every [PSR-12](https://www.php-fig.org/psr/psr-12/) ...
- ... except camelCase, that's too bad, we use snake_case

## Merge Request Process

1. Ensure you strip any trailing spaces off
2. Increase the version numbers in any examples files and the README.md to the new version that this
   Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/).
3. You may merge the Pull Request in once you have the sign-off of two other developers, or if you
   do not have permission to do that, you may request the second reviewer to merge it for you.

## Code of Conduct

### Our Pledge

In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
nationality, personal appearance, race, religion, or sexual identity and
orientation.

### Our Standards

Examples of behavior that contributes to creating a positive environment
include:

* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members

Examples of unacceptable behavior by participants include:

* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
  address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
  professional setting

### Our Responsibilities

Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.

Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.

### Scope

This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.

### Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at diogo@fc.up.pt. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.

Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.

### Attribution

This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at [http://contributor-covenant.org/version/1/4][version]

[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/