[DOCUMENTATION][REFACTOR] Add documentation to all flagged function and do some small cleanup

This commit is contained in:
Hugo Sales 2020-11-06 19:47:15 +00:00 committed by Hugo Sales
parent 67d4702743
commit e8feb2ae84
Signed by: someonewithpc
GPG Key ID: 7D0C7EAFC9D835A0
21 changed files with 363 additions and 92 deletions

View File

@ -28,6 +28,7 @@ use App\Core\Log;
use App\Entity\Avatar; use App\Entity\Avatar;
use App\Entity\File; use App\Entity\File;
use App\Util\Common; use App\Util\Common;
use App\Util\Exception\ClientException;
use Component\Media\Exception\NoAvatarException; use Component\Media\Exception\NoAvatarException;
use Exception; use Exception;
use Symfony\Component\Asset\Package; use Symfony\Component\Asset\Package;
@ -39,6 +40,9 @@ use Symfony\Component\HttpFoundation\Response;
abstract class Utils abstract class Utils
{ {
/**
* Perform file validation (checks and normalization) and store the given file
*/
public static function validateAndStoreFile(SymfonyFile $sfile, public static function validateAndStoreFile(SymfonyFile $sfile,
string $dest_dir, string $dest_dir,
?string $title = null, ?string $title = null,
@ -87,7 +91,14 @@ abstract class Utils
return $response; return $response;
} }
public static function error($except, $id, array $res) /**
* 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)) { switch (count($res)) {
case 0: case 0:
@ -96,23 +107,15 @@ abstract class Utils
return $res[0]; return $res[0];
default: default:
Log::error('Media query returned more than one result for identifier: \"' . $id . '\"'); Log::error('Media query returned more than one result for identifier: \"' . $id . '\"');
throw new Exception(_m('Internal server error')); throw new ClientException(_m('Internal server error'));
} }
} }
public static function getAvatar(string $nickname) /**
{ * Get the file info by id
return self::error(NoAvatarException::class, *
$nickname, * Returns the file's hash, mimetype and title
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]);
}));
}
public static function getFileInfo(int $id) public static function getFileInfo(int $id)
{ {
return self::error(NoSuchFileException::class, return self::error(NoSuchFileException::class,
@ -126,14 +129,62 @@ abstract class Utils
})); }));
} }
public static function getAttachmentFileInfo(int $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 = self::getFileInfo($id);
$res['file_path'] = Common::config('attachments', 'dir') . $res['file_hash']; $res['file_path'] = Common::config('attachments', 'dir') . $res['file_hash'];
return $res; return $res;
} }
public static function getAvatarFileInfo(string $nickname) // ----- 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 { try {
$res = self::error(NoAvatarException::class, $res = self::error(NoAvatarException::class,
@ -154,24 +205,4 @@ abstract class Utils
return ['file_path' => $filepath, 'mimetype' => 'image/svg+xml', 'title' => null]; return ['file_path' => $filepath, 'mimetype' => 'image/svg+xml', 'title' => null];
} }
} }
public static function getAvatarUrl(?string $nickname = null)
{
if ($nickname == null) {
$user = Common::user();
if ($user != null) {
$nickname = $user->getNickname();
} else {
throw new Exception('No user is logged in and no avatar provided to `getAvatarUrl`');
}
}
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'));
});
}
} }

View File

@ -38,10 +38,14 @@ use Symfony\Component\Form\Extension\Core\Type\TextareaType;
class Posting extends Module class Posting extends Module
{ {
public function onStartTwigPopulateVars(array &$vars) /**
* 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) { if (($user = Common::user()) == null) {
return; return Event::next;
} }
$actor_id = $user->getId(); $actor_id = $user->getId();
@ -79,9 +83,20 @@ class Posting extends Module
return Event::next; 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) 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' => $content, 'is_local' => $is_local, 'reply_to' => $reply_to, 'repeat_of' => $repeat_of]); $note = Note::create([
'gsactor_id' => $actor_id,
'content' => Security::sanitize($content),
'is_local' => $is_local,
'reply_to' => $reply_to,
'repeat_of' => $repeat_of,
]);
$files = []; $files = [];
foreach ($attachments as $f) { foreach ($attachments as $f) {
$nf = Media::validateAndStoreFile($f, Common::config('attachments', 'dir'), $nf = Media::validateAndStoreFile($f, Common::config('attachments', 'dir'),

View File

@ -32,9 +32,16 @@ use Symfony\Component\HttpFoundation\Request;
class Favourite extends Module class Favourite extends Module
{ {
/**
* HTML rendering event that adds the favourite form as a note
* action, if a user is logged in
*/
public function onAddNoteActions(Request $request, Note $note, array &$actions) public function onAddNoteActions(Request $request, Note $note, array &$actions)
{ {
$user = Common::user(); if (($user = Common::user()) == null) {
return Event::next;
}
$opts = ['note_id' => $note->getId(), 'gsactor_id' => $user->getId()]; $opts = ['note_id' => $note->getId(), 'gsactor_id' => $user->getId()];
$is_set = DB::find('favourite', $opts) != null; $is_set = DB::find('favourite', $opts) != null;
$form = Form::create([ $form = Form::create([
@ -42,6 +49,8 @@ class Favourite extends Module
['note_id', HiddenType::class, ['data' => $note->getId()]], ['note_id', HiddenType::class, ['data' => $note->getId()]],
['favourite', SubmitType::class, ['label' => ' ']], ['favourite', SubmitType::class, ['label' => ' ']],
]); ]);
// Form handler
$ret = self::noteActionHandle($request, $form, $note, 'favourite', function ($note, $data) use ($opts) { $ret = self::noteActionHandle($request, $form, $note, 'favourite', function ($note, $data) use ($opts) {
$fave = DB::find('favourite', $opts); $fave = DB::find('favourite', $opts);
if (!$data['is_set'] && ($fave == null)) { if (!$data['is_set'] && ($fave == null)) {
@ -53,9 +62,11 @@ class Favourite extends Module
} }
return Event::stop; return Event::stop;
}); });
if ($ret != null) { if ($ret != null) {
return $ret; return $ret;
} }
$actions[] = $form->createView(); $actions[] = $form->createView();
return Event::next; return Event::next;
} }

View File

@ -32,9 +32,16 @@ use Symfony\Component\HttpFoundation\Request;
class Repeat extends Module class Repeat extends Module
{ {
/**
* HTML rendering event that adds the repeat form as a note
* action, if a user is logged in
*/
public function onAddNoteActions(Request $request, Note $note, array &$actions) public function onAddNoteActions(Request $request, Note $note, array &$actions)
{ {
$user = Common::user(); if (($user = Common::user()) == null) {
return Event::next;
}
$opts = ['gsactor_id' => $user->getId(), 'repeat_of' => $note->getId()]; $opts = ['gsactor_id' => $user->getId(), 'repeat_of' => $note->getId()];
try { try {
$is_set = DB::findOneBy('note', $opts) != null; $is_set = DB::findOneBy('note', $opts) != null;
@ -47,6 +54,8 @@ class Repeat extends Module
['note_id', HiddenType::class, ['data' => $note->getId()]], ['note_id', HiddenType::class, ['data' => $note->getId()]],
['repeat', SubmitType::class, ['label' => ' ']], ['repeat', SubmitType::class, ['label' => ' ']],
]); ]);
// Handle form
$ret = self::noteActionHandle($request, $form, $note, 'repeat', function ($note, $data, $user) use ($opts) { $ret = self::noteActionHandle($request, $form, $note, 'repeat', function ($note, $data, $user) use ($opts) {
$note = DB::findOneBy('note', $opts); $note = DB::findOneBy('note', $opts);
if (!$data['is_set'] && $note == null) { if (!$data['is_set'] && $note == null) {
@ -63,6 +72,7 @@ class Repeat extends Module
} }
return Event::stop; return Event::stop;
}); });
if ($ret != null) { if ($ret != null) {
return $ret; return $ret;
} }

View File

@ -44,23 +44,43 @@ class Reply extends Module
$r->connect('note_reply', '/note/reply/{reply_to<\\d*>}', [self::class, 'replyController']); $r->connect('note_reply', '/note/reply/{reply_to<\\d*>}', [self::class, 'replyController']);
} }
/**
* HTML rendering event that adds the reply form as a note action,
* if a user is logged in
*/
public function onAddNoteActions(Request $request, Note $note, array &$actions) public function onAddNoteActions(Request $request, Note $note, array &$actions)
{ {
if (($user = Common::user()) == null) {
return Event::next;
}
$form = Form::create([ $form = Form::create([
['content', HiddenType::class, ['label' => ' ', 'required' => false]], ['content', HiddenType::class, ['label' => ' ', 'required' => false]],
['attachments', HiddenType::class, ['label' => ' ', 'required' => false]], ['attachments', HiddenType::class, ['label' => ' ', 'required' => false]],
['note_id', HiddenType::class, ['data' => $note->getId()]], ['note_id', HiddenType::class, ['data' => $note->getId()]],
['reply', SubmitType::class, ['label' => ' ']], ['reply', SubmitType::class, ['label' => ' ']],
]); ]);
// Handle form
$ret = self::noteActionHandle($request, $form, $note, 'reply', function ($note, $data) { $ret = self::noteActionHandle($request, $form, $note, 'reply', function ($note, $data) {
if ($data['content'] !== null) { if ($data['content'] !== null) {
// JS submitted // JS submitted
// TODO DO THE THING // TODO Implement in JS
$actor_id = $user->getId();
Posting::storeNote(
$actor_id,
$data['content'],
$data['attachments'],
$is_local = true,
$data['reply_to'],
$repeat_of = null
);
} else { } else {
// JS disabled, redirect // JS disabled, redirect
throw new RedirectException('note_reply', ['reply_to' => $note->getId()]); throw new RedirectException('note_reply', ['reply_to' => $note->getId()]);
} }
}); });
if ($ret != null) { if ($ret != null) {
return $ret; return $ret;
} }
@ -68,6 +88,9 @@ class Reply extends Module
return Event::next; return Event::next;
} }
/**
* Controller for the note reply non-JS page
*/
public function replyController(Request $request, string $reply_to) public function replyController(Request $request, string $reply_to)
{ {
$user = Common::ensureLoggedIn(); $user = Common::ensureLoggedIn();
@ -88,7 +111,14 @@ class Reply extends Module
if ($form->isSubmitted()) { if ($form->isSubmitted()) {
$data = $form->getData(); $data = $form->getData();
if ($form->isValid()) { if ($form->isValid()) {
Posting::storeNote($actor_id, $data['content'], $data['attachments'], $is_local = true, $data['reply_to'], null); Posting::storeNote(
$actor_id,
$data['content'],
$data['attachments'],
$is_local = true,
$data['reply_to'],
$repeat_of = null
);
} else { } else {
throw new InvalidFormException(); throw new InvalidFormException();
} }

View File

@ -45,8 +45,13 @@ use Symfony\Component\HttpFoundation\Request;
class AdminPanel extends Controller class AdminPanel extends Controller
{ {
/**
* Handler for the site admin panel section. Allows the
* administrator to change various configuration options
*/
public function site(Request $request) public function site(Request $request)
{ {
// TODO CHECK PERMISSION
$defaults = Common::getConfigDefaults(); $defaults = Common::getConfigDefaults();
$options = []; $options = [];
foreach ($defaults as $key => $inner) { foreach ($defaults as $key => $inner) {

View File

@ -25,6 +25,9 @@ use Symfony\Component\Validator\Constraints\NotBlank;
class Security extends Controller class Security extends Controller
{ {
/**
* Log a user in
*/
public function login(AuthenticationUtils $authenticationUtils) public function login(AuthenticationUtils $authenticationUtils)
{ {
if ($this->getUser()) { if ($this->getUser()) {
@ -44,6 +47,10 @@ class Security extends Controller
throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.'); throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.');
} }
/**
* Register a user, making sure the nickname is not reserved and
* possibly sending a confirmation email
*/
public function register(Request $request, public function register(Request $request,
EmailVerifier $email_verifier, EmailVerifier $email_verifier,
GuardAuthenticatorHandler $guard_handler, GuardAuthenticatorHandler $guard_handler,

View File

@ -65,6 +65,9 @@ use Symfony\Component\HttpFoundation\Request;
class UserPanel extends AbstractController class UserPanel extends AbstractController
{ {
/**
* Local user personal information panel
*/
public function personal_info(Request $request) public function personal_info(Request $request)
{ {
$user = Common::user(); $user = Common::user();
@ -85,6 +88,9 @@ class UserPanel extends AbstractController
return ['_template' => 'settings/profile.html.twig', 'prof' => $form->createView()]; return ['_template' => 'settings/profile.html.twig', 'prof' => $form->createView()];
} }
/**
* Local user account information panel
*/
public function account(Request $request) public function account(Request $request)
{ {
$user = Common::user(); $user = Common::user();
@ -103,6 +109,9 @@ class UserPanel extends AbstractController
return ['_template' => 'settings/account.html.twig', 'acc' => $form->createView()]; return ['_template' => 'settings/account.html.twig', 'acc' => $form->createView()];
} }
/**
* Local user avatar panel
*/
public function avatar(Request $request) public function avatar(Request $request)
{ {
$form = Form::create([ $form = Form::create([
@ -160,6 +169,9 @@ class UserPanel extends AbstractController
return ['_template' => 'settings/avatar.html.twig', 'avatar' => $form->createView()]; return ['_template' => 'settings/avatar.html.twig', 'avatar' => $form->createView()];
} }
/**
* Local user notification settings tabbed panel
*/
public function notifications(Request $request) public function notifications(Request $request)
{ {
$schema = DB::getConnection()->getSchemaManager(); $schema = DB::getConnection()->getSchemaManager();

View File

@ -1,6 +1,7 @@
<?php <?php
// {{{ License // {{{ License
// This file is part of GNU social - https://www.gnu.org/software/social // 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 // GNU social is free software: you can redistribute it and/or modify
@ -15,6 +16,7 @@
// //
// You should have received a copy of the GNU Affero General Public License // 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/>. // along with GNU social. If not, see <http://www.gnu.org/licenses/>.
// }}} // }}}
namespace App\Core; namespace App\Core;
@ -125,6 +127,10 @@ abstract class Cache
return self::$pools[$pool]->delete($key); return self::$pools[$pool]->delete($key);
} }
/**
* Retrieve a list from the cache, with a different implementation
* for redis and others, trimming to $max_count if given
*/
public static function getList(string $key, callable $calculate, string $pool = 'default', int $max_count = -1, float $beta = 1.0): array public static function getList(string $key, callable $calculate, string $pool = 'default', int $max_count = -1, float $beta = 1.0): array
{ {
if (isset(self::$redis[$pool])) { if (isset(self::$redis[$pool])) {
@ -149,6 +155,7 @@ abstract class Cache
self::$redis[$pool]->lPush($key, ...$res); self::$redis[$pool]->lPush($key, ...$res);
} }
} }
self::$redis[$pool]->lTrim($key, 0, $max_count);
return self::$redis[$pool]->lRange($key, 0, $max_count); return self::$redis[$pool]->lRange($key, 0, $max_count);
} else { } else {
$keys = self::getKeyList($key, $max_count, $beta); $keys = self::getKeyList($key, $max_count, $beta);
@ -160,6 +167,9 @@ abstract class Cache
} }
} }
/**
* Push a value to the list, if not using redis, get, add to subkey and set
*/
public static function pushList(string $key, mixed $value, string $pool = 'default', int $max_count = 64, float $beta = 1.0): void public static function pushList(string $key, mixed $value, string $pool = 'default', int $max_count = 64, float $beta = 1.0): void
{ {
if (isset(self::$redis[$pool])) { if (isset(self::$redis[$pool])) {
@ -179,6 +189,9 @@ abstract class Cache
} }
} }
/**
* Delete a whole list at $key, if not using redis, recurse into keys
*/
public static function deleteList(string $key, string $pool = 'default'): bool public static function deleteList(string $key, string $pool = 'default'): bool
{ {
if (isset(self::$redis[$pool])) { if (isset(self::$redis[$pool])) {
@ -192,6 +205,9 @@ abstract class Cache
} }
} }
/**
* On non-Redis, get the list of keys that store a list at $key
*/
private static function getKeyList(string $key, int $max_count, string $pool, float $beta): RingBuffer private static function getKeyList(string $key, int $max_count, string $pool, float $beta): RingBuffer
{ {
// Get the current keys associated with a list. If the cache // Get the current keys associated with a list. If the cache

View File

@ -57,6 +57,9 @@ class Controller extends AbstractController implements EventSubscriberInterface
} }
} }
/**
* Symfony event when it's searching for which controller to use
*/
public function onKernelController(ControllerEvent $event) public function onKernelController(ControllerEvent $event)
{ {
$controller = $event->getController(); $controller = $event->getController();
@ -69,6 +72,9 @@ class Controller extends AbstractController implements EventSubscriberInterface
return $event; return $event;
} }
/**
* Symfony event when the controller result is not a Response object
*/
public function onKernelView(ViewEvent $event) public function onKernelView(ViewEvent $event)
{ {
$request = $event->getRequest(); $request = $event->getRequest();
@ -99,6 +105,9 @@ class Controller extends AbstractController implements EventSubscriberInterface
return $event; return $event;
} }
/**
* Symfony event when the controller throws an exception
*/
public function onKernelException(ExceptionEvent $event) public function onKernelException(ExceptionEvent $event)
{ {
$except = $event->getThrowable(); $except = $event->getThrowable();

View File

@ -48,6 +48,9 @@ abstract class DB
self::$em = $m; self::$em = $m;
} }
/**
* Perform a Doctrine Query Language query
*/
public static function dql(string $query, array $params = []) public static function dql(string $query, array $params = [])
{ {
$q = new Query(self::$em); $q = new Query(self::$em);
@ -58,6 +61,11 @@ abstract class DB
return $q->getResult(); return $q->getResult();
} }
/**
* Perform a native, parameterized, SQL query. $entities is a map
* from table aliases to class names. Replaces '{select}' in
* $query with the appropriate select list
*/
public static function sql(string $query, array $entities, array $params = []) public static function sql(string $query, array $entities, array $params = [])
{ {
$rsm = new ResultSetMappingBuilder(self::$em); $rsm = new ResultSetMappingBuilder(self::$em);
@ -69,15 +77,23 @@ abstract class DB
foreach ($params as $k => $v) { foreach ($params as $k => $v) {
$q->setParameter($k, $v); $q->setParameter($k, $v);
} }
// dump($q);
// die();
return $q->getResult(); return $q->getResult();
} }
private static array $find_by_ops = ['or', 'and', 'eq', 'neq', 'lt', 'lte', /**
* A list of possible operations needed in self::buildExpression
*/
private static array $find_by_ops = [
'or', 'and', 'eq', 'neq', 'lt', 'lte',
'gt', 'gte', 'is_null', 'in', 'not_in', 'gt', 'gte', 'is_null', 'in', 'not_in',
'contains', 'member_of', 'starts_with', 'ends_with', ]; 'contains', 'member_of', 'starts_with', 'ends_with',
];
/**
* Build a Doctrine Criteria expression from the given $criteria.
*
* @see self::findBy for the syntax
*/
private static function buildExpression(ExpressionBuilder $eb, array $criteria) private static function buildExpression(ExpressionBuilder $eb, array $criteria)
{ {
$expressions = []; $expressions = [];
@ -100,6 +116,13 @@ abstract class DB
return $expressions; return $expressions;
} }
/**
* Query $table according to $criteria. If $criteria's keys are
* one of self::$find_by_ops (and, or, etc), build a subexpression
* with that operator and recurse. Examples of $criteria are
* `['and' => ['lt' => ['foo' => 4], 'gte' => ['bar' => 2]]]` or
* `['in' => ['foo', 'bar']]`
*/
public static function findBy(string $table, array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array public static function findBy(string $table, array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
{ {
$criteria = array_change_key_case($criteria); $criteria = array_change_key_case($criteria);
@ -113,6 +136,9 @@ abstract class DB
} }
} }
/**
* Return the first element of the result of @see self::findBy
*/
public static function findOneBy(string $table, array $criteria, ?array $orderBy = null, ?int $offset = null) public static function findOneBy(string $table, array $criteria, ?array $orderBy = null, ?int $offset = null)
{ {
$res = self::findBy($table, $criteria, $orderBy, 1, $offset); $res = self::findBy($table, $criteria, $orderBy, 1, $offset);
@ -123,18 +149,21 @@ abstract class DB
} }
} }
/**
* Intercept static function calls to allow refering to entities
* without writing the namespace (which is deduced from the call
* context)
*/
public static function __callStatic(string $name, array $args) public static function __callStatic(string $name, array $args)
{ {
foreach (['find', 'getReference', 'getPartialReference', 'getRepository'] as $m) { // TODO Plugins
// TODO Plugins // If the method is one of the following and the first argument doesn't look like a FQCN, add the prefix
$pref = '\App\Entity\\'; $pref = '\App\Entity\\';
if ($name == $m && Formatting::startsWith($name, $pref) === false) { if (in_array($name, ['find', 'getReference', 'getPartialReference', 'getRepository'])
$args[0] = $pref . ucfirst(Formatting::snakeCaseToCamelCase($args[0])); && preg_match('/\\\\/', $args[0]) === 0
} && Formatting::startsWith($args[0], $pref) === false) {
} $args[0] = $pref . ucfirst(Formatting::snakeCaseToCamelCase($args[0]));
$args[0] = preg_replace('/Gsactor/', 'GSActor', $args[0]);
if (isset($args[0]) && is_string($args[0])) {
$args[0] = preg_replace('/Gsactor/', 'GSActor', $args[0] ?? '');
} }
return self::$em->{$name}(...$args); return self::$em->{$name}(...$args);

View File

@ -25,8 +25,18 @@ use App\Core\DB\DB;
use App\Util\Formatting; use App\Util\Formatting;
use DateTime; use DateTime;
class Entity /**
* Base class to all entities, with some utilities
*/
abstract class Entity
{ {
/**
* Create an instance of the called class or fill in the
* properties of $obj with the associative array $args. Doesn't
* persist the result
*
* @param null|mixed $obj
*/
public static function create(array $args, $obj = null) public static function create(array $args, $obj = null)
{ {
$class = get_called_class(); $class = get_called_class();
@ -43,12 +53,21 @@ class Entity
return $obj; return $obj;
} }
/**
* Create a new instance, but check for duplicates
*/
public static function createOrUpdate(array $args, array $find_by) public static function createOrUpdate(array $args, array $find_by)
{ {
$table = Formatting::camelCaseToSnakeCase(get_called_class()); $table = Formatting::camelCaseToSnakeCase(get_called_class());
return self::create($args, DB::findBy($table, $find_by)[0]); return self::create($args, DB::findOneBy($table, $find_by));
} }
/**
* Remove a given $obj or whatever is found by `DB::findBy(..., $args)`
* from the database. Doesn't flush
*
* @param null|mixed $obj
*/
public static function remove(array $args, $obj = null) public static function remove(array $args, $obj = null)
{ {
$class = '\\' . get_called_class(); $class = '\\' . get_called_class();

View File

@ -46,6 +46,9 @@ abstract class Form
self::$form_factory = $ff; self::$form_factory = $ff;
} }
/**
* Create a form with the given associative array $form as fields
*/
public static function create(array $form, public static function create(array $form,
?object $target = null, ?object $target = null,
array $extra_data = [], array $extra_data = [],
@ -79,11 +82,19 @@ abstract class Form
return $fb->getForm(); return $fb->getForm();
} }
/**
* Whether the given $field of $form has the `required` property
* set, defaults to true
*/
public static function isRequired(array $form, string $field): bool public static function isRequired(array $form, string $field): bool
{ {
return $form[$field][2]['required'] ?? true; return $form[$field][2]['required'] ?? true;
} }
/**
* Handle the full life cycle of a form. Creates it with @see
* self::create and inserts the submitted values into the database
*/
public static function handle(array $form_definition, Request $request, object $target, array $extra_args = [], ?callable $extra_step = null, array $create_args = []) public static function handle(array $form_definition, Request $request, object $target, array $extra_args = [], ?callable $extra_step = null, array $create_args = [])
{ {
$form = self::create($form_definition, $target, ...$create_args); $form = self::create($form_definition, $target, ...$create_args);

View File

@ -234,6 +234,15 @@ abstract class I18n
]; ];
} }
/**
* Format the given associative array $messages in the ICU
* translation format, with the given $params. Allows for a
* declarative use of the translation engine, for example
* `formatICU(['she' => ['She has one foo', 'She has many foo'],
* 'he' => ['He has one foo', 'He has many foo']], ['she' => 1])`
*
* @see http://userguide.icu-project.org/formatparse/messages
*/
public static function formatICU(array $messages, array $params): string public static function formatICU(array $messages, array $params): string
{ {
$res = ''; $res = '';
@ -293,10 +302,12 @@ abstract class I18n
* *
* @todo add parameters * @todo add parameters
*/ */
function _m(): string function _m(...$args): string
{ {
$domain = I18n::_mdomain(debug_backtrace()[0]['file']); // Get the file where this function was called from, reducing the
$args = func_get_args(); // memory and performance inpact by not returning the arguments,
// and only 2 frames (this and previous)
$domain = I18n::_mdomain(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS)[0]['file'], 2);
switch (count($args)) { switch (count($args)) {
case 1: case 1:
// Empty parameters, simple message // Empty parameters, simple message

View File

@ -75,8 +75,10 @@ class TransExtractor extends AbstractFileExtractor implements ExtractorInterface
], ],
]; ];
// TODO probably shouldn't be done this way
// {{{Code from PhpExtractor // {{{Code from PhpExtractor
// See vendor/symfony/translation/Extractor/PhpExtractor.php
//
const MESSAGE_TOKEN = 300; const MESSAGE_TOKEN = 300;
const METHOD_ARGUMENTS_TOKEN = 1000; const METHOD_ARGUMENTS_TOKEN = 1000;
const DOMAIN_TOKEN = 1001; const DOMAIN_TOKEN = 1001;
@ -143,6 +145,9 @@ class TransExtractor extends AbstractFileExtractor implements ExtractorInterface
} }
} }
/**
* {@inheritdoc}
*/
private function skipMethodArgument(\Iterator $tokenIterator) private function skipMethodArgument(\Iterator $tokenIterator)
{ {
$openBraces = 0; $openBraces = 0;
@ -284,6 +289,9 @@ class TransExtractor extends AbstractFileExtractor implements ExtractorInterface
} }
} }
/**
* Store the $message in the message catalogue $mc
*/
private function store(MessageCatalogue $mc, string $message, private function store(MessageCatalogue $mc, string $message,
string $domain, string $filename, ?int $line_no = null) string $domain, string $filename, ?int $line_no = null)
{ {
@ -293,6 +301,11 @@ class TransExtractor extends AbstractFileExtractor implements ExtractorInterface
$mc->setMetadata($message, $metadata, $domain); $mc->setMetadata($message, $metadata, $domain);
} }
/**
* Calls `::_m_dynamic` from the class defined in $filename and
* stores the results in the catalogue. For cases when the
* translation can't be done in a static (non-PHP) file
*/
private function storeDynamic(MessageCatalogue $mc, string $filename) private function storeDynamic(MessageCatalogue $mc, string $filename)
{ {
require_once $filename; require_once $filename;

View File

@ -24,8 +24,16 @@ use App\Util\Common;
use Symfony\Component\Form\Form; use Symfony\Component\Form\Form;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
class Module /**
* Base class for all GNU social modules (plugins and components)
*/
abstract class Module
{ {
/**
* Serialize the class to store in the cache
*
* @param mixed $state
*/
public static function __set_state($state) public static function __set_state($state)
{ {
$class = get_called_class(); $class = get_called_class();
@ -36,6 +44,10 @@ class Module
return $obj; return $obj;
} }
/**
* Handle the $form submission for the note action for note if
* $note->getId() == $data['note_id']
*/
public static function noteActionHandle(Request $request, Form $form, Note $note, string $form_name, callable $handle) public static function noteActionHandle(Request $request, Form $form, Note $note, string $form_name, callable $handle)
{ {
if ('POST' === $request->getMethod() && $request->request->has($form_name)) { if ('POST' === $request->getMethod() && $request->request->has($form_name)) {

View File

@ -60,6 +60,9 @@ class ModuleManager
protected array $modules = []; protected array $modules = [];
protected array $events = []; protected array $events = [];
/**
* Add the $fqcn class from $path as a module
*/
public function add(string $fqcn, string $path) public function add(string $fqcn, string $path)
{ {
list($type, $module) = preg_split('/\\\\/', $fqcn, 0, PREG_SPLIT_NO_EMPTY); list($type, $module) = preg_split('/\\\\/', $fqcn, 0, PREG_SPLIT_NO_EMPTY);
@ -69,6 +72,9 @@ class ModuleManager
$this->modules[$id] = $obj; $this->modules[$id] = $obj;
} }
/**
* Container-build-time step that preprocesses the registering of events
*/
public function preRegisterEvents() public function preRegisterEvents()
{ {
foreach ($this->modules as $id => $obj) { foreach ($this->modules as $id => $obj) {
@ -83,6 +89,9 @@ class ModuleManager
} }
} }
/**
* Compiler pass responsible for registering all modules
*/
public static function process(?ContainerBuilder $container = null) public static function process(?ContainerBuilder $container = null)
{ {
$module_paths = array_merge(glob(INSTALLDIR . '/components/*/*.php'), glob(INSTALLDIR . '/plugins/*/*.php')); $module_paths = array_merge(glob(INSTALLDIR . '/components/*/*.php'), glob(INSTALLDIR . '/plugins/*/*.php'));
@ -113,6 +122,11 @@ class ModuleManager
file_put_contents(CACHE_FILE, "<?php\nreturn " . var_export($module_manager, true) . ';'); file_put_contents(CACHE_FILE, "<?php\nreturn " . var_export($module_manager, true) . ';');
} }
/**
* Serialize this class, for dumping into the cache
*
* @param mixed $state
*/
public static function __set_state($state) public static function __set_state($state)
{ {
$obj = new self(); $obj = new self();
@ -121,6 +135,10 @@ class ModuleManager
return $obj; return $obj;
} }
/**
* Load the modules at runtime. In production requires the cache
* file to exist, in dev it rebuilds this cache
*/
public function loadModules() public function loadModules()
{ {
if ($_ENV['APP_ENV'] == 'prod' && !file_exists(CACHE_FILE)) { if ($_ENV['APP_ENV'] == 'prod' && !file_exists(CACHE_FILE)) {

View File

@ -232,6 +232,11 @@ class Note extends Entity
return null; return null;
} }
/**
* Whether this note is visible to the given actor
*
* @param mixed $a
*/
public function isVisibleTo(/* GSActor|LocalUser */ $a): bool public function isVisibleTo(/* GSActor|LocalUser */ $a): bool
{ {
$scope = NoteScope::create($this->scope); $scope = NoteScope::create($this->scope);

View File

@ -75,6 +75,10 @@ class Kernel extends BaseKernel
} }
} }
/**
* Symfony framework function override responsible for registering
* bundles (similar to our modules)
*/
public function registerBundles(): iterable public function registerBundles(): iterable
{ {
$contents = require $this->getProjectDir() . '/config/bundles.php'; $contents = require $this->getProjectDir() . '/config/bundles.php';
@ -90,6 +94,11 @@ class Kernel extends BaseKernel
return dirname(__DIR__); return dirname(__DIR__);
} }
/**
* Configure the container. A 'compile-time' step in the Symfony
* framework that allows caching of the initialization of all
* services and modules
*/
protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader): void protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader): void
{ {
$container->addResource(new FileResource($this->getProjectDir() . '/config/bundles.php')); $container->addResource(new FileResource($this->getProjectDir() . '/config/bundles.php'));
@ -118,6 +127,9 @@ class Kernel extends BaseKernel
} }
} }
/**
* Configure HTTP(S) route to controller mapping
*/
protected function configureRoutes(RoutingConfigurator $routes): void protected function configureRoutes(RoutingConfigurator $routes): void
{ {
$config = \dirname(__DIR__) . '/config'; $config = \dirname(__DIR__) . '/config';
@ -131,6 +143,10 @@ class Kernel extends BaseKernel
} }
} }
/**
* 'Compile-time' step that builds the container, allowing us to
* define compiler passes
*/
protected function build(ContainerBuilder $container): void protected function build(ContainerBuilder $container): void
{ {
parent::build($container); parent::build($container);

View File

@ -23,7 +23,12 @@ use App\Util\Exception\ServerException;
abstract class Bitmap abstract class Bitmap
{ {
public static function _do(int $r, bool $instance) /**
* Convert an to or from an integer and an array of constants for
* each bit. If $instance, return an object with the corresponding
* properties set
*/
private static function _do(int $r, bool $instance)
{ {
$init = $r; $init = $r;
$class = get_called_class(); $class = get_called_class();

View File

@ -89,6 +89,15 @@ abstract class Common
return self::user()->getActor(); return self::user()->getActor();
} }
public static function userNickname(): ?string
{
if (($user = self::user()) == null) {
throw new NoLoggedInUser();
} else {
return $user->getNickname();
}
}
public static function ensureLoggedIn(): LocalUser public static function ensureLoggedIn(): LocalUser
{ {
if (($user = self::user()) == null) { if (($user = self::user()) == null) {
@ -114,32 +123,9 @@ abstract class Common
} }
} }
// function array_diff_recursive($arr1, $arr2) /**
// { * A recursive `array_diff`, while PHP itself doesn't provide one
// $outputDiff = []; */
// foreach ($arr1 as $key => $value) {
// // if the key exists in the second array, recursively call this function
// // if it is an array, otherwise check if the value is in arr2
// if (array_key_exists($key, $arr2)) {
// if (is_array($value)) {
// $recursiveDiff = self::array_diff_recursive($value, $arr2[$key]);
// if (count($recursiveDiff)) {
// $outputDiff[$key] = $recursiveDiff;
// }
// } else if (!in_array($value, $arr2)) {
// $outputDiff[$key] = $value;
// }
// } else if (!in_array($value, $arr2)) {
// // if the key is not in the second array, check if the value is in
// // the second array (this is a quirk of how array_diff works)
// $outputDiff[$key] = $value;
// }
// }
// return $outputDiff;
// }
public function array_diff_recursive(array $array1, array $array2) public function array_diff_recursive(array $array1, array $array2)
{ {
$difference = []; $difference = [];