[UTIL][HTML] HTML abstraction class was extended with a more specialised Heading class
This little abstraction layer made it a bit easier to add a different title to a Note or Actor Feed Collection template, from whichever controller that uses it. Please, bear in mind, that abstract templates such as those found in Components\Collection, may act in a very 'declarative' way upon using them. This makes it difficult to dynamically choose what type of header is used without undergoing a mining operation in the likes of a pyramid of doom. Hence, this _little_ change.
This commit is contained in:
parent
f66e178dfc
commit
e70acd5c3b
|
@ -4,7 +4,11 @@
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<section class="frame-section frame-section-padding">
|
<section class="frame-section frame-section-padding">
|
||||||
<h1 class="frame-section-title">{{ title }}</h1>
|
<header class="feed-header">
|
||||||
|
{% if actors_feed_title is defined %}
|
||||||
|
{{ actors_feed_title.getHtml() }}
|
||||||
|
{% endif %}
|
||||||
|
</header>
|
||||||
|
|
||||||
{% set prepend_actors_collection = handle_event('PrependActorsCollection', request) %}
|
{% set prepend_actors_collection = handle_event('PrependActorsCollection', request) %}
|
||||||
{% for widget in prepend_actors_collection %}
|
{% for widget in prepend_actors_collection %}
|
||||||
|
|
|
@ -14,18 +14,10 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{% if notes is defined %}
|
{% if notes is defined %}
|
||||||
<header class="feed-header" title="{{ 'Current page main header' | trans }}">
|
<header class="feed-header">
|
||||||
{% set current_path = app.request.get('_route') %}
|
{% set current_path = app.request.get('_route') %}
|
||||||
{% if page_title is defined %}
|
{% if notes_feed_title is defined %}
|
||||||
{% if current_path starts with 'feed_' or 'conversation' in current_path %}
|
{{ notes_feed_title.getHtml() }}
|
||||||
<h1 class="section-title" role="heading">{{ page_title | trans }}</h1>
|
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
|
||||||
{% if current_path starts with 'search' %}
|
|
||||||
<h3 class="heading-no-margin">{{ 'Notes found' | trans }}</h3>
|
|
||||||
{% else %}
|
|
||||||
<h1 class="section-title">{{ 'Notes' | trans }}</h1>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<nav class="feed-actions" title="{{ 'Actions that change how the feed behaves' | trans }}">
|
<nav class="feed-actions" title="{{ 'Actions that change how the feed behaves' | trans }}">
|
||||||
<details class="feed-actions-details" role="group">
|
<details class="feed-actions-details" role="group">
|
||||||
|
|
|
@ -40,6 +40,7 @@ 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 App\Util\Exception\ServerException;
|
use App\Util\Exception\ServerException;
|
||||||
|
use App\Util\HTML\Heading;
|
||||||
use Component\Collection\Util\Controller\FeedController;
|
use Component\Collection\Util\Controller\FeedController;
|
||||||
use Component\Conversation\Entity\ConversationMute;
|
use Component\Conversation\Entity\ConversationMute;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||||
|
@ -58,11 +59,14 @@ class Conversation extends FeedController
|
||||||
*/
|
*/
|
||||||
public function showConversation(Request $request, int $conversation_id): array
|
public function showConversation(Request $request, int $conversation_id): array
|
||||||
{
|
{
|
||||||
|
$page_title = _m('Conversation');
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'_template' => 'collection/notes.html.twig',
|
'_template' => 'collection/notes.html.twig',
|
||||||
'notes' => $this->query(query: "note-conversation:{$conversation_id}")['notes'] ?? [],
|
'notes' => $this->query(query: "note-conversation:{$conversation_id}")['notes'] ?? [],
|
||||||
'should_format' => false,
|
'should_format' => false,
|
||||||
'page_title' => _m('Conversation'),
|
'page_title' => $page_title,
|
||||||
|
'notes_feed_title' => (new Heading(1, [], $page_title)),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -37,6 +37,7 @@ namespace Component\Feed\Controller;
|
||||||
|
|
||||||
use function App\Core\I18n\_m;
|
use function App\Core\I18n\_m;
|
||||||
use App\Util\Common;
|
use App\Util\Common;
|
||||||
|
use App\Util\HTML\Heading;
|
||||||
use Component\Collection\Util\Controller\FeedController;
|
use Component\Collection\Util\Controller\FeedController;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|
||||||
|
@ -48,9 +49,11 @@ class Feeds extends FeedController
|
||||||
public function public(Request $request): array
|
public function public(Request $request): array
|
||||||
{
|
{
|
||||||
$data = $this->query('note-local:true');
|
$data = $this->query('note-local:true');
|
||||||
|
$page_title = _m(\is_null(Common::user()) ? 'Feed' : 'Planet');
|
||||||
return [
|
return [
|
||||||
'_template' => 'collection/notes.html.twig',
|
'_template' => 'collection/notes.html.twig',
|
||||||
'page_title' => _m(\is_null(Common::user()) ? 'Feed' : 'Planet'),
|
'page_title' => $page_title,
|
||||||
|
'notes_feed_title' => (new Heading(level: 1, classes: ['feed-title'], text: $page_title)),
|
||||||
'notes' => $data['notes'],
|
'notes' => $data['notes'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -65,6 +68,7 @@ class Feeds extends FeedController
|
||||||
return [
|
return [
|
||||||
'_template' => 'collection/notes.html.twig',
|
'_template' => 'collection/notes.html.twig',
|
||||||
'page_title' => _m('Home'),
|
'page_title' => _m('Home'),
|
||||||
|
'notes_feed_title' => (new Heading(level: 1, classes: ['feed-title'], text: 'Home')),
|
||||||
'notes' => $data['notes'],
|
'notes' => $data['notes'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,12 +22,11 @@ declare(strict_types = 1);
|
||||||
namespace Component\Group;
|
namespace Component\Group;
|
||||||
|
|
||||||
use App\Core\Event;
|
use App\Core\Event;
|
||||||
use App\Entity\Activity;
|
|
||||||
use Component\Notification\Notification;
|
|
||||||
use function App\Core\I18n\_m;
|
use function App\Core\I18n\_m;
|
||||||
use App\Core\Modules\Component;
|
use App\Core\Modules\Component;
|
||||||
use App\Core\Router\RouteLoader;
|
use App\Core\Router\RouteLoader;
|
||||||
use App\Core\Router\Router;
|
use App\Core\Router\Router;
|
||||||
|
use App\Entity\Activity;
|
||||||
use App\Entity\Actor;
|
use App\Entity\Actor;
|
||||||
use App\Util\Common;
|
use App\Util\Common;
|
||||||
use App\Util\HTML;
|
use App\Util\HTML;
|
||||||
|
@ -35,6 +34,7 @@ use App\Util\Nickname;
|
||||||
use Component\Circle\Controller\SelfTagsSettings;
|
use Component\Circle\Controller\SelfTagsSettings;
|
||||||
use Component\Group\Controller as C;
|
use Component\Group\Controller as C;
|
||||||
use Component\Group\Entity\LocalGroup;
|
use Component\Group\Entity\LocalGroup;
|
||||||
|
use Component\Notification\Notification;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|
||||||
class Group extends Component
|
class Group extends Component
|
||||||
|
@ -51,15 +51,10 @@ class Group extends Component
|
||||||
/**
|
/**
|
||||||
* Enqueues a notification for an Actor (such as person or group) which means
|
* Enqueues a notification for an Actor (such as person or group) which means
|
||||||
* it shows up in their home feed and such.
|
* it shows up in their home feed and such.
|
||||||
* @param Actor $sender
|
|
||||||
* @param Activity $activity
|
|
||||||
* @param array $targets
|
|
||||||
* @param string|null $reason
|
|
||||||
* @return bool
|
|
||||||
*/
|
*/
|
||||||
public function onNewNotificationWithTargets(Actor $sender, Activity $activity, array $targets = [], ?string $reason = null): bool
|
public function onNewNotificationWithTargets(Actor $sender, Activity $activity, array $targets = [], ?string $reason = null): bool
|
||||||
{
|
{
|
||||||
foreach($targets as $target) {
|
foreach ($targets as $target) {
|
||||||
if ($target->isGroup()) {
|
if ($target->isGroup()) {
|
||||||
// The Group announces to its subscribers
|
// The Group announces to its subscribers
|
||||||
Notification::notify($target, $activity, $target->getSubscribers(), $reason);
|
Notification::notify($target, $activity, $target->getSubscribers(), $reason);
|
||||||
|
@ -82,7 +77,6 @@ class Group extends Component
|
||||||
$res[] = HTML::html(['a' => ['attrs' => ['href' => $url, 'title' => _m('Edit group settings'), 'class' => 'profile-extra-actions'], _m('Group settings')]]);
|
$res[] = HTML::html(['a' => ['attrs' => ['href' => $url, 'title' => _m('Edit group settings'), 'class' => 'profile-extra-actions'], _m('Group settings')]]);
|
||||||
}
|
}
|
||||||
$res[] = HTML::html(['a' => ['attrs' => ['href' => Router::url('blog_post', ['in' => $group->getId()]), 'title' => _m('Make a new blog post'), 'class' => 'profile-extra-actions'], _m('Post in blog')]]);
|
$res[] = HTML::html(['a' => ['attrs' => ['href' => Router::url('blog_post', ['in' => $group->getId()]), 'title' => _m('Make a new blog post'), 'class' => 'profile-extra-actions'], _m('Post in blog')]]);
|
||||||
|
|
||||||
}
|
}
|
||||||
return Event::next;
|
return Event::next;
|
||||||
}
|
}
|
||||||
|
@ -90,7 +84,7 @@ class Group extends Component
|
||||||
public function onPopulateSettingsTabs(Request $request, string $section, array &$tabs): bool
|
public function onPopulateSettingsTabs(Request $request, string $section, array &$tabs): bool
|
||||||
{
|
{
|
||||||
if ($section === 'profile' && $request->get('_route') === 'group_settings') {
|
if ($section === 'profile' && $request->get('_route') === 'group_settings') {
|
||||||
$group_id = (int)$request->get('id');
|
$group_id = (int) $request->get('id');
|
||||||
$group = Actor::getById($group_id);
|
$group = Actor::getById($group_id);
|
||||||
$tabs[] = [
|
$tabs[] = [
|
||||||
'title' => 'Self tags',
|
'title' => 'Self tags',
|
||||||
|
|
|
@ -46,7 +46,7 @@ class Link extends Component
|
||||||
preg_match_all($this->getURLRegex(), $content, $matched_urls);
|
preg_match_all($this->getURLRegex(), $content, $matched_urls);
|
||||||
$matched_urls = array_unique($matched_urls[1]);
|
$matched_urls = array_unique($matched_urls[1]);
|
||||||
foreach ($matched_urls as $match) {
|
foreach ($matched_urls as $match) {
|
||||||
if (in_array($match, $ignore)) {
|
if (\in_array($match, $ignore)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -30,6 +30,7 @@ use App\Entity as E;
|
||||||
use App\Entity\LocalUser;
|
use App\Entity\LocalUser;
|
||||||
use App\Util\Exception\ClientException;
|
use App\Util\Exception\ClientException;
|
||||||
use App\Util\Exception\ServerException;
|
use App\Util\Exception\ServerException;
|
||||||
|
use App\Util\HTML\Heading;
|
||||||
use Component\Collection\Util\Controller\FeedController;
|
use Component\Collection\Util\Controller\FeedController;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|
||||||
|
@ -79,6 +80,8 @@ class PersonFeed extends FeedController
|
||||||
'actor' => $person,
|
'actor' => $person,
|
||||||
'nickname' => $person->getNickname(),
|
'nickname' => $person->getNickname(),
|
||||||
'notes' => E\Note::getAllNotesByActor($person),
|
'notes' => E\Note::getAllNotesByActor($person),
|
||||||
|
'page_title' => _m($person->getNickname() . '\'s profile'),
|
||||||
|
'notes_feed_title' => (new Heading(level: 2, classes: ['section-title'], text: 'Notes by ' . $person->getNickname())),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,7 @@ use App\Util\Exception\BugFoundException;
|
||||||
use App\Util\Exception\RedirectException;
|
use App\Util\Exception\RedirectException;
|
||||||
use App\Util\Form\FormFields;
|
use App\Util\Form\FormFields;
|
||||||
use App\Util\Formatting;
|
use App\Util\Formatting;
|
||||||
|
use App\Util\HTML\Heading;
|
||||||
use Component\Collection\Util\Controller\FeedController;
|
use Component\Collection\Util\Controller\FeedController;
|
||||||
use Component\Search as Comp;
|
use Component\Search as Comp;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||||
|
@ -135,6 +136,8 @@ class Search extends FeedController
|
||||||
'search_form' => Comp\Search::searchForm($request, query: $q, add_subscribe: !\is_null($actor)),
|
'search_form' => Comp\Search::searchForm($request, query: $q, add_subscribe: !\is_null($actor)),
|
||||||
'search_builder_form' => $search_builder_form->createView(),
|
'search_builder_form' => $search_builder_form->createView(),
|
||||||
'notes' => $notes ?? [],
|
'notes' => $notes ?? [],
|
||||||
|
'notes_feed_title' => (new Heading(level: 3, classes: ['section-title'], text: 'Notes found')),
|
||||||
|
'actors_feed_title' => (new Heading(level: 3, classes: ['section-title'], text: 'Actors found')),
|
||||||
'actors' => $actors ?? [],
|
'actors' => $actors ?? [],
|
||||||
'page' => 1, // TODO paginate
|
'page' => 1, // TODO paginate
|
||||||
];
|
];
|
||||||
|
|
|
@ -39,8 +39,6 @@ use App\Core\DB\DB;
|
||||||
use App\Core\Event;
|
use App\Core\Event;
|
||||||
use App\Core\GSFile;
|
use App\Core\GSFile;
|
||||||
use App\Core\HTTPClient;
|
use App\Core\HTTPClient;
|
||||||
use App\Util\HTML;
|
|
||||||
use Plugin\ActivityPub\Util\Explorer;
|
|
||||||
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;
|
||||||
|
@ -52,6 +50,7 @@ use App\Util\Exception\DuplicateFoundException;
|
||||||
use App\Util\Exception\NoSuchActorException;
|
use App\Util\Exception\NoSuchActorException;
|
||||||
use App\Util\Exception\ServerException;
|
use App\Util\Exception\ServerException;
|
||||||
use App\Util\Formatting;
|
use App\Util\Formatting;
|
||||||
|
use App\Util\HTML;
|
||||||
use App\Util\TemporaryFile;
|
use App\Util\TemporaryFile;
|
||||||
use Component\Attachment\Entity\ActorToAttachment;
|
use Component\Attachment\Entity\ActorToAttachment;
|
||||||
use Component\Attachment\Entity\AttachmentToNote;
|
use Component\Attachment\Entity\AttachmentToNote;
|
||||||
|
@ -66,6 +65,7 @@ use Exception;
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
use Plugin\ActivityPub\ActivityPub;
|
use Plugin\ActivityPub\ActivityPub;
|
||||||
use Plugin\ActivityPub\Entity\ActivitypubObject;
|
use Plugin\ActivityPub\Entity\ActivitypubObject;
|
||||||
|
use Plugin\ActivityPub\Util\Explorer;
|
||||||
use Plugin\ActivityPub\Util\Model;
|
use Plugin\ActivityPub\Util\Model;
|
||||||
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
|
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
|
||||||
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
|
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
|
||||||
|
@ -118,8 +118,8 @@ class Note extends Model
|
||||||
$type_note = \is_string($json) ? self::jsonToType($json) : $json;
|
$type_note = \is_string($json) ? self::jsonToType($json) : $json;
|
||||||
$actor_id = null;
|
$actor_id = null;
|
||||||
$actor = null;
|
$actor = null;
|
||||||
$to = $type_note->has('to') ? (is_string($type_note->get('to')) ? [$type_note->get('to')] : $type_note->get('to')) : [];
|
$to = $type_note->has('to') ? (\is_string($type_note->get('to')) ? [$type_note->get('to')] : $type_note->get('to')) : [];
|
||||||
$cc = $type_note->has('cc') ? (is_string($type_note->get('cc')) ? [$type_note->get('cc')] : $type_note->get('cc')) : [];
|
$cc = $type_note->has('cc') ? (\is_string($type_note->get('cc')) ? [$type_note->get('cc')] : $type_note->get('cc')) : [];
|
||||||
if ($json instanceof AbstractObject
|
if ($json instanceof AbstractObject
|
||||||
&& \array_key_exists('test_authority', $options)
|
&& \array_key_exists('test_authority', $options)
|
||||||
&& $options['test_authority']
|
&& $options['test_authority']
|
||||||
|
@ -258,7 +258,7 @@ class Note extends Model
|
||||||
$explorer = new Explorer();
|
$explorer = new Explorer();
|
||||||
try {
|
try {
|
||||||
$actors = $explorer->lookup($ap_tag->get('href'));
|
$actors = $explorer->lookup($ap_tag->get('href'));
|
||||||
foreach($actors as $actor) {
|
foreach ($actors as $actor) {
|
||||||
$object_mentions_ids[$actor->getId()] = $ap_tag->get('href');
|
$object_mentions_ids[$actor->getId()] = $ap_tag->get('href');
|
||||||
}
|
}
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
|
@ -356,7 +356,6 @@ class Note extends Model
|
||||||
break;
|
break;
|
||||||
case VisibilityScope::GROUP:
|
case VisibilityScope::GROUP:
|
||||||
// Will have the group in the To
|
// Will have the group in the To
|
||||||
// no break
|
|
||||||
case VisibilityScope::COLLECTION:
|
case VisibilityScope::COLLECTION:
|
||||||
// Since we don't support sending unlisted/followers-only
|
// Since we don't support sending unlisted/followers-only
|
||||||
// notices, arriving here means we're instead answering to that type
|
// notices, arriving here means we're instead answering to that type
|
||||||
|
|
|
@ -87,6 +87,15 @@
|
||||||
padding: unset;
|
padding: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.feed-header > h1,
|
||||||
|
.feed-header > h2,
|
||||||
|
.feed-header > h3,
|
||||||
|
.feed-header > h4,
|
||||||
|
.feed-header > h5,
|
||||||
|
.feed-header > h6 {
|
||||||
|
margin-bottom: unset;
|
||||||
|
}
|
||||||
|
|
||||||
.feed-actions-details summary,
|
.feed-actions-details summary,
|
||||||
.note-actions-extra-details summary {
|
.note-actions-extra-details summary {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
|
@ -241,7 +241,7 @@
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-info-url-nickname {
|
.profile-info-url strong {
|
||||||
font-size: 1.215rem;
|
font-size: 1.215rem;
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
}
|
}
|
||||||
|
|
|
@ -74,12 +74,12 @@ use Symfony\Component\Messenger\MessageBusInterface;
|
||||||
use Symfony\Component\Routing\RouterInterface;
|
use Symfony\Component\Routing\RouterInterface;
|
||||||
use Symfony\Component\Security\Core\Security as SSecurity;
|
use Symfony\Component\Security\Core\Security as SSecurity;
|
||||||
use Symfony\Component\Security\Http\Util\TargetPathTrait;
|
use Symfony\Component\Security\Http\Util\TargetPathTrait;
|
||||||
|
use Symfony\Component\Yaml;
|
||||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
use SymfonyCasts\Bundle\ResetPassword\ResetPasswordHelperInterface;
|
use SymfonyCasts\Bundle\ResetPassword\ResetPasswordHelperInterface;
|
||||||
use SymfonyCasts\Bundle\VerifyEmail\VerifyEmailHelperInterface;
|
use SymfonyCasts\Bundle\VerifyEmail\VerifyEmailHelperInterface;
|
||||||
use Twig\Environment;
|
use Twig\Environment;
|
||||||
use Symfony\Component\Yaml;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @codeCoverageIgnore
|
* @codeCoverageIgnore
|
||||||
|
|
|
@ -36,7 +36,6 @@ use App\Core\DB\DB;
|
||||||
use App\Core\Event;
|
use App\Core\Event;
|
||||||
use App\Core\Log;
|
use App\Core\Log;
|
||||||
use App\Entity\Actor;
|
use App\Entity\Actor;
|
||||||
use App\Entity\Note;
|
|
||||||
use App\Util\Exception\NicknameException;
|
use App\Util\Exception\NicknameException;
|
||||||
use App\Util\Exception\ServerException;
|
use App\Util\Exception\ServerException;
|
||||||
use Component\Circle\Circle;
|
use Component\Circle\Circle;
|
||||||
|
|
|
@ -30,16 +30,31 @@ declare(strict_types = 1);
|
||||||
namespace App\Util;
|
namespace App\Util;
|
||||||
|
|
||||||
use BadMethodCallException;
|
use BadMethodCallException;
|
||||||
|
use const ENT_QUOTES;
|
||||||
|
use const ENT_SUBSTITUTE;
|
||||||
use Functional as F;
|
use Functional as F;
|
||||||
use HtmlSanitizer\SanitizerInterface;
|
use HtmlSanitizer\SanitizerInterface;
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
|
use function str_starts_with;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @mixin SanitizerInterface
|
* @mixin SanitizerInterface
|
||||||
|
*
|
||||||
* @method static string sanitize(string $html)
|
* @method static string sanitize(string $html)
|
||||||
*/
|
*/
|
||||||
abstract class HTML
|
abstract class HTML
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* Tags whose content is sensitive to indentation, so we shouldn't indent them
|
||||||
|
*/
|
||||||
|
public const NO_INDENT_TAGS = ['a', 'b', 'em', 'i', 'q', 's', 'p', 'sub', 'sup', 'u'];
|
||||||
|
public const ALLOWED_TAGS = ['p', 'b', 'br', 'a', 'span', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
|
||||||
|
public const FORBIDDEN_ATTRIBUTES = [
|
||||||
|
'onerror', 'form', 'onforminput', 'onbeforescriptexecute', 'formaction', 'onfocus', 'onload',
|
||||||
|
'data', 'event', 'autofocus', 'onactivate', 'onanimationstart', 'onwebkittransitionend', 'onblur', 'poster',
|
||||||
|
'onratechange', 'ontoggle', 'onscroll', 'actiontype', 'dirname', 'srcdoc',
|
||||||
|
];
|
||||||
|
public const SELF_CLOSING_TAG = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'link', 'meta', 'param', 'source', 'track', 'wbr'];
|
||||||
private static ?SanitizerInterface $sanitizer;
|
private static ?SanitizerInterface $sanitizer;
|
||||||
|
|
||||||
public static function setSanitizer($sanitizer): void
|
public static function setSanitizer($sanitizer): void
|
||||||
|
@ -47,21 +62,6 @@ abstract class HTML
|
||||||
self::$sanitizer = $sanitizer;
|
self::$sanitizer = $sanitizer;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Tags whose content is sensitive to indentation, so we shouldn't indent them
|
|
||||||
*/
|
|
||||||
public const NO_INDENT_TAGS = ['a', 'b', 'em', 'i', 'q', 's', 'p', 'sub', 'sup', 'u'];
|
|
||||||
|
|
||||||
public const ALLOWED_TAGS = ['p', 'b', 'br', 'a', 'span', 'div', 'hr'];
|
|
||||||
|
|
||||||
public const FORBIDDEN_ATTRIBUTES = [
|
|
||||||
'onerror', 'form', 'onforminput', 'onbeforescriptexecute', 'formaction', 'onfocus', 'onload',
|
|
||||||
'data', 'event', 'autofocus', 'onactivate', 'onanimationstart', 'onwebkittransitionend', 'onblur', 'poster',
|
|
||||||
'onratechange', 'ontoggle', 'onscroll', 'actiontype', 'dirname', 'srcdoc',
|
|
||||||
];
|
|
||||||
|
|
||||||
public const SELF_CLOSING_TAG = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'link', 'meta', 'param', 'source', 'track', 'wbr'];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an HTML tag without attributes
|
* Creates an HTML tag without attributes
|
||||||
*/
|
*/
|
||||||
|
@ -78,14 +78,12 @@ abstract class HTML
|
||||||
$html = '<' . $tag . (\is_string($attrs) ? ($attrs ? ' ' : '') . $attrs : self::attr($attrs, $options));
|
$html = '<' . $tag . (\is_string($attrs) ? ($attrs ? ' ' : '') . $attrs : self::attr($attrs, $options));
|
||||||
if (\in_array($tag, self::SELF_CLOSING_TAG)) {
|
if (\in_array($tag, self::SELF_CLOSING_TAG)) {
|
||||||
$html .= '>';
|
$html .= '>';
|
||||||
} else {
|
} elseif (($options['indent'] ?? true) && !\in_array($tag, self::NO_INDENT_TAGS)) {
|
||||||
if (!\in_array($tag, self::NO_INDENT_TAGS) && ($options['indent'] ?? true)) {
|
|
||||||
$inner = Formatting::indent($content);
|
$inner = Formatting::indent($content);
|
||||||
$html .= ">\n" . ($inner == '' ? '' : $inner . "\n") . "</{$tag}>";
|
$html .= ">\n" . ($inner === '' ? '' : $inner . "\n") . "</{$tag}>";
|
||||||
} else {
|
} else {
|
||||||
$html .= ">{$content}</{$tag}>";
|
$html .= ">{$content}</{$tag}>";
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return $html;
|
return $html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,12 +100,11 @@ abstract class HTML
|
||||||
*/
|
*/
|
||||||
private static function process_attribute(string $val, string $key, array $options): string
|
private static function process_attribute(string $val, string $key, array $options): string
|
||||||
{
|
{
|
||||||
if (\in_array($key, array_merge($options['forbidden_attributes'] ?? [], self::FORBIDDEN_ATTRIBUTES))
|
if (\in_array($key, array_merge($options['forbidden_attributes'] ?? [], self::FORBIDDEN_ATTRIBUTES)) || str_starts_with($val, 'javascript:')) {
|
||||||
|| str_starts_with($val, 'javascript:')) {
|
|
||||||
throw new InvalidArgumentException("HTML::html: Attribute {$key} is not allowed");
|
throw new InvalidArgumentException("HTML::html: Attribute {$key} is not allowed");
|
||||||
}
|
}
|
||||||
if (!($options['raw'] ?? false)) {
|
if (!($options['raw'] ?? false)) {
|
||||||
$val = htmlspecialchars($val, flags: \ENT_QUOTES | \ENT_SUBSTITUTE, double_encode: false);
|
$val = htmlspecialchars($val, flags: ENT_QUOTES | ENT_SUBSTITUTE, double_encode: false);
|
||||||
}
|
}
|
||||||
return "{$key}=\"{$val}\"";
|
return "{$key}=\"{$val}\"";
|
||||||
}
|
}
|
||||||
|
@ -122,7 +119,7 @@ abstract class HTML
|
||||||
if ($options['raw'] ?? false) {
|
if ($options['raw'] ?? false) {
|
||||||
return $html;
|
return $html;
|
||||||
} else {
|
} else {
|
||||||
return htmlspecialchars($html, flags: \ENT_QUOTES | \ENT_SUBSTITUTE, double_encode: false);
|
return htmlspecialchars($html, flags: ENT_QUOTES | ENT_SUBSTITUTE, double_encode: false);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$out = '';
|
$out = '';
|
||||||
|
@ -134,10 +131,10 @@ abstract class HTML
|
||||||
$is_tag = \is_string($tag) && preg_match('/[A-Za-z][A-Za-z0-9]*/', $tag);
|
$is_tag = \is_string($tag) && preg_match('/[A-Za-z][A-Za-z0-9]*/', $tag);
|
||||||
$inner = self::html($contents, $options);
|
$inner = self::html($contents, $options);
|
||||||
if ($is_tag) {
|
if ($is_tag) {
|
||||||
if (!\in_array($tag, array_merge($options['allowed_tags'] ?? [], self::ALLOWED_TAGS))) {
|
if (!\in_array($tag, array_merge($options['allowed_tags'] ?? [], self::ALLOWED_TAGS), true)) {
|
||||||
throw new InvalidArgumentException("HTML::html: Tag {$tag} is not allowed");
|
throw new InvalidArgumentException("HTML::html: Tag {$tag} is not allowed");
|
||||||
}
|
}
|
||||||
if (!empty($inner) && !\in_array($tag, self::NO_INDENT_TAGS) && ($options['indent'] ?? true)) {
|
if (!empty($inner) && !\in_array($tag, self::NO_INDENT_TAGS, true) && ($options['indent'] ?? true)) {
|
||||||
$inner = "\n" . Formatting::indent($inner) . "\n";
|
$inner = "\n" . Formatting::indent($inner) . "\n";
|
||||||
}
|
}
|
||||||
$out .= "<{$tag}{$attrs}>{$inner}</{$tag}>";
|
$out .= "<{$tag}{$attrs}>{$inner}</{$tag}>";
|
||||||
|
@ -154,8 +151,8 @@ abstract class HTML
|
||||||
{
|
{
|
||||||
if (method_exists(self::$sanitizer, $name)) {
|
if (method_exists(self::$sanitizer, $name)) {
|
||||||
return self::$sanitizer->{$name}(...$args);
|
return self::$sanitizer->{$name}(...$args);
|
||||||
} else {
|
}
|
||||||
|
|
||||||
throw new BadMethodCallException("Method Security::{$name} doesn't exist");
|
throw new BadMethodCallException("Method Security::{$name} doesn't exist");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
90
src/Util/HTML/Heading.php
Normal file
90
src/Util/HTML/Heading.php
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
<?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/>.
|
||||||
|
// }}}
|
||||||
|
|
||||||
|
namespace App\Util\HTML;
|
||||||
|
|
||||||
|
use function App\Core\I18n\_m;
|
||||||
|
use App\Util\HTML;
|
||||||
|
use Twig\Markup;
|
||||||
|
|
||||||
|
class Heading extends HTML
|
||||||
|
{
|
||||||
|
private string $heading_type = 'h1';
|
||||||
|
private string $heading_text;
|
||||||
|
private array $classes = [];
|
||||||
|
|
||||||
|
public function __construct(int $level, array $classes, string $text)
|
||||||
|
{
|
||||||
|
$this->setHeadingText($text);
|
||||||
|
foreach ($classes as $class) {
|
||||||
|
$this->addClass($class);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($level >= 1 && $level <= 6) {
|
||||||
|
$this->heading_type = 'h' . $level;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addClass(string $c): self
|
||||||
|
{
|
||||||
|
if (!\in_array($c, $this->classes, true)) {
|
||||||
|
$this->classes[] = $c;
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHtml(): Markup
|
||||||
|
{
|
||||||
|
return new Markup($this->__toString(), 'UTF-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __toString()
|
||||||
|
{
|
||||||
|
return $this::html([$this->getHeadingType() => ['attrs' => ['class' => !empty($this->getClasses()) ? implode(' ', $this->getClasses()) : ''], _m($this->getHeadingText())]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHeadingType(): string
|
||||||
|
{
|
||||||
|
return $this->heading_type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setHeadingType(string $value): static
|
||||||
|
{
|
||||||
|
$this->heading_type = $value;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getClasses(): array
|
||||||
|
{
|
||||||
|
return $this->classes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHeadingText(): string
|
||||||
|
{
|
||||||
|
return $this->heading_text;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setHeadingText(string $value): static
|
||||||
|
{
|
||||||
|
$this->heading_text = $value;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,11 +1,13 @@
|
||||||
{% extends '/collection/notes.html.twig' %}
|
{% extends '/collection/notes.html.twig' %}
|
||||||
|
|
||||||
{% block title %}{% trans %}%nickname%'s profile{% endtrans %}{% endblock %}
|
{% block title %}
|
||||||
|
{% if page_title is defined %}
|
||||||
|
{{ page_title }}
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
{% block profile_view %}
|
{% include 'cards/blocks/profile.html.twig' with { profile_card_type: 'main' } %}
|
||||||
{% include '/cards/blocks/profile.html.twig' %}
|
<hr>
|
||||||
{% endblock profile_view %}
|
|
||||||
|
|
||||||
{{ parent() }}
|
{{ parent() }}
|
||||||
{% endblock body %}
|
{% endblock body %}
|
||||||
|
|
|
@ -73,21 +73,9 @@
|
||||||
<div class="note-text" tabindex="0"
|
<div class="note-text" tabindex="0"
|
||||||
title="{{ 'Main note content' | trans }}">
|
title="{{ 'Main note content' | trans }}">
|
||||||
{% set paragraph_array = note.getRenderedSplit() %}
|
{% set paragraph_array = note.getRenderedSplit() %}
|
||||||
{% if 'conversation' not in app.request.get('_route') and paragraph_array | length > 3 %}
|
|
||||||
<p>{{ paragraph_array[0] | raw }}</p>
|
|
||||||
<details class="note-text-details">
|
|
||||||
<summary class="note-text-details-summary">
|
|
||||||
<small>{% trans %}Expand to see all content{% endtrans %}</small>
|
|
||||||
</summary>
|
|
||||||
{% for paragraph in paragraph_array | slice(1, paragraph_array | length) %}
|
|
||||||
<p>{{ paragraph | raw }}</p>
|
|
||||||
{% endfor %}
|
|
||||||
</details>
|
|
||||||
{% else %}
|
|
||||||
{% for paragraph in paragraph_array %}
|
{% for paragraph in paragraph_array %}
|
||||||
<p>{{ paragraph | raw }}</p>
|
<p>{{ paragraph | raw }}</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock note_text %}
|
{% endblock note_text %}
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
{% set actor_fullname = actor.getFullname() %}
|
|
||||||
{% set actor_nickname = actor.getNickname() %}
|
|
||||||
{% set actor_avatar = actor.getAvatarUrl() %}
|
|
||||||
{% set actor_avatar_dimensions = actor.getAvatarDimensions() %}
|
|
||||||
{% set actor_tags = actor.getSelfTags() %}
|
|
||||||
{% set actor_has_bio = actor.hasBio() %}
|
|
||||||
{% set actor_uri = actor.getUri() %}
|
|
||||||
{% set actor_url = actor.getUrl() %}
|
|
||||||
{% set actor_is_local = actor.getIsLocal() %}
|
|
||||||
{% set mention = mention(actor) %}
|
|
||||||
|
|
||||||
{% block profile_view %}
|
{% block profile_view %}
|
||||||
|
{% set actor_fullname = actor.getFullname() %}
|
||||||
|
{% set actor_nickname = actor.getNickname() %}
|
||||||
|
{% set actor_avatar = actor.getAvatarUrl() %}
|
||||||
|
{% set actor_avatar_dimensions = actor.getAvatarDimensions() %}
|
||||||
|
{% set actor_tags = actor.getSelfTags() %}
|
||||||
|
{% set actor_has_bio = actor.hasBio() %}
|
||||||
|
{% set actor_uri = actor.getUri() %}
|
||||||
|
{% set actor_url = actor.getUrl() %}
|
||||||
|
{% set actor_is_local = actor.getIsLocal() %}
|
||||||
|
{% set mention = mention(actor) %}
|
||||||
|
|
||||||
<section id='profile-{{ actor.id }}' class='profile'
|
<section id='profile-{{ actor.id }}' class='profile'
|
||||||
title="{% trans %} %actor_nickname%'s profile information{% endtrans %}">
|
title="{% trans %} %actor_nickname%'s profile information{% endtrans %}">
|
||||||
<header>
|
<header>
|
||||||
|
@ -19,12 +19,20 @@
|
||||||
title="{% trans %} %actor_nickname%'s avatar{% endtrans %}"
|
title="{% trans %} %actor_nickname%'s avatar{% endtrans %}"
|
||||||
width="{{ actor_avatar_dimensions['width'] }}"
|
width="{{ actor_avatar_dimensions['width'] }}"
|
||||||
height="{{ actor_avatar_dimensions['height'] }}">
|
height="{{ actor_avatar_dimensions['height'] }}">
|
||||||
<section>
|
<div>
|
||||||
<a class="profile-info-url" href="{{ actor_url }}">
|
<a class="profile-info-url" href="{{ actor_url }}">
|
||||||
|
{% if profile_card_type is defined and profile_card_type starts with 'main' %}
|
||||||
|
<h1 class="profile-info-url-nickname"
|
||||||
|
title="{% trans %} %actor_nickname%'s profile {% endtrans %}">
|
||||||
|
{% if actor_fullname is not null %}{{ actor_fullname }}{% else %}{{ actor_nickname }}{% endif %}
|
||||||
|
</h1>
|
||||||
|
{% else %}
|
||||||
<strong class="profile-info-url-nickname"
|
<strong class="profile-info-url-nickname"
|
||||||
title="{% trans %} %actor_nickname%'s profile {% endtrans %}">
|
title="{% trans %} %actor_nickname%'s profile {% endtrans %}">
|
||||||
{% if actor_fullname is not null %}{{ actor_fullname }}{% else %}{{ actor_nickname }}{% endif %}
|
{% if actor_fullname is not null %}{{ actor_fullname }}{% else %}{{ actor_nickname }}{% endif %}
|
||||||
</strong>
|
</strong>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if not actor_is_local %}
|
{% if not actor_is_local %}
|
||||||
<span class="profile-info-url-remote">
|
<span class="profile-info-url-remote">
|
||||||
<a href="{{ actor_uri }}" class="u-url" title="{% trans %} %actor_nickname%'s permalink {% endtrans %}">{{ mention }}</a>
|
<a href="{{ actor_uri }}" class="u-url" title="{% trans %} %actor_nickname%'s permalink {% endtrans %}">{{ mention }}</a>
|
||||||
|
@ -40,7 +48,7 @@
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="profile-stats">
|
<div class="profile-stats">
|
||||||
<span class="profile-stats-subscriptions"
|
<span class="profile-stats-subscriptions"
|
||||||
|
|
|
@ -21,7 +21,6 @@ declare(strict_types = 1);
|
||||||
|
|
||||||
namespace App\Tests\Util;
|
namespace App\Tests\Util;
|
||||||
|
|
||||||
use App\Util\HTML;
|
|
||||||
use Jchook\AssertThrows\AssertThrows;
|
use Jchook\AssertThrows\AssertThrows;
|
||||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||||
use TypeError;
|
use TypeError;
|
||||||
|
@ -32,17 +31,17 @@ class HTMLTest extends WebTestCase
|
||||||
|
|
||||||
public function testHTML()
|
public function testHTML()
|
||||||
{
|
{
|
||||||
static::assertSame('', HTML::html(''));
|
static::assertSame('', HTML\HTML::html(''));
|
||||||
static::assertSame('<a></a>', HTML::html(['a' => '']));
|
static::assertSame('<a></a>', HTML\HTML::html(['a' => '']));
|
||||||
static::assertSame("<div>\n <p></p>\n</div>", HTML::html(['div' => ['p' => '']]));
|
static::assertSame("<div>\n <p></p>\n</div>", HTML\HTML::html(['div' => ['p' => '']]));
|
||||||
static::assertSame("<div>\n <div>\n <p></p>\n </div>\n</div>", HTML::html(['div' => ['div' => ['p' => '']]]));
|
static::assertSame("<div>\n <div>\n <p></p>\n </div>\n</div>", HTML\HTML::html(['div' => ['div' => ['p' => '']]]));
|
||||||
static::assertSame("<div>\n <div>\n <div>\n <p></p>\n </div>\n </div>\n</div>", HTML::html(['div' => ['div' => ['div' => ['p' => '']]]]));
|
static::assertSame("<div>\n <div>\n <div>\n <p></p>\n </div>\n </div>\n</div>", HTML\HTML::html(['div' => ['div' => ['div' => ['p' => '']]]]));
|
||||||
static::assertSame('<a href="test"><p></p></a>', HTML::html(['a' => ['attrs' => ['href' => 'test'], 'p' => '']]));
|
static::assertSame('<a href="test"><p></p></a>', HTML\HTML::html(['a' => ['attrs' => ['href' => 'test'], 'p' => '']]));
|
||||||
static::assertSame('<a><p>foo</p><br></a>', HTML::html(['a' => ['p' => 'foo', 'br' => 'empty']]));
|
static::assertSame('<a><p>foo</p><br></a>', HTML\HTML::html(['a' => ['p' => 'foo', 'br' => 'empty']]));
|
||||||
static::assertSame("<div>\n <a><p>foo</p><br></a>\n</div>", HTML::html(['div' => ['a' => ['p' => 'foo', 'br' => 'empty']]]));
|
static::assertSame("<div>\n <a><p>foo</p><br></a>\n</div>", HTML\HTML::html(['div' => ['a' => ['p' => 'foo', 'br' => 'empty']]]));
|
||||||
static::assertSame('<div><a><p>foo</p><br></a></div>', HTML::html(['div' => ['a' => ['p' => 'foo', 'br' => 'empty']]], options: ['indent' => false]));
|
static::assertSame('<div><a><p>foo</p><br></a></div>', HTML\HTML::html(['div' => ['a' => ['p' => 'foo', 'br' => 'empty']]], options: ['indent' => false]));
|
||||||
static::assertThrows(TypeError::class, fn () => HTML::html(1));
|
static::assertThrows(TypeError::class, fn () => HTML\HTML::html(1));
|
||||||
static::assertSame('<a href="test">foo</a>', HTML::tag('a', ['href' => 'test'], content: 'foo', options: ['empty' => false]));
|
static::assertSame('<a href="test">foo</a>', HTML\HTML::tag('a', ['href' => 'test'], content: 'foo', options: ['empty' => false]));
|
||||||
static::assertSame('<br>', HTML::tag('br', attrs: null, content: null, options: ['empty' => true]));
|
static::assertSame('<br>', HTML\HTML::tag('br', attrs: null, content: null, options: ['empty' => true]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user