aboutsummaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
authorThomas Citharel <tcit@tcit.fr>2017-05-31 09:31:18 +0200
committerThomas Citharel <tcit@tcit.fr>2017-06-23 10:08:54 +0200
commitbf6c0346d8d35a719dd1bff1cb4d573d422f99ff (patch)
tree04fb80e20ad1cffacb72357a632a9afe2ec058cb /src
parentd6d3f4ec3698effd1d4b063e295341791bdcf7d7 (diff)
downloadwallabag-bf6c0346d8d35a719dd1bff1cb4d573d422f99ff.tar.gz
wallabag-bf6c0346d8d35a719dd1bff1cb4d573d422f99ff.tar.zst
wallabag-bf6c0346d8d35a719dd1bff1cb4d573d422f99ff.zip
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
Diffstat (limited to 'src')
-rw-r--r--src/Wallabag/AnnotationBundle/Controller/WallabagAnnotationController.php8
-rw-r--r--src/Wallabag/ApiBundle/Controller/EntryRestController.php42
-rw-r--r--src/Wallabag/CoreBundle/Command/InstallCommand.php2
-rw-r--r--src/Wallabag/CoreBundle/Controller/ConfigController.php53
-rw-r--r--src/Wallabag/CoreBundle/Controller/EntryController.php13
-rw-r--r--src/Wallabag/CoreBundle/Controller/ShareController.php165
-rw-r--r--src/Wallabag/CoreBundle/Controller/TagController.php5
-rw-r--r--src/Wallabag/CoreBundle/Entity/Activity.php295
-rw-r--r--src/Wallabag/CoreBundle/Entity/Config.php2
-rw-r--r--src/Wallabag/CoreBundle/Entity/Entry.php26
-rw-r--r--src/Wallabag/CoreBundle/Entity/Notification.php1
-rw-r--r--src/Wallabag/CoreBundle/Entity/Share.php140
-rw-r--r--src/Wallabag/CoreBundle/Event/Activity/Actions/Annotation/AnnotationCreatedEvent.php8
-rw-r--r--src/Wallabag/CoreBundle/Event/Activity/Actions/Annotation/AnnotationDeletedEvent.php8
-rw-r--r--src/Wallabag/CoreBundle/Event/Activity/Actions/Annotation/AnnotationEditedEvent.php8
-rw-r--r--src/Wallabag/CoreBundle/Event/Activity/Actions/Annotation/AnnotationEvent.php39
-rw-r--r--src/Wallabag/CoreBundle/Event/Activity/Actions/Entry/EntryDeletedEvent.php11
-rw-r--r--src/Wallabag/CoreBundle/Event/Activity/Actions/Entry/EntryEditedEvent.php11
-rw-r--r--src/Wallabag/CoreBundle/Event/Activity/Actions/Entry/EntryEvent.php41
-rw-r--r--src/Wallabag/CoreBundle/Event/Activity/Actions/Entry/EntryFavouriteEvent.php14
-rw-r--r--src/Wallabag/CoreBundle/Event/Activity/Actions/Entry/EntryReadEvent.php14
-rw-r--r--src/Wallabag/CoreBundle/Event/Activity/Actions/Entry/EntrySavedEvent.php11
-rw-r--r--src/Wallabag/CoreBundle/Event/Activity/Actions/Entry/EntryTaggedEvent.php55
-rw-r--r--src/Wallabag/CoreBundle/Event/Activity/Actions/Federation/FederationEvent.php40
-rw-r--r--src/Wallabag/CoreBundle/Event/Activity/Actions/Federation/FollowEvent.php39
-rw-r--r--src/Wallabag/CoreBundle/Event/Activity/Actions/Federation/RecommendedEntryEvent.php43
-rw-r--r--src/Wallabag/CoreBundle/Event/Activity/Actions/Federation/UnfollowEvent.php39
-rw-r--r--src/Wallabag/CoreBundle/Event/Activity/Actions/Share/ShareAcceptedEvent.php11
-rw-r--r--src/Wallabag/CoreBundle/Event/Activity/Actions/Share/ShareCancelledEvent.php11
-rw-r--r--src/Wallabag/CoreBundle/Event/Activity/Actions/Share/ShareCreatedEvent.php11
-rw-r--r--src/Wallabag/CoreBundle/Event/Activity/Actions/Share/ShareDeniedEvent.php11
-rw-r--r--src/Wallabag/CoreBundle/Event/Activity/Actions/Share/ShareEvent.php39
-rw-r--r--src/Wallabag/CoreBundle/Event/Activity/Actions/User/UserDeletedEvent.php8
-rw-r--r--src/Wallabag/CoreBundle/Event/Activity/Actions/User/UserEditedEvent.php8
-rw-r--r--src/Wallabag/CoreBundle/Event/Activity/Actions/User/UserEvent.php41
-rw-r--r--src/Wallabag/CoreBundle/Event/Activity/ActivitySubscriber.php224
-rw-r--r--src/Wallabag/CoreBundle/Event/EntryDeletedEvent.php26
-rw-r--r--src/Wallabag/CoreBundle/Event/EntrySavedEvent.php26
-rw-r--r--src/Wallabag/CoreBundle/Event/Subscriber/DownloadImagesSubscriber.php4
-rw-r--r--src/Wallabag/CoreBundle/Repository/ChangeRepository.php26
-rw-r--r--src/Wallabag/CoreBundle/Repository/EntryRepository.php14
-rw-r--r--src/Wallabag/CoreBundle/Resources/config/services.yml8
-rw-r--r--src/Wallabag/CoreBundle/Resources/translations/messages.en.yml11
-rw-r--r--src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml5
-rw-r--r--src/Wallabag/CoreBundle/Resources/translations/messages.pt.yml2
-rw-r--r--src/Wallabag/CoreBundle/Resources/views/themes/material/Config/index.html.twig45
-rw-r--r--src/Wallabag/CoreBundle/Resources/views/themes/material/layout.html.twig5
-rw-r--r--src/Wallabag/FederationBundle/Command/CreateAccountsCommand.php126
-rw-r--r--src/Wallabag/FederationBundle/Controller/InboxController.php43
-rw-r--r--src/Wallabag/FederationBundle/Controller/LikedController.php19
-rw-r--r--src/Wallabag/FederationBundle/Controller/MetadataController.php69
-rw-r--r--src/Wallabag/FederationBundle/Controller/OutboxController.php23
-rw-r--r--src/Wallabag/FederationBundle/Controller/ProfileController.php425
-rw-r--r--src/Wallabag/FederationBundle/Controller/RecommandController.php34
-rw-r--r--src/Wallabag/FederationBundle/DependencyInjection/Configuration.php25
-rw-r--r--src/Wallabag/FederationBundle/DependencyInjection/WallabagFederationExtension.php26
-rw-r--r--src/Wallabag/FederationBundle/Entity/Account.php307
-rw-r--r--src/Wallabag/FederationBundle/Entity/Instance.php111
-rw-r--r--src/Wallabag/FederationBundle/EventListener/CreateAccountListener.php54
-rw-r--r--src/Wallabag/FederationBundle/Federation/CloudId.php78
-rw-r--r--src/Wallabag/FederationBundle/Form/Type/AccountType.php46
-rw-r--r--src/Wallabag/FederationBundle/Repository/AccountRepository.php48
-rw-r--r--src/Wallabag/FederationBundle/Repository/InstanceRepository.php10
-rw-r--r--src/Wallabag/FederationBundle/Resources/config/services.yml8
-rw-r--r--src/Wallabag/FederationBundle/Resources/views/themes/material/User/followers.html.twig88
-rw-r--r--src/Wallabag/FederationBundle/Resources/views/themes/material/User/profile.html.twig77
-rw-r--r--src/Wallabag/FederationBundle/Resources/views/themes/material/User/profile_header.html.twig35
-rw-r--r--src/Wallabag/FederationBundle/Resources/views/themes/material/User/recommendations.html.twig4
-rw-r--r--src/Wallabag/FederationBundle/WallabagFederationBundle.php9
-rw-r--r--src/Wallabag/UserBundle/Entity/User.php27
70 files changed, 3289 insertions, 72 deletions
diff --git a/src/Wallabag/AnnotationBundle/Controller/WallabagAnnotationController.php b/src/Wallabag/AnnotationBundle/Controller/WallabagAnnotationController.php
index 2b4b0e8d..45fd9cf3 100644
--- a/src/Wallabag/AnnotationBundle/Controller/WallabagAnnotationController.php
+++ b/src/Wallabag/AnnotationBundle/Controller/WallabagAnnotationController.php
@@ -10,6 +10,9 @@ use Wallabag\AnnotationBundle\Entity\Annotation;
10use Wallabag\AnnotationBundle\Form\EditAnnotationType; 10use Wallabag\AnnotationBundle\Form\EditAnnotationType;
11use Wallabag\AnnotationBundle\Form\NewAnnotationType; 11use Wallabag\AnnotationBundle\Form\NewAnnotationType;
12use Wallabag\CoreBundle\Entity\Entry; 12use Wallabag\CoreBundle\Entity\Entry;
13use Wallabag\CoreBundle\Event\Activity\Actions\Annotation\AnnotationCreatedEvent;
14use Wallabag\CoreBundle\Event\Activity\Actions\Annotation\AnnotationDeletedEvent;
15use Wallabag\CoreBundle\Event\Activity\Actions\Annotation\AnnotationEditedEvent;
13 16
14class WallabagAnnotationController extends FOSRestController 17class WallabagAnnotationController extends FOSRestController
15{ 18{
@@ -64,6 +67,8 @@ class WallabagAnnotationController extends FOSRestController
64 $em->persist($annotation); 67 $em->persist($annotation);
65 $em->flush(); 68 $em->flush();
66 69
70 $this->get('event_dispatcher')->dispatch(AnnotationCreatedEvent::NAME, new AnnotationCreatedEvent($annotation));
71
67 $json = $this->get('serializer')->serialize($annotation, 'json'); 72 $json = $this->get('serializer')->serialize($annotation, 'json');
68 73
69 return JsonResponse::fromJsonString($json); 74 return JsonResponse::fromJsonString($json);
@@ -100,6 +105,7 @@ class WallabagAnnotationController extends FOSRestController
100 $em->flush(); 105 $em->flush();
101 106
102 $json = $this->get('serializer')->serialize($annotation, 'json'); 107 $json = $this->get('serializer')->serialize($annotation, 'json');
108 $this->get('event_dispatcher')->dispatch(AnnotationEditedEvent::NAME, new AnnotationEditedEvent($annotation));
103 109
104 return JsonResponse::fromJsonString($json); 110 return JsonResponse::fromJsonString($json);
105 } 111 }
@@ -124,6 +130,8 @@ class WallabagAnnotationController extends FOSRestController
124 $em->remove($annotation); 130 $em->remove($annotation);
125 $em->flush(); 131 $em->flush();
126 132
133 $this->get('event_dispatcher')->dispatch(AnnotationDeletedEvent::NAME, new AnnotationDeletedEvent($annotation));
134
127 $json = $this->get('serializer')->serialize($annotation, 'json'); 135 $json = $this->get('serializer')->serialize($annotation, 'json');
128 136
129 return (new JsonResponse())->setJson($json); 137 return (new JsonResponse())->setJson($json);
diff --git a/src/Wallabag/ApiBundle/Controller/EntryRestController.php b/src/Wallabag/ApiBundle/Controller/EntryRestController.php
index 768c4fdc..d1c95da6 100644
--- a/src/Wallabag/ApiBundle/Controller/EntryRestController.php
+++ b/src/Wallabag/ApiBundle/Controller/EntryRestController.php
@@ -11,8 +11,10 @@ use Symfony\Component\HttpFoundation\JsonResponse;
11use Symfony\Component\Routing\Generator\UrlGeneratorInterface; 11use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
12use Wallabag\CoreBundle\Entity\Entry; 12use Wallabag\CoreBundle\Entity\Entry;
13use Wallabag\CoreBundle\Entity\Tag; 13use Wallabag\CoreBundle\Entity\Tag;
14use Wallabag\CoreBundle\Event\EntrySavedEvent;
15use Wallabag\CoreBundle\Event\EntryDeletedEvent; 14use Wallabag\CoreBundle\Event\EntryDeletedEvent;
15use Wallabag\CoreBundle\Event\EntrySavedEvent;
16use Wallabag\CoreBundle\Event\EntryTaggedEvent;
17use Wallabag\CoreBundle\Event\EntryUpdatedEvent;
16 18
17class EntryRestController extends WallabagRestController 19class EntryRestController extends WallabagRestController
18{ 20{
@@ -356,6 +358,8 @@ class EntryRestController extends WallabagRestController
356 358
357 $this->upsertEntry($entry, $request, true); 359 $this->upsertEntry($entry, $request, true);
358 360
361 $this->get('event_dispatcher')->dispatch(EntryUpdatedEvent::NAME, new EntryUpdatedEvent($entry));
362
359 return $this->sendResponse($entry); 363 return $this->sendResponse($entry);
360 } 364 }
361 365
@@ -397,6 +401,7 @@ class EntryRestController extends WallabagRestController
397 $em->flush(); 401 $em->flush();
398 402
399 // entry saved, dispatch event about it! 403 // entry saved, dispatch event about it!
404 $this->get('event_dispatcher')->dispatch(EntryUpdatedEvent::NAME, new EntryUpdatedEvent($entry));
400 $this->get('event_dispatcher')->dispatch(EntrySavedEvent::NAME, new EntrySavedEvent($entry)); 405 $this->get('event_dispatcher')->dispatch(EntrySavedEvent::NAME, new EntrySavedEvent($entry));
401 406
402 return $this->sendResponse($entry); 407 return $this->sendResponse($entry);
@@ -467,6 +472,7 @@ class EntryRestController extends WallabagRestController
467 $this->validateUserAccess($entry->getUser()->getId()); 472 $this->validateUserAccess($entry->getUser()->getId());
468 473
469 $tags = $request->request->get('tags', ''); 474 $tags = $request->request->get('tags', '');
475 $tagsEntries = [];
470 if (!empty($tags)) { 476 if (!empty($tags)) {
471 $this->get('wallabag_core.tags_assigner')->assignTagsToEntry($entry, $tags); 477 $this->get('wallabag_core.tags_assigner')->assignTagsToEntry($entry, $tags);
472 } 478 }
@@ -475,6 +481,8 @@ class EntryRestController extends WallabagRestController
475 $em->persist($entry); 481 $em->persist($entry);
476 $em->flush(); 482 $em->flush();
477 483
484 $this->get('event_dispatcher')->dispatch(EntryTaggedEvent::NAME, new EntryTaggedEvent($entry, $tagsEntries));
485
478 return $this->sendResponse($entry); 486 return $this->sendResponse($entry);
479 } 487 }
480 488
@@ -668,11 +676,11 @@ class EntryRestController extends WallabagRestController
668 } 676 }
669 677
670 if (!is_null($isArchived)) { 678 if (!is_null($isArchived)) {
671 $entry->setArchived((bool) $isArchived); 679 $entry->setArchived((bool)$isArchived);
672 } 680 }
673 681
674 if (!is_null($isStarred)) { 682 if (!is_null($isStarred)) {
675 $entry->setStarred((bool) $isStarred); 683 $entry->setStarred((bool)$isStarred);
676 } 684 }
677 685
678 if (!empty($tags)) { 686 if (!empty($tags)) {
@@ -680,9 +688,9 @@ class EntryRestController extends WallabagRestController
680 } 688 }
681 689
682 if (!is_null($isPublic)) { 690 if (!is_null($isPublic)) {
683 if (true === (bool) $isPublic && null === $entry->getUid()) { 691 if (true === (bool)$isPublic && null === $entry->getUid()) {
684 $entry->generateUid(); 692 $entry->generateUid();
685 } elseif (false === (bool) $isPublic) { 693 } elseif (false === (bool)$isPublic) {
686 $entry->cleanUid(); 694 $entry->cleanUid();
687 } 695 }
688 } 696 }
@@ -694,4 +702,28 @@ class EntryRestController extends WallabagRestController
694 // entry saved, dispatch event about it! 702 // entry saved, dispatch event about it!
695 $this->get('event_dispatcher')->dispatch(EntrySavedEvent::NAME, new EntrySavedEvent($entry)); 703 $this->get('event_dispatcher')->dispatch(EntrySavedEvent::NAME, new EntrySavedEvent($entry));
696 } 704 }
705
706 /**
707 * Gets history since a date.
708 *
709 * @ApiDoc(
710 * parameters={
711 * {"name"="since", "dataType"="integer", "required"=true, "format"="A timestamp", "description"="Timestamp of the history's start"},
712 * }
713 * )
714 *
715 * @return JsonResponse
716 */
717 public function getEntriesHistoryAction(Request $request)
718 {
719 $this->validateAuthentication();
720
721 $res = $this->getDoctrine()
722 ->getRepository('WallabagCoreBundle:Change')
723 ->findChangesSinceDate($request->query->get('since'));
724
725 $json = $this->get('serializer')->serialize($res, 'json');
726
727 return (new JsonResponse())->setJson($json);
728 }
697} 729}
diff --git a/src/Wallabag/CoreBundle/Command/InstallCommand.php b/src/Wallabag/CoreBundle/Command/InstallCommand.php
index eb725a59..e327bbe5 100644
--- a/src/Wallabag/CoreBundle/Command/InstallCommand.php
+++ b/src/Wallabag/CoreBundle/Command/InstallCommand.php
@@ -286,7 +286,7 @@ class InstallCommand extends ContainerAwareCommand
286 286
287 protected function setupConfig() 287 protected function setupConfig()
288 { 288 {
289 $this->defaultOutput->writeln('<info><comment>Step 4 of 5.</comment> Config setup.</info>'); 289 $this->defaultOutput->writeln('<info><comment>Step 4 of 5.</comment> config setup.</info>');
290 $em = $this->getContainer()->get('doctrine.orm.entity_manager'); 290 $em = $this->getContainer()->get('doctrine.orm.entity_manager');
291 291
292 // cleanup before insert new stuff 292 // cleanup before insert new stuff
diff --git a/src/Wallabag/CoreBundle/Controller/ConfigController.php b/src/Wallabag/CoreBundle/Controller/ConfigController.php
index d4170d39..23af98e5 100644
--- a/src/Wallabag/CoreBundle/Controller/ConfigController.php
+++ b/src/Wallabag/CoreBundle/Controller/ConfigController.php
@@ -10,12 +10,16 @@ use Symfony\Component\HttpFoundation\Request;
10use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; 10use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
11use Wallabag\CoreBundle\Entity\Config; 11use Wallabag\CoreBundle\Entity\Config;
12use Wallabag\CoreBundle\Entity\TaggingRule; 12use Wallabag\CoreBundle\Entity\TaggingRule;
13use Wallabag\CoreBundle\Event\Activity\Actions\User\UserDeletedEvent;
14use Wallabag\CoreBundle\Event\Activity\Actions\User\UserEditedEvent;
13use Wallabag\CoreBundle\Form\Type\ConfigType; 15use Wallabag\CoreBundle\Form\Type\ConfigType;
14use Wallabag\CoreBundle\Form\Type\ChangePasswordType; 16use Wallabag\CoreBundle\Form\Type\ChangePasswordType;
15use Wallabag\CoreBundle\Form\Type\RssType; 17use Wallabag\CoreBundle\Form\Type\RssType;
16use Wallabag\CoreBundle\Form\Type\TaggingRuleType; 18use Wallabag\CoreBundle\Form\Type\TaggingRuleType;
17use Wallabag\CoreBundle\Form\Type\UserInformationType; 19use Wallabag\CoreBundle\Form\Type\UserInformationType;
18use Wallabag\CoreBundle\Tools\Utils; 20use Wallabag\CoreBundle\Tools\Utils;
21use Wallabag\FederationBundle\Form\Type\AccountType;
22use Wallabag\UserBundle\Entity\User;
19 23
20class ConfigController extends Controller 24class ConfigController extends Controller
21{ 25{
@@ -82,6 +86,50 @@ class ConfigController extends Controller
82 if ($userForm->isSubmitted() && $userForm->isValid()) { 86 if ($userForm->isSubmitted() && $userForm->isValid()) {
83 $userManager->updateUser($user, true); 87 $userManager->updateUser($user, true);
84 88
89 $this->get('event_dispatcher')->dispatch(UserEditedEvent::NAME, new UserEditedEvent($user->getAccount()));
90
91 $this->get('session')->getFlashBag()->add(
92 'notice',
93 'flashes.config.notice.user_updated'
94 );
95
96 return $this->redirect($this->generateUrl('config').'#set3');
97 }
98
99 // handle account information
100 $account = $user->getAccount();
101 $accountForm = $this->createForm(AccountType::class, $account, [
102 'action' => $this->generateUrl('config').'#set3',
103 ]);
104 $accountForm->handleRequest($request);
105
106 if ($accountForm->isSubmitted() && $accountForm->isValid()) {
107
108 $avatar = $account->getAvatar();
109 $banner = $account->getBanner();
110
111 if (null !== $avatar) {
112 $avatarFileName = md5(uniqid('', true)) . '.' . $avatar->guessExtension();
113
114 $avatar->move(
115 $this->getParameter('media_directory') . '/avatar',
116 $avatarFileName
117 );
118 $account->setAvatar($avatarFileName);
119 }
120
121 if (null != $banner) {
122 $bannerFileName = md5(uniqid('', true)) . '.' . $banner->guessExtension();
123
124 $banner->move(
125 $this->get('media_directory') . '/banner',
126 $bannerFileName
127 );
128 $account->setBanner($bannerFileName);
129 }
130
131 $this->get('event_dispatcher')->dispatch(UserEditedEvent::NAME, new UserEditedEvent($user));
132
85 $this->get('session')->getFlashBag()->add( 133 $this->get('session')->getFlashBag()->add(
86 'notice', 134 'notice',
87 'flashes.config.notice.user_updated' 135 'flashes.config.notice.user_updated'
@@ -145,6 +193,7 @@ class ConfigController extends Controller
145 'pwd' => $pwdForm->createView(), 193 'pwd' => $pwdForm->createView(),
146 'user' => $userForm->createView(), 194 'user' => $userForm->createView(),
147 'new_tagging_rule' => $newTaggingRule->createView(), 195 'new_tagging_rule' => $newTaggingRule->createView(),
196 'account' => $accountForm->createView(),
148 ], 197 ],
149 'rss' => [ 198 'rss' => [
150 'username' => $user->getUsername(), 199 'username' => $user->getUsername(),
@@ -400,9 +449,13 @@ class ConfigController extends Controller
400 $this->get('security.token_storage')->setToken(null); 449 $this->get('security.token_storage')->setToken(null);
401 $request->getSession()->invalidate(); 450 $request->getSession()->invalidate();
402 451
452 $account = $user->getAccount();
453
403 $em = $this->get('fos_user.user_manager'); 454 $em = $this->get('fos_user.user_manager');
404 $em->deleteUser($user); 455 $em->deleteUser($user);
405 456
457 $this->get('event_dispatcher')->dispatch(UserDeletedEvent::NAME, new UserDeletedEvent($account));
458
406 return $this->redirect($this->generateUrl('fos_user_security_login')); 459 return $this->redirect($this->generateUrl('fos_user_security_login'));
407 } 460 }
408 461
diff --git a/src/Wallabag/CoreBundle/Controller/EntryController.php b/src/Wallabag/CoreBundle/Controller/EntryController.php
index fafa49f1..5e4462ed 100644
--- a/src/Wallabag/CoreBundle/Controller/EntryController.php
+++ b/src/Wallabag/CoreBundle/Controller/EntryController.php
@@ -9,12 +9,14 @@ use Symfony\Bundle\FrameworkBundle\Controller\Controller;
9use Symfony\Component\HttpFoundation\Request; 9use Symfony\Component\HttpFoundation\Request;
10use Symfony\Component\Routing\Generator\UrlGeneratorInterface; 10use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
11use Wallabag\CoreBundle\Entity\Entry; 11use Wallabag\CoreBundle\Entity\Entry;
12use Wallabag\CoreBundle\Event\Activity\Actions\Entry\EntryEditedEvent;
13use Wallabag\CoreBundle\Event\Activity\Actions\Entry\EntryFavouriteEvent;
14use Wallabag\CoreBundle\Event\Activity\Actions\Entry\EntryReadEvent;
15use Wallabag\CoreBundle\Event\Activity\Actions\Entry\EntrySavedEvent;
12use Wallabag\CoreBundle\Form\Type\EntryFilterType; 16use Wallabag\CoreBundle\Form\Type\EntryFilterType;
13use Wallabag\CoreBundle\Form\Type\EditEntryType; 17use Wallabag\CoreBundle\Form\Type\EditEntryType;
14use Wallabag\CoreBundle\Form\Type\NewEntryType; 18use Wallabag\CoreBundle\Form\Type\NewEntryType;
15use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache; 19use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
16use Wallabag\CoreBundle\Event\EntrySavedEvent;
17use Wallabag\CoreBundle\Event\EntryDeletedEvent;
18use Wallabag\CoreBundle\Form\Type\SearchEntryType; 20use Wallabag\CoreBundle\Form\Type\SearchEntryType;
19 21
20class EntryController extends Controller 22class EntryController extends Controller
@@ -405,6 +407,8 @@ class EntryController extends Controller
405 $entry->toggleArchive(); 407 $entry->toggleArchive();
406 $this->getDoctrine()->getManager()->flush(); 408 $this->getDoctrine()->getManager()->flush();
407 409
410 $this->get('event_dispatcher')->dispatch(EntryReadEvent::NAME, new EntryReadEvent($entry));
411
408 $message = 'flashes.entry.notice.entry_unarchived'; 412 $message = 'flashes.entry.notice.entry_unarchived';
409 if ($entry->isArchived()) { 413 if ($entry->isArchived()) {
410 $message = 'flashes.entry.notice.entry_archived'; 414 $message = 'flashes.entry.notice.entry_archived';
@@ -437,6 +441,8 @@ class EntryController extends Controller
437 $entry->toggleStar(); 441 $entry->toggleStar();
438 $this->getDoctrine()->getManager()->flush(); 442 $this->getDoctrine()->getManager()->flush();
439 443
444 $this->get('event_dispatcher')->dispatch(EntryFavouriteEvent::NAME, new EntryFavouriteEvent($entry));
445
440 $message = 'flashes.entry.notice.entry_unstarred'; 446 $message = 'flashes.entry.notice.entry_unstarred';
441 if ($entry->isStarred()) { 447 if ($entry->isStarred()) {
442 $message = 'flashes.entry.notice.entry_starred'; 448 $message = 'flashes.entry.notice.entry_starred';
@@ -473,9 +479,6 @@ class EntryController extends Controller
473 UrlGeneratorInterface::ABSOLUTE_PATH 479 UrlGeneratorInterface::ABSOLUTE_PATH
474 ); 480 );
475 481
476 // entry deleted, dispatch event about it!
477 $this->get('event_dispatcher')->dispatch(EntryDeletedEvent::NAME, new EntryDeletedEvent($entry));
478
479 $em = $this->getDoctrine()->getManager(); 482 $em = $this->getDoctrine()->getManager();
480 $em->remove($entry); 483 $em->remove($entry);
481 $em->flush(); 484 $em->flush();
diff --git a/src/Wallabag/CoreBundle/Controller/ShareController.php b/src/Wallabag/CoreBundle/Controller/ShareController.php
new file mode 100644
index 00000000..d6f83ebc
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Controller/ShareController.php
@@ -0,0 +1,165 @@
1<?php
2
3namespace Wallabag\CoreBundle\Controller;
4
5use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
6use Symfony\Bundle\FrameworkBundle\Controller\Controller;
7use Symfony\Component\HttpFoundation\RedirectResponse;
8use Symfony\Component\Security\Core\Exception\AccessDeniedException;
9use Symfony\Component\Security\Core\Exception\InvalidArgumentException;
10use Wallabag\CoreBundle\Entity\Entry;
11use Wallabag\CoreBundle\Entity\Notification;
12use Wallabag\CoreBundle\Entity\Share;
13use Wallabag\CoreBundle\Event\Activity\Actions\Entry\EntrySavedEvent;
14use Wallabag\CoreBundle\Event\Activity\Actions\Share\ShareAcceptedEvent;
15use Wallabag\CoreBundle\Event\Activity\Actions\Share\ShareCancelledEvent;
16use Wallabag\CoreBundle\Event\Activity\Actions\Share\ShareCreatedEvent;
17use Wallabag\CoreBundle\Event\Activity\Actions\Share\ShareDeniedEvent;
18use Wallabag\CoreBundle\Notifications\NoAction;
19use Wallabag\CoreBundle\Notifications\YesAction;
20use Wallabag\UserBundle\Entity\User;
21
22class ShareController extends Controller
23{
24 /**
25 * @Route("/share-user/{entry}/{destination}", name="share-entry-user", requirements={"entry" = "\d+", "destination" = "\d+"})
26 * @param Entry $entry
27 * @param User $destination
28 * @throws AccessDeniedException
29 * @throws InvalidArgumentException
30 */
31 public function shareEntryAction(Entry $entry, User $destination)
32 {
33
34 if ($entry->getUser() !== $this->getUser()) {
35 throw new AccessDeniedException("You can't share this entry");
36 }
37
38 if ($destination === $this->getUser()) {
39 throw new InvalidArgumentException("You can't share entries to yourself");
40 }
41
42 $share = new Share();
43 $share->setUserOrigin($this->getUser())
44 ->setEntry($entry)
45 ->setUserDestination($destination);
46
47 $em = $this->getDoctrine()->getManager();
48 $em->persist($share);
49 $em->flush();
50
51 $this->get('event_dispatcher')->dispatch(ShareCreatedEvent::NAME, new ShareCancelledEvent($share));
52
53 $accept = new YesAction($this->generateUrl('share-entry-user-accept', ['share' => $share->getId()]));
54
55 $deny = new NoAction($this->generateUrl('share-entry-user-refuse', ['share' => $share->getId()]));
56
57 $notification = new Notification($destination);
58 $notification->setType(Notification::TYPE_SHARE)
59 ->setTitle($this->get('translator')->trans('share.notification.new.title'))
60 ->addAction($accept)
61 ->addAction($deny);
62
63 $em->persist($notification);
64 $em->flush();
65
66 $this->redirectToRoute('view', ['id' => $entry->getId()]);
67 }
68
69 /**
70 * @Route("/share-user/accept/{share}", name="share-entry-user-accept")
71 *
72 * @param Share $share
73 * @return RedirectResponse
74 * @throws AccessDeniedException
75 */
76 public function acceptShareAction(Share $share)
77 {
78 if ($share->getUserDestination() !== $this->getUser()) {
79 throw new AccessDeniedException("You can't accept this entry");
80 }
81
82 $entry = new Entry($this->getUser());
83 $entry->setUrl($share->getEntry()->getUrl());
84
85 $em = $this->getDoctrine()->getManager();
86
87 if (false === $this->checkIfEntryAlreadyExists($entry)) {
88 $this->updateEntry($entry);
89
90 $em->persist($entry);
91 $em->flush();
92
93 $this->get('event_dispatcher')->dispatch(ShareAcceptedEvent::NAME, new ShareAcceptedEvent($share));
94
95 // entry saved, dispatch event about it!
96 $this->get('event_dispatcher')->dispatch(EntrySavedEvent::NAME, new EntrySavedEvent($entry));
97 }
98
99 $em->remove($share);
100 $em->flush(); // we keep the previous flush above in case the event dispatcher would lead in using the saved entry
101
102 return $this->redirect($this->generateUrl('homepage'));
103 }
104
105 /**
106 * @Route("/share-user/refuse/{share}", name="share-entry-user-refuse")
107 *
108 * @param Share $share
109 * @return RedirectResponse
110 */
111 public function refuseShareAction(Share $share)
112 {
113 $em = $this->getDoctrine()->getManager();
114 $em->remove($share);
115 $em->flush();
116
117 $this->get('event_dispatcher')->dispatch(ShareDeniedEvent::NAME, new ShareDeniedEvent($share));
118
119 return $this->redirect($this->generateUrl('homepage'));
120 }
121
122 /**
123 * Fetch content and update entry.
124 * In case it fails, entry will return to avod loosing the data.
125 *
126 * @param Entry $entry
127 * @param string $prefixMessage Should be the translation key: entry_saved or entry_reloaded
128 *
129 * @return Entry
130 */
131 private function updateEntry(Entry $entry, $prefixMessage = 'entry_saved')
132 {
133 // put default title in case of fetching content failed
134 $entry->setTitle('No title found');
135
136 $message = 'flashes.entry.notice.'.$prefixMessage;
137
138 try {
139 $entry = $this->get('wallabag_core.content_proxy')->updateEntry($entry, $entry->getUrl());
140 } catch (\Exception $e) {
141 $this->get('logger')->error('Error while saving an entry', [
142 'exception' => $e,
143 'entry' => $entry,
144 ]);
145
146 $message = 'flashes.entry.notice.'.$prefixMessage.'_failed';
147 }
148
149 $this->get('session')->getFlashBag()->add('notice', $message);
150
151 return $entry;
152 }
153
154 /**
155 * Check for existing entry, if it exists, redirect to it with a message.
156 *
157 * @param Entry $entry
158 *
159 * @return Entry|bool
160 */
161 private function checkIfEntryAlreadyExists(Entry $entry)
162 {
163 return $this->get('wallabag_core.entry_repository')->findByUrlAndUserId($entry->getUrl(), $this->getUser()->getId());
164 }
165}
diff --git a/src/Wallabag/CoreBundle/Controller/TagController.php b/src/Wallabag/CoreBundle/Controller/TagController.php
index a8b1eadd..6cc78458 100644
--- a/src/Wallabag/CoreBundle/Controller/TagController.php
+++ b/src/Wallabag/CoreBundle/Controller/TagController.php
@@ -9,6 +9,7 @@ use Symfony\Bundle\FrameworkBundle\Controller\Controller;
9use Symfony\Component\HttpFoundation\Request; 9use Symfony\Component\HttpFoundation\Request;
10use Wallabag\CoreBundle\Entity\Entry; 10use Wallabag\CoreBundle\Entity\Entry;
11use Wallabag\CoreBundle\Entity\Tag; 11use Wallabag\CoreBundle\Entity\Tag;
12use Wallabag\CoreBundle\Event\Activity\Actions\Entry\EntryTaggedEvent;
12use Wallabag\CoreBundle\Form\Type\NewTagType; 13use Wallabag\CoreBundle\Form\Type\NewTagType;
13use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; 14use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
14 15
@@ -37,6 +38,8 @@ class TagController extends Controller
37 $em->persist($entry); 38 $em->persist($entry);
38 $em->flush(); 39 $em->flush();
39 40
41 $this->get('event_dispatcher')->dispatch(EntryTaggedEvent::NAME, new EntryTaggedEvent($entry, $tags));
42
40 $this->get('session')->getFlashBag()->add( 43 $this->get('session')->getFlashBag()->add(
41 'notice', 44 'notice',
42 'flashes.tag.notice.tag_added' 45 'flashes.tag.notice.tag_added'
@@ -64,6 +67,8 @@ class TagController extends Controller
64 $em = $this->getDoctrine()->getManager(); 67 $em = $this->getDoctrine()->getManager();
65 $em->flush(); 68 $em->flush();
66 69
70 $this->get('event_dispatcher')->dispatch(EntryTaggedEvent::NAME, new EntryTaggedEvent($entry, $tag), true);
71
67 // remove orphan tag in case no entries are associated to it 72 // remove orphan tag in case no entries are associated to it
68 if (count($tag->getEntries()) === 0) { 73 if (count($tag->getEntries()) === 0) {
69 $em->remove($tag); 74 $em->remove($tag);
diff --git a/src/Wallabag/CoreBundle/Entity/Activity.php b/src/Wallabag/CoreBundle/Entity/Activity.php
new file mode 100644
index 00000000..08a3f1fb
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Entity/Activity.php
@@ -0,0 +1,295 @@
1<?php
2
3namespace Wallabag\CoreBundle\Entity;
4
5use Doctrine\ORM\Mapping as ORM;
6use Wallabag\FederationBundle\Entity\Account;
7
8/**
9 * Change.
10 *
11 * This entity stores a datetime for each activity.
12 *
13 * @ORM\Entity(repositoryClass="Wallabag\CoreBundle\Repository\ChangeRepository")
14 * @ORM\Table(name="`activity`")
15 */
16class Activity
17{
18 /**
19 * Object types
20 */
21 const ENTRY_OBJECT = 1;
22 const TAG_OBJECT = 2;
23 const USER_OBJECT = 3;
24 const SHARE_OBJECT = 4;
25 const GROUP_OBJECT = 5;
26 const ANNOTATION_OBJECT = 6;
27 const CONFIG_OBJECT = 7;
28 const ACCOUNT_OBJECT = 8;
29
30 /**
31 * Events
32 */
33
34 /**
35 * Entry events
36 */
37 const ENTRY_ADD = 10; // done
38 const ENTRY_EDIT = 11; // done
39 const ENTRY_READ = 12; // done
40 const ENTRY_UNREAD = 13; // done
41 const ENTRY_FAVOURITE = 14; // done
42 const ENTRY_UNFAVOURITE = 15; // done
43 const ENTRY_DELETE = 19; // done
44
45 /**
46 * Tag events
47 */
48 const TAG_CREATE = 20; // not yet implemented
49 const TAG_EDIT = 21; // not yet implemented
50 const TAG_REMOVE = 29; // not yet implemented
51
52 /**
53 * Entry - Tag events
54 */
55 const ENTRY_ADD_TAG = 30; // done
56 const ENTRY_REMOVE_TAG = 39; // done
57
58 /**
59 * Entry - Annotation events
60 */
61 const ANNOTATION_ADD = 40; // done
62 const ANNOTATION_EDIT = 41; // done
63 const ANNOTATION_REMOVE = 49; // done
64
65 /**
66 * User events
67 */
68 const USER_CREATE = 50; // done
69 const USER_EDIT = 51; // done
70 const USER_REMOVE = 59; // done
71
72 /**
73 * Federation events
74 */
75 const FOLLOW_ACCOUNT = 61;
76 const UNFOLLOW_ACCOUNT = 62;
77 const RECOMMEND_ENTRY = 63;
78
79 /**
80 * Share events
81 */
82 const USER_SHARE_CREATED = 70; // done
83 const USER_SHARE_ACCEPTED = 71; // done
84 const USER_SHARE_REFUSED = 72; // done
85 const USER_SHARE_CANCELLED = 79; // not implemented yet
86
87 /**
88 * Group events
89 */
90 const GROUP_CREATE = 80;
91 const GROUP_EDIT = 81;
92 const GROUP_ADD_MEMBER = 82;
93 const GROUP_EDIT_MEMBER = 83;
94 const GROUP_REMOVE_MEMBER = 84;
95 const GROUP_SHARE_ENTRY = 85;
96 const GROUP_DELETE = 89;
97
98 /**
99 * @var int
100 *
101 * @ORM\Column(type="integer")
102 * @ORM\Id
103 * @ORM\GeneratedValue(strategy="AUTO")
104 */
105 private $id;
106
107 /**
108 * @var int
109 *
110 * @ORM\Column(type="integer")
111 */
112 private $activityType;
113
114 /**
115 * @var Account
116 */
117 private $user;
118
119 /**
120 * @var int
121 *
122 * @ORM\Column(type="integer")
123 */
124 private $primaryObjectType;
125
126 /**
127 * @var int
128 *
129 * @ORM\Column(type="integer")
130 */
131 private $primaryObjectId;
132
133 /**
134 * @var int
135 *
136 * @ORM\Column(type="integer", nullable=true)
137 */
138 private $secondaryObjectType;
139
140 /**
141 * @var int
142 *
143 * @ORM\Column(type="integer", nullable=true)
144 */
145 private $secondaryObjectId;
146
147 /**
148 * @var \DateTime
149 *
150 * @ORM\Column(name="created_at", type="datetime")
151 */
152 private $createdAt;
153
154 public function __construct($activityType, $primaryObjectType, $primaryObjectId)
155 {
156 $this->activityType = $activityType;
157 $this->primaryObjectType = $primaryObjectType;
158 $this->primaryObjectId = $primaryObjectId;
159 $this->createdAt = new \DateTime();
160 }
161
162 /**
163 * @return int
164 */
165 public function getId()
166 {
167 return $this->id;
168 }
169
170 /**
171 * @return int
172 */
173 public function getActivityType()
174 {
175 return $this->activityType;
176 }
177
178 /**
179 * @param int $activityType
180 * @return Activity
181 */
182 public function setActivityType($activityType)
183 {
184 $this->activityType = $activityType;
185 return $this;
186 }
187
188 /**
189 * @return \DateTime
190 */
191 public function getCreatedAt()
192 {
193 return $this->createdAt;
194 }
195
196 /**
197 * @param \DateTime $createdAt
198 * @return Activity
199 */
200 public function setCreatedAt(\DateTime $createdAt)
201 {
202 $this->createdAt = $createdAt;
203 return $this;
204 }
205
206 /**
207 * @return int
208 */
209 public function getPrimaryObjectId()
210 {
211 return $this->primaryObjectId;
212 }
213
214 /**
215 * @param $primaryObjectId
216 * @return Activity
217 */
218 public function setPrimaryObjectId($primaryObjectId)
219 {
220 $this->primaryObjectId = $primaryObjectId;
221 return $this;
222 }
223
224 /**
225 * @return Account
226 */
227 public function getUser()
228 {
229 return $this->user;
230 }
231
232 /**
233 * @param Account $user
234 * @return Activity
235 */
236 public function setUser($user)
237 {
238 $this->user = $user;
239 return $this;
240 }
241
242 /**
243 * @return int
244 */
245 public function getPrimaryObjectType()
246 {
247 return $this->primaryObjectType;
248 }
249
250 /**
251 * @param int $primaryObjectType
252 * @return Activity
253 */
254 public function setPrimaryObjectType($primaryObjectType)
255 {
256 $this->primaryObjectType = $primaryObjectType;
257 return $this;
258 }
259
260 /**
261 * @return int
262 */
263 public function getSecondaryObjectType()
264 {
265 return $this->secondaryObjectType;
266 }
267
268 /**
269 * @param int $secondaryObjectType
270 * @return Activity
271 */
272 public function setSecondaryObjectType($secondaryObjectType)
273 {
274 $this->secondaryObjectType = $secondaryObjectType;
275 return $this;
276 }
277
278 /**
279 * @return int
280 */
281 public function getSecondaryObjectId()
282 {
283 return $this->secondaryObjectId;
284 }
285
286 /**
287 * @param int $secondaryObjectId
288 * @return Activity
289 */
290 public function setSecondaryObjectId($secondaryObjectId)
291 {
292 $this->secondaryObjectId = $secondaryObjectId;
293 return $this;
294 }
295}
diff --git a/src/Wallabag/CoreBundle/Entity/Config.php b/src/Wallabag/CoreBundle/Entity/Config.php
index b902ae2c..f42a49b3 100644
--- a/src/Wallabag/CoreBundle/Entity/Config.php
+++ b/src/Wallabag/CoreBundle/Entity/Config.php
@@ -8,7 +8,7 @@ use Symfony\Component\Validator\Constraints as Assert;
8use Wallabag\UserBundle\Entity\User; 8use Wallabag\UserBundle\Entity\User;
9 9
10/** 10/**
11 * Config. 11 * config.
12 * 12 *
13 * @ORM\Entity(repositoryClass="Wallabag\CoreBundle\Repository\ConfigRepository") 13 * @ORM\Entity(repositoryClass="Wallabag\CoreBundle\Repository\ConfigRepository")
14 * @ORM\Table(name="`config`") 14 * @ORM\Table(name="`config`")
diff --git a/src/Wallabag/CoreBundle/Entity/Entry.php b/src/Wallabag/CoreBundle/Entity/Entry.php
index a0503c39..6ca17126 100644
--- a/src/Wallabag/CoreBundle/Entity/Entry.php
+++ b/src/Wallabag/CoreBundle/Entity/Entry.php
@@ -233,13 +233,21 @@ class Entry
233 */ 233 */
234 private $tags; 234 private $tags;
235 235
236 /* 236 /**
237 * @var boolean
238 *
239 * @ORM\Column(name="recommended", type="boolean", nullable=true)
240 */
241 private $recommended;
242
243 /**
237 * @param User $user 244 * @param User $user
238 */ 245 */
239 public function __construct(User $user) 246 public function __construct(User $user)
240 { 247 {
241 $this->user = $user; 248 $this->user = $user;
242 $this->tags = new ArrayCollection(); 249 $this->tags = new ArrayCollection();
250 $this->changes = new ArrayCollection();
243 } 251 }
244 252
245 /** 253 /**
@@ -778,4 +786,20 @@ class Entry
778 786
779 return $this; 787 return $this;
780 } 788 }
789
790 /**
791 * @return bool
792 */
793 public function isRecommended()
794 {
795 return $this->recommended;
796 }
797
798 /**
799 * @param bool $recommended
800 */
801 public function setRecommended($recommended)
802 {
803 $this->recommended = $recommended;
804 }
781} 805}
diff --git a/src/Wallabag/CoreBundle/Entity/Notification.php b/src/Wallabag/CoreBundle/Entity/Notification.php
index aa4c03c3..d4304f39 100644
--- a/src/Wallabag/CoreBundle/Entity/Notification.php
+++ b/src/Wallabag/CoreBundle/Entity/Notification.php
@@ -91,6 +91,7 @@ class Notification implements NotificationInterface
91 const TYPE_ADMIN = 0; 91 const TYPE_ADMIN = 0;
92 const TYPE_USER = 1; 92 const TYPE_USER = 1;
93 const TYPE_RELEASE = 2; 93 const TYPE_RELEASE = 2;
94 const TYPE_SHARE = 3;
94 95
95 public function __construct(User $user = null) 96 public function __construct(User $user = null)
96 { 97 {
diff --git a/src/Wallabag/CoreBundle/Entity/Share.php b/src/Wallabag/CoreBundle/Entity/Share.php
new file mode 100644
index 00000000..a55b4e67
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Entity/Share.php
@@ -0,0 +1,140 @@
1<?php
2
3namespace Wallabag\CoreBundle\Entity;
4
5use Wallabag\FederationBundle\Entity\Account;
6use Wallabag\UserBundle\Entity\User;
7use Doctrine\ORM\Mapping as ORM;
8
9/**
10 * Share.
11 *
12 * @ORM\Entity
13 */
14class Share
15{
16 /**
17 * @var int
18 *
19 * @ORM\Column(name="id", type="integer")
20 * @ORM\Id
21 * @ORM\GeneratedValue(strategy="AUTO")
22 */
23 private $id;
24
25 /**
26 * @var Account
27 *
28 * @ORM\ManyToOne(targetEntity="Wallabag\FederationBundle\Entity\Account")
29 */
30 private $userOrigin;
31
32 /**
33 * @var Account
34 *
35 * @ORM\ManyToOne(targetEntity="Wallabag\FederationBundle\Entity\Account")
36 */
37 private $userDestination;
38
39 /**
40 * @var Entry
41 *
42 * @ORM\ManyToOne(targetEntity="Wallabag\CoreBundle\Entity\Entry")
43 */
44 private $entry;
45
46 /**
47 * @var boolean
48 *
49 * @ORM\Column(name="accepted", type="boolean")
50 */
51 private $accepted;
52
53 /**
54 * Share constructor.
55 */
56 public function __construct()
57 {
58 $this->accepted = false;
59 }
60
61 /**
62 * @return int
63 */
64 public function getId()
65 {
66 return $this->id;
67 }
68
69 /**
70 * @return Account
71 */
72 public function getUserOrigin()
73 {
74 return $this->userOrigin;
75 }
76
77 /**
78 * @param User $userOrigin
79 * @return Share
80 */
81 public function setUserOrigin(User $userOrigin)
82 {
83 $this->userOrigin = $userOrigin;
84 return $this;
85 }
86
87 /**
88 * @return Account
89 */
90 public function getUserDestination()
91 {
92 return $this->userDestination;
93 }
94
95 /**
96 * @param User $userDestination
97 * @return Share
98 */
99 public function setUserDestination(User $userDestination)
100 {
101 $this->userDestination = $userDestination;
102 return $this;
103 }
104
105 /**
106 * @return bool
107 */
108 public function isAccepted()
109 {
110 return $this->accepted;
111 }
112
113 /**
114 * @param bool $accepted
115 * @return Share
116 */
117 public function setAccepted($accepted)
118 {
119 $this->accepted = $accepted;
120 return $this;
121 }
122
123 /**
124 * @return Entry
125 */
126 public function getEntry()
127 {
128 return $this->entry;
129 }
130
131 /**
132 * @param Entry $entry
133 * @return Share
134 */
135 public function setEntry(Entry $entry)
136 {
137 $this->entry = $entry;
138 return $this;
139 }
140}
diff --git a/src/Wallabag/CoreBundle/Event/Activity/Actions/Annotation/AnnotationCreatedEvent.php b/src/Wallabag/CoreBundle/Event/Activity/Actions/Annotation/AnnotationCreatedEvent.php
new file mode 100644
index 00000000..b3667703
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Event/Activity/Actions/Annotation/AnnotationCreatedEvent.php
@@ -0,0 +1,8 @@
1<?php
2
3namespace Wallabag\CoreBundle\Event\Activity\Actions\Annotation;
4
5class AnnotationCreatedEvent extends AnnotationEvent
6{
7 const NAME = 'annotation.created';
8}
diff --git a/src/Wallabag/CoreBundle/Event/Activity/Actions/Annotation/AnnotationDeletedEvent.php b/src/Wallabag/CoreBundle/Event/Activity/Actions/Annotation/AnnotationDeletedEvent.php
new file mode 100644
index 00000000..60d53849
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Event/Activity/Actions/Annotation/AnnotationDeletedEvent.php
@@ -0,0 +1,8 @@
1<?php
2
3namespace Wallabag\CoreBundle\Event\Activity\Actions\Annotation;
4
5class AnnotationDeletedEvent extends AnnotationEvent
6{
7 const NAME = 'annotation.deleted';
8}
diff --git a/src/Wallabag/CoreBundle/Event/Activity/Actions/Annotation/AnnotationEditedEvent.php b/src/Wallabag/CoreBundle/Event/Activity/Actions/Annotation/AnnotationEditedEvent.php
new file mode 100644
index 00000000..385b8025
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Event/Activity/Actions/Annotation/AnnotationEditedEvent.php
@@ -0,0 +1,8 @@
1<?php
2
3namespace Wallabag\CoreBundle\Event\Activity\Actions\Annotation;
4
5class AnnotationEditedEvent extends AnnotationEvent
6{
7 const NAME = 'annotation.edited';
8}
diff --git a/src/Wallabag/CoreBundle/Event/Activity/Actions/Annotation/AnnotationEvent.php b/src/Wallabag/CoreBundle/Event/Activity/Actions/Annotation/AnnotationEvent.php
new file mode 100644
index 00000000..b4cb93af
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Event/Activity/Actions/Annotation/AnnotationEvent.php
@@ -0,0 +1,39 @@
1<?php
2
3namespace Wallabag\CoreBundle\Event\Activity\Actions\Annotation;
4
5use Symfony\Component\EventDispatcher\Event;
6use Wallabag\AnnotationBundle\Entity\Annotation;
7
8/**
9 * This event is fired when annotation-relative stuff is made.
10 */
11abstract class AnnotationEvent extends Event
12{
13 protected $annotation;
14
15 /**
16 * AnnotationEvent constructor.
17 * @param Annotation $annotation
18 */
19 public function __construct(Annotation $annotation)
20 {
21 $this->annotation = $annotation;
22 }
23
24 /**
25 * @return Annotation
26 */
27 public function getAnnotation()
28 {
29 return $this->annotation;
30 }
31
32 /**
33 * @param Annotation $annotation
34 */
35 public function setAnnotation(Annotation $annotation)
36 {
37 $this->annotation = $annotation;
38 }
39}
diff --git a/src/Wallabag/CoreBundle/Event/Activity/Actions/Entry/EntryDeletedEvent.php b/src/Wallabag/CoreBundle/Event/Activity/Actions/Entry/EntryDeletedEvent.php
new file mode 100644
index 00000000..1d413d41
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Event/Activity/Actions/Entry/EntryDeletedEvent.php
@@ -0,0 +1,11 @@
1<?php
2
3namespace Wallabag\CoreBundle\Event\Activity\Actions\Entry;
4
5/**
6 * This event is fired as soon as an entry is deleted.
7 */
8class EntryDeletedEvent extends EntryEvent
9{
10 const NAME = 'entry.deleted';
11}
diff --git a/src/Wallabag/CoreBundle/Event/Activity/Actions/Entry/EntryEditedEvent.php b/src/Wallabag/CoreBundle/Event/Activity/Actions/Entry/EntryEditedEvent.php
new file mode 100644
index 00000000..f7528bb4
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Event/Activity/Actions/Entry/EntryEditedEvent.php
@@ -0,0 +1,11 @@
1<?php
2
3namespace Wallabag\CoreBundle\Event\Activity\Actions\Entry;
4
5/**
6 * This event is fired as soon as an entry was edited.
7 */
8class EntryEditedEvent extends EntryEvent
9{
10 const NAME = 'entry.edited';
11}
diff --git a/src/Wallabag/CoreBundle/Event/Activity/Actions/Entry/EntryEvent.php b/src/Wallabag/CoreBundle/Event/Activity/Actions/Entry/EntryEvent.php
new file mode 100644
index 00000000..0e0c90d0
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Event/Activity/Actions/Entry/EntryEvent.php
@@ -0,0 +1,41 @@
1<?php
2
3namespace Wallabag\CoreBundle\Event\Activity\Actions\Entry;
4
5use Symfony\Component\EventDispatcher\Event;
6use Wallabag\CoreBundle\Entity\Entry;
7
8/**
9 * This event is fired when entry-related stuff is made.
10 */
11abstract class EntryEvent extends Event
12{
13 protected $entry;
14
15 /**
16 * EntryEvent constructor.
17 * @param Entry $entry
18 */
19 public function __construct(Entry $entry)
20 {
21 $this->entry = $entry;
22 }
23
24 /**
25 * @return Entry
26 */
27 public function getEntry()
28 {
29 return $this->entry;
30 }
31
32 /**
33 * @param Entry $entry
34 * @return EntryEvent
35 */
36 public function setEntry(Entry $entry)
37 {
38 $this->entry = $entry;
39 return $this;
40 }
41}
diff --git a/src/Wallabag/CoreBundle/Event/Activity/Actions/Entry/EntryFavouriteEvent.php b/src/Wallabag/CoreBundle/Event/Activity/Actions/Entry/EntryFavouriteEvent.php
new file mode 100644
index 00000000..98edb00d
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Event/Activity/Actions/Entry/EntryFavouriteEvent.php
@@ -0,0 +1,14 @@
1<?php
2
3namespace Wallabag\CoreBundle\Event\Activity\Actions\Entry;
4
5use Symfony\Component\EventDispatcher\Event;
6use Wallabag\CoreBundle\Entity\Entry;
7
8/**
9 * This event is fired as soon as an entry was favourited.
10 */
11class EntryFavouriteEvent extends EntryEvent
12{
13 const NAME = 'entry.favourite';
14}
diff --git a/src/Wallabag/CoreBundle/Event/Activity/Actions/Entry/EntryReadEvent.php b/src/Wallabag/CoreBundle/Event/Activity/Actions/Entry/EntryReadEvent.php
new file mode 100644
index 00000000..be6e6b40
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Event/Activity/Actions/Entry/EntryReadEvent.php
@@ -0,0 +1,14 @@
1<?php
2
3namespace Wallabag\CoreBundle\Event\Activity\Actions\Entry;
4
5use Symfony\Component\EventDispatcher\Event;
6use Wallabag\CoreBundle\Entity\Entry;
7
8/**
9 * This event is fired as soon as an entry was favourited.
10 */
11class EntryReadEvent extends EntryEvent
12{
13 const NAME = 'entry.read';
14}
diff --git a/src/Wallabag/CoreBundle/Event/Activity/Actions/Entry/EntrySavedEvent.php b/src/Wallabag/CoreBundle/Event/Activity/Actions/Entry/EntrySavedEvent.php
new file mode 100644
index 00000000..20c623c5
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Event/Activity/Actions/Entry/EntrySavedEvent.php
@@ -0,0 +1,11 @@
1<?php
2
3namespace Wallabag\CoreBundle\Event\Activity\Actions\Entry;
4
5/**
6 * This event is fired as soon as an entry was saved.
7 */
8class EntrySavedEvent extends EntryEvent
9{
10 const NAME = 'entry.saved';
11}
diff --git a/src/Wallabag/CoreBundle/Event/Activity/Actions/Entry/EntryTaggedEvent.php b/src/Wallabag/CoreBundle/Event/Activity/Actions/Entry/EntryTaggedEvent.php
new file mode 100644
index 00000000..1ea8a7f1
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Event/Activity/Actions/Entry/EntryTaggedEvent.php
@@ -0,0 +1,55 @@
1<?php
2
3namespace Wallabag\CoreBundle\Event\Activity\Actions\Entry;
4
5use Wallabag\CoreBundle\Entity\Entry;
6use Wallabag\CoreBundle\Entity\Tag;
7
8/**
9 * This event is fired as soon as a tag is added on an entry.
10 */
11class EntryTaggedEvent extends EntryEvent
12{
13 const NAME = 'entry.tagged';
14
15 /** @var Tag[] */
16 protected $tags;
17
18 /**
19 * @var boolean
20 */
21 protected $remove;
22
23 /**
24 * EntryTaggedEvent constructor.
25 * @param Entry $entry
26 * @param $tags
27 * @param bool $remove
28 */
29 public function __construct(Entry $entry, $tags, $remove = false)
30 {
31 parent::__construct($entry);
32
33 if (false === is_array($tags)) {
34 $tags = [$tags];
35 }
36
37 $this->tags = $tags;
38 }
39
40 /**
41 * @return Tag[]
42 */
43 public function getTags()
44 {
45 return $this->tags;
46 }
47
48 /**
49 * @return bool
50 */
51 public function isRemove()
52 {
53 return $this->remove;
54 }
55}
diff --git a/src/Wallabag/CoreBundle/Event/Activity/Actions/Federation/FederationEvent.php b/src/Wallabag/CoreBundle/Event/Activity/Actions/Federation/FederationEvent.php
new file mode 100644
index 00000000..da18330e
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Event/Activity/Actions/Federation/FederationEvent.php
@@ -0,0 +1,40 @@
1<?php
2
3namespace Wallabag\CoreBundle\Event\Activity\Actions\Federation;
4
5use Symfony\Component\EventDispatcher\Event;
6use Wallabag\FederationBundle\Entity\Account;
7
8abstract class FederationEvent extends Event
9{
10 protected $account;
11
12 /**
13 * FederationEvent constructor.
14 * @param Account $account
15 */
16 public function __construct(Account $account)
17 {
18 $this->account = $account;
19 }
20
21 /**
22 * @return Account
23 */
24 public function getAccount()
25 {
26 return $this->account;
27 }
28
29 /**
30 * @param Account $account
31 * @return FederationEvent
32 */
33 public function setAccount(Account $account)
34 {
35 $this->account = $account;
36 return $this;
37 }
38
39
40}
diff --git a/src/Wallabag/CoreBundle/Event/Activity/Actions/Federation/FollowEvent.php b/src/Wallabag/CoreBundle/Event/Activity/Actions/Federation/FollowEvent.php
new file mode 100644
index 00000000..4004932e
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Event/Activity/Actions/Federation/FollowEvent.php
@@ -0,0 +1,39 @@
1<?php
2
3namespace Wallabag\CoreBundle\Event\Activity\Actions\Federation;
4
5use Wallabag\FederationBundle\Entity\Account;
6
7/**
8 * This event is fired as soon as an account was followed.
9 */
10class FollowEvent extends FederationEvent
11{
12 const NAME = 'federation.follow';
13
14 protected $follower;
15
16 public function __construct(Account $accountFollowed, Account $follower)
17 {
18 parent::__construct($accountFollowed);
19 $this->follower = $follower;
20 }
21
22 /**
23 * @return Account
24 */
25 public function getFollower()
26 {
27 return $this->follower;
28 }
29
30 /**
31 * @param Account $follower
32 * @return FollowEvent
33 */
34 public function setFollower(Account $follower)
35 {
36 $this->follower = $follower;
37 return $this;
38 }
39}
diff --git a/src/Wallabag/CoreBundle/Event/Activity/Actions/Federation/RecommendedEntryEvent.php b/src/Wallabag/CoreBundle/Event/Activity/Actions/Federation/RecommendedEntryEvent.php
new file mode 100644
index 00000000..998b56d5
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Event/Activity/Actions/Federation/RecommendedEntryEvent.php
@@ -0,0 +1,43 @@
1<?php
2
3namespace Wallabag\CoreBundle\Event\Activity\Actions\Federation;
4
5use Symfony\Component\EventDispatcher\Event;
6use Wallabag\CoreBundle\Entity\Entry;
7
8/**
9 * This event is fired as soon as an entry was recommended.
10 */
11class RecommendedEntryEvent extends Event
12{
13 const NAME = 'federation.recommend';
14
15 protected $entry;
16
17 /**
18 * FederationEvent constructor.
19 * @param Entry $entry
20 */
21 public function __construct(Entry $entry)
22 {
23 $this->entry = $entry;
24 }
25
26 /**
27 * @return Entry
28 */
29 public function getEntry()
30 {
31 return $this->entry;
32 }
33
34 /**
35 * @param Entry $entry
36 * @return RecommendedEntryEvent
37 */
38 public function setEntry(Entry $entry)
39 {
40 $this->entry = $entry;
41 return $this;
42 }
43}
diff --git a/src/Wallabag/CoreBundle/Event/Activity/Actions/Federation/UnfollowEvent.php b/src/Wallabag/CoreBundle/Event/Activity/Actions/Federation/UnfollowEvent.php
new file mode 100644
index 00000000..bf9a35f8
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Event/Activity/Actions/Federation/UnfollowEvent.php
@@ -0,0 +1,39 @@
1<?php
2
3namespace Wallabag\CoreBundle\Event\Activity\Actions\Federation;
4
5use Wallabag\FederationBundle\Entity\Account;
6
7/**
8 * This event is fired as soon as an account is being unfollowed
9 */
10class UnfollowEvent extends FederationEvent
11{
12 const NAME = 'federation.unfollow';
13
14 protected $follower;
15
16 public function __construct(Account $accountFollowed, Account $follower)
17 {
18 parent::__construct($accountFollowed);
19 $this->follower = $follower;
20 }
21
22 /**
23 * @return Account
24 */
25 public function getFollower()
26 {
27 return $this->follower;
28 }
29
30 /**
31 * @param Account $follower
32 * @return UnfollowEvent
33 */
34 public function setFollower(Account $follower)
35 {
36 $this->follower = $follower;
37 return $this;
38 }
39}
diff --git a/src/Wallabag/CoreBundle/Event/Activity/Actions/Share/ShareAcceptedEvent.php b/src/Wallabag/CoreBundle/Event/Activity/Actions/Share/ShareAcceptedEvent.php
new file mode 100644
index 00000000..e171ef04
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Event/Activity/Actions/Share/ShareAcceptedEvent.php
@@ -0,0 +1,11 @@
1<?php
2
3namespace Wallabag\CoreBundle\Event\Activity\Actions\Share;
4
5/**
6 * This event is fired as soon as an share is accepted
7 */
8class ShareAcceptedEvent extends ShareEvent
9{
10 const NAME = 'share.accepted';
11}
diff --git a/src/Wallabag/CoreBundle/Event/Activity/Actions/Share/ShareCancelledEvent.php b/src/Wallabag/CoreBundle/Event/Activity/Actions/Share/ShareCancelledEvent.php
new file mode 100644
index 00000000..26bee896
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Event/Activity/Actions/Share/ShareCancelledEvent.php
@@ -0,0 +1,11 @@
1<?php
2
3namespace Wallabag\CoreBundle\Event\Activity\Actions\Share;
4
5/**
6 * This event is fired as soon as an share is cancelled
7 */
8class ShareCancelledEvent extends ShareEvent
9{
10 const NAME = 'share.cancelled';
11}
diff --git a/src/Wallabag/CoreBundle/Event/Activity/Actions/Share/ShareCreatedEvent.php b/src/Wallabag/CoreBundle/Event/Activity/Actions/Share/ShareCreatedEvent.php
new file mode 100644
index 00000000..c2cb72d8
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Event/Activity/Actions/Share/ShareCreatedEvent.php
@@ -0,0 +1,11 @@
1<?php
2
3namespace Wallabag\CoreBundle\Event\Activity\Actions\Share;
4
5/**
6 * This event is fired as soon as a share is created.
7 */
8class ShareCreatedEvent extends ShareEvent
9{
10 const NAME = 'share.created';
11}
diff --git a/src/Wallabag/CoreBundle/Event/Activity/Actions/Share/ShareDeniedEvent.php b/src/Wallabag/CoreBundle/Event/Activity/Actions/Share/ShareDeniedEvent.php
new file mode 100644
index 00000000..fcdfd1ce
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Event/Activity/Actions/Share/ShareDeniedEvent.php
@@ -0,0 +1,11 @@
1<?php
2
3namespace Wallabag\CoreBundle\Event\Activity\Actions\Share;
4
5/**
6 * This event is fired as soon as an share is denied
7 */
8class ShareDeniedEvent extends ShareEvent
9{
10 const NAME = 'share.denied';
11}
diff --git a/src/Wallabag/CoreBundle/Event/Activity/Actions/Share/ShareEvent.php b/src/Wallabag/CoreBundle/Event/Activity/Actions/Share/ShareEvent.php
new file mode 100644
index 00000000..0022a39f
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Event/Activity/Actions/Share/ShareEvent.php
@@ -0,0 +1,39 @@
1<?php
2
3namespace Wallabag\CoreBundle\Event\Activity\Actions\Share;
4
5use Symfony\Component\EventDispatcher\Event;
6use Wallabag\CoreBundle\Entity\Share;
7
8/**
9 * This event is fired when share-related stuff is made.
10 */
11abstract class ShareEvent extends Event
12{
13 protected $share;
14
15 /**
16 * ShareEvent constructor.
17 * @param Share $share
18 */
19 public function __construct(Share $share)
20 {
21 $this->share = $share;
22 }
23
24 /**
25 * @return Share
26 */
27 public function getShare()
28 {
29 return $this->share;
30 }
31
32 /**
33 * @param Share $share
34 */
35 public function setShare(Share $share)
36 {
37 $this->share = $share;
38 }
39}
diff --git a/src/Wallabag/CoreBundle/Event/Activity/Actions/User/UserDeletedEvent.php b/src/Wallabag/CoreBundle/Event/Activity/Actions/User/UserDeletedEvent.php
new file mode 100644
index 00000000..df06db8c
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Event/Activity/Actions/User/UserDeletedEvent.php
@@ -0,0 +1,8 @@
1<?php
2
3namespace Wallabag\CoreBundle\Event\Activity\Actions\User;
4
5class UserDeletedEvent extends UserEvent
6{
7 const NAME = 'user.deleted';
8}
diff --git a/src/Wallabag/CoreBundle/Event/Activity/Actions/User/UserEditedEvent.php b/src/Wallabag/CoreBundle/Event/Activity/Actions/User/UserEditedEvent.php
new file mode 100644
index 00000000..27f8f2d5
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Event/Activity/Actions/User/UserEditedEvent.php
@@ -0,0 +1,8 @@
1<?php
2
3namespace Wallabag\CoreBundle\Event\Activity\Actions\User;
4
5class UserEditedEvent extends UserEvent
6{
7 const NAME = 'user.edited';
8}
diff --git a/src/Wallabag/CoreBundle/Event/Activity/Actions/User/UserEvent.php b/src/Wallabag/CoreBundle/Event/Activity/Actions/User/UserEvent.php
new file mode 100644
index 00000000..e3807abf
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Event/Activity/Actions/User/UserEvent.php
@@ -0,0 +1,41 @@
1<?php
2
3namespace Wallabag\CoreBundle\Event\Activity\Actions\User;
4
5use Symfony\Component\EventDispatcher\Event;
6use Wallabag\UserBundle\Entity\User;
7
8/**
9 * This event is fired when user-related stuff is made.
10 */
11abstract class UserEvent extends Event
12{
13 protected $user;
14
15 /**
16 * UserEvent constructor.
17 * @param User $user
18 */
19 public function __construct(User $user)
20 {
21 $this->user = $user;
22 }
23
24 /**
25 * @return User
26 */
27 public function getUser()
28 {
29 return $this->user;
30 }
31
32 /**
33 * @param User $user
34 * @return UserEvent
35 */
36 public function setUser(User $user)
37 {
38 $this->user = $user;
39 return $this;
40 }
41}
diff --git a/src/Wallabag/CoreBundle/Event/Activity/ActivitySubscriber.php b/src/Wallabag/CoreBundle/Event/Activity/ActivitySubscriber.php
new file mode 100644
index 00000000..81379ff2
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Event/Activity/ActivitySubscriber.php
@@ -0,0 +1,224 @@
1<?php
2
3namespace Wallabag\CoreBundle\Event\Activity;
4
5use Doctrine\ORM\EntityManager;
6use FOS\UserBundle\Event\UserEvent;
7use FOS\UserBundle\FOSUserEvents;
8use Psr\Log\LoggerInterface;
9use Symfony\Component\EventDispatcher\Event;
10use Symfony\Component\EventDispatcher\EventSubscriberInterface;
11use Wallabag\CoreBundle\Entity\Activity;
12use Wallabag\CoreBundle\Event\Activity\Actions\Annotation\AnnotationCreatedEvent;
13use Wallabag\CoreBundle\Event\Activity\Actions\Annotation\AnnotationDeletedEvent;
14use Wallabag\CoreBundle\Event\Activity\Actions\Annotation\AnnotationEditedEvent;
15use Wallabag\CoreBundle\Event\Activity\Actions\Annotation\AnnotationEvent;
16use Wallabag\CoreBundle\Event\Activity\Actions\Entry\EntryDeletedEvent;
17use Wallabag\CoreBundle\Event\Activity\Actions\Entry\EntryEditedEvent;
18use Wallabag\CoreBundle\Event\Activity\Actions\Entry\EntryEvent;
19use Wallabag\CoreBundle\Event\Activity\Actions\Entry\EntryFavouriteEvent;
20use Wallabag\CoreBundle\Event\Activity\Actions\Entry\EntryReadEvent;
21use Wallabag\CoreBundle\Event\Activity\Actions\Entry\EntrySavedEvent;
22use Wallabag\CoreBundle\Event\Activity\Actions\Entry\EntryTaggedEvent;
23use Wallabag\CoreBundle\Event\Activity\Actions\Federation\FollowEvent;
24use Wallabag\CoreBundle\Event\Activity\Actions\Federation\RecommendedEntryEvent;
25use Wallabag\CoreBundle\Event\Activity\Actions\Federation\UnfollowEvent;
26use Wallabag\CoreBundle\Event\Activity\Actions\Share\ShareAcceptedEvent;
27use Wallabag\CoreBundle\Event\Activity\Actions\Share\ShareCancelledEvent;
28use Wallabag\CoreBundle\Event\Activity\Actions\Share\ShareCreatedEvent;
29use Wallabag\CoreBundle\Event\Activity\Actions\Share\ShareDeniedEvent;
30use Wallabag\CoreBundle\Event\Activity\Actions\Share\ShareEvent;
31use Wallabag\CoreBundle\Event\Activity\Actions\User\UserDeletedEvent;
32use Wallabag\CoreBundle\Event\Activity\Actions\User\UserEditedEvent;
33use Wallabag\CoreBundle\Notifications\ActionInterface;
34
35/**
36 * This listener will create the associated configuration when a user register.
37 * This configuration will be created right after the registration (no matter if it needs an email validation).
38 */
39class ActivitySubscriber implements EventSubscriberInterface
40{
41
42 /**
43 * @var EntityManager
44 */
45 private $em;
46
47 /**
48 * @var LoggerInterface $logger
49 */
50 private $logger;
51
52 public function __construct(EntityManager $em, LoggerInterface $logger)
53 {
54 $this->em = $em;
55 $this->logger = $logger;
56 }
57
58 public static function getSubscribedEvents()
59 {
60 return [
61 EntrySavedEvent::NAME => 'entryActivity',
62 EntryDeletedEvent::NAME => 'entryActivity',
63 EntryEditedEvent::NAME => 'entryActivity',
64 EntryTaggedEvent::NAME => 'taggedEntry',
65 EntryFavouriteEvent::NAME => 'entryActivity',
66 EntryReadEvent::NAME => 'entryActivity',
67
68 AnnotationCreatedEvent::NAME => 'annotationActivity',
69 AnnotationEditedEvent::NAME => 'annotationActivity',
70 AnnotationDeletedEvent::NAME => 'annotationActivity',
71
72 FollowEvent::NAME => 'followedAccount',
73 UnfollowEvent::NAME => 'unfollowedAccount',
74 RecommendedEntryEvent::NAME => 'recommendedEntry',
75
76 ShareCreatedEvent::NAME => 'shareActivity',
77 ShareAcceptedEvent::NAME => 'shareActivity',
78 ShareDeniedEvent::NAME => 'shareActivity',
79 ShareCancelledEvent::NAME => 'shareActivity',
80
81 // when a user register using the normal form
82 FOSUserEvents::REGISTRATION_COMPLETED => 'userActivity',
83 // when we manually create a user using the command line
84 // OR when we create it from the config UI
85 FOSUserEvents::USER_CREATED => 'userActivity',
86 UserEditedEvent::NAME => 'userActivity',
87 UserDeletedEvent::NAME => 'userActivity',
88 ];
89 }
90
91 public function userActivity(Event $event)
92 {
93 $activityType = 0;
94 if ($event instanceof UserEvent) {
95 $activityType = Activity::USER_CREATE;
96 } elseif ($event instanceof UserEditedEvent) {
97 $activityType = Activity::USER_EDIT;
98 } elseif ($event instanceof UserDeletedEvent) {
99 $activityType = Activity::USER_REMOVE;
100 }
101
102 $user = $event->getUser();
103 $activity = new Activity($activityType, Activity::USER_OBJECT, $user->getId());
104 $activity->setUser($user->getAccount());
105 $this->em->persist($activity);
106 $this->em->flush();
107 }
108
109 public function entryActivity(EntryEvent $event)
110 {
111 $entry = $event->getEntry();
112
113 $activityType = 0;
114 if ($event instanceof EntrySavedEvent) {
115 $activityType = Activity::ENTRY_ADD;
116 } elseif ($event instanceof EntryDeletedEvent) {
117 $activityType = Activity::ENTRY_DELETE;
118 } elseif ($event instanceof EntryEditedEvent) {
119 $activityType = Activity::ENTRY_EDIT;
120 } elseif ($event instanceof EntryFavouriteEvent) {
121 if ($entry->isStarred()) {
122 $activityType = Activity::ENTRY_FAVOURITE;
123 } else {
124 $activityType = Activity::ENTRY_UNFAVOURITE;
125 }
126 } elseif ($event instanceof EntryReadEvent) {
127 if ($entry->isArchived()) {
128 $activityType = Activity::ENTRY_READ;
129 } else {
130 $activityType = Activity::ENTRY_UNREAD;
131 }
132 }
133
134 $activity = new Activity($activityType, Activity::ENTRY_OBJECT, $entry->getId());
135 $activity->setUser($entry->getUser()->getAccount());
136 $this->em->persist($activity);
137 $this->em->flush();
138 }
139
140 public function taggedEntry(EntryTaggedEvent $event)
141 {
142 $entry = $event->getEntry();
143 $activity = new Activity($event->isRemove() ? Activity::ENTRY_REMOVE_TAG : Activity::ENTRY_ADD_TAG, Activity::ENTRY_OBJECT, $entry->getId());
144 $activity->setUser($entry->getUser()->getAccount());
145 $activity->setSecondaryObjectType(Activity::TAG_OBJECT)
146 ->setSecondaryObjectId($event->getTags()[0]->getId());
147 $this->em->persist($activity);
148 $this->em->flush();
149 }
150
151 public function annotationActivity(AnnotationEvent $event)
152 {
153 $annotation = $event->getAnnotation();
154
155 $activityType = 0;
156 if ($event instanceof AnnotationCreatedEvent) {
157 $activityType = Activity::ANNOTATION_ADD;
158 } elseif ($event instanceof AnnotationEditedEvent) {
159 $activityType = Activity::ANNOTATION_EDIT;
160 } elseif ($event instanceof AnnotationDeletedEvent) {
161 $activityType = Activity::ANNOTATION_REMOVE;
162 }
163
164 $activity = new Activity($activityType, Activity::ANNOTATION_OBJECT, $annotation->getId());
165 $activity->setUser($annotation->getUser()->getAccount());
166 $this->em->persist($activity);
167 $this->em->flush();
168 }
169
170 public function followedAccount(FollowEvent $event)
171 {
172 $activity = new Activity(Activity::FOLLOW_ACCOUNT, Activity::ACCOUNT_OBJECT, $event->getAccount()->getId());
173 $activity->setUser($event->getAccount());
174 $activity->setSecondaryObjectType(Activity::ACCOUNT_OBJECT)
175 ->setSecondaryObjectId($event->getFollower()->getId());
176 $this->em->persist($activity);
177 $this->em->flush();
178 }
179
180 public function unfollowedAccount(UnfollowEvent $event)
181 {
182 $activity = new Activity(Activity::UNFOLLOW_ACCOUNT, Activity::ACCOUNT_OBJECT, $event->getAccount()->getId());
183 $activity->setUser($event->getAccount());
184 $activity->setSecondaryObjectType(Activity::ACCOUNT_OBJECT)
185 ->setSecondaryObjectId($event->getFollower()->getId());
186 $this->em->persist($activity);
187 $this->em->flush();
188 }
189
190 public function recommendedEntry(RecommendedEntryEvent $event)
191 {
192 $entry = $event->getEntry();
193 $account = $entry->getUser()->getAccount();
194 $activity = new Activity(Activity::RECOMMEND_ENTRY, Activity::ACCOUNT_OBJECT, $account->getId());
195 $activity->setUser($account);
196 $activity->setSecondaryObjectType(Activity::ENTRY_OBJECT)
197 ->setSecondaryObjectId($entry->getId());
198 $this->em->persist($activity);
199 $this->em->flush();
200 }
201
202 public function shareActivity(ShareEvent $event)
203 {
204 $share = $event->getShare();
205
206 $activityType = 0;
207 if ($event instanceof ShareCreatedEvent) {
208 $activityType = Activity::USER_SHARE_CREATED;
209 } elseif ($event instanceof ShareAcceptedEvent) {
210 $activityType = Activity::USER_SHARE_ACCEPTED;
211 } elseif ($event instanceof ShareDeniedEvent) {
212 $activityType = Activity::USER_SHARE_REFUSED;
213 } elseif ($event instanceof ShareCancelledEvent) {
214 $activityType = Activity::USER_SHARE_CANCELLED;
215 }
216
217 $activity = new Activity($activityType, Activity::SHARE_OBJECT, $share->getId());
218 $activity->setUser($share->getUserOrigin());
219 $activity->setSecondaryObjectType(Activity::ACCOUNT_OBJECT)
220 ->setSecondaryObjectId($share->getUserDestination()->getId());
221 $this->em->persist($activity);
222 $this->em->flush();
223 }
224}
diff --git a/src/Wallabag/CoreBundle/Event/EntryDeletedEvent.php b/src/Wallabag/CoreBundle/Event/EntryDeletedEvent.php
deleted file mode 100644
index e9061d04..00000000
--- a/src/Wallabag/CoreBundle/Event/EntryDeletedEvent.php
+++ /dev/null
@@ -1,26 +0,0 @@
1<?php
2
3namespace Wallabag\CoreBundle\Event;
4
5use Symfony\Component\EventDispatcher\Event;
6use Wallabag\CoreBundle\Entity\Entry;
7
8/**
9 * This event is fired as soon as an entry is deleted.
10 */
11class EntryDeletedEvent extends Event
12{
13 const NAME = 'entry.deleted';
14
15 protected $entry;
16
17 public function __construct(Entry $entry)
18 {
19 $this->entry = $entry;
20 }
21
22 public function getEntry()
23 {
24 return $this->entry;
25 }
26}
diff --git a/src/Wallabag/CoreBundle/Event/EntrySavedEvent.php b/src/Wallabag/CoreBundle/Event/EntrySavedEvent.php
deleted file mode 100644
index 5fdb5221..00000000
--- a/src/Wallabag/CoreBundle/Event/EntrySavedEvent.php
+++ /dev/null
@@ -1,26 +0,0 @@
1<?php
2
3namespace Wallabag\CoreBundle\Event;
4
5use Symfony\Component\EventDispatcher\Event;
6use Wallabag\CoreBundle\Entity\Entry;
7
8/**
9 * This event is fired as soon as an entry was saved.
10 */
11class EntrySavedEvent extends Event
12{
13 const NAME = 'entry.saved';
14
15 protected $entry;
16
17 public function __construct(Entry $entry)
18 {
19 $this->entry = $entry;
20 }
21
22 public function getEntry()
23 {
24 return $this->entry;
25 }
26}
diff --git a/src/Wallabag/CoreBundle/Event/Subscriber/DownloadImagesSubscriber.php b/src/Wallabag/CoreBundle/Event/Subscriber/DownloadImagesSubscriber.php
index 4ebe837b..5c3550c2 100644
--- a/src/Wallabag/CoreBundle/Event/Subscriber/DownloadImagesSubscriber.php
+++ b/src/Wallabag/CoreBundle/Event/Subscriber/DownloadImagesSubscriber.php
@@ -4,10 +4,10 @@ namespace Wallabag\CoreBundle\Event\Subscriber;
4 4
5use Symfony\Component\EventDispatcher\EventSubscriberInterface; 5use Symfony\Component\EventDispatcher\EventSubscriberInterface;
6use Psr\Log\LoggerInterface; 6use Psr\Log\LoggerInterface;
7use Wallabag\CoreBundle\Event\Activity\Actions\Entry\EntryDeletedEvent;
8use Wallabag\CoreBundle\Event\Activity\Actions\Entry\EntrySavedEvent;
7use Wallabag\CoreBundle\Helper\DownloadImages; 9use Wallabag\CoreBundle\Helper\DownloadImages;
8use Wallabag\CoreBundle\Entity\Entry; 10use Wallabag\CoreBundle\Entity\Entry;
9use Wallabag\CoreBundle\Event\EntrySavedEvent;
10use Wallabag\CoreBundle\Event\EntryDeletedEvent;
11use Doctrine\ORM\EntityManager; 11use Doctrine\ORM\EntityManager;
12 12
13class DownloadImagesSubscriber implements EventSubscriberInterface 13class DownloadImagesSubscriber implements EventSubscriberInterface
diff --git a/src/Wallabag/CoreBundle/Repository/ChangeRepository.php b/src/Wallabag/CoreBundle/Repository/ChangeRepository.php
new file mode 100644
index 00000000..18d015a7
--- /dev/null
+++ b/src/Wallabag/CoreBundle/Repository/ChangeRepository.php
@@ -0,0 +1,26 @@
1<?php
2
3namespace Wallabag\CoreBundle\Repository;
4
5use Doctrine\ORM\EntityRepository;
6
7class ChangeRepository extends EntityRepository
8{
9 /**
10 * Used only in test case to get a tag for our entry.
11 *
12 * @param int $timestamp
13 *
14 * @return Tag
15 */
16 public function findChangesSinceDate($timestamp)
17 {
18 $date = new \DateTime();
19 $date->setTimestamp($timestamp);
20
21 return $this->createQueryBuilder('c')
22 ->where('c.createdAt >= :timestamp')->setParameter('timestamp', $date)
23 ->getQuery()
24 ->getResult();
25 }
26}
diff --git a/src/Wallabag/CoreBundle/Repository/EntryRepository.php b/src/Wallabag/CoreBundle/Repository/EntryRepository.php
index 9bda4e15..4bbd05ff 100644
--- a/src/Wallabag/CoreBundle/Repository/EntryRepository.php
+++ b/src/Wallabag/CoreBundle/Repository/EntryRepository.php
@@ -4,6 +4,7 @@ namespace Wallabag\CoreBundle\Repository;
4 4
5use Doctrine\ORM\EntityRepository; 5use Doctrine\ORM\EntityRepository;
6use Doctrine\ORM\Query; 6use Doctrine\ORM\Query;
7use Doctrine\ORM\QueryBuilder;
7use Pagerfanta\Adapter\DoctrineORMAdapter; 8use Pagerfanta\Adapter\DoctrineORMAdapter;
8use Pagerfanta\Pagerfanta; 9use Pagerfanta\Pagerfanta;
9use Wallabag\CoreBundle\Entity\Tag; 10use Wallabag\CoreBundle\Entity\Tag;
@@ -89,7 +90,7 @@ class EntryRepository extends EntityRepository
89 * 90 *
90 * @param int $userId 91 * @param int $userId
91 * @param string $term 92 * @param string $term
92 * @param strint $currentRoute 93 * @param string $currentRoute
93 * 94 *
94 * @return QueryBuilder 95 * @return QueryBuilder
95 */ 96 */
@@ -414,4 +415,15 @@ class EntryRepository extends EntityRepository
414 ->getQuery() 415 ->getQuery()
415 ->getResult(); 416 ->getResult();
416 } 417 }
418
419 /**
420 * @param $userId
421 * @return QueryBuilder
422 */
423 public function getBuilderForRecommendationsByUser($userId)
424 {
425 return $this->getBuilderByUser($userId)
426 ->andWhere('e.recommended = true')
427 ;
428 }
417} 429}
diff --git a/src/Wallabag/CoreBundle/Resources/config/services.yml b/src/Wallabag/CoreBundle/Resources/config/services.yml
index 183b6690..6864993c 100644
--- a/src/Wallabag/CoreBundle/Resources/config/services.yml
+++ b/src/Wallabag/CoreBundle/Resources/config/services.yml
@@ -222,3 +222,11 @@ services:
222 arguments: 222 arguments:
223 - "%wallabag_core.site_credentials.encryption_key_path%" 223 - "%wallabag_core.site_credentials.encryption_key_path%"
224 - "@logger" 224 - "@logger"
225
226 wallabag_core.activity.subscriber:
227 class: Wallabag\CoreBundle\Event\Activity\ActivitySubscriber
228 arguments:
229 - "@doctrine.orm.default_entity_manager"
230 - "@logger"
231 tags:
232 - { name: kernel.event_subscriber }
diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.en.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.en.yml
index 98f1b48c..4f19556f 100644
--- a/src/Wallabag/CoreBundle/Resources/translations/messages.en.yml
+++ b/src/Wallabag/CoreBundle/Resources/translations/messages.en.yml
@@ -20,7 +20,7 @@ menu:
20 starred: 'Starred' 20 starred: 'Starred'
21 archive: 'Archive' 21 archive: 'Archive'
22 all_articles: 'All entries' 22 all_articles: 'All entries'
23 config: 'Config' 23 config: 'config'
24 tags: 'Tags' 24 tags: 'Tags'
25 internal_settings: 'Internal Settings' 25 internal_settings: 'Internal Settings'
26 import: 'Import' 26 import: 'Import'
@@ -51,7 +51,7 @@ footer:
51 stats: Since %user_creation% you read %nb_archives% articles. That is about %per_day% a day! 51 stats: Since %user_creation% you read %nb_archives% articles. That is about %per_day% a day!
52 52
53config: 53config:
54 page_title: 'Config' 54 page_title: 'config'
55 tab_menu: 55 tab_menu:
56 settings: 'Settings' 56 settings: 'Settings'
57 rss: 'RSS' 57 rss: 'RSS'
@@ -108,6 +108,11 @@ config:
108 description: If you remove your account, ALL your articles, ALL your tags, ALL your annotations and your account will be PERMANENTLY removed (it can't be UNDONE). You'll then be logged out. 108 description: If you remove your account, ALL your articles, ALL your tags, ALL your annotations and your account will be PERMANENTLY removed (it can't be UNDONE). You'll then be logged out.
109 confirm: Are you really sure? (THIS CAN'T BE UNDONE) 109 confirm: Are you really sure? (THIS CAN'T BE UNDONE)
110 button: Delete my account 110 button: Delete my account
111 form_account:
112 title: "Profile"
113 description_label: "Description de votre compte. Ce texte sera affiché sur votre page de profil."
114 avatar_label: "Avatar"
115 banner_label: "Bannière"
111 reset: 116 reset:
112 title: Reset area (a.k.a danger zone) 117 title: Reset area (a.k.a danger zone)
113 description: By hitting buttons below you'll have ability to remove some information from your account. Be aware that these actions are IRREVERSIBLE. 118 description: By hitting buttons below you'll have ability to remove some information from your account. Be aware that these actions are IRREVERSIBLE.
@@ -562,7 +567,7 @@ error:
562flashes: 567flashes:
563 config: 568 config:
564 notice: 569 notice:
565 config_saved: 'Config saved.' 570 config_saved: 'config saved.'
566 password_updated: 'Password updated' 571 password_updated: 'Password updated'
567 password_not_updated_demo: "In demonstration mode, you can't change password for this user." 572 password_not_updated_demo: "In demonstration mode, you can't change password for this user."
568 user_updated: 'Information updated' 573 user_updated: 'Information updated'
diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml
index 277f2f63..2c53c12b 100644
--- a/src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml
+++ b/src/Wallabag/CoreBundle/Resources/translations/messages.fr.yml
@@ -108,6 +108,11 @@ config:
108 description: "Si vous confirmez la suppression de votre compte, TOUS les articles, TOUS les tags, TOUTES les annotations et votre compte seront DÉFINITIVEMENT supprimé (c’est IRRÉVERSIBLE). Vous serez ensuite déconnecté." 108 description: "Si vous confirmez la suppression de votre compte, TOUS les articles, TOUS les tags, TOUTES les annotations et votre compte seront DÉFINITIVEMENT supprimé (c’est IRRÉVERSIBLE). Vous serez ensuite déconnecté."
109 confirm: "Vous êtes vraiment sûr ? (C’EST IRRÉVERSIBLE)" 109 confirm: "Vous êtes vraiment sûr ? (C’EST IRRÉVERSIBLE)"
110 button: "Supprimer mon compte" 110 button: "Supprimer mon compte"
111 form_account:
112 title: "Profil"
113 description_label: "Description de votre compte. Ce texte sera affiché sur votre page de profil."
114 avatar_label: "Avatar"
115 banner_label: "Bannière"
111 reset: 116 reset:
112 title: "Réinitialisation (attention danger !)" 117 title: "Réinitialisation (attention danger !)"
113 description: "En cliquant sur les boutons ci-dessous vous avez la possibilité de supprimer certaines informations de votre compte. Attention, ces actions sont IRRÉVERSIBLES !" 118 description: "En cliquant sur les boutons ci-dessous vous avez la possibilité de supprimer certaines informations de votre compte. Attention, ces actions sont IRRÉVERSIBLES !"
diff --git a/src/Wallabag/CoreBundle/Resources/translations/messages.pt.yml b/src/Wallabag/CoreBundle/Resources/translations/messages.pt.yml
index 2d71cb9d..b77bfd7f 100644
--- a/src/Wallabag/CoreBundle/Resources/translations/messages.pt.yml
+++ b/src/Wallabag/CoreBundle/Resources/translations/messages.pt.yml
@@ -51,7 +51,7 @@ footer:
51 stats: 'Desde %user_creation% você leu %nb_archives% artigos. Isso é %per_day% por dia!' 51 stats: 'Desde %user_creation% você leu %nb_archives% artigos. Isso é %per_day% por dia!'
52 52
53config: 53config:
54 page_title: 'Config' 54 page_title: 'config'
55 tab_menu: 55 tab_menu:
56 settings: 'Configurações' 56 settings: 'Configurações'
57 rss: 'RSS' 57 rss: 'RSS'
diff --git a/src/Wallabag/CoreBundle/Resources/views/themes/material/Config/index.html.twig b/src/Wallabag/CoreBundle/Resources/views/themes/material/Config/index.html.twig
index bd5932b0..c2beb35a 100644
--- a/src/Wallabag/CoreBundle/Resources/views/themes/material/Config/index.html.twig
+++ b/src/Wallabag/CoreBundle/Resources/views/themes/material/Config/index.html.twig
@@ -220,6 +220,51 @@
220 </form> 220 </form>
221 221
222 <br /><hr /><br /> 222 <br /><hr /><br />
223 <h5>{{ 'config.form_account.title'|trans }}</h5>
224
225 {{ form_start(form.account) }}
226 {{ form_errors(form.account) }}
227
228 <div class="row">
229 <div class="input-field col s12">
230 {{ form_errors(form.account.description) }}
231 {{ form_widget(form.account.description, {'attr': {'class': 'materialize-textarea'}}) }}
232 {{ form_label(form.account.description) }}
233 </div>
234 </div>
235
236 <div class="row">
237 <div class="file-field input-field col s12">
238 {{ form_errors(form.account.avatar) }}
239 <div class="btn">
240 <span>{{ 'config.form_account.avatar_label'|trans }}</span>
241 {{ form_widget(form.account.avatar) }}
242 </div>
243 <div class="file-path-wrapper">
244 <input class="file-path validate" type="text">
245 </div>
246 </div>
247 </div>
248
249 <div class="row">
250 <div class="file-field input-field col s12">
251 {{ form_errors(form.account.banner) }}
252 <div class="btn">
253 <span>{{ 'config.form_account.banner_label'|trans }}</span>
254 {{ form_widget(form.account.banner) }}
255 </div>
256 <div class="file-path-wrapper">
257 <input class="file-path validate" type="text">
258 </div>
259 </div>
260 </div>
261
262 {{ form_widget(form.account.save, {'attr': {'class': 'btn waves-effect waves-light'}}) }}
263 <a href="{{ path('user-profile', {'user': app.user.account.username}) }}" class="btn">{{ 'config.form_account.view_profile' | trans }}</a>
264 {{ form_widget(form.account._token) }}
265 </form>
266
267 <br /><hr /><br />
223 268
224 <div class="row"> 269 <div class="row">
225 <h5>{{ 'config.reset.title'|trans }}</h5> 270 <h5>{{ 'config.reset.title'|trans }}</h5>
diff --git a/src/Wallabag/CoreBundle/Resources/views/themes/material/layout.html.twig b/src/Wallabag/CoreBundle/Resources/views/themes/material/layout.html.twig
index ccc44931..f93b635f 100644
--- a/src/Wallabag/CoreBundle/Resources/views/themes/material/layout.html.twig
+++ b/src/Wallabag/CoreBundle/Resources/views/themes/material/layout.html.twig
@@ -117,6 +117,11 @@
117 {% if unreadNotifs > 0 %}<span id="notifications-count" class="red-text text-accent-2">{{ unreadNotifs }}</span>{% endif %} 117 {% if unreadNotifs > 0 %}<span id="notifications-count" class="red-text text-accent-2">{{ unreadNotifs }}</span>{% endif %}
118 </a> 118 </a>
119 </li> 119 </li>
120 <li id="button_profile">
121 <a class="nav-panel-menu button-collapse-right tooltipped" data-position="bottom" data-delay="50" data-tooltip="{{ 'menu.top.profile' | trans }}" href="{{ path('user-profile', {'user': app.user.account.username}) }}">
122 <i class="material-icons">person</i>
123 </a>
124 </li>
120 <li id="button_filters"> 125 <li id="button_filters">
121 <a class="nav-panel-menu button-collapse-right tooltipped js-filters-action" data-position="bottom" data-delay="50" data-tooltip="{{ 'menu.top.filter_entries'|trans }}" href="#" data-activates="filters"> 126 <a class="nav-panel-menu button-collapse-right tooltipped js-filters-action" data-position="bottom" data-delay="50" data-tooltip="{{ 'menu.top.filter_entries'|trans }}" href="#" data-activates="filters">
122 <i class="material-icons">filter_list</i> 127 <i class="material-icons">filter_list</i>
diff --git a/src/Wallabag/FederationBundle/Command/CreateAccountsCommand.php b/src/Wallabag/FederationBundle/Command/CreateAccountsCommand.php
new file mode 100644
index 00000000..f2ca3b06
--- /dev/null
+++ b/src/Wallabag/FederationBundle/Command/CreateAccountsCommand.php
@@ -0,0 +1,126 @@
1<?php
2
3namespace Wallabag\FederationBundle\Command;
4
5use Doctrine\ORM\NoResultException;
6use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
7use Symfony\Component\Console\Input\InputArgument;
8use Symfony\Component\Console\Input\InputInterface;
9use Symfony\Component\Console\Output\OutputInterface;
10use Wallabag\FederationBundle\Entity\Account;
11use Wallabag\FederationBundle\Entity\Instance;
12use Wallabag\UserBundle\Entity\User;
13
14class CreateAccountsCommand extends ContainerAwareCommand
15{
16 /** @var OutputInterface */
17 protected $output;
18
19 protected $created = 0;
20
21 protected function configure()
22 {
23 $this
24 ->setName('wallabag:federation:create-accounts')
25 ->setDescription('Creates missing federation accounts')
26 ->setHelp('This command creates accounts for federation for missing users')
27 ->addArgument(
28 'username',
29 InputArgument::OPTIONAL,
30 'User to create an account for'
31 );
32 }
33
34 protected function execute(InputInterface $input, OutputInterface $output)
35 {
36 $this->output = $output;
37
38 $domainName = $this->getContainer()->getParameter('domain_name');
39 $instance = $this->checkInstance($domainName);
40
41 $username = $input->getArgument('username');
42
43 if ($username) {
44 try {
45 $user = $this->getUser($username);
46 $this->createAccount($user, $instance);
47 } catch (NoResultException $e) {
48 $output->writeln(sprintf('<error>User "%s" not found.</error>', $username));
49
50 return 1;
51 }
52 } else {
53 $users = $this->getDoctrine()->getRepository('WallabagUserBundle:User')->findAll();
54
55 $output->writeln(sprintf('Creating through %d user federated accounts', count($users)));
56
57 foreach ($users as $user) {
58 $output->writeln(sprintf('Processing user %s', $user->getUsername()));
59 $this->createAccount($user, $instance);
60 }
61 $output->writeln(sprintf('Creating user federated accounts. %d accounts created in total', $this->created));
62 }
63
64 return 0;
65 }
66
67 /**
68 * @param User $user
69 * @param $instance
70 */
71 private function createAccount(User $user, Instance $instance)
72 {
73 $em = $this->getContainer()->get('doctrine.orm.entity_manager');
74 $repo = $em->getRepository('WallabagFederationBundle:Account');
75
76 if ($repo->findBy(['user' => $user->getId()])) {
77 return;
78 }
79
80 $account = new Account();
81 $account->setUsername($user->getUsername())
82 ->setUser($user)
83 ->setServer($instance);
84
85 $em->persist($account);
86 $em->flush();
87
88 $user->setAccount($account);
89 $em->persist($account);
90 $em->flush();
91
92 ++$this->created;
93 }
94
95 private function checkInstance($domainName)
96 {
97 $em = $this->getContainer()->get('doctrine.orm.entity_manager');
98 $repo = $em->getRepository('WallabagFederationBundle:Instance');
99
100 $instance = $repo->findOneByDomain($domainName);
101 if (!$instance) {
102 $instance = new Instance($domainName);
103
104 $em->persist($instance);
105 $em->flush();
106 }
107 return $instance;
108 }
109
110 /**
111 * Fetches a user from its username.
112 *
113 * @param string $username
114 *
115 * @return \Wallabag\UserBundle\Entity\User
116 */
117 private function getUser($username)
118 {
119 return $this->getDoctrine()->getRepository('WallabagUserBundle:User')->findOneByUserName($username);
120 }
121
122 private function getDoctrine()
123 {
124 return $this->getContainer()->get('doctrine');
125 }
126}
diff --git a/src/Wallabag/FederationBundle/Controller/InboxController.php b/src/Wallabag/FederationBundle/Controller/InboxController.php
new file mode 100644
index 00000000..99cfeadf
--- /dev/null
+++ b/src/Wallabag/FederationBundle/Controller/InboxController.php
@@ -0,0 +1,43 @@
1<?php
2
3namespace Wallabag\FederationBundle\Controller;
4
5use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
6use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
7use Symfony\Bundle\FrameworkBundle\Controller\Controller;
8use Symfony\Component\HttpFoundation\Request;
9use Symfony\Component\HttpFoundation\Response;
10use Wallabag\FederationBundle\Entity\Account;
11use Wallabag\FederationBundle\Entity\Instance;
12use Wallabag\FederationBundle\Federation\CloudId;
13
14class InboxController extends Controller
15{
16 /**
17 * @Route("/profile/inbox", name="user-inbox")
18 *
19 * @param Request $request
20 * @return Response
21 */
22 public function userInboxAction(Request $request)
23 {
24 $em = $this->getDoctrine()->getManager();
25 $response = new Response();
26
27 if ($activity = json_decode($request->getContent())) {
28 if ($activity->type === 'Follow' && isset($activity->actor->id)) {
29 $cloudId = new CloudId($activity->actor->id);
30 $account = new Account();
31 $account->setServer($cloudId->getRemote())
32 ->setUsername($cloudId->getUser());
33 $em->persist($account);
34 $em->flush();
35
36 $response->setStatusCode(201);
37 } else {
38 $response->setStatusCode(400);
39 }
40 }
41 return $response;
42 }
43}
diff --git a/src/Wallabag/FederationBundle/Controller/LikedController.php b/src/Wallabag/FederationBundle/Controller/LikedController.php
new file mode 100644
index 00000000..3e86d50d
--- /dev/null
+++ b/src/Wallabag/FederationBundle/Controller/LikedController.php
@@ -0,0 +1,19 @@
1<?php
2
3namespace Wallabag\FederationBundle\Controller;
4
5use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
6use Symfony\Bundle\FrameworkBundle\Controller\Controller;
7use Wallabag\CoreBundle\Entity\Entry;
8
9class LikedController extends Controller
10{
11 /**
12 * @Route("/like/{entry}", name="like")
13 * @param Entry $entry
14 */
15 public function likeAction(Entry $entry)
16 {
17
18 }
19}
diff --git a/src/Wallabag/FederationBundle/Controller/MetadataController.php b/src/Wallabag/FederationBundle/Controller/MetadataController.php
new file mode 100644
index 00000000..90d3eb4d
--- /dev/null
+++ b/src/Wallabag/FederationBundle/Controller/MetadataController.php
@@ -0,0 +1,69 @@
1<?php
2
3namespace Wallabag\FederationBundle\Controller;
4
5use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
6use Symfony\Bundle\FrameworkBundle\Controller\Controller;
7use Symfony\Component\HttpFoundation\JsonResponse;
8use Symfony\Component\HttpFoundation\Request;
9use Symfony\Component\HttpFoundation\Response;
10use Wallabag\FederationBundle\Federation\CloudId;
11
12class MetadataController extends Controller
13{
14 /**
15 * @Route("/.well-known/host-meta", name="webfinger-host-meta")
16 *
17 * @return Response
18 */
19 public function getHostMeta()
20 {
21 $response = new Response();
22 $response->setContent('<XRD xmlns=\"http://docs.oasis-open.org/ns/xri/xrd-1.0\">
23<Link rel=\"lrdd\" type=\"application/xrd+xml\" template=\"' . $this->generateUrl('webfinger') . '?resource={uri}\"/>
24</XRD>');
25 $response->headers->set('Content-Type', 'application/xrd+xml');
26 return $response;
27 }
28
29 /**
30 * @Route("/.well-known/webfinger", name="webfinger")
31 * @param Request $request
32 * @return JsonResponse
33 */
34 public function getWebfingerData(Request $request)
35 {
36 $subject = $request->query->get('resource');
37
38 if (!$subject || strlen($subject) < 12 || 0 !== strpos($subject, 'acct:') || false !== strpos($subject, '@')) {
39 return new JsonResponse([]);
40 }
41 $subjectId = substr($subject, 5);
42
43 $cloudId = $this->getCloudId($subjectId);
44
45
46 $data = [
47 'subject' => $subject,
48 'aliases' => [$this->generateUrl('profile', $cloudId->getUser())],
49 'links' => [
50 [
51 'rel' => 'http://webfinger.net/rel/profile-page',
52 'type' => 'text/html',
53 'href' => $this->generateUrl('profile', $cloudId->getUser())
54 ],
55 ]
56 ];
57 return new JsonResponse($data);
58 }
59
60 private function getCloudId($subjectId)
61 {
62 return new CloudId($subjectId);
63 }
64
65
66 #
67 # {"subject":"acct:tcit@social.tcit.fr","aliases":["https://social.tcit.fr/@tcit","https://social.tcit.fr/users/tcit"],"links":[{"rel":"http://webfinger.net/rel/profile-page","type":"text/html","href":"https://social.tcit.fr/@tcit"},{"rel":"http://schemas.google.com/g/2010#updates-from","type":"application/atom+xml","href":"https://social.tcit.fr/users/tcit.atom"},{"rel":"self","type":"application/activity+json","href":"https://social.tcit.fr/@tcit"},{"rel":"salmon","href":"https://social.tcit.fr/api/salmon/1"},{"rel":"magic-public-key","href":"data:application/magic-public-key,RSA.pXwYMUdFg3XUd-bGsh8CyiMRGpRGAWuCdM5pDWx5uM4pW2pM3xbHbcI21j9h8BmlAiPg6hbZD73KGly2N8Rt5iIS0I-l6i8kA1JCCdlAaDTRd41RKMggZDoQvjVZQtsyE1VzMeU2kbqqTFN6ew7Hvbd6O0NhixoKoZ5f3jwuBDZoT0p1TAcaMdmG8oqHD97isizkDnRn8cOBA6wtI-xb5xP2zxZMsLpTDZLiKU8XcPKZCw4OfQfmDmKkHtrFb77jCAQj_s_FxjVnvxRwmfhNnWy0D-LUV_g63nHh_b5zXIeV92QZLvDYbgbezmzUzv9UeA1s70GGbaDqCIy85gw9-w==.AQAB"},{"rel":"http://ostatus.org/schema/1.0/subscribe","template":"https://social.tcit.fr/authorize_follow?acct={uri}"}]}
68 #
69}
diff --git a/src/Wallabag/FederationBundle/Controller/OutboxController.php b/src/Wallabag/FederationBundle/Controller/OutboxController.php
new file mode 100644
index 00000000..87ebdba1
--- /dev/null
+++ b/src/Wallabag/FederationBundle/Controller/OutboxController.php
@@ -0,0 +1,23 @@
1<?php
2
3namespace Wallabag\FederationBundle\Controller;
4
5use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
6use Symfony\Bundle\FrameworkBundle\Controller\Controller;
7use Symfony\Component\HttpFoundation\Request;
8use Wallabag\FederationBundle\Entity\Account;
9use Wallabag\UserBundle\Entity\User;
10
11class OutboxController extends Controller
12{
13 /**
14 * @Route("/profile/outbox", name="user-outbox")
15 *
16 * @param Request $request
17 * @param Account $user
18 */
19 public function userOutboxAction(Request $request, Account $user)
20 {
21
22 }
23}
diff --git a/src/Wallabag/FederationBundle/Controller/ProfileController.php b/src/Wallabag/FederationBundle/Controller/ProfileController.php
new file mode 100644
index 00000000..7e472e11
--- /dev/null
+++ b/src/Wallabag/FederationBundle/Controller/ProfileController.php
@@ -0,0 +1,425 @@
1<?php
2
3namespace Wallabag\FederationBundle\Controller;
4
5use Pagerfanta\Adapter\DoctrineORMAdapter;
6use Pagerfanta\Pagerfanta;
7use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
8use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
9use Symfony\Bundle\FrameworkBundle\Controller\Controller;
10use Symfony\Component\HttpFoundation\JsonResponse;
11use Symfony\Component\HttpFoundation\Request;
12use Symfony\Component\HttpFoundation\Response;
13use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
14use Wallabag\CoreBundle\Entity\Entry;
15use Wallabag\FederationBundle\Entity\Account;
16use Wallabag\FederationBundle\Federation\CloudId;
17
18class ProfileController extends Controller
19{
20 /**
21 * @Route("/profile/@{user}", name="user-profile")
22 * @ParamConverter("user", class="WallabagFederationBundle:Account", options={
23 * "repository_method" = "findOneByUsername"})
24 *
25 * @param Request $request
26 * @param Account $user
27 * @return JsonResponse|Response
28 */
29 public function getUserProfile(Request $request, Account $user)
30 {
31 if (in_array('application/ld+json; profile="https://www.w3.org/ns/activitystreams', $request->getAcceptableContentTypes(), true)) {
32 $data = [
33 '@context' => 'https://www.w3.org/ns/activitystreams',
34 'type' => 'Person',
35 'id' => CloudId::getCloudIdFromAccount($user, $this->generateUrl('homepage', [], UrlGeneratorInterface::ABSOLUTE_URL))->getDisplayId(),
36 'following' => $this->generateUrl('following', ['user' => $user->getUsername()], UrlGeneratorInterface::ABSOLUTE_URL),
37 'followers' => $this->generateUrl('followers', ['user' => $user->getUsername()], UrlGeneratorInterface::ABSOLUTE_URL),
38 //'liked' => $this->generateUrl('recommended', ['user' => $user], UrlGeneratorInterface::ABSOLUTE_URL),
39 'inbox' => $this->generateUrl('user-inbox', ['user' => $user], UrlGeneratorInterface::ABSOLUTE_URL),
40 'outbox' => $this->generateUrl('user-outbox', ['user' => $user->getUsername()], UrlGeneratorInterface::ABSOLUTE_URL),
41 'preferredUsername' => $user->getUser()->getName(),
42 'name' => $user->getUsername(),
43 //'oauthAuthorizationEndpoint' => $this->generateUrl('fos_oauth_server_authorize', [], UrlGeneratorInterface::ABSOLUTE_URL),
44 'oauthTokenEndpoint' => $this->generateUrl('fos_oauth_server_token', [], UrlGeneratorInterface::ABSOLUTE_URL),
45 //'publicInbox' => $this->generateUrl('public_inbox', [], UrlGeneratorInterface::ABSOLUTE_URL),
46 ];
47 return new JsonResponse($data);
48 }
49 return $this->render(
50 'WallabagFederationBundle:User:profile.html.twig', [
51 'user' => $user,
52 'registration_enabled' => $this->getParameter('wallabag_user.registration_enabled'),
53 ]
54 );
55 }
56
57 /**
58 * @Route("/profile/@{user}/followings/{page}", name="following", defaults={"page" : 0})
59 * @ParamConverter("user", class="WallabagFederationBundle:Account", options={
60 * "repository_method" = "findOneByUsername"})
61 *
62 * @param Request $request
63 * @param Account $user
64 * @param int $page
65 * @return JsonResponse|Response
66 */
67 public function getUsersFollowing(Request $request, Account $user, $page = 0)
68 {
69 $qb = $this->getDoctrine()->getRepository('WallabagFederationBundle:Account')->getBuilderForFollowingsByAccount($user->getId());
70
71 $pagerAdapter = new DoctrineORMAdapter($qb->getQuery(), true, false);
72
73 $following = new Pagerfanta($pagerAdapter);
74 $totalFollowing = $following->getNbResults();
75
76 $activityStream = in_array('application/ld+json; profile="https://www.w3.org/ns/activitystreams', $request->getAcceptableContentTypes(), true);
77
78 if ($page === 0 && $activityStream) {
79 /** Home page */
80 $dataPrez = [
81 '@context' => 'https://www.w3.org/ns/activitystreams',
82 'summary' => $user->getUsername() . " followings'",
83 'type' => 'Collection',
84 'id' => $this->generateUrl('following', ['user' => $user->getUsername()], UrlGeneratorInterface::ABSOLUTE_URL),
85 'totalItems' => $totalFollowing,
86 'first' => [
87 '@context' => 'https://www.w3.org/ns/activitystreams',
88 'type' => 'Link',
89 'href' => $this->generateUrl('following', ['user' => $user->getUsername(), 'page' => 1], UrlGeneratorInterface::ABSOLUTE_URL),
90 'name' => 'First page of ' . $user->getUsername() . ' followings'
91 ],
92 'last' => [
93 '@context' => 'https://www.w3.org/ns/activitystreams',
94 'type' => 'Link',
95 'href' => $this->generateUrl('following', ['user' => $user->getUsername(), 'page' => $following->getNbPages()], UrlGeneratorInterface::ABSOLUTE_URL),
96 'name' => 'Last page of ' . $user->getUsername() . ' followings'
97 ]
98 ];
99 return new JsonResponse($dataPrez);
100 //}
101 }
102
103 $following->setMaxPerPage(30);
104 $following->setCurrentPage($page);
105
106 if (!$activityStream) {
107 return $this->render('WallabagFederationBundle:User:followers.html.twig', [
108 'users' => $following,
109 'user' => $user,
110 'registration_enabled' => $this->getParameter('wallabag_user.registration_enabled'),
111 ]);
112 }
113
114 $items = [];
115
116 foreach ($following->getCurrentPageResults() as $follow) {
117 /** @var Account $follow */
118 /** Items in the page */
119 $items[] = [
120 '@context' => 'https://www.w3.org/ns/activitystreams',
121 'type' => 'Person',
122 'name' => $follow->getUsername(),
123 'id' => CloudId::getCloudIdFromAccount($follow),
124 ];
125 }
126
127 $data = [
128 'summary' => 'Page ' . $page . ' of ' . $user->getUsername() . ' followers',
129 'partOf' => $this->generateUrl('following', ['user' => $user->getUsername()], UrlGeneratorInterface::ABSOLUTE_URL),
130 'type' => 'OrderedCollectionPage',
131 'startIndex' => ($page - 1) * 30,
132 'orderedItems' => $items,
133 'first' => [
134 '@context' => 'https://www.w3.org/ns/activitystreams',
135 'type' => 'Link',
136 'href' => $this->generateUrl('following', ['user' => $user->getUsername(), 'page' => 1], UrlGeneratorInterface::ABSOLUTE_URL),
137 'name' => 'First page of ' . $user->getUsername() . ' followings'
138 ],
139 'last' => [
140 '@context' => 'https://www.w3.org/ns/activitystreams',
141 'type' => 'Link',
142 'href' => $this->generateUrl('following', ['user' => $user->getUsername(), 'page' => $following->getNbPages()], UrlGeneratorInterface::ABSOLUTE_URL),
143 'name' => 'Last page of ' . $user->getUsername() . ' followings'
144 ],
145 ];
146
147 /** Previous page */
148 if ($page > 1) {
149 $data['prev'] = [
150 '@context' => 'https://www.w3.org/ns/activitystreams',
151 'type' => 'Link',
152 'href' => $this->generateUrl('following', ['user' => $user->getUsername(), 'page' => $page - 1], UrlGeneratorInterface::ABSOLUTE_URL),
153 'name' => 'Previous page of ' . $user->getUsername() . ' followings'
154 ];
155 }
156
157 /** Next page */
158 if ($page < $following->getNbPages()) {
159 $data['next'] = [
160 '@context' => 'https://www.w3.org/ns/activitystreams',
161 'type' => 'Link',
162 'href' => $this->generateUrl('following', ['user' => $user->getUsername(), 'page' => $page + 1], UrlGeneratorInterface::ABSOLUTE_URL),
163 'name' => 'Next page of ' . $user->getUsername() . ' followings'
164 ];
165 }
166
167 return new JsonResponse($data);
168 }
169
170 /**
171 * @Route("/profile/@{user}/followers/{page}", name="followers", defaults={"page" : 0})
172 * @ParamConverter("user", class="WallabagFederationBundle:Account", options={
173 * "repository_method" = "findOneByUsername"})
174 *
175 * @param Request $request
176 * @param Account $user
177 * @return JsonResponse
178 */
179 public function getUsersFollowers(Request $request, Account $user, $page)
180 {
181 $qb = $this->getDoctrine()->getRepository('WallabagFederationBundle:Account')->getBuilderForFollowersByAccount($user->getId());
182
183 $pagerAdapter = new DoctrineORMAdapter($qb->getQuery(), true, false);
184
185 $followers = new Pagerfanta($pagerAdapter);
186 $totalFollowers = $followers->getNbResults();
187
188 $activityStream = in_array('application/ld+json; profile="https://www.w3.org/ns/activitystreams', $request->getAcceptableContentTypes(), true);
189
190 if ($page === 0 && $activityStream) {
191 /** Home page */
192 $dataPrez = [
193 '@context' => 'https://www.w3.org/ns/activitystreams',
194 'summary' => $user->getUsername() . " followers'",
195 'type' => 'Collection',
196 'id' => $this->generateUrl('followers', ['user' => $user->getUsername()], UrlGeneratorInterface::ABSOLUTE_URL),
197 'totalItems' => $totalFollowers,
198 'first' => [
199 '@context' => 'https://www.w3.org/ns/activitystreams',
200 'type' => 'Link',
201 'href' => $this->generateUrl('followers', ['user' => $user->getUsername(), 'page' => 1], UrlGeneratorInterface::ABSOLUTE_URL),
202 'name' => 'First page of ' . $user->getUsername() . ' followers'
203 ],
204 'last' => [
205 '@context' => 'https://www.w3.org/ns/activitystreams',
206 'type' => 'Link',
207 'href' => $this->generateUrl('followers', ['user' => $user->getUsername(), 'page' => $followers->getNbPages()], UrlGeneratorInterface::ABSOLUTE_URL),
208 'name' => 'Last page of ' . $user->getUsername() . ' followers'
209 ]
210 ];
211 return new JsonResponse($dataPrez);
212 }
213
214 $followers->setMaxPerPage(30);
215 if (!$activityStream && $page === 0) {
216 $followers->setCurrentPage(1);
217 } else {
218 $followers->setCurrentPage($page);
219 }
220
221 if (!$activityStream) {
222 return $this->render('WallabagFederationBundle:User:followers.html.twig', [
223 'users' => $followers,
224 'user' => $user,
225 'registration_enabled' => $this->getParameter('wallabag_user.registration_enabled'),
226 ]);
227 }
228
229 $items = [];
230
231 foreach ($followers->getCurrentPageResults() as $follow) {
232 /** @var Account $follow */
233 /** Items in the page */
234 $items[] = [
235 '@context' => 'https://www.w3.org/ns/activitystreams',
236 'type' => 'Person',
237 'name' => $follow->getUsername(),
238 'id' => CloudId::getCloudIdFromAccount($follow)->getDisplayId(),
239 ];
240 }
241 $data = [
242 'summary' => 'Page ' . $page . ' of ' . $user->getUsername() . ' followers',
243 'partOf' => $this->generateUrl('followers', ['user' => $user->getUsername()], UrlGeneratorInterface::ABSOLUTE_URL),
244 'type' => 'OrderedCollectionPage',
245 'startIndex' => ($page - 1) * 30,
246 'orderedItems' => $items,
247 'first' => [
248 '@context' => 'https://www.w3.org/ns/activitystreams',
249 'type' => 'Link',
250 'href' => $this->generateUrl('followers', ['user' => $user->getUsername(), 'page' => 1], UrlGeneratorInterface::ABSOLUTE_URL),
251 'name' => 'First page of ' . $user->getUsername() . ' followers'
252 ],
253 'last' => [
254 '@context' => 'https://www.w3.org/ns/activitystreams',
255 'type' => 'Link',
256 'href' => $this->generateUrl('followers', ['user' => $user->getUsername(), 'page' => $followers->getNbPages()], UrlGeneratorInterface::ABSOLUTE_URL),
257 'name' => 'Last page of ' . $user->getUsername() . ' followers'
258 ],
259 ];
260
261 /** Previous page */
262 if ($page > 1) {
263 $data['prev'] = [
264 '@context' => 'https://www.w3.org/ns/activitystreams',
265 'type' => 'Link',
266 'href' => $this->generateUrl('followers', ['user' => $user->getUsername(), 'page' => $page - 1], UrlGeneratorInterface::ABSOLUTE_URL),
267 'name' => 'Previous page of ' . $user->getUsername() . ' followers'
268 ];
269 }
270
271 /** Next page */
272 if ($page < $followers->getNbPages()) {
273 $data['next'] = [
274 '@context' => 'https://www.w3.org/ns/activitystreams',
275 'type' => 'Link',
276 'href' => $this->generateUrl('followers', ['user' => $user->getUsername(), 'page' => $page + 1], UrlGeneratorInterface::ABSOLUTE_URL),
277 'name' => 'Next page of ' . $user->getUsername() . ' followers'
278 ];
279 }
280
281 return new JsonResponse($data);
282 }
283
284 /**
285 * @Route("/profile/@{userToFollow}/follow", name="follow-user")
286 * @ParamConverter("userToFollow", class="WallabagFederationBundle:Account", options={
287 * "repository_method" = "findOneByUsername"})
288 * @param Account $userToFollow
289 */
290 public function followAccountAction(Account $userToFollow)
291 {
292 // if we're on our own instance
293 if ($this->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_FULLY')) {
294
295 /** @var Account $userAccount */
296 $userAccount = $this->getUser()->getAccount();
297
298 if ($userToFollow === $userAccount) {
299 $this->createAccessDeniedException("You can't follow yourself");
300 }
301
302 $em = $this->getDoctrine()->getManager();
303
304 $userAccount->addFollowing($userToFollow);
305 $userToFollow->addFollower($userAccount);
306
307 $em->persist($userAccount);
308 $em->persist($userToFollow);
309
310 $em->flush();
311 } else {
312 // ask cloud id and redirect to instance
313 }
314 }
315
316 /**
317 * @Route("/profile/@{user}/recommendations", name="user-recommendations", defaults={"page" : 0})
318 * @ParamConverter("user", class="WallabagFederationBundle:Account", options={
319 * "repository_method" = "findOneByUsername"})
320 *
321 * @param Request $request
322 * @param Account $user
323 * @param int $page
324 * @return JsonResponse|Response
325 */
326 public function getUsersRecommendationsAction(Request $request, Account $user, $page = 0)
327 {
328 $qb = $this->getDoctrine()->getRepository('WallabagCoreBundle:Entry')->getBuilderForRecommendationsByUser($user->getUser()->getId());
329
330 $pagerAdapter = new DoctrineORMAdapter($qb->getQuery(), true, false);
331
332 $recommendations = new Pagerfanta($pagerAdapter);
333 $totalRecommendations = $recommendations->getNbResults();
334
335 $activityStream = in_array('application/ld+json; profile="https://www.w3.org/ns/activitystreams', $request->getAcceptableContentTypes(), true);
336
337 if ($page === 0 && $activityStream) {
338 $dataPrez = [
339 '@context' => 'https://www.w3.org/ns/activitystreams',
340 'summary' => $user->getUsername() . " recommendations'",
341 'type' => 'Collection',
342 'id' => $this->generateUrl('user-recommendations', ['user' => $user->getUsername()], UrlGeneratorInterface::ABSOLUTE_URL),
343 'totalItems' => $totalRecommendations,
344 'first' => [
345 '@context' => 'https://www.w3.org/ns/activitystreams',
346 'type' => 'Link',
347 'href' => $this->generateUrl('user-recommendations', ['user' => $user->getUsername(), 'page' => 1], UrlGeneratorInterface::ABSOLUTE_URL),
348 'name' => 'First page of ' . $user->getUsername() . ' followers'
349 ],
350 'last' => [
351 '@context' => 'https://www.w3.org/ns/activitystreams',
352 'type' => 'Link',
353 'href' => $this->generateUrl('user-recommendations', ['user' => $user->getUsername(), 'page' => $recommendations->getNbPages()], UrlGeneratorInterface::ABSOLUTE_URL),
354 'name' => 'Last page of ' . $user->getUsername() . ' followers'
355 ]
356 ];
357 return new JsonResponse($dataPrez);
358 }
359
360 $recommendations->setMaxPerPage(30);
361 if (!$activityStream && $page === 0) {
362 $recommendations->setCurrentPage(1);
363 } else {
364 $recommendations->setCurrentPage($page);
365 }
366
367 if (!$activityStream) {
368 return $this->render('WallabagFederationBundle:User:recommendations.html.twig', [
369 'recommendations' => $recommendations,
370 'registration_enabled' => $this->getParameter('wallabag_user.registration_enabled'),
371 ]);
372 }
373
374 $items = [];
375
376 foreach ($recommendations->getCurrentPageResults() as $recommendation) {
377 $items[] = [
378 '@context' => 'https://www.w3.org/ns/activitystreams',
379 'type' => 'Person',
380 'name' => $recommendation->getTitle(),
381 'id' => $recommendation->getUrl(),
382 ];
383 }
384 $data = [
385 'summary' => 'Page ' . $page . ' of ' . $user->getUsername() . ' recommendations',
386 'partOf' => $this->generateUrl('user-recommendations', ['user' => $user->getUsername()], UrlGeneratorInterface::ABSOLUTE_URL),
387 'type' => 'OrderedCollectionPage',
388 'startIndex' => ($page - 1) * 30,
389 'orderedItems' => $items,
390 'first' => [
391 '@context' => 'https://www.w3.org/ns/activitystreams',
392 'type' => 'Link',
393 'href' => $this->generateUrl('user-recommendations', ['user' => $user->getUsername(), 'page' => 1], UrlGeneratorInterface::ABSOLUTE_URL),
394 'name' => 'First page of ' . $user->getUsername() . ' recommendations'
395 ],
396 'last' => [
397 '@context' => 'https://www.w3.org/ns/activitystreams',
398 'type' => 'Link',
399 'href' => $this->generateUrl('user-recommendations', ['user' => $user->getUsername(), 'page' => $recommendations->getNbPages()], UrlGeneratorInterface::ABSOLUTE_URL),
400 'name' => 'Last page of ' . $user->getUsername() . ' recommendations'
401 ],
402 ];
403
404 if ($page > 1) {
405 $data['prev'] = [
406 '@context' => 'https://www.w3.org/ns/activitystreams',
407 'type' => 'Link',
408 'href' => $this->generateUrl('user-recommendations', ['user' => $user->getUsername(), 'page' => $page - 1], UrlGeneratorInterface::ABSOLUTE_URL),
409 'name' => 'Previous page of ' . $user->getUsername() . ' recommendations'
410 ];
411 }
412
413 if ($page < $recommendations->getNbPages()) {
414 $data['next'] = [
415 '@context' => 'https://www.w3.org/ns/activitystreams',
416 'type' => 'Link',
417 'href' => $this->generateUrl('user-recommendations', ['user' => $user->getUsername(), 'page' => $page + 1], UrlGeneratorInterface::ABSOLUTE_URL),
418 'name' => 'Next page of ' . $user->getUsername() . ' recommendations'
419 ];
420 }
421
422 return new JsonResponse($data);
423 }
424
425}
diff --git a/src/Wallabag/FederationBundle/Controller/RecommandController.php b/src/Wallabag/FederationBundle/Controller/RecommandController.php
new file mode 100644
index 00000000..ea5a6660
--- /dev/null
+++ b/src/Wallabag/FederationBundle/Controller/RecommandController.php
@@ -0,0 +1,34 @@
1<?php
2
3namespace Wallabag\FederationBundle\Controller;
4
5use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
6use Symfony\Bundle\FrameworkBundle\Controller\Controller;
7use Wallabag\CoreBundle\Entity\Entry;
8use Wallabag\CoreBundle\Event\Activity\Actions\Federation\RecommendedEntryEvent;
9use Wallabag\FederationBundle\Entity\Account;
10
11class RecommandController extends Controller
12{
13 /**
14 * @Route("/recommend/{entry}", name="recommend-entry")
15 *
16 * @param Entry $entry
17 */
18 public function postRecommendAction(Entry $entry)
19 {
20 if ($entry->getUser() !== $this->getUser()) {
21 $this->createAccessDeniedException("You can't recommend entries which are not your own");
22 }
23 $em = $this->getDoctrine()->getManager();
24
25 $entry->setRecommended(true);
26
27 $em->persist($entry);
28 $em->flush();
29
30 $this->get('event_dispatcher')->dispatch(RecommendedEntryEvent::NAME, new RecommendedEntryEvent($entry));
31
32 $this->redirectToRoute('view', ['id' => $entry->getId()]);
33 }
34}
diff --git a/src/Wallabag/FederationBundle/DependencyInjection/Configuration.php b/src/Wallabag/FederationBundle/DependencyInjection/Configuration.php
new file mode 100644
index 00000000..7754e8e4
--- /dev/null
+++ b/src/Wallabag/FederationBundle/DependencyInjection/Configuration.php
@@ -0,0 +1,25 @@
1<?php
2
3namespace Wallabag\FederationBundle\DependencyInjection;
4
5use Symfony\Component\Config\Definition\Builder\TreeBuilder;
6use Symfony\Component\Config\Definition\ConfigurationInterface;
7
8class Configuration implements ConfigurationInterface
9{
10 public function getConfigTreeBuilder()
11 {
12 $treeBuilder = new TreeBuilder();
13 $rootNode = $treeBuilder->root('wallabag_user');
14
15 $rootNode
16 ->children()
17 ->booleanNode('registration_enabled')
18 ->defaultValue(true)
19 ->end()
20 ->end()
21 ;
22
23 return $treeBuilder;
24 }
25}
diff --git a/src/Wallabag/FederationBundle/DependencyInjection/WallabagFederationExtension.php b/src/Wallabag/FederationBundle/DependencyInjection/WallabagFederationExtension.php
new file mode 100644
index 00000000..ceeb8379
--- /dev/null
+++ b/src/Wallabag/FederationBundle/DependencyInjection/WallabagFederationExtension.php
@@ -0,0 +1,26 @@
1<?php
2
3namespace Wallabag\FederationBundle\DependencyInjection;
4
5use Symfony\Component\DependencyInjection\ContainerBuilder;
6use Symfony\Component\Config\FileLocator;
7use Symfony\Component\HttpKernel\DependencyInjection\Extension;
8use Symfony\Component\DependencyInjection\Loader;
9
10class WallabagFederationExtension extends Extension
11{
12 public function load(array $configs, ContainerBuilder $container)
13 {
14 $configuration = new Configuration();
15 $config = $this->processConfiguration($configuration, $configs);
16
17 $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
18 $loader->load('services.yml');
19 $container->setParameter('wallabag_user.registration_enabled', $config['registration_enabled']);
20 }
21
22 public function getAlias()
23 {
24 return 'wallabag_federation';
25 }
26}
diff --git a/src/Wallabag/FederationBundle/Entity/Account.php b/src/Wallabag/FederationBundle/Entity/Account.php
new file mode 100644
index 00000000..c44050d9
--- /dev/null
+++ b/src/Wallabag/FederationBundle/Entity/Account.php
@@ -0,0 +1,307 @@
1<?php
2
3namespace Wallabag\FederationBundle\Entity;
4
5use Doctrine\Common\Collections\ArrayCollection;
6use Doctrine\Common\Collections\Collection;
7use Doctrine\ORM\Mapping as ORM;
8use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
9use Symfony\Component\Validator\Constraints as Assert;
10use Wallabag\CoreBundle\Entity\Entry;
11use Wallabag\UserBundle\Entity\User;
12
13/**
14 * Account.
15 *
16 * @ORM\Entity(repositoryClass="Wallabag\FederationBundle\Repository\AccountRepository")
17 * @UniqueEntity(fields={"username", "server"}).
18 * @ORM\Table(name="`account`")
19 */
20class Account
21{
22 /**
23 * @var int
24 *
25 * @ORM\Column(name="id", type="integer")
26 * @ORM\Id
27 * @ORM\GeneratedValue(strategy="AUTO")
28 *
29 */
30 private $id;
31
32 /**
33 * @var string
34 *
35 * @ORM\Column(name="username", type="string")
36 */
37 private $username;
38
39 /**
40 * @var Instance
41 *
42 * @ORM\ManyToOne(targetEntity="Wallabag\FederationBundle\Entity\Instance", inversedBy="users")
43 */
44 private $server;
45
46 /**
47 * @var User
48 *
49 * @ORM\OneToOne(targetEntity="Wallabag\UserBundle\Entity\User", inversedBy="account")
50 * @ORM\JoinColumn(nullable=true)
51 */
52 private $user;
53
54 /**
55 * @var ArrayCollection
56 *
57 * @ORM\ManyToMany(targetEntity="Wallabag\FederationBundle\Entity\Account", mappedBy="following")
58 */
59 private $followers;
60
61 /**
62 * @var ArrayCollection
63 *
64 * @ORM\ManyToMany(targetEntity="Wallabag\FederationBundle\Entity\Account", inversedBy="followers")
65 * @ORM\JoinTable(name="follow",
66 * joinColumns={@ORM\JoinColumn(name="account_id", referencedColumnName="id")},
67 * inverseJoinColumns={@ORM\JoinColumn(name="follow_account_id", referencedColumnName="id")}
68 * )
69 */
70 private $following;
71
72 /**
73 * @var string
74 *
75 * @ORM\Column(type="string", nullable=true)
76 *
77 * @Assert\File(mimeTypes={ "image/gif", "image/jpeg", "image/svg+xml", "image/webp", "image/png" })
78 */
79 private $avatar;
80
81 /**
82 * @var string
83 *
84 * @ORM\Column(type="string", nullable=true)
85 *
86 * @Assert\File(mimeTypes={ "image/gif", "image/jpeg", "image/svg+xml", "image/webp", "image/png" })
87 */
88 private $banner;
89
90 /**
91 * @var string
92 *
93 * @ORM\Column(type="text", nullable=true)
94 */
95 private $description;
96
97 /**
98 * Account constructor.
99 */
100 public function __construct()
101 {
102 $this->followers = new ArrayCollection();
103 $this->following = new ArrayCollection();
104 $this->liked = new ArrayCollection();
105 }
106
107 /**
108 * @return int
109 */
110 public function getId()
111 {
112 return $this->id;
113 }
114
115 /**
116 * @return string
117 */
118 public function getUsername()
119 {
120 return $this->username;
121 }
122
123 /**
124 * @param string $username
125 * @return Account
126 */
127 public function setUsername($username)
128 {
129 $this->username = $username;
130 return $this;
131 }
132
133 /**
134 * @return string
135 */
136 public function getServer()
137 {
138 return $this->server;
139 }
140
141 /**
142 * @param string $server
143 * @return Account
144 */
145 public function setServer($server)
146 {
147 $this->server = $server;
148 return $this;
149 }
150
151 /**
152 * @return User
153 */
154 public function getUser()
155 {
156 return $this->user;
157 }
158
159 /**
160 * @param User $user
161 * @return Account
162 */
163 public function setUser(User $user)
164 {
165 $this->user = $user;
166 return $this;
167 }
168
169 /**
170 * @return Collection
171 */
172 public function getFollowers()
173 {
174 return $this->followers;
175 }
176
177 /**
178 * @param Collection $followers
179 * @return Account
180 */
181 public function setFollowers($followers)
182 {
183 $this->followers = $followers;
184 return $this;
185 }
186
187 /**
188 * @param Account $account
189 * @return Account
190 */
191 public function addFollower(Account $account)
192 {
193 $this->followers->add($account);
194 return $this;
195 }
196
197 /**
198 * @return Collection
199 */
200 public function getFollowing()
201 {
202 return $this->following;
203 }
204
205 /**
206 * @param Collection $following
207 * @return Account
208 */
209 public function setFollowing(Collection $following)
210 {
211 $this->following = $following;
212 return $this;
213 }
214
215 /**
216 * @param Account $account
217 * @return Account
218 */
219 public function addFollowing(Account $account)
220 {
221 $this->following->add($account);
222 return $this;
223 }
224
225 /**
226 * @return Collection
227 */
228 public function getLiked()
229 {
230 return $this->liked;
231 }
232
233 /**
234 * @param Collection $liked
235 * @return Account
236 */
237 public function setLiked(Collection $liked)
238 {
239 $this->liked = $liked;
240 return $this;
241 }
242
243 /**
244 * @param Entry $entry
245 * @return Account
246 */
247 public function addLiked(Entry $entry)
248 {
249 $this->liked->add($entry);
250 return $this;
251 }
252
253 /**
254 * @return string
255 */
256 public function getAvatar()
257 {
258 return $this->avatar;
259 }
260
261 /**
262 * @param string $avatar
263 * @return Account
264 */
265 public function setAvatar($avatar)
266 {
267 $this->avatar = $avatar;
268 return $this;
269 }
270
271 /**
272 * @return string
273 */
274 public function getBanner()
275 {
276 return $this->banner;
277 }
278
279 /**
280 * @param string $banner
281 * @return Account
282 */
283 public function setBanner($banner)
284 {
285 $this->banner = $banner;
286 return $this;
287 }
288
289 /**
290 * @return string
291 */
292 public function getDescription()
293 {
294 return $this->description;
295 }
296
297 /**
298 * @param string $description
299 * @return Account
300 */
301 public function setDescription($description)
302 {
303 $this->description = $description;
304 return $this;
305 }
306
307}
diff --git a/src/Wallabag/FederationBundle/Entity/Instance.php b/src/Wallabag/FederationBundle/Entity/Instance.php
new file mode 100644
index 00000000..ff8960cd
--- /dev/null
+++ b/src/Wallabag/FederationBundle/Entity/Instance.php
@@ -0,0 +1,111 @@
1<?php
2
3namespace Wallabag\FederationBundle\Entity;
4
5use Doctrine\ORM\Mapping as ORM;
6use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
7
8/**
9 * Account.
10 *
11 * @ORM\Entity
12 * @UniqueEntity(fields={"domain"}).
13 * @ORM\Table(name="`instance`")
14 */
15class Instance {
16 /**
17 * @var int
18 *
19 * @ORM\Column(name="id", type="integer")
20 * @ORM\Id
21 * @ORM\GeneratedValue(strategy="AUTO")
22 *
23 */
24 private $id;
25
26 /**
27 * @var string
28 *
29 * @ORM\Column(name="domain", type="string")
30 */
31 private $domain;
32
33 /**
34 * @var float
35 *
36 * @ORM\Column(name="score", type="float")
37 */
38 private $score = 0;
39
40 /**
41 * @var array
42 *
43 * @ORM\OneToMany(targetEntity="Wallabag\FederationBundle\Entity\Account", mappedBy="server")
44 */
45 private $users;
46
47 /**
48 * Instance constructor.
49 * @param string $domain
50 */
51 public function __construct($domain)
52 {
53 $this->domain = $domain;
54 }
55
56 /**
57 * @return int
58 */
59 public function getId()
60 {
61 return $this->id;
62 }
63
64 /**
65 * @return string
66 */
67 public function getDomain()
68 {
69 return $this->domain;
70 }
71
72 /**
73 * @param string $domain
74 */
75 public function setDomain($domain)
76 {
77 $this->domain = $domain;
78 }
79
80 /**
81 * @return float
82 */
83 public function getScore()
84 {
85 return $this->score;
86 }
87
88 /**
89 * @param float $score
90 */
91 public function setScore($score)
92 {
93 $this->score = $score;
94 }
95
96 /**
97 * @return array
98 */
99 public function getUsers()
100 {
101 return $this->users;
102 }
103
104 /**
105 * @param array $users
106 */
107 public function setUsers($users)
108 {
109 $this->users = $users;
110 }
111}
diff --git a/src/Wallabag/FederationBundle/EventListener/CreateAccountListener.php b/src/Wallabag/FederationBundle/EventListener/CreateAccountListener.php
new file mode 100644
index 00000000..92626b15
--- /dev/null
+++ b/src/Wallabag/FederationBundle/EventListener/CreateAccountListener.php
@@ -0,0 +1,54 @@
1<?php
2
3namespace Wallabag\FederationBundle\EventListener;
4
5use Doctrine\ORM\EntityManager;
6use FOS\UserBundle\Event\UserEvent;
7use FOS\UserBundle\FOSUserEvents;
8use Symfony\Component\EventDispatcher\EventDispatcherInterface;
9use Symfony\Component\EventDispatcher\EventSubscriberInterface;
10use Wallabag\CoreBundle\Entity\Config;
11use Wallabag\FederationBundle\Entity\Account;
12
13/**
14 * This listener will create the associated configuration when a user register.
15 * This configuration will be created right after the registration (no matter if it needs an email validation).
16 */
17class CreateAccountListener implements EventSubscriberInterface
18{
19 private $em;
20 private $domainName;
21
22 public function __construct(EntityManager $em, $domainName)
23 {
24 $this->em = $em;
25 $this->domainName = $domainName;
26 }
27
28 public static function getSubscribedEvents()
29 {
30 return [
31 // when a user register using the normal form
32 FOSUserEvents::REGISTRATION_COMPLETED => 'createAccount',
33 // when we manually create a user using the command line
34 // OR when we create it from the config UI
35 FOSUserEvents::USER_CREATED => 'createAccount',
36 ];
37 }
38
39 public function createAccount(UserEvent $event)
40 {
41 $user = $event->getUser();
42 $account = new Account();
43 $account->setUser($user)
44 ->setUsername($user->getUsername())
45 ->setServer($this->domainName);
46
47 $this->em->persist($account);
48
49 $user->setAccount($account);
50
51 $this->em->persist($user);
52 $this->em->flush();
53 }
54}
diff --git a/src/Wallabag/FederationBundle/Federation/CloudId.php b/src/Wallabag/FederationBundle/Federation/CloudId.php
new file mode 100644
index 00000000..038ea5e8
--- /dev/null
+++ b/src/Wallabag/FederationBundle/Federation/CloudId.php
@@ -0,0 +1,78 @@
1<?php
2
3namespace Wallabag\FederationBundle\Federation;
4
5use Wallabag\FederationBundle\Entity\Account;
6
7class CloudId {
8
9 /** @var string */
10 private $id;
11
12 /** @var string */
13 private $user;
14
15 /** @var string */
16 private $remote;
17
18 /**
19 * CloudId constructor.
20 *
21 * @param string $id
22 */
23 public function __construct($id) {
24 $this->id = $id;
25
26 $atPos = strpos($id, '@');
27 $user = substr($id, 0, $atPos);
28 $remote = substr($id, $atPos + 1);
29 if (!empty($user) && !empty($remote)) {
30 $this->user = $user;
31 $this->remote = $remote;
32 }
33 }
34
35 /**
36 * The full remote cloud id
37 *
38 * @return string
39 */
40 public function getId() {
41 return $this->id;
42 }
43
44 public function getDisplayId() {
45 return str_replace('https://', '', str_replace('http://', '', $this->getId()));
46 }
47
48 /**
49 * The username on the remote server
50 *
51 * @return string
52 */
53 public function getUser() {
54 return $this->user;
55 }
56
57 /**
58 * The base address of the remote server
59 *
60 * @return string
61 */
62 public function getRemote() {
63 return $this->remote;
64 }
65
66 /**
67 * @param Account $account
68 * @param string $domain
69 * @return CloudId
70 */
71 public static function getCloudIdFromAccount(Account $account, $domain = '')
72 {
73 if ($account->getServer() !== null) {
74 return new self($account->getUsername() . '@' . $account->getServer());
75 }
76 return new self($account->getUsername() . '@' . $domain);
77 }
78}
diff --git a/src/Wallabag/FederationBundle/Form/Type/AccountType.php b/src/Wallabag/FederationBundle/Form/Type/AccountType.php
new file mode 100644
index 00000000..af291ced
--- /dev/null
+++ b/src/Wallabag/FederationBundle/Form/Type/AccountType.php
@@ -0,0 +1,46 @@
1<?php
2
3namespace Wallabag\FederationBundle\Form\Type;
4
5use Symfony\Component\Form\AbstractType;
6use Symfony\Component\Form\Extension\Core\Type\FileType;
7use Symfony\Component\Form\Extension\Core\Type\SubmitType;
8use Symfony\Component\Form\Extension\Core\Type\TextareaType;
9use Symfony\Component\Form\FormBuilderInterface;
10use Symfony\Component\OptionsResolver\OptionsResolver;
11use Wallabag\FederationBundle\Entity\Account;
12
13class AccountType extends AbstractType
14{
15 public function buildForm(FormBuilderInterface $builder, array $options)
16 {
17 $builder
18 ->add('description', TextareaType::class, ['label' => 'config.form_account.description_label'])
19 ->add('avatar', FileType::class, [
20 'label' => 'config.form_account.avatar_label',
21 'required' => false,
22 'data_class' => null,
23 ])
24 ->add('banner', FileType::class, [
25 'label' => 'config.form_account.banner_label',
26 'required' => false,
27 'data_class' => null,
28 ])
29 ->add('save', SubmitType::class, [
30 'label' => 'config.form.save',
31 ])
32 ;
33 }
34
35 public function configureOptions(OptionsResolver $resolver)
36 {
37 $resolver->setDefaults(array(
38 'data_class' => Account::class,
39 ));
40 }
41
42 public function getBlockPrefix()
43 {
44 return 'update_account';
45 }
46}
diff --git a/src/Wallabag/FederationBundle/Repository/AccountRepository.php b/src/Wallabag/FederationBundle/Repository/AccountRepository.php
new file mode 100644
index 00000000..e39bc582
--- /dev/null
+++ b/src/Wallabag/FederationBundle/Repository/AccountRepository.php
@@ -0,0 +1,48 @@
1<?php
2
3namespace Wallabag\FederationBundle\Repository;
4
5use Doctrine\ORM\EntityRepository;
6use Doctrine\ORM\QueryBuilder;
7
8class AccountRepository extends EntityRepository
9{
10 /**
11 * @param $accountId
12 * @return QueryBuilder
13 */
14 public function getBuilderForFollowingsByAccount($accountId)
15 {
16 return $this->createQueryBuilder('a')
17 ->select('f.id, f.username')
18 ->innerJoin('a.following', 'f')
19 ->where('a.id = :accountId')->setParameter('accountId', $accountId)
20 ;
21 }
22
23 /**
24 * @param $accountId
25 * @return QueryBuilder
26 */
27 public function getBuilderForFollowersByAccount($accountId)
28 {
29 return $this->createQueryBuilder('a')
30 ->innerJoin('a.followers', 'f')
31 ->where('a.id = :accountId')->setParameter('accountId', $accountId)
32 ;
33 }
34
35 /**
36 * @param $username
37 * @return QueryBuilder
38 * @throws \Doctrine\ORM\NonUniqueResultException
39 */
40 public function findAccountByUsername($username)
41 {
42 return $this->createQueryBuilder('a')
43 ->where('a.username = :username')->setParameter('username', $username)
44 ->andWhere('a.server = null')
45 ->getQuery()
46 ->getOneOrNullResult();
47 }
48}
diff --git a/src/Wallabag/FederationBundle/Repository/InstanceRepository.php b/src/Wallabag/FederationBundle/Repository/InstanceRepository.php
new file mode 100644
index 00000000..6365d5c4
--- /dev/null
+++ b/src/Wallabag/FederationBundle/Repository/InstanceRepository.php
@@ -0,0 +1,10 @@
1<?php
2
3namespace Wallabag\FederationBundle\Repository;
4
5use Doctrine\ORM\EntityRepository;
6
7class InstanceRepository extends EntityRepository
8{
9
10}
diff --git a/src/Wallabag/FederationBundle/Resources/config/services.yml b/src/Wallabag/FederationBundle/Resources/config/services.yml
new file mode 100644
index 00000000..1b337bb2
--- /dev/null
+++ b/src/Wallabag/FederationBundle/Resources/config/services.yml
@@ -0,0 +1,8 @@
1services:
2 wallabag_federation.listener.create_accounts:
3 class: Wallabag\FederationBundle\EventListener\CreateAccountListener
4 arguments:
5 - "@doctrine.orm.entity_manager"
6 - "%domain_name%"
7 tags:
8 - { name: kernel.event_subscriber }
diff --git a/src/Wallabag/FederationBundle/Resources/views/themes/material/User/followers.html.twig b/src/Wallabag/FederationBundle/Resources/views/themes/material/User/followers.html.twig
new file mode 100644
index 00000000..d31ccc33
--- /dev/null
+++ b/src/Wallabag/FederationBundle/Resources/views/themes/material/User/followers.html.twig
@@ -0,0 +1,88 @@
1{% extends "WallabagCoreBundle::base.html.twig" %}
2
3{% block css %}
4 {{ parent() }}
5 {% if not app.debug %}
6 <link rel="stylesheet" href="{{ asset('bundles/wallabagcore/material.css') }}">
7 {% endif %}
8{% endblock %}
9
10{% block scripts %}
11 {{ parent() }}
12 <script src="{{ asset('bundles/wallabagcore/material' ~ (app.debug ? '.dev' : '') ~ '.js') }}"></script>
13{% endblock %}
14
15{% block header %}
16{% endblock %}
17
18{% block body_class %}reset-left{% endblock %}
19
20{% block messages %}
21 {% for flashMessage in app.session.flashbag.get('notice') %}
22 <script>
23 Materialize.toast('{{ flashMessage|trans }}', 4000);
24 </script>
25 {% endfor %}
26{% endblock %}
27
28{% block menu %}
29 <nav class="cyan darken-1">
30 <div class="nav-wrapper nav-panels">
31 <a href="#" data-activates="slide-out" class="nav-panel-menu button-collapse"><i class="material-icons">menu</i></a>
32 <div class="left action">
33 <a href="{{ path('homepage') }}">wallabag</a>
34 </div>
35 <ul class="input-field nav-panel-buttom">
36 {% if not is_granted('IS_AUTHENTICATED_FULLY') %}
37 <li>
38 <a href="{{ path('fos_user_security_login') }}">Login</a>
39 </li>
40 {% if registration_enabled %}
41 <li>
42 <a href="{{ path('fos_user_registration_register') }}">Register</a>
43 </li>
44 {% endif %}
45 {% else %}
46 <li>
47 <a href="{{ path('fos_user_security_logout') }}">Logout</a>
48 </li>
49 {% endif %}
50 </ul>
51 </div>
52 </nav>
53
54{% endblock %}
55
56{% block content %}
57<div class="container">
58 <div class="row">
59 <div class="col offset-s2 s8">
60 {{ include('@WallabagFederation/themes/material/User/profile_header.html.twig') }}
61 <ul class="collection">
62 {% for account in users %}
63 <li class="collection-item avatar">
64 <i class="material-icons circle">folder</i>
65 <span class="title">{{ account.username }}@{{ account.server }}</span>
66 <p>First Line</p>
67 <a href="#!" class="secondary-content"><i class="material-icons">grade</i></a>
68 </li>
69 {% endfor %}
70 </ul>
71 </div>
72 </div>
73</div>
74{% endblock %}
75
76{% block footer %}
77 <footer class="page-footer cyan darken-2">
78 <div class="footer-copyright">
79 <div class="container">
80 <div class="row right">
81 <p>
82 {{ 'footer.wallabag.powered_by'|trans }} <a target="_blank" href="https://wallabag.org" class="grey-text text-lighten-4">wallabag</a>
83 </p>
84 </div>
85 </div>
86 </div>
87 </footer>
88{% endblock %}
diff --git a/src/Wallabag/FederationBundle/Resources/views/themes/material/User/profile.html.twig b/src/Wallabag/FederationBundle/Resources/views/themes/material/User/profile.html.twig
new file mode 100644
index 00000000..c52d0ca4
--- /dev/null
+++ b/src/Wallabag/FederationBundle/Resources/views/themes/material/User/profile.html.twig
@@ -0,0 +1,77 @@
1{% extends "WallabagCoreBundle::base.html.twig" %}
2
3{% block css %}
4 {{ parent() }}
5 {% if not app.debug %}
6 <link rel="stylesheet" href="{{ asset('bundles/wallabagcore/material.css') }}">
7 {% endif %}
8{% endblock %}
9
10{% block scripts %}
11 {{ parent() }}
12 <script src="{{ asset('bundles/wallabagcore/material' ~ (app.debug ? '.dev' : '') ~ '.js') }}"></script>
13{% endblock %}
14
15{% block header %}
16{% endblock %}
17
18{% block body_class %}reset-left{% endblock %}
19
20{% block messages %}
21 {% for flashMessage in app.session.flashbag.get('notice') %}
22 <script>
23 Materialize.toast('{{ flashMessage|trans }}', 4000);
24 </script>
25 {% endfor %}
26{% endblock %}
27
28{% block menu %}
29<nav class="cyan darken-1">
30 <div class="nav-wrapper nav-panels">
31 <a href="#" data-activates="slide-out" class="nav-panel-menu button-collapse"><i class="material-icons">menu</i></a>
32 <div class="left action">
33 <a href="{{ path('homepage') }}">wallabag</a>
34 </div>
35 <ul class="input-field nav-panel-buttom">
36 {% if not is_granted('IS_AUTHENTICATED_FULLY') %}
37 <li>
38 <a href="{{ path('fos_user_security_login') }}">Login</a>
39 </li>
40 {% if registration_enabled %}
41 <li>
42 <a href="{{ path('fos_user_registration_register') }}">Register</a>
43 </li>
44 {% endif %}
45 {% else %}
46 <li>
47 <a href="{{ path('fos_user_security_logout') }}">Logout</a>
48 </li>
49 {% endif %}
50 </ul>
51 </div>
52</nav>
53
54{% endblock %}
55
56{% block content %}
57 <div class="container">
58 <div class="row">
59 <div class="col offset-l2 l8">
60 {{ include('@WallabagFederation/themes/material/User/profile_header.html.twig') }}
61 </div>
62 </div>
63{% endblock %}
64
65{% block footer %}
66 <footer class="page-footer cyan darken-2">
67 <div class="footer-copyright">
68 <div class="container">
69 <div class="row right">
70 <p>
71 {{ 'footer.wallabag.powered_by'|trans }} <a target="_blank" href="https://wallabag.org" class="grey-text text-lighten-4">wallabag</a>
72 </p>
73 </div>
74 </div>
75 </div>
76 </footer>
77{% endblock %}
diff --git a/src/Wallabag/FederationBundle/Resources/views/themes/material/User/profile_header.html.twig b/src/Wallabag/FederationBundle/Resources/views/themes/material/User/profile_header.html.twig
new file mode 100644
index 00000000..703de79f
--- /dev/null
+++ b/src/Wallabag/FederationBundle/Resources/views/themes/material/User/profile_header.html.twig
@@ -0,0 +1,35 @@
1{% set usershow = user.username %}
2{% if user.user.name is not null %}
3 {% set usershow = user.user.name %}
4{% endif %}
5
6<p>
7 {{ usershow }} utilise wallabag pour lire et archiver son contenu. Vous pouvez le/la suivre et interagir si vous possédez un compte quelque part dans le "fediverse". Si ce n'est pas le cas, vous pouvez en créer un ici.
8</p>
9<div class="card grey lighten-5 z-depth-1 profile">
10 <div class="card-content">
11 <img src="{{ asset('uploads/media/avatar/') ~ user.avatar }}" alt="" class="circle responsive-img center-block avatar">
12 <h3 class="center-align">{{ usershow }}</h3>
13 <h6 class="center-align">@{{ user.username }}</h6>
14 <a class="btn-floating btn-large halfway-fab waves-effect waves-light red" href="{{ path('follow-user', {'userToFollow': user.username}) }}"><i class="material-icons">person_add</i></a>
15 <div class="details">
16 <div class="bio">
17 <p>{{ user.description }}</p>
18 </div>
19 <div class="details-counters">
20 <div class="counter">
21 <a href="{{ path('followers', {'user': user.username}) }}">
22 <span class="counter-label">Followers</span>
23 <span class="counter-number">{{ user.followers | length }}</span>
24 </a>
25 </div>
26 <div class="counter">
27 <a href="{{ path('following', {'user': user.username}) }}">
28 <span class="counter-label">Following</span>
29 <span class="counter-number">{{ user.following | length }}</span>
30 </a>
31 </div>
32 </div>
33 </div>
34 </div>
35</div>
diff --git a/src/Wallabag/FederationBundle/Resources/views/themes/material/User/recommendations.html.twig b/src/Wallabag/FederationBundle/Resources/views/themes/material/User/recommendations.html.twig
new file mode 100644
index 00000000..b3d0d2cf
--- /dev/null
+++ b/src/Wallabag/FederationBundle/Resources/views/themes/material/User/recommendations.html.twig
@@ -0,0 +1,4 @@
1{{ dump(recommendations) }}
2{% for entry in recommendations %}
3{{ include('@WallabagCore/themes/material/Entry/_card_list.html.twig') }}
4{% endfor %}
diff --git a/src/Wallabag/FederationBundle/WallabagFederationBundle.php b/src/Wallabag/FederationBundle/WallabagFederationBundle.php
new file mode 100644
index 00000000..f9bd665a
--- /dev/null
+++ b/src/Wallabag/FederationBundle/WallabagFederationBundle.php
@@ -0,0 +1,9 @@
1<?php
2
3namespace Wallabag\FederationBundle;
4
5use Symfony\Component\HttpKernel\Bundle\Bundle;
6
7class WallabagFederationBundle extends Bundle
8{
9}
diff --git a/src/Wallabag/UserBundle/Entity/User.php b/src/Wallabag/UserBundle/Entity/User.php
index aed5c73e..f11a277d 100644
--- a/src/Wallabag/UserBundle/Entity/User.php
+++ b/src/Wallabag/UserBundle/Entity/User.php
@@ -15,6 +15,7 @@ use Symfony\Component\Security\Core\User\UserInterface;
15use Wallabag\ApiBundle\Entity\Client; 15use Wallabag\ApiBundle\Entity\Client;
16use Wallabag\CoreBundle\Entity\Config; 16use Wallabag\CoreBundle\Entity\Config;
17use Wallabag\CoreBundle\Entity\Entry; 17use Wallabag\CoreBundle\Entity\Entry;
18use Wallabag\FederationBundle\Entity\Account;
18 19
19/** 20/**
20 * User. 21 * User.
@@ -129,6 +130,14 @@ class User extends BaseUser implements TwoFactorInterface, TrustedComputerInterf
129 */ 130 */
130 protected $default_client; 131 protected $default_client;
131 132
133 /**
134 * @ORM\OneToOne(targetEntity="Wallabag\FederationBundle\Entity\Account", mappedBy="user", cascade={"remove"})
135 */
136 protected $account;
137
138 /**
139 * User constructor.
140 */
132 public function __construct() 141 public function __construct()
133 { 142 {
134 parent::__construct(); 143 parent::__construct();
@@ -333,4 +342,22 @@ class User extends BaseUser implements TwoFactorInterface, TrustedComputerInterf
333 { 342 {
334 $this->notifications = $notifications; 343 $this->notifications = $notifications;
335 } 344 }
345
346 /**
347 * @return Account
348 */
349 public function getAccount()
350 {
351 return $this->account;
352 }
353
354 /**
355 * @param mixed $account
356 * @return User
357 */
358 public function setAccount(Account $account)
359 {
360 $this->account = $account;
361 return $this;
362 }
336} 363}