Add rss for entries

will fix #1000
This commit is contained in:
Jeremy 2015-03-28 14:27:45 +01:00
parent f98a2a0fc3
commit 0c83fd5994
13 changed files with 510 additions and 61 deletions

View file

@ -42,3 +42,4 @@ parameters:
theme: baggy
language: en_US
from_email: no-reply@wallabag.org
rss_limit: 50

View file

@ -198,6 +198,7 @@ class InstallCommand extends ContainerAwareCommand
$config = new Config($user);
$config->setTheme($this->getContainer()->getParameter('theme'));
$config->setItemsPerPage($this->getContainer()->getParameter('items_on_page'));
$config->setRssLimit($this->getContainer()->getParameter('rss_limit'));
$config->setLanguage($this->getContainer()->getParameter('language'));
$em->persist($config);

View file

@ -5,11 +5,14 @@ namespace Wallabag\CoreBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Wallabag\CoreBundle\Entity\Config;
use Wallabag\CoreBundle\Entity\User;
use Wallabag\CoreBundle\Form\Type\ChangePasswordType;
use Wallabag\CoreBundle\Form\Type\UserType;
use Wallabag\CoreBundle\Form\Type\NewUserType;
use Wallabag\CoreBundle\Form\Type\RssType;
use Wallabag\CoreBundle\Tools\Utils;
class ConfigController extends Controller
{
@ -77,6 +80,22 @@ class ConfigController extends Controller
return $this->redirect($this->generateUrl('config'));
}
// handle rss information
$rssForm = $this->createForm(new RssType(), $config);
$rssForm->handleRequest($request);
if ($rssForm->isValid()) {
$em->persist($config);
$em->flush();
$this->get('session')->getFlashBag()->add(
'notice',
'RSS information updated'
);
return $this->redirect($this->generateUrl('config'));
}
// handle adding new user
$newUser = new User();
$newUserForm = $this->createForm(new NewUserType(), $newUser);
@ -88,6 +107,7 @@ class ConfigController extends Controller
$config = new Config($newUser);
$config->setTheme($this->container->getParameter('theme'));
$config->setItemsPerPage($this->container->getParameter('items_on_page'));
$config->setRssLimit($this->getContainer()->getParameter('rss_limit'));
$config->setLanguage($this->container->getParameter('language'));
$em->persist($config);
@ -103,13 +123,43 @@ class ConfigController extends Controller
}
return $this->render('WallabagCoreBundle:Config:index.html.twig', array(
'configForm' => $configForm->createView(),
'pwdForm' => $pwdForm->createView(),
'userForm' => $userForm->createView(),
'newUserForm' => $newUserForm->createView(),
'form' => array(
'config' => $configForm->createView(),
'rss' => $rssForm->createView(),
'pwd' => $pwdForm->createView(),
'user' => $userForm->createView(),
'new_user' => $newUserForm->createView(),
),
'rss' => array(
'username' => $user->getUsername(),
'token' => $config->getRssToken(),
)
));
}
/**
* @param Request $request
*
* @Route("/generate-token", name="generate_token")
*
* @return JsonResponse
*/
public function generateTokenAction(Request $request)
{
$config = $this->getConfig();
$config->setRssToken(Utils::generateToken());
$em = $this->getDoctrine()->getManager();
$em->persist($config);
$em->flush();
if ($request->isXmlHttpRequest()) {
return new JsonResponse(array('token' => $config->getRssToken()));
}
return $request->headers->get('referer') ? $this->redirect($request->headers->get('referer')) : $this->redirectToRoute('config');
}
/**
* Retrieve config for the current user.
* If no config were found, create a new one.

View file

@ -0,0 +1,84 @@
<?php
namespace Wallabag\CoreBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Wallabag\CoreBundle\Entity\User;
use Wallabag\CoreBundle\Entity\Entry;
class RssController extends Controller
{
/**
* Shows unread entries for current user
*
* @Route("/{username}/{token}/unread.xml", name="unread_rss", defaults={"_format"="xml"})
* @ParamConverter("user", class="WallabagCoreBundle:User", converter="username_rsstoken_converter")
*
* @return \Symfony\Component\HttpFoundation\Response
*/
public function showUnreadAction(User $user)
{
$entries = $this->getDoctrine()
->getRepository('WallabagCoreBundle:Entry')
->findUnreadByUser(
$user->getId(),
0,
$user->getConfig()->getRssLimit() ?: $this->getContainer()->getParameter('rss_limit')
);
return $this->render('WallabagCoreBundle:Entry:entries.xml.twig', array(
'type' => 'unread',
'entries' => $entries,
));
}
/**
* Shows read entries for current user
*
* @Route("/{username}/{token}/archive.xml", name="archive_rss")
* @ParamConverter("user", class="WallabagCoreBundle:User", converter="username_rsstoken_converter")
*
* @return \Symfony\Component\HttpFoundation\Response
*/
public function showArchiveAction(User $user)
{
$entries = $this->getDoctrine()
->getRepository('WallabagCoreBundle:Entry')
->findArchiveByUser(
$user->getId(),
0,
$user->getConfig()->getRssLimit() ?: $this->getContainer()->getParameter('rss_limit')
);
return $this->render('WallabagCoreBundle:Entry:entries.xml.twig', array(
'type' => 'archive',
'entries' => $entries,
));
}
/**
* Shows starred entries for current user
*
* @Route("/{username}/{token}/starred.xml", name="starred_rss")
* @ParamConverter("user", class="WallabagCoreBundle:User", converter="username_rsstoken_converter")
*
* @return \Symfony\Component\HttpFoundation\Response
*/
public function showStarredAction(User $user)
{
$entries = $this->getDoctrine()
->getRepository('WallabagCoreBundle:Entry')
->findStarredByUser(
$user->getId(),
0,
$user->getConfig()->getRssLimit() ?: $this->getContainer()->getParameter('rss_limit')
);
return $this->render('WallabagCoreBundle:Entry:entries.xml.twig', array(
'type' => 'starred',
'entries' => $entries,
));
}
}

View file

@ -32,12 +32,12 @@ class Config
private $theme;
/**
* @var string
* @var integer
*
* @Assert\NotBlank()
* @ORM\Column(name="items_per_page", type="integer", nullable=false)
*/
private $items_per_page;
private $itemsPerPage;
/**
* @var string
@ -47,6 +47,20 @@ class Config
*/
private $language;
/**
* @var string
*
* @ORM\Column(name="rss_token", type="string", nullable=true)
*/
private $rssToken;
/**
* @var integer
*
* @ORM\Column(name="rss_limit", type="integer", nullable=true)
*/
private $rssLimit;
/**
* @ORM\OneToOne(targetEntity="User", inversedBy="config")
*/
@ -58,8 +72,6 @@ class Config
public function __construct(User $user)
{
$this->user = $user;
$this->items_per_page = 12;
$this->language = 'en_US';
}
/**
@ -96,26 +108,26 @@ class Config
}
/**
* Set items_per_page
* Set itemsPerPage
*
* @param integer $itemsPerPage
* @return Config
*/
public function setItemsPerPage($itemsPerPage)
{
$this->items_per_page = $itemsPerPage;
$this->itemsPerPage = $itemsPerPage;
return $this;
}
/**
* Get items_per_page
* Get itemsPerPage
*
* @return integer
*/
public function getItemsPerPage()
{
return $this->items_per_page;
return $this->itemsPerPage;
}
/**
@ -163,4 +175,50 @@ class Config
{
return $this->user;
}
/**
* Set rssToken
*
* @param string $rssToken
* @return Config
*/
public function setRssToken($rssToken)
{
$this->rssToken = $rssToken;
return $this;
}
/**
* Get rssToken
*
* @return string
*/
public function getRssToken()
{
return $this->rssToken;
}
/**
* Set rssLimit
*
* @param string $rssLimit
* @return Config
*/
public function setRssLimit($rssLimit)
{
$this->rssLimit = $rssLimit;
return $this;
}
/**
* Get rssLimit
*
* @return string
*/
public function getRssLimit()
{
return $this->rssLimit;
}
}

View file

@ -14,7 +14,7 @@ use JMS\Serializer\Annotation\Expose;
* User
*
* @ORM\Table(name="user")
* @ORM\Entity
* @ORM\Entity(repositoryClass="Wallabag\CoreBundle\Repository\UserRepository")
* @ORM\HasLifecycleCallbacks()
* @ExclusionPolicy("all")
*/

View file

@ -0,0 +1,29 @@
<?php
namespace Wallabag\CoreBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class RssType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('rss_limit', 'text')
->add('save', 'submit')
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Wallabag\CoreBundle\Entity\Config',
));
}
public function getName()
{
return 'rss_config';
}
}

View file

@ -0,0 +1,92 @@
<?php
namespace Wallabag\CoreBundle\ParamConverter;
use Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter\ParamConverterInterface;
use Doctrine\Common\Persistence\ManagerRegistry;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Wallabag\CoreBundle\Entity\User;
/**
* ParamConverter used in the RSS controller to retrieve the right user according to
* username & token given in the url.
*
* @see http://stfalcon.com/en/blog/post/symfony2-custom-paramconverter
*/
class UsernameRssTokenConverter implements ParamConverterInterface
{
private $registry;
/**
* @param ManagerRegistry $registry Manager registry
*/
public function __construct(ManagerRegistry $registry = null)
{
$this->registry = $registry;
}
/**
* {@inheritdoc}
*
* Check, if object supported by our converter
*/
public function supports(ParamConverter $configuration)
{
// If there is no manager, this means that only Doctrine DBAL is configured
// In this case we can do nothing and just return
if (null === $this->registry || !count($this->registry->getManagers())) {
return false;
}
// Check, if option class was set in configuration
if (null === $configuration->getClass()) {
return false;
}
// Get actual entity manager for class
$em = $this->registry->getManagerForClass($configuration->getClass());
// Check, if class name is what we need
if ('Wallabag\CoreBundle\Entity\User' !== $em->getClassMetadata($configuration->getClass())->getName()) {
return false;
}
return true;
}
/**
* {@inheritdoc}
*
* Applies converting
*
* @throws \InvalidArgumentException When route attributes are missing
* @throws NotFoundHttpException When object not found
*/
public function apply(Request $request, ParamConverter $configuration)
{
$username = $request->attributes->get('username');
$rssToken = $request->attributes->get('token');
// Check, if route attributes exists
if (null === $username || null === $rssToken) {
throw new \InvalidArgumentException('Route attribute is missing');
}
// Get actual entity manager for class
$em = $this->registry->getManagerForClass($configuration->getClass());
$userRepository = $em->getRepository($configuration->getClass());
// Try to find user by its username and config rss_token
$user = $userRepository->findOneByUsernameAndRsstoken($username, $rssToken);
if (null === $user || !($user instanceof User)) {
throw new NotFoundHttpException(sprintf('%s not found.', $configuration->getClass()));
}
// Map found user to the route's parameter
$request->attributes->set($configuration->getName(), $user);
}
}

View file

@ -0,0 +1,26 @@
<?php
namespace Wallabag\CoreBundle\Repository;
use Doctrine\ORM\EntityRepository;
class UserRepository extends EntityRepository
{
/**
* Find a user by its username and rss roken
*
* @param string $username
* @param string $rssToken
*
* @return User|null
*/
public function findOneByUsernameAndRsstoken($username, $rssToken)
{
return $this->createQueryBuilder('u')
->leftJoin('u.config', 'c')
->where('c.rssToken = :rss_token')->setParameter('rss_token', $rssToken)
->andWhere('u.username = :username')->setParameter('username', $username)
->getQuery()
->getOneOrNullResult();
}
}

View file

@ -36,3 +36,10 @@ services:
- @doctrine
tags:
- { name: form.type, alias: forgot_password }
wallabag_core.param_converter.username_rsstoken_converter:
class: Wallabag\CoreBundle\ParamConverter\UsernameRssTokenConverter
tags:
- { name: request.param_converter, converter: username_rsstoken_converter }
arguments:
- @doctrine

View file

@ -5,129 +5,169 @@
{% block content %}
<h2>{% trans %}Wallabag configuration{% endtrans %}</h2>
<form action="{{ path('config') }}" method="post" {{ form_enctype(configForm) }}>
{{ form_errors(configForm) }}
<form action="{{ path('config') }}" method="post" {{ form_enctype(form.config) }}>
{{ form_errors(form.config) }}
<fieldset class="w500p inline">
<div class="row">
{{ form_label(configForm.theme) }}
{{ form_errors(configForm.theme) }}
{{ form_widget(configForm.theme) }}
{{ form_label(form.config.theme) }}
{{ form_errors(form.config.theme) }}
{{ form_widget(form.config.theme) }}
</div>
</fieldset>
<fieldset class="w500p inline">
<div class="row">
{{ form_label(configForm.items_per_page) }}
{{ form_errors(configForm.items_per_page) }}
{{ form_widget(configForm.items_per_page) }}
{{ form_label(form.config.items_per_page) }}
{{ form_errors(form.config.items_per_page) }}
{{ form_widget(form.config.items_per_page) }}
</div>
</fieldset>
<fieldset class="w500p inline">
<div class="row">
{{ form_label(configForm.language) }}
{{ form_errors(configForm.language) }}
{{ form_widget(configForm.language) }}
{{ form_label(form.config.language) }}
{{ form_errors(form.config.language) }}
{{ form_widget(form.config.language) }}
</div>
</fieldset>
{{ form_rest(configForm) }}
{{ form_rest(form.config) }}
</form>
<h2>{% trans %}RSS configuration{% endtrans %}</h2>
<form action="{{ path('config') }}" method="post" {{ form_enctype(form.rss) }}>
{{ form_errors(form.rss) }}
<fieldset class="w500p inline">
<div class="row">
<label>Rss token</label>
{% if rss.token %}
{{ rss.token }}
{% else %}
<em>No token</em>
{% endif %}
<a href="{{ path('generate_token') }}">Regenerate ?</a>
</div>
</fieldset>
<fieldset class="w500p inline">
<div class="row">
<label>Rss links:</label>
<ul>
<li><a href="{{ path('unread_rss', {'username': rss.username, 'token': rss.token}) }}">unread</a></li>
<li><a href="{{ path('starred_rss', {'username': rss.username, 'token': rss.token}) }}">fav</a></li>
<li><a href="{{ path('archive_rss', {'username': rss.username, 'token': rss.token}) }}">archives</a></li>
</ul>
</div>
</fieldset>
<fieldset class="w500p inline">
<div class="row">
{{ form_label(form.rss.rss_limit) }}
{{ form_errors(form.rss.rss_limit) }}
{{ form_widget(form.rss.rss_limit) }}
</div>
</fieldset>
{{ form_rest(form.rss) }}
</form>
<h2>{% trans %}User information{% endtrans %}</h2>
<form action="{{ path('config') }}" method="post" {{ form_enctype(userForm) }}>
{{ form_errors(userForm) }}
<form action="{{ path('config') }}" method="post" {{ form_enctype(form.user) }}>
{{ form_errors(form.user) }}
<fieldset class="w500p inline">
<div class="row">
{{ form_label(userForm.username) }}
{{ form_errors(userForm.username) }}
{{ form_widget(userForm.username) }}
{{ form_label(form.user.username) }}
{{ form_errors(form.user.username) }}
{{ form_widget(form.user.username) }}
</div>
</fieldset>
<fieldset class="w500p inline">
<div class="row">
{{ form_label(userForm.name) }}
{{ form_errors(userForm.name) }}
{{ form_widget(userForm.name) }}
{{ form_label(form.user.name) }}
{{ form_errors(form.user.name) }}
{{ form_widget(form.user.name) }}
</div>
</fieldset>
<fieldset class="w500p inline">
<div class="row">
{{ form_label(userForm.email) }}
{{ form_errors(userForm.email) }}
{{ form_widget(userForm.email) }}
{{ form_label(form.user.email) }}
{{ form_errors(form.user.email) }}
{{ form_widget(form.user.email) }}
</div>
</fieldset>
{{ form_rest(userForm) }}
{{ form_rest(form.user) }}
</form>
<h2>{% trans %}Change your password{% endtrans %}</h2>
<form action="{{ path('config') }}" method="post" {{ form_enctype(pwdForm) }}>
{{ form_errors(pwdForm) }}
<form action="{{ path('config') }}" method="post" {{ form_enctype(form.pwd) }}>
{{ form_errors(form.pwd) }}
<fieldset class="w500p inline">
<div class="row">
{{ form_label(pwdForm.old_password) }}
{{ form_errors(pwdForm.old_password) }}
{{ form_widget(pwdForm.old_password) }}
{{ form_label(form.pwd.old_password) }}
{{ form_errors(form.pwd.old_password) }}
{{ form_widget(form.pwd.old_password) }}
</div>
</fieldset>
<fieldset class="w500p inline">
<div class="row">
{{ form_label(pwdForm.new_password.first) }}
{{ form_errors(pwdForm.new_password.first) }}
{{ form_widget(pwdForm.new_password.first) }}
{{ form_label(form.pwd.new_password.first) }}
{{ form_errors(form.pwd.new_password.first) }}
{{ form_widget(form.pwd.new_password.first) }}
</div>
</fieldset>
<fieldset class="w500p inline">
<div class="row">
{{ form_label(pwdForm.new_password.second) }}
{{ form_errors(pwdForm.new_password.second) }}
{{ form_widget(pwdForm.new_password.second) }}
{{ form_label(form.pwd.new_password.second) }}
{{ form_errors(form.pwd.new_password.second) }}
{{ form_widget(form.pwd.new_password.second) }}
</div>
</fieldset>
{{ form_rest(pwdForm) }}
{{ form_rest(form.pwd) }}
</form>
<h2>{% trans %}Add a user{% endtrans %}</h2>
<form action="{{ path('config') }}" method="post" {{ form_enctype(newUserForm) }}>
{{ form_errors(newUserForm) }}
<form action="{{ path('config') }}" method="post" {{ form_enctype(form.new_user) }}>
{{ form_errors(form.new_user) }}
<fieldset class="w500p inline">
<div class="row">
{{ form_label(newUserForm.username) }}
{{ form_errors(newUserForm.username) }}
{{ form_widget(newUserForm.username) }}
{{ form_label(form.new_user.username) }}
{{ form_errors(form.new_user.username) }}
{{ form_widget(form.new_user.username) }}
</div>
</fieldset>
<fieldset class="w500p inline">
<div class="row">
{{ form_label(newUserForm.password) }}
{{ form_errors(newUserForm.password) }}
{{ form_widget(newUserForm.password) }}
{{ form_label(form.new_user.password) }}
{{ form_errors(form.new_user.password) }}
{{ form_widget(form.new_user.password) }}
</div>
</fieldset>
<fieldset class="w500p inline">
<div class="row">
{{ form_label(newUserForm.email) }}
{{ form_errors(newUserForm.email) }}
{{ form_widget(newUserForm.email) }}
{{ form_label(form.new_user.email) }}
{{ form_errors(form.new_user.email) }}
{{ form_widget(form.new_user.email) }}
</div>
</fieldset>
{{ form_rest(newUserForm) }}
{{ form_rest(form.new_user) }}
</form>
{% endblock %}

View file

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/">
<channel>
<title>wallabag — {{type}} feed</title>
<link>{{ url('unread') }}</link>
<pubDate>{{ "now"|date('D, d M Y H:i:s') }}</pubDate>
<generator>wallabag</generator>
<description>wallabag {{type}} elements</description>
{% for entry in entries %}
<item>
<title><![CDATA[{{ entry.title }}]]></title>
<source url="{{ url('view', { 'id': entry.id }) }}">wallabag</source>
<link>{{ url('view', { 'id': entry.id }) }}</link>
<guid>{{ url('view', { 'id': entry.id }) }}</guid>
<pubDate>{{ entry.createdAt|date('D, d M Y H:i:s') }}</pubDate>
<description>
<![CDATA[
{%- if entry.content| readingTime > 0 %}
{% trans %}estimated reading time :{% endtrans %} {{ entry.content| readingTime }} min
{% else -%}
{% trans %}estimated reading time :{% endtrans %} &lt; 1 min
{% endif -%}
{{ entry.content -}}
]]>
</description>
</item>
{% endfor %}
</channel>
</rss>

View file

@ -0,0 +1,27 @@
<?php
namespace Wallabag\CoreBundle\Tools;
class Utils
{
/**
* Generate a token used for RSS
*
* @return string
*/
public static function generateToken()
{
if (ini_get('open_basedir') === '') {
if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
// alternative to /dev/urandom for Windows
$token = substr(base64_encode(uniqid(mt_rand(), true)), 0, 20);
} else {
$token = substr(base64_encode(file_get_contents('/dev/urandom', false, null, 0, 20)), 0, 15);
}
} else {
$token = substr(base64_encode(uniqid(mt_rand(), true)), 0, 20);
}
return str_replace('+', '', $token);
}
}