aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/Wallabag/FederationBundle
diff options
context:
space:
mode:
Diffstat (limited to 'src/Wallabag/FederationBundle')
-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
22 files changed, 1665 insertions, 0 deletions
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}