Implement TextFormatter for posts

Get rid of formatting on user bios, we'll do that with JavaScript
This commit is contained in:
Toby Zerner 2015-07-22 16:03:48 +09:30
parent 526b2c48df
commit f9ef9d791b
15 changed files with 243 additions and 1973 deletions

View File

@ -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"
},

File diff suppressed because it is too large Load Diff

View File

@ -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();

View File

@ -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'),

View File

@ -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();

View File

@ -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();

View File

@ -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

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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');
}
}

View File

@ -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']]);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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.
*

View File

@ -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');