[TOOLS] Fix (most) issues found by PHPStan

This commit is contained in:
Hugo Sales 2021-11-25 23:08:30 +00:00
parent 8fd02ef152
commit b1262919da
Signed by: someonewithpc
GPG Key ID: 7D0C7EAFC9D835A0
18 changed files with 221 additions and 134 deletions

View File

@ -1,6 +1,6 @@
<?php <?php
declare(strict_types=1); declare(strict_types = 1);
// {{{ License // {{{ License
@ -27,8 +27,11 @@ use App\Core\Controller;
use App\Core\DB\DB; use App\Core\DB\DB;
use App\Core\Event; use App\Core\Event;
use App\Core\Form; use App\Core\Form;
use function App\Core\I18n\_m;
use App\Core\Log;
use App\Core\Router\Router; use App\Core\Router\Router;
use App\Util\Common; use App\Util\Common;
use App\Util\Exception\ClientException;
use App\Util\Exception\InvalidFormException; use App\Util\Exception\InvalidFormException;
use App\Util\Exception\NoLoggedInUser; use App\Util\Exception\NoLoggedInUser;
use App\Util\Exception\NoSuchNoteException; use App\Util\Exception\NoSuchNoteException;
@ -36,24 +39,23 @@ use App\Util\Exception\RedirectException;
use Plugin\Favourite\Entity\Favourite as FavouriteEntity; use Plugin\Favourite\Entity\Favourite as FavouriteEntity;
use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use function App\Core\I18n\_m;
class Favourite extends Controller class Favourite extends Controller
{ {
/** /**
* @throws RedirectException
* @throws NoSuchNoteException
* @throws InvalidFormException
* @throws \App\Util\Exception\ServerException * @throws \App\Util\Exception\ServerException
* @throws InvalidFormException
* @throws NoLoggedInUser * @throws NoLoggedInUser
* @throws NoSuchNoteException
* @throws RedirectException
*/ */
public function favouriteAddNote(Request $request, int $id): bool|array public function favouriteAddNote(Request $request, int $id): bool|array
{ {
$user = Common::ensureLoggedIn(); $user = Common::ensureLoggedIn();
$opts = ['id' => $id]; $actor_id = $user->getId();
$opts = ['id' => $id];
$add_favourite_note = DB::find('note', $opts); $add_favourite_note = DB::find('note', $opts);
if (is_null($add_favourite_note)) { if (\is_null($add_favourite_note)) {
throw new NoSuchNoteException(); throw new NoSuchNoteException();
} }
@ -62,7 +64,7 @@ class Favourite extends Controller
[ [
'label' => _m('Favourite note!'), 'label' => _m('Favourite note!'),
'attr' => [ 'attr' => [
'title' => _m('Favourite this note!') 'title' => _m('Favourite this note!'),
], ],
], ],
], ],
@ -71,41 +73,52 @@ class Favourite extends Controller
$form_add_to_favourite->handleRequest($request); $form_add_to_favourite->handleRequest($request);
if ($form_add_to_favourite->isSubmitted()) { if ($form_add_to_favourite->isSubmitted()) {
$opts = ['note_id' => $id, 'actor_id' => $user->getId()]; $opts = ['note_id' => $id, 'actor_id' => $user->getId()];
$note_already_favourited = DB::find('favourite', $opts); $note_already_favourited = DB::find('favourite', $opts);
if (is_null($note_already_favourited)) { if (\is_null($note_already_favourited)) {
$opts = ['note_id' => $id, 'actor_id' => $user->getId()]; $opts = ['note_id' => $id, 'actor_id' => $user->getId()];
DB::persist(FavouriteEntity::create($opts)); DB::persist(FavouriteEntity::create($opts));
DB::flush(); DB::flush();
} }
if (array_key_exists('from', $get_params = $this->params())) { // Redirect user to where they came from
# TODO anchor on element id // Prevent open redirect
throw new RedirectException($get_params['from']); if (!\is_null($from = $this->string('from'))) {
if (Router::isAbsolute($from)) {
Log::warning("Actor {$actor_id} attempted to reply to a note and then get redirected to another host, or the URL was invalid ({$from})");
throw new ClientException(_m('Can not redirect to outside the website from here'), 400); // 400 Bad request (deceptive)
} else {
// TODO anchor on element id
throw new RedirectException($from);
}
} else {
// If we don't have a URL to return to, go to the instance root
throw new RedirectException('root');
} }
} }
return [ return [
'_template' => 'favourite/add_to_favourites.html.twig', '_template' => 'favourite/add_to_favourites.html.twig',
'note' => $add_favourite_note, 'note' => $add_favourite_note,
'add_favourite' => $form_add_to_favourite->createView(), 'add_favourite' => $form_add_to_favourite->createView(),
]; ];
} }
/** /**
* @throws RedirectException
* @throws NoSuchNoteException
* @throws InvalidFormException
* @throws \App\Util\Exception\ServerException * @throws \App\Util\Exception\ServerException
* @throws InvalidFormException
* @throws NoLoggedInUser * @throws NoLoggedInUser
* @throws NoSuchNoteException
* @throws RedirectException
*/ */
public function favouriteRemoveNote(Request $request, int $id): array public function favouriteRemoveNote(Request $request, int $id): array
{ {
$user = Common::ensureLoggedIn(); $user = Common::ensureLoggedIn();
$opts = ['note_id' => $id, 'actor_id' => $user->getId()]; $actor_id = $user->getId();
$opts = ['note_id' => $id, 'actor_id' => $user->getId()];
$remove_favourite_note = DB::find('favourite', $opts); $remove_favourite_note = DB::find('favourite', $opts);
if (is_null($remove_favourite_note)) { if (\is_null($remove_favourite_note)) {
throw new NoSuchNoteException(); throw new NoSuchNoteException();
} }
@ -114,7 +127,7 @@ class Favourite extends Controller
[ [
'label' => _m('Remove favourite'), 'label' => _m('Remove favourite'),
'attr' => [ 'attr' => [
'title' => _m('Remove note from favourites.') 'title' => _m('Remove note from favourites.'),
], ],
], ],
], ],
@ -127,17 +140,27 @@ class Favourite extends Controller
DB::flush(); DB::flush();
} }
if (array_key_exists('from', $get_params = $this->params())) { // Redirect user to where they came from
# TODO anchor on element id // Prevent open redirect
throw new RedirectException($get_params['from']); if (!\is_null($from = $this->string('from'))) {
if (Router::isAbsolute($from)) {
Log::warning("Actor {$actor_id} attempted to reply to a note and then get redirected to another host, or the URL was invalid ({$from})");
throw new ClientException(_m('Can not redirect to outside the website from here'), 400); // 400 Bad request (deceptive)
} else {
// TODO anchor on element id
throw new RedirectException($from);
}
} else {
// If we don't have a URL to return to, go to the instance root
throw new RedirectException('root');
} }
} }
$note = DB::find('note', ['id' => $id]); $note = DB::find('note', ['id' => $id]);
return [ return [
'_template' => 'favourite/remove_from_favourites.html.twig', '_template' => 'favourite/remove_from_favourites.html.twig',
'note' => $note, 'note' => $note,
'remove_favourite' => $form_remove_favourite->createView(), 'remove_favourite' => $form_remove_favourite->createView(),
]; ];
} }
@ -155,8 +178,8 @@ class Favourite extends Controller
Event::handle('FormatNoteList', [$notes, &$notes_out]); Event::handle('FormatNoteList', [$notes, &$notes_out]);
return [ return [
'_template' => 'network/feed.html.twig', '_template' => 'network/feed.html.twig',
'notes' => $notes_out, 'notes' => $notes_out,
'page_title' => 'Favourites timeline.', 'page_title' => 'Favourites timeline.',
]; ];
} }
@ -170,9 +193,9 @@ class Favourite extends Controller
/** /**
* Reverse favourites stream * Reverse favourites stream
* *
* @return array template
* @throws NoLoggedInUser user not logged in * @throws NoLoggedInUser user not logged in
* *
* @return array template
*/ */
public function reverseFavouritesByActorId(Request $request, int $id): array public function reverseFavouritesByActorId(Request $request, int $id): array
{ {
@ -189,8 +212,8 @@ class Favourite extends Controller
Event::handle('FormatNoteList', [$notes, &$notes_out]); Event::handle('FormatNoteList', [$notes, &$notes_out]);
return [ return [
'_template' => 'network/feed.html.twig', '_template' => 'network/feed.html.twig',
'notes' => $notes, 'notes' => $notes,
'page_title' => 'Reverse favourites timeline.', 'page_title' => 'Reverse favourites timeline.',
]; ];
} }

View File

@ -26,17 +26,18 @@ namespace Plugin\Repeat\Controller;
use App\Core\Controller; use App\Core\Controller;
use App\Core\DB\DB; use App\Core\DB\DB;
use App\Core\Form; use App\Core\Form;
use App\Entity\Actor;
use Component\Posting\Posting;
use function App\Core\I18n\_m; use function App\Core\I18n\_m;
use App\Core\Log; use App\Core\Log;
use App\Core\Router\Router; use App\Core\Router\Router;
use App\Entity\Actor;
use App\Entity\Language;
use App\Entity\Note; use App\Entity\Note;
use App\Util\Common; use App\Util\Common;
use App\Util\Exception\ClientException; use App\Util\Exception\ClientException;
use App\Util\Exception\NoLoggedInUser; use App\Util\Exception\NoLoggedInUser;
use App\Util\Exception\NoSuchNoteException; use App\Util\Exception\NoSuchNoteException;
use App\Util\Exception\RedirectException; use App\Util\Exception\RedirectException;
use Component\Posting\Posting;
use Plugin\Repeat\Entity\NoteRepeat; use Plugin\Repeat\Entity\NoteRepeat;
use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
@ -54,9 +55,9 @@ class Repeat extends Controller
*/ */
public function repeatAddNote(Request $request, int $id): bool|array public function repeatAddNote(Request $request, int $id): bool|array
{ {
$user = Common::ensureLoggedIn(); $user = Common::ensureLoggedIn();
$actor_id = $user->getId(); $actor_id = $user->getId();
$opts = ['actor_id' => $actor_id, 'repeat_of' => $id]; $opts = ['actor_id' => $actor_id, 'repeat_of' => $id];
$note_already_repeated = DB::count('note_repeat', $opts) >= 1; $note_already_repeated = DB::count('note_repeat', $opts) >= 1;
@ -88,9 +89,10 @@ class Repeat extends Controller
// Create a new note with the same content as the original // Create a new note with the same content as the original
$repeat = Posting::storeLocalNote( $repeat = Posting::storeLocalNote(
actor: Actor::getById($actor_id), actor: Actor::getById($actor_id),
content:$note->getContent(), content: $note->getContent(),
content_type: $note->getContentType(), content_type: $note->getContentType(),
processed_attachments: $note->getAttachmentsWithTitle() language: Language::getFromId($note->getLanguageId())->getLocale(),
processed_attachments: $note->getAttachmentsWithTitle(),
); );
// Find the id of the note we just created // Find the id of the note we just created
@ -112,13 +114,13 @@ class Repeat extends Controller
// Redirect user to where they came from // Redirect user to where they came from
// Prevent open redirect // Prevent open redirect
if (\array_key_exists('from', (array) $get_params = $this->params())) { if (!\is_null($from = $this->string('from'))) {
if (Router::isAbsolute($get_params['from'])) { if (Router::isAbsolute($from)) {
Log::warning("Actor {$actor_id} attempted to reply to a note and then get redirected to another host, or the URL was invalid ({$get_params['from']})"); Log::warning("Actor {$actor_id} attempted to reply to a note and then get redirected to another host, or the URL was invalid ({$from})");
throw new ClientException(_m('Can not redirect to outside the website from here'), 400); // 400 Bad request (deceptive) throw new ClientException(_m('Can not redirect to outside the website from here'), 400); // 400 Bad request (deceptive)
} else { } else {
// TODO anchor on element id // TODO anchor on element id
throw new RedirectException($get_params['from']); throw new RedirectException($from);
} }
} else { } else {
// If we don't have a URL to return to, go to the instance root // If we don't have a URL to return to, go to the instance root
@ -143,6 +145,7 @@ class Repeat extends Controller
public function repeatRemoveNote(Request $request, int $id): array public function repeatRemoveNote(Request $request, int $id): array
{ {
$user = Common::ensureLoggedIn(); $user = Common::ensureLoggedIn();
$actor_id = $user->getId();
$opts = ['id' => $id]; $opts = ['id' => $id];
$remove_repeat_note = DB::find('note', $opts); $remove_repeat_note = DB::find('note', $opts);
if (\is_null($remove_repeat_note)) { if (\is_null($remove_repeat_note)) {
@ -176,16 +179,17 @@ class Repeat extends Controller
// Redirect user to where they came from // Redirect user to where they came from
// Prevent open redirect // Prevent open redirect
if (\array_key_exists('from', (array) $get_params = $this->params())) { if (!\is_null($from = $this->string('from'))) {
if (Router::isAbsolute($get_params['from'])) { if (Router::isAbsolute($from)) {
Log::warning("Actor {$actor_id} attempted to reply to a note and then get redirected to another host, or the URL was invalid ({$get_params['from']})"); Log::warning("Actor {$actor_id} attempted to reply to a note and then get redirected to another host, or the URL was invalid ({$from})");
throw new ClientException(_m('Can not redirect to outside the website from here'), 400); // 400 Bad request (deceptive) throw new ClientException(_m('Can not redirect to outside the website from here'), 400); // 400 Bad request (deceptive)
} else { } else {
// TODO anchor on element id // TODO anchor on element id
throw new RedirectException($get_params['from']); throw new RedirectException($from);
} }
} else { } else {
throw new RedirectException('root'); // If we don't have a URL to return to, go to the instance root // If we don't have a URL to return to, go to the instance root
throw new RedirectException('root');
} }
} }

View File

@ -39,6 +39,7 @@ use App\Util\Exception\ClientException;
use App\Util\Exception\InvalidFormException; use App\Util\Exception\InvalidFormException;
use App\Util\Exception\NoSuchNoteException; use App\Util\Exception\NoSuchNoteException;
use App\Util\Exception\RedirectException; use App\Util\Exception\RedirectException;
use App\Util\Form\FormFields;
use Component\Posting\Posting; use Component\Posting\Posting;
use Plugin\Reply\Entity\NoteReply; use Plugin\Reply\Entity\NoteReply;
use Symfony\Component\Form\Extension\Core\Type\FileType; use Symfony\Component\Form\Extension\Core\Type\FileType;
@ -70,13 +71,10 @@ class Reply extends Controller
throw new NoSuchNoteException(); throw new NoSuchNoteException();
} }
// TODO shouldn't this be the posting form?
$form = Form::create([ $form = Form::create([
['content', TextareaType::class, [ ['content', TextareaType::class, ['label' => _m('Reply'), 'label_attr' => ['class' => 'section-form-label'], 'help' => _m('Please input your reply.')]],
'label' => _m('Reply'), FormFields::language($user->getActor(), context_actor: $note->getActor(), label: 'Note language', help: null),
'label_attr' => ['class' => 'section-form-label'],
'help' => _m('Please input your reply.'),
],
],
['attachments', FileType::class, ['label' => ' ', 'multiple' => true, 'required' => false]], ['attachments', FileType::class, ['label' => ' ', 'multiple' => true, 'required' => false]],
['replyform', SubmitType::class, ['label' => _m('Submit')]], ['replyform', SubmitType::class, ['label' => _m('Submit')]],
]); ]);
@ -91,6 +89,7 @@ class Reply extends Controller
actor: Actor::getWithPK($actor_id), actor: Actor::getWithPK($actor_id),
content: $data['content'], content: $data['content'],
content_type: 'text/plain', // TODO content_type: 'text/plain', // TODO
language: $data['language'],
attachments: $data['attachments'], attachments: $data['attachments'],
); );
@ -116,13 +115,13 @@ class Reply extends Controller
// Redirect user to where they came from // Redirect user to where they came from
// Prevent open redirect // Prevent open redirect
if (\array_key_exists('from', (array) $get_params = $this->params())) { if (!\is_null($from = $this->string('from'))) {
if (Router::isAbsolute($get_params['from'])) { if (Router::isAbsolute($from)) {
Log::warning("Actor {$actor_id} attempted to reply to a note and then get redirected to another host, or the URL was invalid ({$get_params['from']})"); Log::warning("Actor {$actor_id} attempted to reply to a note and then get redirected to another host, or the URL was invalid ({$from})");
throw new ClientException(_m('Can not redirect to outside the website from here'), 400); // 400 Bad request (deceptive) throw new ClientException(_m('Can not redirect to outside the website from here'), 400); // 400 Bad request (deceptive)
} else { } else {
// TODO anchor on element id // TODO anchor on element id
throw new RedirectException($get_params['from']); throw new RedirectException($from);
} }
} else { } else {
// If we don't have a URL to return to, go to the instance root // If we don't have a URL to return to, go to the instance root

View File

@ -1,6 +1,6 @@
<?php <?php
declare(strict_types=1); declare(strict_types = 1);
// {{{ License // {{{ License
@ -26,7 +26,6 @@ namespace Plugin\Reply\Entity;
use App\Core\DB\DB; use App\Core\DB\DB;
use App\Core\Entity; use App\Core\Entity;
use App\Entity\Note; use App\Entity\Note;
use function PHPUnit\Framework\isEmpty;
/** /**
* Entity for notices * Entity for notices
@ -87,16 +86,16 @@ class NoteReply extends Entity
where reply_to = :note_id where reply_to = :note_id
order by n.created DESC order by n.created DESC
EOF, EOF,
['note_id' => $note->getId()] ['note_id' => $note->getId()],
); );
} }
public static function getReplyToNote(Note $note): ?int public static function getReplyToNote(Note $note): ?int
{ {
$result = DB::dql('select nr.reply_to from note_reply nr ' $result = DB::dql('select nr.reply_to from note_reply nr '
. 'where nr.note_id = :note_id', ['note_id' => $note->getId()]); . 'where nr.note_id = :note_id', ['note_id' => $note->getId()], );
if (!isEmpty($result)) { if (!empty($result)) {
return $result['reply_to']; return $result['reply_to'];
} }
@ -106,19 +105,19 @@ class NoteReply extends Entity
public static function schemaDef(): array public static function schemaDef(): array
{ {
return [ return [
'name' => 'note_reply', 'name' => 'note_reply',
'fields' => [ 'fields' => [
'note_id' => ['type' => 'int', 'not null' => true, 'foreign key' => true, 'target' => 'Note.id', 'multiplicity' => 'one to one', 'description' => 'The id of the reply itself'], 'note_id' => ['type' => 'int', 'not null' => true, 'foreign key' => true, 'target' => 'Note.id', 'multiplicity' => 'one to one', 'description' => 'The id of the reply itself'],
'actor_id' => ['type' => 'int', 'not null' => true, 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'one to one', 'description' => 'Who made this reply'], 'actor_id' => ['type' => 'int', 'not null' => true, 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'one to one', 'description' => 'Who made this reply'],
'reply_to' => ['type' => 'int', 'not null' => true, 'foreign key' => true, 'target' => 'Note.id', 'multiplicity' => 'one to one', 'description' => 'Note this is a reply of'], 'reply_to' => ['type' => 'int', 'not null' => true, 'foreign key' => true, 'target' => 'Note.id', 'multiplicity' => 'one to one', 'description' => 'Note this is a reply of'],
], ],
'primary key' => ['note_id'], 'primary key' => ['note_id'],
'foreign keys' => [ 'foreign keys' => [
'note_id_to_id_fkey' => ['note', ['note_id' => 'id']], 'note_id_to_id_fkey' => ['note', ['note_id' => 'id']],
'note_reply_to_id_fkey' => ['note', ['reply_to' => 'id']], 'note_reply_to_id_fkey' => ['note', ['reply_to' => 'id']],
'actor_reply_to_id_fkey' => ['actor', ['actor_id' => 'id']], 'actor_reply_to_id_fkey' => ['actor', ['actor_id' => 'id']],
], ],
'indexes' => [ 'indexes' => [
'note_reply_to_idx' => ['reply_to'], 'note_reply_to_idx' => ['reply_to'],
], ],
]; ];

View File

@ -44,7 +44,7 @@ use App\Entity\Note;
use App\Util\Common; use App\Util\Common;
use App\Util\Exception\ClientException; use App\Util\Exception\ClientException;
use App\Util\Exception\NotFoundException; use App\Util\Exception\NotFoundException;
use NotImplementedException; use App\Util\Exception\NotImplementedException;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
class Network extends Controller class Network extends Controller

View File

@ -45,6 +45,7 @@ use App\Core\Form;
use function App\Core\I18n\_m; use function App\Core\I18n\_m;
use App\Core\Log; use App\Core\Log;
use App\Entity\ActorLanguage; use App\Entity\ActorLanguage;
use App\Entity\Language;
use App\Entity\UserNotificationPrefs; use App\Entity\UserNotificationPrefs;
use App\Util\Common; use App\Util\Common;
use App\Util\Exception\AuthenticationException; use App\Util\Exception\AuthenticationException;
@ -63,6 +64,7 @@ use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType; use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\SubmitButton;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
// }}} Imports // }}} Imports
@ -202,12 +204,14 @@ class UserPanel extends Controller
$form->handleRequest($request); $form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) { if ($form->isSubmitted() && $form->isValid()) {
$go_back = $form->get('go_back')->isClicked(); /** @var SubmitButton $button */
$button = $form->get('go_back');
$go_back = $button->isClicked();
$data = $form->getData(); $data = $form->getData();
asort($data); // Sort by the order value asort($data); // Sort by the order value
$data = array_keys($data); // This keeps the order and gives us a unique number for each $data = array_keys($data); // This keeps the order and gives us a unique number for each
foreach ($data as $order => $locale) { foreach ($data as $order => $locale) {
$lang = Cache::getHashMapKey('languages', $locale); $lang = Language::getFromLocale($locale);
$actor_lang = DB::getReference('actor_language', ['actor_id' => $user->getId(), 'language_id' => $lang->getId()]); $actor_lang = DB::getReference('actor_language', ['actor_id' => $user->getId(), 'language_id' => $lang->getId()]);
$actor_lang->setOrdering($order + 1); $actor_lang->setOrdering($order + 1);
} }

View File

@ -29,13 +29,14 @@ use App\Entity\LocalUser;
use App\Entity\Note; use App\Entity\Note;
use App\Util\Common; use App\Util\Common;
use App\Util\Exception\ConfigurationException; use App\Util\Exception\ConfigurationException;
use App\Util\Exception\NotImplementedException;
use Functional as F; use Functional as F;
use InvalidArgumentException; use InvalidArgumentException;
use NotImplementedException;
use Redis; use Redis;
use RedisCluster; use RedisCluster;
use Symfony\Component\Cache\Adapter; use Symfony\Component\Cache\Adapter;
use Symfony\Component\Cache\Adapter\ChainAdapter; use Symfony\Component\Cache\Adapter\ChainAdapter;
use Symfony\Component\Cache\CacheItem;
abstract class Cache abstract class Cache
{ {
@ -154,7 +155,7 @@ abstract class Cache
* Retrieve a list from the cache, with a different implementation * Retrieve a list from the cache, with a different implementation
* for redis and others, trimming to $max_count if given * for redis and others, trimming to $max_count if given
* *
* @param callable(?Item $item, bool &$save): string|object|array<int,mixed> $calculate * @param callable(?CacheItem $item, bool &$save): (string|object|array<int,mixed>) $calculate
*/ */
public static function getList(string $key, callable $calculate, string $pool = 'default', ?int $max_count = null, ?int $left = null, ?int $right = null, float $beta = 1.0): array public static function getList(string $key, callable $calculate, string $pool = 'default', ?int $max_count = null, ?int $left = null, ?int $right = null, float $beta = 1.0): array
{ {
@ -191,7 +192,8 @@ abstract class Cache
return self::$redis[$pool]->lRange($key, $left ?? 0, ($right ?? $max_count ?? 0) - 1); return self::$redis[$pool]->lRange($key, $left ?? 0, ($right ?? $max_count ?? 0) - 1);
} else { } else {
return self::get($key, function () use ($calculate, $max_count) { return self::get($key, function () use ($calculate, $max_count) {
$res = $calculate(null); $save = true;
$res = $calculate(null, $save);
if ($max_count != -1) { if ($max_count != -1) {
$res = \array_slice($res, 0, $max_count); $res = \array_slice($res, 0, $max_count);
} }
@ -261,7 +263,7 @@ abstract class Cache
* Retrieve a hashmap from the cache, with a different implementation * Retrieve a hashmap from the cache, with a different implementation
* for redis and others. Different from lists, works with string map_keys * for redis and others. Different from lists, works with string map_keys
* *
* @param callable(?Item $item, bool &$save): string|object|array<string,mixed> $calculate * @param callable(?CacheItem $item, bool &$save): (string|object|array<string,mixed>) $calculate
* @TODO cleanup * @TODO cleanup
*/ */
public static function getHashMap(string $map_key, callable $calculate, string $pool = 'default', float $beta = 1.0): array public static function getHashMap(string $map_key, callable $calculate, string $pool = 'default', float $beta = 1.0): array
@ -286,7 +288,7 @@ abstract class Cache
$save = true; // Pass by reference $save = true; // Pass by reference
$res = $calculate(null, $save); $res = $calculate(null, $save);
if ($save) { if ($save) {
self::setHashMap($map_key, $res, $pool, $beta); self::setHashMap($map_key, $res, $pool);
return $res; return $res;
} }
} }
@ -314,7 +316,7 @@ abstract class Cache
self::$redis[$pool]->exec(); self::$redis[$pool]->exec();
} }
} else { } else {
self::set($map_key, \array_slice($value, 0, $max_count), $pool); self::set($map_key, $value, $pool);
} }
} }

View File

@ -49,6 +49,7 @@ use App\Core\I18n\I18n;
use App\Core\Queue\Queue; use App\Core\Queue\Queue;
use App\Core\Router\Router; use App\Core\Router\Router;
use App\Kernel; use App\Kernel;
use App\Security\EmailVerifier;
use App\Util\Common; use App\Util\Common;
use App\Util\Exception\ConfigurationException; use App\Util\Exception\ConfigurationException;
use App\Util\Formatting; use App\Util\Formatting;
@ -172,6 +173,7 @@ class GNUsocial implements EventSubscriberInterface
Router::setRouter($this->router); Router::setRouter($this->router);
HTTPClient::setClient($this->client); HTTPClient::setClient($this->client);
Formatting::setTwig($this->twig); Formatting::setTwig($this->twig);
EmailVerifier::setHelpers($this->email_verify_helper, $this->mailer_helper);
Cache::setupCache(); Cache::setupCache();
DB::initTableMap(); DB::initTableMap();

View File

@ -165,7 +165,7 @@ class AttachmentThumbnail extends Entity
public static function sizeStrToInt(string $size) public static function sizeStrToInt(string $size)
{ {
return self::SIZE_MAP[$size] ?? self::SIZE_MAP[self::SIZE_SMALL]; return self::SIZE_MAP[$size] ?? self::SIZE_SMALL;
} }
private ?Attachment $attachment = null; private ?Attachment $attachment = null;

View File

@ -1,6 +1,6 @@
<?php <?php
declare(strict_types=1); declare(strict_types = 1);
// {{{ 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
@ -21,6 +21,7 @@ declare(strict_types=1);
namespace App\Security; namespace App\Security;
use function App\Core\I18n\_m;
use App\Core\Router\Router; use App\Core\Router\Router;
use App\Entity\LocalUser; use App\Entity\LocalUser;
use App\Util\Common; use App\Util\Common;
@ -41,8 +42,12 @@ use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator; use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
use Symfony\Component\Security\Guard\AuthenticatorInterface; use Symfony\Component\Security\Guard\AuthenticatorInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
use Symfony\Component\Security\Http\Util\TargetPathTrait; use Symfony\Component\Security\Http\Util\TargetPathTrait;
use function App\Core\I18n\_m;
/** /**
* User authenticator * User authenticator
@ -67,10 +72,6 @@ class Authenticator extends AbstractFormLoginAuthenticator implements Authentica
$this->csrfTokenManager = $csrfTokenManager; $this->csrfTokenManager = $csrfTokenManager;
} }
/**
* @param Request $request
* @return bool
*/
public function supports(Request $request): bool public function supports(Request $request): bool
{ {
return self::LOGIN_ROUTE === $request->attributes->get('_route') && $request->isMethod('POST'); return self::LOGIN_ROUTE === $request->attributes->get('_route') && $request->isMethod('POST');
@ -92,10 +93,11 @@ class Authenticator extends AbstractFormLoginAuthenticator implements Authentica
* Get a user given credentials and a CSRF token * Get a user given credentials and a CSRF token
* *
* @param array<string, string> $credentials result of self::getCredentials * @param array<string, string> $credentials result of self::getCredentials
* @param UserProviderInterface $userProvider *
* @return ?LocalUser
* @throws NoSuchActorException * @throws NoSuchActorException
* @throws ServerException * @throws ServerException
*
* @return ?LocalUser
*/ */
public function getUser($credentials, UserProviderInterface $userProvider): ?LocalUser public function getUser($credentials, UserProviderInterface $userProvider): ?LocalUser
{ {
@ -110,7 +112,7 @@ class Authenticator extends AbstractFormLoginAuthenticator implements Authentica
} elseif (Nickname::isValid($credentials['nickname_or_email'])) { } elseif (Nickname::isValid($credentials['nickname_or_email'])) {
$user = LocalUser::getByNickname($credentials['nickname_or_email']); $user = LocalUser::getByNickname($credentials['nickname_or_email']);
} }
if (is_null($user)) { if (\is_null($user)) {
throw new NoSuchActorException('No such local user.'); throw new NoSuchActorException('No such local user.');
} }
$credentials['nickname'] = $user->getNickname(); $credentials['nickname'] = $user->getNickname();
@ -124,8 +126,8 @@ class Authenticator extends AbstractFormLoginAuthenticator implements Authentica
/** /**
* @param array<string, string> $credentials result of self::getCredentials * @param array<string, string> $credentials result of self::getCredentials
* @param LocalUser $user * @param LocalUser $user
* @return bool *
* @throws ServerException * @throws ServerException
*/ */
public function checkCredentials($credentials, $user): bool public function checkCredentials($credentials, $user): bool
@ -144,7 +146,7 @@ class Authenticator extends AbstractFormLoginAuthenticator implements Authentica
{ {
$nickname = $token->getUser(); $nickname = $token->getUser();
if ($nickname instanceof Stringable) { if ($nickname instanceof Stringable) {
$nickname = (string)$nickname; $nickname = (string) $nickname;
} elseif ($nickname instanceof UserInterface) { } elseif ($nickname instanceof UserInterface) {
$nickname = $nickname->getUserIdentifier(); $nickname = $nickname->getUserIdentifier();
} }

View File

@ -4,7 +4,8 @@ declare(strict_types = 1);
namespace App\Security; namespace App\Security;
use Doctrine\ORM\EntityManagerInterface; use App\Core\DB\DB;
use App\Entity\LocalUser;
use Symfony\Bridge\Twig\Mime\TemplatedEmail; use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Mailer\MailerInterface; use Symfony\Component\Mailer\MailerInterface;
@ -12,22 +13,23 @@ use Symfony\Component\Security\Core\User\UserInterface;
use SymfonyCasts\Bundle\VerifyEmail\Exception\VerifyEmailExceptionInterface; use SymfonyCasts\Bundle\VerifyEmail\Exception\VerifyEmailExceptionInterface;
use SymfonyCasts\Bundle\VerifyEmail\VerifyEmailHelperInterface; use SymfonyCasts\Bundle\VerifyEmail\VerifyEmailHelperInterface;
class EmailVerifier abstract class EmailVerifier
{ {
private $verifyEmailHelper; private static $verifyEmailHelper;
private $mailer; private static $mailer;
private $entityManager;
public function __construct(VerifyEmailHelperInterface $helper, MailerInterface $mailer, EntityManagerInterface $manager) public static function setHelpers(VerifyEmailHelperInterface $helper, MailerInterface $mailer)
{ {
$this->verifyEmailHelper = $helper; self::$verifyEmailHelper = $helper;
$this->mailer = $mailer; self::$mailer = $mailer;
$this->entityManager = $manager;
} }
public function sendEmailConfirmation(string $verifyEmailRouteName, UserInterface $user, TemplatedEmail $email): void /**
* @param LocalUser $user
*/
public static function sendEmailConfirmation(string $verifyEmailRouteName, UserInterface $user, TemplatedEmail $email): void
{ {
$signatureComponents = $this->verifyEmailHelper->generateSignature( $signatureComponents = self::$verifyEmailHelper->generateSignature(
$verifyEmailRouteName, $verifyEmailRouteName,
$user->getId(), $user->getId(),
$user->getOutgoingEmail(), $user->getOutgoingEmail(),
@ -41,19 +43,19 @@ class EmailVerifier
$email->context($context); $email->context($context);
$this->mailer->send($email); self::$mailer->send($email);
} }
/** /**
* @param LocalUser $user
*
* @throws VerifyEmailExceptionInterface * @throws VerifyEmailExceptionInterface
*/ */
public function handleEmailConfirmation(Request $request, UserInterface $user): void public static function handleEmailConfirmation(Request $request, UserInterface $user): void
{ {
$this->verifyEmailHelper->validateEmailConfirmation($request->getUri(), $user->getId(), $user->getOutgoingEmail()); self::$verifyEmailHelper->validateEmailConfirmation($request->getUri(), $user->getId(), $user->getOutgoingEmail());
$user->setIsEmailVerified(true);
$user->setIsVerified(true); DB::persist($user);
DB::flush();
$this->entityManager->persist($user);
$this->entityManager->flush();
} }
} }

View File

@ -99,7 +99,7 @@ abstract class Common
/** /**
* Set sysadmin's configuration preferences for GNU social * Set sysadmin's configuration preferences for GNU social
* *
* @param $transient keep this setting in memory only * @param bool $transient keep this setting in memory only
*/ */
public static function setConfig(string $section, string $setting, $value, bool $transient = false): void public static function setConfig(string $section, string $setting, $value, bool $transient = false): void
{ {

View File

@ -0,0 +1,49 @@
<?php
declare(strict_types = 1);
// {{{ 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/>.
// }}}
/**
* Class for 'assertion' exceptions. Logs when an unexpected state is found and is treated as a ServerException downstream
* HTTP code 500
*
* @category Exception
* @package GNUsocial
*
* @author Hugo Sales <hugo@hsal.es>
* @copyright 2021 Free Software Foundation, Inc http://www.fsf.org
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
*/
namespace App\Util\Exception;
use App\Core\Log;
use Throwable;
class BugFoundException extends ServerException
{
public function __construct(string $log_message, string $message = '', int $code = 500, ?Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
$frame = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, limit: 2)[1];
Log::critical("{$log_message} in {$frame['file']}:{$frame['line']}");
}
}

View File

@ -43,13 +43,13 @@ class EmailTakenException extends EmailException
public function __construct(?Actor $actor = null, ?string $msg = null, int $code = 400) public function __construct(?Actor $actor = null, ?string $msg = null, int $code = 400)
{ {
$this->profile = $actor; $this->actor = $actor;
parent::__construct($msg, $code); parent::__construct($msg, $code);
} }
protected function defaultMessage(): string protected function defaultMessage(): string
{ {
// TRANS: Validation error in form for registration, profile and group settings, etc. // TRANS: Validation error in form for registration, actor and group settings, etc.
return _m('Email is already in use on this server.'); return _m('Email is already in use on this server.');
} }
} }

View File

@ -52,13 +52,13 @@ class NicknameTakenException extends NicknameException
public function __construct(?Actor $actor = null, ?string $msg = null, int $code = 400) public function __construct(?Actor $actor = null, ?string $msg = null, int $code = 400)
{ {
$this->profile = $actor; $this->actor = $actor;
parent::__construct($msg, $code); parent::__construct($msg, $code);
} }
protected function defaultMessage(): string protected function defaultMessage(): string
{ {
// TRANS: Validation error in form for registration, profile and group settings, etc. // TRANS: Validation error in form for registration, actor and group settings, etc.
return _m('Nickname is already in use on this server.'); return _m('Nickname is already in use on this server.');
} }
} }

View File

@ -1,9 +1,10 @@
<?php <?php
declare(strict_types=1); declare(strict_types = 1);
namespace App\Util\Form; namespace App\Util\Form;
use function App\Core\I18n\_m;
use App\Entity\Actor; use App\Entity\Actor;
use App\Entity\Language; use App\Entity\Language;
use App\Util\Common; use App\Util\Common;
@ -12,7 +13,6 @@ use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\RepeatedType; use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
use Symfony\Component\Validator\Constraints\Length; use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Component\Validator\Constraints\NotBlank;
use function App\Core\I18n\_m;
abstract class FormFields abstract class FormFields
{ {
@ -29,13 +29,13 @@ abstract class FormFields
return [ return [
'password', RepeatedType::class, 'password', RepeatedType::class,
[ [
'type' => PasswordType::class, 'type' => PasswordType::class,
'first_options' => [ 'first_options' => [
'label' => _m('Password'), 'label' => _m('Password'),
'label_attr' => ['class' => 'section-form-label'], 'label_attr' => ['class' => 'section-form-label'],
'attr' => array_merge(['placeholder' => _m('********'), 'required' => $options['required'] ?? true], $options['attr'] ?? []), 'attr' => array_merge(['placeholder' => _m('********'), 'required' => $options['required'] ?? true], $options['attr'] ?? []),
'constraints' => $constraints, 'constraints' => $constraints,
'help' => _m('Write a password with at least {min_length} characters, and a maximum of {max_length}.', ['min_length' => Common::config('password', 'min_length'), 'max_length' => Common::config('password', 'max_length')]), 'help' => _m('Write a password with at least {min_length} characters, and a maximum of {max_length}.', ['min_length' => Common::config('password', 'min_length'), 'max_length' => Common::config('password', 'max_length')]),
], ],
'second_options' => [ 'second_options' => [
'label' => _m('Repeat Password'), 'label' => _m('Repeat Password'),
@ -45,8 +45,8 @@ abstract class FormFields
'required' => $options['required'] ?? true, 'required' => $options['required'] ?? true,
'constraints' => $constraints, 'constraints' => $constraints,
], ],
'mapped' => false, 'mapped' => false,
'required' => $options['required'] ?? true, 'required' => $options['required'] ?? true,
'invalid_message' => _m('The password fields must match'), 'invalid_message' => _m('The password fields must match'),
], ],
]; ];
@ -67,24 +67,24 @@ abstract class FormFields
'constraints' => [ 'constraints' => [
new NotBlank(['message' => _m('Please enter a password')]), new NotBlank(['message' => _m('Please enter a password')]),
new Length(['min' => Common::config('password', 'min_length'), 'minMessage' => _m(['Your password should be at least # characters'], ['count' => Common::config('password', 'min_length')]), new Length(['min' => Common::config('password', 'min_length'), 'minMessage' => _m(['Your password should be at least # characters'], ['count' => Common::config('password', 'min_length')]),
'max' => Common::config('password', 'max_length'), 'maxMessage' => _m(['Your password should be at most # characters'], ['count' => Common::config('password', 'max_length')]),]), 'max' => Common::config('password', 'max_length'), 'maxMessage' => _m(['Your password should be at most # characters'], ['count' => Common::config('password', 'max_length')]), ]),
],], ], ],
]; ];
} }
public static function language(Actor $actor, ?Actor $context_actor, string $label, string $help, bool $multiple = false, bool $required = true, ?bool $use_short_display = null): array public static function language(Actor $actor, ?Actor $context_actor, string $label, ?string $help = null, bool $multiple = false, bool $required = true, ?bool $use_short_display = null): array
{ {
[$language_choices, $preferred_language_choices] = Language::getSortedLanguageChoices($actor, $context_actor, use_short_display: $use_short_display); [$language_choices, $preferred_language_choices] = Language::getSortedLanguageChoices($actor, $context_actor, use_short_display: $use_short_display);
return [ return [
'language' . ($multiple ? 's' : ''), 'language' . ($multiple ? 's' : ''),
ChoiceType::class, ChoiceType::class,
[ [
'label' => _m($label), 'label' => _m($label),
'preferred_choices' => $preferred_language_choices, 'preferred_choices' => $preferred_language_choices,
'choices' => $language_choices, 'choices' => $language_choices,
'required' => $required, 'required' => $required,
'multiple' => $multiple, 'multiple' => $multiple,
'help' => _m($help), 'help' => _m($help),
], ],
]; ];
} }

View File

@ -24,7 +24,7 @@ declare(strict_types = 1);
namespace App\Util; namespace App\Util;
use App\Core\DB\DB; use App\Core\DB\DB;
use App\Core\Log; use App\Util\Exception\BugFoundException;
use App\Util\Exception\DuplicateFoundException; use App\Util\Exception\DuplicateFoundException;
use App\Util\Exception\NicknameEmptyException; use App\Util\Exception\NicknameEmptyException;
use App\Util\Exception\NicknameException; use App\Util\Exception\NicknameException;
@ -155,7 +155,7 @@ class Nickname
} catch (NotFoundException) { } catch (NotFoundException) {
// continue // continue
} catch (DuplicateFoundException) { } catch (DuplicateFoundException) {
Log::critial("Duplicate entry in `local_user` for nickname={$nickname}"); throw new BugFoundException("Duplicate entry in `local_user` for nickname={$nickname}");
} }
break; break;
// @codeCoverageIgnoreStart // @codeCoverageIgnoreStart

View File

@ -22,6 +22,7 @@ declare(strict_types = 1);
namespace App\Util; namespace App\Util;
use function App\Core\I18n\_m; use function App\Core\I18n\_m;
use App\Util\Exception\BugFoundException;
use App\Util\Exception\ServerException; use App\Util\Exception\ServerException;
use App\Util\Exception\TemporaryFileException; use App\Util\Exception\TemporaryFileException;
use LogicException; use LogicException;
@ -129,7 +130,7 @@ class TemporaryFile extends SplFileInfo
if (!\is_null($this->resource) && $this->resource !== false) { if (!\is_null($this->resource) && $this->resource !== false) {
$path = $this->getRealPath(); $path = $this->getRealPath();
if ($path === false) { if ($path === false) {
throw new BugFoundException(); throw new BugFoundException('Tried to cleanup a file but it\'s real path is false, while resource isn\'t');
} }
$this->close(); $this->close();
if (file_exists($path)) { if (file_exists($path)) {