Commit 29070e6f authored by Emma's avatar Emma 😻

custom stylesheets

parent 28cb8251
Pipeline #9823997 passed with stage
in 4 minutes and 39 seconds
......@@ -5,6 +5,7 @@
* Added CAPTCHA to registration form.
* Added a command that prunes IP addresses on many entities, optionally after
they reach a provided age.
* Added custom CSS.
* Added Dutch, Finnish, German, Greek and Portuguese (Brazilian) translations.
* Added explanation of the various user form fields.
* Added forum categories.
......
......@@ -19,4 +19,10 @@
&:active {
color: @button-active-text-color;
}
&--inline {
font-size: 100%;
display: inline;
padding: 0.25rem 0.5rem;
}
}
......@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"content-hash": "f74201bd3ac3e40dc9f06f98873f4385",
"content-hash": "2ff0468dfdb9e91d6afbe8681955388d",
"packages": [
{
"name": "composer/ca-bundle",
......@@ -2376,6 +2376,50 @@
],
"time": "2017-01-02T13:31:39+00:00"
},
{
"name": "sabberworm/php-css-parser",
"version": "8.1.0",
"source": {
"type": "git",
"url": "https://github.com/sabberworm/PHP-CSS-Parser.git",
"reference": "850cbbcbe7fbb155387a151ea562897a67e242ef"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sabberworm/PHP-CSS-Parser/zipball/850cbbcbe7fbb155387a151ea562897a67e242ef",
"reference": "850cbbcbe7fbb155387a151ea562897a67e242ef",
"shasum": ""
},
"require": {
"php": ">=5.3.2"
},
"require-dev": {
"phpunit/phpunit": "*"
},
"type": "library",
"autoload": {
"psr-0": {
"Sabberworm\\CSS": "lib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Raphael Schweikert"
}
],
"description": "Parser for CSS Files written in PHP",
"homepage": "http://www.sabberworm.com/blog/2010/6/10/php-css-parser",
"keywords": [
"css",
"parser",
"stylesheet"
],
"time": "2016-07-19T19:14:21+00:00"
},
{
"name": "scheb/two-factor-bundle",
"version": "v2.9.0",
......
<?php
namespace Raddit\AppBundle\Controller;
use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\EntityManager;
use Pagerfanta\Adapter\DoctrineSelectableAdapter;
use Pagerfanta\Pagerfanta;
use Raddit\AppBundle\Entity\Stylesheet;
use Raddit\AppBundle\Form\StylesheetType;
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 {
/**
* @param EntityManager $em
* @param int $page
*
* @return Response
*/
public function listAction(EntityManager $em, int $page) {
$repository = $em->getRepository(Stylesheet::class);
$criteria = Criteria::create()->orderBy(['timestamp' => 'DESC']);
$stylesheets = new Pagerfanta(new DoctrineSelectableAdapter($repository, $criteria));
$stylesheets->setMaxPerPage(25);
$stylesheets->setCurrentPage($page);
return $this->render('@RadditApp/stylesheet_list.html.twig', [
'stylesheets' => $stylesheets,
]);
}
/**
* @Security("is_granted('ROLE_USER')")
*
* @param Request $request
* @param EntityManager $em
*
* @return Response
*/
public function createAction(Request $request, EntityManager $em) {
$stylesheet = new Stylesheet();
$form = $this->createForm(StylesheetType::class, $stylesheet);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$stylesheet->setUser($this->getUser());
$em->persist($stylesheet);
$em->flush();
$this->addFlash('success', 'stylesheets.created_notice');
return $this->redirectToRoute('raddit_app_edit_stylesheet', [
'id' => $stylesheet->getId(),
]);
}
return $this->render('@RadditApp/stylesheet_create.html.twig', [
'form' => $form->createView(),
]);
}
/**
* @Security("is_granted('edit', stylesheet)")
*
* @param Request $request
* @param EntityManager $em
* @param Stylesheet $stylesheet
*
* @return Response
*/
public function editAction(Request $request, EntityManager $em, Stylesheet $stylesheet) {
$form = $this->createForm(StylesheetType::class, $stylesheet);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$stylesheet->setTimestamp(new \DateTime('@'.time()));
$em->flush();
$this->addFlash('success', 'stylesheets.edited_notice');
return $this->redirectToRoute('raddit_app_edit_stylesheet', [
'id' => $stylesheet->getId(),
]);
}
return $this->render('@RadditApp/stylesheet_edit.html.twig', [
'form' => $form->createView(),
'stylesheet' => $stylesheet,
]);
}
/**
* Deliver the raw stylesheet.
*
* @param Request $request
* @param Stylesheet $stylesheet
*
* @return Response
*/
public function rawAction(Request $request, Stylesheet $stylesheet) {
$response = new Response();
$response->setPublic();
$response->setLastModified($stylesheet->getTimestamp());
if ($response->isNotModified($request)) {
return $response;
}
$response->setContent($stylesheet->getCss());
return $response;
}
}
<?php
namespace Raddit\AppBundle\DoctrineMigrations;
use Doctrine\DBAL\Migrations\AbstractMigration;
use Doctrine\DBAL\Schema\Schema;
class Version20170712232516 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 SEQUENCE stylesheets_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
$this->addSql('CREATE TABLE stylesheets (id BIGINT NOT NULL, user_id BIGINT DEFAULT NULL, name TEXT NOT NULL, css TEXT NOT NULL, append_to_default_style BOOLEAN NOT NULL, night_friendly BOOLEAN NOT NULL, timestamp TIMESTAMP(0) WITH TIME ZONE NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_1560D2A9A76ED395 ON stylesheets (user_id)');
$this->addSql('CREATE UNIQUE INDEX stylesheets_user_name_idx ON stylesheets (user_id, name)');
$this->addSql('ALTER TABLE stylesheets ADD CONSTRAINT FK_1560D2A9A76ED395 FOREIGN KEY (user_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE forums ADD stylesheet_id BIGINT DEFAULT NULL');
$this->addSql('ALTER TABLE forums ADD night_stylesheet_id BIGINT DEFAULT NULL');
$this->addSql('ALTER TABLE forums ADD CONSTRAINT FK_FE5E5AB8997679EC FOREIGN KEY (stylesheet_id) REFERENCES stylesheets (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE forums ADD CONSTRAINT FK_FE5E5AB848A9533F FOREIGN KEY (night_stylesheet_id) REFERENCES stylesheets (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('CREATE INDEX IDX_FE5E5AB8997679EC ON forums (stylesheet_id)');
$this->addSql('CREATE INDEX IDX_FE5E5AB848A9533F ON forums (night_stylesheet_id)');
$this->addSql('ALTER TABLE users ADD show_custom_stylesheets BOOLEAN DEFAULT TRUE NOT NULL');
}
/**
* @param Schema $schema
*/
public function down(Schema $schema) {
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.');
$this->addSql('ALTER TABLE forums DROP CONSTRAINT FK_FE5E5AB8997679EC');
$this->addSql('ALTER TABLE forums DROP CONSTRAINT FK_FE5E5AB848A9533F');
$this->addSql('DROP SEQUENCE stylesheets_id_seq CASCADE');
$this->addSql('DROP TABLE stylesheets');
$this->addSql('ALTER TABLE users DROP show_custom_stylesheets');
$this->addSql('DROP INDEX IDX_FE5E5AB8997679EC');
$this->addSql('DROP INDEX IDX_FE5E5AB848A9533F');
$this->addSql('ALTER TABLE forums DROP stylesheet_id');
$this->addSql('ALTER TABLE forums DROP night_stylesheet_id');
}
}
......@@ -109,6 +109,20 @@ class Forum {
*/
private $category;
/**
* @ORM\ManyToOne(targetEntity="Stylesheet")
*
* @var Stylesheet|null
*/
private $stylesheet;
/**
* @ORM\ManyToOne(targetEntity="Stylesheet")
*
* @var Stylesheet|null
*/
private $nightStylesheet;
public function __construct() {
$this->created = new \DateTime('@'.time());
$this->moderators = new ArrayCollection();
......@@ -257,4 +271,32 @@ class Forum {
public function setCategory($category) {
$this->category = $category;
}
/**
* @return Stylesheet|null
*/
public function getStylesheet() {
return $this->stylesheet;
}
/**
* @param Stylesheet|null $stylesheet
*/
public function setStylesheet($stylesheet) {
$this->stylesheet = $stylesheet;
}
/**
* @return Stylesheet|null
*/
public function getNightStylesheet() {
return $this->nightStylesheet;
}
/**
* @param Stylesheet|null $nightStylesheet
*/
public function setNightStylesheet($nightStylesheet) {
$this->nightStylesheet = $nightStylesheet;
}
}
<?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()
* @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;
}
}
......@@ -176,6 +176,13 @@ class User implements UserInterface, TwoFactorInterface {
*/
private $emailAuthCode;
/**
* @ORM\Column(type="boolean", options={"default": true})
*
* @var bool
*/
private $showCustomStylesheets = true;
public function __construct() {
$this->created = new \DateTime('@'.time());
$this->moderatorTokens = new ArrayCollection();
......@@ -499,6 +506,20 @@ class User implements UserInterface, TwoFactorInterface {
$this->emailAuthCode = $authCode;
}
/**
* @return bool
*/
public function isShowCustomStylesheets(): bool {
return $this->showCustomStylesheets;
}
/**
* @param bool $showCustomStylesheets
*/
public function setShowCustomStylesheets(bool $showCustomStylesheets) {
$this->showCustomStylesheets = $showCustomStylesheets;
}
/**
* Returns the canonical form of the username.
*
......
......@@ -5,6 +5,7 @@ namespace Raddit\AppBundle\Form;
use Doctrine\ORM\EntityRepository;
use Raddit\AppBundle\Entity\Forum;
use Raddit\AppBundle\Entity\ForumCategory;
use Raddit\AppBundle\Entity\Stylesheet;
use Raddit\AppBundle\Form\Type\MarkdownType;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
......@@ -44,7 +45,27 @@ final class ForumType extends AbstractType {
},
'required' => false,
'placeholder' => 'forum_form.uncategorized_placeholder',
]);
])
->add('stylesheet', EntityType::class, [
'class' => Stylesheet::class,
'choice_label' => 'name',
'placeholder' => 'forum_form.no_style',
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('s')->where('s.nightFriendly = FALSE');
},
'required' => false,
])
->add('nightStylesheet', EntityType::class, [
'class' => Stylesheet::class,
'choice_label' => 'name',
'label' => 'forum_form.night_stylesheet',
'placeholder' => 'forum_form.no_style',
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('s')->where('s.nightFriendly = TRUE');
},
'required' => false,
])
;
if ($this->authorizationChecker->isGranted('ROLE_ADMIN')) {
$builder->add('featured', CheckboxType::class, [
......
<?php
namespace Raddit\AppBundle\Form;
use Raddit\AppBundle\Entity\Stylesheet;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class StylesheetType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$editing = $builder->getData() && $builder->getData()->getId();
$builder
->add('name', TextType::class, [
'label' => 'stylesheets.name',
])
->add('css', TextareaType::class, [
'label' => 'stylesheets.css',
])
->add('appendToDefaultStyle', CheckboxType::class, [
'required' => false,
'label' => 'stylesheets.append_to_default_style',
])
->add('nightFriendly', CheckboxType::class, [
'required' => false,
'label' => 'stylesheets.night_friendly',
])
->add('submit', SubmitType::class, [
'label' => $editing ? 'stylesheets.edit_stylesheet' : 'stylesheets.create_stylesheet',
]);
}
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults([
'data_class' => Stylesheet::class,
]);
}
}
......@@ -25,6 +25,9 @@ final class UserSettingsType extends AbstractType {
->add('night_mode', CheckboxType::class, [
'required' => false,
])
->add('show_custom_stylesheets', CheckboxType::class, [
'required' => false,
])
->add('save', SubmitType::class);
}
......
......@@ -128,6 +128,27 @@ raddit_app_unsubscribe:
path: /unsub/{id}
methods: [POST]
raddit_app_stylesheets:
defaults: { _controller: RadditAppBundle:Stylesheet:list, page: 1 }
path: /stylesheets/{page}
methods: [GET]
requirements: { page: \d+ }
raddit_app_create_stylesheet:
defaults: { _controller: RadditAppBundle:Stylesheet:create }
path: /create_stylesheet
methods: [GET, POST]
raddit_app_edit_stylesheet:
defaults: { _controller: RadditAppBundle:Stylesheet:edit }
path: /edit_stylesheet/{id}
methods: [GET, POST]
raddit_app_raw_stylesheet:
defaults: { _controller: RadditAppBundle:Stylesheet:raw, _format: css }
path: /user_style/{id}.css
methods: [GET]
raddit_app_fetch_title:
defaults: { _controller: RadditAppBundle:Ajax:fetchTitle, _format: json }
path: /ft.json
......
......@@ -91,6 +91,9 @@ forum_form:
confirm_delete: Are you sure you want to delete this forum and all its submissions and comments?
featured: Show on front page
uncategorized_placeholder: (uncategorized)
stylesheet: Stylesheet
night_stylesheet: Stylesheet (night mode)
no_style: (no style)
forum_list:
name: Name
......@@ -204,6 +207,23 @@ site_nav:
messages: Messages
wiki: Wiki
stylesheets:
name: Name
css: CSS
author: Author
edit: Edit
append_to_default_style: Append to default style
append_to_default_style_description: Leave this checked unless your CSS provides a complete stylesheet for the entire site.
night_friendly: Night friendly
create_stylesheet: Create stylesheet
edit_stylesheet: Edit stylesheet
new_stylesheet: New stylesheet
list_title: Custom stylesheets
created_notice: Your stylesheet has been created.
edited_notice: Your changes have been saved.
create_title: Creating a stylesheet
edit_title: Editing stylesheet %name%
submission_form:
title: Title
url: URL
......@@ -287,6 +307,7 @@ user_settings:
user_settings_form:
locale: Language
night_mode: Night mode
show_custom_stylesheets: Show custom stylesheets
save: Save changes
wiki:
......
......@@ -2,3 +2,12 @@
'The name must contain only contain letters, numbers, and underscores.': 'The name must contain only contain letters, numbers, and underscores.'
'Two-factor cannot be enabled without providing an email address.': 'Two-factor cannot be enabled without providing an email address.'
'No such user.': 'No such user.'
'Error from CSS parser: {{ error }}': 'Error from CSS parser: {{ error }}'
'"progid:" syntax is not allowed on line {{ line }}': '"progid:" syntax is not allowed on line {{ line }}'
'Property "{{ property }}" is not allowed on line {{ line }}': 'Property "{{ property }}" is not allowed on line {{ line }}'
'Non UTF-8 charsets is not allowed on line {{ line }}': 'Non UTF-8 charset is not allowed on line {{ line }}'
'Script URL is not allowed on line {{ line }}': 'Script URL is not allowed on line {{ line }}'
'External resource is not allowed on line {{ line }}': 'External resource is not allowed on line {{ line }}'
'expression() syntax is not allowed on line {{ line }}': 'expression() syntax is not allowed on line {{ line }}'
'expression() syntax is not allowed in strings on line {{ line }}': 'expression() syntax is not allowed in strings on line {{ line }}'
'Recursion limit reached': 'Recursion limit reached'
{% extends '@RadditApp/forum_base.html.twig' %}
{% block page_classes %}edit-forum-page{% endblock %}
{% block title %}{{ 'edit_forum.title'|trans({'%forum%': forum.name}) }}{% endblock %}
{% block title 'edit_forum.title'|trans({'%forum%': forum.name}) %}
{% set no_custom_stylesheets = true %}
{% block body %}
{{ form_start(form) }}
......@@ -12,6 +12,17 @@
{{ form_row(form.description) }}
{{ form_row(form.category) }}