diff --git a/components/Media/Utils.php b/components/Media/Utils.php index 38174c256e..d8ab47c9f2 100644 --- a/components/Media/Utils.php +++ b/components/Media/Utils.php @@ -28,6 +28,7 @@ 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; @@ -39,6 +40,9 @@ 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, @@ -87,7 +91,14 @@ abstract class Utils 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)) { case 0: @@ -96,23 +107,15 @@ abstract class Utils return $res[0]; default: 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) - { - 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 file info by id + * + * Returns the file's hash, mimetype and title + */ public static function getFileInfo(int $id) { 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['file_path'] = Common::config('attachments', 'dir') . $res['file_hash']; 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 { $res = self::error(NoAvatarException::class, @@ -154,24 +205,4 @@ abstract class Utils 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')); - }); - } } diff --git a/components/Posting/Posting.php b/components/Posting/Posting.php index 2ef8500371..b24cd4cd55 100644 --- a/components/Posting/Posting.php +++ b/components/Posting/Posting.php @@ -38,10 +38,14 @@ use Symfony\Component\Form\Extension\Core\Type\TextareaType; 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) { - return; + return Event::next; } $actor_id = $user->getId(); @@ -79,9 +83,20 @@ class Posting extends Module 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' => $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 = []; foreach ($attachments as $f) { $nf = Media::validateAndStoreFile($f, Common::config('attachments', 'dir'), diff --git a/plugins/Favourite/Favourite.php b/plugins/Favourite/Favourite.php index d8fcb0b047..1329df1974 100644 --- a/plugins/Favourite/Favourite.php +++ b/plugins/Favourite/Favourite.php @@ -32,9 +32,16 @@ use Symfony\Component\HttpFoundation\Request; 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) { - $user = Common::user(); + if (($user = Common::user()) == null) { + return Event::next; + } + $opts = ['note_id' => $note->getId(), 'gsactor_id' => $user->getId()]; $is_set = DB::find('favourite', $opts) != null; $form = Form::create([ @@ -42,6 +49,8 @@ class Favourite extends Module ['note_id', HiddenType::class, ['data' => $note->getId()]], ['favourite', SubmitType::class, ['label' => ' ']], ]); + + // Form handler $ret = self::noteActionHandle($request, $form, $note, 'favourite', function ($note, $data) use ($opts) { $fave = DB::find('favourite', $opts); if (!$data['is_set'] && ($fave == null)) { @@ -53,9 +62,11 @@ class Favourite extends Module } return Event::stop; }); + if ($ret != null) { return $ret; } + $actions[] = $form->createView(); return Event::next; } diff --git a/plugins/Repeat/Repeat.php b/plugins/Repeat/Repeat.php index 940aa7c5e7..fe84f23787 100644 --- a/plugins/Repeat/Repeat.php +++ b/plugins/Repeat/Repeat.php @@ -32,9 +32,16 @@ use Symfony\Component\HttpFoundation\Request; 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) { - $user = Common::user(); + if (($user = Common::user()) == null) { + return Event::next; + } + $opts = ['gsactor_id' => $user->getId(), 'repeat_of' => $note->getId()]; try { $is_set = DB::findOneBy('note', $opts) != null; @@ -47,6 +54,8 @@ class Repeat extends Module ['note_id', HiddenType::class, ['data' => $note->getId()]], ['repeat', SubmitType::class, ['label' => ' ']], ]); + + // Handle form $ret = self::noteActionHandle($request, $form, $note, 'repeat', function ($note, $data, $user) use ($opts) { $note = DB::findOneBy('note', $opts); if (!$data['is_set'] && $note == null) { @@ -63,6 +72,7 @@ class Repeat extends Module } return Event::stop; }); + if ($ret != null) { return $ret; } diff --git a/plugins/Reply/Reply.php b/plugins/Reply/Reply.php index e9909a9373..2aca3c3e06 100644 --- a/plugins/Reply/Reply.php +++ b/plugins/Reply/Reply.php @@ -44,23 +44,43 @@ class Reply extends Module $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) { + if (($user = Common::user()) == null) { + return Event::next; + } + $form = Form::create([ ['content', HiddenType::class, ['label' => ' ', 'required' => false]], ['attachments', HiddenType::class, ['label' => ' ', 'required' => false]], ['note_id', HiddenType::class, ['data' => $note->getId()]], ['reply', SubmitType::class, ['label' => ' ']], ]); + + // Handle form $ret = self::noteActionHandle($request, $form, $note, 'reply', function ($note, $data) { if ($data['content'] !== null) { // 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 { // JS disabled, redirect throw new RedirectException('note_reply', ['reply_to' => $note->getId()]); } }); + if ($ret != null) { return $ret; } @@ -68,6 +88,9 @@ class Reply extends Module return Event::next; } + /** + * Controller for the note reply non-JS page + */ public function replyController(Request $request, string $reply_to) { $user = Common::ensureLoggedIn(); @@ -88,7 +111,14 @@ class Reply extends Module if ($form->isSubmitted()) { $data = $form->getData(); 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 { throw new InvalidFormException(); } diff --git a/src/Controller/AdminPanel.php b/src/Controller/AdminPanel.php index 8b1cd89baf..129f793a6c 100644 --- a/src/Controller/AdminPanel.php +++ b/src/Controller/AdminPanel.php @@ -45,8 +45,13 @@ use Symfony\Component\HttpFoundation\Request; class AdminPanel extends Controller { + /** + * Handler for the site admin panel section. Allows the + * administrator to change various configuration options + */ public function site(Request $request) { + // TODO CHECK PERMISSION $defaults = Common::getConfigDefaults(); $options = []; foreach ($defaults as $key => $inner) { diff --git a/src/Controller/Security.php b/src/Controller/Security.php index 0c22b9a28b..20d781698a 100644 --- a/src/Controller/Security.php +++ b/src/Controller/Security.php @@ -25,6 +25,9 @@ use Symfony\Component\Validator\Constraints\NotBlank; class Security extends Controller { + /** + * Log a user in + */ public function login(AuthenticationUtils $authenticationUtils) { 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.'); } + /** + * Register a user, making sure the nickname is not reserved and + * possibly sending a confirmation email + */ public function register(Request $request, EmailVerifier $email_verifier, GuardAuthenticatorHandler $guard_handler, diff --git a/src/Controller/UserPanel.php b/src/Controller/UserPanel.php index f302dcb36c..78f5dcad88 100644 --- a/src/Controller/UserPanel.php +++ b/src/Controller/UserPanel.php @@ -65,6 +65,9 @@ use Symfony\Component\HttpFoundation\Request; class UserPanel extends AbstractController { + /** + * Local user personal information panel + */ public function personal_info(Request $request) { $user = Common::user(); @@ -85,6 +88,9 @@ class UserPanel extends AbstractController return ['_template' => 'settings/profile.html.twig', 'prof' => $form->createView()]; } + /** + * Local user account information panel + */ public function account(Request $request) { $user = Common::user(); @@ -103,6 +109,9 @@ class UserPanel extends AbstractController return ['_template' => 'settings/account.html.twig', 'acc' => $form->createView()]; } + /** + * Local user avatar panel + */ public function avatar(Request $request) { $form = Form::create([ @@ -160,6 +169,9 @@ class UserPanel extends AbstractController return ['_template' => 'settings/avatar.html.twig', 'avatar' => $form->createView()]; } + /** + * Local user notification settings tabbed panel + */ public function notifications(Request $request) { $schema = DB::getConnection()->getSchemaManager(); diff --git a/src/Core/Cache.php b/src/Core/Cache.php index f547b08d81..e40b09968c 100644 --- a/src/Core/Cache.php +++ b/src/Core/Cache.php @@ -1,6 +1,7 @@ . + // }}} namespace App\Core; @@ -125,6 +127,10 @@ abstract class Cache return self::$pools[$pool]->delete($key); } + /** + * Retrieve a list from the cache, with a different implementation + * for redis and others, trimming to $max_count if given + */ public static function getList(string $key, callable $calculate, string $pool = 'default', int $max_count = -1, float $beta = 1.0): array { if (isset(self::$redis[$pool])) { @@ -149,6 +155,7 @@ abstract class Cache self::$redis[$pool]->lPush($key, ...$res); } } + self::$redis[$pool]->lTrim($key, 0, $max_count); return self::$redis[$pool]->lRange($key, 0, $max_count); } else { $keys = self::getKeyList($key, $max_count, $beta); @@ -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 { 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 { 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 { // Get the current keys associated with a list. If the cache diff --git a/src/Core/Controller.php b/src/Core/Controller.php index c73ee8d98c..d2e96d19e5 100644 --- a/src/Core/Controller.php +++ b/src/Core/Controller.php @@ -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) { $controller = $event->getController(); @@ -69,6 +72,9 @@ class Controller extends AbstractController implements EventSubscriberInterface return $event; } + /** + * Symfony event when the controller result is not a Response object + */ public function onKernelView(ViewEvent $event) { $request = $event->getRequest(); @@ -99,6 +105,9 @@ class Controller extends AbstractController implements EventSubscriberInterface return $event; } + /** + * Symfony event when the controller throws an exception + */ public function onKernelException(ExceptionEvent $event) { $except = $event->getThrowable(); diff --git a/src/Core/DB/DB.php b/src/Core/DB/DB.php index 4a5558b9c4..08896dc31c 100644 --- a/src/Core/DB/DB.php +++ b/src/Core/DB/DB.php @@ -48,6 +48,9 @@ abstract class DB self::$em = $m; } + /** + * Perform a Doctrine Query Language query + */ public static function dql(string $query, array $params = []) { $q = new Query(self::$em); @@ -58,6 +61,11 @@ abstract class DB return $q->getResult(); } + /** + * Perform a native, parameterized, SQL query. $entities is a map + * from table aliases to class names. Replaces '{select}' in + * $query with the appropriate select list + */ public static function sql(string $query, array $entities, array $params = []) { $rsm = new ResultSetMappingBuilder(self::$em); @@ -69,15 +77,23 @@ abstract class DB foreach ($params as $k => $v) { $q->setParameter($k, $v); } - // dump($q); - // die(); 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', - '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) { $expressions = []; @@ -100,6 +116,13 @@ abstract class DB return $expressions; } + /** + * Query $table according to $criteria. If $criteria's keys are + * one of self::$find_by_ops (and, or, etc), build a subexpression + * with that operator and recurse. Examples of $criteria are + * `['and' => ['lt' => ['foo' => 4], 'gte' => ['bar' => 2]]]` or + * `['in' => ['foo', 'bar']]` + */ public static function findBy(string $table, array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array { $criteria = array_change_key_case($criteria); @@ -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) { $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) { - foreach (['find', 'getReference', 'getPartialReference', 'getRepository'] as $m) { - // TODO Plugins - $pref = '\App\Entity\\'; - if ($name == $m && Formatting::startsWith($name, $pref) === false) { - $args[0] = $pref . ucfirst(Formatting::snakeCaseToCamelCase($args[0])); - } - } - - if (isset($args[0]) && is_string($args[0])) { - $args[0] = preg_replace('/Gsactor/', 'GSActor', $args[0] ?? ''); + // TODO Plugins + // If the method is one of the following and the first argument doesn't look like a FQCN, add the prefix + $pref = '\App\Entity\\'; + if (in_array($name, ['find', 'getReference', 'getPartialReference', 'getRepository']) + && preg_match('/\\\\/', $args[0]) === 0 + && Formatting::startsWith($args[0], $pref) === false) { + $args[0] = $pref . ucfirst(Formatting::snakeCaseToCamelCase($args[0])); + $args[0] = preg_replace('/Gsactor/', 'GSActor', $args[0]); } return self::$em->{$name}(...$args); diff --git a/src/Core/Entity.php b/src/Core/Entity.php index 50e39621c1..295006bd72 100644 --- a/src/Core/Entity.php +++ b/src/Core/Entity.php @@ -25,8 +25,18 @@ use App\Core\DB\DB; use App\Util\Formatting; 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) { $class = get_called_class(); @@ -43,12 +53,21 @@ class Entity return $obj; } + /** + * Create a new instance, but check for duplicates + */ public static function createOrUpdate(array $args, array $find_by) { $table = Formatting::camelCaseToSnakeCase(get_called_class()); - return self::create($args, DB::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) { $class = '\\' . get_called_class(); diff --git a/src/Core/Form.php b/src/Core/Form.php index 750e46749b..f80650bcb9 100644 --- a/src/Core/Form.php +++ b/src/Core/Form.php @@ -46,6 +46,9 @@ abstract class Form self::$form_factory = $ff; } + /** + * Create a form with the given associative array $form as fields + */ public static function create(array $form, ?object $target = null, array $extra_data = [], @@ -79,11 +82,19 @@ abstract class Form return $fb->getForm(); } + /** + * Whether the given $field of $form has the `required` property + * set, defaults to true + */ public static function isRequired(array $form, string $field): bool { return $form[$field][2]['required'] ?? true; } + /** + * Handle the full life cycle of a form. Creates it with @see + * self::create and inserts the submitted values into the database + */ public static function handle(array $form_definition, Request $request, object $target, array $extra_args = [], ?callable $extra_step = null, array $create_args = []) { $form = self::create($form_definition, $target, ...$create_args); diff --git a/src/Core/I18n/I18n.php b/src/Core/I18n/I18n.php index f528c69f2f..59ce4096c0 100644 --- a/src/Core/I18n/I18n.php +++ b/src/Core/I18n/I18n.php @@ -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 { $res = ''; @@ -293,10 +302,12 @@ abstract class I18n * * @todo add parameters */ -function _m(): string +function _m(...$args): string { - $domain = I18n::_mdomain(debug_backtrace()[0]['file']); - $args = func_get_args(); + // Get the file where this function was called from, reducing the + // memory and performance inpact by not returning the arguments, + // and only 2 frames (this and previous) + $domain = I18n::_mdomain(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS)[0]['file'], 2); switch (count($args)) { case 1: // Empty parameters, simple message diff --git a/src/Core/I18n/TransExtractor.php b/src/Core/I18n/TransExtractor.php index 08e70c3e4f..726c1c6b5e 100644 --- a/src/Core/I18n/TransExtractor.php +++ b/src/Core/I18n/TransExtractor.php @@ -75,8 +75,10 @@ class TransExtractor extends AbstractFileExtractor implements ExtractorInterface ], ]; + // TODO probably shouldn't be done this way // {{{Code from PhpExtractor - + // See vendor/symfony/translation/Extractor/PhpExtractor.php + // const MESSAGE_TOKEN = 300; const METHOD_ARGUMENTS_TOKEN = 1000; const DOMAIN_TOKEN = 1001; @@ -143,6 +145,9 @@ class TransExtractor extends AbstractFileExtractor implements ExtractorInterface } } + /** + * {@inheritdoc} + */ private function skipMethodArgument(\Iterator $tokenIterator) { $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, string $domain, string $filename, ?int $line_no = null) { @@ -293,6 +301,11 @@ class TransExtractor extends AbstractFileExtractor implements ExtractorInterface $mc->setMetadata($message, $metadata, $domain); } + /** + * Calls `::_m_dynamic` from the class defined in $filename and + * stores the results in the catalogue. For cases when the + * translation can't be done in a static (non-PHP) file + */ private function storeDynamic(MessageCatalogue $mc, string $filename) { require_once $filename; diff --git a/src/Core/Module.php b/src/Core/Module.php index 962389b048..1d829b77a5 100644 --- a/src/Core/Module.php +++ b/src/Core/Module.php @@ -24,8 +24,16 @@ use App\Util\Common; use Symfony\Component\Form\Form; 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) { $class = get_called_class(); @@ -36,6 +44,10 @@ class Module 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) { if ('POST' === $request->getMethod() && $request->request->has($form_name)) { diff --git a/src/Core/ModuleManager.php b/src/Core/ModuleManager.php index 800b4d5cd8..581629cd63 100644 --- a/src/Core/ModuleManager.php +++ b/src/Core/ModuleManager.php @@ -60,6 +60,9 @@ class ModuleManager protected array $modules = []; protected array $events = []; + /** + * Add the $fqcn class from $path as a module + */ public function add(string $fqcn, string $path) { list($type, $module) = preg_split('/\\\\/', $fqcn, 0, PREG_SPLIT_NO_EMPTY); @@ -69,6 +72,9 @@ class ModuleManager $this->modules[$id] = $obj; } + /** + * Container-build-time step that preprocesses the registering of events + */ public function preRegisterEvents() { 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) { $module_paths = array_merge(glob(INSTALLDIR . '/components/*/*.php'), glob(INSTALLDIR . '/plugins/*/*.php')); @@ -113,6 +122,11 @@ class ModuleManager file_put_contents(CACHE_FILE, "scope); diff --git a/src/Kernel.php b/src/Kernel.php index f00b154036..f3eea69e57 100644 --- a/src/Kernel.php +++ b/src/Kernel.php @@ -75,6 +75,10 @@ class Kernel extends BaseKernel } } + /** + * Symfony framework function override responsible for registering + * bundles (similar to our modules) + */ public function registerBundles(): iterable { $contents = require $this->getProjectDir() . '/config/bundles.php'; @@ -90,6 +94,11 @@ class Kernel extends BaseKernel 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 { $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 { $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 { parent::build($container); diff --git a/src/Util/Bitmap.php b/src/Util/Bitmap.php index 5db951e70e..286e9b6a30 100644 --- a/src/Util/Bitmap.php +++ b/src/Util/Bitmap.php @@ -23,7 +23,12 @@ use App\Util\Exception\ServerException; 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; $class = get_called_class(); diff --git a/src/Util/Common.php b/src/Util/Common.php index 97e7f4c021..1cd7422ad2 100644 --- a/src/Util/Common.php +++ b/src/Util/Common.php @@ -89,6 +89,15 @@ abstract class Common 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 { if (($user = self::user()) == null) { @@ -114,32 +123,9 @@ abstract class Common } } - // function array_diff_recursive($arr1, $arr2) - // { - // $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; - // } - + /** + * A recursive `array_diff`, while PHP itself doesn't provide one + */ public function array_diff_recursive(array $array1, array $array2) { $difference = [];