new WhiteOctober\PagerfantaBundle\WhiteOctoberPagerfantaBundle(),
new FOS\JsRoutingBundle\FOSJsRoutingBundle(),
new BD\GuzzleSiteAuthenticatorBundle\BDGuzzleSiteAuthenticatorBundle(),
+ new OldSound\RabbitMqBundle\OldSoundRabbitMqBundle(),
// wallabag bundles
new Wallabag\CoreBundle\WallabagCoreBundle(),
new Wallabag\UserBundle\WallabagUserBundle(),
new Wallabag\ImportBundle\WallabagImportBundle(),
new Wallabag\AnnotationBundle\WallabagAnnotationBundle(),
- new OldSound\RabbitMqBundle\OldSoundRabbitMqBundle(),
+ new Wallabag\FederationBundle\WallabagFederationBundle(),
];
if (in_array($this->getEnvironment(), ['dev', 'test'], true)) {
--- /dev/null
+<?php
+
+namespace Application\Migrations;
+
+use Doctrine\DBAL\Migrations\AbstractMigration;
+use Doctrine\DBAL\Schema\Schema;
+use Doctrine\DBAL\Schema\SchemaException;
+use Symfony\Component\DependencyInjection\ContainerAwareInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Doctrine\DBAL\Migrations\SkipMigrationException;
+
+/**
+ * Creates the Change table.
+ */
+class Version20170328185535 extends AbstractMigration implements ContainerAwareInterface
+{
+ /**
+ * @var ContainerInterface
+ */
+ private $container;
+
+ public function setContainer(ContainerInterface $container = null)
+ {
+ $this->container = $container;
+ }
+
+ private function getTable($tableName)
+ {
+ return $this->container->getParameter('database_table_prefix').$tableName;
+ }
+
+ /**
+ * @param Schema $schema
+ */
+ public function up(Schema $schema)
+ {
+ try {
+ $schema->getTable($this->getTable('change'));
+ } catch (SchemaException $e) {
+ // The Change table doesn't exist, we need to create it
+ if (10 == $e->getCode()) {
+ if ($this->connection->getDatabasePlatform()->getName() == 'sqlite') {
+ $this->addSql('CREATE TABLE '.$this->getTable('change').' (id INTEGER NOT NULL, entry_id INTEGER DEFAULT NULL, type INTEGER NOT NULL, created_at DATETIME NOT NULL, PRIMARY KEY(id), CONSTRAINT FK_133B9D0FBA364942 FOREIGN KEY (entry_id) REFERENCES '.$this->getTable('entry').' (id) NOT DEFERRABLE INITIALLY IMMEDIATE)');
+
+ return true;
+ }
+
+ $changeTable = $schema->createTable($this->getTable('change'));
+ $changeTable->addColumn(
+ 'id',
+ 'integer',
+ ['autoincrement' => true]
+ );
+ $changeTable->addColumn(
+ 'type',
+ 'integer',
+ ['notnull' => false]
+ );
+ $changeTable->addColumn(
+ 'created_at',
+ 'datetime',
+ ['notnull' => false]
+ );
+ $changeTable->addColumn(
+ 'entry_id',
+ 'integer',
+ ['notnull' => false]
+ );
+
+ $changeTable->setPrimaryKey(['id']);
+
+ $changeTable->addForeignKeyConstraint(
+ $this->getTable('entry'),
+ ['entry_id'],
+ ['id'],
+ ['onDelete' => 'CASCADE'],
+ 'IDX_change_entry'
+ );
+
+ return true;
+ }
+ }
+
+ throw new SkipMigrationException('It seems that you already played this migration.');
+ }
+
+ /**
+ * @param Schema $schema
+ */
+ public function down(Schema $schema)
+ {
+ try {
+ $changeTable = $schema->getTable($this->getTable('change'));
+ $schema->dropTable($changeTable->getName());
+ } catch (SchemaException $e) {
+ throw new SkipMigrationException('It seems that you already played this migration.');
+ }
+ }
+}
@import 'nav';
@import 'sidenav';
@import 'notifications';
+@import 'profile';
@import 'various';
/* Tools */
border-bottom: 1px solid #ddd;
}
+nav,
+body:not(.reset-left) main,
+footer {
+ padding-left: 240px;
+}
+
main,
#content,
.valign-wrapper {
--- /dev/null
+.profile {
+ .details {
+ display: flex;
+ flex-direction: row;
+
+ .bio {
+ flex: 1;
+ font-size: 14px;
+ line-height: 18px;
+ padding: 5px 10px;
+ order: 1;
+
+ p {
+ font-size: 14px;
+ font-weight: 400;
+ overflow: hidden;
+ word-break: normal;
+ word-wrap: break-word;
+ }
+ }
+
+ .details-counters {
+ order: 0;
+ display: flex;
+ flex-direction: row;
+ }
+
+ .counter {
+ width: 80px;
+ color: #9baec8;
+ padding: 5px 10px 0;
+ margin-bottom: 10px;
+ border-right: 1px solid #9baec8;
+ cursor: default;
+ position: relative;
+
+ .counter-label {
+ font-size: 12px;
+ text-transform: uppercase;
+ display: block;
+ margin-bottom: 5px;
+ }
+
+ .counter-number {
+ font-weight: 500;
+ font-size: 18px;
+ color: #00bcd4;
+ }
+ }
+ }
+ img.avatar {
+ width: 10em;
+ }
+}
parameters:
# Allows to use the live reload feature for changes in assets
- use_webpack_dev_server: false
+ use_webpack_dev_server: true
craue_config.cache_adapter.class: Craue\ConfigBundle\CacheAdapter\SymfonyCacheComponentAdapter
+ media_directory: '%kernel.root_dir%/../web/uploads/media'
framework:
#esi: ~
domain_name: https://your-wallabag-url-instance.com
+ # Instance URL
+ domain_name: wallabag.tld
+
mailer_transport: smtp
mailer_host: 127.0.0.1
mailer_user: ~
type: annotation
prefix: /
+wallabag_federation:
+ resource: "@WallabagFederationBundle/Controller/"
+ type: annotation
+ prefix: /
+
app:
resource: "@WallabagCoreBundle/Controller/"
type: annotation
- { path: ^/settings, roles: ROLE_SUPER_ADMIN }
- { path: ^/annotations, roles: ROLE_USER }
- { path: ^/users, roles: ROLE_SUPER_ADMIN }
+ - { path: ^/profile, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/, roles: ROLE_USER }
use Wallabag\AnnotationBundle\Form\EditAnnotationType;
use Wallabag\AnnotationBundle\Form\NewAnnotationType;
use Wallabag\CoreBundle\Entity\Entry;
+use Wallabag\CoreBundle\Event\Activity\Actions\Annotation\AnnotationCreatedEvent;
+use Wallabag\CoreBundle\Event\Activity\Actions\Annotation\AnnotationDeletedEvent;
+use Wallabag\CoreBundle\Event\Activity\Actions\Annotation\AnnotationEditedEvent;
class WallabagAnnotationController extends FOSRestController
{
$em->persist($annotation);
$em->flush();
+ $this->get('event_dispatcher')->dispatch(AnnotationCreatedEvent::NAME, new AnnotationCreatedEvent($annotation));
+
$json = $this->get('serializer')->serialize($annotation, 'json');
return JsonResponse::fromJsonString($json);
$em->flush();
$json = $this->get('serializer')->serialize($annotation, 'json');
+ $this->get('event_dispatcher')->dispatch(AnnotationEditedEvent::NAME, new AnnotationEditedEvent($annotation));
return JsonResponse::fromJsonString($json);
}
$em->remove($annotation);
$em->flush();
+ $this->get('event_dispatcher')->dispatch(AnnotationDeletedEvent::NAME, new AnnotationDeletedEvent($annotation));
+
$json = $this->get('serializer')->serialize($annotation, 'json');
return (new JsonResponse())->setJson($json);
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Wallabag\CoreBundle\Entity\Entry;
use Wallabag\CoreBundle\Entity\Tag;
-use Wallabag\CoreBundle\Event\EntrySavedEvent;
use Wallabag\CoreBundle\Event\EntryDeletedEvent;
+use Wallabag\CoreBundle\Event\EntrySavedEvent;
+use Wallabag\CoreBundle\Event\EntryTaggedEvent;
+use Wallabag\CoreBundle\Event\EntryUpdatedEvent;
class EntryRestController extends WallabagRestController
{
$this->upsertEntry($entry, $request, true);
+ $this->get('event_dispatcher')->dispatch(EntryUpdatedEvent::NAME, new EntryUpdatedEvent($entry));
+
return $this->sendResponse($entry);
}
$em->flush();
// entry saved, dispatch event about it!
+ $this->get('event_dispatcher')->dispatch(EntryUpdatedEvent::NAME, new EntryUpdatedEvent($entry));
$this->get('event_dispatcher')->dispatch(EntrySavedEvent::NAME, new EntrySavedEvent($entry));
return $this->sendResponse($entry);
$this->validateUserAccess($entry->getUser()->getId());
$tags = $request->request->get('tags', '');
+ $tagsEntries = [];
if (!empty($tags)) {
$this->get('wallabag_core.tags_assigner')->assignTagsToEntry($entry, $tags);
}
$em->persist($entry);
$em->flush();
+ $this->get('event_dispatcher')->dispatch(EntryTaggedEvent::NAME, new EntryTaggedEvent($entry, $tagsEntries));
+
return $this->sendResponse($entry);
}
}
if (!is_null($isArchived)) {
- $entry->setArchived((bool) $isArchived);
+ $entry->setArchived((bool)$isArchived);
}
if (!is_null($isStarred)) {
- $entry->setStarred((bool) $isStarred);
+ $entry->setStarred((bool)$isStarred);
}
if (!empty($tags)) {
}
if (!is_null($isPublic)) {
- if (true === (bool) $isPublic && null === $entry->getUid()) {
+ if (true === (bool)$isPublic && null === $entry->getUid()) {
$entry->generateUid();
- } elseif (false === (bool) $isPublic) {
+ } elseif (false === (bool)$isPublic) {
$entry->cleanUid();
}
}
// entry saved, dispatch event about it!
$this->get('event_dispatcher')->dispatch(EntrySavedEvent::NAME, new EntrySavedEvent($entry));
}
+
+ /**
+ * Gets history since a date.
+ *
+ * @ApiDoc(
+ * parameters={
+ * {"name"="since", "dataType"="integer", "required"=true, "format"="A timestamp", "description"="Timestamp of the history's start"},
+ * }
+ * )
+ *
+ * @return JsonResponse
+ */
+ public function getEntriesHistoryAction(Request $request)
+ {
+ $this->validateAuthentication();
+
+ $res = $this->getDoctrine()
+ ->getRepository('WallabagCoreBundle:Change')
+ ->findChangesSinceDate($request->query->get('since'));
+
+ $json = $this->get('serializer')->serialize($res, 'json');
+
+ return (new JsonResponse())->setJson($json);
+ }
}
protected function setupConfig()
{
- $this->defaultOutput->writeln('<info><comment>Step 4 of 5.</comment> Config setup.</info>');
+ $this->defaultOutput->writeln('<info><comment>Step 4 of 5.</comment> config setup.</info>');
$em = $this->getContainer()->get('doctrine.orm.entity_manager');
// cleanup before insert new stuff
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Wallabag\CoreBundle\Entity\Config;
use Wallabag\CoreBundle\Entity\TaggingRule;
+use Wallabag\CoreBundle\Event\Activity\Actions\User\UserDeletedEvent;
+use Wallabag\CoreBundle\Event\Activity\Actions\User\UserEditedEvent;
use Wallabag\CoreBundle\Form\Type\ConfigType;
use Wallabag\CoreBundle\Form\Type\ChangePasswordType;
use Wallabag\CoreBundle\Form\Type\RssType;
use Wallabag\CoreBundle\Form\Type\TaggingRuleType;
use Wallabag\CoreBundle\Form\Type\UserInformationType;
use Wallabag\CoreBundle\Tools\Utils;
+use Wallabag\FederationBundle\Form\Type\AccountType;
+use Wallabag\UserBundle\Entity\User;
class ConfigController extends Controller
{
if ($userForm->isSubmitted() && $userForm->isValid()) {
$userManager->updateUser($user, true);
+ $this->get('event_dispatcher')->dispatch(UserEditedEvent::NAME, new UserEditedEvent($user->getAccount()));
+
+ $this->get('session')->getFlashBag()->add(
+ 'notice',
+ 'flashes.config.notice.user_updated'
+ );
+
+ return $this->redirect($this->generateUrl('config').'#set3');
+ }
+
+ // handle account information
+ $account = $user->getAccount();
+ $accountForm = $this->createForm(AccountType::class, $account, [
+ 'action' => $this->generateUrl('config').'#set3',
+ ]);
+ $accountForm->handleRequest($request);
+
+ if ($accountForm->isSubmitted() && $accountForm->isValid()) {
+
+ $avatar = $account->getAvatar();
+ $banner = $account->getBanner();
+
+ if (null !== $avatar) {
+ $avatarFileName = md5(uniqid('', true)) . '.' . $avatar->guessExtension();
+
+ $avatar->move(
+ $this->getParameter('media_directory') . '/avatar',
+ $avatarFileName
+ );
+ $account->setAvatar($avatarFileName);
+ }
+
+ if (null != $banner) {
+ $bannerFileName = md5(uniqid('', true)) . '.' . $banner->guessExtension();
+
+ $banner->move(
+ $this->get('media_directory') . '/banner',
+ $bannerFileName
+ );
+ $account->setBanner($bannerFileName);
+ }
+
+ $this->get('event_dispatcher')->dispatch(UserEditedEvent::NAME, new UserEditedEvent($user));
+
$this->get('session')->getFlashBag()->add(
'notice',
'flashes.config.notice.user_updated'
'pwd' => $pwdForm->createView(),
'user' => $userForm->createView(),
'new_tagging_rule' => $newTaggingRule->createView(),
+ 'account' => $accountForm->createView(),
],
'rss' => [
'username' => $user->getUsername(),
$this->get('security.token_storage')->setToken(null);
$request->getSession()->invalidate();
+ $account = $user->getAccount();
+
$em = $this->get('fos_user.user_manager');
$em->deleteUser($user);
+ $this->get('event_dispatcher')->dispatch(UserDeletedEvent::NAME, new UserDeletedEvent($account));
+
return $this->redirect($this->generateUrl('fos_user_security_login'));
}
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Wallabag\CoreBundle\Entity\Entry;
+use Wallabag\CoreBundle\Event\Activity\Actions\Entry\EntryEditedEvent;
+use Wallabag\CoreBundle\Event\Activity\Actions\Entry\EntryFavouriteEvent;
+use Wallabag\CoreBundle\Event\Activity\Actions\Entry\EntryReadEvent;
+use Wallabag\CoreBundle\Event\Activity\Actions\Entry\EntrySavedEvent;
use Wallabag\CoreBundle\Form\Type\EntryFilterType;
use Wallabag\CoreBundle\Form\Type\EditEntryType;
use Wallabag\CoreBundle\Form\Type\NewEntryType;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
-use Wallabag\CoreBundle\Event\EntrySavedEvent;
-use Wallabag\CoreBundle\Event\EntryDeletedEvent;
use Wallabag\CoreBundle\Form\Type\SearchEntryType;
class EntryController extends Controller
$entry->toggleArchive();
$this->getDoctrine()->getManager()->flush();
+ $this->get('event_dispatcher')->dispatch(EntryReadEvent::NAME, new EntryReadEvent($entry));
+
$message = 'flashes.entry.notice.entry_unarchived';
if ($entry->isArchived()) {
$message = 'flashes.entry.notice.entry_archived';
$entry->toggleStar();
$this->getDoctrine()->getManager()->flush();
+ $this->get('event_dispatcher')->dispatch(EntryFavouriteEvent::NAME, new EntryFavouriteEvent($entry));
+
$message = 'flashes.entry.notice.entry_unstarred';
if ($entry->isStarred()) {
$message = 'flashes.entry.notice.entry_starred';
UrlGeneratorInterface::ABSOLUTE_PATH
);
- // entry deleted, dispatch event about it!
- $this->get('event_dispatcher')->dispatch(EntryDeletedEvent::NAME, new EntryDeletedEvent($entry));
-
$em = $this->getDoctrine()->getManager();
$em->remove($entry);
$em->flush();
--- /dev/null
+<?php
+
+namespace Wallabag\CoreBundle\Controller;
+
+use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
+use Symfony\Bundle\FrameworkBundle\Controller\Controller;
+use Symfony\Component\HttpFoundation\RedirectResponse;
+use Symfony\Component\Security\Core\Exception\AccessDeniedException;
+use Symfony\Component\Security\Core\Exception\InvalidArgumentException;
+use Wallabag\CoreBundle\Entity\Entry;
+use Wallabag\CoreBundle\Entity\Notification;
+use Wallabag\CoreBundle\Entity\Share;
+use Wallabag\CoreBundle\Event\Activity\Actions\Entry\EntrySavedEvent;
+use Wallabag\CoreBundle\Event\Activity\Actions\Share\ShareAcceptedEvent;
+use Wallabag\CoreBundle\Event\Activity\Actions\Share\ShareCancelledEvent;
+use Wallabag\CoreBundle\Event\Activity\Actions\Share\ShareCreatedEvent;
+use Wallabag\CoreBundle\Event\Activity\Actions\Share\ShareDeniedEvent;
+use Wallabag\CoreBundle\Notifications\NoAction;
+use Wallabag\CoreBundle\Notifications\YesAction;
+use Wallabag\UserBundle\Entity\User;
+
+class ShareController extends Controller
+{
+ /**
+ * @Route("/share-user/{entry}/{destination}", name="share-entry-user", requirements={"entry" = "\d+", "destination" = "\d+"})
+ * @param Entry $entry
+ * @param User $destination
+ * @throws AccessDeniedException
+ * @throws InvalidArgumentException
+ */
+ public function shareEntryAction(Entry $entry, User $destination)
+ {
+
+ if ($entry->getUser() !== $this->getUser()) {
+ throw new AccessDeniedException("You can't share this entry");
+ }
+
+ if ($destination === $this->getUser()) {
+ throw new InvalidArgumentException("You can't share entries to yourself");
+ }
+
+ $share = new Share();
+ $share->setUserOrigin($this->getUser())
+ ->setEntry($entry)
+ ->setUserDestination($destination);
+
+ $em = $this->getDoctrine()->getManager();
+ $em->persist($share);
+ $em->flush();
+
+ $this->get('event_dispatcher')->dispatch(ShareCreatedEvent::NAME, new ShareCancelledEvent($share));
+
+ $accept = new YesAction($this->generateUrl('share-entry-user-accept', ['share' => $share->getId()]));
+
+ $deny = new NoAction($this->generateUrl('share-entry-user-refuse', ['share' => $share->getId()]));
+
+ $notification = new Notification($destination);
+ $notification->setType(Notification::TYPE_SHARE)
+ ->setTitle($this->get('translator')->trans('share.notification.new.title'))
+ ->addAction($accept)
+ ->addAction($deny);
+
+ $em->persist($notification);
+ $em->flush();
+
+ $this->redirectToRoute('view', ['id' => $entry->getId()]);
+ }
+
+ /**
+ * @Route("/share-user/accept/{share}", name="share-entry-user-accept")
+ *
+ * @param Share $share
+ * @return RedirectResponse
+ * @throws AccessDeniedException
+ */
+ public function acceptShareAction(Share $share)
+ {
+ if ($share->getUserDestination() !== $this->getUser()) {
+ throw new AccessDeniedException("You can't accept this entry");
+ }
+
+ $entry = new Entry($this->getUser());
+ $entry->setUrl($share->getEntry()->getUrl());
+
+ $em = $this->getDoctrine()->getManager();
+
+ if (false === $this->checkIfEntryAlreadyExists($entry)) {
+ $this->updateEntry($entry);
+
+ $em->persist($entry);
+ $em->flush();
+
+ $this->get('event_dispatcher')->dispatch(ShareAcceptedEvent::NAME, new ShareAcceptedEvent($share));
+
+ // entry saved, dispatch event about it!
+ $this->get('event_dispatcher')->dispatch(EntrySavedEvent::NAME, new EntrySavedEvent($entry));
+ }
+
+ $em->remove($share);
+ $em->flush(); // we keep the previous flush above in case the event dispatcher would lead in using the saved entry
+
+ return $this->redirect($this->generateUrl('homepage'));
+ }
+
+ /**
+ * @Route("/share-user/refuse/{share}", name="share-entry-user-refuse")
+ *
+ * @param Share $share
+ * @return RedirectResponse
+ */
+ public function refuseShareAction(Share $share)
+ {
+ $em = $this->getDoctrine()->getManager();
+ $em->remove($share);
+ $em->flush();
+
+ $this->get('event_dispatcher')->dispatch(ShareDeniedEvent::NAME, new ShareDeniedEvent($share));
+
+ return $this->redirect($this->generateUrl('homepage'));
+ }
+
+ /**
+ * Fetch content and update entry.
+ * In case it fails, entry will return to avod loosing the data.
+ *
+ * @param Entry $entry
+ * @param string $prefixMessage Should be the translation key: entry_saved or entry_reloaded
+ *
+ * @return Entry
+ */
+ private function updateEntry(Entry $entry, $prefixMessage = 'entry_saved')
+ {
+ // put default title in case of fetching content failed
+ $entry->setTitle('No title found');
+
+ $message = 'flashes.entry.notice.'.$prefixMessage;
+
+ try {
+ $entry = $this->get('wallabag_core.content_proxy')->updateEntry($entry, $entry->getUrl());
+ } catch (\Exception $e) {
+ $this->get('logger')->error('Error while saving an entry', [
+ 'exception' => $e,
+ 'entry' => $entry,
+ ]);
+
+ $message = 'flashes.entry.notice.'.$prefixMessage.'_failed';
+ }
+
+ $this->get('session')->getFlashBag()->add('notice', $message);
+
+ return $entry;
+ }
+
+ /**
+ * Check for existing entry, if it exists, redirect to it with a message.
+ *
+ * @param Entry $entry
+ *
+ * @return Entry|bool
+ */
+ private function checkIfEntryAlreadyExists(Entry $entry)
+ {
+ return $this->get('wallabag_core.entry_repository')->findByUrlAndUserId($entry->getUrl(), $this->getUser()->getId());
+ }
+}
use Symfony\Component\HttpFoundation\Request;
use Wallabag\CoreBundle\Entity\Entry;
use Wallabag\CoreBundle\Entity\Tag;
+use Wallabag\CoreBundle\Event\Activity\Actions\Entry\EntryTaggedEvent;
use Wallabag\CoreBundle\Form\Type\NewTagType;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
$em->persist($entry);
$em->flush();
+ $this->get('event_dispatcher')->dispatch(EntryTaggedEvent::NAME, new EntryTaggedEvent($entry, $tags));
+
$this->get('session')->getFlashBag()->add(
'notice',
'flashes.tag.notice.tag_added'
$em = $this->getDoctrine()->getManager();
$em->flush();
+ $this->get('event_dispatcher')->dispatch(EntryTaggedEvent::NAME, new EntryTaggedEvent($entry, $tag), true);
+
// remove orphan tag in case no entries are associated to it
if (count($tag->getEntries()) === 0) {
$em->remove($tag);
--- /dev/null
+<?php
+
+namespace Wallabag\CoreBundle\Entity;
+
+use Doctrine\ORM\Mapping as ORM;
+use Wallabag\FederationBundle\Entity\Account;
+
+/**
+ * Change.
+ *
+ * This entity stores a datetime for each activity.
+ *
+ * @ORM\Entity(repositoryClass="Wallabag\CoreBundle\Repository\ChangeRepository")
+ * @ORM\Table(name="`activity`")
+ */
+class Activity
+{
+ /**
+ * Object types
+ */
+ const ENTRY_OBJECT = 1;
+ const TAG_OBJECT = 2;
+ const USER_OBJECT = 3;
+ const SHARE_OBJECT = 4;
+ const GROUP_OBJECT = 5;
+ const ANNOTATION_OBJECT = 6;
+ const CONFIG_OBJECT = 7;
+ const ACCOUNT_OBJECT = 8;
+
+ /**
+ * Events
+ */
+
+ /**
+ * Entry events
+ */
+ const ENTRY_ADD = 10; // done
+ const ENTRY_EDIT = 11; // done
+ const ENTRY_READ = 12; // done
+ const ENTRY_UNREAD = 13; // done
+ const ENTRY_FAVOURITE = 14; // done
+ const ENTRY_UNFAVOURITE = 15; // done
+ const ENTRY_DELETE = 19; // done
+
+ /**
+ * Tag events
+ */
+ const TAG_CREATE = 20; // not yet implemented
+ const TAG_EDIT = 21; // not yet implemented
+ const TAG_REMOVE = 29; // not yet implemented
+
+ /**
+ * Entry - Tag events
+ */
+ const ENTRY_ADD_TAG = 30; // done
+ const ENTRY_REMOVE_TAG = 39; // done
+
+ /**
+ * Entry - Annotation events
+ */
+ const ANNOTATION_ADD = 40; // done
+ const ANNOTATION_EDIT = 41; // done
+ const ANNOTATION_REMOVE = 49; // done
+
+ /**
+ * User events
+ */
+ const USER_CREATE = 50; // done
+ const USER_EDIT = 51; // done
+ const USER_REMOVE = 59; // done
+
+ /**
+ * Federation events
+ */
+ const FOLLOW_ACCOUNT = 61;
+ const UNFOLLOW_ACCOUNT = 62;
+ const RECOMMEND_ENTRY = 63;
+
+ /**
+ * Share events
+ */
+ const USER_SHARE_CREATED = 70; // done
+ const USER_SHARE_ACCEPTED = 71; // done
+ const USER_SHARE_REFUSED = 72; // done
+ const USER_SHARE_CANCELLED = 79; // not implemented yet
+
+ /**
+ * Group events
+ */
+ const GROUP_CREATE = 80;
+ const GROUP_EDIT = 81;
+ const GROUP_ADD_MEMBER = 82;
+ const GROUP_EDIT_MEMBER = 83;
+ const GROUP_REMOVE_MEMBER = 84;
+ const GROUP_SHARE_ENTRY = 85;
+ const GROUP_DELETE = 89;
+
+ /**
+ * @var int
+ *
+ * @ORM\Column(type="integer")
+ * @ORM\Id
+ * @ORM\GeneratedValue(strategy="AUTO")
+ */
+ private $id;
+
+ /**
+ * @var int
+ *
+ * @ORM\Column(type="integer")
+ */
+ private $activityType;
+
+ /**
+ * @var Account
+ */
+ private $user;
+
+ /**
+ * @var int
+ *
+ * @ORM\Column(type="integer")
+ */
+ private $primaryObjectType;
+
+ /**
+ * @var int
+ *
+ * @ORM\Column(type="integer")
+ */
+ private $primaryObjectId;
+
+ /**
+ * @var int
+ *
+ * @ORM\Column(type="integer", nullable=true)
+ */
+ private $secondaryObjectType;
+
+ /**
+ * @var int
+ *
+ * @ORM\Column(type="integer", nullable=true)
+ */
+ private $secondaryObjectId;
+
+ /**
+ * @var \DateTime
+ *
+ * @ORM\Column(name="created_at", type="datetime")
+ */
+ private $createdAt;
+
+ public function __construct($activityType, $primaryObjectType, $primaryObjectId)
+ {
+ $this->activityType = $activityType;
+ $this->primaryObjectType = $primaryObjectType;
+ $this->primaryObjectId = $primaryObjectId;
+ $this->createdAt = new \DateTime();
+ }
+
+ /**
+ * @return int
+ */
+ public function getId()
+ {
+ return $this->id;
+ }
+
+ /**
+ * @return int
+ */
+ public function getActivityType()
+ {
+ return $this->activityType;
+ }
+
+ /**
+ * @param int $activityType
+ * @return Activity
+ */
+ public function setActivityType($activityType)
+ {
+ $this->activityType = $activityType;
+ return $this;
+ }
+
+ /**
+ * @return \DateTime
+ */
+ public function getCreatedAt()
+ {
+ return $this->createdAt;
+ }
+
+ /**
+ * @param \DateTime $createdAt
+ * @return Activity
+ */
+ public function setCreatedAt(\DateTime $createdAt)
+ {
+ $this->createdAt = $createdAt;
+ return $this;
+ }
+
+ /**
+ * @return int
+ */
+ public function getPrimaryObjectId()
+ {
+ return $this->primaryObjectId;
+ }
+
+ /**
+ * @param $primaryObjectId
+ * @return Activity
+ */
+ public function setPrimaryObjectId($primaryObjectId)
+ {
+ $this->primaryObjectId = $primaryObjectId;
+ return $this;
+ }
+
+ /**
+ * @return Account
+ */
+ public function getUser()
+ {
+ return $this->user;
+ }
+
+ /**
+ * @param Account $user
+ * @return Activity
+ */
+ public function setUser($user)
+ {
+ $this->user = $user;
+ return $this;
+ }
+
+ /**
+ * @return int
+ */
+ public function getPrimaryObjectType()
+ {
+ return $this->primaryObjectType;
+ }
+
+ /**
+ * @param int $primaryObjectType
+ * @return Activity
+ */
+ public function setPrimaryObjectType($primaryObjectType)
+ {
+ $this->primaryObjectType = $primaryObjectType;
+ return $this;
+ }
+
+ /**
+ * @return int
+ */
+ public function getSecondaryObjectType()
+ {
+ return $this->secondaryObjectType;
+ }
+
+ /**
+ * @param int $secondaryObjectType
+ * @return Activity
+ */
+ public function setSecondaryObjectType($secondaryObjectType)
+ {
+ $this->secondaryObjectType = $secondaryObjectType;
+ return $this;
+ }
+
+ /**
+ * @return int
+ */
+ public function getSecondaryObjectId()
+ {
+ return $this->secondaryObjectId;
+ }
+
+ /**
+ * @param int $secondaryObjectId
+ * @return Activity
+ */
+ public function setSecondaryObjectId($secondaryObjectId)
+ {
+ $this->secondaryObjectId = $secondaryObjectId;
+ return $this;
+ }
+}
use Wallabag\UserBundle\Entity\User;
/**
- * Config.
+ * config.
*
* @ORM\Entity(repositoryClass="Wallabag\CoreBundle\Repository\ConfigRepository")
* @ORM\Table(name="`config`")
*/
private $tags;
- /*
+ /**
+ * @var boolean
+ *
+ * @ORM\Column(name="recommended", type="boolean", nullable=true)
+ */
+ private $recommended;
+
+ /**
* @param User $user
*/
public function __construct(User $user)
{
$this->user = $user;
$this->tags = new ArrayCollection();
+ $this->changes = new ArrayCollection();
}
/**
return $this;
}
+
+ /**
+ * @return bool
+ */
+ public function isRecommended()
+ {
+ return $this->recommended;
+ }
+
+ /**
+ * @param bool $recommended
+ */
+ public function setRecommended($recommended)
+ {
+ $this->recommended = $recommended;
+ }
}
const TYPE_ADMIN = 0;
const TYPE_USER = 1;
const TYPE_RELEASE = 2;
+ const TYPE_SHARE = 3;
public function __construct(User $user = null)
{
--- /dev/null
+<?php
+
+namespace Wallabag\CoreBundle\Entity;
+
+use Wallabag\FederationBundle\Entity\Account;
+use Wallabag\UserBundle\Entity\User;
+use Doctrine\ORM\Mapping as ORM;
+
+/**
+ * Share.
+ *
+ * @ORM\Entity
+ */
+class Share
+{
+ /**
+ * @var int
+ *
+ * @ORM\Column(name="id", type="integer")
+ * @ORM\Id
+ * @ORM\GeneratedValue(strategy="AUTO")
+ */
+ private $id;
+
+ /**
+ * @var Account
+ *
+ * @ORM\ManyToOne(targetEntity="Wallabag\FederationBundle\Entity\Account")
+ */
+ private $userOrigin;
+
+ /**
+ * @var Account
+ *
+ * @ORM\ManyToOne(targetEntity="Wallabag\FederationBundle\Entity\Account")
+ */
+ private $userDestination;
+
+ /**
+ * @var Entry
+ *
+ * @ORM\ManyToOne(targetEntity="Wallabag\CoreBundle\Entity\Entry")
+ */
+ private $entry;
+
+ /**
+ * @var boolean
+ *
+ * @ORM\Column(name="accepted", type="boolean")
+ */
+ private $accepted;
+
+ /**
+ * Share constructor.
+ */
+ public function __construct()
+ {
+ $this->accepted = false;
+ }
+
+ /**
+ * @return int
+ */
+ public function getId()
+ {
+ return $this->id;
+ }
+
+ /**
+ * @return Account
+ */
+ public function getUserOrigin()
+ {
+ return $this->userOrigin;
+ }
+
+ /**
+ * @param User $userOrigin
+ * @return Share
+ */
+ public function setUserOrigin(User $userOrigin)
+ {
+ $this->userOrigin = $userOrigin;
+ return $this;
+ }
+
+ /**
+ * @return Account
+ */
+ public function getUserDestination()
+ {
+ return $this->userDestination;
+ }
+
+ /**
+ * @param User $userDestination
+ * @return Share
+ */
+ public function setUserDestination(User $userDestination)
+ {
+ $this->userDestination = $userDestination;
+ return $this;
+ }
+
+ /**
+ * @return bool
+ */
+ public function isAccepted()
+ {
+ return $this->accepted;
+ }
+
+ /**
+ * @param bool $accepted
+ * @return Share
+ */
+ public function setAccepted($accepted)
+ {
+ $this->accepted = $accepted;
+ return $this;
+ }
+
+ /**
+ * @return Entry
+ */
+ public function getEntry()
+ {
+ return $this->entry;
+ }
+
+ /**
+ * @param Entry $entry
+ * @return Share
+ */
+ public function setEntry(Entry $entry)
+ {
+ $this->entry = $entry;
+ return $this;
+ }
+}
--- /dev/null
+<?php
+
+namespace Wallabag\CoreBundle\Event\Activity\Actions\Annotation;
+
+class AnnotationCreatedEvent extends AnnotationEvent
+{
+ const NAME = 'annotation.created';
+}
--- /dev/null
+<?php
+
+namespace Wallabag\CoreBundle\Event\Activity\Actions\Annotation;
+
+class AnnotationDeletedEvent extends AnnotationEvent
+{
+ const NAME = 'annotation.deleted';
+}
--- /dev/null
+<?php
+
+namespace Wallabag\CoreBundle\Event\Activity\Actions\Annotation;
+
+class AnnotationEditedEvent extends AnnotationEvent
+{
+ const NAME = 'annotation.edited';
+}
--- /dev/null
+<?php
+
+namespace Wallabag\CoreBundle\Event\Activity\Actions\Annotation;
+
+use Symfony\Component\EventDispatcher\Event;
+use Wallabag\AnnotationBundle\Entity\Annotation;
+
+/**
+ * This event is fired when annotation-relative stuff is made.
+ */
+abstract class AnnotationEvent extends Event
+{
+ protected $annotation;
+
+ /**
+ * AnnotationEvent constructor.
+ * @param Annotation $annotation
+ */
+ public function __construct(Annotation $annotation)
+ {
+ $this->annotation = $annotation;
+ }
+
+ /**
+ * @return Annotation
+ */
+ public function getAnnotation()
+ {
+ return $this->annotation;
+ }
+
+ /**
+ * @param Annotation $annotation
+ */
+ public function setAnnotation(Annotation $annotation)
+ {
+ $this->annotation = $annotation;
+ }
+}
--- /dev/null
+<?php
+
+namespace Wallabag\CoreBundle\Event\Activity\Actions\Entry;
+
+/**
+ * This event is fired as soon as an entry is deleted.
+ */
+class EntryDeletedEvent extends EntryEvent
+{
+ const NAME = 'entry.deleted';
+}
--- /dev/null
+<?php
+
+namespace Wallabag\CoreBundle\Event\Activity\Actions\Entry;
+
+/**
+ * This event is fired as soon as an entry was edited.
+ */
+class EntryEditedEvent extends EntryEvent
+{
+ const NAME = 'entry.edited';
+}
--- /dev/null
+<?php
+
+namespace Wallabag\CoreBundle\Event\Activity\Actions\Entry;
+
+use Symfony\Component\EventDispatcher\Event;
+use Wallabag\CoreBundle\Entity\Entry;
+
+/**
+ * This event is fired when entry-related stuff is made.
+ */
+abstract class EntryEvent extends Event
+{
+ protected $entry;
+
+ /**
+ * EntryEvent constructor.
+ * @param Entry $entry
+ */
+ public function __construct(Entry $entry)
+ {
+ $this->entry = $entry;
+ }
+
+ /**
+ * @return Entry
+ */
+ public function getEntry()
+ {
+ return $this->entry;
+ }
+
+ /**
+ * @param Entry $entry
+ * @return EntryEvent
+ */
+ public function setEntry(Entry $entry)
+ {
+ $this->entry = $entry;
+ return $this;
+ }
+}
--- /dev/null
+<?php
+
+namespace Wallabag\CoreBundle\Event\Activity\Actions\Entry;
+
+use Symfony\Component\EventDispatcher\Event;
+use Wallabag\CoreBundle\Entity\Entry;
+
+/**
+ * This event is fired as soon as an entry was favourited.
+ */
+class EntryFavouriteEvent extends EntryEvent
+{
+ const NAME = 'entry.favourite';
+}
--- /dev/null
+<?php
+
+namespace Wallabag\CoreBundle\Event\Activity\Actions\Entry;
+
+use Symfony\Component\EventDispatcher\Event;
+use Wallabag\CoreBundle\Entity\Entry;
+
+/**
+ * This event is fired as soon as an entry was favourited.
+ */
+class EntryReadEvent extends EntryEvent
+{
+ const NAME = 'entry.read';
+}
--- /dev/null
+<?php
+
+namespace Wallabag\CoreBundle\Event\Activity\Actions\Entry;
+
+/**
+ * This event is fired as soon as an entry was saved.
+ */
+class EntrySavedEvent extends EntryEvent
+{
+ const NAME = 'entry.saved';
+}
--- /dev/null
+<?php
+
+namespace Wallabag\CoreBundle\Event\Activity\Actions\Entry;
+
+use Wallabag\CoreBundle\Entity\Entry;
+use Wallabag\CoreBundle\Entity\Tag;
+
+/**
+ * This event is fired as soon as a tag is added on an entry.
+ */
+class EntryTaggedEvent extends EntryEvent
+{
+ const NAME = 'entry.tagged';
+
+ /** @var Tag[] */
+ protected $tags;
+
+ /**
+ * @var boolean
+ */
+ protected $remove;
+
+ /**
+ * EntryTaggedEvent constructor.
+ * @param Entry $entry
+ * @param $tags
+ * @param bool $remove
+ */
+ public function __construct(Entry $entry, $tags, $remove = false)
+ {
+ parent::__construct($entry);
+
+ if (false === is_array($tags)) {
+ $tags = [$tags];
+ }
+
+ $this->tags = $tags;
+ }
+
+ /**
+ * @return Tag[]
+ */
+ public function getTags()
+ {
+ return $this->tags;
+ }
+
+ /**
+ * @return bool
+ */
+ public function isRemove()
+ {
+ return $this->remove;
+ }
+}
--- /dev/null
+<?php
+
+namespace Wallabag\CoreBundle\Event\Activity\Actions\Federation;
+
+use Symfony\Component\EventDispatcher\Event;
+use Wallabag\FederationBundle\Entity\Account;
+
+abstract class FederationEvent extends Event
+{
+ protected $account;
+
+ /**
+ * FederationEvent constructor.
+ * @param Account $account
+ */
+ public function __construct(Account $account)
+ {
+ $this->account = $account;
+ }
+
+ /**
+ * @return Account
+ */
+ public function getAccount()
+ {
+ return $this->account;
+ }
+
+ /**
+ * @param Account $account
+ * @return FederationEvent
+ */
+ public function setAccount(Account $account)
+ {
+ $this->account = $account;
+ return $this;
+ }
+
+
+}
--- /dev/null
+<?php
+
+namespace Wallabag\CoreBundle\Event\Activity\Actions\Federation;
+
+use Wallabag\FederationBundle\Entity\Account;
+
+/**
+ * This event is fired as soon as an account was followed.
+ */
+class FollowEvent extends FederationEvent
+{
+ const NAME = 'federation.follow';
+
+ protected $follower;
+
+ public function __construct(Account $accountFollowed, Account $follower)
+ {
+ parent::__construct($accountFollowed);
+ $this->follower = $follower;
+ }
+
+ /**
+ * @return Account
+ */
+ public function getFollower()
+ {
+ return $this->follower;
+ }
+
+ /**
+ * @param Account $follower
+ * @return FollowEvent
+ */
+ public function setFollower(Account $follower)
+ {
+ $this->follower = $follower;
+ return $this;
+ }
+}
--- /dev/null
+<?php
+
+namespace Wallabag\CoreBundle\Event\Activity\Actions\Federation;
+
+use Symfony\Component\EventDispatcher\Event;
+use Wallabag\CoreBundle\Entity\Entry;
+
+/**
+ * This event is fired as soon as an entry was recommended.
+ */
+class RecommendedEntryEvent extends Event
+{
+ const NAME = 'federation.recommend';
+
+ protected $entry;
+
+ /**
+ * FederationEvent constructor.
+ * @param Entry $entry
+ */
+ public function __construct(Entry $entry)
+ {
+ $this->entry = $entry;
+ }
+
+ /**
+ * @return Entry
+ */
+ public function getEntry()
+ {
+ return $this->entry;
+ }
+
+ /**
+ * @param Entry $entry
+ * @return RecommendedEntryEvent
+ */
+ public function setEntry(Entry $entry)
+ {
+ $this->entry = $entry;
+ return $this;
+ }
+}
--- /dev/null
+<?php
+
+namespace Wallabag\CoreBundle\Event\Activity\Actions\Federation;
+
+use Wallabag\FederationBundle\Entity\Account;
+
+/**
+ * This event is fired as soon as an account is being unfollowed
+ */
+class UnfollowEvent extends FederationEvent
+{
+ const NAME = 'federation.unfollow';
+
+ protected $follower;
+
+ public function __construct(Account $accountFollowed, Account $follower)
+ {
+ parent::__construct($accountFollowed);
+ $this->follower = $follower;
+ }
+
+ /**
+ * @return Account
+ */
+ public function getFollower()
+ {
+ return $this->follower;
+ }
+
+ /**
+ * @param Account $follower
+ * @return UnfollowEvent
+ */
+ public function setFollower(Account $follower)
+ {
+ $this->follower = $follower;
+ return $this;
+ }
+}
--- /dev/null
+<?php
+
+namespace Wallabag\CoreBundle\Event\Activity\Actions\Share;
+
+/**
+ * This event is fired as soon as an share is accepted
+ */
+class ShareAcceptedEvent extends ShareEvent
+{
+ const NAME = 'share.accepted';
+}
--- /dev/null
+<?php
+
+namespace Wallabag\CoreBundle\Event\Activity\Actions\Share;
+
+/**
+ * This event is fired as soon as an share is cancelled
+ */
+class ShareCancelledEvent extends ShareEvent
+{
+ const NAME = 'share.cancelled';
+}
--- /dev/null
+<?php
+
+namespace Wallabag\CoreBundle\Event\Activity\Actions\Share;
+
+/**
+ * This event is fired as soon as a share is created.
+ */
+class ShareCreatedEvent extends ShareEvent
+{
+ const NAME = 'share.created';
+}
--- /dev/null
+<?php
+
+namespace Wallabag\CoreBundle\Event\Activity\Actions\Share;
+
+/**
+ * This event is fired as soon as an share is denied
+ */
+class ShareDeniedEvent extends ShareEvent
+{
+ const NAME = 'share.denied';
+}
--- /dev/null
+<?php
+
+namespace Wallabag\CoreBundle\Event\Activity\Actions\Share;
+
+use Symfony\Component\EventDispatcher\Event;
+use Wallabag\CoreBundle\Entity\Share;
+
+/**
+ * This event is fired when share-related stuff is made.
+ */
+abstract class ShareEvent extends Event
+{
+ protected $share;
+
+ /**
+ * ShareEvent constructor.
+ * @param Share $share
+ */
+ public function __construct(Share $share)
+ {
+ $this->share = $share;
+ }
+
+ /**
+ * @return Share
+ */
+ public function getShare()
+ {
+ return $this->share;
+ }
+
+ /**
+ * @param Share $share
+ */
+ public function setShare(Share $share)
+ {
+ $this->share = $share;
+ }
+}
--- /dev/null
+<?php
+
+namespace Wallabag\CoreBundle\Event\Activity\Actions\User;
+
+class UserDeletedEvent extends UserEvent
+{
+ const NAME = 'user.deleted';
+}
--- /dev/null
+<?php
+
+namespace Wallabag\CoreBundle\Event\Activity\Actions\User;
+
+class UserEditedEvent extends UserEvent
+{
+ const NAME = 'user.edited';
+}
--- /dev/null
+<?php
+
+namespace Wallabag\CoreBundle\Event\Activity\Actions\User;
+
+use Symfony\Component\EventDispatcher\Event;
+use Wallabag\UserBundle\Entity\User;
+
+/**
+ * This event is fired when user-related stuff is made.
+ */
+abstract class UserEvent extends Event
+{
+ protected $user;
+
+ /**
+ * UserEvent constructor.
+ * @param User $user
+ */
+ public function __construct(User $user)
+ {
+ $this->user = $user;
+ }
+
+ /**
+ * @return User
+ */
+ public function getUser()
+ {
+ return $this->user;
+ }
+
+ /**
+ * @param User $user
+ * @return UserEvent
+ */
+ public function setUser(User $user)
+ {
+ $this->user = $user;
+ return $this;
+ }
+}
--- /dev/null
+<?php
+
+namespace Wallabag\CoreBundle\Event\Activity;
+
+use Doctrine\ORM\EntityManager;
+use FOS\UserBundle\Event\UserEvent;
+use FOS\UserBundle\FOSUserEvents;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\EventDispatcher\Event;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Wallabag\CoreBundle\Entity\Activity;
+use Wallabag\CoreBundle\Event\Activity\Actions\Annotation\AnnotationCreatedEvent;
+use Wallabag\CoreBundle\Event\Activity\Actions\Annotation\AnnotationDeletedEvent;
+use Wallabag\CoreBundle\Event\Activity\Actions\Annotation\AnnotationEditedEvent;
+use Wallabag\CoreBundle\Event\Activity\Actions\Annotation\AnnotationEvent;
+use Wallabag\CoreBundle\Event\Activity\Actions\Entry\EntryDeletedEvent;
+use Wallabag\CoreBundle\Event\Activity\Actions\Entry\EntryEditedEvent;
+use Wallabag\CoreBundle\Event\Activity\Actions\Entry\EntryEvent;
+use Wallabag\CoreBundle\Event\Activity\Actions\Entry\EntryFavouriteEvent;
+use Wallabag\CoreBundle\Event\Activity\Actions\Entry\EntryReadEvent;
+use Wallabag\CoreBundle\Event\Activity\Actions\Entry\EntrySavedEvent;
+use Wallabag\CoreBundle\Event\Activity\Actions\Entry\EntryTaggedEvent;
+use Wallabag\CoreBundle\Event\Activity\Actions\Federation\FollowEvent;
+use Wallabag\CoreBundle\Event\Activity\Actions\Federation\RecommendedEntryEvent;
+use Wallabag\CoreBundle\Event\Activity\Actions\Federation\UnfollowEvent;
+use Wallabag\CoreBundle\Event\Activity\Actions\Share\ShareAcceptedEvent;
+use Wallabag\CoreBundle\Event\Activity\Actions\Share\ShareCancelledEvent;
+use Wallabag\CoreBundle\Event\Activity\Actions\Share\ShareCreatedEvent;
+use Wallabag\CoreBundle\Event\Activity\Actions\Share\ShareDeniedEvent;
+use Wallabag\CoreBundle\Event\Activity\Actions\Share\ShareEvent;
+use Wallabag\CoreBundle\Event\Activity\Actions\User\UserDeletedEvent;
+use Wallabag\CoreBundle\Event\Activity\Actions\User\UserEditedEvent;
+use Wallabag\CoreBundle\Notifications\ActionInterface;
+
+/**
+ * This listener will create the associated configuration when a user register.
+ * This configuration will be created right after the registration (no matter if it needs an email validation).
+ */
+class ActivitySubscriber implements EventSubscriberInterface
+{
+
+ /**
+ * @var EntityManager
+ */
+ private $em;
+
+ /**
+ * @var LoggerInterface $logger
+ */
+ private $logger;
+
+ public function __construct(EntityManager $em, LoggerInterface $logger)
+ {
+ $this->em = $em;
+ $this->logger = $logger;
+ }
+
+ public static function getSubscribedEvents()
+ {
+ return [
+ EntrySavedEvent::NAME => 'entryActivity',
+ EntryDeletedEvent::NAME => 'entryActivity',
+ EntryEditedEvent::NAME => 'entryActivity',
+ EntryTaggedEvent::NAME => 'taggedEntry',
+ EntryFavouriteEvent::NAME => 'entryActivity',
+ EntryReadEvent::NAME => 'entryActivity',
+
+ AnnotationCreatedEvent::NAME => 'annotationActivity',
+ AnnotationEditedEvent::NAME => 'annotationActivity',
+ AnnotationDeletedEvent::NAME => 'annotationActivity',
+
+ FollowEvent::NAME => 'followedAccount',
+ UnfollowEvent::NAME => 'unfollowedAccount',
+ RecommendedEntryEvent::NAME => 'recommendedEntry',
+
+ ShareCreatedEvent::NAME => 'shareActivity',
+ ShareAcceptedEvent::NAME => 'shareActivity',
+ ShareDeniedEvent::NAME => 'shareActivity',
+ ShareCancelledEvent::NAME => 'shareActivity',
+
+ // when a user register using the normal form
+ FOSUserEvents::REGISTRATION_COMPLETED => 'userActivity',
+ // when we manually create a user using the command line
+ // OR when we create it from the config UI
+ FOSUserEvents::USER_CREATED => 'userActivity',
+ UserEditedEvent::NAME => 'userActivity',
+ UserDeletedEvent::NAME => 'userActivity',
+ ];
+ }
+
+ public function userActivity(Event $event)
+ {
+ $activityType = 0;
+ if ($event instanceof UserEvent) {
+ $activityType = Activity::USER_CREATE;
+ } elseif ($event instanceof UserEditedEvent) {
+ $activityType = Activity::USER_EDIT;
+ } elseif ($event instanceof UserDeletedEvent) {
+ $activityType = Activity::USER_REMOVE;
+ }
+
+ $user = $event->getUser();
+ $activity = new Activity($activityType, Activity::USER_OBJECT, $user->getId());
+ $activity->setUser($user->getAccount());
+ $this->em->persist($activity);
+ $this->em->flush();
+ }
+
+ public function entryActivity(EntryEvent $event)
+ {
+ $entry = $event->getEntry();
+
+ $activityType = 0;
+ if ($event instanceof EntrySavedEvent) {
+ $activityType = Activity::ENTRY_ADD;
+ } elseif ($event instanceof EntryDeletedEvent) {
+ $activityType = Activity::ENTRY_DELETE;
+ } elseif ($event instanceof EntryEditedEvent) {
+ $activityType = Activity::ENTRY_EDIT;
+ } elseif ($event instanceof EntryFavouriteEvent) {
+ if ($entry->isStarred()) {
+ $activityType = Activity::ENTRY_FAVOURITE;
+ } else {
+ $activityType = Activity::ENTRY_UNFAVOURITE;
+ }
+ } elseif ($event instanceof EntryReadEvent) {
+ if ($entry->isArchived()) {
+ $activityType = Activity::ENTRY_READ;
+ } else {
+ $activityType = Activity::ENTRY_UNREAD;
+ }
+ }
+
+ $activity = new Activity($activityType, Activity::ENTRY_OBJECT, $entry->getId());
+ $activity->setUser($entry->getUser()->getAccount());
+ $this->em->persist($activity);
+ $this->em->flush();
+ }
+
+ public function taggedEntry(EntryTaggedEvent $event)
+ {
+ $entry = $event->getEntry();
+ $activity = new Activity($event->isRemove() ? Activity::ENTRY_REMOVE_TAG : Activity::ENTRY_ADD_TAG, Activity::ENTRY_OBJECT, $entry->getId());
+ $activity->setUser($entry->getUser()->getAccount());
+ $activity->setSecondaryObjectType(Activity::TAG_OBJECT)
+ ->setSecondaryObjectId($event->getTags()[0]->getId());
+ $this->em->persist($activity);
+ $this->em->flush();
+ }
+
+ public function annotationActivity(AnnotationEvent $event)
+ {
+ $annotation = $event->getAnnotation();
+
+ $activityType = 0;
+ if ($event instanceof AnnotationCreatedEvent) {
+ $activityType = Activity::ANNOTATION_ADD;
+ } elseif ($event instanceof AnnotationEditedEvent) {
+ $activityType = Activity::ANNOTATION_EDIT;
+ } elseif ($event instanceof AnnotationDeletedEvent) {
+ $activityType = Activity::ANNOTATION_REMOVE;
+ }
+
+ $activity = new Activity($activityType, Activity::ANNOTATION_OBJECT, $annotation->getId());
+ $activity->setUser($annotation->getUser()->getAccount());
+ $this->em->persist($activity);
+ $this->em->flush();
+ }
+
+ public function followedAccount(FollowEvent $event)
+ {
+ $activity = new Activity(Activity::FOLLOW_ACCOUNT, Activity::ACCOUNT_OBJECT, $event->getAccount()->getId());
+ $activity->setUser($event->getAccount());
+ $activity->setSecondaryObjectType(Activity::ACCOUNT_OBJECT)
+ ->setSecondaryObjectId($event->getFollower()->getId());
+ $this->em->persist($activity);
+ $this->em->flush();
+ }
+
+ public function unfollowedAccount(UnfollowEvent $event)
+ {
+ $activity = new Activity(Activity::UNFOLLOW_ACCOUNT, Activity::ACCOUNT_OBJECT, $event->getAccount()->getId());
+ $activity->setUser($event->getAccount());
+ $activity->setSecondaryObjectType(Activity::ACCOUNT_OBJECT)
+ ->setSecondaryObjectId($event->getFollower()->getId());
+ $this->em->persist($activity);
+ $this->em->flush();
+ }
+
+ public function recommendedEntry(RecommendedEntryEvent $event)
+ {
+ $entry = $event->getEntry();
+ $account = $entry->getUser()->getAccount();
+ $activity = new Activity(Activity::RECOMMEND_ENTRY, Activity::ACCOUNT_OBJECT, $account->getId());
+ $activity->setUser($account);
+ $activity->setSecondaryObjectType(Activity::ENTRY_OBJECT)
+ ->setSecondaryObjectId($entry->getId());
+ $this->em->persist($activity);
+ $this->em->flush();
+ }
+
+ public function shareActivity(ShareEvent $event)
+ {
+ $share = $event->getShare();
+
+ $activityType = 0;
+ if ($event instanceof ShareCreatedEvent) {
+ $activityType = Activity::USER_SHARE_CREATED;
+ } elseif ($event instanceof ShareAcceptedEvent) {
+ $activityType = Activity::USER_SHARE_ACCEPTED;
+ } elseif ($event instanceof ShareDeniedEvent) {
+ $activityType = Activity::USER_SHARE_REFUSED;
+ } elseif ($event instanceof ShareCancelledEvent) {
+ $activityType = Activity::USER_SHARE_CANCELLED;
+ }
+
+ $activity = new Activity($activityType, Activity::SHARE_OBJECT, $share->getId());
+ $activity->setUser($share->getUserOrigin());
+ $activity->setSecondaryObjectType(Activity::ACCOUNT_OBJECT)
+ ->setSecondaryObjectId($share->getUserDestination()->getId());
+ $this->em->persist($activity);
+ $this->em->flush();
+ }
+}
+++ /dev/null
-<?php
-
-namespace Wallabag\CoreBundle\Event;
-
-use Symfony\Component\EventDispatcher\Event;
-use Wallabag\CoreBundle\Entity\Entry;
-
-/**
- * This event is fired as soon as an entry is deleted.
- */
-class EntryDeletedEvent extends Event
-{
- const NAME = 'entry.deleted';
-
- protected $entry;
-
- public function __construct(Entry $entry)
- {
- $this->entry = $entry;
- }
-
- public function getEntry()
- {
- return $this->entry;
- }
-}
+++ /dev/null
-<?php
-
-namespace Wallabag\CoreBundle\Event;
-
-use Symfony\Component\EventDispatcher\Event;
-use Wallabag\CoreBundle\Entity\Entry;
-
-/**
- * This event is fired as soon as an entry was saved.
- */
-class EntrySavedEvent extends Event
-{
- const NAME = 'entry.saved';
-
- protected $entry;
-
- public function __construct(Entry $entry)
- {
- $this->entry = $entry;
- }
-
- public function getEntry()
- {
- return $this->entry;
- }
-}
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Psr\Log\LoggerInterface;
+use Wallabag\CoreBundle\Event\Activity\Actions\Entry\EntryDeletedEvent;
+use Wallabag\CoreBundle\Event\Activity\Actions\Entry\EntrySavedEvent;
use Wallabag\CoreBundle\Helper\DownloadImages;
use Wallabag\CoreBundle\Entity\Entry;
-use Wallabag\CoreBundle\Event\EntrySavedEvent;
-use Wallabag\CoreBundle\Event\EntryDeletedEvent;
use Doctrine\ORM\EntityManager;
class DownloadImagesSubscriber implements EventSubscriberInterface
--- /dev/null
+<?php
+
+namespace Wallabag\CoreBundle\Repository;
+
+use Doctrine\ORM\EntityRepository;
+
+class ChangeRepository extends EntityRepository
+{
+ /**
+ * Used only in test case to get a tag for our entry.
+ *
+ * @param int $timestamp
+ *
+ * @return Tag
+ */
+ public function findChangesSinceDate($timestamp)
+ {
+ $date = new \DateTime();
+ $date->setTimestamp($timestamp);
+
+ return $this->createQueryBuilder('c')
+ ->where('c.createdAt >= :timestamp')->setParameter('timestamp', $date)
+ ->getQuery()
+ ->getResult();
+ }
+}
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Query;
+use Doctrine\ORM\QueryBuilder;
use Pagerfanta\Adapter\DoctrineORMAdapter;
use Pagerfanta\Pagerfanta;
use Wallabag\CoreBundle\Entity\Tag;
*
* @param int $userId
* @param string $term
- * @param strint $currentRoute
+ * @param string $currentRoute
*
* @return QueryBuilder
*/
->getQuery()
->getResult();
}
+
+ /**
+ * @param $userId
+ * @return QueryBuilder
+ */
+ public function getBuilderForRecommendationsByUser($userId)
+ {
+ return $this->getBuilderByUser($userId)
+ ->andWhere('e.recommended = true')
+ ;
+ }
}
arguments:
- "%wallabag_core.site_credentials.encryption_key_path%"
- "@logger"
+
+ wallabag_core.activity.subscriber:
+ class: Wallabag\CoreBundle\Event\Activity\ActivitySubscriber
+ arguments:
+ - "@doctrine.orm.default_entity_manager"
+ - "@logger"
+ tags:
+ - { name: kernel.event_subscriber }
starred: 'Starred'
archive: 'Archive'
all_articles: 'All entries'
- config: 'Config'
+ config: 'config'
tags: 'Tags'
internal_settings: 'Internal Settings'
import: 'Import'
stats: Since %user_creation% you read %nb_archives% articles. That is about %per_day% a day!
config:
- page_title: 'Config'
+ page_title: 'config'
tab_menu:
settings: 'Settings'
rss: 'RSS'
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.
confirm: Are you really sure? (THIS CAN'T BE UNDONE)
button: Delete my account
+ form_account:
+ title: "Profile"
+ description_label: "Description de votre compte. Ce texte sera affiché sur votre page de profil."
+ avatar_label: "Avatar"
+ banner_label: "Bannière"
reset:
title: Reset area (a.k.a danger zone)
description: By hitting buttons below you'll have ability to remove some information from your account. Be aware that these actions are IRREVERSIBLE.
flashes:
config:
notice:
- config_saved: 'Config saved.'
+ config_saved: 'config saved.'
password_updated: 'Password updated'
password_not_updated_demo: "In demonstration mode, you can't change password for this user."
user_updated: 'Information updated'
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é."
confirm: "Vous êtes vraiment sûr ? (C’EST IRRÉVERSIBLE)"
button: "Supprimer mon compte"
+ form_account:
+ title: "Profil"
+ description_label: "Description de votre compte. Ce texte sera affiché sur votre page de profil."
+ avatar_label: "Avatar"
+ banner_label: "Bannière"
reset:
title: "Réinitialisation (attention danger !)"
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 !"
stats: 'Desde %user_creation% você leu %nb_archives% artigos. Isso é %per_day% por dia!'
config:
- page_title: 'Config'
+ page_title: 'config'
tab_menu:
settings: 'Configurações'
rss: 'RSS'
{{ form_widget(form.user._token) }}
</form>
+ <br /><hr /><br />
+ <h5>{{ 'config.form_account.title'|trans }}</h5>
+
+ {{ form_start(form.account) }}
+ {{ form_errors(form.account) }}
+
+ <div class="row">
+ <div class="input-field col s12">
+ {{ form_errors(form.account.description) }}
+ {{ form_widget(form.account.description, {'attr': {'class': 'materialize-textarea'}}) }}
+ {{ form_label(form.account.description) }}
+ </div>
+ </div>
+
+ <div class="row">
+ <div class="file-field input-field col s12">
+ {{ form_errors(form.account.avatar) }}
+ <div class="btn">
+ <span>{{ 'config.form_account.avatar_label'|trans }}</span>
+ {{ form_widget(form.account.avatar) }}
+ </div>
+ <div class="file-path-wrapper">
+ <input class="file-path validate" type="text">
+ </div>
+ </div>
+ </div>
+
+ <div class="row">
+ <div class="file-field input-field col s12">
+ {{ form_errors(form.account.banner) }}
+ <div class="btn">
+ <span>{{ 'config.form_account.banner_label'|trans }}</span>
+ {{ form_widget(form.account.banner) }}
+ </div>
+ <div class="file-path-wrapper">
+ <input class="file-path validate" type="text">
+ </div>
+ </div>
+ </div>
+
+ {{ form_widget(form.account.save, {'attr': {'class': 'btn waves-effect waves-light'}}) }}
+ <a href="{{ path('user-profile', {'user': app.user.account.username}) }}" class="btn">{{ 'config.form_account.view_profile' | trans }}</a>
+ {{ form_widget(form.account._token) }}
+ </form>
+
<br /><hr /><br />
<div class="row">
{% if unreadNotifs > 0 %}<span id="notifications-count" class="red-text text-accent-2">{{ unreadNotifs }}</span>{% endif %}
</a>
</li>
+ <li id="button_profile">
+ <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}) }}">
+ <i class="material-icons">person</i>
+ </a>
+ </li>
<li id="button_filters">
<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">
<i class="material-icons">filter_list</i>
--- /dev/null
+<?php
+
+namespace Wallabag\FederationBundle\Command;
+
+use Doctrine\ORM\NoResultException;
+use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Wallabag\FederationBundle\Entity\Account;
+use Wallabag\FederationBundle\Entity\Instance;
+use Wallabag\UserBundle\Entity\User;
+
+class CreateAccountsCommand extends ContainerAwareCommand
+{
+ /** @var OutputInterface */
+ protected $output;
+
+ protected $created = 0;
+
+ protected function configure()
+ {
+ $this
+ ->setName('wallabag:federation:create-accounts')
+ ->setDescription('Creates missing federation accounts')
+ ->setHelp('This command creates accounts for federation for missing users')
+ ->addArgument(
+ 'username',
+ InputArgument::OPTIONAL,
+ 'User to create an account for'
+ );
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ $this->output = $output;
+
+ $domainName = $this->getContainer()->getParameter('domain_name');
+ $instance = $this->checkInstance($domainName);
+
+ $username = $input->getArgument('username');
+
+ if ($username) {
+ try {
+ $user = $this->getUser($username);
+ $this->createAccount($user, $instance);
+ } catch (NoResultException $e) {
+ $output->writeln(sprintf('<error>User "%s" not found.</error>', $username));
+
+ return 1;
+ }
+ } else {
+ $users = $this->getDoctrine()->getRepository('WallabagUserBundle:User')->findAll();
+
+ $output->writeln(sprintf('Creating through %d user federated accounts', count($users)));
+
+ foreach ($users as $user) {
+ $output->writeln(sprintf('Processing user %s', $user->getUsername()));
+ $this->createAccount($user, $instance);
+ }
+ $output->writeln(sprintf('Creating user federated accounts. %d accounts created in total', $this->created));
+ }
+
+ return 0;
+ }
+
+ /**
+ * @param User $user
+ * @param $instance
+ */
+ private function createAccount(User $user, Instance $instance)
+ {
+ $em = $this->getContainer()->get('doctrine.orm.entity_manager');
+ $repo = $em->getRepository('WallabagFederationBundle:Account');
+
+ if ($repo->findBy(['user' => $user->getId()])) {
+ return;
+ }
+
+ $account = new Account();
+ $account->setUsername($user->getUsername())
+ ->setUser($user)
+ ->setServer($instance);
+
+ $em->persist($account);
+ $em->flush();
+
+ $user->setAccount($account);
+ $em->persist($account);
+ $em->flush();
+
+ ++$this->created;
+ }
+
+ private function checkInstance($domainName)
+ {
+ $em = $this->getContainer()->get('doctrine.orm.entity_manager');
+ $repo = $em->getRepository('WallabagFederationBundle:Instance');
+
+ $instance = $repo->findOneByDomain($domainName);
+ if (!$instance) {
+ $instance = new Instance($domainName);
+
+ $em->persist($instance);
+ $em->flush();
+ }
+ return $instance;
+ }
+
+ /**
+ * Fetches a user from its username.
+ *
+ * @param string $username
+ *
+ * @return \Wallabag\UserBundle\Entity\User
+ */
+ private function getUser($username)
+ {
+ return $this->getDoctrine()->getRepository('WallabagUserBundle:User')->findOneByUserName($username);
+ }
+
+ private function getDoctrine()
+ {
+ return $this->getContainer()->get('doctrine');
+ }
+}
--- /dev/null
+<?php
+
+namespace Wallabag\FederationBundle\Controller;
+
+use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
+use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
+use Symfony\Bundle\FrameworkBundle\Controller\Controller;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Wallabag\FederationBundle\Entity\Account;
+use Wallabag\FederationBundle\Entity\Instance;
+use Wallabag\FederationBundle\Federation\CloudId;
+
+class InboxController extends Controller
+{
+ /**
+ * @Route("/profile/inbox", name="user-inbox")
+ *
+ * @param Request $request
+ * @return Response
+ */
+ public function userInboxAction(Request $request)
+ {
+ $em = $this->getDoctrine()->getManager();
+ $response = new Response();
+
+ if ($activity = json_decode($request->getContent())) {
+ if ($activity->type === 'Follow' && isset($activity->actor->id)) {
+ $cloudId = new CloudId($activity->actor->id);
+ $account = new Account();
+ $account->setServer($cloudId->getRemote())
+ ->setUsername($cloudId->getUser());
+ $em->persist($account);
+ $em->flush();
+
+ $response->setStatusCode(201);
+ } else {
+ $response->setStatusCode(400);
+ }
+ }
+ return $response;
+ }
+}
--- /dev/null
+<?php
+
+namespace Wallabag\FederationBundle\Controller;
+
+use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
+use Symfony\Bundle\FrameworkBundle\Controller\Controller;
+use Wallabag\CoreBundle\Entity\Entry;
+
+class LikedController extends Controller
+{
+ /**
+ * @Route("/like/{entry}", name="like")
+ * @param Entry $entry
+ */
+ public function likeAction(Entry $entry)
+ {
+
+ }
+}
--- /dev/null
+<?php
+
+namespace Wallabag\FederationBundle\Controller;
+
+use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
+use Symfony\Bundle\FrameworkBundle\Controller\Controller;
+use Symfony\Component\HttpFoundation\JsonResponse;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Wallabag\FederationBundle\Federation\CloudId;
+
+class MetadataController extends Controller
+{
+ /**
+ * @Route("/.well-known/host-meta", name="webfinger-host-meta")
+ *
+ * @return Response
+ */
+ public function getHostMeta()
+ {
+ $response = new Response();
+ $response->setContent('<XRD xmlns=\"http://docs.oasis-open.org/ns/xri/xrd-1.0\">
+<Link rel=\"lrdd\" type=\"application/xrd+xml\" template=\"' . $this->generateUrl('webfinger') . '?resource={uri}\"/>
+</XRD>');
+ $response->headers->set('Content-Type', 'application/xrd+xml');
+ return $response;
+ }
+
+ /**
+ * @Route("/.well-known/webfinger", name="webfinger")
+ * @param Request $request
+ * @return JsonResponse
+ */
+ public function getWebfingerData(Request $request)
+ {
+ $subject = $request->query->get('resource');
+
+ if (!$subject || strlen($subject) < 12 || 0 !== strpos($subject, 'acct:') || false !== strpos($subject, '@')) {
+ return new JsonResponse([]);
+ }
+ $subjectId = substr($subject, 5);
+
+ $cloudId = $this->getCloudId($subjectId);
+
+
+ $data = [
+ 'subject' => $subject,
+ 'aliases' => [$this->generateUrl('profile', $cloudId->getUser())],
+ 'links' => [
+ [
+ 'rel' => 'http://webfinger.net/rel/profile-page',
+ 'type' => 'text/html',
+ 'href' => $this->generateUrl('profile', $cloudId->getUser())
+ ],
+ ]
+ ];
+ return new JsonResponse($data);
+ }
+
+ private function getCloudId($subjectId)
+ {
+ return new CloudId($subjectId);
+ }
+
+
+ #
+ # {"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}"}]}
+ #
+}
--- /dev/null
+<?php
+
+namespace Wallabag\FederationBundle\Controller;
+
+use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
+use Symfony\Bundle\FrameworkBundle\Controller\Controller;
+use Symfony\Component\HttpFoundation\Request;
+use Wallabag\FederationBundle\Entity\Account;
+use Wallabag\UserBundle\Entity\User;
+
+class OutboxController extends Controller
+{
+ /**
+ * @Route("/profile/outbox", name="user-outbox")
+ *
+ * @param Request $request
+ * @param Account $user
+ */
+ public function userOutboxAction(Request $request, Account $user)
+ {
+
+ }
+}
--- /dev/null
+<?php
+
+namespace Wallabag\FederationBundle\Controller;
+
+use Pagerfanta\Adapter\DoctrineORMAdapter;
+use Pagerfanta\Pagerfanta;
+use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
+use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
+use Symfony\Bundle\FrameworkBundle\Controller\Controller;
+use Symfony\Component\HttpFoundation\JsonResponse;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
+use Wallabag\CoreBundle\Entity\Entry;
+use Wallabag\FederationBundle\Entity\Account;
+use Wallabag\FederationBundle\Federation\CloudId;
+
+class ProfileController extends Controller
+{
+ /**
+ * @Route("/profile/@{user}", name="user-profile")
+ * @ParamConverter("user", class="WallabagFederationBundle:Account", options={
+ * "repository_method" = "findOneByUsername"})
+ *
+ * @param Request $request
+ * @param Account $user
+ * @return JsonResponse|Response
+ */
+ public function getUserProfile(Request $request, Account $user)
+ {
+ if (in_array('application/ld+json; profile="https://www.w3.org/ns/activitystreams', $request->getAcceptableContentTypes(), true)) {
+ $data = [
+ '@context' => 'https://www.w3.org/ns/activitystreams',
+ 'type' => 'Person',
+ 'id' => CloudId::getCloudIdFromAccount($user, $this->generateUrl('homepage', [], UrlGeneratorInterface::ABSOLUTE_URL))->getDisplayId(),
+ 'following' => $this->generateUrl('following', ['user' => $user->getUsername()], UrlGeneratorInterface::ABSOLUTE_URL),
+ 'followers' => $this->generateUrl('followers', ['user' => $user->getUsername()], UrlGeneratorInterface::ABSOLUTE_URL),
+ //'liked' => $this->generateUrl('recommended', ['user' => $user], UrlGeneratorInterface::ABSOLUTE_URL),
+ 'inbox' => $this->generateUrl('user-inbox', ['user' => $user], UrlGeneratorInterface::ABSOLUTE_URL),
+ 'outbox' => $this->generateUrl('user-outbox', ['user' => $user->getUsername()], UrlGeneratorInterface::ABSOLUTE_URL),
+ 'preferredUsername' => $user->getUser()->getName(),
+ 'name' => $user->getUsername(),
+ //'oauthAuthorizationEndpoint' => $this->generateUrl('fos_oauth_server_authorize', [], UrlGeneratorInterface::ABSOLUTE_URL),
+ 'oauthTokenEndpoint' => $this->generateUrl('fos_oauth_server_token', [], UrlGeneratorInterface::ABSOLUTE_URL),
+ //'publicInbox' => $this->generateUrl('public_inbox', [], UrlGeneratorInterface::ABSOLUTE_URL),
+ ];
+ return new JsonResponse($data);
+ }
+ return $this->render(
+ 'WallabagFederationBundle:User:profile.html.twig', [
+ 'user' => $user,
+ 'registration_enabled' => $this->getParameter('wallabag_user.registration_enabled'),
+ ]
+ );
+ }
+
+ /**
+ * @Route("/profile/@{user}/followings/{page}", name="following", defaults={"page" : 0})
+ * @ParamConverter("user", class="WallabagFederationBundle:Account", options={
+ * "repository_method" = "findOneByUsername"})
+ *
+ * @param Request $request
+ * @param Account $user
+ * @param int $page
+ * @return JsonResponse|Response
+ */
+ public function getUsersFollowing(Request $request, Account $user, $page = 0)
+ {
+ $qb = $this->getDoctrine()->getRepository('WallabagFederationBundle:Account')->getBuilderForFollowingsByAccount($user->getId());
+
+ $pagerAdapter = new DoctrineORMAdapter($qb->getQuery(), true, false);
+
+ $following = new Pagerfanta($pagerAdapter);
+ $totalFollowing = $following->getNbResults();
+
+ $activityStream = in_array('application/ld+json; profile="https://www.w3.org/ns/activitystreams', $request->getAcceptableContentTypes(), true);
+
+ if ($page === 0 && $activityStream) {
+ /** Home page */
+ $dataPrez = [
+ '@context' => 'https://www.w3.org/ns/activitystreams',
+ 'summary' => $user->getUsername() . " followings'",
+ 'type' => 'Collection',
+ 'id' => $this->generateUrl('following', ['user' => $user->getUsername()], UrlGeneratorInterface::ABSOLUTE_URL),
+ 'totalItems' => $totalFollowing,
+ 'first' => [
+ '@context' => 'https://www.w3.org/ns/activitystreams',
+ 'type' => 'Link',
+ 'href' => $this->generateUrl('following', ['user' => $user->getUsername(), 'page' => 1], UrlGeneratorInterface::ABSOLUTE_URL),
+ 'name' => 'First page of ' . $user->getUsername() . ' followings'
+ ],
+ 'last' => [
+ '@context' => 'https://www.w3.org/ns/activitystreams',
+ 'type' => 'Link',
+ 'href' => $this->generateUrl('following', ['user' => $user->getUsername(), 'page' => $following->getNbPages()], UrlGeneratorInterface::ABSOLUTE_URL),
+ 'name' => 'Last page of ' . $user->getUsername() . ' followings'
+ ]
+ ];
+ return new JsonResponse($dataPrez);
+ //}
+ }
+
+ $following->setMaxPerPage(30);
+ $following->setCurrentPage($page);
+
+ if (!$activityStream) {
+ return $this->render('WallabagFederationBundle:User:followers.html.twig', [
+ 'users' => $following,
+ 'user' => $user,
+ 'registration_enabled' => $this->getParameter('wallabag_user.registration_enabled'),
+ ]);
+ }
+
+ $items = [];
+
+ foreach ($following->getCurrentPageResults() as $follow) {
+ /** @var Account $follow */
+ /** Items in the page */
+ $items[] = [
+ '@context' => 'https://www.w3.org/ns/activitystreams',
+ 'type' => 'Person',
+ 'name' => $follow->getUsername(),
+ 'id' => CloudId::getCloudIdFromAccount($follow),
+ ];
+ }
+
+ $data = [
+ 'summary' => 'Page ' . $page . ' of ' . $user->getUsername() . ' followers',
+ 'partOf' => $this->generateUrl('following', ['user' => $user->getUsername()], UrlGeneratorInterface::ABSOLUTE_URL),
+ 'type' => 'OrderedCollectionPage',
+ 'startIndex' => ($page - 1) * 30,
+ 'orderedItems' => $items,
+ 'first' => [
+ '@context' => 'https://www.w3.org/ns/activitystreams',
+ 'type' => 'Link',
+ 'href' => $this->generateUrl('following', ['user' => $user->getUsername(), 'page' => 1], UrlGeneratorInterface::ABSOLUTE_URL),
+ 'name' => 'First page of ' . $user->getUsername() . ' followings'
+ ],
+ 'last' => [
+ '@context' => 'https://www.w3.org/ns/activitystreams',
+ 'type' => 'Link',
+ 'href' => $this->generateUrl('following', ['user' => $user->getUsername(), 'page' => $following->getNbPages()], UrlGeneratorInterface::ABSOLUTE_URL),
+ 'name' => 'Last page of ' . $user->getUsername() . ' followings'
+ ],
+ ];
+
+ /** Previous page */
+ if ($page > 1) {
+ $data['prev'] = [
+ '@context' => 'https://www.w3.org/ns/activitystreams',
+ 'type' => 'Link',
+ 'href' => $this->generateUrl('following', ['user' => $user->getUsername(), 'page' => $page - 1], UrlGeneratorInterface::ABSOLUTE_URL),
+ 'name' => 'Previous page of ' . $user->getUsername() . ' followings'
+ ];
+ }
+
+ /** Next page */
+ if ($page < $following->getNbPages()) {
+ $data['next'] = [
+ '@context' => 'https://www.w3.org/ns/activitystreams',
+ 'type' => 'Link',
+ 'href' => $this->generateUrl('following', ['user' => $user->getUsername(), 'page' => $page + 1], UrlGeneratorInterface::ABSOLUTE_URL),
+ 'name' => 'Next page of ' . $user->getUsername() . ' followings'
+ ];
+ }
+
+ return new JsonResponse($data);
+ }
+
+ /**
+ * @Route("/profile/@{user}/followers/{page}", name="followers", defaults={"page" : 0})
+ * @ParamConverter("user", class="WallabagFederationBundle:Account", options={
+ * "repository_method" = "findOneByUsername"})
+ *
+ * @param Request $request
+ * @param Account $user
+ * @return JsonResponse
+ */
+ public function getUsersFollowers(Request $request, Account $user, $page)
+ {
+ $qb = $this->getDoctrine()->getRepository('WallabagFederationBundle:Account')->getBuilderForFollowersByAccount($user->getId());
+
+ $pagerAdapter = new DoctrineORMAdapter($qb->getQuery(), true, false);
+
+ $followers = new Pagerfanta($pagerAdapter);
+ $totalFollowers = $followers->getNbResults();
+
+ $activityStream = in_array('application/ld+json; profile="https://www.w3.org/ns/activitystreams', $request->getAcceptableContentTypes(), true);
+
+ if ($page === 0 && $activityStream) {
+ /** Home page */
+ $dataPrez = [
+ '@context' => 'https://www.w3.org/ns/activitystreams',
+ 'summary' => $user->getUsername() . " followers'",
+ 'type' => 'Collection',
+ 'id' => $this->generateUrl('followers', ['user' => $user->getUsername()], UrlGeneratorInterface::ABSOLUTE_URL),
+ 'totalItems' => $totalFollowers,
+ 'first' => [
+ '@context' => 'https://www.w3.org/ns/activitystreams',
+ 'type' => 'Link',
+ 'href' => $this->generateUrl('followers', ['user' => $user->getUsername(), 'page' => 1], UrlGeneratorInterface::ABSOLUTE_URL),
+ 'name' => 'First page of ' . $user->getUsername() . ' followers'
+ ],
+ 'last' => [
+ '@context' => 'https://www.w3.org/ns/activitystreams',
+ 'type' => 'Link',
+ 'href' => $this->generateUrl('followers', ['user' => $user->getUsername(), 'page' => $followers->getNbPages()], UrlGeneratorInterface::ABSOLUTE_URL),
+ 'name' => 'Last page of ' . $user->getUsername() . ' followers'
+ ]
+ ];
+ return new JsonResponse($dataPrez);
+ }
+
+ $followers->setMaxPerPage(30);
+ if (!$activityStream && $page === 0) {
+ $followers->setCurrentPage(1);
+ } else {
+ $followers->setCurrentPage($page);
+ }
+
+ if (!$activityStream) {
+ return $this->render('WallabagFederationBundle:User:followers.html.twig', [
+ 'users' => $followers,
+ 'user' => $user,
+ 'registration_enabled' => $this->getParameter('wallabag_user.registration_enabled'),
+ ]);
+ }
+
+ $items = [];
+
+ foreach ($followers->getCurrentPageResults() as $follow) {
+ /** @var Account $follow */
+ /** Items in the page */
+ $items[] = [
+ '@context' => 'https://www.w3.org/ns/activitystreams',
+ 'type' => 'Person',
+ 'name' => $follow->getUsername(),
+ 'id' => CloudId::getCloudIdFromAccount($follow)->getDisplayId(),
+ ];
+ }
+ $data = [
+ 'summary' => 'Page ' . $page . ' of ' . $user->getUsername() . ' followers',
+ 'partOf' => $this->generateUrl('followers', ['user' => $user->getUsername()], UrlGeneratorInterface::ABSOLUTE_URL),
+ 'type' => 'OrderedCollectionPage',
+ 'startIndex' => ($page - 1) * 30,
+ 'orderedItems' => $items,
+ 'first' => [
+ '@context' => 'https://www.w3.org/ns/activitystreams',
+ 'type' => 'Link',
+ 'href' => $this->generateUrl('followers', ['user' => $user->getUsername(), 'page' => 1], UrlGeneratorInterface::ABSOLUTE_URL),
+ 'name' => 'First page of ' . $user->getUsername() . ' followers'
+ ],
+ 'last' => [
+ '@context' => 'https://www.w3.org/ns/activitystreams',
+ 'type' => 'Link',
+ 'href' => $this->generateUrl('followers', ['user' => $user->getUsername(), 'page' => $followers->getNbPages()], UrlGeneratorInterface::ABSOLUTE_URL),
+ 'name' => 'Last page of ' . $user->getUsername() . ' followers'
+ ],
+ ];
+
+ /** Previous page */
+ if ($page > 1) {
+ $data['prev'] = [
+ '@context' => 'https://www.w3.org/ns/activitystreams',
+ 'type' => 'Link',
+ 'href' => $this->generateUrl('followers', ['user' => $user->getUsername(), 'page' => $page - 1], UrlGeneratorInterface::ABSOLUTE_URL),
+ 'name' => 'Previous page of ' . $user->getUsername() . ' followers'
+ ];
+ }
+
+ /** Next page */
+ if ($page < $followers->getNbPages()) {
+ $data['next'] = [
+ '@context' => 'https://www.w3.org/ns/activitystreams',
+ 'type' => 'Link',
+ 'href' => $this->generateUrl('followers', ['user' => $user->getUsername(), 'page' => $page + 1], UrlGeneratorInterface::ABSOLUTE_URL),
+ 'name' => 'Next page of ' . $user->getUsername() . ' followers'
+ ];
+ }
+
+ return new JsonResponse($data);
+ }
+
+ /**
+ * @Route("/profile/@{userToFollow}/follow", name="follow-user")
+ * @ParamConverter("userToFollow", class="WallabagFederationBundle:Account", options={
+ * "repository_method" = "findOneByUsername"})
+ * @param Account $userToFollow
+ */
+ public function followAccountAction(Account $userToFollow)
+ {
+ // if we're on our own instance
+ if ($this->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_FULLY')) {
+
+ /** @var Account $userAccount */
+ $userAccount = $this->getUser()->getAccount();
+
+ if ($userToFollow === $userAccount) {
+ $this->createAccessDeniedException("You can't follow yourself");
+ }
+
+ $em = $this->getDoctrine()->getManager();
+
+ $userAccount->addFollowing($userToFollow);
+ $userToFollow->addFollower($userAccount);
+
+ $em->persist($userAccount);
+ $em->persist($userToFollow);
+
+ $em->flush();
+ } else {
+ // ask cloud id and redirect to instance
+ }
+ }
+
+ /**
+ * @Route("/profile/@{user}/recommendations", name="user-recommendations", defaults={"page" : 0})
+ * @ParamConverter("user", class="WallabagFederationBundle:Account", options={
+ * "repository_method" = "findOneByUsername"})
+ *
+ * @param Request $request
+ * @param Account $user
+ * @param int $page
+ * @return JsonResponse|Response
+ */
+ public function getUsersRecommendationsAction(Request $request, Account $user, $page = 0)
+ {
+ $qb = $this->getDoctrine()->getRepository('WallabagCoreBundle:Entry')->getBuilderForRecommendationsByUser($user->getUser()->getId());
+
+ $pagerAdapter = new DoctrineORMAdapter($qb->getQuery(), true, false);
+
+ $recommendations = new Pagerfanta($pagerAdapter);
+ $totalRecommendations = $recommendations->getNbResults();
+
+ $activityStream = in_array('application/ld+json; profile="https://www.w3.org/ns/activitystreams', $request->getAcceptableContentTypes(), true);
+
+ if ($page === 0 && $activityStream) {
+ $dataPrez = [
+ '@context' => 'https://www.w3.org/ns/activitystreams',
+ 'summary' => $user->getUsername() . " recommendations'",
+ 'type' => 'Collection',
+ 'id' => $this->generateUrl('user-recommendations', ['user' => $user->getUsername()], UrlGeneratorInterface::ABSOLUTE_URL),
+ 'totalItems' => $totalRecommendations,
+ 'first' => [
+ '@context' => 'https://www.w3.org/ns/activitystreams',
+ 'type' => 'Link',
+ 'href' => $this->generateUrl('user-recommendations', ['user' => $user->getUsername(), 'page' => 1], UrlGeneratorInterface::ABSOLUTE_URL),
+ 'name' => 'First page of ' . $user->getUsername() . ' followers'
+ ],
+ 'last' => [
+ '@context' => 'https://www.w3.org/ns/activitystreams',
+ 'type' => 'Link',
+ 'href' => $this->generateUrl('user-recommendations', ['user' => $user->getUsername(), 'page' => $recommendations->getNbPages()], UrlGeneratorInterface::ABSOLUTE_URL),
+ 'name' => 'Last page of ' . $user->getUsername() . ' followers'
+ ]
+ ];
+ return new JsonResponse($dataPrez);
+ }
+
+ $recommendations->setMaxPerPage(30);
+ if (!$activityStream && $page === 0) {
+ $recommendations->setCurrentPage(1);
+ } else {
+ $recommendations->setCurrentPage($page);
+ }
+
+ if (!$activityStream) {
+ return $this->render('WallabagFederationBundle:User:recommendations.html.twig', [
+ 'recommendations' => $recommendations,
+ 'registration_enabled' => $this->getParameter('wallabag_user.registration_enabled'),
+ ]);
+ }
+
+ $items = [];
+
+ foreach ($recommendations->getCurrentPageResults() as $recommendation) {
+ $items[] = [
+ '@context' => 'https://www.w3.org/ns/activitystreams',
+ 'type' => 'Person',
+ 'name' => $recommendation->getTitle(),
+ 'id' => $recommendation->getUrl(),
+ ];
+ }
+ $data = [
+ 'summary' => 'Page ' . $page . ' of ' . $user->getUsername() . ' recommendations',
+ 'partOf' => $this->generateUrl('user-recommendations', ['user' => $user->getUsername()], UrlGeneratorInterface::ABSOLUTE_URL),
+ 'type' => 'OrderedCollectionPage',
+ 'startIndex' => ($page - 1) * 30,
+ 'orderedItems' => $items,
+ 'first' => [
+ '@context' => 'https://www.w3.org/ns/activitystreams',
+ 'type' => 'Link',
+ 'href' => $this->generateUrl('user-recommendations', ['user' => $user->getUsername(), 'page' => 1], UrlGeneratorInterface::ABSOLUTE_URL),
+ 'name' => 'First page of ' . $user->getUsername() . ' recommendations'
+ ],
+ 'last' => [
+ '@context' => 'https://www.w3.org/ns/activitystreams',
+ 'type' => 'Link',
+ 'href' => $this->generateUrl('user-recommendations', ['user' => $user->getUsername(), 'page' => $recommendations->getNbPages()], UrlGeneratorInterface::ABSOLUTE_URL),
+ 'name' => 'Last page of ' . $user->getUsername() . ' recommendations'
+ ],
+ ];
+
+ if ($page > 1) {
+ $data['prev'] = [
+ '@context' => 'https://www.w3.org/ns/activitystreams',
+ 'type' => 'Link',
+ 'href' => $this->generateUrl('user-recommendations', ['user' => $user->getUsername(), 'page' => $page - 1], UrlGeneratorInterface::ABSOLUTE_URL),
+ 'name' => 'Previous page of ' . $user->getUsername() . ' recommendations'
+ ];
+ }
+
+ if ($page < $recommendations->getNbPages()) {
+ $data['next'] = [
+ '@context' => 'https://www.w3.org/ns/activitystreams',
+ 'type' => 'Link',
+ 'href' => $this->generateUrl('user-recommendations', ['user' => $user->getUsername(), 'page' => $page + 1], UrlGeneratorInterface::ABSOLUTE_URL),
+ 'name' => 'Next page of ' . $user->getUsername() . ' recommendations'
+ ];
+ }
+
+ return new JsonResponse($data);
+ }
+
+}
--- /dev/null
+<?php
+
+namespace Wallabag\FederationBundle\Controller;
+
+use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
+use Symfony\Bundle\FrameworkBundle\Controller\Controller;
+use Wallabag\CoreBundle\Entity\Entry;
+use Wallabag\CoreBundle\Event\Activity\Actions\Federation\RecommendedEntryEvent;
+use Wallabag\FederationBundle\Entity\Account;
+
+class RecommandController extends Controller
+{
+ /**
+ * @Route("/recommend/{entry}", name="recommend-entry")
+ *
+ * @param Entry $entry
+ */
+ public function postRecommendAction(Entry $entry)
+ {
+ if ($entry->getUser() !== $this->getUser()) {
+ $this->createAccessDeniedException("You can't recommend entries which are not your own");
+ }
+ $em = $this->getDoctrine()->getManager();
+
+ $entry->setRecommended(true);
+
+ $em->persist($entry);
+ $em->flush();
+
+ $this->get('event_dispatcher')->dispatch(RecommendedEntryEvent::NAME, new RecommendedEntryEvent($entry));
+
+ $this->redirectToRoute('view', ['id' => $entry->getId()]);
+ }
+}
--- /dev/null
+<?php
+
+namespace Wallabag\FederationBundle\DependencyInjection;
+
+use Symfony\Component\Config\Definition\Builder\TreeBuilder;
+use Symfony\Component\Config\Definition\ConfigurationInterface;
+
+class Configuration implements ConfigurationInterface
+{
+ public function getConfigTreeBuilder()
+ {
+ $treeBuilder = new TreeBuilder();
+ $rootNode = $treeBuilder->root('wallabag_user');
+
+ $rootNode
+ ->children()
+ ->booleanNode('registration_enabled')
+ ->defaultValue(true)
+ ->end()
+ ->end()
+ ;
+
+ return $treeBuilder;
+ }
+}
--- /dev/null
+<?php
+
+namespace Wallabag\FederationBundle\DependencyInjection;
+
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\Config\FileLocator;
+use Symfony\Component\HttpKernel\DependencyInjection\Extension;
+use Symfony\Component\DependencyInjection\Loader;
+
+class WallabagFederationExtension extends Extension
+{
+ public function load(array $configs, ContainerBuilder $container)
+ {
+ $configuration = new Configuration();
+ $config = $this->processConfiguration($configuration, $configs);
+
+ $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
+ $loader->load('services.yml');
+ $container->setParameter('wallabag_user.registration_enabled', $config['registration_enabled']);
+ }
+
+ public function getAlias()
+ {
+ return 'wallabag_federation';
+ }
+}
--- /dev/null
+<?php
+
+namespace Wallabag\FederationBundle\Entity;
+
+use Doctrine\Common\Collections\ArrayCollection;
+use Doctrine\Common\Collections\Collection;
+use Doctrine\ORM\Mapping as ORM;
+use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
+use Symfony\Component\Validator\Constraints as Assert;
+use Wallabag\CoreBundle\Entity\Entry;
+use Wallabag\UserBundle\Entity\User;
+
+/**
+ * Account.
+ *
+ * @ORM\Entity(repositoryClass="Wallabag\FederationBundle\Repository\AccountRepository")
+ * @UniqueEntity(fields={"username", "server"}).
+ * @ORM\Table(name="`account`")
+ */
+class Account
+{
+ /**
+ * @var int
+ *
+ * @ORM\Column(name="id", type="integer")
+ * @ORM\Id
+ * @ORM\GeneratedValue(strategy="AUTO")
+ *
+ */
+ private $id;
+
+ /**
+ * @var string
+ *
+ * @ORM\Column(name="username", type="string")
+ */
+ private $username;
+
+ /**
+ * @var Instance
+ *
+ * @ORM\ManyToOne(targetEntity="Wallabag\FederationBundle\Entity\Instance", inversedBy="users")
+ */
+ private $server;
+
+ /**
+ * @var User
+ *
+ * @ORM\OneToOne(targetEntity="Wallabag\UserBundle\Entity\User", inversedBy="account")
+ * @ORM\JoinColumn(nullable=true)
+ */
+ private $user;
+
+ /**
+ * @var ArrayCollection
+ *
+ * @ORM\ManyToMany(targetEntity="Wallabag\FederationBundle\Entity\Account", mappedBy="following")
+ */
+ private $followers;
+
+ /**
+ * @var ArrayCollection
+ *
+ * @ORM\ManyToMany(targetEntity="Wallabag\FederationBundle\Entity\Account", inversedBy="followers")
+ * @ORM\JoinTable(name="follow",
+ * joinColumns={@ORM\JoinColumn(name="account_id", referencedColumnName="id")},
+ * inverseJoinColumns={@ORM\JoinColumn(name="follow_account_id", referencedColumnName="id")}
+ * )
+ */
+ private $following;
+
+ /**
+ * @var string
+ *
+ * @ORM\Column(type="string", nullable=true)
+ *
+ * @Assert\File(mimeTypes={ "image/gif", "image/jpeg", "image/svg+xml", "image/webp", "image/png" })
+ */
+ private $avatar;
+
+ /**
+ * @var string
+ *
+ * @ORM\Column(type="string", nullable=true)
+ *
+ * @Assert\File(mimeTypes={ "image/gif", "image/jpeg", "image/svg+xml", "image/webp", "image/png" })
+ */
+ private $banner;
+
+ /**
+ * @var string
+ *
+ * @ORM\Column(type="text", nullable=true)
+ */
+ private $description;
+
+ /**
+ * Account constructor.
+ */
+ public function __construct()
+ {
+ $this->followers = new ArrayCollection();
+ $this->following = new ArrayCollection();
+ $this->liked = new ArrayCollection();
+ }
+
+ /**
+ * @return int
+ */
+ public function getId()
+ {
+ return $this->id;
+ }
+
+ /**
+ * @return string
+ */
+ public function getUsername()
+ {
+ return $this->username;
+ }
+
+ /**
+ * @param string $username
+ * @return Account
+ */
+ public function setUsername($username)
+ {
+ $this->username = $username;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getServer()
+ {
+ return $this->server;
+ }
+
+ /**
+ * @param string $server
+ * @return Account
+ */
+ public function setServer($server)
+ {
+ $this->server = $server;
+ return $this;
+ }
+
+ /**
+ * @return User
+ */
+ public function getUser()
+ {
+ return $this->user;
+ }
+
+ /**
+ * @param User $user
+ * @return Account
+ */
+ public function setUser(User $user)
+ {
+ $this->user = $user;
+ return $this;
+ }
+
+ /**
+ * @return Collection
+ */
+ public function getFollowers()
+ {
+ return $this->followers;
+ }
+
+ /**
+ * @param Collection $followers
+ * @return Account
+ */
+ public function setFollowers($followers)
+ {
+ $this->followers = $followers;
+ return $this;
+ }
+
+ /**
+ * @param Account $account
+ * @return Account
+ */
+ public function addFollower(Account $account)
+ {
+ $this->followers->add($account);
+ return $this;
+ }
+
+ /**
+ * @return Collection
+ */
+ public function getFollowing()
+ {
+ return $this->following;
+ }
+
+ /**
+ * @param Collection $following
+ * @return Account
+ */
+ public function setFollowing(Collection $following)
+ {
+ $this->following = $following;
+ return $this;
+ }
+
+ /**
+ * @param Account $account
+ * @return Account
+ */
+ public function addFollowing(Account $account)
+ {
+ $this->following->add($account);
+ return $this;
+ }
+
+ /**
+ * @return Collection
+ */
+ public function getLiked()
+ {
+ return $this->liked;
+ }
+
+ /**
+ * @param Collection $liked
+ * @return Account
+ */
+ public function setLiked(Collection $liked)
+ {
+ $this->liked = $liked;
+ return $this;
+ }
+
+ /**
+ * @param Entry $entry
+ * @return Account
+ */
+ public function addLiked(Entry $entry)
+ {
+ $this->liked->add($entry);
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getAvatar()
+ {
+ return $this->avatar;
+ }
+
+ /**
+ * @param string $avatar
+ * @return Account
+ */
+ public function setAvatar($avatar)
+ {
+ $this->avatar = $avatar;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getBanner()
+ {
+ return $this->banner;
+ }
+
+ /**
+ * @param string $banner
+ * @return Account
+ */
+ public function setBanner($banner)
+ {
+ $this->banner = $banner;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getDescription()
+ {
+ return $this->description;
+ }
+
+ /**
+ * @param string $description
+ * @return Account
+ */
+ public function setDescription($description)
+ {
+ $this->description = $description;
+ return $this;
+ }
+
+}
--- /dev/null
+<?php
+
+namespace Wallabag\FederationBundle\Entity;
+
+use Doctrine\ORM\Mapping as ORM;
+use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
+
+/**
+ * Account.
+ *
+ * @ORM\Entity
+ * @UniqueEntity(fields={"domain"}).
+ * @ORM\Table(name="`instance`")
+ */
+class Instance {
+ /**
+ * @var int
+ *
+ * @ORM\Column(name="id", type="integer")
+ * @ORM\Id
+ * @ORM\GeneratedValue(strategy="AUTO")
+ *
+ */
+ private $id;
+
+ /**
+ * @var string
+ *
+ * @ORM\Column(name="domain", type="string")
+ */
+ private $domain;
+
+ /**
+ * @var float
+ *
+ * @ORM\Column(name="score", type="float")
+ */
+ private $score = 0;
+
+ /**
+ * @var array
+ *
+ * @ORM\OneToMany(targetEntity="Wallabag\FederationBundle\Entity\Account", mappedBy="server")
+ */
+ private $users;
+
+ /**
+ * Instance constructor.
+ * @param string $domain
+ */
+ public function __construct($domain)
+ {
+ $this->domain = $domain;
+ }
+
+ /**
+ * @return int
+ */
+ public function getId()
+ {
+ return $this->id;
+ }
+
+ /**
+ * @return string
+ */
+ public function getDomain()
+ {
+ return $this->domain;
+ }
+
+ /**
+ * @param string $domain
+ */
+ public function setDomain($domain)
+ {
+ $this->domain = $domain;
+ }
+
+ /**
+ * @return float
+ */
+ public function getScore()
+ {
+ return $this->score;
+ }
+
+ /**
+ * @param float $score
+ */
+ public function setScore($score)
+ {
+ $this->score = $score;
+ }
+
+ /**
+ * @return array
+ */
+ public function getUsers()
+ {
+ return $this->users;
+ }
+
+ /**
+ * @param array $users
+ */
+ public function setUsers($users)
+ {
+ $this->users = $users;
+ }
+}
--- /dev/null
+<?php
+
+namespace Wallabag\FederationBundle\EventListener;
+
+use Doctrine\ORM\EntityManager;
+use FOS\UserBundle\Event\UserEvent;
+use FOS\UserBundle\FOSUserEvents;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Wallabag\CoreBundle\Entity\Config;
+use Wallabag\FederationBundle\Entity\Account;
+
+/**
+ * This listener will create the associated configuration when a user register.
+ * This configuration will be created right after the registration (no matter if it needs an email validation).
+ */
+class CreateAccountListener implements EventSubscriberInterface
+{
+ private $em;
+ private $domainName;
+
+ public function __construct(EntityManager $em, $domainName)
+ {
+ $this->em = $em;
+ $this->domainName = $domainName;
+ }
+
+ public static function getSubscribedEvents()
+ {
+ return [
+ // when a user register using the normal form
+ FOSUserEvents::REGISTRATION_COMPLETED => 'createAccount',
+ // when we manually create a user using the command line
+ // OR when we create it from the config UI
+ FOSUserEvents::USER_CREATED => 'createAccount',
+ ];
+ }
+
+ public function createAccount(UserEvent $event)
+ {
+ $user = $event->getUser();
+ $account = new Account();
+ $account->setUser($user)
+ ->setUsername($user->getUsername())
+ ->setServer($this->domainName);
+
+ $this->em->persist($account);
+
+ $user->setAccount($account);
+
+ $this->em->persist($user);
+ $this->em->flush();
+ }
+}
--- /dev/null
+<?php
+
+namespace Wallabag\FederationBundle\Federation;
+
+use Wallabag\FederationBundle\Entity\Account;
+
+class CloudId {
+
+ /** @var string */
+ private $id;
+
+ /** @var string */
+ private $user;
+
+ /** @var string */
+ private $remote;
+
+ /**
+ * CloudId constructor.
+ *
+ * @param string $id
+ */
+ public function __construct($id) {
+ $this->id = $id;
+
+ $atPos = strpos($id, '@');
+ $user = substr($id, 0, $atPos);
+ $remote = substr($id, $atPos + 1);
+ if (!empty($user) && !empty($remote)) {
+ $this->user = $user;
+ $this->remote = $remote;
+ }
+ }
+
+ /**
+ * The full remote cloud id
+ *
+ * @return string
+ */
+ public function getId() {
+ return $this->id;
+ }
+
+ public function getDisplayId() {
+ return str_replace('https://', '', str_replace('http://', '', $this->getId()));
+ }
+
+ /**
+ * The username on the remote server
+ *
+ * @return string
+ */
+ public function getUser() {
+ return $this->user;
+ }
+
+ /**
+ * The base address of the remote server
+ *
+ * @return string
+ */
+ public function getRemote() {
+ return $this->remote;
+ }
+
+ /**
+ * @param Account $account
+ * @param string $domain
+ * @return CloudId
+ */
+ public static function getCloudIdFromAccount(Account $account, $domain = '')
+ {
+ if ($account->getServer() !== null) {
+ return new self($account->getUsername() . '@' . $account->getServer());
+ }
+ return new self($account->getUsername() . '@' . $domain);
+ }
+}
--- /dev/null
+<?php
+
+namespace Wallabag\FederationBundle\Form\Type;
+
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\Extension\Core\Type\FileType;
+use Symfony\Component\Form\Extension\Core\Type\SubmitType;
+use Symfony\Component\Form\Extension\Core\Type\TextareaType;
+use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+use Wallabag\FederationBundle\Entity\Account;
+
+class AccountType extends AbstractType
+{
+ public function buildForm(FormBuilderInterface $builder, array $options)
+ {
+ $builder
+ ->add('description', TextareaType::class, ['label' => 'config.form_account.description_label'])
+ ->add('avatar', FileType::class, [
+ 'label' => 'config.form_account.avatar_label',
+ 'required' => false,
+ 'data_class' => null,
+ ])
+ ->add('banner', FileType::class, [
+ 'label' => 'config.form_account.banner_label',
+ 'required' => false,
+ 'data_class' => null,
+ ])
+ ->add('save', SubmitType::class, [
+ 'label' => 'config.form.save',
+ ])
+ ;
+ }
+
+ public function configureOptions(OptionsResolver $resolver)
+ {
+ $resolver->setDefaults(array(
+ 'data_class' => Account::class,
+ ));
+ }
+
+ public function getBlockPrefix()
+ {
+ return 'update_account';
+ }
+}
--- /dev/null
+<?php
+
+namespace Wallabag\FederationBundle\Repository;
+
+use Doctrine\ORM\EntityRepository;
+use Doctrine\ORM\QueryBuilder;
+
+class AccountRepository extends EntityRepository
+{
+ /**
+ * @param $accountId
+ * @return QueryBuilder
+ */
+ public function getBuilderForFollowingsByAccount($accountId)
+ {
+ return $this->createQueryBuilder('a')
+ ->select('f.id, f.username')
+ ->innerJoin('a.following', 'f')
+ ->where('a.id = :accountId')->setParameter('accountId', $accountId)
+ ;
+ }
+
+ /**
+ * @param $accountId
+ * @return QueryBuilder
+ */
+ public function getBuilderForFollowersByAccount($accountId)
+ {
+ return $this->createQueryBuilder('a')
+ ->innerJoin('a.followers', 'f')
+ ->where('a.id = :accountId')->setParameter('accountId', $accountId)
+ ;
+ }
+
+ /**
+ * @param $username
+ * @return QueryBuilder
+ * @throws \Doctrine\ORM\NonUniqueResultException
+ */
+ public function findAccountByUsername($username)
+ {
+ return $this->createQueryBuilder('a')
+ ->where('a.username = :username')->setParameter('username', $username)
+ ->andWhere('a.server = null')
+ ->getQuery()
+ ->getOneOrNullResult();
+ }
+}
--- /dev/null
+<?php
+
+namespace Wallabag\FederationBundle\Repository;
+
+use Doctrine\ORM\EntityRepository;
+
+class InstanceRepository extends EntityRepository
+{
+
+}
--- /dev/null
+services:
+ wallabag_federation.listener.create_accounts:
+ class: Wallabag\FederationBundle\EventListener\CreateAccountListener
+ arguments:
+ - "@doctrine.orm.entity_manager"
+ - "%domain_name%"
+ tags:
+ - { name: kernel.event_subscriber }
--- /dev/null
+{% extends "WallabagCoreBundle::base.html.twig" %}
+
+{% block css %}
+ {{ parent() }}
+ {% if not app.debug %}
+ <link rel="stylesheet" href="{{ asset('bundles/wallabagcore/material.css') }}">
+ {% endif %}
+{% endblock %}
+
+{% block scripts %}
+ {{ parent() }}
+ <script src="{{ asset('bundles/wallabagcore/material' ~ (app.debug ? '.dev' : '') ~ '.js') }}"></script>
+{% endblock %}
+
+{% block header %}
+{% endblock %}
+
+{% block body_class %}reset-left{% endblock %}
+
+{% block messages %}
+ {% for flashMessage in app.session.flashbag.get('notice') %}
+ <script>
+ Materialize.toast('{{ flashMessage|trans }}', 4000);
+ </script>
+ {% endfor %}
+{% endblock %}
+
+{% block menu %}
+ <nav class="cyan darken-1">
+ <div class="nav-wrapper nav-panels">
+ <a href="#" data-activates="slide-out" class="nav-panel-menu button-collapse"><i class="material-icons">menu</i></a>
+ <div class="left action">
+ <a href="{{ path('homepage') }}">wallabag</a>
+ </div>
+ <ul class="input-field nav-panel-buttom">
+ {% if not is_granted('IS_AUTHENTICATED_FULLY') %}
+ <li>
+ <a href="{{ path('fos_user_security_login') }}">Login</a>
+ </li>
+ {% if registration_enabled %}
+ <li>
+ <a href="{{ path('fos_user_registration_register') }}">Register</a>
+ </li>
+ {% endif %}
+ {% else %}
+ <li>
+ <a href="{{ path('fos_user_security_logout') }}">Logout</a>
+ </li>
+ {% endif %}
+ </ul>
+ </div>
+ </nav>
+
+{% endblock %}
+
+{% block content %}
+<div class="container">
+ <div class="row">
+ <div class="col offset-s2 s8">
+ {{ include('@WallabagFederation/themes/material/User/profile_header.html.twig') }}
+ <ul class="collection">
+ {% for account in users %}
+ <li class="collection-item avatar">
+ <i class="material-icons circle">folder</i>
+ <span class="title">{{ account.username }}@{{ account.server }}</span>
+ <p>First Line</p>
+ <a href="#!" class="secondary-content"><i class="material-icons">grade</i></a>
+ </li>
+ {% endfor %}
+ </ul>
+ </div>
+ </div>
+</div>
+{% endblock %}
+
+{% block footer %}
+ <footer class="page-footer cyan darken-2">
+ <div class="footer-copyright">
+ <div class="container">
+ <div class="row right">
+ <p>
+ {{ 'footer.wallabag.powered_by'|trans }} <a target="_blank" href="https://wallabag.org" class="grey-text text-lighten-4">wallabag</a>
+ </p>
+ </div>
+ </div>
+ </div>
+ </footer>
+{% endblock %}
--- /dev/null
+{% extends "WallabagCoreBundle::base.html.twig" %}
+
+{% block css %}
+ {{ parent() }}
+ {% if not app.debug %}
+ <link rel="stylesheet" href="{{ asset('bundles/wallabagcore/material.css') }}">
+ {% endif %}
+{% endblock %}
+
+{% block scripts %}
+ {{ parent() }}
+ <script src="{{ asset('bundles/wallabagcore/material' ~ (app.debug ? '.dev' : '') ~ '.js') }}"></script>
+{% endblock %}
+
+{% block header %}
+{% endblock %}
+
+{% block body_class %}reset-left{% endblock %}
+
+{% block messages %}
+ {% for flashMessage in app.session.flashbag.get('notice') %}
+ <script>
+ Materialize.toast('{{ flashMessage|trans }}', 4000);
+ </script>
+ {% endfor %}
+{% endblock %}
+
+{% block menu %}
+<nav class="cyan darken-1">
+ <div class="nav-wrapper nav-panels">
+ <a href="#" data-activates="slide-out" class="nav-panel-menu button-collapse"><i class="material-icons">menu</i></a>
+ <div class="left action">
+ <a href="{{ path('homepage') }}">wallabag</a>
+ </div>
+ <ul class="input-field nav-panel-buttom">
+ {% if not is_granted('IS_AUTHENTICATED_FULLY') %}
+ <li>
+ <a href="{{ path('fos_user_security_login') }}">Login</a>
+ </li>
+ {% if registration_enabled %}
+ <li>
+ <a href="{{ path('fos_user_registration_register') }}">Register</a>
+ </li>
+ {% endif %}
+ {% else %}
+ <li>
+ <a href="{{ path('fos_user_security_logout') }}">Logout</a>
+ </li>
+ {% endif %}
+ </ul>
+ </div>
+</nav>
+
+{% endblock %}
+
+{% block content %}
+ <div class="container">
+ <div class="row">
+ <div class="col offset-l2 l8">
+ {{ include('@WallabagFederation/themes/material/User/profile_header.html.twig') }}
+ </div>
+ </div>
+{% endblock %}
+
+{% block footer %}
+ <footer class="page-footer cyan darken-2">
+ <div class="footer-copyright">
+ <div class="container">
+ <div class="row right">
+ <p>
+ {{ 'footer.wallabag.powered_by'|trans }} <a target="_blank" href="https://wallabag.org" class="grey-text text-lighten-4">wallabag</a>
+ </p>
+ </div>
+ </div>
+ </div>
+ </footer>
+{% endblock %}
--- /dev/null
+{% set usershow = user.username %}
+{% if user.user.name is not null %}
+ {% set usershow = user.user.name %}
+{% endif %}
+
+<p>
+ {{ 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.
+</p>
+<div class="card grey lighten-5 z-depth-1 profile">
+ <div class="card-content">
+ <img src="{{ asset('uploads/media/avatar/') ~ user.avatar }}" alt="" class="circle responsive-img center-block avatar">
+ <h3 class="center-align">{{ usershow }}</h3>
+ <h6 class="center-align">@{{ user.username }}</h6>
+ <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>
+ <div class="details">
+ <div class="bio">
+ <p>{{ user.description }}</p>
+ </div>
+ <div class="details-counters">
+ <div class="counter">
+ <a href="{{ path('followers', {'user': user.username}) }}">
+ <span class="counter-label">Followers</span>
+ <span class="counter-number">{{ user.followers | length }}</span>
+ </a>
+ </div>
+ <div class="counter">
+ <a href="{{ path('following', {'user': user.username}) }}">
+ <span class="counter-label">Following</span>
+ <span class="counter-number">{{ user.following | length }}</span>
+ </a>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
--- /dev/null
+{{ dump(recommendations) }}
+{% for entry in recommendations %}
+{{ include('@WallabagCore/themes/material/Entry/_card_list.html.twig') }}
+{% endfor %}
--- /dev/null
+<?php
+
+namespace Wallabag\FederationBundle;
+
+use Symfony\Component\HttpKernel\Bundle\Bundle;
+
+class WallabagFederationBundle extends Bundle
+{
+}
use Wallabag\ApiBundle\Entity\Client;
use Wallabag\CoreBundle\Entity\Config;
use Wallabag\CoreBundle\Entity\Entry;
+use Wallabag\FederationBundle\Entity\Account;
/**
* User.
*/
protected $default_client;
+ /**
+ * @ORM\OneToOne(targetEntity="Wallabag\FederationBundle\Entity\Account", mappedBy="user", cascade={"remove"})
+ */
+ protected $account;
+
+ /**
+ * User constructor.
+ */
public function __construct()
{
parent::__construct();
{
$this->notifications = $notifications;
}
+
+ /**
+ * @return Account
+ */
+ public function getAccount()
+ {
+ return $this->account;
+ }
+
+ /**
+ * @param mixed $account
+ * @return User
+ */
+ public function setAccount(Account $account)
+ {
+ $this->account = $account;
+ return $this;
+ }
}
$this->assertContains('Checking system requirements.', $tester->getDisplay());
$this->assertContains('Setting up database.', $tester->getDisplay());
$this->assertContains('Administration setup.', $tester->getDisplay());
- $this->assertContains('Config setup.', $tester->getDisplay());
+ $this->assertContains('config setup.', $tester->getDisplay());
$this->assertContains('Run migrations.', $tester->getDisplay());
}
$this->assertContains('Setting up database.', $tester->getDisplay());
$this->assertContains('Dropping database, creating database and schema, clearing the cache', $tester->getDisplay());
$this->assertContains('Administration setup.', $tester->getDisplay());
- $this->assertContains('Config setup.', $tester->getDisplay());
+ $this->assertContains('config setup.', $tester->getDisplay());
$this->assertContains('Run migrations.', $tester->getDisplay());
// we force to reset everything
$this->assertContains('Checking system requirements.', $tester->getDisplay());
$this->assertContains('Setting up database.', $tester->getDisplay());
$this->assertContains('Administration setup.', $tester->getDisplay());
- $this->assertContains('Config setup.', $tester->getDisplay());
+ $this->assertContains('config setup.', $tester->getDisplay());
$this->assertContains('Run migrations.', $tester->getDisplay());
// the current database doesn't already exist
$this->assertContains('Checking system requirements.', $tester->getDisplay());
$this->assertContains('Setting up database.', $tester->getDisplay());
$this->assertContains('Administration setup.', $tester->getDisplay());
- $this->assertContains('Config setup.', $tester->getDisplay());
+ $this->assertContains('config setup.', $tester->getDisplay());
$this->assertContains('Run migrations.', $tester->getDisplay());
$this->assertContains('Dropping schema and creating schema', $tester->getDisplay());
$this->assertContains('Checking system requirements.', $tester->getDisplay());
$this->assertContains('Setting up database.', $tester->getDisplay());
$this->assertContains('Administration setup.', $tester->getDisplay());
- $this->assertContains('Config setup.', $tester->getDisplay());
+ $this->assertContains('config setup.', $tester->getDisplay());
$this->assertContains('Run migrations.', $tester->getDisplay());
$this->assertContains('Creating schema', $tester->getDisplay());
$this->assertContains('Checking system requirements.', $tester->getDisplay());
$this->assertContains('Setting up database.', $tester->getDisplay());
$this->assertContains('Administration setup.', $tester->getDisplay());
- $this->assertContains('Config setup.', $tester->getDisplay());
+ $this->assertContains('config setup.', $tester->getDisplay());
$this->assertContains('Run migrations.', $tester->getDisplay());
}
}