1
0
Fork 0
mirror of https://github.com/wallabag/wallabag.git synced 2025-08-26 18:21:02 +00:00

Move source files directly under src/ directory

This commit is contained in:
Yassine Guedidi 2024-02-19 00:39:48 +01:00
parent 804261bc26
commit a37b385c23
190 changed files with 19 additions and 21 deletions

View file

@ -0,0 +1,20 @@
<?php
namespace Wallabag\CoreBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController as BaseAbstractController;
use Wallabag\CoreBundle\Entity\User;
abstract class AbstractController extends BaseAbstractController
{
/**
* @return User|null
*/
protected function getUser()
{
$user = parent::getUser();
\assert(null === $user || $user instanceof User);
return $user;
}
}

View file

@ -0,0 +1,171 @@
<?php
namespace Wallabag\CoreBundle\Controller;
use Doctrine\ORM\EntityManagerInterface;
use FOS\RestBundle\Controller\AbstractFOSRestController;
use JMS\Serializer\SerializerInterface;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Wallabag\CoreBundle\Entity\Annotation;
use Wallabag\CoreBundle\Entity\Entry;
use Wallabag\CoreBundle\Entity\User;
use Wallabag\CoreBundle\Form\Type\EditAnnotationType;
use Wallabag\CoreBundle\Form\Type\NewAnnotationType;
use Wallabag\CoreBundle\Repository\AnnotationRepository;
class AnnotationController extends AbstractFOSRestController
{
protected EntityManagerInterface $entityManager;
protected SerializerInterface $serializer;
protected FormFactoryInterface $formFactory;
public function __construct(EntityManagerInterface $entityManager, SerializerInterface $serializer, FormFactoryInterface $formFactory)
{
$this->entityManager = $entityManager;
$this->serializer = $serializer;
$this->formFactory = $formFactory;
}
/**
* Retrieve annotations for an entry.
*
* @see Api\WallabagRestController
*
* @Route("/annotations/{entry}.{_format}", methods={"GET"}, name="annotations_get_annotations", defaults={"_format": "json"})
*
* @return JsonResponse
*/
public function getAnnotationsAction(Entry $entry, AnnotationRepository $annotationRepository)
{
$annotationRows = $annotationRepository->findByEntryIdAndUserId($entry->getId(), $this->getUser()->getId());
$total = \count($annotationRows);
$annotations = ['total' => $total, 'rows' => $annotationRows];
$json = $this->serializer->serialize($annotations, 'json');
return (new JsonResponse())->setJson($json);
}
/**
* Creates a new annotation.
*
* @see Api\WallabagRestController
*
* @Route("/annotations/{entry}.{_format}", methods={"POST"}, name="annotations_post_annotation", defaults={"_format": "json"})
*
* @return JsonResponse
*/
public function postAnnotationAction(Request $request, Entry $entry)
{
$data = json_decode($request->getContent(), true);
$annotation = new Annotation($this->getUser());
$annotation->setEntry($entry);
$form = $this->formFactory->createNamed('', NewAnnotationType::class, $annotation, [
'csrf_protection' => false,
'allow_extra_fields' => true,
]);
$form->submit($data);
if ($form->isValid()) {
$this->entityManager->persist($annotation);
$this->entityManager->flush();
$json = $this->serializer->serialize($annotation, 'json');
return JsonResponse::fromJsonString($json);
}
return $form;
}
/**
* Updates an annotation.
*
* @see Api\WallabagRestController
*
* @Route("/annotations/{annotation}.{_format}", methods={"PUT"}, name="annotations_put_annotation", defaults={"_format": "json"})
*
* @return JsonResponse
*/
public function putAnnotationAction(Request $request, AnnotationRepository $annotationRepository, int $annotation)
{
try {
$annotation = $this->validateAnnotation($annotationRepository, $annotation, $this->getUser()->getId());
$data = json_decode($request->getContent(), true, 512, \JSON_THROW_ON_ERROR);
$form = $this->formFactory->createNamed('', EditAnnotationType::class, $annotation, [
'csrf_protection' => false,
'allow_extra_fields' => true,
]);
$form->submit($data);
if ($form->isValid()) {
$this->entityManager->persist($annotation);
$this->entityManager->flush();
$json = $this->serializer->serialize($annotation, 'json');
return JsonResponse::fromJsonString($json);
}
return $form;
} catch (\InvalidArgumentException $e) {
throw new NotFoundHttpException($e);
}
}
/**
* Removes an annotation.
*
* @see Api\WallabagRestController
*
* @Route("/annotations/{annotation}.{_format}", methods={"DELETE"}, name="annotations_delete_annotation", defaults={"_format": "json"})
*
* @return JsonResponse
*/
public function deleteAnnotationAction(AnnotationRepository $annotationRepository, int $annotation)
{
try {
$annotation = $this->validateAnnotation($annotationRepository, $annotation, $this->getUser()->getId());
$this->entityManager->remove($annotation);
$this->entityManager->flush();
$json = $this->serializer->serialize($annotation, 'json');
return (new JsonResponse())->setJson($json);
} catch (\InvalidArgumentException $e) {
throw new NotFoundHttpException($e);
}
}
/**
* @return User|null
*/
protected function getUser()
{
$user = parent::getUser();
\assert(null === $user || $user instanceof User);
return $user;
}
private function validateAnnotation(AnnotationRepository $annotationRepository, int $annotationId, int $userId)
{
$annotation = $annotationRepository->findOneByIdAndUserId($annotationId, $userId);
if (null === $annotation) {
throw new NotFoundHttpException();
}
return $annotation;
}
}

View file

@ -0,0 +1,187 @@
<?php
namespace Wallabag\CoreBundle\Controller\Api;
use Nelmio\ApiDocBundle\Annotation\Operation;
use OpenApi\Annotations as OA;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Wallabag\CoreBundle\Entity\Annotation;
use Wallabag\CoreBundle\Entity\Entry;
class AnnotationRestController extends WallabagRestController
{
/**
* Retrieve annotations for an entry.
*
* @Operation(
* tags={"Annotations"},
* summary="Retrieve annotations for an entry.",
* @OA\Parameter(
* name="entry",
* in="path",
* description="The entry ID",
* required=true,
* @OA\Schema(
* type="integer",
* pattern="\w+",
* )
* ),
* @OA\Response(
* response="200",
* description="Returned when successful"
* )
* )
*
* @Route("/api/annotations/{entry}.{_format}", methods={"GET"}, name="api_get_annotations", defaults={"_format": "json"})
*
* @return Response
*/
public function getAnnotationsAction(Entry $entry)
{
$this->validateAuthentication();
return $this->forward('Wallabag\CoreBundle\Controller\AnnotationController::getAnnotationsAction', [
'entry' => $entry,
]);
}
/**
* Creates a new annotation.
*
* @Operation(
* tags={"Annotations"},
* summary="Creates a new annotation.",
* @OA\Parameter(
* name="entry",
* in="path",
* description="The entry ID",
* required=true,
* @OA\Schema(
* type="integer",
* pattern="\w+",
* )
* ),
* @OA\RequestBody(
* @OA\JsonContent(
* type="object",
* required={"text"},
* @OA\Property(
* property="ranges",
* type="array",
* description="The range array for the annotation",
* @OA\Items(
* type="string",
* pattern="\w+",
* )
* ),
* @OA\Property(
* property="quote",
* type="array",
* description="The annotated text",
* @OA\Items(
* type="string",
* )
* ),
* @OA\Property(
* property="text",
* type="array",
* description="Content of annotation",
* @OA\Items(
* type="string",
* )
* ),
* )
* ),
* @OA\Response(
* response="200",
* description="Returned when successful"
* )
* )
*
* @Route("/api/annotations/{entry}.{_format}", methods={"POST"}, name="api_post_annotation", defaults={"_format": "json"})
*
* @return Response
*/
public function postAnnotationAction(Request $request, Entry $entry)
{
$this->validateAuthentication();
return $this->forward('Wallabag\CoreBundle\Controller\AnnotationController::postAnnotationAction', [
'request' => $request,
'entry' => $entry,
]);
}
/**
* Updates an annotation.
*
* @Operation(
* tags={"Annotations"},
* summary="Updates an annotation.",
* @OA\Parameter(
* name="annotation",
* in="path",
* description="The annotation ID",
* required=true,
* @OA\Schema(
* type="string",
* pattern="\w+",
* )
* ),
* @OA\Response(
* response="200",
* description="Returned when successful"
* )
* )
*
* @Route("/api/annotations/{annotation}.{_format}", methods={"PUT"}, name="api_put_annotation", defaults={"_format": "json"})
*
* @return Response
*/
public function putAnnotationAction(int $annotation, Request $request)
{
$this->validateAuthentication();
return $this->forward('Wallabag\CoreBundle\Controller\AnnotationController::putAnnotationAction', [
'annotation' => $annotation,
'request' => $request,
]);
}
/**
* Removes an annotation.
*
* @Operation(
* tags={"Annotations"},
* summary="Removes an annotation.",
* @OA\Parameter(
* name="annotation",
* in="path",
* description="The annotation ID",
* required=true,
* @OA\Schema(
* type="string",
* pattern="\w+",
* )
* ),
* @OA\Response(
* response="200",
* description="Returned when successful"
* )
* )
*
* @Route("/api/annotations/{annotation}.{_format}", methods={"DELETE"}, name="api_delete_annotation", defaults={"_format": "json"})
*
* @return Response
*/
public function deleteAnnotationAction(int $annotation)
{
$this->validateAuthentication();
return $this->forward('Wallabag\CoreBundle\Controller\AnnotationController::deleteAnnotationAction', [
'annotation' => $annotation,
]);
}
}

View file

@ -0,0 +1,44 @@
<?php
namespace Wallabag\CoreBundle\Controller\Api;
use JMS\Serializer\SerializationContext;
use JMS\Serializer\SerializerInterface;
use Nelmio\ApiDocBundle\Annotation\Operation;
use OpenApi\Annotations as OA;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Annotation\Route;
class ConfigRestController extends WallabagRestController
{
/**
* Retrieve configuration for current user.
*
* @Operation(
* tags={"Config"},
* summary="Retrieve configuration for current user.",
* @OA\Response(
* response="200",
* description="Returned when successful"
* )
* )
*
* @Route("/api/config.{_format}", methods={"GET"}, name="api_get_config", defaults={"_format": "json"})
*
* @return JsonResponse
*/
public function getConfigAction(SerializerInterface $serializer)
{
$this->validateAuthentication();
$json = $serializer->serialize(
$this->getUser()->getConfig(),
'json',
SerializationContext::create()->setGroups(['config_api'])
);
return (new JsonResponse())
->setJson($json)
->setStatusCode(JsonResponse::HTTP_OK);
}
}

View file

@ -0,0 +1,110 @@
<?php
namespace Wallabag\CoreBundle\Controller\Api;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\Translation\TranslatorInterface;
use Wallabag\CoreBundle\Controller\AbstractController;
use Wallabag\CoreBundle\Entity\Api\Client;
use Wallabag\CoreBundle\Form\Type\Api\ClientType;
use Wallabag\CoreBundle\Repository\Api\ClientRepository;
class DeveloperController extends AbstractController
{
/**
* List all clients and link to create a new one.
*
* @Route("/developer", name="developer")
*
* @return Response
*/
public function indexAction(ClientRepository $repo)
{
$clients = $repo->findByUser($this->getUser()->getId());
return $this->render('Developer/index.html.twig', [
'clients' => $clients,
]);
}
/**
* Create a client (an app).
*
* @Route("/developer/client/create", name="developer_create_client")
*
* @return Response
*/
public function createClientAction(Request $request, EntityManagerInterface $entityManager, TranslatorInterface $translator)
{
$client = new Client($this->getUser());
$clientForm = $this->createForm(ClientType::class, $client);
$clientForm->handleRequest($request);
if ($clientForm->isSubmitted() && $clientForm->isValid()) {
$client->setAllowedGrantTypes(['token', 'authorization_code', 'password', 'refresh_token']);
$entityManager->persist($client);
$entityManager->flush();
$this->addFlash(
'notice',
$translator->trans('flashes.developer.notice.client_created', ['%name%' => $client->getName()])
);
return $this->render('Developer/client_parameters.html.twig', [
'client_id' => $client->getPublicId(),
'client_secret' => $client->getSecret(),
'client_name' => $client->getName(),
]);
}
return $this->render('Developer/client.html.twig', [
'form' => $clientForm->createView(),
]);
}
/**
* Remove a client.
*
* @Route("/developer/client/delete/{id}", requirements={"id" = "\d+"}, name="developer_delete_client", methods={"POST"})
*
* @return RedirectResponse
*/
public function deleteClientAction(Request $request, Client $client, EntityManagerInterface $entityManager, TranslatorInterface $translator)
{
if (!$this->isCsrfTokenValid('delete-client', $request->request->get('token'))) {
throw $this->createAccessDeniedException('Bad CSRF token.');
}
if (null === $this->getUser() || $client->getUser()->getId() !== $this->getUser()->getId()) {
throw $this->createAccessDeniedException('You can not access this client.');
}
$entityManager->remove($client);
$entityManager->flush();
$this->addFlash(
'notice',
$translator->trans('flashes.developer.notice.client_deleted', ['%name%' => $client->getName()])
);
return $this->redirect($this->generateUrl('developer'));
}
/**
* Display developer how to use an existing app.
*
* @Route("/developer/howto/first-app", name="developer_howto_firstapp")
*
* @return Response
*/
public function howtoFirstAppAction()
{
return $this->render('Developer/howto_app.html.twig', [
'wallabag_url' => $this->getParameter('domain_name'),
]);
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,97 @@
<?php
namespace Wallabag\CoreBundle\Controller\Api;
use Hateoas\Configuration\Route as HateoasRoute;
use Hateoas\Representation\Factory\PagerfantaFactory;
use Nelmio\ApiDocBundle\Annotation\Operation;
use OpenApi\Annotations as OA;
use Pagerfanta\Doctrine\ORM\QueryAdapter as DoctrineORMAdapter;
use Pagerfanta\Pagerfanta;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Wallabag\CoreBundle\Repository\EntryRepository;
class SearchRestController extends WallabagRestController
{
/**
* Search all entries by term.
*
* @Operation(
* tags={"Search"},
* summary="Search all entries by term.",
* @OA\Parameter(
* name="term",
* in="query",
* description="Any query term",
* required=false,
* @OA\Schema(type="string")
* ),
* @OA\Parameter(
* name="page",
* in="query",
* description="what page you want.",
* required=false,
* @OA\Schema(
* type="integer",
* default=1
* )
* ),
* @OA\Parameter(
* name="perPage",
* in="query",
* description="results per page.",
* required=false,
* @OA\Schema(
* type="integer",
* default=30
* )
* ),
* @OA\Response(
* response="200",
* description="Returned when successful"
* )
* )
*
* @Route("/api/search.{_format}", methods={"GET"}, name="api_get_search", defaults={"_format": "json"})
*
* @return JsonResponse
*/
public function getSearchAction(Request $request, EntryRepository $entryRepository)
{
$this->validateAuthentication();
$term = $request->query->get('term');
$page = (int) $request->query->get('page', 1);
$perPage = (int) $request->query->get('perPage', 30);
$qb = $entryRepository->getBuilderForSearchByUser(
$this->getUser()->getId(),
$term,
null
);
$pagerAdapter = new DoctrineORMAdapter($qb->getQuery(), true, false);
$pager = new Pagerfanta($pagerAdapter);
$pager->setMaxPerPage($perPage);
$pager->setCurrentPage($page);
$pagerfantaFactory = new PagerfantaFactory('page', 'perPage');
$paginatedCollection = $pagerfantaFactory->createRepresentation(
$pager,
new HateoasRoute(
'api_get_search',
[
'term' => $term,
'page' => $page,
'perPage' => $perPage,
],
true
)
);
return $this->sendResponse($paginatedCollection);
}
}

View file

@ -0,0 +1,203 @@
<?php
namespace Wallabag\CoreBundle\Controller\Api;
use Nelmio\ApiDocBundle\Annotation\Operation;
use OpenApi\Annotations as OA;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Wallabag\CoreBundle\Entity\Entry;
use Wallabag\CoreBundle\Entity\Tag;
use Wallabag\CoreBundle\Repository\EntryRepository;
use Wallabag\CoreBundle\Repository\TagRepository;
class TagRestController extends WallabagRestController
{
/**
* Retrieve all tags.
*
* @Operation(
* tags={"Tags"},
* summary="Retrieve all tags.",
* @OA\Response(
* response="200",
* description="Returned when successful"
* )
* )
*
* @Route("/api/tags.{_format}", methods={"GET"}, name="api_get_tags", defaults={"_format": "json"})
*
* @return JsonResponse
*/
public function getTagsAction(TagRepository $tagRepository)
{
$this->validateAuthentication();
$tags = $tagRepository->findAllFlatTagsWithNbEntries($this->getUser()->getId());
$json = $this->serializer->serialize($tags, 'json');
return (new JsonResponse())->setJson($json);
}
/**
* Permanently remove one tag from **every** entry by passing the Tag label.
*
* @Operation(
* tags={"Tags"},
* summary="Permanently remove one tag from every entry by passing the Tag label.",
* @OA\Parameter(
* name="tag",
* in="query",
* description="Tag as a string",
* required=true,
* @OA\Schema(
* type="string",
* pattern="\w+",
* )
* ),
* @OA\Response(
* response="200",
* description="Returned when successful"
* )
* )
*
* @Route("/api/tag/label.{_format}", methods={"DELETE"}, name="api_delete_tag_label", defaults={"_format": "json"})
*
* @return JsonResponse
*/
public function deleteTagLabelAction(Request $request, TagRepository $tagRepository, EntryRepository $entryRepository)
{
$this->validateAuthentication();
$label = $request->get('tag', '');
$tags = $tagRepository->findByLabelsAndUser([$label], $this->getUser()->getId());
if (empty($tags)) {
throw $this->createNotFoundException('Tag not found');
}
$tag = $tags[0];
$entryRepository->removeTag($this->getUser()->getId(), $tag);
$this->cleanOrphanTag($tag);
$json = $this->serializer->serialize($tag, 'json');
return (new JsonResponse())->setJson($json);
}
/**
* Permanently remove some tags from **every** entry.
*
* @Operation(
* tags={"Tags"},
* summary="Permanently remove some tags from every entry.",
* @OA\Parameter(
* name="tags",
* in="query",
* description="Tags as strings (comma splitted)",
* required=true,
* @OA\Schema(
* type="string",
* example="tag1,tag2",
* )
* ),
* @OA\Response(
* response="200",
* description="Returned when successful"
* )
* )
*
* @Route("/api/tags/label.{_format}", methods={"DELETE"}, name="api_delete_tags_label", defaults={"_format": "json"})
*
* @return JsonResponse
*/
public function deleteTagsLabelAction(Request $request, TagRepository $tagRepository, EntryRepository $entryRepository)
{
$this->validateAuthentication();
$tagsLabels = $request->get('tags', '');
$tags = $tagRepository->findByLabelsAndUser(explode(',', $tagsLabels), $this->getUser()->getId());
if (empty($tags)) {
throw $this->createNotFoundException('Tags not found');
}
$entryRepository->removeTags($this->getUser()->getId(), $tags);
$this->cleanOrphanTag($tags);
$json = $this->serializer->serialize($tags, 'json');
return (new JsonResponse())->setJson($json);
}
/**
* Permanently remove one tag from **every** entry by passing the Tag ID.
*
* @Operation(
* tags={"Tags"},
* summary="Permanently remove one tag from every entry by passing the Tag ID.",
* @OA\Parameter(
* name="tag",
* in="path",
* description="The tag",
* required=true,
* @OA\Schema(
* type="integer",
* pattern="\w+",
* )
* ),
* @OA\Response(
* response="200",
* description="Returned when successful"
* )
* )
*
* @Route("/api/tags/{tag}.{_format}", methods={"DELETE"}, name="api_delete_tag", defaults={"_format": "json"})
*
* @return JsonResponse
*/
public function deleteTagAction(Tag $tag, TagRepository $tagRepository, EntryRepository $entryRepository)
{
$this->validateAuthentication();
$tagFromDb = $tagRepository->findByLabelsAndUser([$tag->getLabel()], $this->getUser()->getId());
if (empty($tagFromDb)) {
throw $this->createNotFoundException('Tag not found');
}
$entryRepository->removeTag($this->getUser()->getId(), $tag);
$this->cleanOrphanTag($tag);
$json = $this->serializer->serialize($tag, 'json');
return (new JsonResponse())->setJson($json);
}
/**
* Remove orphan tag in case no entries are associated to it.
*
* @param Tag|array $tags
*/
private function cleanOrphanTag($tags)
{
if (!\is_array($tags)) {
$tags = [$tags];
}
foreach ($tags as $tag) {
if (0 === \count($tag->getEntries())) {
$this->entityManager->remove($tag);
}
}
$this->entityManager->flush();
}
}

View file

@ -0,0 +1,50 @@
<?php
namespace Wallabag\CoreBundle\Controller\Api;
use JMS\Serializer\SerializationContext;
use JMS\Serializer\SerializerBuilder;
use Nelmio\ApiDocBundle\Annotation\Operation;
use OpenApi\Annotations as OA;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class TaggingRuleRestController extends WallabagRestController
{
/**
* Export all tagging rules as a json file.
*
* @Operation(
* tags={"TaggingRule"},
* summary="Export all tagging rules as a json file.",
* @OA\Response(
* response="200",
* description="Returned when successful"
* )
* )
*
* @Route("/api/taggingrule/export.{_format}", methods={"GET"}, name="api_get_taggingrule_export", defaults={"_format": "json"})
*
* @return Response
*/
public function getTaggingruleExportAction()
{
$this->validateAuthentication();
$data = SerializerBuilder::create()->build()->serialize(
$this->getUser()->getConfig()->getTaggingRules(),
'json',
SerializationContext::create()->setGroups(['export_tagging_rule'])
);
return Response::create(
$data,
200,
[
'Content-type' => 'application/json',
'Content-Disposition' => 'attachment; filename="tagging_rules_' . $this->getUser()->getUsername() . '.json"',
'Content-Transfer-Encoding' => 'UTF-8',
]
);
}
}

View file

@ -0,0 +1,214 @@
<?php
namespace Wallabag\CoreBundle\Controller\Api;
use Craue\ConfigBundle\Util\Config;
use Doctrine\ORM\EntityManagerInterface;
use FOS\UserBundle\Event\UserEvent;
use FOS\UserBundle\FOSUserEvents;
use FOS\UserBundle\Model\UserManagerInterface;
use JMS\Serializer\SerializationContext;
use Nelmio\ApiDocBundle\Annotation\Model;
use Nelmio\ApiDocBundle\Annotation\Operation;
use OpenApi\Annotations as OA;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Wallabag\CoreBundle\Entity\Api\Client;
use Wallabag\CoreBundle\Entity\User;
use Wallabag\CoreBundle\Form\Type\NewUserType;
class UserRestController extends WallabagRestController
{
/**
* Retrieve current logged in user information.
*
* @Operation(
* tags={"User"},
* summary="Retrieve current logged in user information.",
* @OA\Response(
* response="200",
* description="Returned when successful",
* @Model(type=User::class, groups={"user_api"}))
* )
* )
*
* @Route("/api/user.{_format}", methods={"GET"}, name="api_get_user", defaults={"_format": "json"})
*
* @return JsonResponse
*/
public function getUserAction()
{
$this->validateAuthentication();
return $this->sendUser($this->getUser());
}
/**
* Register an user and create a client.
*
* @Operation(
* tags={"User"},
* summary="Register an user and create a client.",
* @OA\RequestBody(
* @OA\JsonContent(
* type="object",
* required={"username", "password", "email"},
* @OA\Property(
* property="username",
* description="The user's username",
* type="string",
* example="wallabag",
* ),
* @OA\Property(
* property="password",
* description="The user's password",
* type="string",
* example="hidden_value",
* ),
* @OA\Property(
* property="email",
* description="The user's email",
* type="string",
* example="wallabag@wallabag.io",
* ),
* @OA\Property(
* property="client_name",
* description="The client name (to be used by your app)",
* type="string",
* example="Fancy App",
* ),
* )
* ),
* @OA\Response(
* response="201",
* description="Returned when successful",
* @Model(type=User::class, groups={"user_api_with_client"})),
* ),
* @OA\Response(
* response="403",
* description="Server doesn't allow registrations"
* ),
* @OA\Response(
* response="400",
* description="Request is incorrectly formatted"
* )
* )
*
* @todo Make this method (or the whole API) accessible only through https
*
* @Route("/api/user.{_format}", methods={"PUT"}, name="api_put_user", defaults={"_format": "json"})
*
* @return JsonResponse
*/
public function putUserAction(Request $request, Config $craueConfig, UserManagerInterface $userManager, EntityManagerInterface $entityManager, EventDispatcherInterface $eventDispatcher)
{
if (!$this->getParameter('fosuser_registration') || !$craueConfig->get('api_user_registration')) {
$json = $this->serializer->serialize(['error' => "Server doesn't allow registrations"], 'json');
return (new JsonResponse())
->setJson($json)
->setStatusCode(JsonResponse::HTTP_FORBIDDEN);
}
$user = $userManager->createUser();
\assert($user instanceof User);
// user will be disabled BY DEFAULT to avoid spamming account to be enabled
$user->setEnabled(false);
$form = $this->createForm(NewUserType::class, $user, [
'csrf_protection' => false,
]);
// simulate form submission
$form->submit([
'username' => $request->request->get('username'),
'plainPassword' => [
'first' => $request->request->get('password'),
'second' => $request->request->get('password'),
],
'email' => $request->request->get('email'),
]);
if ($form->isSubmitted() && false === $form->isValid()) {
$view = $this->view($form, 400);
$view->setFormat('json');
// handle errors in a more beautiful way than the default view
$data = json_decode($this->handleView($view)->getContent(), true)['errors']['children'];
$errors = [];
if (isset($data['username']['errors'])) {
$errors['username'] = $this->translateErrors($data['username']['errors']);
}
if (isset($data['email']['errors'])) {
$errors['email'] = $this->translateErrors($data['email']['errors']);
}
if (isset($data['plainPassword']['children']['first']['errors'])) {
$errors['password'] = $this->translateErrors($data['plainPassword']['children']['first']['errors']);
}
$json = $this->serializer->serialize(['error' => $errors], 'json');
return (new JsonResponse())
->setJson($json)
->setStatusCode(JsonResponse::HTTP_BAD_REQUEST);
}
// create a default client
$client = new Client($user);
$client->setName($request->request->get('client_name', 'Default client'));
$entityManager->persist($client);
$user->addClient($client);
$userManager->updateUser($user);
// dispatch a created event so the associated config will be created
$eventDispatcher->dispatch(new UserEvent($user, $request), FOSUserEvents::USER_CREATED);
return $this->sendUser($user, 'user_api_with_client', JsonResponse::HTTP_CREATED);
}
/**
* Send user response.
*
* @param string $group Used to define with serialized group might be used
* @param int $status HTTP Status code to send
*
* @return JsonResponse
*/
private function sendUser(User $user, $group = 'user_api', $status = JsonResponse::HTTP_OK)
{
$json = $this->serializer->serialize(
$user,
'json',
SerializationContext::create()->setGroups([$group])
);
return (new JsonResponse())
->setJson($json)
->setStatusCode($status);
}
/**
* Translate errors message.
*
* @param array $errors
*
* @return array
*/
private function translateErrors($errors)
{
$translatedErrors = [];
foreach ($errors as $error) {
$translatedErrors[] = $this->translator->trans($error);
}
return $translatedErrors;
}
}

View file

@ -0,0 +1,144 @@
<?php
namespace Wallabag\CoreBundle\Controller\Api;
use Craue\ConfigBundle\Util\Config;
use Doctrine\ORM\EntityManagerInterface;
use FOS\RestBundle\Controller\AbstractFOSRestController;
use JMS\Serializer\SerializationContext;
use JMS\Serializer\SerializerInterface;
use Nelmio\ApiDocBundle\Annotation\Model;
use Nelmio\ApiDocBundle\Annotation\Operation;
use OpenApi\Annotations as OA;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Contracts\Translation\TranslatorInterface;
use Wallabag\CoreBundle\Entity\Api\ApplicationInfo;
use Wallabag\CoreBundle\Entity\User;
class WallabagRestController extends AbstractFOSRestController
{
protected EntityManagerInterface $entityManager;
protected SerializerInterface $serializer;
protected AuthorizationCheckerInterface $authorizationChecker;
protected TokenStorageInterface $tokenStorage;
protected TranslatorInterface $translator;
public function __construct(EntityManagerInterface $entityManager, SerializerInterface $serializer, AuthorizationCheckerInterface $authorizationChecker, TokenStorageInterface $tokenStorage, TranslatorInterface $translator)
{
$this->entityManager = $entityManager;
$this->serializer = $serializer;
$this->authorizationChecker = $authorizationChecker;
$this->tokenStorage = $tokenStorage;
$this->translator = $translator;
}
/**
* Retrieve version number.
*
* @Operation(
* tags={"Information"},
* summary="Retrieve version number.",
* @OA\Response(
* response="200",
* description="Returned when successful",
* @OA\JsonContent(
* description="Version number of the application.",
* type="string",
* example="2.5.2",
* )
* )
* )
*
* @deprecated Should use info endpoint instead
*
* @Route("/api/version.{_format}", methods={"GET"}, name="api_get_version", defaults={"_format": "json"})
*
* @return JsonResponse
*/
public function getVersionAction()
{
$version = $this->getParameter('wallabag_core.version');
$json = $this->serializer->serialize($version, 'json');
return (new JsonResponse())->setJson($json);
}
/**
* @Operation(
* tags={"Information"},
* summary="Retrieve information about the running wallabag application.",
* @OA\Response(
* response="200",
* description="Returned when successful",
* @Model(type=ApplicationInfo::class),
* )
* )
*
* @Route("/api/info.{_format}", methods={"GET"}, name="api_get_info", defaults={"_format": "json"})
*
* @return JsonResponse
*/
public function getInfoAction(Config $craueConfig)
{
$info = new ApplicationInfo(
$this->getParameter('wallabag_core.version'),
$this->getParameter('fosuser_registration') && $craueConfig->get('api_user_registration'),
);
return (new JsonResponse())->setJson($this->serializer->serialize($info, 'json'));
}
protected function validateAuthentication()
{
if (false === $this->authorizationChecker->isGranted('IS_AUTHENTICATED_FULLY')) {
throw new AccessDeniedException();
}
}
/**
* Validate that the first id is equal to the second one.
* If not, throw exception. It means a user try to access information from an other user.
*
* @param int $requestUserId User id from the requested source
*/
protected function validateUserAccess($requestUserId)
{
$user = $this->tokenStorage->getToken()->getUser();
\assert($user instanceof User);
if ($requestUserId !== $user->getId()) {
throw $this->createAccessDeniedException('Access forbidden. Entry user id: ' . $requestUserId . ', logged user id: ' . $user->getId());
}
}
/**
* Shortcut to send data serialized in json.
*
* @return JsonResponse
*/
protected function sendResponse($data)
{
// https://github.com/schmittjoh/JMSSerializerBundle/issues/293
$context = new SerializationContext();
$context->setSerializeNull(true);
$json = $this->serializer->serialize($data, 'json', $context);
return (new JsonResponse())->setJson($json);
}
/**
* @return User|null
*/
protected function getUser()
{
$user = parent::getUser();
\assert(null === $user || $user instanceof User);
return $user;
}
}

View file

@ -0,0 +1,796 @@
<?php
namespace Wallabag\CoreBundle\Controller;
use Craue\ConfigBundle\Util\Config;
use Doctrine\DBAL\Platforms\SqlitePlatform;
use Doctrine\ORM\EntityManagerInterface;
use FOS\UserBundle\Model\UserManagerInterface;
use JMS\Serializer\SerializationContext;
use JMS\Serializer\SerializerBuilder;
use PragmaRX\Recovery\Recovery as BackupCodes;
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Google\GoogleAuthenticatorInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Validator\Constraints\Locale as LocaleConstraint;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Wallabag\CoreBundle\Entity\Config as ConfigEntity;
use Wallabag\CoreBundle\Entity\IgnoreOriginUserRule;
use Wallabag\CoreBundle\Entity\RuleInterface;
use Wallabag\CoreBundle\Entity\TaggingRule;
use Wallabag\CoreBundle\Event\ConfigUpdatedEvent;
use Wallabag\CoreBundle\Form\Type\ChangePasswordType;
use Wallabag\CoreBundle\Form\Type\ConfigType;
use Wallabag\CoreBundle\Form\Type\FeedType;
use Wallabag\CoreBundle\Form\Type\IgnoreOriginUserRuleType;
use Wallabag\CoreBundle\Form\Type\TaggingRuleImportType;
use Wallabag\CoreBundle\Form\Type\TaggingRuleType;
use Wallabag\CoreBundle\Form\Type\UserInformationType;
use Wallabag\CoreBundle\Helper\Redirect;
use Wallabag\CoreBundle\Repository\AnnotationRepository;
use Wallabag\CoreBundle\Repository\ConfigRepository;
use Wallabag\CoreBundle\Repository\EntryRepository;
use Wallabag\CoreBundle\Repository\IgnoreOriginUserRuleRepository;
use Wallabag\CoreBundle\Repository\TaggingRuleRepository;
use Wallabag\CoreBundle\Repository\TagRepository;
use Wallabag\CoreBundle\Repository\UserRepository;
use Wallabag\CoreBundle\Tools\Utils;
class ConfigController extends AbstractController
{
private EntityManagerInterface $entityManager;
private UserManagerInterface $userManager;
private EntryRepository $entryRepository;
private TagRepository $tagRepository;
private AnnotationRepository $annotationRepository;
private ConfigRepository $configRepository;
private EventDispatcherInterface $eventDispatcher;
private Redirect $redirectHelper;
public function __construct(
EntityManagerInterface $entityManager,
UserManagerInterface $userManager,
EntryRepository $entryRepository,
TagRepository $tagRepository,
AnnotationRepository $annotationRepository,
ConfigRepository $configRepository,
EventDispatcherInterface $eventDispatcher,
Redirect $redirectHelper
) {
$this->entityManager = $entityManager;
$this->userManager = $userManager;
$this->entryRepository = $entryRepository;
$this->tagRepository = $tagRepository;
$this->annotationRepository = $annotationRepository;
$this->configRepository = $configRepository;
$this->eventDispatcher = $eventDispatcher;
$this->redirectHelper = $redirectHelper;
}
/**
* @Route("/config", name="config")
*/
public function indexAction(Request $request, Config $craueConfig, TaggingRuleRepository $taggingRuleRepository, IgnoreOriginUserRuleRepository $ignoreOriginUserRuleRepository, UserRepository $userRepository)
{
$config = $this->getConfig();
$user = $this->getUser();
// handle basic config detail (this form is defined as a service)
$configForm = $this->createForm(ConfigType::class, $config, ['action' => $this->generateUrl('config')]);
$configForm->handleRequest($request);
if ($configForm->isSubmitted() && $configForm->isValid()) {
$this->eventDispatcher->dispatch(new ConfigUpdatedEvent($config), ConfigUpdatedEvent::NAME);
$this->entityManager->persist($config);
$this->entityManager->flush();
$request->getSession()->set('_locale', $config->getLanguage());
$this->addFlash(
'notice',
'flashes.config.notice.config_saved'
);
return $this->redirect($this->generateUrl('config'));
}
// handle changing password
$pwdForm = $this->createForm(ChangePasswordType::class, null, ['action' => $this->generateUrl('config') . '#set4']);
$pwdForm->handleRequest($request);
if ($pwdForm->isSubmitted() && $pwdForm->isValid()) {
$message = 'flashes.config.notice.password_updated';
$user->setPlainPassword($pwdForm->get('new_password')->getData());
$this->userManager->updateUser($user, true);
$this->addFlash('notice', $message);
return $this->redirect($this->generateUrl('config') . '#set4');
}
// handle changing user information
$userForm = $this->createForm(UserInformationType::class, $user, [
'validation_groups' => ['Profile'],
'action' => $this->generateUrl('config') . '#set3',
]);
$userForm->handleRequest($request);
if ($userForm->isSubmitted() && $userForm->isValid()) {
$this->userManager->updateUser($user, true);
$this->addFlash(
'notice',
'flashes.config.notice.user_updated'
);
return $this->redirect($this->generateUrl('config') . '#set3');
}
// handle feed information
$feedForm = $this->createForm(FeedType::class, $config, ['action' => $this->generateUrl('config') . '#set2']);
$feedForm->handleRequest($request);
if ($feedForm->isSubmitted() && $feedForm->isValid()) {
$this->entityManager->persist($config);
$this->entityManager->flush();
$this->addFlash(
'notice',
'flashes.config.notice.feed_updated'
);
return $this->redirect($this->generateUrl('config') . '#set2');
}
// handle tagging rule
$taggingRule = new TaggingRule();
$action = $this->generateUrl('config') . '#set5';
if ($request->query->has('tagging-rule')) {
$taggingRule = $taggingRuleRepository->find($request->query->get('tagging-rule'));
if ($this->getUser()->getId() !== $taggingRule->getConfig()->getUser()->getId()) {
return $this->redirect($action);
}
$action = $this->generateUrl('config') . '?tagging-rule=' . $taggingRule->getId() . '#set5';
}
$newTaggingRule = $this->createForm(TaggingRuleType::class, $taggingRule, ['action' => $action]);
$newTaggingRule->handleRequest($request);
if ($newTaggingRule->isSubmitted() && $newTaggingRule->isValid()) {
$taggingRule->setConfig($config);
$this->entityManager->persist($taggingRule);
$this->entityManager->flush();
$this->addFlash(
'notice',
'flashes.config.notice.tagging_rules_updated'
);
return $this->redirect($this->generateUrl('config') . '#set5');
}
// handle tagging rules import
$taggingRulesImportform = $this->createForm(TaggingRuleImportType::class);
$taggingRulesImportform->handleRequest($request);
if ($taggingRulesImportform->isSubmitted() && $taggingRulesImportform->isValid()) {
$message = 'flashes.config.notice.tagging_rules_not_imported';
$file = $taggingRulesImportform->get('file')->getData();
if (null !== $file && $file->isValid() && \in_array($file->getClientMimeType(), ['application/json', 'application/octet-stream'], true)) {
$content = json_decode(file_get_contents($file->getPathname()), true);
if (\is_array($content)) {
foreach ($content as $rule) {
$taggingRule = new TaggingRule();
$taggingRule->setRule($rule['rule']);
$taggingRule->setTags($rule['tags']);
$taggingRule->setConfig($config);
$this->entityManager->persist($taggingRule);
}
$this->entityManager->flush();
$message = 'flashes.config.notice.tagging_rules_imported';
}
}
$this->addFlash('notice', $message);
return $this->redirect($this->generateUrl('config') . '#set5');
}
// handle ignore origin rules
$ignoreOriginUserRule = new IgnoreOriginUserRule();
$action = $this->generateUrl('config') . '#set6';
if ($request->query->has('ignore-origin-user-rule')) {
$ignoreOriginUserRule = $ignoreOriginUserRuleRepository
->find($request->query->get('ignore-origin-user-rule'));
if ($this->getUser()->getId() !== $ignoreOriginUserRule->getConfig()->getUser()->getId()) {
return $this->redirect($action);
}
$action = $this->generateUrl('config', [
'ignore-origin-user-rule' => $ignoreOriginUserRule->getId(),
]) . '#set6';
}
$newIgnoreOriginUserRule = $this->createForm(IgnoreOriginUserRuleType::class, $ignoreOriginUserRule, ['action' => $action]);
$newIgnoreOriginUserRule->handleRequest($request);
if ($newIgnoreOriginUserRule->isSubmitted() && $newIgnoreOriginUserRule->isValid()) {
$ignoreOriginUserRule->setConfig($config);
$this->entityManager->persist($ignoreOriginUserRule);
$this->entityManager->flush();
$this->addFlash(
'notice',
'flashes.config.notice.ignore_origin_rules_updated'
);
return $this->redirect($this->generateUrl('config') . '#set6');
}
return $this->render('Config/index.html.twig', [
'form' => [
'config' => $configForm->createView(),
'feed' => $feedForm->createView(),
'pwd' => $pwdForm->createView(),
'user' => $userForm->createView(),
'new_tagging_rule' => $newTaggingRule->createView(),
'import_tagging_rule' => $taggingRulesImportform->createView(),
'new_ignore_origin_user_rule' => $newIgnoreOriginUserRule->createView(),
],
'feed' => [
'username' => $user->getUsername(),
'token' => $config->getFeedToken(),
],
'wallabag_url' => $this->getParameter('domain_name'),
'enabled_users' => $userRepository->getSumEnabledUsers(),
]);
}
/**
* Disable 2FA using email.
*
* @Route("/config/otp/email/disable", name="disable_otp_email", methods={"POST"})
*/
public function disableOtpEmailAction(Request $request)
{
if (!$this->isCsrfTokenValid('otp', $request->request->get('token'))) {
throw $this->createAccessDeniedException('Bad CSRF token.');
}
$user = $this->getUser();
$user->setEmailTwoFactor(false);
$this->userManager->updateUser($user, true);
$this->addFlash(
'notice',
'flashes.config.notice.otp_disabled'
);
return $this->redirect($this->generateUrl('config') . '#set3');
}
/**
* Enable 2FA using email.
*
* @Route("/config/otp/email", name="config_otp_email", methods={"POST"})
*/
public function otpEmailAction(Request $request)
{
if (!$this->isCsrfTokenValid('otp', $request->request->get('token'))) {
throw $this->createAccessDeniedException('Bad CSRF token.');
}
$user = $this->getUser();
$user->setGoogleAuthenticatorSecret(null);
$user->setBackupCodes(null);
$user->setEmailTwoFactor(true);
$this->userManager->updateUser($user, true);
$this->addFlash(
'notice',
'flashes.config.notice.otp_enabled'
);
return $this->redirect($this->generateUrl('config') . '#set3');
}
/**
* Disable 2FA using OTP app.
*
* @Route("/config/otp/app/disable", name="disable_otp_app", methods={"POST"})
*/
public function disableOtpAppAction(Request $request)
{
if (!$this->isCsrfTokenValid('otp', $request->request->get('token'))) {
throw $this->createAccessDeniedException('Bad CSRF token.');
}
$user = $this->getUser();
$user->setGoogleAuthenticatorSecret('');
$user->setBackupCodes(null);
$this->userManager->updateUser($user, true);
$this->addFlash(
'notice',
'flashes.config.notice.otp_disabled'
);
return $this->redirect($this->generateUrl('config') . '#set3');
}
/**
* Enable 2FA using OTP app, user will need to confirm the generated code from the app.
*
* @Route("/config/otp/app", name="config_otp_app", methods={"POST"})
*/
public function otpAppAction(Request $request, GoogleAuthenticatorInterface $googleAuthenticator)
{
if (!$this->isCsrfTokenValid('otp', $request->request->get('token'))) {
throw $this->createAccessDeniedException('Bad CSRF token.');
}
$user = $this->getUser();
$secret = $googleAuthenticator->generateSecret();
$user->setGoogleAuthenticatorSecret($secret);
$user->setEmailTwoFactor(false);
$backupCodes = (new BackupCodes())->toArray();
$backupCodesHashed = array_map(
function ($backupCode) {
return password_hash($backupCode, \PASSWORD_DEFAULT);
},
$backupCodes
);
$user->setBackupCodes($backupCodesHashed);
$this->userManager->updateUser($user, true);
$this->addFlash(
'notice',
'flashes.config.notice.otp_enabled'
);
return $this->render('Config/otp_app.html.twig', [
'backupCodes' => $backupCodes,
'qr_code' => $googleAuthenticator->getQRContent($user),
'secret' => $secret,
]);
}
/**
* Cancelling 2FA using OTP app.
*
* @Route("/config/otp/app/cancel", name="config_otp_app_cancel")
*
* XXX: commented until we rewrite 2fa with a real two-steps activation
*/
/*public function otpAppCancelAction()
{
$user = $this->getUser();
$user->setGoogleAuthenticatorSecret(null);
$user->setBackupCodes(null);
$this->userManager->updateUser($user, true);
return $this->redirect($this->generateUrl('config') . '#set3');
}*/
/**
* Validate OTP code.
*
* @Route("/config/otp/app/check", name="config_otp_app_check", methods={"POST"})
*/
public function otpAppCheckAction(Request $request, GoogleAuthenticatorInterface $googleAuthenticator)
{
if (!$this->isCsrfTokenValid('otp', $request->request->get('token'))) {
throw $this->createAccessDeniedException('Bad CSRF token.');
}
$isValid = $googleAuthenticator->checkCode(
$this->getUser(),
$request->get('_auth_code')
);
if (true === $isValid) {
$this->addFlash(
'notice',
'flashes.config.notice.otp_enabled'
);
return $this->redirect($this->generateUrl('config') . '#set3');
}
$this->addFlash(
'two_factor',
'scheb_two_factor.code_invalid'
);
$this->addFlash(
'notice',
'scheb_two_factor.code_invalid'
);
return $this->redirect($this->generateUrl('config') . '#set3');
}
/**
* @Route("/generate-token", name="generate_token")
*
* @return RedirectResponse|JsonResponse
*/
public function generateTokenAction(Request $request)
{
$config = $this->getConfig();
$config->setFeedToken(Utils::generateToken());
$this->entityManager->persist($config);
$this->entityManager->flush();
if ($request->isXmlHttpRequest()) {
return new JsonResponse(['token' => $config->getFeedToken()]);
}
$this->addFlash(
'notice',
'flashes.config.notice.feed_token_updated'
);
return $this->redirect($this->generateUrl('config') . '#set2');
}
/**
* @Route("/revoke-token", name="revoke_token")
*
* @return RedirectResponse|JsonResponse
*/
public function revokeTokenAction(Request $request)
{
$config = $this->getConfig();
$config->setFeedToken(null);
$this->entityManager->persist($config);
$this->entityManager->flush();
if ($request->isXmlHttpRequest()) {
return new JsonResponse();
}
$this->addFlash(
'notice',
'flashes.config.notice.feed_token_revoked'
);
return $this->redirect($this->generateUrl('config') . '#set2');
}
/**
* Deletes a tagging rule and redirect to the config homepage.
*
* @Route("/tagging-rule/delete/{id}", requirements={"id" = "\d+"}, name="delete_tagging_rule")
*
* @return RedirectResponse
*/
public function deleteTaggingRuleAction(TaggingRule $rule)
{
$this->validateRuleAction($rule);
$this->entityManager->remove($rule);
$this->entityManager->flush();
$this->addFlash(
'notice',
'flashes.config.notice.tagging_rules_deleted'
);
return $this->redirect($this->generateUrl('config') . '#set5');
}
/**
* Edit a tagging rule.
*
* @Route("/tagging-rule/edit/{id}", requirements={"id" = "\d+"}, name="edit_tagging_rule")
*
* @return RedirectResponse
*/
public function editTaggingRuleAction(TaggingRule $rule)
{
$this->validateRuleAction($rule);
return $this->redirect($this->generateUrl('config') . '?tagging-rule=' . $rule->getId() . '#set5');
}
/**
* Deletes an ignore origin rule and redirect to the config homepage.
*
* @Route("/ignore-origin-user-rule/delete/{id}", requirements={"id" = "\d+"}, name="delete_ignore_origin_rule")
*
* @return RedirectResponse
*/
public function deleteIgnoreOriginRuleAction(IgnoreOriginUserRule $rule)
{
$this->validateRuleAction($rule);
$this->entityManager->remove($rule);
$this->entityManager->flush();
$this->addFlash(
'notice',
'flashes.config.notice.ignore_origin_rules_deleted'
);
return $this->redirect($this->generateUrl('config') . '#set6');
}
/**
* Edit an ignore origin rule.
*
* @Route("/ignore-origin-user-rule/edit/{id}", requirements={"id" = "\d+"}, name="edit_ignore_origin_rule")
*
* @return RedirectResponse
*/
public function editIgnoreOriginRuleAction(IgnoreOriginUserRule $rule)
{
$this->validateRuleAction($rule);
return $this->redirect($this->generateUrl('config') . '?ignore-origin-user-rule=' . $rule->getId() . '#set6');
}
/**
* Remove all annotations OR tags OR entries for the current user.
*
* @Route("/reset/{type}", requirements={"id" = "annotations|tags|entries"}, name="config_reset", methods={"POST"})
*
* @return RedirectResponse
*/
public function resetAction(Request $request, string $type, AnnotationRepository $annotationRepository, EntryRepository $entryRepository)
{
if (!$this->isCsrfTokenValid('reset-area', $request->request->get('token'))) {
throw $this->createAccessDeniedException('Bad CSRF token.');
}
switch ($type) {
case 'annotations':
$annotationRepository->removeAllByUserId($this->getUser()->getId());
break;
case 'tags':
$this->removeAllTagsByUserId($this->getUser()->getId());
break;
case 'entries':
// SQLite doesn't care about cascading remove, so we need to manually remove associated stuff
// otherwise they won't be removed ...
if ($this->entityManager->getConnection()->getDatabasePlatform() instanceof SqlitePlatform) {
$annotationRepository->removeAllByUserId($this->getUser()->getId());
}
// manually remove tags to avoid orphan tag
$this->removeAllTagsByUserId($this->getUser()->getId());
$entryRepository->removeAllByUserId($this->getUser()->getId());
break;
case 'archived':
if ($this->entityManager->getConnection()->getDatabasePlatform() instanceof SqlitePlatform) {
$this->removeAnnotationsForArchivedByUserId($this->getUser()->getId());
}
// manually remove tags to avoid orphan tag
$this->removeTagsForArchivedByUserId($this->getUser()->getId());
$entryRepository->removeArchivedByUserId($this->getUser()->getId());
break;
}
$this->addFlash(
'notice',
'flashes.config.notice.' . $type . '_reset'
);
return $this->redirect($this->generateUrl('config') . '#set3');
}
/**
* Delete account for current user.
*
* @Route("/account/delete", name="delete_account", methods={"POST"})
*
* @throws AccessDeniedHttpException
*
* @return RedirectResponse
*/
public function deleteAccountAction(Request $request, UserRepository $userRepository, TokenStorageInterface $tokenStorage)
{
if (!$this->isCsrfTokenValid('delete-account', $request->request->get('token'))) {
throw $this->createAccessDeniedException('Bad CSRF token.');
}
$enabledUsers = $userRepository->getSumEnabledUsers();
if ($enabledUsers <= 1) {
throw new AccessDeniedHttpException();
}
$user = $this->getUser();
// logout current user
$tokenStorage->setToken(null);
$request->getSession()->invalidate();
$this->userManager->deleteUser($user);
return $this->redirect($this->generateUrl('fos_user_security_login'));
}
/**
* Switch view mode for current user.
*
* @Route("/config/view-mode", name="switch_view_mode")
*
* @return RedirectResponse
*/
public function changeViewModeAction(Request $request)
{
$user = $this->getUser();
$user->getConfig()->setListMode(!$user->getConfig()->getListMode());
$this->entityManager->persist($user);
$this->entityManager->flush();
$redirectUrl = $this->redirectHelper->to($request->query->get('redirect'));
return $this->redirect($redirectUrl);
}
/**
* Change the locale for the current user.
*
* @param string $language
*
* @Route("/locale/{language}", name="changeLocale")
*
* @return RedirectResponse
*/
public function setLocaleAction(Request $request, ValidatorInterface $validator, $language = null)
{
$errors = $validator->validate($language, new LocaleConstraint(['canonicalize' => true]));
if (0 === \count($errors)) {
$request->getSession()->set('_locale', $language);
}
return $this->redirect($request->headers->get('referer', $this->generateUrl('homepage')));
}
/**
* Export tagging rules for the logged in user.
*
* @Route("/tagging-rule/export", name="export_tagging_rule")
*
* @return Response
*/
public function exportTaggingRulesAction()
{
$data = SerializerBuilder::create()->build()->serialize(
$this->getUser()->getConfig()->getTaggingRules(),
'json',
SerializationContext::create()->setGroups(['export_tagging_rule'])
);
return Response::create(
$data,
200,
[
'Content-type' => 'application/json',
'Content-Disposition' => 'attachment; filename="tagging_rules_' . $this->getUser()->getUsername() . '.json"',
'Content-Transfer-Encoding' => 'UTF-8',
]
);
}
/**
* Remove all tags for given tags and a given user and cleanup orphan tags.
*
* @param array $tags
* @param int $userId
*/
private function removeAllTagsByStatusAndUserId($tags, $userId)
{
if (empty($tags)) {
return;
}
$this->entryRepository->removeTags($userId, $tags);
// cleanup orphan tags
foreach ($tags as $tag) {
if (0 === \count($tag->getEntries())) {
$this->entityManager->remove($tag);
}
}
$this->entityManager->flush();
}
/**
* Remove all tags for a given user and cleanup orphan tags.
*
* @param int $userId
*/
private function removeAllTagsByUserId($userId)
{
$tags = $this->tagRepository->findAllTags($userId);
$this->removeAllTagsByStatusAndUserId($tags, $userId);
}
/**
* Remove all tags for a given user and cleanup orphan tags.
*
* @param int $userId
*/
private function removeTagsForArchivedByUserId($userId)
{
$tags = $this->tagRepository->findForArchivedArticlesByUser($userId);
$this->removeAllTagsByStatusAndUserId($tags, $userId);
}
private function removeAnnotationsForArchivedByUserId($userId)
{
$archivedEntriesAnnotations = $this->annotationRepository
->findAllArchivedEntriesByUser($userId);
foreach ($archivedEntriesAnnotations as $archivedEntriesAnnotation) {
$this->entityManager->remove($archivedEntriesAnnotation);
}
$this->entityManager->flush();
}
/**
* Validate that a rule can be edited/deleted by the current user.
*/
private function validateRuleAction(RuleInterface $rule)
{
if ($this->getUser()->getId() !== $rule->getConfig()->getUser()->getId()) {
throw $this->createAccessDeniedException('You can not access this rule.');
}
}
/**
* Retrieve config for the current user.
* If no config were found, create a new one.
*
* @return ConfigEntity
*/
private function getConfig()
{
$config = $this->configRepository->findOneByUser($this->getUser());
// should NEVER HAPPEN ...
if (!$config) {
$config = new ConfigEntity($this->getUser());
}
return $config;
}
}

View file

@ -0,0 +1,735 @@
<?php
namespace Wallabag\CoreBundle\Controller;
use Craue\ConfigBundle\Util\Config;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\NoResultException;
use Pagerfanta\Doctrine\ORM\QueryAdapter as DoctrineORMAdapter;
use Pagerfanta\Exception\OutOfRangeCurrentPageException;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
use Spiriit\Bundle\FormFilterBundle\Filter\FilterBuilderUpdaterInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\Translation\TranslatorInterface;
use Wallabag\CoreBundle\Entity\Entry;
use Wallabag\CoreBundle\Entity\Tag;
use Wallabag\CoreBundle\Event\EntryDeletedEvent;
use Wallabag\CoreBundle\Event\EntrySavedEvent;
use Wallabag\CoreBundle\Form\Type\EditEntryType;
use Wallabag\CoreBundle\Form\Type\EntryFilterType;
use Wallabag\CoreBundle\Form\Type\NewEntryType;
use Wallabag\CoreBundle\Form\Type\SearchEntryType;
use Wallabag\CoreBundle\Helper\ContentProxy;
use Wallabag\CoreBundle\Helper\PreparePagerForEntries;
use Wallabag\CoreBundle\Helper\Redirect;
use Wallabag\CoreBundle\Repository\EntryRepository;
use Wallabag\CoreBundle\Repository\TagRepository;
class EntryController extends AbstractController
{
private EntityManagerInterface $entityManager;
private EventDispatcherInterface $eventDispatcher;
private EntryRepository $entryRepository;
private Redirect $redirectHelper;
private PreparePagerForEntries $preparePagerForEntriesHelper;
private FilterBuilderUpdaterInterface $filterBuilderUpdater;
private ContentProxy $contentProxy;
public function __construct(EntityManagerInterface $entityManager, EventDispatcherInterface $eventDispatcher, EntryRepository $entryRepository, Redirect $redirectHelper, PreparePagerForEntries $preparePagerForEntriesHelper, FilterBuilderUpdaterInterface $filterBuilderUpdater, ContentProxy $contentProxy)
{
$this->entityManager = $entityManager;
$this->eventDispatcher = $eventDispatcher;
$this->entryRepository = $entryRepository;
$this->redirectHelper = $redirectHelper;
$this->preparePagerForEntriesHelper = $preparePagerForEntriesHelper;
$this->filterBuilderUpdater = $filterBuilderUpdater;
$this->contentProxy = $contentProxy;
}
/**
* @Route("/mass", name="mass_action")
*
* @return Response
*/
public function massAction(Request $request, TagRepository $tagRepository)
{
$values = $request->request->all();
$tagsToAdd = [];
$tagsToRemove = [];
$action = 'toggle-read';
if (isset($values['toggle-star'])) {
$action = 'toggle-star';
} elseif (isset($values['delete'])) {
$action = 'delete';
} elseif (isset($values['tag'])) {
$action = 'tag';
if (isset($values['tags'])) {
$labels = array_filter(explode(',', $values['tags']),
function ($v) {
$v = trim($v);
return '' !== $v;
});
foreach ($labels as $label) {
$remove = false;
if (str_starts_with($label, '-')) {
$label = substr($label, 1);
$remove = true;
}
$tag = $tagRepository->findOneByLabel($label);
if ($remove) {
if (null !== $tag) {
$tagsToRemove[] = $tag;
}
} else {
if (null === $tag) {
$tag = new Tag();
$tag->setLabel($label);
}
$tagsToAdd[] = $tag;
}
}
}
}
if (isset($values['entry-checkbox'])) {
foreach ($values['entry-checkbox'] as $id) {
/** @var Entry * */
$entry = $this->entryRepository->findById((int) $id)[0];
$this->checkUserAction($entry);
if ('toggle-read' === $action) {
$entry->toggleArchive();
} elseif ('toggle-star' === $action) {
$entry->toggleStar();
} elseif ('tag' === $action) {
foreach ($tagsToAdd as $tag) {
$entry->addTag($tag);
}
foreach ($tagsToRemove as $tag) {
$entry->removeTag($tag);
}
} elseif ('delete' === $action) {
$this->eventDispatcher->dispatch(new EntryDeletedEvent($entry), EntryDeletedEvent::NAME);
$this->entityManager->remove($entry);
}
}
$this->entityManager->flush();
}
$redirectUrl = $this->redirectHelper->to($request->query->get('redirect'));
return $this->redirect($redirectUrl);
}
/**
* @param int $page
*
* @Route("/search/{page}", name="search", defaults={"page" = 1})
*
* Default parameter for page is hardcoded (in duplication of the defaults from the Route)
* because this controller is also called inside the layout template without any page as argument
*
* @return Response
*/
public function searchFormAction(Request $request, $page = 1, $currentRoute = null)
{
// fallback to retrieve currentRoute from query parameter instead of injected one (when using inside a template)
if (null === $currentRoute && $request->query->has('currentRoute')) {
$currentRoute = $request->query->get('currentRoute');
}
$form = $this->createForm(SearchEntryType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
return $this->showEntries('search', $request, $page);
}
return $this->render('Entry/search_form.html.twig', [
'form' => $form->createView(),
'currentRoute' => $currentRoute,
]);
}
/**
* @Route("/new-entry", name="new_entry")
*
* @return Response
*/
public function addEntryFormAction(Request $request, TranslatorInterface $translator)
{
$entry = new Entry($this->getUser());
$form = $this->createForm(NewEntryType::class, $entry);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$existingEntry = $this->checkIfEntryAlreadyExists($entry);
if (false !== $existingEntry) {
$this->addFlash(
'notice',
$translator->trans('flashes.entry.notice.entry_already_saved', ['%date%' => $existingEntry->getCreatedAt()->format('d-m-Y')])
);
return $this->redirect($this->generateUrl('view', ['id' => $existingEntry->getId()]));
}
$this->updateEntry($entry);
$this->entityManager->persist($entry);
$this->entityManager->flush();
// entry saved, dispatch event about it!
$this->eventDispatcher->dispatch(new EntrySavedEvent($entry), EntrySavedEvent::NAME);
return $this->redirect($this->generateUrl('homepage'));
}
return $this->render('Entry/new_form.html.twig', [
'form' => $form->createView(),
]);
}
/**
* @Route("/bookmarklet", name="bookmarklet")
*
* @return Response
*/
public function addEntryViaBookmarkletAction(Request $request)
{
$entry = new Entry($this->getUser());
$entry->setUrl($request->get('url'));
if (false === $this->checkIfEntryAlreadyExists($entry)) {
$this->updateEntry($entry);
$this->entityManager->persist($entry);
$this->entityManager->flush();
// entry saved, dispatch event about it!
$this->eventDispatcher->dispatch(new EntrySavedEvent($entry), EntrySavedEvent::NAME);
}
return $this->redirect($this->generateUrl('homepage'));
}
/**
* @Route("/new", name="new")
*
* @return Response
*/
public function addEntryAction()
{
return $this->render('Entry/new.html.twig');
}
/**
* Edit an entry content.
*
* @Route("/edit/{id}", requirements={"id" = "\d+"}, name="edit")
*
* @return Response
*/
public function editEntryAction(Request $request, Entry $entry)
{
$this->checkUserAction($entry);
$form = $this->createForm(EditEntryType::class, $entry);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->entityManager->persist($entry);
$this->entityManager->flush();
$this->addFlash(
'notice',
'flashes.entry.notice.entry_updated'
);
return $this->redirect($this->generateUrl('view', ['id' => $entry->getId()]));
}
return $this->render('Entry/edit.html.twig', [
'form' => $form->createView(),
]);
}
/**
* Shows all entries for current user.
*
* @param int $page
*
* @Route("/all/list/{page}", name="all", defaults={"page" = "1"})
*
* @return Response
*/
public function showAllAction(Request $request, $page)
{
return $this->showEntries('all', $request, $page);
}
/**
* Shows unread entries for current user.
*
* @param int $page
*
* @Route("/unread/list/{page}", name="unread", defaults={"page" = "1"})
*
* @return Response
*/
public function showUnreadAction(Request $request, $page)
{
// load the quickstart if no entry in database
if (1 === (int) $page && 0 === $this->entryRepository->countAllEntriesByUser($this->getUser()->getId())) {
return $this->redirect($this->generateUrl('quickstart'));
}
return $this->showEntries('unread', $request, $page);
}
/**
* Shows read entries for current user.
*
* @param int $page
*
* @Route("/archive/list/{page}", name="archive", defaults={"page" = "1"})
*
* @return Response
*/
public function showArchiveAction(Request $request, $page)
{
return $this->showEntries('archive', $request, $page);
}
/**
* Shows starred entries for current user.
*
* @param int $page
*
* @Route("/starred/list/{page}", name="starred", defaults={"page" = "1"})
*
* @return Response
*/
public function showStarredAction(Request $request, $page)
{
return $this->showEntries('starred', $request, $page);
}
/**
* Shows untagged articles for current user.
*
* @param int $page
*
* @Route("/untagged/list/{page}", name="untagged", defaults={"page" = "1"})
*
* @return Response
*/
public function showUntaggedEntriesAction(Request $request, $page)
{
return $this->showEntries('untagged', $request, $page);
}
/**
* Shows entries with annotations for current user.
*
* @param int $page
*
* @Route("/annotated/list/{page}", name="annotated", defaults={"page" = "1"})
*
* @return Response
*/
public function showWithAnnotationsEntriesAction(Request $request, $page)
{
return $this->showEntries('annotated', $request, $page);
}
/**
* Shows random entry depending on the given type.
*
* @Route("/{type}/random", name="random_entry", requirements={"type": "unread|starred|archive|untagged|annotated|all"})
*
* @return RedirectResponse
*/
public function redirectRandomEntryAction(string $type = 'all')
{
try {
$entry = $this->entryRepository
->getRandomEntry($this->getUser()->getId(), $type);
} catch (NoResultException $e) {
$this->addFlash('notice', 'flashes.entry.notice.no_random_entry');
return $this->redirect($this->generateUrl($type));
}
return $this->redirect($this->generateUrl('view', ['id' => $entry->getId()]));
}
/**
* Shows entry content.
*
* @Route("/view/{id}", requirements={"id" = "\d+"}, name="view")
*
* @return Response
*/
public function viewAction(Entry $entry)
{
$this->checkUserAction($entry);
return $this->render(
'Entry/entry.html.twig',
['entry' => $entry]
);
}
/**
* Reload an entry.
* Refetch content from the website and make it readable again.
*
* @Route("/reload/{id}", requirements={"id" = "\d+"}, name="reload_entry")
*
* @return RedirectResponse
*/
public function reloadAction(Entry $entry)
{
$this->checkUserAction($entry);
$this->updateEntry($entry, 'entry_reloaded');
// if refreshing entry failed, don't save it
if ($this->getParameter('wallabag_core.fetching_error_message') === $entry->getContent()) {
$this->addFlash('notice', 'flashes.entry.notice.entry_reloaded_failed');
return $this->redirect($this->generateUrl('view', ['id' => $entry->getId()]));
}
$this->entityManager->persist($entry);
$this->entityManager->flush();
// entry saved, dispatch event about it!
$this->eventDispatcher->dispatch(new EntrySavedEvent($entry), EntrySavedEvent::NAME);
return $this->redirect($this->generateUrl('view', ['id' => $entry->getId()]));
}
/**
* Changes read status for an entry.
*
* @Route("/archive/{id}", requirements={"id" = "\d+"}, name="archive_entry")
*
* @return RedirectResponse
*/
public function toggleArchiveAction(Request $request, Entry $entry)
{
$this->checkUserAction($entry);
$entry->toggleArchive();
$this->entityManager->flush();
$message = 'flashes.entry.notice.entry_unarchived';
if ($entry->isArchived()) {
$message = 'flashes.entry.notice.entry_archived';
}
$this->addFlash(
'notice',
$message
);
$redirectUrl = $this->redirectHelper->to($request->query->get('redirect'));
return $this->redirect($redirectUrl);
}
/**
* Changes starred status for an entry.
*
* @Route("/star/{id}", requirements={"id" = "\d+"}, name="star_entry")
*
* @return RedirectResponse
*/
public function toggleStarAction(Request $request, Entry $entry)
{
$this->checkUserAction($entry);
$entry->toggleStar();
$entry->updateStar($entry->isStarred());
$this->entityManager->flush();
$message = 'flashes.entry.notice.entry_unstarred';
if ($entry->isStarred()) {
$message = 'flashes.entry.notice.entry_starred';
}
$this->addFlash(
'notice',
$message
);
$redirectUrl = $this->redirectHelper->to($request->query->get('redirect'));
return $this->redirect($redirectUrl);
}
/**
* Deletes entry and redirect to the homepage or the last viewed page.
*
* @Route("/delete/{id}", requirements={"id" = "\d+"}, name="delete_entry")
*
* @return RedirectResponse
*/
public function deleteEntryAction(Request $request, Entry $entry)
{
$this->checkUserAction($entry);
// generates the view url for this entry to check for redirection later
// to avoid redirecting to the deleted entry. Ugh.
$url = $this->generateUrl(
'view',
['id' => $entry->getId()]
);
// entry deleted, dispatch event about it!
$this->eventDispatcher->dispatch(new EntryDeletedEvent($entry), EntryDeletedEvent::NAME);
$this->entityManager->remove($entry);
$this->entityManager->flush();
$this->addFlash(
'notice',
'flashes.entry.notice.entry_deleted'
);
// don't redirect user to the deleted entry (check that the referer doesn't end with the same url)
$prev = $request->query->get('redirect', '');
$to = (1 !== preg_match('#' . $url . '$#i', $prev) ? $prev : null);
$redirectUrl = $this->redirectHelper->to($to);
return $this->redirect($redirectUrl);
}
/**
* Get public URL for entry (and generate it if necessary).
*
* @Route("/share/{id}", requirements={"id" = "\d+"}, name="share")
*
* @return Response
*/
public function shareAction(Entry $entry)
{
$this->checkUserAction($entry);
if (null === $entry->getUid()) {
$entry->generateUid();
$this->entityManager->persist($entry);
$this->entityManager->flush();
}
return $this->redirect($this->generateUrl('share_entry', [
'uid' => $entry->getUid(),
]));
}
/**
* Disable public sharing for an entry.
*
* @Route("/share/delete/{id}", requirements={"id" = "\d+"}, name="delete_share")
*
* @return Response
*/
public function deleteShareAction(Entry $entry)
{
$this->checkUserAction($entry);
$entry->cleanUid();
$this->entityManager->persist($entry);
$this->entityManager->flush();
return $this->redirect($this->generateUrl('view', [
'id' => $entry->getId(),
]));
}
/**
* Ability to view a content publicly.
*
* @Route("/share/{uid}", requirements={"uid" = ".+"}, name="share_entry")
* @Cache(maxage="25200", smaxage="25200", public=true)
*
* @return Response
*/
public function shareEntryAction(Entry $entry, Config $craueConfig)
{
if (!$craueConfig->get('share_public')) {
throw $this->createAccessDeniedException('Sharing an entry is disabled for this user.');
}
return $this->render(
'Entry/share.html.twig',
['entry' => $entry]
);
}
/**
* List the entries with the same domain as the current one.
*
* @param int $page
*
* @Route("/domain/{id}/{page}", requirements={"id" = ".+"}, defaults={"page" = 1}, name="same_domain")
*
* @return Response
*/
public function getSameDomainEntries(Request $request, $page = 1)
{
return $this->showEntries('same-domain', $request, $page);
}
/**
* Global method to retrieve entries depending on the given type
* It returns the response to be send.
*
* @param string $type Entries type: unread, starred or archive
* @param int $page
*
* @return Response
*/
private function showEntries($type, Request $request, $page)
{
$searchTerm = (isset($request->get('search_entry')['term']) ? trim($request->get('search_entry')['term']) : '');
$currentRoute = (null !== $request->query->get('currentRoute') ? $request->query->get('currentRoute') : '');
$formOptions = [];
switch ($type) {
case 'search':
$qb = $this->entryRepository->getBuilderForSearchByUser($this->getUser()->getId(), $searchTerm, $currentRoute);
break;
case 'untagged':
$qb = $this->entryRepository->getBuilderForUntaggedByUser($this->getUser()->getId());
break;
case 'starred':
$qb = $this->entryRepository->getBuilderForStarredByUser($this->getUser()->getId());
$formOptions['filter_starred'] = true;
break;
case 'archive':
$qb = $this->entryRepository->getBuilderForArchiveByUser($this->getUser()->getId());
$formOptions['filter_archived'] = true;
break;
case 'annotated':
$qb = $this->entryRepository->getBuilderForAnnotationsByUser($this->getUser()->getId());
break;
case 'unread':
$qb = $this->entryRepository->getBuilderForUnreadByUser($this->getUser()->getId());
$formOptions['filter_unread'] = true;
break;
case 'same-domain':
$qb = $this->entryRepository->getBuilderForSameDomainByUser($this->getUser()->getId(), $request->get('id'));
break;
case 'all':
$qb = $this->entryRepository->getBuilderForAllByUser($this->getUser()->getId());
break;
default:
throw new \InvalidArgumentException(sprintf('Type "%s" is not implemented.', $type));
}
$form = $this->createForm(EntryFilterType::class, [], $formOptions);
if ($request->query->has($form->getName())) {
// manually bind values from the request
$form->submit($request->query->get($form->getName()));
// build the query from the given form object
$this->filterBuilderUpdater->addFilterConditions($form, $qb);
}
$pagerAdapter = new DoctrineORMAdapter($qb->getQuery(), true, false);
$entries = $this->preparePagerForEntriesHelper->prepare($pagerAdapter);
try {
$entries->setCurrentPage($page);
} catch (OutOfRangeCurrentPageException $e) {
if ($page > 1) {
return $this->redirect($this->generateUrl($type, ['page' => $entries->getNbPages()]), 302);
}
}
return $this->render(
'Entry/entries.html.twig', [
'form' => $form->createView(),
'entries' => $entries,
'currentPage' => $page,
'searchTerm' => $searchTerm,
'isFiltered' => $form->isSubmitted(),
]
);
}
/**
* Fetch content and update entry.
* In case it fails, $entry->getContent will return an error message.
*
* @param string $prefixMessage Should be the translation key: entry_saved or entry_reloaded
*/
private function updateEntry(Entry $entry, $prefixMessage = 'entry_saved')
{
$message = 'flashes.entry.notice.' . $prefixMessage;
try {
$this->contentProxy->updateEntry($entry, $entry->getUrl());
} catch (\Exception $e) {
// $this->logger->error('Error while saving an entry', [
// 'exception' => $e,
// 'entry' => $entry,
// ]);
$message = 'flashes.entry.notice.' . $prefixMessage . '_failed';
}
if (empty($entry->getDomainName())) {
$this->contentProxy->setEntryDomainName($entry);
}
if (empty($entry->getTitle())) {
$this->contentProxy->setDefaultEntryTitle($entry);
}
$this->addFlash('notice', $message);
}
/**
* Check if the logged user can manage the given entry.
*/
private function checkUserAction(Entry $entry)
{
if (null === $this->getUser() || $this->getUser()->getId() !== $entry->getUser()->getId()) {
throw $this->createAccessDeniedException('You can not access this entry.');
}
}
/**
* Check for existing entry, if it exists, redirect to it with a message.
*
* @return Entry|bool
*/
private function checkIfEntryAlreadyExists(Entry $entry)
{
return $this->entryRepository->findByUrlAndUserId($entry->getUrl(), $this->getUser()->getId());
}
}

View file

@ -0,0 +1,122 @@
<?php
namespace Wallabag\CoreBundle\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Wallabag\CoreBundle\Helper\EntriesExport;
use Wallabag\CoreBundle\Repository\EntryRepository;
use Wallabag\CoreBundle\Repository\TagRepository;
/**
* The try/catch can be removed once all formats will be implemented.
* Still need implementation: txt.
*/
class ExportController extends AbstractController
{
/**
* Gets one entry content.
*
* @Route("/export/{id}.{format}", name="export_entry", requirements={
* "format": "epub|pdf|json|xml|txt|csv",
* "id": "\d+"
* })
*
* @return Response
*/
public function downloadEntryAction(Request $request, EntryRepository $entryRepository, EntriesExport $entriesExport, string $format, int $id)
{
try {
$entry = $entryRepository->find($id);
/*
* We duplicate EntryController::checkUserAction here as a quick fix for an improper authorization vulnerability
*
* This should be eventually rewritten
*/
if (null === $entry || null === $this->getUser() || $this->getUser()->getId() !== $entry->getUser()->getId()) {
throw new NotFoundHttpException();
}
return $entriesExport
->setEntries($entry)
->updateTitle('entry')
->updateAuthor('entry')
->exportAs($format);
} catch (\InvalidArgumentException $e) {
throw new NotFoundHttpException($e->getMessage());
}
}
/**
* Export all entries for current user.
*
* @Route("/export/{category}.{format}", name="export_entries", requirements={
* "format": "epub|pdf|json|xml|txt|csv",
* "category": "all|unread|starred|archive|tag_entries|untagged|search|annotated|same_domain"
* })
*
* @return Response
*/
public function downloadEntriesAction(Request $request, EntryRepository $entryRepository, TagRepository $tagRepository, EntriesExport $entriesExport, string $format, string $category, int $entry = 0)
{
$method = ucfirst($category);
$methodBuilder = 'getBuilderFor' . $method . 'ByUser';
$title = $method;
if ('same_domain' === $category) {
$entries = $entryRepository->getBuilderForSameDomainByUser(
$this->getUser()->getId(),
$request->get('entry')
)->getQuery()
->getResult();
$title = 'Same domain';
} elseif ('tag_entries' === $category) {
$tag = $tagRepository->findOneBySlug($request->query->get('tag'));
$entries = $entryRepository->findAllByTagId(
$this->getUser()->getId(),
$tag->getId()
);
$title = 'Tag ' . $tag->getLabel();
} elseif ('search' === $category) {
$searchTerm = (isset($request->get('search_entry')['term']) ? $request->get('search_entry')['term'] : '');
$currentRoute = (null !== $request->query->get('currentRoute') ? $request->query->get('currentRoute') : '');
$entries = $entryRepository->getBuilderForSearchByUser(
$this->getUser()->getId(),
$searchTerm,
$currentRoute
)->getQuery()
->getResult();
$title = 'Search ' . $searchTerm;
} elseif ('annotated' === $category) {
$entries = $entryRepository->getBuilderForAnnotationsByUser(
$this->getUser()->getId()
)->getQuery()
->getResult();
$title = 'With annotations';
} else {
$entries = $entryRepository
->$methodBuilder($this->getUser()->getId())
->getQuery()
->getResult();
}
try {
return $entriesExport
->setEntries($entries)
->updateTitle($title)
->updateAuthor($method)
->exportAs($format);
} catch (\InvalidArgumentException $e) {
throw new NotFoundHttpException($e->getMessage());
}
}
}

View file

@ -0,0 +1,239 @@
<?php
namespace Wallabag\CoreBundle\Controller;
use Pagerfanta\Adapter\ArrayAdapter;
use Pagerfanta\Doctrine\ORM\QueryAdapter as DoctrineORMAdapter;
use Pagerfanta\Exception\OutOfRangeCurrentPageException;
use Pagerfanta\Pagerfanta;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Wallabag\CoreBundle\Entity\Tag;
use Wallabag\CoreBundle\Entity\User;
use Wallabag\CoreBundle\Helper\PreparePagerForEntries;
use Wallabag\CoreBundle\Repository\EntryRepository;
class FeedController extends AbstractController
{
private EntryRepository $entryRepository;
public function __construct(EntryRepository $entryRepository)
{
$this->entryRepository = $entryRepository;
}
/**
* Shows unread entries for current user.
*
* @Route("/feed/{username}/{token}/unread/{page}", name="unread_feed", defaults={"page"=1, "_format"="xml"})
*
* @ParamConverter("user", class="Wallabag\CoreBundle\Entity\User", converter="username_feed_token_converter")
*
* @return Response
*/
public function showUnreadFeedAction(User $user, $page)
{
return $this->showEntries('unread', $user, $page);
}
/**
* Shows read entries for current user.
*
* @Route("/feed/{username}/{token}/archive/{page}", name="archive_feed", defaults={"page"=1, "_format"="xml"})
*
* @ParamConverter("user", class="Wallabag\CoreBundle\Entity\User", converter="username_feed_token_converter")
*
* @return Response
*/
public function showArchiveFeedAction(User $user, $page)
{
return $this->showEntries('archive', $user, $page);
}
/**
* Shows starred entries for current user.
*
* @Route("/feed/{username}/{token}/starred/{page}", name="starred_feed", defaults={"page"=1, "_format"="xml"})
*
* @ParamConverter("user", class="Wallabag\CoreBundle\Entity\User", converter="username_feed_token_converter")
*
* @return Response
*/
public function showStarredFeedAction(User $user, $page)
{
return $this->showEntries('starred', $user, $page);
}
/**
* Shows all entries for current user.
*
* @Route("/feed/{username}/{token}/all/{page}", name="all_feed", defaults={"page"=1, "_format"="xml"})
*
* @ParamConverter("user", class="Wallabag\CoreBundle\Entity\User", converter="username_feed_token_converter")
*
* @return Response
*/
public function showAllFeedAction(User $user, $page)
{
return $this->showEntries('all', $user, $page);
}
/**
* Shows entries associated to a tag for current user.
*
* @Route("/feed/{username}/{token}/tags/{slug}/{page}", name="tag_feed", defaults={"page"=1, "_format"="xml"})
*
* @ParamConverter("user", class="Wallabag\CoreBundle\Entity\User", converter="username_feed_token_converter")
* @ParamConverter("tag", options={"mapping": {"slug": "slug"}})
*
* @return Response
*/
public function showTagsFeedAction(Request $request, User $user, Tag $tag, PreparePagerForEntries $preparePagerForEntries, $page)
{
$sort = $request->query->get('sort', 'created');
$sorts = [
'created' => 'createdAt',
'updated' => 'updatedAt',
];
if (!isset($sorts[$sort])) {
throw new BadRequestHttpException(sprintf('Sort "%s" is not available.', $sort));
}
$url = $this->generateUrl(
'tag_feed',
[
'username' => $user->getUsername(),
'token' => $user->getConfig()->getFeedToken(),
'slug' => $tag->getSlug(),
],
UrlGeneratorInterface::ABSOLUTE_URL
);
$entriesByTag = $this->entryRepository->findAllByTagId(
$user->getId(),
$tag->getId(),
$sorts[$sort]
);
$pagerAdapter = new ArrayAdapter($entriesByTag);
$entries = $preparePagerForEntries->prepare(
$pagerAdapter,
$user
);
$perPage = $user->getConfig()->getFeedLimit() ?: $this->getParameter('wallabag_core.feed_limit');
$entries->setMaxPerPage($perPage);
if (null === $entries) {
throw $this->createNotFoundException('No entries found?');
}
try {
$entries->setCurrentPage($page);
} catch (OutOfRangeCurrentPageException $e) {
if ($page > 1) {
return $this->redirect($url . '?page=' . $entries->getNbPages(), 302);
}
}
return $this->render(
'Entry/entries.xml.twig',
[
'type' => 'tag',
'url' => $url,
'entries' => $entries,
'user' => $user->getUsername(),
'domainName' => $this->getParameter('domain_name'),
'version' => $this->getParameter('wallabag_core.version'),
'tag' => $tag->getSlug(),
'updated' => $this->prepareFeedUpdatedDate($entries, $sort),
],
new Response('', 200, ['Content-Type' => 'application/atom+xml'])
);
}
private function prepareFeedUpdatedDate(Pagerfanta $entries, $sort = 'created')
{
$currentPageResults = $entries->getCurrentPageResults();
if (isset($currentPageResults[0])) {
$firstEntry = $currentPageResults[0];
if ('created' === $sort) {
return $firstEntry->getCreatedAt();
}
return $firstEntry->getUpdatedAt();
}
return null;
}
/**
* Global method to retrieve entries depending on the given type
* It returns the response to be send.
*
* @param string $type Entries type: unread, starred or archive
* @param int $page
*
* @return Response
*/
private function showEntries(string $type, User $user, $page = 1)
{
switch ($type) {
case 'starred':
$qb = $this->entryRepository->getBuilderForStarredByUser($user->getId());
break;
case 'archive':
$qb = $this->entryRepository->getBuilderForArchiveByUser($user->getId());
break;
case 'unread':
$qb = $this->entryRepository->getBuilderForUnreadByUser($user->getId());
break;
case 'all':
$qb = $this->entryRepository->getBuilderForAllByUser($user->getId());
break;
default:
throw new \InvalidArgumentException(sprintf('Type "%s" is not implemented.', $type));
}
$pagerAdapter = new DoctrineORMAdapter($qb->getQuery(), true, false);
$entries = new Pagerfanta($pagerAdapter);
$perPage = $user->getConfig()->getFeedLimit() ?: $this->getParameter('wallabag_core.feed_limit');
$entries->setMaxPerPage($perPage);
$url = $this->generateUrl(
$type . '_feed',
[
'username' => $user->getUsername(),
'token' => $user->getConfig()->getFeedToken(),
],
UrlGeneratorInterface::ABSOLUTE_URL
);
try {
$entries->setCurrentPage((int) $page);
} catch (OutOfRangeCurrentPageException $e) {
if ($page > 1) {
return $this->redirect($url . '/' . $entries->getNbPages());
}
}
return $this->render('Entry/entries.xml.twig', [
'type' => $type,
'url' => $url,
'entries' => $entries,
'user' => $user->getUsername(),
'domainName' => $this->getParameter('domain_name'),
'version' => $this->getParameter('wallabag_core.version'),
'updated' => $this->prepareFeedUpdatedDate($entries),
], new Response('', 200, ['Content-Type' => 'application/atom+xml']));
}
}

View file

@ -0,0 +1,151 @@
<?php
namespace Wallabag\CoreBundle\Controller;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Form\Form;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\Translation\TranslatorInterface;
use Wallabag\CoreBundle\Entity\IgnoreOriginInstanceRule;
use Wallabag\CoreBundle\Form\Type\IgnoreOriginInstanceRuleType;
use Wallabag\CoreBundle\Repository\IgnoreOriginInstanceRuleRepository;
/**
* IgnoreOriginInstanceRuleController controller.
*
* @Route("/ignore-origin-instance-rules")
*/
class IgnoreOriginInstanceRuleController extends AbstractController
{
private EntityManagerInterface $entityManager;
private TranslatorInterface $translator;
public function __construct(EntityManagerInterface $entityManager, TranslatorInterface $translator)
{
$this->entityManager = $entityManager;
$this->translator = $translator;
}
/**
* Lists all IgnoreOriginInstanceRule entities.
*
* @Route("/", name="ignore_origin_instance_rules_index", methods={"GET"})
*/
public function indexAction(IgnoreOriginInstanceRuleRepository $repository)
{
$rules = $repository->findAll();
return $this->render('IgnoreOriginInstanceRule/index.html.twig', [
'rules' => $rules,
]);
}
/**
* Creates a new ignore origin instance rule entity.
*
* @Route("/new", name="ignore_origin_instance_rules_new", methods={"GET", "POST"})
*
* @return Response
*/
public function newAction(Request $request)
{
$ignoreOriginInstanceRule = new IgnoreOriginInstanceRule();
$form = $this->createForm(IgnoreOriginInstanceRuleType::class, $ignoreOriginInstanceRule);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->entityManager->persist($ignoreOriginInstanceRule);
$this->entityManager->flush();
$this->addFlash(
'notice',
$this->translator->trans('flashes.ignore_origin_instance_rule.notice.added')
);
return $this->redirectToRoute('ignore_origin_instance_rules_index');
}
return $this->render('IgnoreOriginInstanceRule/new.html.twig', [
'rule' => $ignoreOriginInstanceRule,
'form' => $form->createView(),
]);
}
/**
* Displays a form to edit an existing ignore origin instance rule entity.
*
* @Route("/{id}/edit", name="ignore_origin_instance_rules_edit", methods={"GET", "POST"})
*
* @return Response
*/
public function editAction(Request $request, IgnoreOriginInstanceRule $ignoreOriginInstanceRule)
{
$deleteForm = $this->createDeleteForm($ignoreOriginInstanceRule);
$editForm = $this->createForm(IgnoreOriginInstanceRuleType::class, $ignoreOriginInstanceRule);
$editForm->handleRequest($request);
if ($editForm->isSubmitted() && $editForm->isValid()) {
$this->entityManager->persist($ignoreOriginInstanceRule);
$this->entityManager->flush();
$this->addFlash(
'notice',
$this->translator->trans('flashes.ignore_origin_instance_rule.notice.updated')
);
return $this->redirectToRoute('ignore_origin_instance_rules_index');
}
return $this->render('IgnoreOriginInstanceRule/edit.html.twig', [
'rule' => $ignoreOriginInstanceRule,
'edit_form' => $editForm->createView(),
'delete_form' => $deleteForm->createView(),
]);
}
/**
* Deletes a site credential entity.
*
* @Route("/{id}", name="ignore_origin_instance_rules_delete", methods={"DELETE"})
*
* @return RedirectResponse
*/
public function deleteAction(Request $request, IgnoreOriginInstanceRule $ignoreOriginInstanceRule)
{
$form = $this->createDeleteForm($ignoreOriginInstanceRule);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->addFlash(
'notice',
$this->translator->trans('flashes.ignore_origin_instance_rule.notice.deleted')
);
$this->entityManager->remove($ignoreOriginInstanceRule);
$this->entityManager->flush();
}
return $this->redirectToRoute('ignore_origin_instance_rules_index');
}
/**
* Creates a form to delete a ignore origin instance rule entity.
*
* @param IgnoreOriginInstanceRule $ignoreOriginInstanceRule The ignore origin instance rule entity
*
* @return FormInterface The form
*/
private function createDeleteForm(IgnoreOriginInstanceRule $ignoreOriginInstanceRule)
{
return $this->createFormBuilder()
->setAction($this->generateUrl('ignore_origin_instance_rules_delete', ['id' => $ignoreOriginInstanceRule->getId()]))
->setMethod('DELETE')
->getForm()
;
}
}

View file

@ -0,0 +1,83 @@
<?php
namespace Wallabag\CoreBundle\Controller\Import;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\Translation\TranslatorInterface;
use Wallabag\CoreBundle\Controller\AbstractController;
use Wallabag\CoreBundle\Form\Type\UploadImportType;
use Wallabag\CoreBundle\Import\ImportInterface;
abstract class BrowserController extends AbstractController
{
/**
* @Route("/import/browser", name="import_browser")
*
* @return Response
*/
public function indexAction(Request $request, TranslatorInterface $translator)
{
$form = $this->createForm(UploadImportType::class);
$form->handleRequest($request);
$wallabag = $this->getImportService();
$wallabag->setUser($this->getUser());
if ($form->isSubmitted() && $form->isValid()) {
$file = $form->get('file')->getData();
$markAsRead = $form->get('mark_as_read')->getData();
$name = $this->getUser()->getId() . '.json';
if (null !== $file && \in_array($file->getClientMimeType(), $this->getParameter('wallabag_core.allow_mimetypes'), true) && $file->move($this->getParameter('wallabag_core.resource_dir'), $name)) {
$res = $wallabag
->setFilepath($this->getParameter('wallabag_core.resource_dir') . '/' . $name)
->setMarkAsRead($markAsRead)
->import();
$message = 'flashes.import.notice.failed';
if (true === $res) {
$summary = $wallabag->getSummary();
$message = $translator->trans('flashes.import.notice.summary', [
'%imported%' => $summary['imported'],
'%skipped%' => $summary['skipped'],
]);
if (0 < $summary['queued']) {
$message = $translator->trans('flashes.import.notice.summary_with_queue', [
'%queued%' => $summary['queued'],
]);
}
unlink($this->getParameter('wallabag_core.resource_dir') . '/' . $name);
}
$this->addFlash('notice', $message);
return $this->redirect($this->generateUrl('homepage'));
}
$this->addFlash('notice', 'flashes.import.notice.failed_on_file');
}
return $this->render($this->getImportTemplate(), [
'form' => $form->createView(),
'import' => $wallabag,
]);
}
/**
* Return the service to handle the import.
*
* @return ImportInterface
*/
abstract protected function getImportService();
/**
* Return the template used for the form.
*
* @return string
*/
abstract protected function getImportTemplate();
}

View file

@ -0,0 +1,51 @@
<?php
namespace Wallabag\CoreBundle\Controller\Import;
use Craue\ConfigBundle\Util\Config;
use OldSound\RabbitMqBundle\RabbitMq\Producer as RabbitMqProducer;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\Translation\TranslatorInterface;
use Wallabag\CoreBundle\Import\ChromeImport;
use Wallabag\CoreBundle\Redis\Producer as RedisProducer;
class ChromeController extends BrowserController
{
private ChromeImport $chromeImport;
private Config $craueConfig;
private RabbitMqProducer $rabbitMqProducer;
private RedisProducer $redisProducer;
public function __construct(ChromeImport $chromeImport, Config $craueConfig, RabbitMqProducer $rabbitMqProducer, RedisProducer $redisProducer)
{
$this->chromeImport = $chromeImport;
$this->craueConfig = $craueConfig;
$this->rabbitMqProducer = $rabbitMqProducer;
$this->redisProducer = $redisProducer;
}
/**
* @Route("/import/chrome", name="import_chrome")
*/
public function indexAction(Request $request, TranslatorInterface $translator)
{
return parent::indexAction($request, $translator);
}
protected function getImportService()
{
if ($this->craueConfig->get('import_with_rabbitmq')) {
$this->chromeImport->setProducer($this->rabbitMqProducer);
} elseif ($this->craueConfig->get('import_with_redis')) {
$this->chromeImport->setProducer($this->redisProducer);
}
return $this->chromeImport;
}
protected function getImportTemplate()
{
return 'Import/Chrome/index.html.twig';
}
}

View file

@ -0,0 +1,84 @@
<?php
namespace Wallabag\CoreBundle\Controller\Import;
use Craue\ConfigBundle\Util\Config;
use OldSound\RabbitMqBundle\RabbitMq\Producer as RabbitMqProducer;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\Translation\TranslatorInterface;
use Wallabag\CoreBundle\Controller\AbstractController;
use Wallabag\CoreBundle\Form\Type\UploadImportType;
use Wallabag\CoreBundle\Import\DeliciousImport;
use Wallabag\CoreBundle\Redis\Producer as RedisProducer;
class DeliciousController extends AbstractController
{
private RabbitMqProducer $rabbitMqProducer;
private RedisProducer $redisProducer;
public function __construct(RabbitMqProducer $rabbitMqProducer, RedisProducer $redisProducer)
{
$this->rabbitMqProducer = $rabbitMqProducer;
$this->redisProducer = $redisProducer;
}
/**
* @Route("/import/delicious", name="import_delicious")
*/
public function indexAction(Request $request, DeliciousImport $delicious, Config $craueConfig, TranslatorInterface $translator)
{
$form = $this->createForm(UploadImportType::class);
$form->handleRequest($request);
$delicious->setUser($this->getUser());
if ($craueConfig->get('import_with_rabbitmq')) {
$delicious->setProducer($this->rabbitMqProducer);
} elseif ($craueConfig->get('import_with_redis')) {
$delicious->setProducer($this->redisProducer);
}
if ($form->isSubmitted() && $form->isValid()) {
$file = $form->get('file')->getData();
$markAsRead = $form->get('mark_as_read')->getData();
$name = 'delicious_' . $this->getUser()->getId() . '.json';
if (null !== $file && \in_array($file->getClientMimeType(), $this->getParameter('wallabag_core.allow_mimetypes'), true) && $file->move($this->getParameter('wallabag_core.resource_dir'), $name)) {
$res = $delicious
->setFilepath($this->getParameter('wallabag_core.resource_dir') . '/' . $name)
->setMarkAsRead($markAsRead)
->import();
$message = 'flashes.import.notice.failed';
if (true === $res) {
$summary = $delicious->getSummary();
$message = $translator->trans('flashes.import.notice.summary', [
'%imported%' => $summary['imported'],
'%skipped%' => $summary['skipped'],
]);
if (0 < $summary['queued']) {
$message = $translator->trans('flashes.import.notice.summary_with_queue', [
'%queued%' => $summary['queued'],
]);
}
unlink($this->getParameter('wallabag_core.resource_dir') . '/' . $name);
}
$this->addFlash('notice', $message);
return $this->redirect($this->generateUrl('homepage'));
}
$this->addFlash('notice', 'flashes.import.notice.failed_on_file');
}
return $this->render('Import/Delicious/index.html.twig', [
'form' => $form->createView(),
'import' => $delicious,
]);
}
}

View file

@ -0,0 +1,51 @@
<?php
namespace Wallabag\CoreBundle\Controller\Import;
use Craue\ConfigBundle\Util\Config;
use OldSound\RabbitMqBundle\RabbitMq\Producer as RabbitMqProducer;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\Translation\TranslatorInterface;
use Wallabag\CoreBundle\Import\ElcuratorImport;
use Wallabag\CoreBundle\Redis\Producer as RedisProducer;
class ElcuratorController extends WallabagController
{
private ElcuratorImport $elcuratorImport;
private Config $craueConfig;
private RabbitMqProducer $rabbitMqProducer;
private RedisProducer $redisProducer;
public function __construct(ElcuratorImport $elcuratorImport, Config $craueConfig, RabbitMqProducer $rabbitMqProducer, RedisProducer $redisProducer)
{
$this->elcuratorImport = $elcuratorImport;
$this->craueConfig = $craueConfig;
$this->rabbitMqProducer = $rabbitMqProducer;
$this->redisProducer = $redisProducer;
}
/**
* @Route("/import/elcurator", name="import_elcurator")
*/
public function indexAction(Request $request, TranslatorInterface $translator)
{
return parent::indexAction($request, $translator);
}
protected function getImportService()
{
if ($this->craueConfig->get('import_with_rabbitmq')) {
$this->elcuratorImport->setProducer($this->rabbitMqProducer);
} elseif ($this->craueConfig->get('import_with_redis')) {
$this->elcuratorImport->setProducer($this->redisProducer);
}
return $this->elcuratorImport;
}
protected function getImportTemplate()
{
return 'Import/Elcurator/index.html.twig';
}
}

View file

@ -0,0 +1,51 @@
<?php
namespace Wallabag\CoreBundle\Controller\Import;
use Craue\ConfigBundle\Util\Config;
use OldSound\RabbitMqBundle\RabbitMq\Producer as RabbitMqProducer;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\Translation\TranslatorInterface;
use Wallabag\CoreBundle\Import\FirefoxImport;
use Wallabag\CoreBundle\Redis\Producer as RedisProducer;
class FirefoxController extends BrowserController
{
private FirefoxImport $firefoxImport;
private Config $craueConfig;
private RabbitMqProducer $rabbitMqProducer;
private RedisProducer $redisProducer;
public function __construct(FirefoxImport $firefoxImport, Config $craueConfig, RabbitMqProducer $rabbitMqProducer, RedisProducer $redisProducer)
{
$this->firefoxImport = $firefoxImport;
$this->craueConfig = $craueConfig;
$this->rabbitMqProducer = $rabbitMqProducer;
$this->redisProducer = $redisProducer;
}
/**
* @Route("/import/firefox", name="import_firefox")
*/
public function indexAction(Request $request, TranslatorInterface $translator)
{
return parent::indexAction($request, $translator);
}
protected function getImportService()
{
if ($this->craueConfig->get('import_with_rabbitmq')) {
$this->firefoxImport->setProducer($this->rabbitMqProducer);
} elseif ($this->craueConfig->get('import_with_redis')) {
$this->firefoxImport->setProducer($this->redisProducer);
}
return $this->firefoxImport;
}
protected function getImportTemplate()
{
return 'Import/Firefox/index.html.twig';
}
}

View file

@ -0,0 +1,83 @@
<?php
namespace Wallabag\CoreBundle\Controller\Import;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\Translation\TranslatorInterface;
use Wallabag\CoreBundle\Controller\AbstractController;
use Wallabag\CoreBundle\Form\Type\UploadImportType;
use Wallabag\CoreBundle\Import\ImportInterface;
abstract class HtmlController extends AbstractController
{
/**
* @Route("/import/html", name="import_html")
*
* @return Response
*/
public function indexAction(Request $request, TranslatorInterface $translator)
{
$form = $this->createForm(UploadImportType::class);
$form->handleRequest($request);
$wallabag = $this->getImportService();
$wallabag->setUser($this->getUser());
if ($form->isSubmitted() && $form->isValid()) {
$file = $form->get('file')->getData();
$markAsRead = $form->get('mark_as_read')->getData();
$name = $this->getUser()->getId() . '.html';
if (null !== $file && \in_array($file->getClientMimeType(), $this->getParameter('wallabag_core.allow_mimetypes'), true) && $file->move($this->getParameter('wallabag_core.resource_dir'), $name)) {
$res = $wallabag
->setFilepath($this->getParameter('wallabag_core.resource_dir') . '/' . $name)
->setMarkAsRead($markAsRead)
->import();
$message = 'flashes.import.notice.failed';
if (true === $res) {
$summary = $wallabag->getSummary();
$message = $translator->trans('flashes.import.notice.summary', [
'%imported%' => $summary['imported'],
'%skipped%' => $summary['skipped'],
]);
if (0 < $summary['queued']) {
$message = $translator->trans('flashes.import.notice.summary_with_queue', [
'%queued%' => $summary['queued'],
]);
}
unlink($this->getParameter('wallabag_core.resource_dir') . '/' . $name);
}
$this->addFlash('notice', $message);
return $this->redirect($this->generateUrl('homepage'));
}
$this->addFlash('notice', 'flashes.import.notice.failed_on_file');
}
return $this->render($this->getImportTemplate(), [
'form' => $form->createView(),
'import' => $wallabag,
]);
}
/**
* Return the service to handle the import.
*
* @return ImportInterface
*/
abstract protected function getImportService();
/**
* Return the template used for the form.
*
* @return string
*/
abstract protected function getImportTemplate();
}

View file

@ -0,0 +1,95 @@
<?php
namespace Wallabag\CoreBundle\Controller\Import;
use Craue\ConfigBundle\Util\Config;
use Predis\Client;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Wallabag\CoreBundle\Consumer\RabbitMQConsumerTotalProxy;
use Wallabag\CoreBundle\Controller\AbstractController;
use Wallabag\CoreBundle\Import\ImportChain;
class ImportController extends AbstractController
{
private RabbitMQConsumerTotalProxy $rabbitMQConsumerTotalProxy;
public function __construct(RabbitMQConsumerTotalProxy $rabbitMQConsumerTotalProxy)
{
$this->rabbitMQConsumerTotalProxy = $rabbitMQConsumerTotalProxy;
}
/**
* @Route("/import/", name="import")
*/
public function importAction(ImportChain $importChain)
{
return $this->render('Import/index.html.twig', [
'imports' => $importChain->getAll(),
]);
}
/**
* Display how many messages are queue (both in Redis and RabbitMQ).
* Only for admins.
*/
public function checkQueueAction(AuthorizationCheckerInterface $authorizationChecker, Config $craueConfig)
{
$nbRedisMessages = null;
$nbRabbitMessages = null;
$redisNotInstalled = false;
$rabbitNotInstalled = false;
if (!$authorizationChecker->isGranted('ROLE_SUPER_ADMIN')) {
return $this->render('Import/check_queue.html.twig');
}
if ($craueConfig->get('import_with_rabbitmq')) {
// in case rabbit is activated but not installed
try {
$nbRabbitMessages = $this->rabbitMQConsumerTotalProxy->getTotalMessage('pocket')
+ $this->rabbitMQConsumerTotalProxy->getTotalMessage('readability')
+ $this->rabbitMQConsumerTotalProxy->getTotalMessage('wallabag_v1')
+ $this->rabbitMQConsumerTotalProxy->getTotalMessage('wallabag_v2')
+ $this->rabbitMQConsumerTotalProxy->getTotalMessage('firefox')
+ $this->rabbitMQConsumerTotalProxy->getTotalMessage('chrome')
+ $this->rabbitMQConsumerTotalProxy->getTotalMessage('instapaper')
+ $this->rabbitMQConsumerTotalProxy->getTotalMessage('pinboard')
+ $this->rabbitMQConsumerTotalProxy->getTotalMessage('delicious')
+ $this->rabbitMQConsumerTotalProxy->getTotalMessage('elcurator')
+ $this->rabbitMQConsumerTotalProxy->getTotalMessage('shaarli')
+ $this->rabbitMQConsumerTotalProxy->getTotalMessage('pocket_html')
;
} catch (\Exception $e) {
$rabbitNotInstalled = true;
}
} elseif ($craueConfig->get('import_with_redis')) {
$redis = $this->get(Client::class);
try {
$nbRedisMessages = $redis->llen('wallabag.import.pocket')
+ $redis->llen('wallabag.import.readability')
+ $redis->llen('wallabag.import.wallabag_v1')
+ $redis->llen('wallabag.import.wallabag_v2')
+ $redis->llen('wallabag.import.firefox')
+ $redis->llen('wallabag.import.chrome')
+ $redis->llen('wallabag.import.instapaper')
+ $redis->llen('wallabag.import.pinboard')
+ $redis->llen('wallabag.import.delicious')
+ $redis->llen('wallabag.import.elcurator')
+ $redis->llen('wallabag.import.shaarli')
+ $redis->llen('wallabag.import.pocket_html')
;
} catch (\Exception $e) {
$redisNotInstalled = true;
}
}
return $this->render('Import/check_queue.html.twig', [
'nbRedisMessages' => $nbRedisMessages,
'nbRabbitMessages' => $nbRabbitMessages,
'redisNotInstalled' => $redisNotInstalled,
'rabbitNotInstalled' => $rabbitNotInstalled,
]);
}
}

View file

@ -0,0 +1,84 @@
<?php
namespace Wallabag\CoreBundle\Controller\Import;
use Craue\ConfigBundle\Util\Config;
use OldSound\RabbitMqBundle\RabbitMq\Producer as RabbitMqProducer;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\Translation\TranslatorInterface;
use Wallabag\CoreBundle\Controller\AbstractController;
use Wallabag\CoreBundle\Form\Type\UploadImportType;
use Wallabag\CoreBundle\Import\InstapaperImport;
use Wallabag\CoreBundle\Redis\Producer as RedisProducer;
class InstapaperController extends AbstractController
{
private RabbitMqProducer $rabbitMqProducer;
private RedisProducer $redisProducer;
public function __construct(RabbitMqProducer $rabbitMqProducer, RedisProducer $redisProducer)
{
$this->rabbitMqProducer = $rabbitMqProducer;
$this->redisProducer = $redisProducer;
}
/**
* @Route("/import/instapaper", name="import_instapaper")
*/
public function indexAction(Request $request, InstapaperImport $instapaper, Config $craueConfig, TranslatorInterface $translator)
{
$form = $this->createForm(UploadImportType::class);
$form->handleRequest($request);
$instapaper->setUser($this->getUser());
if ($craueConfig->get('import_with_rabbitmq')) {
$instapaper->setProducer($this->rabbitMqProducer);
} elseif ($craueConfig->get('import_with_redis')) {
$instapaper->setProducer($this->redisProducer);
}
if ($form->isSubmitted() && $form->isValid()) {
$file = $form->get('file')->getData();
$markAsRead = $form->get('mark_as_read')->getData();
$name = 'instapaper_' . $this->getUser()->getId() . '.csv';
if (null !== $file && \in_array($file->getClientMimeType(), $this->getParameter('wallabag_core.allow_mimetypes'), true) && $file->move($this->getParameter('wallabag_core.resource_dir'), $name)) {
$res = $instapaper
->setFilepath($this->getParameter('wallabag_core.resource_dir') . '/' . $name)
->setMarkAsRead($markAsRead)
->import();
$message = 'flashes.import.notice.failed';
if (true === $res) {
$summary = $instapaper->getSummary();
$message = $translator->trans('flashes.import.notice.summary', [
'%imported%' => $summary['imported'],
'%skipped%' => $summary['skipped'],
]);
if (0 < $summary['queued']) {
$message = $translator->trans('flashes.import.notice.summary_with_queue', [
'%queued%' => $summary['queued'],
]);
}
unlink($this->getParameter('wallabag_core.resource_dir') . '/' . $name);
}
$this->addFlash('notice', $message);
return $this->redirect($this->generateUrl('homepage'));
}
$this->addFlash('notice', 'flashes.import.notice.failed_on_file');
}
return $this->render('Import/Instapaper/index.html.twig', [
'form' => $form->createView(),
'import' => $instapaper,
]);
}
}

View file

@ -0,0 +1,84 @@
<?php
namespace Wallabag\CoreBundle\Controller\Import;
use Craue\ConfigBundle\Util\Config;
use OldSound\RabbitMqBundle\RabbitMq\Producer as RabbitMqProducer;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\Translation\TranslatorInterface;
use Wallabag\CoreBundle\Controller\AbstractController;
use Wallabag\CoreBundle\Form\Type\UploadImportType;
use Wallabag\CoreBundle\Import\PinboardImport;
use Wallabag\CoreBundle\Redis\Producer as RedisProducer;
class PinboardController extends AbstractController
{
private RabbitMqProducer $rabbitMqProducer;
private RedisProducer $redisProducer;
public function __construct(RabbitMqProducer $rabbitMqProducer, RedisProducer $redisProducer)
{
$this->rabbitMqProducer = $rabbitMqProducer;
$this->redisProducer = $redisProducer;
}
/**
* @Route("/import/pinboard", name="import_pinboard")
*/
public function indexAction(Request $request, PinboardImport $pinboard, Config $craueConfig, TranslatorInterface $translator)
{
$form = $this->createForm(UploadImportType::class);
$form->handleRequest($request);
$pinboard->setUser($this->getUser());
if ($craueConfig->get('import_with_rabbitmq')) {
$pinboard->setProducer($this->rabbitMqProducer);
} elseif ($craueConfig->get('import_with_redis')) {
$pinboard->setProducer($this->redisProducer);
}
if ($form->isSubmitted() && $form->isValid()) {
$file = $form->get('file')->getData();
$markAsRead = $form->get('mark_as_read')->getData();
$name = 'pinboard_' . $this->getUser()->getId() . '.json';
if (null !== $file && \in_array($file->getClientMimeType(), $this->getParameter('wallabag_core.allow_mimetypes'), true) && $file->move($this->getParameter('wallabag_core.resource_dir'), $name)) {
$res = $pinboard
->setFilepath($this->getParameter('wallabag_core.resource_dir') . '/' . $name)
->setMarkAsRead($markAsRead)
->import();
$message = 'flashes.import.notice.failed';
if (true === $res) {
$summary = $pinboard->getSummary();
$message = $translator->trans('flashes.import.notice.summary', [
'%imported%' => $summary['imported'],
'%skipped%' => $summary['skipped'],
]);
if (0 < $summary['queued']) {
$message = $translator->trans('flashes.import.notice.summary_with_queue', [
'%queued%' => $summary['queued'],
]);
}
unlink($this->getParameter('wallabag_core.resource_dir') . '/' . $name);
}
$this->addFlash('notice', $message);
return $this->redirect($this->generateUrl('homepage'));
}
$this->addFlash('notice', 'flashes.import.notice.failed_on_file');
}
return $this->render('Import/Pinboard/index.html.twig', [
'form' => $form->createView(),
'import' => $pinboard,
]);
}
}

View file

@ -0,0 +1,135 @@
<?php
namespace Wallabag\CoreBundle\Controller\Import;
use Craue\ConfigBundle\Util\Config;
use OldSound\RabbitMqBundle\RabbitMq\Producer as RabbitMqProducer;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Wallabag\CoreBundle\Controller\AbstractController;
use Wallabag\CoreBundle\Import\PocketImport;
use Wallabag\CoreBundle\Redis\Producer as RedisProducer;
class PocketController extends AbstractController
{
private Config $craueConfig;
private RabbitMqProducer $rabbitMqProducer;
private RedisProducer $redisProducer;
private SessionInterface $session;
public function __construct(Config $craueConfig, RabbitMqProducer $rabbitMqProducer, RedisProducer $redisProducer, SessionInterface $session)
{
$this->craueConfig = $craueConfig;
$this->rabbitMqProducer = $rabbitMqProducer;
$this->redisProducer = $redisProducer;
$this->session = $session;
}
/**
* @Route("/import/pocket", name="import_pocket")
*/
public function indexAction(PocketImport $pocketImport)
{
$pocket = $this->getPocketImportService($pocketImport);
$form = $this->createFormBuilder($pocket)
->add('mark_as_read', CheckboxType::class, [
'label' => 'import.form.mark_as_read_label',
'required' => false,
])
->getForm();
return $this->render('Import/Pocket/index.html.twig', [
'import' => $pocket,
'has_consumer_key' => '' === trim($this->getUser()->getConfig()->getPocketConsumerKey()) ? false : true,
'form' => $form->createView(),
]);
}
/**
* @Route("/import/pocket/auth", name="import_pocket_auth")
*/
public function authAction(Request $request, PocketImport $pocketImport)
{
$requestToken = $this->getPocketImportService($pocketImport)
->getRequestToken($this->generateUrl('import', [], UrlGeneratorInterface::ABSOLUTE_URL));
if (false === $requestToken) {
$this->addFlash(
'notice',
'flashes.import.notice.failed'
);
return $this->redirect($this->generateUrl('import_pocket'));
}
$form = $request->request->get('form');
$this->session->set('import.pocket.code', $requestToken);
if (null !== $form && \array_key_exists('mark_as_read', $form)) {
$this->session->set('mark_as_read', $form['mark_as_read']);
}
return $this->redirect(
'https://getpocket.com/auth/authorize?request_token=' . $requestToken . '&redirect_uri=' . $this->generateUrl('import_pocket_callback', [], UrlGeneratorInterface::ABSOLUTE_URL),
301
);
}
/**
* @Route("/import/pocket/callback", name="import_pocket_callback")
*/
public function callbackAction(PocketImport $pocketImport, TranslatorInterface $translator)
{
$message = 'flashes.import.notice.failed';
$pocket = $this->getPocketImportService($pocketImport);
$markAsRead = $this->session->get('mark_as_read');
$this->session->remove('mark_as_read');
// something bad happend on pocket side
if (false === $pocket->authorize($this->session->get('import.pocket.code'))) {
$this->addFlash('notice', $message);
return $this->redirect($this->generateUrl('import_pocket'));
}
if (true === $pocket->setMarkAsRead($markAsRead)->import()) {
$summary = $pocket->getSummary();
$message = $translator->trans('flashes.import.notice.summary', [
'%imported%' => null !== $summary && \array_key_exists('imported', $summary) ? $summary['imported'] : 0,
'%skipped%' => null !== $summary && \array_key_exists('skipped', $summary) ? $summary['skipped'] : 0,
]);
if (null !== $summary && \array_key_exists('queued', $summary) && 0 < $summary['queued']) {
$message = $translator->trans('flashes.import.notice.summary_with_queue', [
'%queued%' => $summary['queued'],
]);
}
}
$this->addFlash('notice', $message);
return $this->redirect($this->generateUrl('homepage'));
}
/**
* Return Pocket Import Service with or without RabbitMQ enabled.
*/
private function getPocketImportService(PocketImport $pocketImport): PocketImport
{
$pocketImport->setUser($this->getUser());
if ($this->craueConfig->get('import_with_rabbitmq')) {
$pocketImport->setProducer($this->rabbitMqProducer);
} elseif ($this->craueConfig->get('import_with_redis')) {
$pocketImport->setProducer($this->redisProducer);
}
return $pocketImport;
}
}

View file

@ -0,0 +1,51 @@
<?php
namespace Wallabag\CoreBundle\Controller\Import;
use Craue\ConfigBundle\Util\Config;
use OldSound\RabbitMqBundle\RabbitMq\Producer as RabbitMqProducer;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\Translation\TranslatorInterface;
use Wallabag\CoreBundle\Import\PocketHtmlImport;
use Wallabag\CoreBundle\Redis\Producer as RedisProducer;
class PocketHtmlController extends HtmlController
{
private PocketHtmlImport $pocketHtmlImport;
private Config $craueConfig;
private RabbitMqProducer $rabbitMqProducer;
private RedisProducer $redisProducer;
public function __construct(PocketHtmlImport $pocketHtmlImport, Config $craueConfig, RabbitMqProducer $rabbitMqProducer, RedisProducer $redisProducer)
{
$this->pocketHtmlImport = $pocketHtmlImport;
$this->craueConfig = $craueConfig;
$this->rabbitMqProducer = $rabbitMqProducer;
$this->redisProducer = $redisProducer;
}
/**
* @Route("/import/pocket_html", name="import_pocket_html")
*/
public function indexAction(Request $request, TranslatorInterface $translator)
{
return parent::indexAction($request, $translator);
}
protected function getImportService()
{
if ($this->craueConfig->get('import_with_rabbitmq')) {
$this->pocketHtmlImport->setProducer($this->rabbitMqProducer);
} elseif ($this->craueConfig->get('import_with_redis')) {
$this->pocketHtmlImport->setProducer($this->redisProducer);
}
return $this->pocketHtmlImport;
}
protected function getImportTemplate()
{
return 'Import/PocketHtml/index.html.twig';
}
}

View file

@ -0,0 +1,84 @@
<?php
namespace Wallabag\CoreBundle\Controller\Import;
use Craue\ConfigBundle\Util\Config;
use OldSound\RabbitMqBundle\RabbitMq\Producer as RabbitMqProducer;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\Translation\TranslatorInterface;
use Wallabag\CoreBundle\Controller\AbstractController;
use Wallabag\CoreBundle\Form\Type\UploadImportType;
use Wallabag\CoreBundle\Import\ReadabilityImport;
use Wallabag\CoreBundle\Redis\Producer as RedisProducer;
class ReadabilityController extends AbstractController
{
private RabbitMqProducer $rabbitMqProducer;
private RedisProducer $redisProducer;
public function __construct(RabbitMqProducer $rabbitMqProducer, RedisProducer $redisProducer)
{
$this->rabbitMqProducer = $rabbitMqProducer;
$this->redisProducer = $redisProducer;
}
/**
* @Route("/import/readability", name="import_readability")
*/
public function indexAction(Request $request, ReadabilityImport $readability, Config $craueConfig, TranslatorInterface $translator)
{
$form = $this->createForm(UploadImportType::class);
$form->handleRequest($request);
$readability->setUser($this->getUser());
if ($craueConfig->get('import_with_rabbitmq')) {
$readability->setProducer($this->rabbitMqProducer);
} elseif ($craueConfig->get('import_with_redis')) {
$readability->setProducer($this->redisProducer);
}
if ($form->isSubmitted() && $form->isValid()) {
$file = $form->get('file')->getData();
$markAsRead = $form->get('mark_as_read')->getData();
$name = 'readability_' . $this->getUser()->getId() . '.json';
if (null !== $file && \in_array($file->getClientMimeType(), $this->getParameter('wallabag_core.allow_mimetypes'), true) && $file->move($this->getParameter('wallabag_core.resource_dir'), $name)) {
$res = $readability
->setFilepath($this->getParameter('wallabag_core.resource_dir') . '/' . $name)
->setMarkAsRead($markAsRead)
->import();
$message = 'flashes.import.notice.failed';
if (true === $res) {
$summary = $readability->getSummary();
$message = $translator->trans('flashes.import.notice.summary', [
'%imported%' => $summary['imported'],
'%skipped%' => $summary['skipped'],
]);
if (0 < $summary['queued']) {
$message = $translator->trans('flashes.import.notice.summary_with_queue', [
'%queued%' => $summary['queued'],
]);
}
unlink($this->getParameter('wallabag_core.resource_dir') . '/' . $name);
}
$this->addFlash('notice', $message);
return $this->redirect($this->generateUrl('homepage'));
}
$this->addFlash('notice', 'flashes.import.notice.failed_on_file');
}
return $this->render('Import/Readability/index.html.twig', [
'form' => $form->createView(),
'import' => $readability,
]);
}
}

View file

@ -0,0 +1,51 @@
<?php
namespace Wallabag\CoreBundle\Controller\Import;
use Craue\ConfigBundle\Util\Config;
use OldSound\RabbitMqBundle\RabbitMq\Producer as RabbitMqProducer;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\Translation\TranslatorInterface;
use Wallabag\CoreBundle\Import\ShaarliImport;
use Wallabag\CoreBundle\Redis\Producer as RedisProducer;
class ShaarliController extends HtmlController
{
private ShaarliImport $shaarliImport;
private Config $craueConfig;
private RabbitMqProducer $rabbitMqProducer;
private RedisProducer $redisProducer;
public function __construct(ShaarliImport $shaarliImport, Config $craueConfig, RabbitMqProducer $rabbitMqProducer, RedisProducer $redisProducer)
{
$this->shaarliImport = $shaarliImport;
$this->craueConfig = $craueConfig;
$this->rabbitMqProducer = $rabbitMqProducer;
$this->redisProducer = $redisProducer;
}
/**
* @Route("/import/shaarli", name="import_shaarli")
*/
public function indexAction(Request $request, TranslatorInterface $translator)
{
return parent::indexAction($request, $translator);
}
protected function getImportService()
{
if ($this->craueConfig->get('import_with_rabbitmq')) {
$this->shaarliImport->setProducer($this->rabbitMqProducer);
} elseif ($this->craueConfig->get('import_with_redis')) {
$this->shaarliImport->setProducer($this->redisProducer);
}
return $this->shaarliImport;
}
protected function getImportTemplate()
{
return 'Import/Shaarli/index.html.twig';
}
}

View file

@ -0,0 +1,87 @@
<?php
namespace Wallabag\CoreBundle\Controller\Import;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Contracts\Translation\TranslatorInterface;
use Wallabag\CoreBundle\Controller\AbstractController;
use Wallabag\CoreBundle\Form\Type\UploadImportType;
use Wallabag\CoreBundle\Import\ImportInterface;
/**
* Define Wallabag import for v1 and v2, since there are very similar.
*/
abstract class WallabagController extends AbstractController
{
/**
* Handle import request.
*
* @return Response|RedirectResponse
*/
public function indexAction(Request $request, TranslatorInterface $translator)
{
$form = $this->createForm(UploadImportType::class);
$form->handleRequest($request);
$wallabag = $this->getImportService();
$wallabag->setUser($this->getUser());
if ($form->isSubmitted() && $form->isValid()) {
$file = $form->get('file')->getData();
$markAsRead = $form->get('mark_as_read')->getData();
$name = $this->getUser()->getId() . '.json';
if (null !== $file && \in_array($file->getClientMimeType(), $this->getParameter('wallabag_core.allow_mimetypes'), true) && $file->move($this->getParameter('wallabag_core.resource_dir'), $name)) {
$res = $wallabag
->setFilepath($this->getParameter('wallabag_core.resource_dir') . '/' . $name)
->setMarkAsRead($markAsRead)
->import();
$message = 'flashes.import.notice.failed';
if (true === $res) {
$summary = $wallabag->getSummary();
$message = $translator->trans('flashes.import.notice.summary', [
'%imported%' => $summary['imported'],
'%skipped%' => $summary['skipped'],
]);
if (0 < $summary['queued']) {
$message = $translator->trans('flashes.import.notice.summary_with_queue', [
'%queued%' => $summary['queued'],
]);
}
unlink($this->getParameter('wallabag_core.resource_dir') . '/' . $name);
}
$this->addFlash('notice', $message);
return $this->redirect($this->generateUrl('homepage'));
}
$this->addFlash('notice', 'flashes.import.notice.failed_on_file');
}
return $this->render($this->getImportTemplate(), [
'form' => $form->createView(),
'import' => $wallabag,
]);
}
/**
* Return the service to handle the import.
*
* @return ImportInterface
*/
abstract protected function getImportService();
/**
* Return the template used for the form.
*
* @return string
*/
abstract protected function getImportTemplate();
}

View file

@ -0,0 +1,51 @@
<?php
namespace Wallabag\CoreBundle\Controller\Import;
use Craue\ConfigBundle\Util\Config;
use OldSound\RabbitMqBundle\RabbitMq\Producer as RabbitMqProducer;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\Translation\TranslatorInterface;
use Wallabag\CoreBundle\Import\WallabagV1Import;
use Wallabag\CoreBundle\Redis\Producer as RedisProducer;
class WallabagV1Controller extends WallabagController
{
private WallabagV1Import $wallabagImport;
private Config $craueConfig;
private RabbitMqProducer $rabbitMqProducer;
private RedisProducer $redisProducer;
public function __construct(WallabagV1Import $wallabagImport, Config $craueConfig, RabbitMqProducer $rabbitMqProducer, RedisProducer $redisProducer)
{
$this->wallabagImport = $wallabagImport;
$this->craueConfig = $craueConfig;
$this->rabbitMqProducer = $rabbitMqProducer;
$this->redisProducer = $redisProducer;
}
/**
* @Route("/import/wallabag-v1", name="import_wallabag_v1")
*/
public function indexAction(Request $request, TranslatorInterface $translator)
{
return parent::indexAction($request, $translator);
}
protected function getImportService()
{
if ($this->craueConfig->get('import_with_rabbitmq')) {
$this->wallabagImport->setProducer($this->rabbitMqProducer);
} elseif ($this->craueConfig->get('import_with_redis')) {
$this->wallabagImport->setProducer($this->redisProducer);
}
return $this->wallabagImport;
}
protected function getImportTemplate()
{
return 'Import/WallabagV1/index.html.twig';
}
}

View file

@ -0,0 +1,51 @@
<?php
namespace Wallabag\CoreBundle\Controller\Import;
use Craue\ConfigBundle\Util\Config;
use OldSound\RabbitMqBundle\RabbitMq\Producer as RabbitMqProducer;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\Translation\TranslatorInterface;
use Wallabag\CoreBundle\Import\WallabagV2Import;
use Wallabag\CoreBundle\Redis\Producer as RedisProducer;
class WallabagV2Controller extends WallabagController
{
private WallabagV2Import $wallabagImport;
private Config $craueConfig;
private RabbitMqProducer $rabbitMqProducer;
private RedisProducer $redisProducer;
public function __construct(WallabagV2Import $wallabagImport, Config $craueConfig, RabbitMqProducer $rabbitMqProducer, RedisProducer $redisProducer)
{
$this->wallabagImport = $wallabagImport;
$this->craueConfig = $craueConfig;
$this->rabbitMqProducer = $rabbitMqProducer;
$this->redisProducer = $redisProducer;
}
/**
* @Route("/import/wallabag-v2", name="import_wallabag_v2")
*/
public function indexAction(Request $request, TranslatorInterface $translator)
{
return parent::indexAction($request, $translator);
}
protected function getImportService()
{
if ($this->craueConfig->get('import_with_rabbitmq')) {
$this->wallabagImport->setProducer($this->rabbitMqProducer);
} elseif ($this->craueConfig->get('import_with_redis')) {
$this->wallabagImport->setProducer($this->redisProducer);
}
return $this->wallabagImport;
}
protected function getImportTemplate()
{
return 'Import/WallabagV2/index.html.twig';
}
}

View file

@ -0,0 +1,198 @@
<?php
namespace Wallabag\CoreBundle\Controller;
use Craue\ConfigBundle\Util\Config;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Form\Form;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\Translation\TranslatorInterface;
use Wallabag\CoreBundle\Entity\SiteCredential;
use Wallabag\CoreBundle\Entity\User;
use Wallabag\CoreBundle\Form\Type\SiteCredentialType;
use Wallabag\CoreBundle\Helper\CryptoProxy;
use Wallabag\CoreBundle\Repository\SiteCredentialRepository;
/**
* SiteCredential controller.
*
* @Route("/site-credentials")
*/
class SiteCredentialController extends AbstractController
{
private EntityManagerInterface $entityManager;
private TranslatorInterface $translator;
private CryptoProxy $cryptoProxy;
private Config $craueConfig;
public function __construct(EntityManagerInterface $entityManager, TranslatorInterface $translator, CryptoProxy $cryptoProxy, Config $craueConfig)
{
$this->entityManager = $entityManager;
$this->translator = $translator;
$this->cryptoProxy = $cryptoProxy;
$this->craueConfig = $craueConfig;
}
/**
* Lists all User entities.
*
* @Route("/", name="site_credentials_index", methods={"GET"})
*/
public function indexAction(SiteCredentialRepository $repository)
{
$this->isSiteCredentialsEnabled();
$credentials = $repository->findByUser($this->getUser());
return $this->render('SiteCredential/index.html.twig', [
'credentials' => $credentials,
]);
}
/**
* Creates a new site credential entity.
*
* @Route("/new", name="site_credentials_new", methods={"GET", "POST"})
*
* @return Response
*/
public function newAction(Request $request)
{
$this->isSiteCredentialsEnabled();
$credential = new SiteCredential($this->getUser());
$form = $this->createForm(SiteCredentialType::class, $credential);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$credential->setUsername($this->cryptoProxy->crypt($credential->getUsername()));
$credential->setPassword($this->cryptoProxy->crypt($credential->getPassword()));
$this->entityManager->persist($credential);
$this->entityManager->flush();
$this->addFlash(
'notice',
$this->translator->trans('flashes.site_credential.notice.added', ['%host%' => $credential->getHost()])
);
return $this->redirectToRoute('site_credentials_index');
}
return $this->render('SiteCredential/new.html.twig', [
'credential' => $credential,
'form' => $form->createView(),
]);
}
/**
* Displays a form to edit an existing site credential entity.
*
* @Route("/{id}/edit", name="site_credentials_edit", methods={"GET", "POST"})
*
* @return Response
*/
public function editAction(Request $request, SiteCredential $siteCredential)
{
$this->isSiteCredentialsEnabled();
$this->checkUserAction($siteCredential);
$deleteForm = $this->createDeleteForm($siteCredential);
$editForm = $this->createForm(SiteCredentialType::class, $siteCredential);
$editForm->handleRequest($request);
if ($editForm->isSubmitted() && $editForm->isValid()) {
$siteCredential->setUsername($this->cryptoProxy->crypt($siteCredential->getUsername()));
$siteCredential->setPassword($this->cryptoProxy->crypt($siteCredential->getPassword()));
$this->entityManager->persist($siteCredential);
$this->entityManager->flush();
$this->addFlash(
'notice',
$this->translator->trans('flashes.site_credential.notice.updated', ['%host%' => $siteCredential->getHost()])
);
return $this->redirectToRoute('site_credentials_index');
}
return $this->render('SiteCredential/edit.html.twig', [
'credential' => $siteCredential,
'edit_form' => $editForm->createView(),
'delete_form' => $deleteForm->createView(),
]);
}
/**
* Deletes a site credential entity.
*
* @Route("/{id}", name="site_credentials_delete", methods={"DELETE"})
*
* @return RedirectResponse
*/
public function deleteAction(Request $request, SiteCredential $siteCredential)
{
$this->isSiteCredentialsEnabled();
$this->checkUserAction($siteCredential);
$form = $this->createDeleteForm($siteCredential);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->addFlash(
'notice',
$this->translator->trans('flashes.site_credential.notice.deleted', ['%host%' => $siteCredential->getHost()])
);
$this->entityManager->remove($siteCredential);
$this->entityManager->flush();
}
return $this->redirectToRoute('site_credentials_index');
}
/**
* Throw a 404 if the feature is disabled.
*/
private function isSiteCredentialsEnabled()
{
if (!$this->craueConfig->get('restricted_access')) {
throw $this->createNotFoundException('Feature "restricted_access" is disabled, controllers too.');
}
}
/**
* Creates a form to delete a site credential entity.
*
* @param SiteCredential $siteCredential The site credential entity
*
* @return FormInterface The form
*/
private function createDeleteForm(SiteCredential $siteCredential)
{
return $this->createFormBuilder()
->setAction($this->generateUrl('site_credentials_delete', ['id' => $siteCredential->getId()]))
->setMethod('DELETE')
->getForm()
;
}
/**
* Check if the logged user can manage the given site credential.
*
* @param SiteCredential $siteCredential The site credential entity
*/
private function checkUserAction(SiteCredential $siteCredential)
{
if (null === $this->getUser() || $this->getUser()->getId() !== $siteCredential->getUser()->getId()) {
throw $this->createAccessDeniedException('You can not access this site credential.');
}
}
}

View file

@ -0,0 +1,47 @@
<?php
namespace Wallabag\CoreBundle\Controller;
use Symfony\Component\Routing\Annotation\Route;
class StaticController extends AbstractController
{
/**
* @Route("/howto", name="howto")
*/
public function howtoAction()
{
$addonsUrl = $this->getParameter('addons_url');
return $this->render(
'Static/howto.html.twig',
[
'addonsUrl' => $addonsUrl,
]
);
}
/**
* @Route("/about", name="about")
*/
public function aboutAction()
{
return $this->render(
'Static/about.html.twig',
[
'version' => $this->getParameter('wallabag_core.version'),
'paypal_url' => $this->getParameter('wallabag_core.paypal_url'),
]
);
}
/**
* @Route("/quickstart", name="quickstart")
*/
public function quickstartAction()
{
return $this->render(
'Static/quickstart.html.twig'
);
}
}

View file

@ -0,0 +1,295 @@
<?php
namespace Wallabag\CoreBundle\Controller;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\QueryBuilder;
use Pagerfanta\Adapter\ArrayAdapter;
use Pagerfanta\Exception\OutOfRangeCurrentPageException;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\Translation\TranslatorInterface;
use Wallabag\CoreBundle\Entity\Entry;
use Wallabag\CoreBundle\Entity\Tag;
use Wallabag\CoreBundle\Form\Type\NewTagType;
use Wallabag\CoreBundle\Form\Type\RenameTagType;
use Wallabag\CoreBundle\Helper\PreparePagerForEntries;
use Wallabag\CoreBundle\Helper\Redirect;
use Wallabag\CoreBundle\Helper\TagsAssigner;
use Wallabag\CoreBundle\Repository\EntryRepository;
use Wallabag\CoreBundle\Repository\TagRepository;
class TagController extends AbstractController
{
private EntityManagerInterface $entityManager;
private TagsAssigner $tagsAssigner;
private Redirect $redirectHelper;
public function __construct(EntityManagerInterface $entityManager, TagsAssigner $tagsAssigner, Redirect $redirectHelper)
{
$this->entityManager = $entityManager;
$this->tagsAssigner = $tagsAssigner;
$this->redirectHelper = $redirectHelper;
}
/**
* @Route("/new-tag/{entry}", requirements={"entry" = "\d+"}, name="new_tag", methods={"POST"})
*
* @return Response
*/
public function addTagFormAction(Request $request, Entry $entry, TranslatorInterface $translator)
{
$form = $this->createForm(NewTagType::class, new Tag());
$form->handleRequest($request);
$tags = $form->get('label')->getData() ?? '';
$tagsExploded = explode(',', $tags);
// avoid too much tag to be added
if (\count($tagsExploded) >= NewTagType::MAX_TAGS || \strlen($tags) >= NewTagType::MAX_LENGTH) {
$message = $translator->trans('flashes.tag.notice.too_much_tags', [
'%tags%' => NewTagType::MAX_TAGS,
'%characters%' => NewTagType::MAX_LENGTH,
]);
$this->addFlash('notice', $message);
return $this->redirect($this->generateUrl('view', ['id' => $entry->getId()]));
}
if ($form->isSubmitted() && $form->isValid()) {
$this->checkUserAction($entry);
$this->tagsAssigner->assignTagsToEntry(
$entry,
$form->get('label')->getData()
);
$this->entityManager->persist($entry);
$this->entityManager->flush();
$this->addFlash(
'notice',
'flashes.tag.notice.tag_added'
);
return $this->redirect($this->generateUrl('view', ['id' => $entry->getId()]));
}
return $this->render('Tag/new_form.html.twig', [
'form' => $form->createView(),
'entry' => $entry,
]);
}
/**
* Removes tag from entry.
*
* @Route("/remove-tag/{entry}/{tag}", requirements={"entry" = "\d+", "tag" = "\d+"}, name="remove_tag")
*
* @return Response
*/
public function removeTagFromEntry(Request $request, Entry $entry, Tag $tag)
{
$this->checkUserAction($entry);
$entry->removeTag($tag);
$this->entityManager->flush();
// remove orphan tag in case no entries are associated to it
if (0 === \count($tag->getEntries())) {
$this->entityManager->remove($tag);
$this->entityManager->flush();
}
$redirectUrl = $this->redirectHelper->to($request->query->get('redirect'), true);
return $this->redirect($redirectUrl);
}
/**
* Shows tags for current user.
*
* @Route("/tag/list", name="tag")
*
* @return Response
*/
public function showTagAction(TagRepository $tagRepository, EntryRepository $entryRepository)
{
$tags = $tagRepository->findAllFlatTagsWithNbEntries($this->getUser()->getId());
$nbEntriesUntagged = $entryRepository->countUntaggedEntriesByUser($this->getUser()->getId());
$renameForms = [];
foreach ($tags as $tag) {
$renameForms[$tag['id']] = $this->createForm(RenameTagType::class, new Tag())->createView();
}
return $this->render('Tag/tags.html.twig', [
'tags' => $tags,
'renameForms' => $renameForms,
'nbEntriesUntagged' => $nbEntriesUntagged,
]);
}
/**
* @param int $page
*
* @Route("/tag/list/{slug}/{page}", name="tag_entries", defaults={"page" = "1"})
* @ParamConverter("tag", options={"mapping": {"slug": "slug"}})
*
* @return Response
*/
public function showEntriesForTagAction(Tag $tag, EntryRepository $entryRepository, PreparePagerForEntries $preparePagerForEntries, $page, Request $request)
{
$entriesByTag = $entryRepository->findAllByTagId(
$this->getUser()->getId(),
$tag->getId()
);
$pagerAdapter = new ArrayAdapter($entriesByTag);
$entries = $preparePagerForEntries->prepare($pagerAdapter);
try {
$entries->setCurrentPage($page);
} catch (OutOfRangeCurrentPageException $e) {
if ($page > 1) {
return $this->redirect($this->generateUrl($request->get('_route'), [
'slug' => $tag->getSlug(),
'page' => $entries->getNbPages(),
]), 302);
}
}
return $this->render('Entry/entries.html.twig', [
'form' => null,
'entries' => $entries,
'currentPage' => $page,
'tag' => $tag,
]);
}
/**
* Rename a given tag with a new label
* Create a new tag with the new name and drop the old one.
*
* @Route("/tag/rename/{slug}", name="tag_rename")
* @ParamConverter("tag", options={"mapping": {"slug": "slug"}})
*
* @return Response
*/
public function renameTagAction(Tag $tag, Request $request, TagRepository $tagRepository, EntryRepository $entryRepository)
{
$form = $this->createForm(RenameTagType::class, new Tag());
$form->handleRequest($request);
$redirectUrl = $this->redirectHelper->to($request->query->get('redirect'), true);
if ($form->isSubmitted() && $form->isValid()) {
$newTag = new Tag();
$newTag->setLabel($form->get('label')->getData());
if ($newTag->getLabel() === $tag->getLabel()) {
return $this->redirect($redirectUrl);
}
$tagFromRepo = $tagRepository->findOneByLabel($newTag->getLabel());
if (null !== $tagFromRepo) {
$newTag = $tagFromRepo;
}
$entries = $entryRepository->findAllByTagId(
$this->getUser()->getId(),
$tag->getId()
);
foreach ($entries as $entry) {
$this->tagsAssigner->assignTagsToEntry(
$entry,
$newTag->getLabel(),
[$newTag]
);
$entry->removeTag($tag);
}
$this->entityManager->flush();
$this->addFlash(
'notice',
'flashes.tag.notice.tag_renamed'
);
}
return $this->redirect($redirectUrl);
}
/**
* Tag search results with the current search term.
*
* @Route("/tag/search/{filter}", name="tag_this_search")
*
* @return Response
*/
public function tagThisSearchAction($filter, Request $request, EntryRepository $entryRepository)
{
$currentRoute = $request->query->has('currentRoute') ? $request->query->get('currentRoute') : '';
/** @var QueryBuilder $qb */
$qb = $entryRepository->getBuilderForSearchByUser($this->getUser()->getId(), $filter, $currentRoute);
$entries = $qb->getQuery()->getResult();
foreach ($entries as $entry) {
$this->tagsAssigner->assignTagsToEntry(
$entry,
$filter
);
// check to avoid duplicate tags creation
foreach ($this->entityManager->getUnitOfWork()->getScheduledEntityInsertions() as $entity) {
if ($entity instanceof Tag && strtolower($entity->getLabel()) === strtolower($filter)) {
continue 2;
}
$this->entityManager->persist($entry);
}
$this->entityManager->flush();
}
return $this->redirect($this->redirectHelper->to($request->query->get('redirect'), true));
}
/**
* Delete a given tag for the current user.
*
* @Route("/tag/delete/{slug}", name="tag_delete")
* @ParamConverter("tag", options={"mapping": {"slug": "slug"}})
*
* @return Response
*/
public function removeTagAction(Tag $tag, Request $request, EntryRepository $entryRepository)
{
foreach ($tag->getEntriesByUserId($this->getUser()->getId()) as $entry) {
$entryRepository->removeTag($this->getUser()->getId(), $tag);
}
// remove orphan tag in case no entries are associated to it
if (0 === \count($tag->getEntries())) {
$this->entityManager->remove($tag);
$this->entityManager->flush();
}
$redirectUrl = $this->redirectHelper->to($request->query->get('redirect'), true);
return $this->redirect($redirectUrl);
}
/**
* Check if the logged user can manage the given entry.
*/
private function checkUserAction(Entry $entry)
{
if (null === $this->getUser() || $this->getUser()->getId() !== $entry->getUser()->getId()) {
throw $this->createAccessDeniedException('You can not access this entry.');
}
}
}

View file

@ -0,0 +1,197 @@
<?php
namespace Wallabag\CoreBundle\Controller;
use Doctrine\ORM\EntityManagerInterface;
use FOS\UserBundle\Event\UserEvent;
use FOS\UserBundle\FOSUserEvents;
use FOS\UserBundle\Model\UserManagerInterface;
use Pagerfanta\Doctrine\ORM\QueryAdapter as DoctrineORMAdapter;
use Pagerfanta\Exception\OutOfRangeCurrentPageException;
use Pagerfanta\Pagerfanta;
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Google\GoogleAuthenticatorInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\Form;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\Translation\TranslatorInterface;
use Wallabag\CoreBundle\Entity\User;
use Wallabag\CoreBundle\Form\Type\NewUserType;
use Wallabag\CoreBundle\Form\Type\SearchUserType;
use Wallabag\CoreBundle\Form\Type\UserType;
use Wallabag\CoreBundle\Repository\UserRepository;
/**
* User controller.
*/
class UserController extends AbstractController
{
private EntityManagerInterface $entityManager;
private TranslatorInterface $translator;
public function __construct(EntityManagerInterface $entityManager, TranslatorInterface $translator)
{
$this->entityManager = $entityManager;
$this->translator = $translator;
}
/**
* Creates a new User entity.
*
* @Route("/users/new", name="user_new", methods={"GET", "POST"})
*/
public function newAction(Request $request, UserManagerInterface $userManager, EventDispatcherInterface $eventDispatcher)
{
$user = $userManager->createUser();
\assert($user instanceof User);
// enable created user by default
$user->setEnabled(true);
$form = $this->createForm(NewUserType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$userManager->updateUser($user);
// dispatch a created event so the associated config will be created
$event = new UserEvent($user, $request);
$eventDispatcher->dispatch($event, FOSUserEvents::USER_CREATED);
$this->addFlash(
'notice',
$this->translator->trans('flashes.user.notice.added', ['%username%' => $user->getUsername()])
);
return $this->redirectToRoute('user_edit', ['id' => $user->getId()]);
}
return $this->render('User/new.html.twig', [
'user' => $user,
'form' => $form->createView(),
]);
}
/**
* Displays a form to edit an existing User entity.
*
* @Route("/users/{id}/edit", name="user_edit", methods={"GET", "POST"})
*/
public function editAction(Request $request, User $user, UserManagerInterface $userManager, GoogleAuthenticatorInterface $googleAuthenticator)
{
$deleteForm = $this->createDeleteForm($user);
$form = $this->createForm(UserType::class, $user);
$form->handleRequest($request);
// `googleTwoFactor` isn't a field within the User entity, we need to define it's value in a different way
if (true === $user->isGoogleAuthenticatorEnabled() && false === $form->isSubmitted()) {
$form->get('googleTwoFactor')->setData(true);
}
if ($form->isSubmitted() && $form->isValid()) {
// handle creation / reset of the OTP secret if checkbox changed from the previous state
if (true === $form->get('googleTwoFactor')->getData() && false === $user->isGoogleAuthenticatorEnabled()) {
$user->setGoogleAuthenticatorSecret($googleAuthenticator->generateSecret());
$user->setEmailTwoFactor(false);
} elseif (false === $form->get('googleTwoFactor')->getData() && true === $user->isGoogleAuthenticatorEnabled()) {
$user->setGoogleAuthenticatorSecret(null);
}
$userManager->updateUser($user);
$this->addFlash(
'notice',
$this->translator->trans('flashes.user.notice.updated', ['%username%' => $user->getUsername()])
);
return $this->redirectToRoute('user_edit', ['id' => $user->getId()]);
}
return $this->render('User/edit.html.twig', [
'user' => $user,
'edit_form' => $form->createView(),
'delete_form' => $deleteForm->createView(),
]);
}
/**
* Deletes a User entity.
*
* @Route("/users/{id}", name="user_delete", methods={"DELETE"})
*/
public function deleteAction(Request $request, User $user)
{
$form = $this->createDeleteForm($user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->addFlash(
'notice',
$this->translator->trans('flashes.user.notice.deleted', ['%username%' => $user->getUsername()])
);
$this->entityManager->remove($user);
$this->entityManager->flush();
}
return $this->redirectToRoute('user_index');
}
/**
* @param int $page
*
* @Route("/users/list/{page}", name="user_index", defaults={"page" = 1})
*
* Default parameter for page is hardcoded (in duplication of the defaults from the Route)
* because this controller is also called inside the layout template without any page as argument
*
* @return Response
*/
public function searchFormAction(Request $request, UserRepository $userRepository, $page = 1)
{
$qb = $userRepository->createQueryBuilder('u');
$form = $this->createForm(SearchUserType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$searchTerm = (isset($request->get('search_user')['term']) ? $request->get('search_user')['term'] : '');
$qb = $userRepository->getQueryBuilderForSearch($searchTerm);
}
$pagerAdapter = new DoctrineORMAdapter($qb->getQuery(), true, false);
$pagerFanta = new Pagerfanta($pagerAdapter);
$pagerFanta->setMaxPerPage(50);
try {
$pagerFanta->setCurrentPage($page);
} catch (OutOfRangeCurrentPageException $e) {
if ($page > 1) {
return $this->redirect($this->generateUrl('user_index', ['page' => $pagerFanta->getNbPages()]), 302);
}
}
return $this->render('User/index.html.twig', [
'searchForm' => $form->createView(),
'users' => $pagerFanta,
]);
}
/**
* Create a form to delete a User entity.
*
* @param User $user The User entity
*
* @return FormInterface The form
*/
private function createDeleteForm(User $user)
{
return $this->createFormBuilder()
->setAction($this->generateUrl('user_delete', ['id' => $user->getId()]))
->setMethod('DELETE')
->getForm()
;
}
}