[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 %}
|
||||
<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) %}
|
||||
{% for widget in prepend_actors_collection %}
|
||||
|
|
|
@ -14,18 +14,10 @@
|
|||
{% endfor %}
|
||||
|
||||
{% 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') %}
|
||||
{% if page_title is defined %}
|
||||
{% if current_path starts with 'feed_' or 'conversation' in current_path %}
|
||||
<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 %}
|
||||
{% if notes_feed_title is defined %}
|
||||
{{ notes_feed_title.getHtml() }}
|
||||
{% endif %}
|
||||
<nav class="feed-actions" title="{{ 'Actions that change how the feed behaves' | trans }}">
|
||||
<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\RedirectException;
|
||||
use App\Util\Exception\ServerException;
|
||||
use App\Util\HTML\Heading;
|
||||
use Component\Collection\Util\Controller\FeedController;
|
||||
use Component\Conversation\Entity\ConversationMute;
|
||||
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
|
||||
{
|
||||
$page_title = _m('Conversation');
|
||||
|
||||
return [
|
||||
'_template' => 'collection/notes.html.twig',
|
||||
'notes' => $this->query(query: "note-conversation:{$conversation_id}")['notes'] ?? [],
|
||||
'should_format' => false,
|
||||
'page_title' => _m('Conversation'),
|
||||
'_template' => 'collection/notes.html.twig',
|
||||
'notes' => $this->query(query: "note-conversation:{$conversation_id}")['notes'] ?? [],
|
||||
'should_format' => false,
|
||||
'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 App\Util\Common;
|
||||
use App\Util\HTML\Heading;
|
||||
use Component\Collection\Util\Controller\FeedController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
|
@ -47,11 +48,13 @@ class Feeds extends FeedController
|
|||
*/
|
||||
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 [
|
||||
'_template' => 'collection/notes.html.twig',
|
||||
'page_title' => _m(\is_null(Common::user()) ? 'Feed' : 'Planet'),
|
||||
'notes' => $data['notes'],
|
||||
'_template' => 'collection/notes.html.twig',
|
||||
'page_title' => $page_title,
|
||||
'notes_feed_title' => (new Heading(level: 1, classes: ['feed-title'], text: $page_title)),
|
||||
'notes' => $data['notes'],
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -63,9 +66,10 @@ class Feeds extends FeedController
|
|||
Common::ensureLoggedIn();
|
||||
$data = $this->query('note-from:subscribed-person,subscribed-group,subscribed-organisation');
|
||||
return [
|
||||
'_template' => 'collection/notes.html.twig',
|
||||
'page_title' => _m('Home'),
|
||||
'notes' => $data['notes'],
|
||||
'_template' => 'collection/notes.html.twig',
|
||||
'page_title' => _m('Home'),
|
||||
'notes_feed_title' => (new Heading(level: 1, classes: ['feed-title'], text: 'Home')),
|
||||
'notes' => $data['notes'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,12 +22,11 @@ declare(strict_types = 1);
|
|||
namespace Component\Group;
|
||||
|
||||
use App\Core\Event;
|
||||
use App\Entity\Activity;
|
||||
use Component\Notification\Notification;
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Core\Modules\Component;
|
||||
use App\Core\Router\RouteLoader;
|
||||
use App\Core\Router\Router;
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Actor;
|
||||
use App\Util\Common;
|
||||
use App\Util\HTML;
|
||||
|
@ -35,6 +34,7 @@ use App\Util\Nickname;
|
|||
use Component\Circle\Controller\SelfTagsSettings;
|
||||
use Component\Group\Controller as C;
|
||||
use Component\Group\Entity\LocalGroup;
|
||||
use Component\Notification\Notification;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
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
|
||||
* 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
|
||||
{
|
||||
foreach($targets as $target) {
|
||||
foreach ($targets as $target) {
|
||||
if ($target->isGroup()) {
|
||||
// The Group announces to its subscribers
|
||||
Notification::notify($target, $activity, $target->getSubscribers(), $reason);
|
||||
|
@ -78,11 +73,10 @@ class Group extends Component
|
|||
$group = $vars['actor'];
|
||||
if (!\is_null($actor) && $group->isGroup()) {
|
||||
if ($actor->canModerate($group)) {
|
||||
$url = Router::url('group_settings', ['id' => $group->getId()]);
|
||||
$url = Router::url('group_settings', ['id' => $group->getId()]);
|
||||
$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')]]);
|
||||
|
||||
}
|
||||
return Event::next;
|
||||
}
|
||||
|
@ -90,7 +84,7 @@ class Group extends Component
|
|||
public function onPopulateSettingsTabs(Request $request, string $section, array &$tabs): bool
|
||||
{
|
||||
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);
|
||||
$tabs[] = [
|
||||
'title' => 'Self tags',
|
||||
|
|
|
@ -46,7 +46,7 @@ class Link extends Component
|
|||
preg_match_all($this->getURLRegex(), $content, $matched_urls);
|
||||
$matched_urls = array_unique($matched_urls[1]);
|
||||
foreach ($matched_urls as $match) {
|
||||
if (in_array($match, $ignore)) {
|
||||
if (\in_array($match, $ignore)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
|
|
|
@ -30,6 +30,7 @@ use App\Entity as E;
|
|||
use App\Entity\LocalUser;
|
||||
use App\Util\Exception\ClientException;
|
||||
use App\Util\Exception\ServerException;
|
||||
use App\Util\HTML\Heading;
|
||||
use Component\Collection\Util\Controller\FeedController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
|
@ -75,10 +76,12 @@ class PersonFeed extends FeedController
|
|||
public function personView(Request $request, Actor $person): array
|
||||
{
|
||||
return [
|
||||
'_template' => 'actor/view.html.twig',
|
||||
'actor' => $person,
|
||||
'nickname' => $person->getNickname(),
|
||||
'notes' => E\Note::getAllNotesByActor($person),
|
||||
'_template' => 'actor/view.html.twig',
|
||||
'actor' => $person,
|
||||
'nickname' => $person->getNickname(),
|
||||
'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\Form\FormFields;
|
||||
use App\Util\Formatting;
|
||||
use App\Util\HTML\Heading;
|
||||
use Component\Collection\Util\Controller\FeedController;
|
||||
use Component\Search as Comp;
|
||||
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_builder_form' => $search_builder_form->createView(),
|
||||
'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 ?? [],
|
||||
'page' => 1, // TODO paginate
|
||||
];
|
||||
|
|
|
@ -39,8 +39,6 @@ use App\Core\DB\DB;
|
|||
use App\Core\Event;
|
||||
use App\Core\GSFile;
|
||||
use App\Core\HTTPClient;
|
||||
use App\Util\HTML;
|
||||
use Plugin\ActivityPub\Util\Explorer;
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Core\Log;
|
||||
use App\Core\Router\Router;
|
||||
|
@ -52,6 +50,7 @@ use App\Util\Exception\DuplicateFoundException;
|
|||
use App\Util\Exception\NoSuchActorException;
|
||||
use App\Util\Exception\ServerException;
|
||||
use App\Util\Formatting;
|
||||
use App\Util\HTML;
|
||||
use App\Util\TemporaryFile;
|
||||
use Component\Attachment\Entity\ActorToAttachment;
|
||||
use Component\Attachment\Entity\AttachmentToNote;
|
||||
|
@ -66,6 +65,7 @@ use Exception;
|
|||
use InvalidArgumentException;
|
||||
use Plugin\ActivityPub\ActivityPub;
|
||||
use Plugin\ActivityPub\Entity\ActivitypubObject;
|
||||
use Plugin\ActivityPub\Util\Explorer;
|
||||
use Plugin\ActivityPub\Util\Model;
|
||||
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
|
||||
|
@ -118,8 +118,8 @@ class Note extends Model
|
|||
$type_note = \is_string($json) ? self::jsonToType($json) : $json;
|
||||
$actor_id = null;
|
||||
$actor = null;
|
||||
$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')) : [];
|
||||
$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')) : [];
|
||||
if ($json instanceof AbstractObject
|
||||
&& \array_key_exists('test_authority', $options)
|
||||
&& $options['test_authority']
|
||||
|
@ -184,7 +184,7 @@ class Note extends Model
|
|||
continue;
|
||||
}
|
||||
try {
|
||||
$actor = ActivityPub::getActorByUri($target);
|
||||
$actor = ActivityPub::getActorByUri($target);
|
||||
$object_mentions_ids[$actor->getId()] = $target;
|
||||
// If $to is a group, set note's scope as Group
|
||||
if ($actor->isGroup()) {
|
||||
|
@ -199,7 +199,7 @@ class Note extends Model
|
|||
continue;
|
||||
}
|
||||
try {
|
||||
$actor = ActivityPub::getActorByUri($target);
|
||||
$actor = ActivityPub::getActorByUri($target);
|
||||
$object_mentions_ids[$actor->getId()] = $target;
|
||||
} catch (Exception $e) {
|
||||
Log::debug('ActivityPub->Model->Note->fromJson->getActorByUri', [$e]);
|
||||
|
@ -248,7 +248,7 @@ class Note extends Model
|
|||
case 'Mention':
|
||||
case 'Group':
|
||||
try {
|
||||
$actor = ActivityPub::getActorByUri($ap_tag->get('href'));
|
||||
$actor = ActivityPub::getActorByUri($ap_tag->get('href'));
|
||||
$object_mentions_ids[$actor->getId()] = $ap_tag->get('href');
|
||||
} catch (Exception $e) {
|
||||
Log::debug('ActivityPub->Model->Note->fromJson->getActorByUri', [$e]);
|
||||
|
@ -258,7 +258,7 @@ class Note extends Model
|
|||
$explorer = new Explorer();
|
||||
try {
|
||||
$actors = $explorer->lookup($ap_tag->get('href'));
|
||||
foreach($actors as $actor) {
|
||||
foreach ($actors as $actor) {
|
||||
$object_mentions_ids[$actor->getId()] = $ap_tag->get('href');
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
|
@ -356,7 +356,6 @@ class Note extends Model
|
|||
break;
|
||||
case VisibilityScope::GROUP:
|
||||
// Will have the group in the To
|
||||
// no break
|
||||
case VisibilityScope::COLLECTION:
|
||||
// Since we don't support sending unlisted/followers-only
|
||||
// notices, arriving here means we're instead answering to that type
|
||||
|
|
|
@ -87,6 +87,15 @@
|
|||
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,
|
||||
.note-actions-extra-details summary {
|
||||
display: block;
|
||||
|
|
|
@ -241,7 +241,7 @@
|
|||
display: block;
|
||||
}
|
||||
|
||||
.profile-info-url-nickname {
|
||||
.profile-info-url strong {
|
||||
font-size: 1.215rem;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
|
|
@ -74,12 +74,12 @@ use Symfony\Component\Messenger\MessageBusInterface;
|
|||
use Symfony\Component\Routing\RouterInterface;
|
||||
use Symfony\Component\Security\Core\Security as SSecurity;
|
||||
use Symfony\Component\Security\Http\Util\TargetPathTrait;
|
||||
use Symfony\Component\Yaml;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
use SymfonyCasts\Bundle\ResetPassword\ResetPasswordHelperInterface;
|
||||
use SymfonyCasts\Bundle\VerifyEmail\VerifyEmailHelperInterface;
|
||||
use Twig\Environment;
|
||||
use Symfony\Component\Yaml;
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
|
@ -230,8 +230,8 @@ class GNUsocial implements EventSubscriberInterface
|
|||
$local_file = INSTALLDIR . '/social.local.yaml';
|
||||
if (!file_exists($local_file)) {
|
||||
$node_name = $_ENV['CONFIG_NODE_NAME'];
|
||||
$domain = $_ENV['CONFIG_DOMAIN'];
|
||||
$yaml = (new Yaml\Dumper(indentation: 2))->dump(['parameters' => ['locals' => ['gnusocial' => ['site' => ['server' => $domain, 'name' => $node_name]]]]], Yaml\Yaml::DUMP_OBJECT_AS_MAP);
|
||||
$domain = $_ENV['CONFIG_DOMAIN'];
|
||||
$yaml = (new Yaml\Dumper(indentation: 2))->dump(['parameters' => ['locals' => ['gnusocial' => ['site' => ['server' => $domain, 'name' => $node_name]]]]], Yaml\Yaml::DUMP_OBJECT_AS_MAP);
|
||||
file_put_contents($local_file, $yaml);
|
||||
}
|
||||
|
||||
|
|
|
@ -36,7 +36,6 @@ use App\Core\DB\DB;
|
|||
use App\Core\Event;
|
||||
use App\Core\Log;
|
||||
use App\Entity\Actor;
|
||||
use App\Entity\Note;
|
||||
use App\Util\Exception\NicknameException;
|
||||
use App\Util\Exception\ServerException;
|
||||
use Component\Circle\Circle;
|
||||
|
|
|
@ -30,16 +30,31 @@ declare(strict_types = 1);
|
|||
namespace App\Util;
|
||||
|
||||
use BadMethodCallException;
|
||||
use const ENT_QUOTES;
|
||||
use const ENT_SUBSTITUTE;
|
||||
use Functional as F;
|
||||
use HtmlSanitizer\SanitizerInterface;
|
||||
use InvalidArgumentException;
|
||||
use function str_starts_with;
|
||||
|
||||
/**
|
||||
* @mixin SanitizerInterface
|
||||
*
|
||||
* @method static string sanitize(string $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;
|
||||
|
||||
public static function setSanitizer($sanitizer): void
|
||||
|
@ -47,21 +62,6 @@ abstract class HTML
|
|||
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
|
||||
*/
|
||||
|
@ -78,13 +78,11 @@ abstract class HTML
|
|||
$html = '<' . $tag . (\is_string($attrs) ? ($attrs ? ' ' : '') . $attrs : self::attr($attrs, $options));
|
||||
if (\in_array($tag, self::SELF_CLOSING_TAG)) {
|
||||
$html .= '>';
|
||||
} elseif (($options['indent'] ?? true) && !\in_array($tag, self::NO_INDENT_TAGS)) {
|
||||
$inner = Formatting::indent($content);
|
||||
$html .= ">\n" . ($inner === '' ? '' : $inner . "\n") . "</{$tag}>";
|
||||
} else {
|
||||
if (!\in_array($tag, self::NO_INDENT_TAGS) && ($options['indent'] ?? true)) {
|
||||
$inner = Formatting::indent($content);
|
||||
$html .= ">\n" . ($inner == '' ? '' : $inner . "\n") . "</{$tag}>";
|
||||
} else {
|
||||
$html .= ">{$content}</{$tag}>";
|
||||
}
|
||||
$html .= ">{$content}</{$tag}>";
|
||||
}
|
||||
return $html;
|
||||
}
|
||||
|
@ -102,12 +100,11 @@ abstract class HTML
|
|||
*/
|
||||
private static function process_attribute(string $val, string $key, array $options): string
|
||||
{
|
||||
if (\in_array($key, array_merge($options['forbidden_attributes'] ?? [], self::FORBIDDEN_ATTRIBUTES))
|
||||
|| str_starts_with($val, 'javascript:')) {
|
||||
if (\in_array($key, array_merge($options['forbidden_attributes'] ?? [], self::FORBIDDEN_ATTRIBUTES)) || str_starts_with($val, 'javascript:')) {
|
||||
throw new InvalidArgumentException("HTML::html: Attribute {$key} is not allowed");
|
||||
}
|
||||
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}\"";
|
||||
}
|
||||
|
@ -122,7 +119,7 @@ abstract class HTML
|
|||
if ($options['raw'] ?? false) {
|
||||
return $html;
|
||||
} else {
|
||||
return htmlspecialchars($html, flags: \ENT_QUOTES | \ENT_SUBSTITUTE, double_encode: false);
|
||||
return htmlspecialchars($html, flags: ENT_QUOTES | ENT_SUBSTITUTE, double_encode: false);
|
||||
}
|
||||
} else {
|
||||
$out = '';
|
||||
|
@ -134,10 +131,10 @@ abstract class HTML
|
|||
$is_tag = \is_string($tag) && preg_match('/[A-Za-z][A-Za-z0-9]*/', $tag);
|
||||
$inner = self::html($contents, $options);
|
||||
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");
|
||||
}
|
||||
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";
|
||||
}
|
||||
$out .= "<{$tag}{$attrs}>{$inner}</{$tag}>";
|
||||
|
@ -154,8 +151,8 @@ abstract class HTML
|
|||
{
|
||||
if (method_exists(self::$sanitizer, $name)) {
|
||||
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' %}
|
||||
|
||||
{% block title %}{% trans %}%nickname%'s profile{% endtrans %}{% endblock %}
|
||||
{% block title %}
|
||||
{% if page_title is defined %}
|
||||
{{ page_title }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% block profile_view %}
|
||||
{% include '/cards/blocks/profile.html.twig' %}
|
||||
{% endblock profile_view %}
|
||||
|
||||
{% include 'cards/blocks/profile.html.twig' with { profile_card_type: 'main' } %}
|
||||
<hr>
|
||||
{{ parent() }}
|
||||
{% endblock body %}
|
||||
|
|
|
@ -73,21 +73,9 @@
|
|||
<div class="note-text" tabindex="0"
|
||||
title="{{ 'Main note content' | trans }}">
|
||||
{% 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 %}
|
||||
<p>{{ paragraph | raw }}</p>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% for paragraph in paragraph_array %}
|
||||
<p>{{ paragraph | raw }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% 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 %}
|
||||
{% 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'
|
||||
title="{% trans %} %actor_nickname%'s profile information{% endtrans %}">
|
||||
<header>
|
||||
|
@ -19,12 +19,20 @@
|
|||
title="{% trans %} %actor_nickname%'s avatar{% endtrans %}"
|
||||
width="{{ actor_avatar_dimensions['width'] }}"
|
||||
height="{{ actor_avatar_dimensions['height'] }}">
|
||||
<section>
|
||||
<div>
|
||||
<a class="profile-info-url" href="{{ actor_url }}">
|
||||
<strong class="profile-info-url-nickname"
|
||||
title="{% trans %} %actor_nickname%'s profile {% endtrans %}">
|
||||
{% if actor_fullname is not null %}{{ actor_fullname }}{% else %}{{ actor_nickname }}{% endif %}
|
||||
</strong>
|
||||
{% 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"
|
||||
title="{% trans %} %actor_nickname%'s profile {% endtrans %}">
|
||||
{% if actor_fullname is not null %}{{ actor_fullname }}{% else %}{{ actor_nickname }}{% endif %}
|
||||
</strong>
|
||||
{% endif %}
|
||||
|
||||
{% if not actor_is_local %}
|
||||
<span class="profile-info-url-remote">
|
||||
<a href="{{ actor_uri }}" class="u-url" title="{% trans %} %actor_nickname%'s permalink {% endtrans %}">{{ mention }}</a>
|
||||
|
@ -40,7 +48,7 @@
|
|||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<div class="profile-stats">
|
||||
<span class="profile-stats-subscriptions"
|
||||
|
|
|
@ -21,7 +21,6 @@ declare(strict_types = 1);
|
|||
|
||||
namespace App\Tests\Util;
|
||||
|
||||
use App\Util\HTML;
|
||||
use Jchook\AssertThrows\AssertThrows;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
use TypeError;
|
||||
|
@ -32,17 +31,17 @@ class HTMLTest extends WebTestCase
|
|||
|
||||
public function testHTML()
|
||||
{
|
||||
static::assertSame('', HTML::html(''));
|
||||
static::assertSame('<a></a>', HTML::html(['a' => '']));
|
||||
static::assertSame("<div>\n <p></p>\n</div>", 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 <div>\n <p></p>\n </div>\n </div>\n</div>", HTML::html(['div' => ['div' => ['div' => ['p' => '']]]]));
|
||||
static::assertSame('<a href="test"><p></p></a>', HTML::html(['a' => ['attrs' => ['href' => 'test'], 'p' => '']]));
|
||||
static::assertSame('<a><p>foo</p><br></a>', 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><a><p>foo</p><br></a></div>', HTML::html(['div' => ['a' => ['p' => 'foo', 'br' => 'empty']]], options: ['indent' => false]));
|
||||
static::assertThrows(TypeError::class, fn () => HTML::html(1));
|
||||
static::assertSame('<a href="test">foo</a>', 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('', HTML\HTML::html(''));
|
||||
static::assertSame('<a></a>', HTML\HTML::html(['a' => '']));
|
||||
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::html(['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::html(['a' => ['attrs' => ['href' => 'test'], 'p' => '']]));
|
||||
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::html(['div' => ['a' => ['p' => 'foo', 'br' => 'empty']]]));
|
||||
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::html(1));
|
||||
static::assertSame('<a href="test">foo</a>', HTML\HTML::tag('a', ['href' => 'test'], content: 'foo', options: ['empty' => false]));
|
||||
static::assertSame('<br>', HTML\HTML::tag('br', attrs: null, content: null, options: ['empty' => true]));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user