Commit ddf6001e authored by Emma's avatar Emma 😻

Squashed commit of the following:

commit 1ea56de09c889ef6ad516d446ef14b6190ce23e5
Author: Emma <emma1312@protonmail.ch>
Date:   Sat Aug 19 19:25:42 2017 +0200

    workaround for lack of uuid support in symfony

commit e2bae7f6242f286f1248b592227b2541d07197f7
Author: Emma <emma1312@protonmail.ch>
Date:   Sat Aug 19 18:53:29 2017 +0200

    document workarounds

commit 9d85c1211ea7c9f6e1934832ec466e4175507454
Author: Emma <emma1312@protonmail.ch>
Date:   Sat Aug 19 18:31:19 2017 +0200

    uuid regex stuff

commit 6a82e4c21256b24d038598f36e364a3c83012439
Author: Emma <emma1312@protonmail.ch>
Date:   Sat Aug 19 18:28:34 2017 +0200

    fix unique validation of theme names

commit eae7becca8d03fe9f683aae6ba19b54f8b62e31d
Author: Emma <emma1312@protonmail.ch>
Date:   Sat Aug 19 00:11:36 2017 +0200

    preliminary, broken stuff
parent 49e84b23
......@@ -9,6 +9,7 @@ imports:
parameters:
days_to_keep_logs: 7
user_forum_creation_interval: 1 day
uuid_regex: '[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}'
wiki_page_regex: '[A-Za-z][A-Za-z0-9_-]*(/[A-Za-z][A-Za-z0-9_-]*)*'
locale: en
......
Workarounds
===
This file describes ugly hacks and workarounds that have been deployed to work
around limitations in the libraries and frameworks we use. The underlying raison
d'etre for these issues should regularly be reinvestigated and the workarounds
removed when better solutions exist.
## `Raddit\AppBundle\Validator\Constraints\UniqueTheme`
Symfony's `UniqueEntity` constraint does not support DTOs, despite the existence
of an `entityClass` field. `UniqueTheme` is a special constraint that works
around this problem for the `Theme` entity and its `ThemeData` DTO.
The `ThemeData::$entityId` property is kept around for the validator's sake.
If/when Symfony gains a UniqueEntity validator that can deal with DTOs, the
aforementioned constraint, validator and DTO property can be removed.
See <https://github.com/symfony/symfony/issues/22592>.
## `UuidAwareORMQueryBuilderLoader`/`UuidAwareEntityType`
These classes are temporary workarounds for lack of UUID support in Symfony's
`ORMQueryBuilderLoader`.
See <https://github.com/symfony/symfony/issues/23808>
......@@ -3,24 +3,25 @@
namespace Raddit\AppBundle\Controller;
use Doctrine\ORM\EntityManager;
use Raddit\AppBundle\Entity\Stylesheet;
use Raddit\AppBundle\Form\StylesheetType;
use Raddit\AppBundle\Repository\StylesheetRepository;
use Raddit\AppBundle\Entity\Theme;
use Raddit\AppBundle\Form\Model\ThemeData;
use Raddit\AppBundle\Form\ThemeType;
use Raddit\AppBundle\Repository\ThemeRepository;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class StylesheetController extends Controller {
class ThemeController extends Controller {
/**
* @param StylesheetRepository $stylesheetRepository
* @param int $page
* @param ThemeRepository $themeRepository
* @param int $page
*
* @return Response
*/
public function listAction(StylesheetRepository $stylesheetRepository, int $page) {
return $this->render('@RadditApp/stylesheet_list.html.twig', [
'stylesheets' => $stylesheetRepository->findAllPaginated($page),
public function listAction(ThemeRepository $themeRepository, int $page) {
return $this->render('@RadditApp/theme_list.html.twig', [
'themes' => $themeRepository->findAllPaginated($page),
]);
}
......@@ -33,78 +34,79 @@ class StylesheetController extends Controller {
* @return Response
*/
public function createAction(Request $request, EntityManager $em) {
$stylesheet = new Stylesheet();
$data = new ThemeData($this->getUser());
$form = $this->createForm(StylesheetType::class, $stylesheet);
$form = $this->createForm(ThemeType::class, $data);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$stylesheet->setUser($this->getUser());
$theme = $data->toTheme();
$em->persist($stylesheet);
$em->persist($theme);
$em->flush();
$this->addFlash('success', 'flash.stylesheet_created');
$this->addFlash('success', 'flash.theme_created');
return $this->redirectToRoute('raddit_app_edit_stylesheet', [
'id' => $stylesheet->getId(),
return $this->redirectToRoute('raddit_app_edit_theme', [
'id' => $theme->getId(),
]);
}
return $this->render('@RadditApp/stylesheet_create.html.twig', [
return $this->render('@RadditApp/theme_create.html.twig', [
'form' => $form->createView(),
]);
}
/**
* @Security("is_granted('edit', stylesheet)")
* @Security("is_granted('edit', theme)")
*
* @param Request $request
* @param EntityManager $em
* @param Stylesheet $stylesheet
* @param Theme $theme
*
* @return Response
*/
public function editAction(Request $request, EntityManager $em, Stylesheet $stylesheet) {
$form = $this->createForm(StylesheetType::class, $stylesheet);
public function editAction(Request $request, EntityManager $em, Theme $theme) {
$data = ThemeData::createFromTheme($theme);
$form = $this->createForm(ThemeType::class, $data);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$stylesheet->setTimestamp(new \DateTime('@'.time()));
$data->updateTheme($theme);
$em->flush();
$this->addFlash('success', 'flash.stylesheet_updated');
$this->addFlash('success', 'flash.theme_updated');
return $this->redirectToRoute('raddit_app_edit_stylesheet', [
'id' => $stylesheet->getId(),
return $this->redirectToRoute('raddit_app_edit_theme', [
'id' => $theme->getId(),
]);
}
return $this->render('@RadditApp/stylesheet_edit.html.twig', [
return $this->render('@RadditApp/theme_edit.html.twig', [
'form' => $form->createView(),
'stylesheet' => $stylesheet,
'theme' => $theme,
]);
}
/**
* Deliver the raw stylesheet.
* Deliver a raw stylesheet.
*
* @param Request $request
* @param Stylesheet $stylesheet
* @param Request $request
* @param Theme $theme
* @param string $field
*
* @return Response
*/
public function rawAction(Request $request, Stylesheet $stylesheet) {
public function stylesheetAction(Request $request, Theme $theme, string $field) {
$response = new Response();
$response->setPublic();
$response->setLastModified($stylesheet->getTimestamp());
$response->setLastModified($theme->getLastModified());
if ($response->isNotModified($request)) {
return $response;
}
$response->setContent($stylesheet->getCss());
$response->setContent($theme->{'get'.ucfirst($field).'Css'}());
return $response;
}
......
<?php
namespace Raddit\AppBundle\DoctrineMigrations;
use Doctrine\DBAL\Migrations\AbstractMigration;
use Doctrine\DBAL\Schema\Schema;
class Version20170818135017 extends AbstractMigration {
/**
* @param Schema $schema
*/
public function up(Schema $schema) {
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.');
$this->addSql('CREATE TABLE themes (id UUID NOT NULL, author_id BIGINT NOT NULL, name TEXT NOT NULL, common_css TEXT DEFAULT NULL, day_css TEXT DEFAULT NULL, night_css TEXT DEFAULT NULL, append_to_default_style BOOLEAN NOT NULL, last_modified TIMESTAMP(0) WITH TIME ZONE NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_154232DEF675F31B ON themes (author_id)');
$this->addSql('CREATE UNIQUE INDEX themes_author_name_idx ON themes (author_id, name)');
$this->addSql('COMMENT ON COLUMN themes.id IS \'(DC2Type:uuid)\'');
$this->addSql('ALTER TABLE themes ADD CONSTRAINT FK_154232DEF675F31B FOREIGN KEY (author_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE forums ADD theme_id UUID DEFAULT NULL');
$this->addSql('COMMENT ON COLUMN forums.theme_id IS \'(DC2Type:uuid)\'');
$this->addSql('CREATE INDEX IDX_FE5E5AB859027487 ON forums (theme_id)');
$this->addSql('ALTER TABLE forums ADD CONSTRAINT FK_FE5E5AB859027487 FOREIGN KEY (theme_id) REFERENCES themes (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('INSERT INTO themes (id, name, author_id, day_css, append_to_default_style, last_modified) SELECT MD5(RANDOM()::TEXT)::UUID, name, user_id, css, append_to_default_style, timestamp FROM stylesheets WHERE NOT night_friendly');
$this->addSql('INSERT INTO themes (id, name, author_id, night_css, append_to_default_style, last_modified) SELECT MD5(RANDOM()::TEXT)::UUID, name, user_id, css, append_to_default_style, timestamp FROM stylesheets WHERE night_friendly');
$this->addSql('WITH cte AS (SELECT t.id AS t_id, s.id AS s_id FROM themes t JOIN stylesheets s ON (t.name = s.name AND t.author_id = s.user_id)) UPDATE forums SET theme_id = cte.t_id FROM cte WHERE stylesheet_id = cte.s_id');
$this->addSql('DROP INDEX idx_fe5e5ab848a9533f');
$this->addSql('DROP INDEX idx_fe5e5ab8997679ec');
$this->addSql('ALTER TABLE forums DROP CONSTRAINT fk_fe5e5ab848a9533f');
$this->addSql('ALTER TABLE forums DROP CONSTRAINT fk_fe5e5ab8997679ec');
$this->addSql('ALTER TABLE forums DROP night_stylesheet_id');
$this->addSql('ALTER TABLE forums DROP stylesheet_id');
$this->addSql('DROP SEQUENCE stylesheets_id_seq CASCADE');
$this->addSql('DROP TABLE stylesheets');
}
/**
* @param Schema $schema
*/
public function down(Schema $schema) {
$this->throwIrreversibleMigrationException();
}
}
......@@ -124,18 +124,11 @@ class Forum {
private $category;
/**
* @ORM\ManyToOne(targetEntity="Stylesheet")
* @ORM\ManyToOne(targetEntity="Theme")
*
* @var Stylesheet|null
* @var Theme|null
*/
private $stylesheet;
/**
* @ORM\ManyToOne(targetEntity="Stylesheet")
*
* @var Stylesheet|null
*/
private $nightStylesheet;
private $theme;
public function __construct() {
$this->created = new \DateTime('@'.time());
......@@ -330,30 +323,16 @@ class Forum {
}
/**
* @return Stylesheet|null
*/
public function getStylesheet() {
return $this->stylesheet;
}
/**
* @param Stylesheet|null $stylesheet
*/
public function setStylesheet($stylesheet) {
$this->stylesheet = $stylesheet;
}
/**
* @return Stylesheet|null
* @return Theme|null
*/
public function getNightStylesheet() {
return $this->nightStylesheet;
public function getTheme() {
return $this->theme;
}
/**
* @param Stylesheet|null $nightStylesheet
* @param Theme|null $theme
*/
public function setNightStylesheet($nightStylesheet) {
$this->nightStylesheet = $nightStylesheet;
public function setTheme($theme) {
$this->theme = $theme;
}
}
<?php
namespace Raddit\AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Raddit\AppBundle\Validator\Constraints\Css;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert;
/**
* A custom stylesheet. Can be applied to forums, user pages, etc.
*
* @ORM\Entity(repositoryClass="Raddit\AppBundle\Repository\StylesheetRepository")
* @ORM\Table(name="stylesheets", uniqueConstraints={
* @ORM\UniqueConstraint(name="stylesheets_user_name_idx", columns={"user_id", "name"})
* })
*
* @UniqueEntity("name")
*/
class Stylesheet {
/**
* @ORM\Column(type="bigint")
* @ORM\GeneratedValue()
* @ORM\Id()
*
* @var int|null
*/
private $id;
/**
* @ORM\Column(type="text")
*
* @Assert\NotBlank()
* @Assert\Length(max=50)
*
* @var string|null
*/
private $name;
/**
* @ORM\Column(type="text")
*
* @Assert\NotBlank()
* @Assert\Length(max=100000)
* @Css()
*
* @var string|null
*/
private $css;
/**
* @ORM\Column(type="boolean")
*
* @var bool
*/
private $appendToDefaultStyle = true;
/**
* @ORM\Column(type="boolean")
*
* @var bool
*/
private $nightFriendly = false;
/**
* @ORM\ManyToOne(targetEntity="User")
*
* @var User|null
*/
private $user;
/**
* @ORM\Column(type="datetimetz")
*
* @var \DateTime
*/
private $timestamp;
public function __construct() {
$this->timestamp = new \DateTime('@'.time());
}
/**
* @return int|null
*/
public function getId() {
return $this->id;
}
/**
* @return null|string
*/
public function getName() {
return $this->name;
}
/**
* @param null|string $name
*/
public function setName($name) {
$this->name = $name;
}
/**
* @return null|string
*/
public function getCss() {
return $this->css;
}
/**
* @param null|string $css
*/
public function setCss($css) {
$this->css = $css;
}
/**
* @return bool
*/
public function isAppendToDefaultStyle(): bool {
return $this->appendToDefaultStyle;
}
/**
* @param bool $appendToDefaultStyle
*/
public function setAppendToDefaultStyle(bool $appendToDefaultStyle) {
$this->appendToDefaultStyle = $appendToDefaultStyle;
}
/**
* @return bool
*/
public function isNightFriendly(): bool {
return $this->nightFriendly;
}
/**
* @param bool $nightFriendly
*/
public function setNightFriendly(bool $nightFriendly) {
$this->nightFriendly = $nightFriendly;
}
/**
* @return null|User
*/
public function getUser() {
return $this->user;
}
/**
* @param null|User $user
*/
public function setUser($user) {
$this->user = $user;
}
/**
* @return \DateTime
*/
public function getTimestamp(): \DateTime {
return $this->timestamp;
}
/**
* @param \DateTime $timestamp
*/
public function setTimestamp(\DateTime $timestamp) {
$this->timestamp = $timestamp;
}
}
<?php
namespace Raddit\AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Ramsey\Uuid\Uuid;
/**
* @ORM\Entity(repositoryClass="Raddit\AppBundle\Repository\ThemeRepository")
* @ORM\Table(name="themes", uniqueConstraints={
* @ORM\UniqueConstraint(name="themes_author_name_idx", columns={"author_id", "name"})
* })
*/
class Theme {
/**
* @ORM\Column(type="uuid")
* @ORM\Id()
*
* @var Uuid
*/
private $id;
/**
* @ORM\Column(type="text")
*
* @var string
*/
private $name;
/**
* @ORM\Column(type="text", nullable=true)
*
* @var string|null
*/
private $commonCss;
/**
* @ORM\Column(type="text", nullable=true)
*
* @var string|null
*/
private $dayCss;
/**
* @ORM\Column(type="text", nullable=true)
*
* @var string|null
*/
private $nightCss;
/**
* @ORM\Column(type="boolean")
*
* @var bool
*/
private $appendToDefaultStyle = true;
/**
* @ORM\JoinColumn(nullable=false)
* @ORM\ManyToOne(targetEntity="User")
*
* @var User
*/
private $author;
/**
* @ORM\Column(type="datetimetz")
*
* @var \DateTime
*/
private $lastModified;
/**
* @param string $name
* @param null|string $commonCss
* @param null|string $dayCss
* @param null|string $nightCss
* @param bool $appendToDefaultStyle
* @param User $author
*/
public function __construct(
string $name,
$commonCss,
$dayCss,
$nightCss,
bool $appendToDefaultStyle,
User $author
) {
$this->id = Uuid::uuid4();
$this->name = $name;
$this->setCss($commonCss, $dayCss, $nightCss);
$this->appendToDefaultStyle = $appendToDefaultStyle;
$this->author = $author;
$this->updateLastModified();
}
public function getId(): Uuid {
return $this->id;
}
public function getName(): string {
return $this->name;
}
public function setName(string $name) {
$this->name = $name;
}
/**
* @return string|null
*/
public function getCommonCss() {
return $this->commonCss;
}
/**
* @return string|null
*/
public function getDayCss() {
return $this->dayCss;
}
/**
* @return string|null
*/
public function getNightCss() {
return $this->nightCss;
}
/**
* @param string|null $commonCss
* @param string|null $dayCss
* @param string|null $nightCss
*/
public function setCss($commonCss, $dayCss, $nightCss) {
if (!$commonCss && !$dayCss && !$nightCss) {
throw new \InvalidArgumentException('At least one CSS field must be filled.');
}
$this->commonCss = $commonCss;
$this->dayCss = $dayCss;
$this->nightCss = $nightCss;
}
public function appendToDefaultStyle(): bool {
return $this->appendToDefaultStyle;
}
public function setAppendToDefaultStyle(bool $appendToDefaultStyle) {
$this->appendToDefaultStyle = $appendToDefaultStyle;
}
public function getAuthor(): User {
return $this->author;
}
public function getLastModified(): \DateTime {
return $this->lastModified;
}
public function updateLastModified() {
$this->lastModified = new \DateTime('@'.time());
}
}
<?php
/*
* This file was seized from Symfony and modified to fix an issue that has not
* yet been resolved upstream. Its original copyright notice follows:
*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Raddit\AppBundle\Form\ChoiceList;
use Doctrine\ORM\QueryBuilder;
use Doctrine\DBAL\Connection;
use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityLoaderInterface;
/**
* Loads entities using a {@link QueryBuilder} instance.
*
* @author Benjamin Eberlei <kontakt@beberlei.de>
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @todo remove this hack
*/
class UuidAwareORMQueryBuilderLoader implements EntityLoaderInterface
{
/**
* Contains the query builder that builds the query for fetching the
* entities.
*
* This property should only be accessed through queryBuilder.
*
* @var QueryBuilder
*/
private $queryBuilder;
/**
* Construct an ORM Query Builder Loader.
*
* @param QueryBuilder $queryBuilder The query builder for creating the query builder
*/
public function __construct(QueryBuilder $queryBuilder)
{
$this->queryBuilder = $queryBuilder;
}
/**
* {@inheritdoc}
*/
public function getEntities()
{
return $this->queryBuilder->getQuery()->execute();
}
/**
* {@inheritdoc}
*/
public function getEntitiesByIds($identifier, array $values)
{
$qb = clone $this->queryBuilder;
$alias = current($qb->getRootAliases());
$parameter = 'ORMQueryBuilderLoader_getEntitiesByIds_'.$identifier;
$parameter = str_replace('.', '_', $parameter);
$where = $qb->expr()->in($alias.'.'.$identifier, ':'.$parameter);
// Guess type
$entity = current($qb->getRootEntities());
$metadata = $qb->getEntityManager()->getClassMetadata($entity);
if (in_array($metadata->getTypeOfField($identifier), ['integer', 'bigint', 'smallint'])) {
$parameterType = Connection::PARAM_INT_ARRAY;
// Filter out non-integer values (e.g. ""). If we don't, some
// databases such as PostgreSQL fail.
$values = array_values(array_filter($values, function ($v) {
return (string) $v === (string) (int) $v || ctype_digit($v);
}));
} elseif (in_array($metadata->getTypeOfField($identifier), ['uuid', 'guid'], true)) {
$parameterType = Connection::PARAM_STR_ARRAY;
// Like above, but we just filter out empty strings.
$values = array_values(array_filter($values, function ($v) {
return (string) $v !== '';
}));
} else {
$parameterType = Connection::PARAM_STR_ARRAY;
}
if (!$values) {
return [];
}