diff --git a/app/AppKernel.php b/app/AppKernel.php index d134de3cc..601b52c3f 100644 --- a/app/AppKernel.php +++ b/app/AppKernel.php @@ -22,7 +22,8 @@ class AppKernel extends Kernel new Nelmio\ApiDocBundle\NelmioApiDocBundle(), new Nelmio\CorsBundle\NelmioCorsBundle(), new Liip\ThemeBundle\LiipThemeBundle(), - new Wallabag\CoreBundle\WallabagCoreBundle() + new Wallabag\CoreBundle\WallabagCoreBundle(), + new Bazinga\Bundle\HateoasBundle\BazingaHateoasBundle() ); if (in_array($this->getEnvironment(), array('dev', 'test'))) { diff --git a/composer.json b/composer.json index 0b162c028..590e26293 100644 --- a/composer.json +++ b/composer.json @@ -74,6 +74,7 @@ "robmorgan/phinx": "~0.4", "tecnick.com/tcpdf": "~6.2", "simplepie/simplepie": "~1.3.1", + "willdurand/hateoas-bundle": "1.0.*@dev", "htmlawed/htmlawed": "dev-master", "liip/theme-bundle": "1.1.3", "wallabag/PHP-Flash-Messages": "dev-master", @@ -82,7 +83,8 @@ "wallabag/PHPePub": "dev-master", "wallabag/php-readability": "dev-master", "wallabag/phpMobi": "dev-master", - "wallabag/Fivefilters_Libraries": "dev-master" + "wallabag/Fivefilters_Libraries": "dev-master", + "pagerfanta/pagerfanta": "~1.0@dev" }, "require-dev": { "doctrine/doctrine-fixtures-bundle": "dev-master", diff --git a/composer.lock b/composer.lock index a6102a819..66f4738b2 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "fd56c671d70f498ccc1996450479fbdc", + "hash": "43d869c37ae73d7b74d3f77c4028bf9c", "packages": [ { "name": "doctrine/annotations", @@ -1386,16 +1386,16 @@ }, { "name": "michelf/php-markdown", - "version": "1.4.1", + "version": "1.5.0", "source": { "type": "git", "url": "https://github.com/michelf/php-markdown.git", - "reference": "de9a19c7bf352d41cc99ed86c3c0ef17e87394b6" + "reference": "e1aabe18173231ebcefc90e615565742fc1c7fd9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/michelf/php-markdown/zipball/de9a19c7bf352d41cc99ed86c3c0ef17e87394b6", - "reference": "de9a19c7bf352d41cc99ed86c3c0ef17e87394b6", + "url": "https://api.github.com/repos/michelf/php-markdown/zipball/e1aabe18173231ebcefc90e615565742fc1c7fd9", + "reference": "e1aabe18173231ebcefc90e615565742fc1c7fd9", "shasum": "" }, "require": { @@ -1417,36 +1417,36 @@ "BSD-3-Clause" ], "authors": [ - { - "name": "Michel Fortin", - "email": "michel.fortin@michelf.ca", - "homepage": "http://michelf.ca/", - "role": "Developer" - }, { "name": "John Gruber", "homepage": "http://daringfireball.net/" + }, + { + "name": "Michel Fortin", + "email": "michel.fortin@michelf.ca", + "homepage": "https://michelf.ca/", + "role": "Developer" } ], "description": "PHP Markdown", - "homepage": "http://michelf.ca/projects/php-markdown/", + "homepage": "https://michelf.ca/projects/php-markdown/", "keywords": [ "markdown" ], - "time": "2014-05-05 02:43:50" + "time": "2015-03-01 12:03:08" }, { "name": "monolog/monolog", - "version": "1.12.0", + "version": "1.13.0", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "1fbe8c2641f2b163addf49cc5e18f144bec6b19f" + "reference": "c41c218e239b50446fd883acb1ecfd4b770caeae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/1fbe8c2641f2b163addf49cc5e18f144bec6b19f", - "reference": "1fbe8c2641f2b163addf49cc5e18f144bec6b19f", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/c41c218e239b50446fd883acb1ecfd4b770caeae", + "reference": "c41c218e239b50446fd883acb1ecfd4b770caeae", "shasum": "" }, "require": { @@ -1463,6 +1463,7 @@ "phpunit/phpunit": "~4.0", "raven/raven": "~0.5", "ruflin/elastica": "0.90.*", + "swiftmailer/swiftmailer": "~5.3", "videlalvaro/php-amqplib": "~2.4" }, "suggest": { @@ -1479,7 +1480,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.12.x-dev" + "dev-master": "1.13.x-dev" } }, "autoload": { @@ -1505,7 +1506,7 @@ "logging", "psr-3" ], - "time": "2014-12-29 21:29:35" + "time": "2015-03-05 01:12:12" }, { "name": "nelmio/api-doc-bundle", @@ -1637,6 +1638,73 @@ ], "time": "2014-12-10 17:26:49" }, + { + "name": "pagerfanta/pagerfanta", + "version": "v1.0.3", + "source": { + "type": "git", + "url": "https://github.com/whiteoctober/Pagerfanta.git", + "reference": "a874d3612d954dcbbb49e5ffe178890918fb76fb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/whiteoctober/Pagerfanta/zipball/a874d3612d954dcbbb49e5ffe178890918fb76fb", + "reference": "a874d3612d954dcbbb49e5ffe178890918fb76fb", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "doctrine/orm": "~2.3", + "doctrine/phpcr-odm": "1.*", + "jackalope/jackalope-doctrine-dbal": "1.*", + "jmikola/geojson": "~1.0", + "mandango/mandango": "~1.0@dev", + "mandango/mondator": "~1.0@dev", + "phpunit/phpunit": "~4", + "propel/propel1": "~1.6", + "ruflin/elastica": "~1.3", + "solarium/solarium": "~3.1" + }, + "suggest": { + "doctrine/mongodb-odm": "To use the DoctrineODMMongoDBAdapter.", + "doctrine/orm": "To use the DoctrineORMAdapter.", + "doctrine/phpcr-odm": "To use the DoctrineODMPhpcrAdapter. >= 1.1.0", + "mandango/mandango": "To use the MandangoAdapter.", + "propel/propel1": "To use the PropelAdapter", + "solarium/solarium": "To use the SolariumAdapter." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-0": { + "Pagerfanta\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Pablo Díez", + "email": "pablodip@gmail.com" + } + ], + "description": "Pagination for PHP 5.3", + "keywords": [ + "page", + "pagination", + "paginator", + "paging" + ], + "time": "2014-10-06 10:57:25" + }, { "name": "phpcollection/phpcollection", "version": "0.4.0", @@ -2645,7 +2713,7 @@ "description": "Libraries from @fivefilters.", "homepage": "https://github.com/wallabag/Fivefilters_Libraries", "support": { - "source": "https://github.com/wallabag/Fivefilters_Libraries/tree/1.0.0", + "source": "https://github.com/wallabag/Fivefilters_Libraries/tree/master", "issues": "https://github.com/wallabag/Fivefilters_Libraries/issues" }, "time": "2015-01-19 20:19:28" @@ -2690,7 +2758,7 @@ "description": "PHP Classes for dynamically generating EPub files.", "homepage": "https://github.com/wallabag/PHPePub", "support": { - "source": "https://github.com/wallabag/PHPePub/tree/2.1.0" + "source": "https://github.com/wallabag/PHPePub/tree/master" }, "time": "2015-01-19 11:44:19" }, @@ -2727,7 +2795,7 @@ "description": "A simple and smart (or stupid) php5 snippets repository", "homepage": "https://github.com/wallabag/kriss_php5", "support": { - "source": "https://github.com/wallabag/kriss_php5/tree/1.0.0" + "source": "https://github.com/wallabag/kriss_php5/tree/master" }, "time": "2015-01-18 21:21:43" }, @@ -2764,7 +2832,7 @@ "description": "Paginate record sets, not tied in directly to a database.", "homepage": "https://github.com/wallabag/pagination", "support": { - "source": "https://github.com/wallabag/pagination/tree/1.0.0" + "source": "https://github.com/wallabag/pagination/tree/master" }, "time": "2015-01-19 09:24:39" }, @@ -2810,7 +2878,7 @@ "sessions" ], "support": { - "source": "https://github.com/wallabag/PHP-Flash-Messages/tree/1.0.0" + "source": "https://github.com/wallabag/PHP-Flash-Messages/tree/master" }, "time": "2015-01-18 19:51:55" }, @@ -2864,7 +2932,7 @@ "html" ], "support": { - "source": "https://github.com/wallabag/php-readability/tree/1.0.0", + "source": "https://github.com/wallabag/php-readability/tree/master", "issues": "https://github.com/wallabag/php-readability/issues" }, "time": "2015-01-19 12:25:38" @@ -2902,10 +2970,126 @@ "description": "An experimental Mobipocket file creator in PHP.", "homepage": "https://github.com/wallabag/phpMobi", "support": { - "source": "https://github.com/wallabag/phpMobi/tree/1.0.0" + "source": "https://github.com/wallabag/phpMobi/tree/master" }, "time": "2015-01-19 12:43:17" }, + { + "name": "willdurand/hateoas", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/willdurand/Hateoas.git", + "reference": "89fe19ad9ce25f15323d76ac22272282ae8a9f14" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/willdurand/Hateoas/zipball/89fe19ad9ce25f15323d76ac22272282ae8a9f14", + "reference": "89fe19ad9ce25f15323d76ac22272282ae8a9f14", + "shasum": "" + }, + "require": { + "doctrine/annotations": "~1.0", + "doctrine/common": "~2.0", + "jms/metadata": "~1.1", + "jms/serializer": "~0.13@dev", + "symfony/expression-language": "~2.4" + }, + "require-dev": { + "atoum/atoum": "*@dev", + "hautelook/frankenstein": "~0.1", + "pagerfanta/pagerfanta": "~1.0", + "phpunit/phpunit": "~3.7", + "symfony/dependency-injection": "~2.0", + "symfony/routing": "~2.0", + "symfony/yaml": "~2.0", + "twig/twig": "~1.12" + }, + "suggest": { + "symfony/routing": "To use the SymfonyRouteFactory.", + "symfony/yaml": "To use yaml based configuration.", + "twig/twig": "To use the Twig extensions." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.4-dev" + } + }, + "autoload": { + "psr-0": { + "Hateoas": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Adrien Brault", + "email": "adrien.brault@gmail.com" + }, + { + "name": "William Durand", + "email": "william.durand1@gmail.com" + } + ], + "description": "A PHP library to support implementing representations for HATEOAS REST web services", + "time": "2015-02-24 15:28:33" + }, + { + "name": "willdurand/hateoas-bundle", + "version": "dev-master", + "target-dir": "Bazinga/Bundle/HateoasBundle", + "source": { + "type": "git", + "url": "https://github.com/willdurand/BazingaHateoasBundle.git", + "reference": "3c86e8080e8a229365a0ce91818da6fe6562376b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/willdurand/BazingaHateoasBundle/zipball/3c86e8080e8a229365a0ce91818da6fe6562376b", + "reference": "3c86e8080e8a229365a0ce91818da6fe6562376b", + "shasum": "" + }, + "require": { + "jms/serializer-bundle": "~0.13", + "symfony/framework-bundle": "~2.2", + "willdurand/hateoas": "~2.0" + }, + "require-dev": { + "symfony/expression-language": "~2.4", + "twig/twig": "~1.12" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-0": { + "Bazinga\\Bundle\\HateoasBundle": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "William Durand", + "email": "william.durand1@gmail.com" + } + ], + "description": "Integration of Hateoas into Symfony2.", + "keywords": [ + "HATEOAS", + "rest" + ], + "time": "2015-02-19 16:27:51" + }, { "name": "willdurand/jsonp-callback-validator", "version": "v1.1.0", @@ -4068,6 +4252,7 @@ "minimum-stability": "dev", "stability-flags": { "nelmio/cors-bundle": 20, + "willdurand/hateoas-bundle": 20, "htmlawed/htmlawed": 20, "wallabag/php-flash-messages": 20, "wallabag/kriss_php5": 20, @@ -4076,6 +4261,7 @@ "wallabag/php-readability": 20, "wallabag/phpmobi": 20, "wallabag/fivefilters_libraries": 20, + "pagerfanta/pagerfanta": 20, "doctrine/doctrine-fixtures-bundle": 20 }, "prefer-stable": true, diff --git a/src/Wallabag/CoreBundle/Controller/EntryController.php b/src/Wallabag/CoreBundle/Controller/EntryController.php index 81ab77887..8a8f3cd79 100644 --- a/src/Wallabag/CoreBundle/Controller/EntryController.php +++ b/src/Wallabag/CoreBundle/Controller/EntryController.php @@ -192,8 +192,9 @@ class EntryController extends Controller { $this->checkUserAction($entry); - $entry->setDeleted(1); - $this->getDoctrine()->getManager()->flush(); + $em = $this->getDoctrine()->getManager(); + $em->remove($entry); + $em->flush(); $this->get('session')->getFlashBag()->add( 'notice', diff --git a/src/Wallabag/CoreBundle/Controller/WallabagRestController.php b/src/Wallabag/CoreBundle/Controller/WallabagRestController.php index e9cd8c939..14f42c488 100644 --- a/src/Wallabag/CoreBundle/Controller/WallabagRestController.php +++ b/src/Wallabag/CoreBundle/Controller/WallabagRestController.php @@ -5,13 +5,40 @@ namespace Wallabag\CoreBundle\Controller; use Nelmio\ApiDocBundle\Annotation\ApiDoc; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Component\HttpFoundation\Response; use Wallabag\CoreBundle\Entity\Entry; use Wallabag\CoreBundle\Entity\Tag; use Wallabag\CoreBundle\Service\Extractor; +use Hateoas\Configuration\Route; +use Hateoas\Representation\Factory\PagerfantaFactory; class WallabagRestController extends Controller { + /** + * @param Entry $entry + * @param string $tags + */ + private function assignTagsToEntry(Entry $entry, $tags) + { + foreach (explode(',', $tags) as $label) { + $label = trim($label); + $tagEntity = $this + ->getDoctrine() + ->getRepository('WallabagCoreBundle:Tag') + ->findOneByLabel($label); + + if (is_null($tagEntity)) { + $tagEntity = new Tag($this->getUser()); + $tagEntity->setLabel($label); + } + + // only add the tag on the entry if the relation doesn't exist + if (!$entry->getTags()->contains($tagEntity)) { + $entry->addTag($tagEntity); + } + } + } + /** * Retrieve salt for a giver user. * @@ -42,7 +69,6 @@ class WallabagRestController extends Controller * parameters={ * {"name"="archive", "dataType"="boolean", "required"=false, "format"="true or false, all entries by default", "description"="filter by archived status."}, * {"name"="star", "dataType"="boolean", "required"=false, "format"="true or false, all entries by default", "description"="filter by starred status."}, - * {"name"="delete", "dataType"="boolean", "required"=false, "format"="true or false, default '0'", "description"="filter by deleted status."}, * {"name"="sort", "dataType"="string", "required"=false, "format"="'created' or 'updated', default 'created'", "description"="sort entries by date."}, * {"name"="order", "dataType"="string", "required"=false, "format"="'asc' or 'desc', default 'desc'", "description"="order of sort."}, * {"name"="page", "dataType"="integer", "required"=false, "format"="default '1'", "description"="what page you want."}, @@ -56,23 +82,33 @@ class WallabagRestController extends Controller { $isArchived = $request->query->get('archive'); $isStarred = $request->query->get('star'); - $isDeleted = $request->query->get('delete', 0); $sort = $request->query->get('sort', 'created'); $order = $request->query->get('order', 'desc'); - $page = $request->query->get('page', 1); - $perPage = $request->query->get('perPage', 30); + $page = (int) $request->query->get('page', 1); + $perPage = (int) $request->query->get('perPage', 30); $tags = $request->query->get('tags', array()); - $entries = $this + $pager = $this ->getDoctrine() ->getRepository('WallabagCoreBundle:Entry') - ->findEntries($this->getUser()->getId(), $isArchived, $isStarred, $isDeleted, $sort, $order); + ->findEntries($this->getUser()->getId(), $isArchived, $isStarred, $sort, $order); - if (!($entries)) { + if (0 === $pager->getNbResults()) { throw $this->createNotFoundException(); } - return $entries; + $pager->setCurrentPage($page); + $pager->setMaxPerPage($perPage); + + $pagerfantaFactory = new PagerfantaFactory('page', 'perPage'); + $paginatedCollection = $pagerfantaFactory->createRepresentation( + $pager, + new Route('api_get_entries', [], $absolute = true) + ); + + $json = $this->get('serializer')->serialize($paginatedCollection, 'json'); + + return new Response($json, 200, array('application/json')); } /** @@ -87,7 +123,13 @@ class WallabagRestController extends Controller */ public function getEntryAction(Entry $entry) { - return $entry; + if ($entry->getUser()->getId() != $this->getUser()->getId()) { + throw $this->createAccessDeniedException('Access forbidden. Entry user id: '.$entry->getUser()->getId().', logged user id: '.$this->getUser()->getId()); + } + + $json = $this->get('serializer')->serialize($entry, 'json'); + + return new Response($json, 200, array('application/json')); } /** @@ -104,7 +146,6 @@ class WallabagRestController extends Controller */ public function postEntriesAction(Request $request) { - //TODO gérer si on passe les tags $url = $request->request->get('url'); $content = Extractor::extract($url); @@ -112,11 +153,19 @@ class WallabagRestController extends Controller $entry->setUrl($url); $entry->setTitle($request->request->get('title') ?: $content->getTitle()); $entry->setContent($content->getBody()); + + $tags = $request->request->get('tags', ''); + if (!empty($tags)) { + $this->assignTagsToEntry($entry, $tags); + } + $em = $this->getDoctrine()->getManager(); $em->persist($entry); $em->flush(); - return $entry; + $json = $this->get('serializer')->serialize($entry, 'json'); + + return new Response($json, 200, array('application/json')); } /** @@ -131,17 +180,18 @@ class WallabagRestController extends Controller * {"name"="tags", "dataType"="string", "required"=false, "format"="tag1,tag2,tag3", "description"="a comma-separated list of tags."}, * {"name"="archive", "dataType"="boolean", "required"=false, "format"="true or false", "description"="archived the entry."}, * {"name"="star", "dataType"="boolean", "required"=false, "format"="true or false", "description"="starred the entry."}, - * {"name"="delete", "dataType"="boolean", "required"=false, "format"="true or false", "description"="flag as deleted. Default false. In case that you don't want to *really* remove it.."}, - * } + * } * ) * @return Entry */ public function patchEntriesAction(Entry $entry, Request $request) { + if ($entry->getUser()->getId() != $this->getUser()->getId()) { + throw $this->createAccessDeniedException('Access forbidden. Entry user id: '.$entry->getUser()->getId().', logged user id: '.$this->getUser()->getId()); + } + $title = $request->request->get("title"); - $tags = $request->request->get("tags", array()); $isArchived = $request->request->get("archive"); - $isDeleted = $request->request->get("delete"); $isStarred = $request->request->get("star"); if (!is_null($title)) { @@ -152,18 +202,21 @@ class WallabagRestController extends Controller $entry->setArchived($isArchived); } - if (!is_null($isDeleted)) { - $entry->setDeleted($isDeleted); - } - if (!is_null($isStarred)) { $entry->setStarred($isStarred); } + $tags = $request->request->get('tags', ''); + if (!empty($tags)) { + $this->assignTagsToEntry($entry, $tags); + } + $em = $this->getDoctrine()->getManager(); $em->flush(); - return $entry; + $json = $this->get('serializer')->serialize($entry, 'json'); + + return new Response($json, 200, array('application/json')); } /** @@ -178,15 +231,17 @@ class WallabagRestController extends Controller */ public function deleteEntriesAction(Entry $entry) { - if ($entry->isDeleted()) { - throw new NotFoundHttpException('This entry is already deleted'); + if ($entry->getUser()->getId() != $this->getUser()->getId()) { + throw $this->createAccessDeniedException('Access forbidden. Entry user id: '.$entry->getUser()->getId().', logged user id: '.$this->getUser()->getId()); } $em = $this->getDoctrine()->getManager(); - $entry->setDeleted(1); + $em->remove($entry); $em->flush(); - return $entry; + $json = $this->get('serializer')->serialize($entry, 'json'); + + return new Response($json, 200, array('application/json')); } /** @@ -200,6 +255,13 @@ class WallabagRestController extends Controller */ public function getEntriesTagsAction(Entry $entry) { + if ($entry->getUser()->getId() != $this->getUser()->getId()) { + throw $this->createAccessDeniedException('Access forbidden. Entry user id: '.$entry->getUser()->getId().', logged user id: '.$this->getUser()->getId()); + } + + $json = $this->get('serializer')->serialize($entry->getTags(), 'json'); + + return new Response($json, 200, array('application/json')); } /** @@ -214,8 +276,24 @@ class WallabagRestController extends Controller * } * ) */ - public function postEntriesTagsAction(Entry $entry) + public function postEntriesTagsAction(Request $request, Entry $entry) { + if ($entry->getUser()->getId() != $this->getUser()->getId()) { + throw $this->createAccessDeniedException('Access forbidden. Entry user id: '.$entry->getUser()->getId().', logged user id: '.$this->getUser()->getId()); + } + + $tags = $request->request->get('tags', ''); + if (!empty($tags)) { + $this->assignTagsToEntry($entry, $tags); + } + + $em = $this->getDoctrine()->getManager(); + $em->persist($entry); + $em->flush(); + + $json = $this->get('serializer')->serialize($entry, 'json'); + + return new Response($json, 200, array('application/json')); } /** @@ -230,29 +308,30 @@ class WallabagRestController extends Controller */ public function deleteEntriesTagsAction(Entry $entry, Tag $tag) { + if ($entry->getUser()->getId() != $this->getUser()->getId()) { + throw $this->createAccessDeniedException('Access forbidden. Entry user id: '.$entry->getUser()->getId().', logged user id: '.$this->getUser()->getId()); + } + + $entry->removeTag($tag); + $em = $this->getDoctrine()->getManager(); + $em->persist($entry); + $em->flush(); + + $json = $this->get('serializer')->serialize($entry, 'json'); + + return new Response($json, 200, array('application/json')); } /** * Retrieve all tags * - * @ApiDoc( - * ) + * @ApiDoc() */ public function getTagsAction() { - } + $json = $this->get('serializer')->serialize($this->getUser()->getTags(), 'json'); - /** - * Retrieve a single tag - * - * @ApiDoc( - * requirements={ - * {"name"="tag", "dataType"="string", "requirement"="\w+", "description"="The tag"} - * } - * ) - */ - public function getTagAction(Tag $tag) - { + return new Response($json, 200, array('application/json')); } /** @@ -266,5 +345,16 @@ class WallabagRestController extends Controller */ public function deleteTagAction(Tag $tag) { + if ($tag->getUser()->getId() != $this->getUser()->getId()) { + throw $this->createAccessDeniedException('Access forbidden. Entry user id: '.$tag->getUser()->getId().', logged user id: '.$this->getUser()->getId()); + } + + $em = $this->getDoctrine()->getManager(); + $em->remove($tag); + $em->flush(); + + $json = $this->get('serializer')->serialize($tag, 'json'); + + return new Response($json, 200, array('application/json')); } } diff --git a/src/Wallabag/CoreBundle/DataFixtures/ORM/LoadEntryData.php b/src/Wallabag/CoreBundle/DataFixtures/ORM/LoadEntryData.php index 3be323ed7..ce12ec5d5 100644 --- a/src/Wallabag/CoreBundle/DataFixtures/ORM/LoadEntryData.php +++ b/src/Wallabag/CoreBundle/DataFixtures/ORM/LoadEntryData.php @@ -6,6 +6,7 @@ use Doctrine\Common\DataFixtures\AbstractFixture; use Doctrine\Common\DataFixtures\OrderedFixtureInterface; use Doctrine\Common\Persistence\ObjectManager; use Wallabag\CoreBundle\Entity\Entry; +use Wallabag\CoreBundle\Entity\Tag; class LoadEntryData extends AbstractFixture implements OrderedFixtureInterface { @@ -37,10 +38,35 @@ class LoadEntryData extends AbstractFixture implements OrderedFixtureInterface $entry3->setTitle('test title entry3'); $entry3->setContent('This is my content /o/'); + $tag1 = new Tag($this->getReference('bob-user')); + $tag1->setLabel("foo"); + $tag2 = new Tag($this->getReference('bob-user')); + $tag2->setLabel("bar"); + + $entry3->addTag($tag1); + $entry3->addTag($tag2); + $manager->persist($entry3); $this->addReference('entry3', $entry3); + $entry4 = new Entry($this->getReference('admin-user')); + $entry4->setUrl('http://0.0.0.0'); + $entry4->setTitle('test title entry4'); + $entry4->setContent('This is my content /o/'); + + $tag1 = new Tag($this->getReference('admin-user')); + $tag1->setLabel("foo"); + $tag2 = new Tag($this->getReference('admin-user')); + $tag2->setLabel("bar"); + + $entry4->addTag($tag1); + $entry4->addTag($tag2); + + $manager->persist($entry4); + + $this->addReference('entry4', $entry4); + $manager->flush(); } diff --git a/src/Wallabag/CoreBundle/Entity/Entry.php b/src/Wallabag/CoreBundle/Entity/Entry.php index 937213b44..75aeae84b 100644 --- a/src/Wallabag/CoreBundle/Entity/Entry.php +++ b/src/Wallabag/CoreBundle/Entity/Entry.php @@ -2,19 +2,24 @@ namespace Wallabag\CoreBundle\Entity; +use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; +use Hateoas\Configuration\Annotation as Hateoas; +use JMS\Serializer\Annotation\XmlRoot; /** * Entry * + * @XmlRoot("entry") * @ORM\Entity(repositoryClass="Wallabag\CoreBundle\Repository\EntryRepository") * @ORM\Table(name="entry") * @ORM\HasLifecycleCallbacks() - * + * @Hateoas\Relation("self", href = "expr('/api/entries/' ~ object.getId())") */ class Entry { + /** @Serializer\XmlAttribute */ /** * @var integer * @@ -53,13 +58,6 @@ class Entry */ private $isStarred = false; - /** - * @var boolean - * - * @ORM\Column(name="is_deleted", type="boolean") - */ - private $isDeleted = false; - /** * @var string * @@ -121,12 +119,19 @@ class Entry */ private $user; + /** + * @ORM\ManyToMany(targetEntity="Tag", inversedBy="entries", cascade={"persist"}) + * @ORM\JoinTable(name="entry_tags") + */ + private $tags; + /* * @param User $user */ public function __construct(User $user) { $this->user = $user; + $this->tags = new ArrayCollection(); } /** @@ -276,22 +281,6 @@ class Entry return $this->user; } - /** - * @return string - */ - public function isDeleted() - { - return $this->isDeleted; - } - - /** - * @param string $isDeleted - */ - public function setDeleted($isDeleted) - { - $this->isDeleted = $isDeleted; - } - /** * @return string */ @@ -400,4 +389,26 @@ class Entry { $this->isPublic = $isPublic; } + + /** + * @return ArrayCollection + */ + public function getTags() + { + return $this->tags; + } + + /** + * @param Tag $tag + */ + public function addTag(Tag $tag) + { + $this->tags[] = $tag; + $tag->addEntry($this); + } + + public function removeTag(Tag $tag) + { + $this->tags->removeElement($tag); + } } diff --git a/src/Wallabag/CoreBundle/Entity/Tag.php b/src/Wallabag/CoreBundle/Entity/Tag.php index 310175635..9ae5867c6 100644 --- a/src/Wallabag/CoreBundle/Entity/Tag.php +++ b/src/Wallabag/CoreBundle/Entity/Tag.php @@ -3,18 +3,25 @@ namespace Wallabag\CoreBundle\Entity; use Doctrine\ORM\Mapping as ORM; +use JMS\Serializer\Annotation\XmlRoot; +use JMS\Serializer\Annotation\ExclusionPolicy; +use JMS\Serializer\Annotation\Expose; +use Doctrine\Common\Collections\ArrayCollection; /** * Tag * + * @XmlRoot("tag") * @ORM\Table(name="tag") - * @ORM\Entity + * @ORM\Entity(repositoryClass="Wallabag\CoreBundle\Repository\TagRepository") + * @ExclusionPolicy("all") */ class Tag { /** * @var integer * + * @Expose * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") @@ -24,10 +31,26 @@ class Tag /** * @var string * + * @Expose * @ORM\Column(name="label", type="text") */ private $label; + /** + * @ORM\ManyToMany(targetEntity="Entry", mappedBy="tags", cascade={"persist"}) + */ + private $entries; + + /** + * @ORM\ManyToOne(targetEntity="User", inversedBy="tags") + */ + private $user; + + public function __construct(User $user) + { + $this->user = $user; + $this->entries = new ArrayCollection(); + } /** * Get id * @@ -58,6 +81,19 @@ class Tag */ public function getLabel() { - return $this->value; + return $this->label; + } + + public function addEntry(Entry $entry) + { + $this->entries[] = $entry; + } + + /** + * @return User + */ + public function getUser() + { + return $this->user; } } diff --git a/src/Wallabag/CoreBundle/Entity/TagsEntries.php b/src/Wallabag/CoreBundle/Entity/TagsEntries.php deleted file mode 100644 index 228263875..000000000 --- a/src/Wallabag/CoreBundle/Entity/TagsEntries.php +++ /dev/null @@ -1,93 +0,0 @@ -id; - } - - /** - * Set entryId - * - * @param integer $entryId - * @return TagsEntries - */ - public function setEntryId($entryId) - { - $this->entryId = $entryId; - - return $this; - } - - /** - * Get entryId - * - * @return integer - */ - public function getEntryId() - { - return $this->entryId; - } - - /** - * Set tagId - * - * @param integer $tagId - * @return TagsEntries - */ - public function setTagId($tagId) - { - $this->tagId = $tagId; - - return $this; - } - - /** - * Get tagId - * - * @return integer - */ - public function getTagId() - { - return $this->tagId; - } -} diff --git a/src/Wallabag/CoreBundle/Entity/User.php b/src/Wallabag/CoreBundle/Entity/User.php index ed5cfe535..f05c8760e 100644 --- a/src/Wallabag/CoreBundle/Entity/User.php +++ b/src/Wallabag/CoreBundle/Entity/User.php @@ -7,6 +7,8 @@ use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\AdvancedUserInterface; use Symfony\Component\Validator\Constraints as Assert; +use JMS\Serializer\Annotation\ExclusionPolicy; +use JMS\Serializer\Annotation\Expose; /** * User @@ -14,12 +16,14 @@ use Symfony\Component\Validator\Constraints as Assert; * @ORM\Table(name="user") * @ORM\Entity * @ORM\HasLifecycleCallbacks() + * @ExclusionPolicy("all") */ class User implements AdvancedUserInterface, \Serializable { /** * @var integer * + * @Expose * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") @@ -97,10 +101,17 @@ class User implements AdvancedUserInterface, \Serializable */ private $config; + /** + * @ORM\OneToMany(targetEntity="Tag", mappedBy="user", cascade={"remove"}) + */ + private $tags; + public function __construct() { - $this->salt = md5(uniqid(null, true)); - $this->entries = new ArrayCollection(); + $this->isActive = true; + $this->salt = md5(uniqid(null, true)); + $this->entries = new ArrayCollection(); + $this->tags = new ArrayCollection(); } /** @@ -274,6 +285,25 @@ class User implements AdvancedUserInterface, \Serializable return $this->entries; } + /** + * @param Entry $entry + * + * @return User + */ + public function addTag(Tag $tag) + { + $this->tags[] = $tag; + + return $this; + } + + /** + * @return ArrayCollection + */ + public function getTags() + { + return $this->tags; + } /** * @inheritDoc */ diff --git a/src/Wallabag/CoreBundle/Repository/EntryRepository.php b/src/Wallabag/CoreBundle/Repository/EntryRepository.php index bedc90d2b..53e8e2ba1 100644 --- a/src/Wallabag/CoreBundle/Repository/EntryRepository.php +++ b/src/Wallabag/CoreBundle/Repository/EntryRepository.php @@ -4,6 +4,8 @@ namespace Wallabag\CoreBundle\Repository; use Doctrine\ORM\EntityRepository; use Doctrine\ORM\Tools\Pagination\Paginator; +use Pagerfanta\Adapter\DoctrineORMAdapter; +use Pagerfanta\Pagerfanta; class EntryRepository extends EntityRepository { @@ -24,7 +26,6 @@ class EntryRepository extends EntityRepository ->leftJoin('e.user', 'u') ->where('e.isArchived = false') ->andWhere('u.id =:userId')->setParameter('userId', $userId) - ->andWhere('e.isDeleted=false') ->orderBy('e.createdAt', 'desc') ->getQuery(); @@ -51,7 +52,6 @@ class EntryRepository extends EntityRepository ->leftJoin('e.user', 'u') ->where('e.isArchived = true') ->andWhere('u.id =:userId')->setParameter('userId', $userId) - ->andWhere('e.isDeleted=false') ->orderBy('e.createdAt', 'desc') ->getQuery(); @@ -78,7 +78,6 @@ class EntryRepository extends EntityRepository ->leftJoin('e.user', 'u') ->where('e.isStarred = true') ->andWhere('u.id =:userId')->setParameter('userId', $userId) - ->andWhere('e.isDeleted = false') ->orderBy('e.createdAt', 'desc') ->getQuery(); @@ -93,17 +92,15 @@ class EntryRepository extends EntityRepository * @param int $userId * @param bool $isArchived * @param bool $isStarred - * @param bool $isDeleted * @param string $sort * @param string $order * * @return array */ - public function findEntries($userId, $isArchived = null, $isStarred = null, $isDeleted = null, $sort = 'created', $order = 'ASC') + public function findEntries($userId, $isArchived = null, $isStarred = null, $sort = 'created', $order = 'ASC') { $qb = $this->createQueryBuilder('e') - ->leftJoin('e.user', 'u') - ->where('u.id =:userId')->setParameter('userId', $userId); + ->where('e.user =:userId')->setParameter('userId', $userId); if (null !== $isArchived) { $qb->andWhere('e.isArchived =:isArchived')->setParameter('isArchived', (bool) $isArchived); @@ -113,18 +110,31 @@ class EntryRepository extends EntityRepository $qb->andWhere('e.isStarred =:isStarred')->setParameter('isStarred', (bool) $isStarred); } - if (null !== $isDeleted) { - $qb->andWhere('e.isDeleted =:isDeleted')->setParameter('isDeleted', (bool) $isDeleted); - } - if ('created' === $sort) { $qb->orderBy('e.createdAt', $order); } elseif ('updated' === $sort) { $qb->orderBy('e.updatedAt', $order); } - return $qb - ->getQuery() - ->getResult(); + $pagerAdapter = new DoctrineORMAdapter($qb); + + return new Pagerfanta($pagerAdapter); + } + + /** + * Fetch an entry with a tag. Only used for tests. + * + * @return Entry + */ + public function findOneWithTags($userId) + { + $qb = $this->createQueryBuilder('e') + ->innerJoin('e.tags', 't') + ->innerJoin('e.user', 'u') + ->addSelect('t', 'u') + ->where('e.user=:userId')->setParameter('userId', $userId) + ; + + return $qb->getQuery()->getResult(); } } diff --git a/src/Wallabag/CoreBundle/Repository/TagRepository.php b/src/Wallabag/CoreBundle/Repository/TagRepository.php new file mode 100644 index 000000000..52f319f11 --- /dev/null +++ b/src/Wallabag/CoreBundle/Repository/TagRepository.php @@ -0,0 +1,9 @@ +getContainer() ->get('doctrine.orm.entity_manager') ->getRepository('WallabagCoreBundle:Entry') - ->findOneByIsDeleted(false); + ->findOneById(1); $client->request('GET', '/delete/'.$content->getId()); $this->assertEquals(302, $client->getResponse()->getStatusCode()); - $res = $client->getContainer() - ->get('doctrine.orm.entity_manager') - ->getRepository('WallabagCoreBundle:Entry') - ->findOneById($content->getId()); + $client->request('GET', '/delete/'.$content->getId()); - $this->assertEquals($res->isDeleted(), true); + $this->assertEquals(404, $client->getResponse()->getStatusCode()); } public function testViewOtherUserEntry() diff --git a/src/Wallabag/CoreBundle/Tests/Controller/WallabagRestControllerTest.php b/src/Wallabag/CoreBundle/Tests/Controller/WallabagRestControllerTest.php index fcfa8ccf9..0eca7cde6 100644 --- a/src/Wallabag/CoreBundle/Tests/Controller/WallabagRestControllerTest.php +++ b/src/Wallabag/CoreBundle/Tests/Controller/WallabagRestControllerTest.php @@ -44,10 +44,6 @@ class WallabagRestControllerTest extends WallabagTestCase public function testWithBadHeaders() { $client = $this->createClient(); - $client->request('GET', '/api/salts/admin.json'); - $salt = json_decode($client->getResponse()->getContent()); - - $headers = $this->generateHeaders('admin', 'mypassword', $salt[0]); $entry = $client->getContainer() ->get('doctrine.orm.entity_manager') @@ -130,7 +126,7 @@ class WallabagRestControllerTest extends WallabagTestCase $entry = $client->getContainer() ->get('doctrine.orm.entity_manager') ->getRepository('WallabagCoreBundle:Entry') - ->findOneByIsDeleted(false); + ->findOneByUser(1); if (!$entry) { $this->markTestSkipped('No content found in db.'); @@ -140,10 +136,79 @@ class WallabagRestControllerTest extends WallabagTestCase $this->assertEquals(200, $client->getResponse()->getStatusCode()); - $res = $client->getContainer() + // We'll try to delete this entry again + $client->request('GET', '/api/salts/admin.json'); + $salt = json_decode($client->getResponse()->getContent()); + + $headers = $this->generateHeaders('admin', 'mypassword', $salt[0]); + + $client->request('DELETE', '/api/entries/'.$entry->getId().'.json', array(), array(), $headers); + + $this->assertEquals(404, $client->getResponse()->getStatusCode()); + } + + public function testGetTagsEntry() + { + $client = $this->createClient(); + $client->request('GET', '/api/salts/admin.json'); + $salt = json_decode($client->getResponse()->getContent()); + $headers = $this->generateHeaders('admin', 'mypassword', $salt[0]); + + $entry = $client->getContainer() ->get('doctrine.orm.entity_manager') ->getRepository('WallabagCoreBundle:Entry') - ->findOneById($entry->getId()); - $this->assertEquals($res->isDeleted(), true); + ->findOneWithTags(1); + + $entry = $entry[0]; + + if (!$entry) { + $this->markTestSkipped('No content found in db.'); + } + + $tags = array(); + foreach ($entry->getTags() as $tag) { + $tags[] = array('id' => $tag->getId(), 'label' => $tag->getLabel()); + } + + $client->request('GET', '/api/entries/'.$entry->getId().'/tags', array(), array(), $headers); + + $this->assertEquals(json_encode($tags, JSON_HEX_QUOT), $client->getResponse()->getContent()); + } + + public function testPostTagsOnEntry() + { + $client = $this->createClient(); + $client->request('GET', '/api/salts/admin.json'); + $salt = json_decode($client->getResponse()->getContent()); + $headers = $this->generateHeaders('admin', 'mypassword', $salt[0]); + + $entry = $client->getContainer() + ->get('doctrine.orm.entity_manager') + ->getRepository('WallabagCoreBundle:Entry') + ->findOneByUser(1); + + if (!$entry) { + $this->markTestSkipped('No content found in db.'); + } + + $newTags = 'tag1,tag2,tag3'; + + $client->request('POST', '/api/entries/'.$entry->getId().'/tags', array('tags' => $newTags), array(), $headers); + + $this->assertEquals(200, $client->getResponse()->getStatusCode()); + + $entryDB = $client->getContainer() + ->get('doctrine.orm.entity_manager') + ->getRepository('WallabagCoreBundle:Entry') + ->find($entry->getId()); + + $tagsInDB = array(); + foreach ($entryDB->getTags()->toArray() as $tag) { + $tagsInDB[$tag->getId()] = $tag->getLabel(); + } + + foreach (explode(',', $newTags) as $tag) { + $this->assertContains($tag, $tagsInDB); + } } }