mirror of
https://github.com/flarum/framework.git
synced 2025-01-19 18:12:59 +08:00
Implement TextFormatter for posts
Get rid of formatting on user bios, we'll do that with JavaScript
This commit is contained in:
parent
526b2c48df
commit
f9ef9d791b
|
@ -11,11 +11,9 @@
|
|||
"php": ">=5.4.0",
|
||||
"illuminate/support": "5.0.*",
|
||||
"tobscure/json-api": "dev-master",
|
||||
"tobscure/permissible": "dev-master",
|
||||
"misd/linkify": "1.1.*",
|
||||
"oyejorge/less.php": "~1.5",
|
||||
"intervention/image": "^2.3.0",
|
||||
"ezyang/htmlpurifier": "^4.6.0",
|
||||
"s9e/text-formatter": "dev-release/php5.3",
|
||||
"psr/http-message": "^1.0",
|
||||
"zendframework/zend-diactoros": "^1.1",
|
||||
"nikic/fast-route": "^0.6",
|
||||
|
@ -23,9 +21,6 @@
|
|||
},
|
||||
"require-dev": {
|
||||
"fzaninotto/faker": "1.4.0",
|
||||
"codeception/codeception": "~2.0.0",
|
||||
"codeception/mockery-module": "*",
|
||||
"laracasts/testdummy": "~2.0",
|
||||
"squizlabs/php_codesniffer": "2.*",
|
||||
"phpspec/phpspec": "^2.2"
|
||||
},
|
||||
|
|
1696
framework/core/composer.lock
generated
1696
framework/core/composer.lock
generated
File diff suppressed because it is too large
Load Diff
|
@ -35,7 +35,7 @@ export default class UserBio extends Component {
|
|||
let subContent;
|
||||
|
||||
if (this.loading) {
|
||||
subContent = <p className="UserBio-placeholder">{LoadingIndicator.component()}</p>;
|
||||
subContent = <p className="UserBio-placeholder">{LoadingIndicator.component({size: 'tiny'})}</p>;
|
||||
} else {
|
||||
const bioHtml = user.bioHtml();
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ export default class User extends mixin(Model, {
|
|||
|
||||
avatarUrl: Model.attribute('avatarUrl'),
|
||||
bio: Model.attribute('bio'),
|
||||
bioHtml: Model.attribute('bioHtml'),
|
||||
bioHtml: computed('bio', bio => '<p>' + $('<div/>').text(bio).html() + '</p>'),
|
||||
preferences: Model.attribute('preferences'),
|
||||
groups: Model.hasMany('groups'),
|
||||
|
||||
|
|
|
@ -23,7 +23,6 @@ class CreatePostsTable extends Migration
|
|||
$table->integer('user_id')->unsigned()->nullable();
|
||||
$table->string('type', 100)->nullable();
|
||||
$table->text('content')->nullable();
|
||||
$table->text('content_html')->nullable();
|
||||
|
||||
$table->dateTime('edit_time')->nullable();
|
||||
$table->integer('edit_user_id')->unsigned()->nullable();
|
||||
|
|
|
@ -22,7 +22,6 @@ class CreateUsersTable extends Migration
|
|||
$table->string('password', 100);
|
||||
$table->string('locale', 10)->default('en');
|
||||
$table->text('bio')->nullable();
|
||||
$table->text('bio_html')->nullable();
|
||||
$table->string('avatar_path', 100)->nullable();
|
||||
$table->binary('preferences')->nullable();
|
||||
$table->dateTime('join_time')->nullable();
|
||||
|
|
|
@ -12,7 +12,7 @@ class UserSerializer extends UserBasicSerializer
|
|||
$canEdit = $user->can($this->actor, 'edit');
|
||||
|
||||
$attributes += [
|
||||
'bioHtml' => $user->bio_html,
|
||||
'bio' => $user->bio,
|
||||
'joinTime' => $user->join_time ? $user->join_time->toRFC3339String() : null,
|
||||
'discussionsCount' => (int) $user->discussions_count,
|
||||
'commentsCount' => (int) $user->comments_count,
|
||||
|
@ -28,7 +28,6 @@ class UserSerializer extends UserBasicSerializer
|
|||
|
||||
if ($canEdit) {
|
||||
$attributes += [
|
||||
'bio' => $user->bio,
|
||||
'isActivated' => $user->is_activated,
|
||||
'email' => $user->email,
|
||||
'isConfirmed' => $user->is_confirmed
|
||||
|
|
|
@ -1,31 +1,93 @@
|
|||
<?php namespace Flarum\Core\Formatter;
|
||||
|
||||
use Flarum\Core\Model;
|
||||
use Illuminate\Contracts\Cache\Repository;
|
||||
use s9e\TextFormatter\Configurator;
|
||||
use s9e\TextFormatter\Unparser;
|
||||
|
||||
interface Formatter
|
||||
class Formatter
|
||||
{
|
||||
/**
|
||||
* Configure the formatter manager before formatting takes place.
|
||||
*
|
||||
* @param FormatterManager $manager
|
||||
*/
|
||||
public function config(FormatterManager $manager);
|
||||
protected $cache;
|
||||
|
||||
/**
|
||||
* Format the text before purification takes place.
|
||||
*
|
||||
* @param string $text
|
||||
* @param Model|null $model The entity that owns the text.
|
||||
* @return string
|
||||
*/
|
||||
public function formatBeforePurification($text, Model $model = null);
|
||||
public function __construct(Repository $cache)
|
||||
{
|
||||
$this->cache = $cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the text after purification takes place.
|
||||
*
|
||||
* @param string $text
|
||||
* @param Model|null $model The entity that owns the text.
|
||||
* @return string
|
||||
*/
|
||||
public function formatAfterPurification($text, Model $model = null);
|
||||
protected function getConfigurator()
|
||||
{
|
||||
$configurator = new Configurator;
|
||||
$configurator->rootRules->enableAutoLineBreaks();
|
||||
|
||||
$configurator->BBCodes->addFromRepository('B');
|
||||
$configurator->BBCodes->addFromRepository('I');
|
||||
$configurator->BBCodes->addFromRepository('U');
|
||||
$configurator->BBCodes->addFromRepository('S');
|
||||
$configurator->BBCodes->addFromRepository('COLOR');
|
||||
$configurator->BBCodes->addFromRepository('URL');
|
||||
$configurator->BBCodes->addFromRepository('EMAIL');
|
||||
$configurator->BBCodes->addFromRepository('CODE');
|
||||
$configurator->BBCodes->addFromRepository('QUOTE');
|
||||
$configurator->BBCodes->addFromRepository('LIST');
|
||||
$configurator->BBCodes->addFromRepository('*');
|
||||
$configurator->BBCodes->addFromRepository('SPOILER');
|
||||
|
||||
$configurator->Autoemail;
|
||||
$configurator->Autolink;
|
||||
|
||||
$configurator->Litedown;
|
||||
|
||||
$configurator->Emoticons->add(':)', '😀');
|
||||
|
||||
return $configurator;
|
||||
}
|
||||
|
||||
protected function getComponent($key)
|
||||
{
|
||||
$cacheKey = 'flarum.formatter.' . $key;
|
||||
|
||||
return $this->cache->rememberForever($cacheKey, function () use ($key) {
|
||||
return $this->getConfigurator()->finalize()[$key];
|
||||
});
|
||||
}
|
||||
|
||||
protected function getParser()
|
||||
{
|
||||
return $this->getComponent('parser');
|
||||
}
|
||||
|
||||
protected function getRenderer()
|
||||
{
|
||||
return $this->getComponent('renderer');
|
||||
}
|
||||
|
||||
public function getJS()
|
||||
{
|
||||
$configurator = $this->getConfigurator();
|
||||
$configurator->enableJavaScript();
|
||||
$configurator->javascript->setMinifier('ClosureCompilerService');
|
||||
|
||||
return $configurator->finalize([
|
||||
'returnParser' => false,
|
||||
'returnRenderer' => false
|
||||
])['js'];
|
||||
}
|
||||
|
||||
public function parse($text)
|
||||
{
|
||||
$parser = $this->getParser();
|
||||
|
||||
return $parser->parse($text);
|
||||
}
|
||||
|
||||
public function render($xml)
|
||||
{
|
||||
$renderer = $this->getRenderer();
|
||||
|
||||
return $renderer->render($xml);
|
||||
}
|
||||
|
||||
public function unparse($xml)
|
||||
{
|
||||
return Unparser::unparse($xml);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,143 +0,0 @@
|
|||
<?php namespace Flarum\Core\Formatter;
|
||||
|
||||
use Flarum\Core\Model;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
use HTMLPurifier;
|
||||
use HTMLPurifier_Config;
|
||||
use LogicException;
|
||||
|
||||
class FormatterManager
|
||||
{
|
||||
/**
|
||||
* @var Container
|
||||
*/
|
||||
protected $container;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $formatters = [];
|
||||
|
||||
/**
|
||||
* @var HTMLPurifier_Config
|
||||
*/
|
||||
protected $htmlPurifierConfig;
|
||||
|
||||
/**
|
||||
* Create a new formatter manager instance.
|
||||
*
|
||||
* @param Container $container
|
||||
*/
|
||||
public function __construct(Container $container)
|
||||
{
|
||||
$this->container = $container;
|
||||
|
||||
// TODO: Studio does not yet merge autoload_files...
|
||||
// https://github.com/franzliedke/studio/commit/4f0f4314db4ed3e36c869a5f79b855c97bdd1be7
|
||||
require __DIR__.'/../../../vendor/ezyang/htmlpurifier/library/HTMLPurifier.composer.php';
|
||||
|
||||
$this->htmlPurifierConfig = $this->getDefaultHtmlPurifierConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the HTMLPurifier configuration object.
|
||||
*
|
||||
* @return HTMLPurifier_Config
|
||||
*/
|
||||
public function getHtmlPurifierConfig()
|
||||
{
|
||||
return $this->htmlPurifierConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new formatter.
|
||||
*
|
||||
* @param string $formatter
|
||||
*/
|
||||
public function add($formatter)
|
||||
{
|
||||
$this->formatters[] = $formatter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the given text using the collected formatters.
|
||||
*
|
||||
* @param string $text
|
||||
* @param Model|null $model The entity that owns the text.
|
||||
* @return string
|
||||
*/
|
||||
public function format($text, Model $model = null)
|
||||
{
|
||||
$formatters = $this->getFormatters();
|
||||
|
||||
foreach ($formatters as $formatter) {
|
||||
$formatter->config($this);
|
||||
}
|
||||
|
||||
foreach ($formatters as $formatter) {
|
||||
$text = $formatter->formatBeforePurification($text, $model);
|
||||
}
|
||||
|
||||
$text = $this->purify($text);
|
||||
|
||||
foreach ($formatters as $formatter) {
|
||||
$text = $formatter->formatAfterPurification($text, $model);
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate the collected formatters.
|
||||
*
|
||||
* @return Formatter[]
|
||||
*/
|
||||
protected function getFormatters()
|
||||
{
|
||||
$formatters = [];
|
||||
|
||||
foreach ($this->formatters as $formatter) {
|
||||
$formatter = $this->container->make($formatter);
|
||||
|
||||
if (! $formatter instanceof Formatter) {
|
||||
throw new LogicException('Formatter ' . get_class($formatter)
|
||||
. ' does not implement ' . Formatter::class);
|
||||
}
|
||||
|
||||
$formatters[] = $formatter;
|
||||
}
|
||||
|
||||
return $formatters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Purify the given text, making sure it is safe to be displayed in web
|
||||
* browsers.
|
||||
*
|
||||
* @param string $text
|
||||
* @return string
|
||||
*/
|
||||
protected function purify($text)
|
||||
{
|
||||
$purifier = new HTMLPurifier($this->htmlPurifierConfig);
|
||||
|
||||
return $purifier->purify($text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default HTMLPurifier config settings.
|
||||
*
|
||||
* @return HTMLPurifier_Config
|
||||
*/
|
||||
protected function getDefaultHtmlPurifierConfig()
|
||||
{
|
||||
$config = HTMLPurifier_Config::createDefault();
|
||||
$config->set('Core.Encoding', 'UTF-8');
|
||||
$config->set('Core.EscapeInvalidTags', true);
|
||||
$config->set('HTML.Doctype', 'HTML 4.01 Strict');
|
||||
$config->set('HTML.Allowed', 'p,em,strong,a[href|title],ul,ol,li,code,pre,blockquote,h1,h2,h3,h4,h5,h6,br,hr,img[src|alt]');
|
||||
$config->set('HTML.Nofollow', true);
|
||||
|
||||
return $config;
|
||||
}
|
||||
}
|
|
@ -21,6 +21,6 @@ class FormatterServiceProvider extends ServiceProvider
|
|||
*/
|
||||
public function register()
|
||||
{
|
||||
$this->app->singleton('flarum.formatter', 'Flarum\Core\Formatter\FormatterManager');
|
||||
$this->app->singleton('flarum.formatter', 'Flarum\Core\Formatter\Formatter');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
<?php namespace Flarum\Core\Formatter;
|
||||
|
||||
use Flarum\Core\Model;
|
||||
use Misd\Linkify\Linkify;
|
||||
|
||||
class LinkifyFormatter extends TextFormatter
|
||||
{
|
||||
/**
|
||||
* @var Linkify
|
||||
*/
|
||||
protected $linkify;
|
||||
|
||||
/**
|
||||
* @param Linkify $linkify
|
||||
*/
|
||||
public function __construct(Linkify $linkify)
|
||||
{
|
||||
$this->linkify = $linkify;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function formatTextBeforePurification($text, Model $post = null)
|
||||
{
|
||||
return $this->linkify->process($text, ['attr' => ['target' => '_blank']]);
|
||||
}
|
||||
}
|
|
@ -1,120 +0,0 @@
|
|||
<?php namespace Flarum\Core\Formatter;
|
||||
|
||||
use Flarum\Core\Model;
|
||||
|
||||
/**
|
||||
* A formatter which formats a block of HTML, while leaving the contents
|
||||
* of specific tags like <code> and <pre> untouched.
|
||||
*/
|
||||
abstract class TextFormatter implements Formatter
|
||||
{
|
||||
/**
|
||||
* A list of tags to ignore when applying formatting.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $ignoreTags = ['code', 'pre'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function config(FormatterManager $manager)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function formatBeforePurification($text, Model $model = null)
|
||||
{
|
||||
return $this->formatAroundIgnoredTags($text, function ($text) use ($model) {
|
||||
return $this->formatTextBeforePurification($text, $model);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function formatAfterPurification($text, Model $model = null)
|
||||
{
|
||||
return $this->formatAroundIgnoredTags($text, function ($text) use ($model) {
|
||||
return $this->formatTextAfterPurification($text, $model);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Format non-ignored text before purification has taken place.
|
||||
*
|
||||
* @param string $text
|
||||
* @param Model $model
|
||||
* @return mixed
|
||||
*/
|
||||
protected function formatTextBeforePurification($text, Model $model = null)
|
||||
{
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format non-ignored text after purification has taken place.
|
||||
*
|
||||
* @param string $text
|
||||
* @param Model $model
|
||||
* @return string
|
||||
*/
|
||||
protected function formatTextAfterPurification($text, Model $model = null)
|
||||
{
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a callback on parts of the provided text that aren't within the list
|
||||
* of ignored tags.
|
||||
*
|
||||
* @param string $text
|
||||
* @param callable $callback
|
||||
* @return string
|
||||
*/
|
||||
protected function formatAroundIgnoredTags($text, callable $callback)
|
||||
{
|
||||
return $this->formatAroundTags($text, $this->ignoreTags, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a callback on parts of the provided text that aren't within the
|
||||
* given list of tags.
|
||||
*
|
||||
* @param string $text
|
||||
* @param array $tags
|
||||
* @param callable $callback
|
||||
* @return string
|
||||
*/
|
||||
protected function formatAroundTags($text, array $tags, callable $callback)
|
||||
{
|
||||
$chunks = preg_split('/(<.+?>)/is', $text, 0, PREG_SPLIT_DELIM_CAPTURE);
|
||||
$openTag = null;
|
||||
|
||||
for ($i = 0; $i < count($chunks); $i++) {
|
||||
if ($i % 2 === 0) { // even numbers are text
|
||||
// Only process this chunk if there are no unclosed $ignoreTags
|
||||
if (null === $openTag) {
|
||||
$chunks[$i] = $callback($chunks[$i]);
|
||||
}
|
||||
} else { // odd numbers are tags
|
||||
// Only process this tag if there are no unclosed $ignoreTags
|
||||
if (null === $openTag) {
|
||||
// Check whether this tag is contained in $ignoreTags and is not self-closing
|
||||
if (preg_match("`<(" . implode('|', $tags) . ").*(?<!/)>$`is", $chunks[$i], $matches)) {
|
||||
$openTag = $matches[1];
|
||||
}
|
||||
} else {
|
||||
// Otherwise, check whether this is the closing tag for $openTag.
|
||||
if (preg_match('`</\s*' . $openTag . '>`i', $chunks[$i], $matches)) {
|
||||
$openTag = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return implode($chunks);
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
<?php namespace Flarum\Core\Posts;
|
||||
|
||||
use DomainException;
|
||||
use Flarum\Core\Formatter\FormatterManager;
|
||||
use Flarum\Core\Formatter\Formatter;
|
||||
use Flarum\Events\PostWasPosted;
|
||||
use Flarum\Events\PostWasRevised;
|
||||
use Flarum\Events\PostWasHidden;
|
||||
|
@ -21,7 +21,7 @@ class CommentPost extends Post
|
|||
/**
|
||||
* The text formatter instance.
|
||||
*
|
||||
* @var FormatterManager
|
||||
* @var Formatter
|
||||
*/
|
||||
protected static $formatter;
|
||||
|
||||
|
@ -59,7 +59,6 @@ class CommentPost extends Post
|
|||
{
|
||||
if ($this->content !== $content) {
|
||||
$this->content = $content;
|
||||
$this->content_html = static::formatContent($this);
|
||||
|
||||
$this->edit_time = time();
|
||||
$this->edit_user_id = $actor->id;
|
||||
|
@ -114,25 +113,40 @@ class CommentPost extends Post
|
|||
}
|
||||
|
||||
/**
|
||||
* Get the content formatted as HTML.
|
||||
* Parse the content before it is saved to the database.
|
||||
*
|
||||
* @param string $value
|
||||
*/
|
||||
public function getContentAttribute($value)
|
||||
{
|
||||
return static::$formatter->unparse($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the content before it is saved to the database.
|
||||
*
|
||||
* @param string $value
|
||||
*/
|
||||
public function setContentAttribute($value)
|
||||
{
|
||||
$this->attributes['content'] = static::$formatter->parse($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content rendered as HTML.
|
||||
*
|
||||
* @param string $value
|
||||
* @return string
|
||||
*/
|
||||
public function getContentHtmlAttribute($value)
|
||||
{
|
||||
if (! $value) {
|
||||
$this->content_html = $value = static::formatContent($this);
|
||||
$this->save();
|
||||
}
|
||||
|
||||
return $value;
|
||||
return static::$formatter->render($this->attributes['content']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get text formatter instance.
|
||||
* Get the text formatter instance.
|
||||
*
|
||||
* @return FormatterManager
|
||||
* @return Formatter
|
||||
*/
|
||||
public static function getFormatter()
|
||||
{
|
||||
|
@ -140,23 +154,12 @@ class CommentPost extends Post
|
|||
}
|
||||
|
||||
/**
|
||||
* Set text formatter instance.
|
||||
* Set the text formatter instance.
|
||||
*
|
||||
* @param FormatterManager $formatter
|
||||
* @param Formatter $formatter
|
||||
*/
|
||||
public static function setFormatter(FormatterManager $formatter)
|
||||
public static function setFormatter(Formatter $formatter)
|
||||
{
|
||||
static::$formatter = $formatter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a string of post content using the set formatter.
|
||||
*
|
||||
* @param CommentPost $post
|
||||
* @return string
|
||||
*/
|
||||
protected static function formatContent(CommentPost $post)
|
||||
{
|
||||
return static::$formatter->format($post->content, $post);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -60,13 +60,6 @@ class User extends Model
|
|||
'notification_read_time'
|
||||
];
|
||||
|
||||
/**
|
||||
* The text formatter instance.
|
||||
*
|
||||
* @var FormatterManager
|
||||
*/
|
||||
protected static $formatter;
|
||||
|
||||
/**
|
||||
* The hasher with which to hash passwords.
|
||||
*
|
||||
|
@ -215,29 +208,12 @@ class User extends Model
|
|||
public function changeBio($bio)
|
||||
{
|
||||
$this->bio = $bio;
|
||||
$this->bio_html = null;
|
||||
|
||||
$this->raise(new UserBioWasChanged($this));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's bio formatted as HTML.
|
||||
*
|
||||
* @param string $value
|
||||
* @return string
|
||||
*/
|
||||
public function getBioHtmlAttribute($value)
|
||||
{
|
||||
if ($value === null) {
|
||||
$this->bio_html = $value = static::formatBio($this);
|
||||
$this->save();
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark all discussions as read.
|
||||
*
|
||||
|
@ -565,27 +541,6 @@ class User extends Model
|
|||
static::$hasher = $hasher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the text formatter instance.
|
||||
*
|
||||
* @param FormatterManager $formatter
|
||||
*/
|
||||
public static function setFormatter(FormatterManager $formatter)
|
||||
{
|
||||
static::$formatter = $formatter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the formatted content of a user's bio.
|
||||
*
|
||||
* @param User $user
|
||||
* @return string
|
||||
*/
|
||||
protected static function formatBio(User $user)
|
||||
{
|
||||
return static::$formatter->format($user->bio, $user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a preference with a transformer and a default value.
|
||||
*
|
||||
|
|
|
@ -18,7 +18,6 @@ class UsersServiceProvider extends ServiceProvider
|
|||
public function boot()
|
||||
{
|
||||
User::setHasher($this->app->make('hash'));
|
||||
User::setFormatter($this->app->make('flarum.formatter'));
|
||||
User::setValidator($this->app->make('validator'));
|
||||
|
||||
$events = $this->app->make('events');
|
||||
|
|
Loading…
Reference in New Issue
Block a user