├── .env.dist ├── .env.test ├── .gitignore ├── .symfony.insight.yaml ├── LICENSE ├── README.md ├── assets ├── _routes.js ├── app.js └── styles │ └── app.css ├── bin ├── console └── phpunit ├── composer.json ├── composer.lock ├── config ├── bundles.php ├── packages │ ├── asset_mapper.yaml │ ├── cache.yaml │ ├── csrf.yaml │ ├── debug.yaml │ ├── doctrine.yaml │ ├── doctrine_migrations.yaml │ ├── framework.yaml │ ├── mailer.yaml │ ├── messenger.yaml │ ├── monolog.yaml │ ├── notifier.yaml │ ├── routing.yaml │ ├── security.yaml │ ├── translation.yaml │ ├── twig.yaml │ ├── validator.yaml │ └── web_profiler.yaml ├── preload.php ├── routes.yaml ├── routes │ ├── framework.yaml │ ├── security.yaml │ └── web_profiler.yaml ├── services.yaml └── validator │ └── validation.yaml ├── docker-compose.override.yml ├── docker-compose.yml ├── images ├── buffer.svg ├── tick-mask.svg └── tick.svg ├── importmap.php ├── migrations ├── Version20201115082734.php ├── Version20230402040938.php ├── Version20230410131624.php ├── Version20230624052414.php ├── Version20230624062646.php ├── Version20250104062315.php └── Version20250104074617.php ├── phpstan.neon ├── phpunit.xml.dist ├── public ├── app │ ├── images │ │ ├── icon-192x192.png │ │ ├── icon-512x512.png │ │ └── screenshots │ │ │ ├── androidphone-item-dark.png │ │ │ ├── androidphone-item-light.png │ │ │ ├── ipad-feeds-search.png │ │ │ ├── ipad-homescreen.jpg │ │ │ ├── ipad-item-dark.png │ │ │ ├── ipad-item-light.png │ │ │ └── ipad-item-share.png │ ├── translations │ │ ├── en.json │ │ └── fr.json │ └── views │ │ ├── author.html │ │ ├── category.html │ │ ├── feed.html │ │ ├── item.html │ │ ├── member.html │ │ └── misc.html ├── favicon.ico ├── index.php ├── robots.txt ├── serviceworker.js └── site.webmanifest ├── src ├── Command │ ├── CollectionCommand.php │ ├── ElasticsearchCheckCommand.php │ ├── ElasticsearchCommand.php │ ├── ElasticsearchCreateCommand.php │ ├── ElasticsearchRemoveCommand.php │ ├── ElasticsearchStatusCommand.php │ ├── GenerateVapidCommand.php │ ├── MaxmindCommand.php │ ├── MemberCreateCommand.php │ ├── PhpunitCommand.php │ ├── PushCommand.php │ └── SetupCommand.php ├── Controller │ ├── AbstractAppController.php │ ├── AuthorController.php │ ├── CategoryController.php │ ├── ConnectionController.php │ ├── EnclosureController.php │ ├── FeedController.php │ ├── IndexController.php │ ├── ItemController.php │ ├── LoginController.php │ ├── LogoutController.php │ ├── MemberController.php │ ├── MemberPasskeyController.php │ ├── PinboardController.php │ ├── ProfileController.php │ ├── ProxyController.php │ ├── PushController.php │ └── SearchController.php ├── DataFixtures │ └── feed.sql ├── Entity │ ├── Action.php │ ├── ActionAuthor.php │ ├── ActionCategory.php │ ├── ActionFeed.php │ ├── ActionItem.php │ ├── ActionTrait.php │ ├── Author.php │ ├── Category.php │ ├── Collection.php │ ├── CollectionFeed.php │ ├── Connection.php │ ├── DateCreatedTrait.php │ ├── Enclosure.php │ ├── ExtraFieldsTrait.php │ ├── Feed.php │ ├── FeedCategory.php │ ├── IdTrait.php │ ├── Item.php │ ├── ItemCategory.php │ ├── Member.php │ └── MemberPasskey.php ├── Event │ ├── ActionAuthorEvent.php │ ├── ActionCategoryEvent.php │ ├── ActionEvent.php │ ├── ActionFeedEvent.php │ ├── ActionItemEvent.php │ ├── AuthorEvent.php │ ├── CategoryEvent.php │ ├── CollectionEvent.php │ ├── CollectionFeedEvent.php │ ├── ConnectionEvent.php │ ├── EnclosureEvent.php │ ├── FeedCategoryEvent.php │ ├── FeedEvent.php │ ├── ItemCategoryEvent.php │ ├── ItemEvent.php │ └── MemberEvent.php ├── EventSubscriber │ ├── PinboardSubscriber.php │ ├── SearchSubscriber.php │ └── SubscribeSubscriber.php ├── Form │ └── Type │ │ ├── AuthorType.php │ │ ├── CategoryType.php │ │ ├── FeedType.php │ │ ├── ImportOpmlType.php │ │ ├── LoginType.php │ │ ├── MemberPasskeyType.php │ │ ├── MemberType.php │ │ ├── ProfileType.php │ │ └── PushType.php ├── Helper │ ├── CleanHelper.php │ ├── DeviceDetectorHelper.php │ ├── ExtraFieldsHelper.php │ ├── JwtHelper.php │ └── MaxmindHelper.php ├── Kernel.php ├── Manager │ ├── AbstractManager.php │ ├── ActionAuthorManager.php │ ├── ActionCategoryManager.php │ ├── ActionFeedManager.php │ ├── ActionItemManager.php │ ├── ActionManager.php │ ├── AuthorManager.php │ ├── CategoryManager.php │ ├── CollectionFeedManager.php │ ├── CollectionManager.php │ ├── ConnectionManager.php │ ├── EnclosureManager.php │ ├── FeedCategoryManager.php │ ├── FeedManager.php │ ├── ItemCategoryManager.php │ ├── ItemManager.php │ ├── MemberManager.php │ ├── MemberPasskeyManager.php │ ├── PushManager.php │ └── SearchManager.php ├── Model │ ├── DeviceDetectorModel.php │ ├── ImportOpmlModel.php │ ├── JwtPayloadModel.php │ ├── LoginModel.php │ ├── ProfileModel.php │ ├── QueryParameterFilterModel.php │ ├── QueryParameterPageModel.php │ └── QueryParameterSortModel.php ├── Repository │ ├── AbstractRepository.php │ ├── ActionAuthorRepository.php │ ├── ActionCategoryRepository.php │ ├── ActionFeedRepository.php │ ├── ActionItemRepository.php │ ├── ActionRepository.php │ ├── AuthorRepository.php │ ├── CategoryRepository.php │ ├── CollectionFeedRepository.php │ ├── CollectionRepository.php │ ├── ConnectionRepository.php │ ├── EnclosureRepository.php │ ├── FeedCategoryRepository.php │ ├── FeedRepository.php │ ├── ItemCategoryRepository.php │ ├── ItemRepository.php │ ├── MemberPasskeyRepository.php │ └── MemberRepository.php └── Security │ ├── ApiAccessDeniedHandler.php │ ├── ApiAccessTokenHandler.php │ ├── SecurityMemberProvider.php │ └── Voter │ ├── AbstractVoter.php │ ├── AuthorVoter.php │ ├── CategoryVoter.php │ ├── ConnectionVoter.php │ ├── EnclosureVoter.php │ ├── FeedVoter.php │ ├── ItemVoter.php │ ├── MemberPasskeyVoter.php │ └── MemberVoter.php ├── symfony.lock ├── templates └── base.html.twig ├── tests ├── ControllerAsAnonymous │ ├── AuthorControllerTest.php │ ├── CategoryControllerTest.php │ ├── ConnectionControllerTest.php │ ├── EnclosureControllerTest.php │ ├── FeedControllerTest.php │ ├── ItemControllerTest.php │ ├── MemberPasskeyControllerTest.php │ └── ProfileControllerTest.php ├── ControllerAsConnected │ ├── AbstractControllerTest.php │ ├── AuthorControllerTest.php │ ├── CategoryControllerTest.php │ ├── EnclosureControllerTest.php │ ├── FeedControllerTest.php │ └── ItemControllerTest.php ├── Entity │ ├── ActionTest.php │ ├── AuthorTest.php │ ├── CategoryTest.php │ ├── FeedTest.php │ └── ItemTest.php ├── Manager │ ├── ActionAuthorManagerTest.php │ ├── ActionCategoryManagerTest.php │ ├── ActionFeedManagerTest.php │ ├── ActionItemManagerTest.php │ ├── ActionManagerTest.php │ ├── AuthorManagerTest.php │ ├── CategoryManagerTest.php │ ├── CollectionFeedManagerTest.php │ ├── CollectionManagerTest.php │ ├── ConnectionManagerTest.php │ ├── EnclosureManagerTest.php │ ├── FeedCategoryManagerTest.php │ ├── FeedManagerTest.php │ ├── ItemCategoryManagerTest.php │ ├── ItemManagerTest.php │ ├── MemberManagerTest.php │ └── MemberPasskeyManagerTest.php └── bootstrap.php ├── translations └── .gitignore └── update.sh /.env.dist: -------------------------------------------------------------------------------- 1 | # In all environments, the following files are loaded if they exist, 2 | # the latter taking precedence over the former: 3 | # 4 | # * .env contains default values for the environment variables needed by the app 5 | # * .env.local uncommitted file with local overrides 6 | # * .env.$APP_ENV committed environment-specific defaults 7 | # * .env.$APP_ENV.local uncommitted environment-specific overrides 8 | # 9 | # Real environment variables win over .env files. 10 | # 11 | # DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES. 12 | # https://symfony.com/doc/current/configuration/secrets.html 13 | # 14 | # Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2). 15 | # https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration 16 | 17 | ###> symfony/framework-bundle ### 18 | APP_ENV=prod 19 | APP_SECRET= 20 | ###< symfony/framework-bundle ### 21 | 22 | ###> doctrine/doctrine-bundle ### 23 | # Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url 24 | # IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml 25 | # 26 | DATABASE_URL="mysql://your-user:your-password@127.0.0.1:3306/app?serverVersion=8.0.36&charset=utf8mb4" 27 | ###< doctrine/doctrine-bundle ### 28 | 29 | ###> symfony/messenger ### 30 | MESSENGER_TRANSPORT_DSN=sync:// 31 | ###< symfony/messenger ### 32 | 33 | ###> symfony/mailer ### 34 | MAILER_DSN=null://null 35 | ###< symfony/mailer ### 36 | 37 | ###> app ### 38 | VAPID_PUBLIC_KEY= 39 | VAPID_PRIVATE_KEY= 40 | MAXMIND_ENABLED=false 41 | MAXMIND_ACCOUNT_ID= 42 | MAXMIND_LICENSE_KEY= 43 | ELASTICSEARCH_ENABLED=false 44 | ELASTICSEARCH_INDEX=feed 45 | ELASTICSEARCH_URL=http://127.0.0.1:9200 46 | ELASTICSEARCH_USERNAME= 47 | ELASTICSEARCH_PASSWORD= 48 | ELASTICSEARCH_API_KEY= 49 | SSL_VERIFY_PEER=true 50 | SSL_VERIFY_HOST=true 51 | LDAP_ENABLED=false 52 | LDAP_SERVER='ldap://127.0.0.1' 53 | LDAP_PORT=389 54 | LDAP_PROTOCOL=3 55 | LDAP_ROOT_DN='cn=Manager,dc=my-domain,dc=com' 56 | LDAP_ROOT_PW='secret' 57 | LDAP_BASE_DN='dc=my-domain,dc=com' 58 | LDAP_SEARCH_USER='mail=[email]' 59 | LDAP_SEARCH_GROUP_ADMIN='cn=admingroup' 60 | ###< app ### 61 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | # define your env variables for the test env here 2 | KERNEL_CLASS='App\Kernel' 3 | APP_SECRET='$ecretf0rt3st' 4 | SYMFONY_DEPRECATIONS_HELPER=999999 5 | PANTHER_APP_ENV=panther 6 | PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots 7 | 8 | DATABASE_URL="mysql://your-user:your-password@your-host:3306/app?serverVersion=8&charset=utf8mb4" 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | ###> symfony/framework-bundle ### 3 | /.env.local 4 | /.env.local.php 5 | /.env.*.local 6 | /config/secrets/prod/prod.decrypt.private.php 7 | /public/bundles/ 8 | /var/ 9 | /vendor/ 10 | ###< symfony/framework-bundle ### 11 | 12 | ###> phpunit/phpunit ### 13 | /phpunit.xml 14 | .phpunit.result.cache 15 | ###< phpunit/phpunit ### 16 | 17 | ###> symfony/phpunit-bridge ### 18 | .phpunit.result.cache 19 | /phpunit.xml 20 | ###< symfony/phpunit-bridge ### 21 | 22 | ###> app ### 23 | .php-cs-fixer.cache 24 | /config/jwt-keys/application.key 25 | /config/jwt-keys/application.pub 26 | /maxmind 27 | /public/.htaccess 28 | ###< app ### 29 | 30 | ###> symfony/asset-mapper ### 31 | /public/assets/ 32 | /assets/vendor/ 33 | ###< symfony/asset-mapper ### 34 | -------------------------------------------------------------------------------- /.symfony.insight.yaml: -------------------------------------------------------------------------------- 1 | php_version: 8.4 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-present Stéphane Dion 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /assets/styles/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | overscroll-behavior-y: none; 3 | } 4 | main { 5 | height: 100%; 6 | overflow-x: hidden; 7 | overflow-y: auto; 8 | overscroll-behavior: contain; 9 | position: relative; 10 | } 11 | .favicon, a.favicon:link, a.favicon:visited { 12 | background-position: center left; 13 | background-repeat: no-repeat; 14 | background-size: 13px 13px; 15 | padding-left: 16px; 16 | } 17 | .mdl-card[dir="rtl"] .favicon, .mdl-card[dir="rtl"] a.favicon:link, .mdl-card[dir="rtl"] a.favicon:visited { 18 | background-position: center right; 19 | padding-left: 0px; 20 | padding-right: 16px; 21 | } 22 | h1 .favicon, h1.favicon, h1 a.favicon:link, h1 a.favicon:visited { 23 | background-size: 16px 16px; 24 | padding-left: 19px; 25 | } 26 | .mdl-card[dir="rtl"] h1 .favicon, .mdl-card[dir="rtl"] h1.favicon, .mdl-card[dir="rtl"] h1 a.favicon:link, .mdl-card[dir="rtl"] h1 a.favicon:visited { 27 | padding-left: 0px; 28 | padding-right: 19px; 29 | } 30 | 31 | img, iframe, video { 32 | max-width: 100%; 33 | } 34 | figure { 35 | display: block; 36 | -webkit-margin-start: 0px; 37 | -webkit-margin-end: 0px; 38 | } 39 | textarea { 40 | min-height: 250px; 41 | } 42 | 43 | body.anonymous .connected { 44 | display: none; 45 | } 46 | body.connected .anonymous { 47 | display: none; 48 | } 49 | 50 | body.online .offline { 51 | display: none; 52 | } 53 | body.offline .online { 54 | display: none; 55 | } 56 | 57 | body.not_administrator .administrator { 58 | display: none; 59 | } 60 | body.administrator .not_administrator { 61 | display: none; 62 | } 63 | 64 | body.no_entries .action-down, body.no_entries .action-up, body.no_entries .action-unread { 65 | display: none; 66 | } 67 | 68 | body.collapse .mdl-card__supporting-text { 69 | display: none; 70 | } 71 | 72 | body .mdl-card.collapse .mdl-card__supporting-text { 73 | display: none; 74 | } 75 | 76 | body .mdl-card.expand .mdl-card__supporting-text { 77 | display: block; 78 | } 79 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | = 80000) { 10 | require dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit'; 11 | } else { 12 | define('PHPUNIT_COMPOSER_INSTALL', dirname(__DIR__).'/vendor/autoload.php'); 13 | require PHPUNIT_COMPOSER_INSTALL; 14 | PHPUnit\TextUI\Command::main(); 15 | } 16 | } else { 17 | if (!is_file(dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php')) { 18 | echo "Unable to find the `simple-phpunit.php` script in `vendor/symfony/phpunit-bridge/bin/`.\n"; 19 | exit(1); 20 | } 21 | 22 | require dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php'; 23 | } 24 | -------------------------------------------------------------------------------- /config/bundles.php: -------------------------------------------------------------------------------- 1 | ['all' => true], 5 | Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], 6 | Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], 7 | Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true], 8 | Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], 9 | Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], 10 | Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true], 11 | Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], 12 | Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], 13 | Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], 14 | Knp\Bundle\PaginatorBundle\KnpPaginatorBundle::class => ['all' => true], 15 | ]; 16 | -------------------------------------------------------------------------------- /config/packages/asset_mapper.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | asset_mapper: 3 | # The paths to make available to the asset mapper. 4 | paths: 5 | - assets/ 6 | missing_import_mode: strict 7 | 8 | when@prod: 9 | framework: 10 | asset_mapper: 11 | missing_import_mode: warn 12 | -------------------------------------------------------------------------------- /config/packages/cache.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | cache: 3 | # Unique name of your app: used to compute stable namespaces for cache keys. 4 | #prefix_seed: your_vendor_name/app_name 5 | 6 | # The "app" cache stores to the filesystem by default. 7 | # The data in this cache should persist between deploys. 8 | # Other options include: 9 | 10 | # Redis 11 | #app: cache.adapter.redis 12 | #default_redis_provider: redis://localhost 13 | 14 | # APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues) 15 | #app: cache.adapter.apcu 16 | 17 | # Namespaced pools use the above "app" backend by default 18 | #pools: 19 | #my.dedicated.cache: null 20 | -------------------------------------------------------------------------------- /config/packages/csrf.yaml: -------------------------------------------------------------------------------- 1 | # Enable stateless CSRF protection for forms and logins/logouts 2 | framework: 3 | form: 4 | csrf_protection: 5 | token_id: submit 6 | 7 | csrf_protection: 8 | stateless_token_ids: 9 | - submit 10 | - authenticate 11 | - logout 12 | -------------------------------------------------------------------------------- /config/packages/debug.yaml: -------------------------------------------------------------------------------- 1 | when@dev: 2 | debug: 3 | # Forwards VarDumper Data clones to a centralized server allowing to inspect dumps on CLI or in your browser. 4 | # See the "server:dump" command to start a new server. 5 | dump_destination: "tcp://%env(VAR_DUMPER_SERVER)%" 6 | -------------------------------------------------------------------------------- /config/packages/doctrine.yaml: -------------------------------------------------------------------------------- 1 | doctrine: 2 | dbal: 3 | url: '%env(resolve:DATABASE_URL)%' 4 | 5 | # IMPORTANT: You MUST configure your server version, 6 | # either here or in the DATABASE_URL env var (see .env file) 7 | #server_version: '16' 8 | 9 | profiling_collect_backtrace: '%kernel.debug%' 10 | use_savepoints: true 11 | orm: 12 | auto_generate_proxy_classes: true 13 | enable_lazy_ghost_objects: true 14 | report_fields_where_declared: true 15 | validate_xml_mapping: true 16 | naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware 17 | identity_generation_preferences: 18 | Doctrine\DBAL\Platforms\PostgreSQLPlatform: identity 19 | auto_mapping: true 20 | mappings: 21 | App: 22 | type: attribute 23 | is_bundle: false 24 | dir: '%kernel.project_dir%/src/Entity' 25 | prefix: 'App\Entity' 26 | alias: App 27 | controller_resolver: 28 | auto_mapping: false 29 | 30 | when@test: 31 | doctrine: 32 | dbal: 33 | # "TEST_TOKEN" is typically set by ParaTest 34 | #dbname_suffix: '_test%env(default::TEST_TOKEN)%' 35 | 36 | when@prod: 37 | doctrine: 38 | orm: 39 | auto_generate_proxy_classes: false 40 | proxy_dir: '%kernel.build_dir%/doctrine/orm/Proxies' 41 | query_cache_driver: 42 | type: pool 43 | pool: doctrine.system_cache_pool 44 | result_cache_driver: 45 | type: pool 46 | pool: doctrine.result_cache_pool 47 | 48 | framework: 49 | cache: 50 | pools: 51 | doctrine.result_cache_pool: 52 | adapter: cache.app 53 | doctrine.system_cache_pool: 54 | adapter: cache.system 55 | -------------------------------------------------------------------------------- /config/packages/doctrine_migrations.yaml: -------------------------------------------------------------------------------- 1 | doctrine_migrations: 2 | migrations_paths: 3 | # namespace is arbitrary but should be different from App\Migrations 4 | # as migrations classes should NOT be autoloaded 5 | 'DoctrineMigrations': '%kernel.project_dir%/migrations' 6 | enable_profiler: false 7 | -------------------------------------------------------------------------------- /config/packages/framework.yaml: -------------------------------------------------------------------------------- 1 | # see https://symfony.com/doc/current/reference/configuration/framework.html 2 | framework: 3 | secret: '%env(APP_SECRET)%' 4 | 5 | # Enables session support. Note that the session will ONLY be started if you read or write from it. 6 | # Remove or comment this section to explicitly disable session support. 7 | session: 8 | enabled: true 9 | handler_id: null 10 | cookie_secure: auto 11 | cookie_samesite: lax 12 | 13 | #esi: true 14 | #fragments: true 15 | 16 | when@test: 17 | framework: 18 | test: true 19 | session: 20 | storage_factory_id: session.storage.factory.mock_file 21 | -------------------------------------------------------------------------------- /config/packages/mailer.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | mailer: 3 | dsn: '%env(MAILER_DSN)%' 4 | -------------------------------------------------------------------------------- /config/packages/messenger.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | messenger: 3 | failure_transport: failed 4 | 5 | transports: 6 | # https://symfony.com/doc/current/messenger.html#transport-configuration 7 | async: 8 | dsn: '%env(MESSENGER_TRANSPORT_DSN)%' 9 | options: 10 | use_notify: true 11 | check_delayed_interval: 60000 12 | retry_strategy: 13 | max_retries: 3 14 | multiplier: 2 15 | failed: 'doctrine://default?queue_name=failed' 16 | # sync: 'sync://' 17 | 18 | default_bus: messenger.bus.default 19 | 20 | buses: 21 | messenger.bus.default: [] 22 | 23 | routing: 24 | Symfony\Component\Mailer\Messenger\SendEmailMessage: async 25 | Symfony\Component\Notifier\Message\ChatMessage: async 26 | Symfony\Component\Notifier\Message\SmsMessage: async 27 | 28 | # Route your messages to the transports 29 | # 'App\Message\YourMessage': async 30 | -------------------------------------------------------------------------------- /config/packages/monolog.yaml: -------------------------------------------------------------------------------- 1 | monolog: 2 | channels: 3 | - deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists 4 | 5 | handlers: 6 | main: 7 | action_level: 'warning' 8 | buffer_size: 50 9 | excluded_http_codes: [400, 401, 403, 404, 405] 10 | handler: 'nested' 11 | type: 'fingers_crossed' 12 | 13 | nested: 14 | channels: ['!event'] 15 | level: 'info' 16 | max_files: 10 17 | path: '%kernel.logs_dir%/%kernel.environment%.log' 18 | type: 'rotating_file' 19 | 20 | when@dev: 21 | monolog: 22 | handlers: 23 | deprecation: 24 | channels: ['deprecation'] 25 | level: 'info' 26 | max_files: 10 27 | path: '%kernel.logs_dir%/%kernel.environment%-deprecation.log' 28 | type: 'rotating_file' 29 | -------------------------------------------------------------------------------- /config/packages/notifier.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | notifier: 3 | chatter_transports: 4 | texter_transports: 5 | channel_policy: 6 | # use chat/slack, chat/telegram, sms/twilio or sms/nexmo 7 | urgent: ['email'] 8 | high: ['email'] 9 | medium: ['email'] 10 | low: ['email'] 11 | admin_recipients: 12 | - { email: admin@example.com } 13 | -------------------------------------------------------------------------------- /config/packages/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | # Configure how to generate URLs in non-HTTP contexts, such as CLI commands. 4 | # See https://symfony.com/doc/current/routing.html#generating-urls-in-commands 5 | #default_uri: http://localhost 6 | 7 | when@prod: 8 | framework: 9 | router: 10 | strict_requirements: null 11 | -------------------------------------------------------------------------------- /config/packages/security.yaml: -------------------------------------------------------------------------------- 1 | security: 2 | # https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords 3 | password_hashers: 4 | Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto' 5 | # https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider 6 | providers: 7 | app_member_provider: 8 | id: App\Security\SecurityMemberProvider 9 | 10 | firewalls: 11 | dev: 12 | pattern: ^/(_(profiler|wdt)|css|images|js)/ 13 | security: false 14 | 15 | api: 16 | pattern: ^/api 17 | context: api 18 | lazy: true 19 | provider: app_member_provider 20 | stateless: true 21 | access_denied_handler: App\Security\ApiAccessDeniedHandler 22 | access_token: 23 | token_handler: App\Security\ApiAccessTokenHandler 24 | 25 | main: 26 | lazy: true 27 | provider: app_member_provider 28 | stateless: false 29 | 30 | # activate different ways to authenticate 31 | # https://symfony.com/doc/current/security.html#the-firewall 32 | 33 | # https://symfony.com/doc/current/security/impersonating_user.html 34 | # switch_user: true 35 | 36 | # Easy way to control access for large sections of your site 37 | # Note: Only the *first* access control that matches will be used 38 | access_control: 39 | # - { path: ^/admin, roles: ROLE_ADMIN } 40 | # - { path: ^/profile, roles: ROLE_USER } 41 | - { path: ^/api, roles: ROLE_USER } 42 | 43 | when@test: 44 | security: 45 | password_hashers: 46 | # By default, password hashers are resource intensive and take time. This is 47 | # important to generate secure password hashes. In tests however, secure hashes 48 | # are not important, waste resources and increase test times. The following 49 | # reduces the work factor to the lowest possible values. 50 | Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 51 | algorithm: auto 52 | cost: 4 # Lowest possible value for bcrypt 53 | time_cost: 3 # Lowest possible value for argon 54 | memory_cost: 10 # Lowest possible value for argon 55 | -------------------------------------------------------------------------------- /config/packages/translation.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | default_locale: en 3 | translator: 4 | default_path: '%kernel.project_dir%/translations' 5 | fallbacks: 6 | - en 7 | providers: 8 | -------------------------------------------------------------------------------- /config/packages/twig.yaml: -------------------------------------------------------------------------------- 1 | twig: 2 | default_path: '%kernel.project_dir%/templates' 3 | globals: 4 | vapidPublicKey: '%env(VAPID_PUBLIC_KEY)%' 5 | file_name_pattern: '*.twig' 6 | 7 | when@test: 8 | twig: 9 | strict_variables: true 10 | -------------------------------------------------------------------------------- /config/packages/validator.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | validation: 3 | # Enables validator auto-mapping support. 4 | # For instance, basic validation constraints will be inferred from Doctrine's metadata. 5 | #auto_mapping: 6 | # App\Entity\: [] 7 | 8 | when@test: 9 | framework: 10 | validation: 11 | not_compromised_password: false 12 | -------------------------------------------------------------------------------- /config/packages/web_profiler.yaml: -------------------------------------------------------------------------------- 1 | when@dev: 2 | web_profiler: 3 | toolbar: true 4 | intercept_redirects: false 5 | 6 | framework: 7 | profiler: 8 | only_exceptions: false 9 | collect_serializer_data: true 10 | 11 | when@test: 12 | web_profiler: 13 | toolbar: false 14 | intercept_redirects: false 15 | 16 | framework: 17 | profiler: { collect: false } 18 | -------------------------------------------------------------------------------- /config/preload.php: -------------------------------------------------------------------------------- 1 | doctrine/doctrine-bundle ### 5 | database: 6 | ports: 7 | - "5432" 8 | ###< doctrine/doctrine-bundle ### 9 | 10 | ###> symfony/mailer ### 11 | mailer: 12 | image: schickling/mailcatcher 13 | ports: ["1025", "1080"] 14 | ###< symfony/mailer ### 15 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | ###> doctrine/doctrine-bundle ### 5 | database: 6 | image: postgres:${POSTGRES_VERSION:-15}-alpine 7 | environment: 8 | POSTGRES_DB: ${POSTGRES_DB:-app} 9 | # You should definitely change the password in production 10 | POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-!ChangeMe!} 11 | POSTGRES_USER: ${POSTGRES_USER:-app} 12 | volumes: 13 | - database_data:/var/lib/postgresql/data:rw 14 | # You may use a bind-mounted host directory instead, so that it is harder to accidentally remove the volume and lose all your data! 15 | # - ./docker/db/data:/var/lib/postgresql/data:rw 16 | ###< doctrine/doctrine-bundle ### 17 | 18 | volumes: 19 | ###> doctrine/doctrine-bundle ### 20 | database_data: 21 | ###< doctrine/doctrine-bundle ### 22 | -------------------------------------------------------------------------------- /images/buffer.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/tick-mask.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/tick.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /importmap.php: -------------------------------------------------------------------------------- 1 | [ 16 | 'path' => './assets/app.js', 17 | 'entrypoint' => true, 18 | ], 19 | 'bootstrap-icons/font/bootstrap-icons.min.css' => [ 20 | 'version' => '1.11.3', 21 | 'type' => 'css', 22 | ], 23 | 'bootstrap' => [ 24 | 'version' => '5.3.3', 25 | ], 26 | '@popperjs/core' => [ 27 | 'version' => '2.11.8', 28 | ], 29 | 'bootstrap/dist/css/bootstrap.min.css' => [ 30 | 'version' => '5.3.3', 31 | 'type' => 'css', 32 | ], 33 | 'moment-timezone' => [ 34 | 'version' => '0.5.47', 35 | ], 36 | 'moment' => [ 37 | 'version' => '2.30.1', 38 | ], 39 | 'jquery.scrollto' => [ 40 | 'version' => '2.1.3', 41 | ], 42 | 'jquery' => [ 43 | 'version' => '3.7.1', 44 | ], 45 | 'handlebars' => [ 46 | 'version' => '4.7.8', 47 | ], 48 | 'i18next' => [ 49 | 'version' => '24.2.2', 50 | ], 51 | 'file-saver' => [ 52 | 'version' => '2.0.5', 53 | ], 54 | ]; 55 | -------------------------------------------------------------------------------- /migrations/Version20230402040938.php: -------------------------------------------------------------------------------- 1 | addSql('ALTER TABLE connection ADD extra_fields JSON DEFAULT NULL, DROP ip, DROP agent'); 24 | } 25 | 26 | public function down(Schema $schema): void 27 | { 28 | // this down() migration is auto-generated, please modify it to your needs 29 | $this->addSql('ALTER TABLE connection ADD ip VARCHAR(255) DEFAULT NULL, ADD agent VARCHAR(255) DEFAULT NULL, DROP extra_fields'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /migrations/Version20230410131624.php: -------------------------------------------------------------------------------- 1 | addSql('ALTER TABLE connection CHANGE token token VARCHAR(500) NOT NULL'); 24 | } 25 | 26 | public function down(Schema $schema): void 27 | { 28 | // this down() migration is auto-generated, please modify it to your needs 29 | $this->addSql('ALTER TABLE connection CHANGE token token VARCHAR(255) NOT NULL'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /migrations/Version20230624052414.php: -------------------------------------------------------------------------------- 1 | addSql('DELETE FROM action_item WHERE action_id = 12;'); 23 | } 24 | 25 | public function down(Schema $schema): void 26 | { 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /migrations/Version20230624062646.php: -------------------------------------------------------------------------------- 1 | addSql('ALTER TABLE messenger_messages CHANGE created_at created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', CHANGE available_at available_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', CHANGE delivered_at delivered_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\''); 24 | } 25 | 26 | public function down(Schema $schema): void 27 | { 28 | // this down() migration is auto-generated, please modify it to your needs 29 | $this->addSql('ALTER TABLE messenger_messages CHANGE created_at created_at DATETIME NOT NULL, CHANGE available_at available_at DATETIME NOT NULL, CHANGE delivered_at delivered_at DATETIME DEFAULT NULL'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /migrations/Version20250104062315.php: -------------------------------------------------------------------------------- 1 | addSql('CREATE TABLE member_passkey (id INT UNSIGNED AUTO_INCREMENT NOT NULL, member_id INT UNSIGNED NOT NULL, title VARCHAR(255) NOT NULL, credential_id VARCHAR(255) NOT NULL, public_key TEXT NOT NULL, last_time_active DATETIME DEFAULT NULL, date_created DATETIME NOT NULL, INDEX member_id (member_id), INDEX credential_id (credential_id), INDEX date_created (date_created), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); 24 | $this->addSql('ALTER TABLE member_passkey ADD CONSTRAINT FK_BF7183E87597D3FE FOREIGN KEY (member_id) REFERENCES `member` (id) ON DELETE CASCADE'); 25 | } 26 | 27 | public function down(Schema $schema): void 28 | { 29 | // this down() migration is auto-generated, please modify it to your needs 30 | $this->addSql('ALTER TABLE member_passkey DROP FOREIGN KEY FK_BF7183E87597D3FE'); 31 | $this->addSql('DROP TABLE member_passkey'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /migrations/Version20250104074617.php: -------------------------------------------------------------------------------- 1 | addSql('ALTER TABLE `member` ADD passkey_challenge VARCHAR(255) DEFAULT NULL'); 24 | } 25 | 26 | public function down(Schema $schema): void 27 | { 28 | // this down() migration is auto-generated, please modify it to your needs 29 | $this->addSql('ALTER TABLE `member` DROP passkey_challenge'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 6 3 | paths: 4 | - bin/ 5 | - config/ 6 | - public/ 7 | - src/ 8 | - tests/ 9 | checkGenericClassInNonGenericObjectType: false 10 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | tests 23 | 24 | 25 | 26 | 27 | 28 | src 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /public/app/images/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephanediondev/feed/2f61d1922279d3d989f7b485e769b2b7da7275b2/public/app/images/icon-192x192.png -------------------------------------------------------------------------------- /public/app/images/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephanediondev/feed/2f61d1922279d3d989f7b485e769b2b7da7275b2/public/app/images/icon-512x512.png -------------------------------------------------------------------------------- /public/app/images/screenshots/androidphone-item-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephanediondev/feed/2f61d1922279d3d989f7b485e769b2b7da7275b2/public/app/images/screenshots/androidphone-item-dark.png -------------------------------------------------------------------------------- /public/app/images/screenshots/androidphone-item-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephanediondev/feed/2f61d1922279d3d989f7b485e769b2b7da7275b2/public/app/images/screenshots/androidphone-item-light.png -------------------------------------------------------------------------------- /public/app/images/screenshots/ipad-feeds-search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephanediondev/feed/2f61d1922279d3d989f7b485e769b2b7da7275b2/public/app/images/screenshots/ipad-feeds-search.png -------------------------------------------------------------------------------- /public/app/images/screenshots/ipad-homescreen.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephanediondev/feed/2f61d1922279d3d989f7b485e769b2b7da7275b2/public/app/images/screenshots/ipad-homescreen.jpg -------------------------------------------------------------------------------- /public/app/images/screenshots/ipad-item-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephanediondev/feed/2f61d1922279d3d989f7b485e769b2b7da7275b2/public/app/images/screenshots/ipad-item-dark.png -------------------------------------------------------------------------------- /public/app/images/screenshots/ipad-item-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephanediondev/feed/2f61d1922279d3d989f7b485e769b2b7da7275b2/public/app/images/screenshots/ipad-item-light.png -------------------------------------------------------------------------------- /public/app/images/screenshots/ipad-item-share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephanediondev/feed/2f61d1922279d3d989f7b485e769b2b7da7275b2/public/app/images/screenshots/ipad-item-share.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephanediondev/feed/2f61d1922279d3d989f7b485e769b2b7da7275b2/public/favicon.ico -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | collectionManager = $collectionManager; 23 | } 24 | 25 | protected function execute(InputInterface $input, OutputInterface $output): int 26 | { 27 | date_default_timezone_set('UTC'); 28 | 29 | $this->collectionManager->start(); 30 | 31 | return Command::SUCCESS; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Command/ElasticsearchCheckCommand.php: -------------------------------------------------------------------------------- 1 | searchManager = $searchManager; 24 | } 25 | 26 | protected function execute(InputInterface $input, OutputInterface $output): int 27 | { 28 | date_default_timezone_set('UTC'); 29 | 30 | if ($this->searchManager->getEnabled()) { 31 | $path = '/_refresh'; 32 | $this->searchManager->query('GET', $path); 33 | 34 | $types = ['author', 'category', 'feed', 'item']; 35 | 36 | $rows = []; 37 | foreach ($types as $type) { 38 | $path = '/'.$this->searchManager->getIndex().'_'.$type.'/_stats'; 39 | $result = $this->searchManager->query('GET', $path); 40 | if (false === isset($result->error)) { 41 | $rows[] = [$type, $result['_all']['primaries']['docs']['count']]; 42 | } 43 | } 44 | 45 | $table = new Table($output); 46 | $table->setHeaders(['index', 'count'])->setRows($rows); 47 | $table->render(); 48 | } 49 | 50 | return Command::SUCCESS; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Command/ElasticsearchCommand.php: -------------------------------------------------------------------------------- 1 | searchManager = $searchManager; 23 | } 24 | 25 | protected function execute(InputInterface $input, OutputInterface $output): int 26 | { 27 | date_default_timezone_set('UTC'); 28 | 29 | $this->searchManager->start(); 30 | 31 | return Command::SUCCESS; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Command/ElasticsearchCreateCommand.php: -------------------------------------------------------------------------------- 1 | searchManager = $searchManager; 25 | } 26 | 27 | protected function execute(InputInterface $input, OutputInterface $output): int 28 | { 29 | date_default_timezone_set('UTC'); 30 | 31 | $helper = new QuestionHelper(); 32 | 33 | $question = new ConfirmationQuestion('Create Elasticsearch indexes? (yes) ', false); 34 | 35 | if ($helper->ask($input, $output, $question)) { 36 | $this->searchManager->create(); 37 | } 38 | 39 | return Command::SUCCESS; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Command/ElasticsearchRemoveCommand.php: -------------------------------------------------------------------------------- 1 | searchManager = $searchManager; 25 | } 26 | 27 | protected function execute(InputInterface $input, OutputInterface $output): int 28 | { 29 | date_default_timezone_set('UTC'); 30 | 31 | $helper = new QuestionHelper(); 32 | 33 | $question = new ConfirmationQuestion('Remove Elasticsearch records? (yes) ', false); 34 | 35 | if ($helper->ask($input, $output, $question)) { 36 | $this->searchManager->remove(); 37 | } 38 | 39 | return Command::SUCCESS; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Command/ElasticsearchStatusCommand.php: -------------------------------------------------------------------------------- 1 | searchManager = $searchManager; 23 | } 24 | 25 | protected function execute(InputInterface $input, OutputInterface $output): int 26 | { 27 | date_default_timezone_set('UTC'); 28 | 29 | if ($this->searchManager->getEnabled()) { 30 | $path = '/'; 31 | $result = $this->searchManager->query('GET', $path); 32 | if (true === is_array($result)) { 33 | if (true === isset($result['error'])) { 34 | $output->writeln(''.print_r($result, true).''); 35 | } else { 36 | $output->writeln(''.print_r($result, true).''); 37 | } 38 | } else { 39 | $output->writeln(''.$result.''); 40 | } 41 | } else { 42 | $output->writeln('Elasticsearch not enabled'); 43 | } 44 | 45 | return Command::SUCCESS; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Command/GenerateVapidCommand.php: -------------------------------------------------------------------------------- 1 | setDescription('Generate VAPID keys'); 19 | } 20 | 21 | protected function execute(InputInterface $input, OutputInterface $output): int 22 | { 23 | $output->writeln(''.$this->getDescription().''); 24 | 25 | $vapid = VAPID::createVapidKeys(); 26 | 27 | $output->writeln('VAPID_PUBLIC_KEY=\''.$vapid['publicKey'].'\''); 28 | $output->writeln('VAPID_PRIVATE_KEY=\''.$vapid['privateKey'].'\''); 29 | 30 | return Command::SUCCESS; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Command/PhpunitCommand.php: -------------------------------------------------------------------------------- 1 | writeln('PHP version: '.phpversion().''); 19 | 20 | $output->writeln('Symfony version: '.Kernel::VERSION.''); 21 | 22 | $output->writeln(''); 23 | 24 | return Command::SUCCESS; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Command/PushCommand.php: -------------------------------------------------------------------------------- 1 | pushManager = $pushManager; 23 | } 24 | 25 | protected function execute(InputInterface $input, OutputInterface $output): int 26 | { 27 | date_default_timezone_set('UTC'); 28 | 29 | $this->pushManager->sendNotifications(); 30 | 31 | return Command::SUCCESS; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Command/SetupCommand.php: -------------------------------------------------------------------------------- 1 | entityManager = $entityManager; 25 | $this->kernelProjectDir = $kernelProjectDir; 26 | } 27 | 28 | protected function execute(InputInterface $input, OutputInterface $output): int 29 | { 30 | $connection = $this->entityManager->getConnection(); 31 | 32 | //add actions 33 | $file = file_get_contents($this->kernelProjectDir.'/src/DataFixtures/feed.sql'); 34 | if ($file) { 35 | $sql = 'SELECT COUNT(*) AS total FROM action'; 36 | $count = $connection->fetchAssociative($sql); 37 | 38 | if (true === isset($count['total']) && 0 == $count['total']) { 39 | $connection->executeQuery($file); 40 | $output->writeln('Feed data inserted'); 41 | } else { 42 | $output->writeln('Feed data already inserted'); 43 | } 44 | } 45 | 46 | return Command::SUCCESS; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Controller/IndexController.php: -------------------------------------------------------------------------------- 1 | headers->set('X-Frame-Options', 'sameorigin'); 18 | return $this->render('base.html.twig', [], $response); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Controller/LogoutController.php: -------------------------------------------------------------------------------- 1 | headers->get('Authorization')) { 25 | $payloadjwtPayloadModel = JwtHelper::getPayload(str_replace('Bearer ', '', $request->headers->get('Authorization'))); 26 | if ($payloadjwtPayloadModel) { 27 | $token = $payloadjwtPayloadModel->getJwtId(); 28 | 29 | if ($connection = $this->connectionManager->getOne(['type' => Connection::TYPE_LOGIN, 'token' => $token, 'member' => $this->getMember()])) { 30 | $data['entry'] = $connection->toArray(); 31 | $data['entry_entity'] = 'connection'; 32 | 33 | $this->connectionManager->remove($connection); 34 | } 35 | } 36 | } 37 | } catch (\Exception $e) { 38 | throw new AccessDeniedException(); 39 | } 40 | 41 | return $this->jsonResponse($data); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Controller/PinboardController.php: -------------------------------------------------------------------------------- 1 | maxmindEnabled = $maxmindEnabled; 23 | } 24 | 25 | #[Route('/pinboard', name: 'index', methods: ['POST'])] 26 | public function pinboard(Request $request): JsonResponse 27 | { 28 | $data = []; 29 | 30 | $content = $this->getContent($request); 31 | 32 | if (true === isset($content['token']) && '' !== $content['token']) { 33 | $connection = $this->connectionManager->getOne(['type' => Connection::TYPE_PINBOARD, 'member' => $this->getMember()]); 34 | 35 | if ($connection) { 36 | $connection->setToken($content['token']); 37 | } else { 38 | $extraFields = DeviceDetectorHelper::asArray($request); 39 | 40 | if ($extraFields['ip'] && '127.0.0.1' != $extraFields['ip'] && true === $this->maxmindEnabled) { 41 | $data = MaxmindHelper::get($extraFields['ip']); 42 | $extraFields = array_merge($extraFields, $data); 43 | } 44 | 45 | $connection = new Connection(); 46 | $connection->setMember($this->getMember()); 47 | $connection->setType(Connection::TYPE_PINBOARD); 48 | $connection->setToken($content['token']); 49 | $connection->setExtraFields($extraFields); 50 | } 51 | $this->connectionManager->persist($connection); 52 | 53 | $data['entry'] = $connection->toArray(); 54 | $data['entry_entity'] = 'connection'; 55 | } else { 56 | $data['errors'][] = [ 57 | 'status' => '400', 58 | 'source' => [ 59 | 'pointer' => '/data/attributes/token', 60 | ], 61 | 'title' => 'Invalid attribute', 62 | 'detail' => 'This value should not be blank.', 63 | ]; 64 | 65 | return $this->jsonResponse($data, JsonResponse::HTTP_BAD_REQUEST); 66 | } 67 | 68 | return $this->jsonResponse($data); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Controller/ProxyController.php: -------------------------------------------------------------------------------- 1 | [ 28 | 'method' => 'GET', 29 | 'user_agent'=> $request->headers->get('User-Agent'), 30 | ] 31 | ]; 32 | 33 | $context = stream_context_create($opts); 34 | 35 | if ($content = file_get_contents($file, false, $context)) { 36 | $contentType = (new \finfo(FILEINFO_MIME))->buffer($content); 37 | 38 | $response->setContent($content); 39 | $response->setStatusCode(Response::HTTP_OK); 40 | if ($contentType) { 41 | $response->headers->set('Content-Type', $contentType); 42 | } 43 | 44 | return $response; 45 | } 46 | } catch (\Exception $e) { 47 | throw new NotFoundHttpException($e->getMessage()); 48 | } 49 | } 50 | } 51 | 52 | $response->setStatusCode(Response::HTTP_NOT_FOUND); 53 | return $response; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/DataFixtures/feed.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO `action` VALUES(1, NULL, 'read', '2016-09-28 12:25:57'); 2 | INSERT INTO `action` VALUES(2, NULL, 'star', '2016-09-28 12:25:57'); 3 | INSERT INTO `action` VALUES(3, NULL, 'subscribe', '2016-09-28 12:25:57'); 4 | INSERT INTO `action` VALUES(4, NULL, 'read_all', '2016-09-28 12:25:57'); 5 | INSERT INTO `action` VALUES(5, NULL, 'exclude', '2016-09-28 12:25:57'); 6 | INSERT INTO `action` VALUES(6, NULL, 'evernote', '2016-09-28 12:25:57'); 7 | INSERT INTO `action` VALUES(7, NULL, 'email', '2016-09-28 12:25:57'); 8 | INSERT INTO `action` VALUES(8, NULL, 'purge', '2016-09-28 12:25:57'); 9 | INSERT INTO `action` VALUES(11, NULL, 'elasticsearch', '2016-09-28 12:25:57'); 10 | INSERT INTO `action` VALUES(12, NULL, 'unread', '2016-09-28 12:25:57'); 11 | INSERT INTO `action` VALUES(13, NULL, 'unstar', '2016-09-28 00:00:00'); 12 | INSERT INTO `action` VALUES(14, NULL, 'include', '2016-09-28 00:00:00'); 13 | INSERT INTO `action` VALUES(15, NULL, 'unsubscribe', '2016-09-28 00:00:00'); 14 | 15 | UPDATE `action` SET reverse = 1 WHERE id = 12; 16 | UPDATE `action` SET reverse = 2 WHERE id = 13; 17 | UPDATE `action` SET reverse = 3 WHERE id = 15; 18 | UPDATE `action` SET reverse = 5 WHERE id = 14; 19 | 20 | UPDATE `action` SET reverse = 12 WHERE id = 1; 21 | UPDATE `action` SET reverse = 13 WHERE id = 2; 22 | UPDATE `action` SET reverse = 15 WHERE id = 3; 23 | UPDATE `action` SET reverse = 14 WHERE id = 5; 24 | -------------------------------------------------------------------------------- /src/Entity/Action.php: -------------------------------------------------------------------------------- 1 | dateCreated = new \Datetime(); 31 | } 32 | 33 | public function __toString() 34 | { 35 | return $this->getTitle() ?? ''; 36 | } 37 | 38 | public function getTitle(): ?string 39 | { 40 | return $this->title; 41 | } 42 | 43 | public function setTitle(?string $title): self 44 | { 45 | $this->title = $title; 46 | 47 | return $this; 48 | } 49 | 50 | public function getReverse(): ?Action 51 | { 52 | return $this->reverse; 53 | } 54 | 55 | public function setReverse(?Action $reverse): self 56 | { 57 | $this->reverse = $reverse; 58 | 59 | return $this; 60 | } 61 | 62 | /** 63 | * @return array 64 | */ 65 | public function toArray(): array 66 | { 67 | return [ 68 | 'id' => $this->getId(), 69 | 'title' => $this->getTitle(), 70 | ]; 71 | } 72 | 73 | /** 74 | * @return array 75 | */ 76 | public function getJsonApiData(): array 77 | { 78 | return [ 79 | 'id' => strval($this->getId()), 80 | 'type' => 'action', 81 | 'attributes' => [ 82 | 'title' => $this->getTitle(), 83 | 'date_created' => $this->getDateCreated() ? $this->getDateCreated()->format('Y-m-d H:i:s') : null, 84 | ], 85 | ]; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Entity/ActionAuthor.php: -------------------------------------------------------------------------------- 1 | dateCreated = new \Datetime(); 31 | } 32 | 33 | public function getAuthor(): ?Author 34 | { 35 | return $this->author; 36 | } 37 | 38 | public function setAuthor(?Author $author): self 39 | { 40 | $this->author = $author; 41 | 42 | return $this; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Entity/ActionCategory.php: -------------------------------------------------------------------------------- 1 | dateCreated = new \Datetime(); 31 | } 32 | 33 | public function getCategory(): ?Category 34 | { 35 | return $this->category; 36 | } 37 | 38 | public function setCategory(?Category $category): self 39 | { 40 | $this->category = $category; 41 | 42 | return $this; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Entity/ActionFeed.php: -------------------------------------------------------------------------------- 1 | dateCreated = new \Datetime(); 31 | } 32 | 33 | public function getFeed(): ?Feed 34 | { 35 | return $this->feed; 36 | } 37 | 38 | public function setFeed(?Feed $feed): self 39 | { 40 | $this->feed = $feed; 41 | 42 | return $this; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Entity/ActionItem.php: -------------------------------------------------------------------------------- 1 | dateCreated = new \Datetime(); 31 | } 32 | 33 | public function getItem(): ?Item 34 | { 35 | return $this->item; 36 | } 37 | 38 | public function setItem(?Item $item): self 39 | { 40 | $this->item = $item; 41 | 42 | return $this; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Entity/ActionTrait.php: -------------------------------------------------------------------------------- 1 | action; 20 | } 21 | 22 | public function setAction(?Action $action): self 23 | { 24 | $this->action = $action; 25 | 26 | return $this; 27 | } 28 | 29 | public function getMember(): ?Member 30 | { 31 | return $this->member; 32 | } 33 | 34 | public function setMember(?Member $member): self 35 | { 36 | $this->member = $member; 37 | 38 | return $this; 39 | } 40 | 41 | /** 42 | * @return array 43 | */ 44 | public function toArray(): array 45 | { 46 | return [ 47 | 'action' => $this->getAction() ? $this->getAction()->toArray() : null, 48 | ]; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Entity/DateCreatedTrait.php: -------------------------------------------------------------------------------- 1 | dateCreated; 15 | } 16 | 17 | public function setDateCreated(?\DateTimeInterface $dateCreated): self 18 | { 19 | $this->dateCreated = $dateCreated; 20 | 21 | return $this; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Entity/ExtraFieldsTrait.php: -------------------------------------------------------------------------------- 1 | $extraFields 12 | */ 13 | #[ORM\Column(name: "extra_fields", type: "json", nullable: true)] 14 | private ?array $extraFields = []; 15 | 16 | /** 17 | * @return array 18 | */ 19 | public function getExtraFields(): ?array 20 | { 21 | return $this->extraFields ?? []; 22 | } 23 | 24 | /** 25 | * @param array|null $extraFields 26 | */ 27 | public function setExtraFields(?array $extraFields): self 28 | { 29 | $this->extraFields = $extraFields; 30 | return $this; 31 | } 32 | 33 | public function getExtraField(string $path, ?string $type = null): mixed 34 | { 35 | return ExtraFieldsHelper::getPath($path, $this->extraFields, $type); 36 | } 37 | 38 | public function setExtraField(string $path, mixed $value): self 39 | { 40 | $this->extraFields = $this->extraFields ?? []; 41 | ExtraFieldsHelper::setPath($path, $value, $this->extraFields); 42 | 43 | return $this; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Entity/FeedCategory.php: -------------------------------------------------------------------------------- 1 | category; 31 | } 32 | 33 | public function setCategory(?Category $category): self 34 | { 35 | $this->category = $category; 36 | 37 | return $this; 38 | } 39 | 40 | public function getFeed(): ?Feed 41 | { 42 | return $this->feed; 43 | } 44 | 45 | public function setFeed(?Feed $feed): self 46 | { 47 | $this->feed = $feed; 48 | 49 | return $this; 50 | } 51 | 52 | 53 | /** 54 | * @return array 55 | */ 56 | public function toArray(): array 57 | { 58 | return [ 59 | 'id' => $this->getCategory() ? $this->getCategory()->getId() : null, 60 | 'title' => $this->getCategory() ? $this->getCategory()->getTitle() : null, 61 | ]; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Entity/IdTrait.php: -------------------------------------------------------------------------------- 1 | true]), ORM\Id, ORM\GeneratedValue(strategy: "IDENTITY")] 10 | private ?int $id = null; 11 | 12 | public function getId(): ?int 13 | { 14 | return $this->id; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Entity/ItemCategory.php: -------------------------------------------------------------------------------- 1 | category; 31 | } 32 | 33 | public function setCategory(?Category $category): self 34 | { 35 | $this->category = $category; 36 | 37 | return $this; 38 | } 39 | 40 | public function getItem(): ?Item 41 | { 42 | return $this->item; 43 | } 44 | 45 | public function setItem(?Item $item): self 46 | { 47 | $this->item = $item; 48 | 49 | return $this; 50 | } 51 | 52 | /** 53 | * @return array 54 | */ 55 | public function toArray(): array 56 | { 57 | return [ 58 | 'id' => $this->getCategory() ? $this->getCategory()->getId() : null, 59 | 'title' => $this->getCategory() ? $this->getCategory()->getTitle() : null, 60 | ]; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Event/ActionAuthorEvent.php: -------------------------------------------------------------------------------- 1 | actionAuthor = $actionAuthor; 21 | } 22 | 23 | public function getActionAuthor(): ActionAuthor 24 | { 25 | return $this->actionAuthor; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Event/ActionCategoryEvent.php: -------------------------------------------------------------------------------- 1 | actionCategory = $actionCategory; 21 | } 22 | 23 | public function getActionCategory(): ActionCategory 24 | { 25 | return $this->actionCategory; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Event/ActionEvent.php: -------------------------------------------------------------------------------- 1 | action = $action; 21 | } 22 | 23 | public function getAction(): Action 24 | { 25 | return $this->action; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Event/ActionFeedEvent.php: -------------------------------------------------------------------------------- 1 | actionFeed = $actionFeed; 21 | } 22 | 23 | public function getActionFeed(): ActionFeed 24 | { 25 | return $this->actionFeed; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Event/ActionItemEvent.php: -------------------------------------------------------------------------------- 1 | actionItem = $actionItem; 21 | } 22 | 23 | public function getActionItem(): ActionItem 24 | { 25 | return $this->actionItem; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Event/AuthorEvent.php: -------------------------------------------------------------------------------- 1 | author = $author; 21 | } 22 | 23 | public function getAuthor(): Author 24 | { 25 | return $this->author; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Event/CategoryEvent.php: -------------------------------------------------------------------------------- 1 | category = $category; 21 | } 22 | 23 | public function getCategory(): Category 24 | { 25 | return $this->category; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Event/CollectionEvent.php: -------------------------------------------------------------------------------- 1 | collection = $collection; 21 | } 22 | 23 | public function getCollection(): Collection 24 | { 25 | return $this->collection; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Event/CollectionFeedEvent.php: -------------------------------------------------------------------------------- 1 | collectionFeed = $collectionFeed; 21 | } 22 | 23 | public function getCollectionFeed(): CollectionFeed 24 | { 25 | return $this->collectionFeed; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Event/ConnectionEvent.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 21 | } 22 | 23 | public function getConnection(): Connection 24 | { 25 | return $this->connection; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Event/EnclosureEvent.php: -------------------------------------------------------------------------------- 1 | enclosure = $enclosure; 21 | } 22 | 23 | public function getEnclosure(): Enclosure 24 | { 25 | return $this->enclosure; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Event/FeedCategoryEvent.php: -------------------------------------------------------------------------------- 1 | feedCategory = $feedCategory; 21 | } 22 | 23 | public function getFeedCategory(): FeedCategory 24 | { 25 | return $this->feedCategory; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Event/FeedEvent.php: -------------------------------------------------------------------------------- 1 | feed = $feed; 21 | } 22 | 23 | public function getFeed(): Feed 24 | { 25 | return $this->feed; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Event/ItemCategoryEvent.php: -------------------------------------------------------------------------------- 1 | itemCategory = $itemCategory; 21 | } 22 | 23 | public function getItemCategory(): ItemCategory 24 | { 25 | return $this->itemCategory; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Event/ItemEvent.php: -------------------------------------------------------------------------------- 1 | item = $item; 21 | } 22 | 23 | public function getItem(): Item 24 | { 25 | return $this->item; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Event/MemberEvent.php: -------------------------------------------------------------------------------- 1 | member = $member; 21 | } 22 | 23 | public function getMember(): Member 24 | { 25 | return $this->member; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/EventSubscriber/PinboardSubscriber.php: -------------------------------------------------------------------------------- 1 | connectionManager = $connectionManager; 20 | } 21 | 22 | public static function getSubscribedEvents(): array 23 | { 24 | return [ 25 | ActionItemEvent::CREATED => 'add', 26 | ActionItemEvent::DELETED => 'delete', 27 | ]; 28 | } 29 | 30 | public function add(ActionItemEvent $actionItemEvent): void 31 | { 32 | $actionItem = $actionItemEvent->getActionItem(); 33 | 34 | if ($actionItem->getAction() && $actionItem->getAction()->getTitle() == 'star') { 35 | $this->query('add', $actionItem); 36 | } 37 | } 38 | 39 | public function delete(ActionItemEvent $actionItemEvent): void 40 | { 41 | $actionItem = $actionItemEvent->getActionItem(); 42 | 43 | if ($actionItem->getAction() && $actionItem->getAction()->getTitle() == 'star') { 44 | $this->query('delete', $actionItem); 45 | } 46 | } 47 | 48 | private function query(string $method, ActionItem $actionItem): void 49 | { 50 | $member = $actionItem->getMember(); 51 | 52 | if ($connection = $this->connectionManager->getOne(['type' => Connection::TYPE_PINBOARD, 'member' => $member])) { 53 | $item = $actionItem->getItem(); 54 | 55 | if ($item) { 56 | $url = 'https://api.pinboard.in/v1/posts/'.$method; 57 | 58 | $fields = [ 59 | 'auth_token' => $connection->getToken(), 60 | 'url' => $item->getLink(), 61 | 'description' => $item->getTitle(), 62 | 'replace' => 'yes', 63 | ]; 64 | 65 | $ci = curl_init(); 66 | curl_setopt($ci, CURLOPT_URL, $url.'?'.http_build_query($fields)); 67 | curl_setopt($ci, CURLOPT_CUSTOMREQUEST, 'GET'); 68 | curl_setopt($ci, CURLOPT_RETURNTRANSFER, 1); 69 | curl_exec($ci); 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/EventSubscriber/SearchSubscriber.php: -------------------------------------------------------------------------------- 1 | searchManager = $searchManager; 24 | $this->itemManager = $itemManager; 25 | } 26 | 27 | public static function getSubscribedEvents(): array 28 | { 29 | return [ 30 | AuthorEvent::DELETED => 'removeAuthor', 31 | CategoryEvent::DELETED => 'removeCategory', 32 | FeedEvent::DELETED => 'removeFeed', 33 | ItemEvent::DELETED => 'removeItem', 34 | ]; 35 | } 36 | 37 | public function removeAuthor(AuthorEvent $authorEvent): void 38 | { 39 | $path = '/'.$this->searchManager->getIndex().'_author/doc/'.$authorEvent->getAuthor()->getId(); 40 | $this->searchManager->query('DELETE', $path); 41 | } 42 | 43 | public function removeCategory(CategoryEvent $categoryEvent): void 44 | { 45 | $path = '/'.$this->searchManager->getIndex().'_category/doc/'.$categoryEvent->getCategory()->getId(); 46 | $this->searchManager->query('DELETE', $path); 47 | } 48 | 49 | public function removeFeed(FeedEvent $feedEvent): void 50 | { 51 | $parameters = []; 52 | $parameters['feed'] = (int) $feedEvent->getFeed()->getId(); 53 | $parameters['sortField'] = 'itm.id'; 54 | $parameters['sortDirection'] = 'ASC'; 55 | foreach ($this->itemManager->getList($parameters)->getResult() as $item) { 56 | $path = '/'.$this->searchManager->getIndex().'_item/doc/'.$item['id']; 57 | $this->searchManager->query('DELETE', $path); 58 | } 59 | 60 | $path = '/'.$this->searchManager->getIndex().'_feed/doc/'.$feedEvent->getFeed()->getId(); 61 | $this->searchManager->query('DELETE', $path); 62 | } 63 | 64 | public function removeItem(ItemEvent $itemEvent): void 65 | { 66 | $path = '/'.$this->searchManager->getIndex().'_item/doc/'.$itemEvent->getItem()->getId(); 67 | $this->searchManager->query('DELETE', $path); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/EventSubscriber/SubscribeSubscriber.php: -------------------------------------------------------------------------------- 1 | memberManager = $memberManager; 18 | } 19 | 20 | public static function getSubscribedEvents(): array 21 | { 22 | return [ 23 | ActionFeedEvent::CREATED => 'unread', 24 | ActionFeedEvent::UPDATED => 'unread', 25 | ActionFeedEvent::DELETED => 'unread', 26 | ]; 27 | } 28 | 29 | public function unread(ActionFeedEvent $actionFeedEvent): void 30 | { 31 | $actionFeed = $actionFeedEvent->getActionFeed(); 32 | 33 | if ($actionFeed->getAction() && $actionFeed->getAction()->getTitle() == 'subscribe') { 34 | if ($member = $actionFeed->getMember()) { 35 | $this->memberManager->syncUnread($member->getid()); 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Form/Type/AuthorType.php: -------------------------------------------------------------------------------- 1 | add('title', TextType::class, [ 19 | 'constraints' => [ 20 | new NotBlank(), 21 | ], 22 | ]); 23 | } 24 | 25 | public function configureOptions(OptionsResolver $resolver): void 26 | { 27 | $resolver->setDefaults([ 28 | 'data_class' => Author::class, 29 | 'csrf_protection' => false, 30 | ]); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Form/Type/CategoryType.php: -------------------------------------------------------------------------------- 1 | add('title', TextType::class, [ 19 | 'constraints' => [ 20 | new NotBlank(), 21 | ], 22 | ]); 23 | } 24 | 25 | public function configureOptions(OptionsResolver $resolver): void 26 | { 27 | $resolver->setDefaults([ 28 | 'data_class' => Category::class, 29 | 'csrf_protection' => false, 30 | ]); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Form/Type/FeedType.php: -------------------------------------------------------------------------------- 1 | add('title', TextType::class, [ 20 | 'constraints' => [ 21 | new NotBlank(), 22 | ], 23 | ]); 24 | 25 | $builder->add('link', TextType::class, [ 26 | 'constraints' => [ 27 | new NotBlank(), 28 | ], 29 | ]); 30 | 31 | $builder->add('website', TextType::class); 32 | 33 | $builder->add('language', TextType::class); 34 | 35 | $builder->add('description', TextareaType::class); 36 | } 37 | 38 | public function configureOptions(OptionsResolver $resolver): void 39 | { 40 | $resolver->setDefaults([ 41 | 'data_class' => Feed::class, 42 | 'csrf_protection' => false, 43 | ]); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Form/Type/ImportOpmlType.php: -------------------------------------------------------------------------------- 1 | add('file', FileType::class); 18 | } 19 | 20 | public function configureOptions(OptionsResolver $resolver): void 21 | { 22 | $resolver->setDefaults([ 23 | 'data_class' => ImportOpmlModel::class, 24 | 'csrf_protection' => false, 25 | ]); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Form/Type/LoginType.php: -------------------------------------------------------------------------------- 1 | add('email', EmailType::class, [ 21 | 'constraints' => [ 22 | new NotBlank(), 23 | new Email(), 24 | ], 25 | ]); 26 | 27 | $builder->add('password', PasswordType::class, [ 28 | 'constraints' => [ 29 | new NotBlank(), 30 | ], 31 | ]); 32 | } 33 | 34 | public function configureOptions(OptionsResolver $resolver): void 35 | { 36 | $resolver->setDefaults([ 37 | 'data_class' => LoginModel::class, 38 | 'csrf_protection' => false, 39 | ]); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Form/Type/MemberPasskeyType.php: -------------------------------------------------------------------------------- 1 | add('title', TextType::class, [ 19 | 'constraints' => [ 20 | new NotBlank(), 21 | ], 22 | ]); 23 | } 24 | 25 | public function configureOptions(OptionsResolver $resolver): void 26 | { 27 | $resolver->setDefaults([ 28 | 'data_class' => MemberPasskey::class, 29 | 'csrf_protection' => false, 30 | ]); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Form/Type/MemberType.php: -------------------------------------------------------------------------------- 1 | add('email', EmailType::class, [ 22 | 'constraints' => [ 23 | new NotBlank(), 24 | new Email(), 25 | ], 26 | ]); 27 | 28 | if (Request::METHOD_POST === $options['request_method']) { 29 | $builder->add('plainPassword', PasswordType::class, [ 30 | 'constraints' => [ 31 | new NotBlank(), 32 | ], 33 | ]); 34 | } 35 | 36 | if (Request::METHOD_PUT === $options['request_method']) { 37 | $builder->add('plainPassword', PasswordType::class); 38 | } 39 | } 40 | 41 | public function configureOptions(OptionsResolver $resolver): void 42 | { 43 | $resolver->setDefaults([ 44 | 'data_class' => Member::class, 45 | 'csrf_protection' => false, 46 | 'request_method' => null, 47 | ]); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Form/Type/ProfileType.php: -------------------------------------------------------------------------------- 1 | add('email', EmailType::class, [ 21 | 'constraints' => [ 22 | new NotBlank(), 23 | new Email(), 24 | ], 25 | ]); 26 | 27 | $builder->add('password', PasswordType::class); 28 | 29 | $builder->add('passwordConfirm', PasswordType::class); 30 | } 31 | 32 | public function configureOptions(OptionsResolver $resolver): void 33 | { 34 | $resolver->setDefaults([ 35 | 'data_class' => ProfileModel::class, 36 | 'csrf_protection' => false, 37 | ]); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Form/Type/PushType.php: -------------------------------------------------------------------------------- 1 | add('endpoint', TextType::class, [ 20 | 'property_path' => 'token', 21 | 'constraints' => [ 22 | new NotBlank(), 23 | ], 24 | ]); 25 | 26 | $builder->add('publicKey', TextType::class, [ 27 | 'property_path' => 'extra_fields[public_key]', 28 | 'constraints' => [ 29 | new NotBlank(), 30 | ], 31 | ]); 32 | 33 | $builder->add('authenticationSecret', PasswordType::class, [ 34 | 'property_path' => 'extra_fields[authentication_secret]', 35 | 'constraints' => [ 36 | new NotBlank(), 37 | ], 38 | ]); 39 | 40 | $builder->add('contentEncoding', TextType::class, [ 41 | 'property_path' => 'extra_fields[content_encoding]', 42 | 'constraints' => [ 43 | new NotBlank(), 44 | ], 45 | ]); 46 | } 47 | 48 | public function configureOptions(OptionsResolver $resolver): void 49 | { 50 | $resolver->setDefaults([ 51 | 'data_class' => Connection::class, 52 | 'csrf_protection' => false, 53 | ]); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Helper/DeviceDetectorHelper.php: -------------------------------------------------------------------------------- 1 | getClientIp()) { 16 | $deviceDetector->setIp($clientIp); 17 | if ($hostbyaddr = gethostbyaddr($clientIp)) { 18 | $deviceDetector->setHostname($hostbyaddr); 19 | } 20 | } 21 | 22 | if ($userAgent = $request->headers->get('User-Agent')) { 23 | $dd = new DeviceDetector($userAgent); 24 | $dd->skipBotDetection(); 25 | $dd->parse(); 26 | 27 | $client = $dd->getClient(); 28 | $os = $dd->getOs(); 29 | $device = $dd->getDeviceName(); 30 | $brand = $dd->getBrandName(); 31 | $model = $dd->getModel(); 32 | 33 | if (true === is_array($client) && true === array_key_exists('name', $client) && true === array_key_exists('version', $client)) { 34 | $deviceDetector->setClient($client['name'].' '.$client['version']); 35 | } 36 | if (true === is_array($os) && true === array_key_exists('name', $os) && true === array_key_exists('version', $os)) { 37 | $deviceDetector->setOs($os['name'].' '.$os['version']); 38 | } 39 | $deviceDetector->setDevice($device); 40 | $deviceDetector->setBrand($brand); 41 | $deviceDetector->setModel($model); 42 | } 43 | 44 | return $deviceDetector; 45 | } 46 | 47 | /** 48 | * @return array 49 | */ 50 | public static function asArray(Request $request): array 51 | { 52 | $deviceDetector = DeviceDetectorHelper::get($request); 53 | 54 | $extraFields = []; 55 | $extraFields['ip'] = $deviceDetector->getIp(); 56 | $extraFields['hostname'] = $deviceDetector->getHostname(); 57 | $extraFields['client'] = $deviceDetector->getClient(); 58 | $extraFields['os'] = $deviceDetector->getOs(); 59 | $extraFields['device'] = $deviceDetector->getDevice(); 60 | $extraFields['brand'] = $deviceDetector->getBrand(); 61 | $extraFields['model'] = $deviceDetector->getModel(); 62 | 63 | return $extraFields; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Helper/ExtraFieldsHelper.php: -------------------------------------------------------------------------------- 1 | |null $fields 16 | */ 17 | public static function getPath(string $path, ?array $fields = [], ?string $type = null): mixed 18 | { 19 | $elements = explode('.', $path); 20 | 21 | if (1 === count($elements)) { 22 | if ($fields && array_key_exists($path, $fields)) { 23 | return static::resolve($fields[$path], $type); 24 | } 25 | return null; 26 | } else { 27 | foreach ($elements as $element) { 28 | $fields = &$fields[$element]; 29 | } 30 | 31 | return static::resolve($fields, $type); 32 | } 33 | } 34 | 35 | /** 36 | * @param array|null $fields 37 | */ 38 | public static function setPath(string $path, mixed $value, ?array &$fields = []): void 39 | { 40 | $elements = explode('.', $path); 41 | 42 | foreach ($elements as &$element) { 43 | $element = '['.$element.']'; 44 | } 45 | 46 | $path = implode('', $elements); 47 | 48 | if ($value instanceof \DateTime) { 49 | $value = (array) $value; 50 | } 51 | 52 | $propertyAccessor = PropertyAccess::createPropertyAccessor(); 53 | $propertyAccessor->setValue($fields, $path, $value); 54 | } 55 | 56 | private static function resolve(mixed $value, ?string $type = null): mixed 57 | { 58 | switch ($type) { 59 | case null: 60 | return $value; 61 | 62 | case self::TYPE_BOOL: 63 | return (bool) $value; 64 | 65 | case self::TYPE_DATETIME: 66 | if (true === is_array($value)) { 67 | return new \DateTime($value['date'], new \DateTimeZone($value['timezone'])); 68 | } elseif ($value instanceof \DateTime) { 69 | return $value; 70 | } elseif (true === is_string($value)) { 71 | return new \DateTime($value); 72 | } 73 | 74 | // no break 75 | case self::TYPE_FLOAT: 76 | return floatval($value); 77 | 78 | case self::TYPE_STRING: 79 | return strval($value); 80 | 81 | default: 82 | return null; 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Helper/MaxmindHelper.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | public static function get(string $ip): array 13 | { 14 | $data = []; 15 | 16 | $file = __DIR__.'/../../maxmind/GeoLite2-City/GeoLite2-City.mmdb'; 17 | 18 | if (file_exists($file)) { 19 | $reader = new Reader($file); 20 | $record = $reader->city($ip); 21 | $reader->close(); 22 | 23 | if ($country = $record->country->name) { 24 | $data['country'] = $country; 25 | } 26 | 27 | if ($subdivision = $record->mostSpecificSubdivision->name) { 28 | $data['subdivision'] = $subdivision; 29 | } 30 | 31 | if ($city = $record->city->name) { 32 | $data['city'] = $city; 33 | } 34 | 35 | if ($latitude = $record->location->latitude) { 36 | $data['latitude'] = $latitude; 37 | } 38 | 39 | if ($longitude = $record->location->longitude) { 40 | $data['longitude'] = $longitude; 41 | } 42 | } 43 | 44 | return $data; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Kernel.php: -------------------------------------------------------------------------------- 1 | eventDispatcher = $eventDispatcher; 18 | } 19 | 20 | public function clearCache(): void 21 | { 22 | if (function_exists('apcu_clear_cache')) { 23 | apcu_clear_cache(); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Manager/ActionManager.php: -------------------------------------------------------------------------------- 1 | actionRepository = $actionRepository; 19 | } 20 | 21 | /** 22 | * @param array $parameters 23 | */ 24 | public function getOne(array $parameters = []): ?Action 25 | { 26 | return $this->actionRepository->getOne($parameters); 27 | } 28 | 29 | /** 30 | * @param array $parameters 31 | */ 32 | public function getList(array $parameters = []): mixed 33 | { 34 | return $this->actionRepository->getList($parameters); 35 | } 36 | 37 | public function persist(Action $action): void 38 | { 39 | if ($action->getId() === null) { 40 | $eventName = ActionEvent::CREATED; 41 | } else { 42 | $eventName = ActionEvent::UPDATED; 43 | } 44 | 45 | $this->actionRepository->persist($action); 46 | 47 | $event = new ActionEvent($action); 48 | $this->eventDispatcher->dispatch($event, $eventName); 49 | 50 | $this->clearCache(); 51 | } 52 | 53 | public function remove(Action $action): void 54 | { 55 | $event = new ActionEvent($action); 56 | $this->eventDispatcher->dispatch($event, ActionEvent::DELETED); 57 | 58 | $this->actionRepository->remove($action); 59 | 60 | $this->clearCache(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Manager/AuthorManager.php: -------------------------------------------------------------------------------- 1 | authorRepository = $authorRepository; 19 | } 20 | 21 | /** 22 | * @param array $parameters 23 | */ 24 | public function getOne(array $parameters = []): ?Author 25 | { 26 | return $this->authorRepository->getOne($parameters); 27 | } 28 | 29 | /** 30 | * @param array $parameters 31 | */ 32 | public function getList(array $parameters = []): mixed 33 | { 34 | return $this->authorRepository->getList($parameters); 35 | } 36 | 37 | public function persist(Author $author): void 38 | { 39 | if ($author->getId() === null) { 40 | $eventName = AuthorEvent::CREATED; 41 | } else { 42 | $eventName = AuthorEvent::UPDATED; 43 | } 44 | 45 | $this->authorRepository->persist($author); 46 | 47 | $event = new AuthorEvent($author); 48 | $this->eventDispatcher->dispatch($event, $eventName); 49 | 50 | $this->clearCache(); 51 | } 52 | 53 | public function remove(Author $author): void 54 | { 55 | $event = new AuthorEvent($author); 56 | $this->eventDispatcher->dispatch($event, AuthorEvent::DELETED); 57 | 58 | $this->authorRepository->remove($author); 59 | 60 | $this->clearCache(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Manager/CategoryManager.php: -------------------------------------------------------------------------------- 1 | categoryRepository = $categoryRepository; 25 | $this->itemCategoryManager = $itemCategoryManager; 26 | $this->feedCategoryManager = $feedCategoryManager; 27 | } 28 | 29 | /** 30 | * @param array $parameters 31 | */ 32 | public function getOne(array $parameters = []): ?Category 33 | { 34 | return $this->categoryRepository->getOne($parameters); 35 | } 36 | 37 | /** 38 | * @param array $parameters 39 | */ 40 | public function getList(array $parameters = []): mixed 41 | { 42 | return $this->categoryRepository->getList($parameters); 43 | } 44 | 45 | public function persist(Category $category): void 46 | { 47 | if ($category->getId() === null) { 48 | $eventName = CategoryEvent::CREATED; 49 | } else { 50 | $eventName = CategoryEvent::UPDATED; 51 | } 52 | 53 | $this->categoryRepository->persist($category); 54 | 55 | $event = new CategoryEvent($category); 56 | $this->eventDispatcher->dispatch($event, $eventName); 57 | 58 | $this->clearCache(); 59 | } 60 | 61 | public function remove(Category $category): void 62 | { 63 | $event = new CategoryEvent($category); 64 | $this->eventDispatcher->dispatch($event, CategoryEvent::DELETED); 65 | 66 | $this->categoryRepository->remove($category); 67 | 68 | $this->clearCache(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Manager/CollectionFeedManager.php: -------------------------------------------------------------------------------- 1 | collectionFeedRepository = $collectionFeedRepository; 19 | } 20 | 21 | /** 22 | * @param array $parameters 23 | */ 24 | public function getOne(array $parameters = []): ?CollectionFeed 25 | { 26 | return $this->collectionFeedRepository->getOne($parameters); 27 | } 28 | 29 | /** 30 | * @param array $parameters 31 | */ 32 | public function getList(array $parameters = []): mixed 33 | { 34 | return $this->collectionFeedRepository->getList($parameters); 35 | } 36 | 37 | public function persist(CollectionFeed $collectionFeed): void 38 | { 39 | if ($collectionFeed->getId() === null) { 40 | $eventName = CollectionFeedEvent::CREATED; 41 | } else { 42 | $eventName = CollectionFeedEvent::UPDATED; 43 | } 44 | 45 | $this->collectionFeedRepository->persist($collectionFeed); 46 | 47 | $event = new CollectionFeedEvent($collectionFeed); 48 | $this->eventDispatcher->dispatch($event, $eventName); 49 | 50 | $this->clearCache(); 51 | } 52 | 53 | public function remove(CollectionFeed $collectionFeed): void 54 | { 55 | $event = new CollectionFeedEvent($collectionFeed); 56 | $this->eventDispatcher->dispatch($event, CollectionFeedEvent::DELETED); 57 | 58 | $this->collectionFeedRepository->remove($collectionFeed); 59 | 60 | $this->clearCache(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Manager/ConnectionManager.php: -------------------------------------------------------------------------------- 1 | connectionRepository = $connectionRepository; 19 | } 20 | 21 | /** 22 | * @param array $parameters 23 | */ 24 | public function getOne(array $parameters = []): ?Connection 25 | { 26 | return $this->connectionRepository->getOne($parameters); 27 | } 28 | 29 | /** 30 | * @param array $parameters 31 | */ 32 | public function getList(array $parameters = []): mixed 33 | { 34 | return $this->connectionRepository->getList($parameters); 35 | } 36 | 37 | public function persist(Connection $connection): void 38 | { 39 | if ($connection->getId() === null) { 40 | $eventName = ConnectionEvent::CREATED; 41 | } else { 42 | $eventName = ConnectionEvent::UPDATED; 43 | } 44 | $connection->setDateModified(new \Datetime()); 45 | 46 | $this->connectionRepository->persist($connection); 47 | 48 | $event = new ConnectionEvent($connection); 49 | $this->eventDispatcher->dispatch($event, $eventName); 50 | 51 | $this->clearCache(); 52 | } 53 | 54 | public function remove(Connection $connection): void 55 | { 56 | $event = new ConnectionEvent($connection); 57 | $this->eventDispatcher->dispatch($event, ConnectionEvent::DELETED); 58 | 59 | $this->connectionRepository->remove($connection); 60 | 61 | $this->clearCache(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Manager/EnclosureManager.php: -------------------------------------------------------------------------------- 1 | enclosureRepository = $enclosureRepository; 19 | } 20 | 21 | /** 22 | * @param array $parameters 23 | */ 24 | public function getOne(array $parameters = []): ?Enclosure 25 | { 26 | return $this->enclosureRepository->getOne($parameters); 27 | } 28 | 29 | /** 30 | * @param array $parameters 31 | */ 32 | public function getList(array $parameters = []): mixed 33 | { 34 | return $this->enclosureRepository->getList($parameters); 35 | } 36 | 37 | public function persist(Enclosure $enclosure): void 38 | { 39 | if ($enclosure->getId() === null) { 40 | $eventName = EnclosureEvent::CREATED; 41 | } else { 42 | $eventName = EnclosureEvent::UPDATED; 43 | } 44 | 45 | $this->enclosureRepository->persist($enclosure); 46 | 47 | $event = new EnclosureEvent($enclosure); 48 | $this->eventDispatcher->dispatch($event, $eventName); 49 | 50 | $this->clearCache(); 51 | } 52 | 53 | public function remove(Enclosure $enclosure): void 54 | { 55 | $event = new EnclosureEvent($enclosure); 56 | $this->eventDispatcher->dispatch($event, EnclosureEvent::DELETED); 57 | 58 | $this->enclosureRepository->remove($enclosure); 59 | 60 | $this->clearCache(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Manager/FeedCategoryManager.php: -------------------------------------------------------------------------------- 1 | feedCategoryRepository = $feedCategoryRepository; 19 | } 20 | 21 | /** 22 | * @param array $parameters 23 | */ 24 | public function getOne(array $parameters = []): ?FeedCategory 25 | { 26 | return $this->feedCategoryRepository->getOne($parameters); 27 | } 28 | 29 | /** 30 | * @param array $parameters 31 | */ 32 | public function getList(array $parameters = []): mixed 33 | { 34 | return $this->feedCategoryRepository->getList($parameters); 35 | } 36 | 37 | public function persist(FeedCategory $feedCategory): void 38 | { 39 | if ($feedCategory->getId() === null) { 40 | $eventName = FeedCategoryEvent::CREATED; 41 | } else { 42 | $eventName = FeedCategoryEvent::UPDATED; 43 | } 44 | 45 | $this->feedCategoryRepository->persist($feedCategory); 46 | 47 | $event = new FeedCategoryEvent($feedCategory); 48 | $this->eventDispatcher->dispatch($event, $eventName); 49 | 50 | $this->clearCache(); 51 | } 52 | 53 | public function remove(FeedCategory $feedCategory): void 54 | { 55 | $event = new FeedCategoryEvent($feedCategory); 56 | $this->eventDispatcher->dispatch($event, FeedCategoryEvent::DELETED); 57 | 58 | $this->feedCategoryRepository->remove($feedCategory); 59 | 60 | $this->clearCache(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Manager/ItemCategoryManager.php: -------------------------------------------------------------------------------- 1 | itemCategoryRepository = $itemCategoryRepository; 19 | } 20 | 21 | /** 22 | * @param array $parameters 23 | */ 24 | public function getOne(array $parameters = []): ?ItemCategory 25 | { 26 | return $this->itemCategoryRepository->getOne($parameters); 27 | } 28 | 29 | /** 30 | * @param array $parameters 31 | */ 32 | public function getList(array $parameters = []): mixed 33 | { 34 | return $this->itemCategoryRepository->getList($parameters); 35 | } 36 | 37 | public function persist(ItemCategory $itemCategory): void 38 | { 39 | if ($itemCategory->getId() === null) { 40 | $eventName = ItemCategoryEvent::CREATED; 41 | } else { 42 | $eventName = ItemCategoryEvent::UPDATED; 43 | } 44 | 45 | $this->itemCategoryRepository->persist($itemCategory); 46 | 47 | $event = new ItemCategoryEvent($itemCategory); 48 | $this->eventDispatcher->dispatch($event, $eventName); 49 | 50 | $this->clearCache(); 51 | } 52 | 53 | public function remove(ItemCategory $itemCategory): void 54 | { 55 | $event = new ItemCategoryEvent($itemCategory); 56 | $this->eventDispatcher->dispatch($event, ItemCategoryEvent::DELETED); 57 | 58 | $this->itemCategoryRepository->remove($itemCategory); 59 | 60 | $this->clearCache(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Manager/MemberManager.php: -------------------------------------------------------------------------------- 1 | memberRepository = $memberRepository; 19 | } 20 | 21 | /** 22 | * @param array $parameters 23 | */ 24 | public function getOne(array $parameters = []): ?Member 25 | { 26 | return $this->memberRepository->getOne($parameters); 27 | } 28 | 29 | /** 30 | * @param array $parameters 31 | */ 32 | public function getList(array $parameters = []): mixed 33 | { 34 | return $this->memberRepository->getList($parameters); 35 | } 36 | 37 | public function persist(Member $member): void 38 | { 39 | if ($member->getId() === null) { 40 | $eventName = MemberEvent::CREATED; 41 | } else { 42 | $eventName = MemberEvent::UPDATED; 43 | } 44 | $member->setDateModified(new \Datetime()); 45 | 46 | $this->memberRepository->persist($member); 47 | 48 | $event = new MemberEvent($member); 49 | $this->eventDispatcher->dispatch($event, $eventName); 50 | 51 | $this->clearCache(); 52 | } 53 | 54 | public function remove(Member $member): void 55 | { 56 | $event = new MemberEvent($member); 57 | $this->eventDispatcher->dispatch($event, MemberEvent::DELETED); 58 | 59 | $this->memberRepository->remove($member); 60 | 61 | $this->clearCache(); 62 | } 63 | 64 | public function syncUnread(?int $member_id): void 65 | { 66 | if ($member_id) { 67 | $this->memberRepository->syncUnread($member_id); 68 | } 69 | } 70 | 71 | public function countUnread(int $member_id): int 72 | { 73 | return $this->memberRepository->countUnread($member_id); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Manager/MemberPasskeyManager.php: -------------------------------------------------------------------------------- 1 | memberPasskeyRepository = $memberPasskeyRepository; 18 | } 19 | 20 | /** 21 | * @param array $parameters 22 | */ 23 | public function getOne(array $parameters = []): ?MemberPasskey 24 | { 25 | return $this->memberPasskeyRepository->getOne($parameters); 26 | } 27 | 28 | /** 29 | * @param array $parameters 30 | */ 31 | public function getList(array $parameters = []): mixed 32 | { 33 | return $this->memberPasskeyRepository->getList($parameters); 34 | } 35 | 36 | public function persist(MemberPasskey $memberPasskey): void 37 | { 38 | $this->memberPasskeyRepository->persist($memberPasskey); 39 | 40 | $this->clearCache(); 41 | } 42 | 43 | public function remove(MemberPasskey $memberPasskey): void 44 | { 45 | $this->memberPasskeyRepository->remove($memberPasskey); 46 | 47 | $this->clearCache(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Model/DeviceDetectorModel.php: -------------------------------------------------------------------------------- 1 | ip; 12 | } 13 | 14 | public function setIp(?string $ip): self 15 | { 16 | $this->ip = $ip; 17 | return $this; 18 | } 19 | 20 | private ?string $hostname = null; 21 | 22 | public function getHostname(): ?string 23 | { 24 | return $this->hostname; 25 | } 26 | 27 | public function setHostname(?string $hostname): self 28 | { 29 | $this->hostname = $hostname; 30 | return $this; 31 | } 32 | 33 | private ?string $client = null; 34 | 35 | public function getClient(): ?string 36 | { 37 | return $this->client; 38 | } 39 | 40 | public function setClient(?string $client): self 41 | { 42 | $this->client = $client; 43 | return $this; 44 | } 45 | 46 | private ?string $os = null; 47 | 48 | public function getOs(): ?string 49 | { 50 | return $this->os; 51 | } 52 | 53 | public function setOs(?string $os): self 54 | { 55 | $this->os = $os; 56 | return $this; 57 | } 58 | 59 | private ?string $device = null; 60 | 61 | public function getDevice(): ?string 62 | { 63 | return $this->device; 64 | } 65 | 66 | public function setDevice(?string $device): self 67 | { 68 | $this->device = $device; 69 | return $this; 70 | } 71 | 72 | private ?string $brand = null; 73 | 74 | public function getBrand(): ?string 75 | { 76 | return $this->brand; 77 | } 78 | 79 | public function setBrand(?string $brand): self 80 | { 81 | $this->brand = $brand; 82 | return $this; 83 | } 84 | 85 | private ?string $model = null; 86 | 87 | public function getModel(): ?string 88 | { 89 | return $this->model; 90 | } 91 | 92 | public function setModel(?string $model): self 93 | { 94 | $this->model = $model; 95 | return $this; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Model/ImportOpmlModel.php: -------------------------------------------------------------------------------- 1 | file; 14 | } 15 | 16 | public function setFile(?string $file): self 17 | { 18 | $this->file = $file; 19 | 20 | return $this; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Model/LoginModel.php: -------------------------------------------------------------------------------- 1 | email; 16 | } 17 | 18 | public function setEmail(?string $email): self 19 | { 20 | $this->email = $email; 21 | 22 | return $this; 23 | } 24 | 25 | public function getPassword(): ?string 26 | { 27 | return $this->password; 28 | } 29 | 30 | public function setPassword(?string $password): self 31 | { 32 | $this->password = $password; 33 | 34 | return $this; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Model/ProfileModel.php: -------------------------------------------------------------------------------- 1 | email; 18 | } 19 | 20 | public function setEmail(?string $email): self 21 | { 22 | $this->email = $email; 23 | 24 | return $this; 25 | } 26 | 27 | public function getPassword(): ?string 28 | { 29 | return $this->password; 30 | } 31 | 32 | public function setPassword(?string $password): self 33 | { 34 | $this->password = $password; 35 | 36 | return $this; 37 | } 38 | 39 | public function getPasswordConfirm(): ?string 40 | { 41 | return $this->passwordConfirm; 42 | } 43 | 44 | public function setPasswordConfirm(?string $passwordConfirm): self 45 | { 46 | $this->passwordConfirm = $passwordConfirm; 47 | 48 | return $this; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Model/QueryParameterFilterModel.php: -------------------------------------------------------------------------------- 1 | $filters 11 | */ 12 | private ?array $filters = null; 13 | 14 | /** 15 | * @param array|null $filters 16 | */ 17 | public function __construct(?array $filters) 18 | { 19 | $this->filters = $filters; 20 | } 21 | 22 | public function get(string $key): mixed 23 | { 24 | return $this->filters[$key] ?? null; 25 | } 26 | 27 | public function getBool(string $key): mixed 28 | { 29 | return true === isset($this->filters[$key]) && true === in_array(strtolower($this->filters[$key]), ['true', '1']) ? true : false; 30 | } 31 | 32 | public function getInt(string $key): mixed 33 | { 34 | return true === isset($this->filters[$key]) ? intval($this->filters[$key]) : null; 35 | } 36 | 37 | /** 38 | * @return array 39 | */ 40 | public function toArray(): array 41 | { 42 | return $this->filters ?? []; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Model/QueryParameterPageModel.php: -------------------------------------------------------------------------------- 1 | $page 11 | */ 12 | private ?array $page = null; 13 | 14 | /** 15 | * @param array|null $page 16 | */ 17 | public function __construct(?array $page) 18 | { 19 | $this->page = $page; 20 | } 21 | 22 | public function getNumber(): int 23 | { 24 | return true === isset($this->page['number']) ? intval($this->page['number']) : 1; 25 | } 26 | 27 | public function getSize(): int 28 | { 29 | return true === isset($this->page['size']) ? intval($this->page['size']) : 20; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Model/QueryParameterSortModel.php: -------------------------------------------------------------------------------- 1 | sort = $sort; 14 | } 15 | 16 | public function raw(): ?string 17 | { 18 | return $this->sort; 19 | } 20 | 21 | /** 22 | * @return array 23 | */ 24 | public function get(): ?array 25 | { 26 | if (null === $this->sort) { 27 | return null; 28 | } 29 | 30 | $criterias = []; 31 | $values = explode(',', urldecode($this->sort)); 32 | foreach ($values as $value) { 33 | $criteria = []; 34 | $value = trim($value); 35 | if (str_starts_with($value, '-')) { 36 | $criteria['direction'] = 'DESC'; 37 | $criteria['field'] = substr($value, 1); 38 | } else { 39 | $criteria['direction'] = 'ASC'; 40 | $criteria['field'] = $value; 41 | } 42 | $criterias[] = $criteria; 43 | } 44 | 45 | return $criterias[0] ?? null; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Repository/AbstractRepository.php: -------------------------------------------------------------------------------- 1 | getEntityClass()); 23 | 24 | $em = $this->getEntityManager(); 25 | $this->connection = $em->getConnection(); 26 | } 27 | 28 | public function getConnection(): Connection 29 | { 30 | return $this->connection; 31 | } 32 | 33 | /** 34 | * @param array $fields 35 | */ 36 | public function insert(string $table, array $fields): int 37 | { 38 | $this->connection->insert($table, $fields); 39 | return intval($this->connection->lastInsertId()); 40 | } 41 | 42 | /** 43 | * @param array $fields 44 | */ 45 | public function update(string $table, array $fields, int $id): void 46 | { 47 | $this->connection->update($table, $fields, ['id' => $id]); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Repository/ActionRepository.php: -------------------------------------------------------------------------------- 1 | $parameters 19 | */ 20 | public function getOne(array $parameters = []): ?Action 21 | { 22 | $em = $this->getEntityManager(); 23 | 24 | $query = $em->createQueryBuilder(); 25 | $query->addSelect('act'); 26 | $query->from(Action::class, 'act'); 27 | 28 | if (true === isset($parameters['id'])) { 29 | $query->andWhere('act.id = :id'); 30 | $query->setParameter(':id', $parameters['id']); 31 | } 32 | 33 | if (true === isset($parameters['title'])) { 34 | $query->andWhere('act.title = :title'); 35 | $query->setParameter(':title', $parameters['title']); 36 | } 37 | 38 | $getQuery = $query->getQuery(); 39 | $getQuery->setMaxResults(1); 40 | 41 | return $getQuery->getOneOrNullResult(); 42 | } 43 | 44 | /** 45 | * @param array $parameters 46 | */ 47 | public function getList(array $parameters = []): mixed 48 | { 49 | $em = $this->getEntityManager(); 50 | 51 | $query = $em->createQueryBuilder(); 52 | $query->addSelect('act'); 53 | $query->from(Action::class, 'act'); 54 | 55 | if (true === isset($parameters['id'])) { 56 | $query->andWhere('act.id = :id'); 57 | $query->setParameter(':id', $parameters['id']); 58 | } 59 | 60 | $query->groupBy('act.id'); 61 | 62 | $getQuery = $query->getQuery(); 63 | return $getQuery; 64 | } 65 | 66 | public function persist(Action $action, bool $flush = true): void 67 | { 68 | $this->getEntityManager()->persist($action); 69 | 70 | if (true === $flush) { 71 | $this->getEntityManager()->flush(); 72 | } 73 | } 74 | 75 | public function remove(Action $action, bool $flush = true): void 76 | { 77 | $this->getEntityManager()->remove($action); 78 | 79 | if (true === $flush) { 80 | $this->getEntityManager()->flush(); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Repository/CollectionRepository.php: -------------------------------------------------------------------------------- 1 | $parameters 19 | */ 20 | public function getOne(array $parameters = []): ?Collection 21 | { 22 | $em = $this->getEntityManager(); 23 | 24 | $query = $em->createQueryBuilder(); 25 | $query->addSelect('col'); 26 | $query->from(Collection::class, 'col'); 27 | 28 | if (true === isset($parameters['id'])) { 29 | $query->andWhere('col.id = :id'); 30 | $query->setParameter(':id', $parameters['id']); 31 | } 32 | 33 | $getQuery = $query->getQuery(); 34 | $getQuery->setMaxResults(1); 35 | 36 | return $getQuery->getOneOrNullResult(); 37 | } 38 | 39 | /** 40 | * @param array $parameters 41 | */ 42 | public function getList(array $parameters = []): mixed 43 | { 44 | $em = $this->getEntityManager(); 45 | 46 | $query = $em->createQueryBuilder(); 47 | $query->addSelect('col'); 48 | $query->from(Collection::class, 'col'); 49 | 50 | if (true === isset($parameters['id'])) { 51 | $query->andWhere('col.id = :id'); 52 | $query->setParameter(':id', $parameters['id']); 53 | } 54 | 55 | $query->addOrderBy('col.id', 'DESC'); 56 | $query->groupBy('col.id'); 57 | 58 | $getQuery = $query->getQuery(); 59 | return $getQuery; 60 | } 61 | 62 | public function persist(Collection $collection, bool $flush = true): void 63 | { 64 | $this->getEntityManager()->persist($collection); 65 | 66 | if (true === $flush) { 67 | $this->getEntityManager()->flush(); 68 | } 69 | } 70 | 71 | public function remove(Collection $collection, bool $flush = true): void 72 | { 73 | $this->getEntityManager()->remove($collection); 74 | 75 | if (true === $flush) { 76 | $this->getEntityManager()->flush(); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Security/ApiAccessDeniedHandler.php: -------------------------------------------------------------------------------- 1 | connectionManager = $connectionManager; 19 | } 20 | 21 | public function getUserBadgeFrom(string $token): UserBadge 22 | { 23 | try { 24 | $payloadjwtPayloadModel = JwtHelper::getPayload($token); 25 | } catch (\Exception $e) { 26 | throw new BadCredentialsException('Invalid token.'); 27 | } 28 | 29 | if (null === $payloadjwtPayloadModel) { 30 | throw new BadCredentialsException('Invalid token.'); 31 | } 32 | 33 | $connection = $this->connectionManager->getOne(['type' => Connection::TYPE_LOGIN, 'token' => $payloadjwtPayloadModel->getJwtId()]); 34 | 35 | if (null === $connection) { 36 | throw new BadCredentialsException('Token not found.'); 37 | } 38 | 39 | if (null === $connection->getMember() || null === $connection->getMember()->getEmail()) { 40 | throw new BadCredentialsException('Member not found.'); 41 | } 42 | 43 | return new UserBadge($connection->getMember()->getEmail()); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Security/Voter/AbstractVoter.php: -------------------------------------------------------------------------------- 1 | security = $security; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Security/Voter/AuthorVoter.php: -------------------------------------------------------------------------------- 1 | getUser(); 20 | if (false === $user instanceof Member) { 21 | return false; 22 | } 23 | 24 | switch ($attribute) { 25 | case 'DELETE': 26 | return $this->security->isGranted('ROLE_ADMIN'); 27 | } 28 | 29 | return true; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Security/Voter/CategoryVoter.php: -------------------------------------------------------------------------------- 1 | getUser(); 20 | if (false === $user instanceof Member) { 21 | return false; 22 | } 23 | 24 | switch ($attribute) { 25 | case 'DELETE': 26 | return $this->security->isGranted('ROLE_ADMIN'); 27 | } 28 | 29 | return true; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Security/Voter/ConnectionVoter.php: -------------------------------------------------------------------------------- 1 | getUser(); 20 | if (false === $user instanceof Member) { 21 | return false; 22 | } 23 | 24 | return $this->security->isGranted('ROLE_ADMIN'); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Security/Voter/EnclosureVoter.php: -------------------------------------------------------------------------------- 1 | getUser(); 20 | if (false === $user instanceof Member) { 21 | return false; 22 | } 23 | 24 | switch ($attribute) { 25 | case 'DELETE': 26 | return $this->security->isGranted('ROLE_ADMIN'); 27 | } 28 | 29 | return true; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Security/Voter/FeedVoter.php: -------------------------------------------------------------------------------- 1 | getUser(); 20 | if (false === $user instanceof Member) { 21 | return false; 22 | } 23 | 24 | switch ($attribute) { 25 | case 'DELETE': 26 | return $this->security->isGranted('ROLE_ADMIN'); 27 | } 28 | 29 | return true; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Security/Voter/ItemVoter.php: -------------------------------------------------------------------------------- 1 | getUser(); 20 | if (false === $user instanceof Member) { 21 | return false; 22 | } 23 | 24 | switch ($attribute) { 25 | case 'DELETE': 26 | return $this->security->isGranted('ROLE_ADMIN'); 27 | } 28 | 29 | return true; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Security/Voter/MemberPasskeyVoter.php: -------------------------------------------------------------------------------- 1 | getUser(); 20 | if (false === $user instanceof Member) { 21 | return false; 22 | } 23 | 24 | switch ($attribute) { 25 | case 'DELETE': 26 | if ($user->getId() !== $subject->getMember()->getId()) { 27 | return false; 28 | } 29 | } 30 | 31 | return true; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Security/Voter/MemberVoter.php: -------------------------------------------------------------------------------- 1 | getUser(); 19 | if (false === $user instanceof Member) { 20 | return false; 21 | } 22 | 23 | switch ($attribute) { 24 | case 'DELETE': 25 | if ($user->getId() === $subject->getId() || true === $subject->getAdministrator()) { 26 | return false; 27 | } 28 | } 29 | 30 | return $this->security->isGranted('ROLE_ADMIN'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /templates/base.html.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Feed 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |
23 |
24 |
25 | 26 | 48 | 49 |
50 |
51 |
52 |
Loading...
53 |
54 |
55 |
56 | 57 | {% block javascripts %} 58 | 61 | {% block importmap %}{{ importmap('app') }}{% endblock %} 62 | {% endblock %} 63 | 64 | 65 | -------------------------------------------------------------------------------- /tests/ControllerAsAnonymous/AuthorControllerTest.php: -------------------------------------------------------------------------------- 1 | client = static::createClient(); 15 | } 16 | 17 | public function testIndex(): void 18 | { 19 | $this->client->request('GET', '/api/authors'); 20 | 21 | $this->assertResponseStatusCodeSame(401); 22 | } 23 | 24 | public function testCreate(): void 25 | { 26 | $this->client->request('POST', '/api/authors'); 27 | 28 | $this->assertResponseStatusCodeSame(401); 29 | } 30 | 31 | public function testRead(): void 32 | { 33 | $this->client->request('GET', '/api/author/0'); 34 | 35 | $this->assertResponseStatusCodeSame(401); 36 | } 37 | 38 | public function testUpdate(): void 39 | { 40 | $this->client->request('PUT', '/api/author/0'); 41 | 42 | $this->assertResponseStatusCodeSame(401); 43 | } 44 | 45 | public function testDelete(): void 46 | { 47 | $this->client->request('DELETE', '/api/author/0'); 48 | 49 | $this->assertResponseStatusCodeSame(401); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/ControllerAsAnonymous/CategoryControllerTest.php: -------------------------------------------------------------------------------- 1 | client = static::createClient(); 15 | } 16 | 17 | public function testIndex(): void 18 | { 19 | $this->client->request('GET', '/api/categories'); 20 | 21 | $this->assertResponseStatusCodeSame(401); 22 | } 23 | 24 | public function testCreate(): void 25 | { 26 | $this->client->request('POST', '/api/categories'); 27 | 28 | $this->assertResponseStatusCodeSame(401); 29 | } 30 | 31 | public function testRead(): void 32 | { 33 | $this->client->request('GET', '/api/category/0'); 34 | 35 | $this->assertResponseStatusCodeSame(401); 36 | } 37 | 38 | public function testUpdate(): void 39 | { 40 | $this->client->request('PUT', '/api/category/0'); 41 | 42 | $this->assertResponseStatusCodeSame(401); 43 | } 44 | 45 | public function testDelete(): void 46 | { 47 | $this->client->request('DELETE', '/api/category/0'); 48 | 49 | $this->assertResponseStatusCodeSame(401); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/ControllerAsAnonymous/ConnectionControllerTest.php: -------------------------------------------------------------------------------- 1 | client = static::createClient(); 15 | } 16 | 17 | public function testIndex(): void 18 | { 19 | $this->client->request('GET', '/api/connections'); 20 | 21 | $this->assertResponseStatusCodeSame(401); 22 | } 23 | 24 | public function testDelete(): void 25 | { 26 | $this->client->request('DELETE', '/api/connection/0'); 27 | 28 | $this->assertResponseStatusCodeSame(401); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/ControllerAsAnonymous/EnclosureControllerTest.php: -------------------------------------------------------------------------------- 1 | client = static::createClient(); 15 | } 16 | 17 | public function testIndex(): void 18 | { 19 | $this->client->request('GET', '/api/enclosures'); 20 | 21 | $this->assertResponseStatusCodeSame(401); 22 | } 23 | 24 | public function testRead(): void 25 | { 26 | $this->client->request('GET', '/api/enclosure/0'); 27 | 28 | $this->assertResponseStatusCodeSame(401); 29 | } 30 | 31 | public function testDelete(): void 32 | { 33 | $this->client->request('DELETE', '/api/enclosure/0'); 34 | 35 | $this->assertResponseStatusCodeSame(401); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/ControllerAsAnonymous/FeedControllerTest.php: -------------------------------------------------------------------------------- 1 | client = static::createClient(); 15 | } 16 | 17 | public function testIndex(): void 18 | { 19 | $this->client->request('GET', '/api/feeds'); 20 | 21 | $this->assertResponseStatusCodeSame(401); 22 | } 23 | 24 | public function testCreate(): void 25 | { 26 | $this->client->request('POST', '/api/feeds'); 27 | 28 | $this->assertResponseStatusCodeSame(401); 29 | } 30 | 31 | public function testRead(): void 32 | { 33 | $this->client->request('GET', '/api/feed/0'); 34 | 35 | $this->assertResponseStatusCodeSame(401); 36 | } 37 | 38 | public function testUpdate(): void 39 | { 40 | $this->client->request('PUT', '/api/feed/0'); 41 | 42 | $this->assertResponseStatusCodeSame(401); 43 | } 44 | 45 | public function testDelete(): void 46 | { 47 | $this->client->request('DELETE', '/api/feed/0'); 48 | 49 | $this->assertResponseStatusCodeSame(401); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/ControllerAsAnonymous/ItemControllerTest.php: -------------------------------------------------------------------------------- 1 | client = static::createClient(); 15 | } 16 | 17 | public function testIndex(): void 18 | { 19 | $this->client->request('GET', '/api/items'); 20 | 21 | $this->assertResponseStatusCodeSame(401); 22 | } 23 | 24 | public function testRead(): void 25 | { 26 | $this->client->request('GET', '/api/item/0'); 27 | 28 | $this->assertResponseStatusCodeSame(401); 29 | } 30 | 31 | public function testDelete(): void 32 | { 33 | $this->client->request('DELETE', '/api/item/0'); 34 | 35 | $this->assertResponseStatusCodeSame(401); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/ControllerAsAnonymous/MemberPasskeyControllerTest.php: -------------------------------------------------------------------------------- 1 | client = static::createClient(); 15 | } 16 | 17 | public function testIndex(): void 18 | { 19 | $this->client->request('GET', '/api/passkeys'); 20 | 21 | $this->assertResponseStatusCodeSame(401); 22 | } 23 | 24 | public function testDelete(): void 25 | { 26 | $this->client->request('DELETE', '/api/passkey/0'); 27 | 28 | $this->assertResponseStatusCodeSame(401); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/ControllerAsAnonymous/ProfileControllerTest.php: -------------------------------------------------------------------------------- 1 | client = static::createClient(); 15 | } 16 | 17 | public function testIndex(): void 18 | { 19 | $this->client->request('GET', '/api/profile'); 20 | 21 | $this->assertResponseStatusCodeSame(401); 22 | } 23 | 24 | public function testConnections(): void 25 | { 26 | $this->client->request('GET', '/api/profile/connections'); 27 | 28 | $this->assertResponseStatusCodeSame(401); 29 | } 30 | 31 | public function testPasskeys(): void 32 | { 33 | $this->client->request('GET', '/api/profile/passkeys'); 34 | 35 | $this->assertResponseStatusCodeSame(401); 36 | } 37 | 38 | public function testUpdate(): void 39 | { 40 | $this->client->request('PUT', '/api/profile'); 41 | 42 | $this->assertResponseStatusCodeSame(401); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/ControllerAsConnected/AbstractControllerTest.php: -------------------------------------------------------------------------------- 1 | get(ConnectionManager::class); 22 | $testConnection = $connectionManager->getOne([]); 23 | 24 | $headers = [ 25 | 'CONTENT_TYPE' => 'application/json', 26 | ]; 27 | 28 | if ($testConnection) { 29 | $jwtPayloadModel = new JwtPayloadModel(); 30 | $jwtPayloadModel->setJwtId($testConnection->getToken()); 31 | $jwtPayloadModel->setAudience(strval($testConnection->getMember()->getId())); 32 | 33 | $headers['HTTP_AUTHORIZATION'] = 'Bearer '.JwtHelper::createToken($jwtPayloadModel); 34 | } 35 | 36 | self::ensureKernelShutdown(); 37 | $this->client = static::createClient([], $headers); 38 | } 39 | 40 | protected function isValidResponseString(string $json): bool 41 | { 42 | $isValidResponseString = Parser::isValidResponseString($json); 43 | 44 | if (false === $isValidResponseString) { 45 | try { 46 | // Use this if you have a response after calling a JSON API server 47 | Parser::parseResponseString($json); 48 | } catch (InputException $e) { 49 | // $jsonapiString is not valid JSON 50 | dump($e->getMessage()); 51 | } catch (ValidationException $e) { 52 | // $jsonapiString is not valid JSON API 53 | dump($e->getMessage()); 54 | } 55 | } 56 | 57 | return $isValidResponseString; 58 | } 59 | 60 | protected function retrieveOneId(string $path): ?int 61 | { 62 | $this->client->request('GET', $path); 63 | $json = $this->client->getResponse()->getContent(); 64 | $content = json_decode($json, true); 65 | 66 | if (true === isset($content['data']) && 0 < count($content['data'])) { 67 | return intval($content['data'][0]['id']); 68 | } 69 | 70 | return null; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /tests/ControllerAsConnected/EnclosureControllerTest.php: -------------------------------------------------------------------------------- 1 | client->request('GET', '/api/enclosures'); 12 | $json = $this->client->getResponse()->getContent(); 13 | $isValidResponseString = $this->isValidResponseString($json); 14 | 15 | $this->assertTrue($isValidResponseString); 16 | $this->assertResponseStatusCodeSame(200); 17 | } 18 | 19 | public function testRead404(): void 20 | { 21 | $this->client->request('GET', '/api/enclosure/0'); 22 | $json = $this->client->getResponse()->getContent(); 23 | $isValidResponseString = $this->isValidResponseString($json); 24 | $content = json_decode($json, true); 25 | 26 | $this->assertTrue($isValidResponseString); 27 | $this->assertResponseStatusCodeSame(404); 28 | $this->assertEquals('404', $content['errors'][0]['status']); 29 | $this->assertEquals('Not Found', $content['errors'][0]['title']); 30 | } 31 | 32 | public function testRead(): void 33 | { 34 | if ($id = $this->retrieveOneId('/api/enclosures')) { 35 | $this->client->request('GET', '/api/enclosure/'.$id); 36 | $json = $this->client->getResponse()->getContent(); 37 | $isValidResponseString = $this->isValidResponseString($json); 38 | $content = json_decode($json, true); 39 | 40 | $this->assertTrue($isValidResponseString); 41 | $this->assertResponseStatusCodeSame(200); 42 | $this->assertEquals('enclosure', $content['data']['type']); 43 | $this->assertEquals($id, $content['data']['id']); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/ControllerAsConnected/ItemControllerTest.php: -------------------------------------------------------------------------------- 1 | client->request('GET', '/api/items'); 12 | $json = $this->client->getResponse()->getContent(); 13 | $isValidResponseString = $this->isValidResponseString($json); 14 | 15 | $this->assertTrue($isValidResponseString); 16 | $this->assertResponseStatusCodeSame(200); 17 | } 18 | 19 | public function testRead404(): void 20 | { 21 | $this->client->request('GET', '/api/item/0'); 22 | $json = $this->client->getResponse()->getContent(); 23 | $isValidResponseString = $this->isValidResponseString($json); 24 | $content = json_decode($json, true); 25 | 26 | $this->assertTrue($isValidResponseString); 27 | $this->assertResponseStatusCodeSame(404); 28 | $this->assertEquals('404', $content['errors'][0]['status']); 29 | $this->assertEquals('Not Found', $content['errors'][0]['title']); 30 | } 31 | 32 | public function testRead(): void 33 | { 34 | if ($id = $this->retrieveOneId('/api/items')) { 35 | $this->client->request('GET', '/api/item/'.$id); 36 | $json = $this->client->getResponse()->getContent(); 37 | $isValidResponseString = $this->isValidResponseString($json); 38 | $content = json_decode($json, true); 39 | 40 | $this->assertTrue($isValidResponseString); 41 | $this->assertResponseStatusCodeSame(200); 42 | $this->assertEquals('item', $content['data']['type']); 43 | $this->assertEquals($id, $content['data']['id']); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/Entity/ActionTest.php: -------------------------------------------------------------------------------- 1 | entityManager = $kernel->getContainer()->get('doctrine')->getManager(); 18 | } 19 | 20 | public function testToString() 21 | { 22 | $this->assertTrue(method_exists(new Action(), '__toString'), 'method __toString missing'); 23 | } 24 | 25 | public function testPersist() 26 | { 27 | $value = 'test-'.uniqid(); 28 | 29 | //add action 30 | $action = new Action(); 31 | $action->setTitle($value); 32 | 33 | $this->entityManager->persist($action); 34 | $this->entityManager->flush(); 35 | 36 | //remove 37 | $this->entityManager->remove($action); 38 | $this->entityManager->flush(); 39 | 40 | $this->assertTrue(true); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/Entity/AuthorTest.php: -------------------------------------------------------------------------------- 1 | entityManager = $kernel->getContainer()->get('doctrine')->getManager(); 20 | } 21 | 22 | public function testToString() 23 | { 24 | $this->assertTrue(method_exists(new Author(), '__toString'), 'method __toString missing'); 25 | } 26 | 27 | public function testPersist() 28 | { 29 | $value = 'test-'.uniqid(); 30 | 31 | //add action 32 | $action = new Action(); 33 | $action->setTitle($value); 34 | 35 | $this->entityManager->persist($action); 36 | $this->entityManager->flush(); 37 | 38 | //add author 39 | $author = new Author(); 40 | $author->setTitle($value); 41 | 42 | //add action author (cascade persist) 43 | $actionAuthor = new ActionAuthor(); 44 | $actionAuthor->setAction($action); 45 | $author->addAction($actionAuthor); 46 | $this->assertTrue($author->hasAction($actionAuthor)); 47 | 48 | $this->entityManager->persist($author); 49 | $this->entityManager->flush(); 50 | 51 | //remove (orphan removal) 52 | $author->removeAction($actionAuthor); 53 | $this->assertFalse($author->hasAction($actionAuthor)); 54 | 55 | $this->entityManager->persist($author); 56 | $this->entityManager->flush(); 57 | 58 | //remove 59 | $this->entityManager->remove($author); 60 | $this->entityManager->remove($action); 61 | $this->entityManager->flush(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/Entity/CategoryTest.php: -------------------------------------------------------------------------------- 1 | entityManager = $kernel->getContainer()->get('doctrine')->getManager(); 20 | } 21 | 22 | public function testToString() 23 | { 24 | $this->assertTrue(method_exists(new Category(), '__toString'), 'method __toString missing'); 25 | } 26 | 27 | public function testPersist() 28 | { 29 | $value = 'test-'.uniqid(); 30 | 31 | //add action 32 | $action = new Action(); 33 | $action->setTitle($value); 34 | 35 | $this->entityManager->persist($action); 36 | $this->entityManager->flush(); 37 | 38 | //add category 39 | $category = new Category(); 40 | $category->setTitle($value); 41 | 42 | //add action category (cascade persist) 43 | $actionCategory = new ActionCategory(); 44 | $actionCategory->setAction($action); 45 | $category->addAction($actionCategory); 46 | $this->assertTrue($category->hasAction($actionCategory)); 47 | 48 | $this->entityManager->persist($category); 49 | $this->entityManager->flush(); 50 | 51 | //remove (orphan removal) 52 | $category->removeAction($actionCategory); 53 | $this->assertFalse($category->hasAction($actionCategory)); 54 | 55 | $this->entityManager->persist($category); 56 | $this->entityManager->flush(); 57 | 58 | //remove 59 | $this->entityManager->remove($category); 60 | $this->entityManager->remove($action); 61 | $this->entityManager->flush(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/Entity/FeedTest.php: -------------------------------------------------------------------------------- 1 | entityManager = $kernel->getContainer()->get('doctrine')->getManager(); 22 | } 23 | 24 | public function testToString() 25 | { 26 | $this->assertTrue(method_exists(new Feed(), '__toString'), 'method __toString missing'); 27 | } 28 | 29 | public function testPersist() 30 | { 31 | $value = 'test-'.uniqid(); 32 | 33 | //add action 34 | $action = new Action(); 35 | $action->setTitle($value); 36 | 37 | $this->entityManager->persist($action); 38 | $this->entityManager->flush(); 39 | 40 | //add category 41 | $category = new Category(); 42 | $category->setTitle($value); 43 | 44 | $this->entityManager->persist($category); 45 | $this->entityManager->flush(); 46 | 47 | //add feed 48 | $feed = new Feed(); 49 | $feed->setTitle($value); 50 | $feed->setLink($value); 51 | 52 | //add action feed (cascade persist) 53 | $actionFeed = new ActionFeed(); 54 | $actionFeed->setAction($action); 55 | $feed->addAction($actionFeed); 56 | $this->assertTrue($feed->hasAction($actionFeed)); 57 | 58 | //add feed category (cascade persist) 59 | $feedCategory = new FeedCategory(); 60 | $feedCategory->setCategory($category); 61 | $feed->addCategory($feedCategory); 62 | $this->assertTrue($feed->hasCategory($feedCategory)); 63 | 64 | $this->entityManager->persist($feed); 65 | $this->entityManager->flush(); 66 | 67 | //remove (orphan removal) 68 | $feed->removeAction($actionFeed); 69 | $this->assertFalse($feed->hasAction($actionFeed)); 70 | 71 | $feed->removeCategory($feedCategory); 72 | $this->assertFalse($feed->hasCategory($feedCategory)); 73 | 74 | $this->entityManager->persist($feed); 75 | $this->entityManager->flush(); 76 | 77 | //remove 78 | $this->entityManager->remove($feed); 79 | $this->entityManager->remove($action); 80 | $this->entityManager->remove($category); 81 | $this->entityManager->flush(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /tests/Manager/ActionAuthorManagerTest.php: -------------------------------------------------------------------------------- 1 | actionManager = static::getContainer()->get('App\Manager\ActionManager'); 28 | 29 | $this->authorManager = static::getContainer()->get('App\Manager\AuthorManager'); 30 | 31 | $this->actionAuthorManager = static::getContainer()->get('App\Manager\ActionAuthorManager'); 32 | } 33 | 34 | public function testPersist(): void 35 | { 36 | $action = new Action(); 37 | $action->setTitle(uniqid('phpunit-')); 38 | 39 | $this->actionManager->persist($action); 40 | 41 | $author = new Author(); 42 | $author->setTitle(uniqid('phpunit-')); 43 | 44 | $this->authorManager->persist($author); 45 | 46 | $actionAuthor = new ActionAuthor(); 47 | $actionAuthor->setAction($action); 48 | $actionAuthor->setAuthor($author); 49 | 50 | $this->actionAuthorManager->persist($actionAuthor); 51 | 52 | $this->assertIsInt($actionAuthor->getId()); 53 | 54 | $this->actionAuthorManager->remove($actionAuthor); 55 | 56 | $this->authorManager->remove($author); 57 | 58 | $this->actionManager->remove($action); 59 | } 60 | 61 | public function testGetOne(): void 62 | { 63 | $test = $this->actionAuthorManager->getOne(['id' => 0]); 64 | $this->assertNull($test); 65 | } 66 | 67 | public function testGetList(): void 68 | { 69 | $test = $this->actionAuthorManager->getList(['id' => 0])->getResult(); 70 | $this->assertIsArray($test); 71 | $this->assertCount(0, $test); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tests/Manager/ActionCategoryManagerTest.php: -------------------------------------------------------------------------------- 1 | actionManager = static::getContainer()->get('App\Manager\ActionManager'); 28 | 29 | $this->categoryManager = static::getContainer()->get('App\Manager\CategoryManager'); 30 | 31 | $this->actionCategoryManager = static::getContainer()->get('App\Manager\ActionCategoryManager'); 32 | } 33 | 34 | public function testPersist(): void 35 | { 36 | $action = new Action(); 37 | $action->setTitle(uniqid('phpunit-')); 38 | 39 | $this->actionManager->persist($action); 40 | 41 | $category = new Category(); 42 | $category->setTitle(uniqid('phpunit-')); 43 | 44 | $this->categoryManager->persist($category); 45 | 46 | $actionCategory = new ActionCategory(); 47 | $actionCategory->setAction($action); 48 | $actionCategory->setCategory($category); 49 | 50 | $this->actionCategoryManager->persist($actionCategory); 51 | 52 | $this->assertIsInt($actionCategory->getId()); 53 | 54 | $this->actionCategoryManager->remove($actionCategory); 55 | 56 | $this->categoryManager->remove($category); 57 | 58 | $this->actionManager->remove($action); 59 | } 60 | 61 | public function testGetOne(): void 62 | { 63 | $test = $this->actionCategoryManager->getOne(['id' => 0]); 64 | $this->assertNull($test); 65 | } 66 | 67 | public function testGetList(): void 68 | { 69 | $test = $this->actionCategoryManager->getList(['id' => 0])->getResult(); 70 | $this->assertIsArray($test); 71 | $this->assertCount(0, $test); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tests/Manager/ActionFeedManagerTest.php: -------------------------------------------------------------------------------- 1 | actionManager = static::getContainer()->get('App\Manager\ActionManager'); 28 | 29 | $this->feedManager = static::getContainer()->get('App\Manager\FeedManager'); 30 | 31 | $this->actionFeedManager = static::getContainer()->get('App\Manager\ActionFeedManager'); 32 | } 33 | 34 | public function testPersist(): void 35 | { 36 | $action = new Action(); 37 | $action->setTitle(uniqid('phpunit-')); 38 | 39 | $this->actionManager->persist($action); 40 | 41 | $feed = new Feed(); 42 | $feed->setTitle(uniqid('phpunit-')); 43 | $feed->setLink(uniqid('phpunit-')); 44 | 45 | $this->feedManager->persist($feed); 46 | 47 | $actionFeed = new ActionFeed(); 48 | $actionFeed->setAction($action); 49 | $actionFeed->setFeed($feed); 50 | 51 | $this->actionFeedManager->persist($actionFeed); 52 | 53 | $this->assertIsInt($actionFeed->getId()); 54 | 55 | $this->actionFeedManager->remove($actionFeed); 56 | 57 | $this->feedManager->remove($feed); 58 | 59 | $this->actionManager->remove($action); 60 | } 61 | 62 | public function testGetOne(): void 63 | { 64 | $test = $this->actionFeedManager->getOne(['id' => 0]); 65 | $this->assertNull($test); 66 | } 67 | 68 | public function testGetList(): void 69 | { 70 | $test = $this->actionFeedManager->getList(['id' => 0])->getResult(); 71 | $this->assertIsArray($test); 72 | $this->assertCount(0, $test); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tests/Manager/ActionManagerTest.php: -------------------------------------------------------------------------------- 1 | actionManager = static::getContainer()->get('App\Manager\ActionManager'); 20 | } 21 | 22 | public function testPersist(): void 23 | { 24 | $action = new Action(); 25 | $action->setTitle(uniqid('phpunit-')); 26 | 27 | $this->actionManager->persist($action); 28 | 29 | $this->assertIsInt($action->getId()); 30 | 31 | $this->actionManager->remove($action); 32 | } 33 | 34 | public function testGetOne(): void 35 | { 36 | $test = $this->actionManager->getOne(['id' => 0]); 37 | $this->assertNull($test); 38 | } 39 | 40 | public function testGetList(): void 41 | { 42 | $test = $this->actionManager->getList(['id' => 0])->getResult(); 43 | $this->assertIsArray($test); 44 | $this->assertCount(0, $test); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/Manager/AuthorManagerTest.php: -------------------------------------------------------------------------------- 1 | authorManager = static::getContainer()->get('App\Manager\AuthorManager'); 20 | } 21 | 22 | public function testPersist(): void 23 | { 24 | $author = new Author(); 25 | $author->setTitle(uniqid('phpunit-')); 26 | 27 | $this->authorManager->persist($author); 28 | 29 | $this->assertIsInt($author->getId()); 30 | 31 | $this->authorManager->remove($author); 32 | } 33 | 34 | public function testGetOne(): void 35 | { 36 | $test = $this->authorManager->getOne(['id' => 0]); 37 | $this->assertNull($test); 38 | } 39 | 40 | public function testGetList(): void 41 | { 42 | $test = $this->authorManager->getList(['id' => 0, 'sortField' => 'aut.id', 'sortDirection' => 'ASC'])->getResult(); 43 | $this->assertIsArray($test); 44 | $this->assertCount(0, $test); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/Manager/CategoryManagerTest.php: -------------------------------------------------------------------------------- 1 | categoryManager = static::getContainer()->get('App\Manager\CategoryManager'); 20 | } 21 | 22 | public function testPersist(): void 23 | { 24 | $category = new Category(); 25 | $category->setTitle(uniqid('phpunit-')); 26 | 27 | $this->categoryManager->persist($category); 28 | 29 | $this->assertIsInt($category->getId()); 30 | 31 | $this->categoryManager->remove($category); 32 | } 33 | 34 | public function testGetOne(): void 35 | { 36 | $test = $this->categoryManager->getOne(['id' => 0]); 37 | $this->assertNull($test); 38 | } 39 | 40 | public function testGetList(): void 41 | { 42 | $test = $this->categoryManager->getList(['id' => 0, 'sortField' => 'cat.id', 'sortDirection' => 'ASC'])->getResult(); 43 | $this->assertIsArray($test); 44 | $this->assertCount(0, $test); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/Manager/CollectionFeedManagerTest.php: -------------------------------------------------------------------------------- 1 | collectionManager = static::getContainer()->get('App\Manager\CollectionManager'); 28 | 29 | $this->feedManager = static::getContainer()->get('App\Manager\FeedManager'); 30 | 31 | $this->collectionFeedManager = static::getContainer()->get('App\Manager\CollectionFeedManager'); 32 | } 33 | 34 | public function testPersist(): void 35 | { 36 | $collection = new Collection(); 37 | 38 | $this->collectionManager->persist($collection); 39 | 40 | $feed = new Feed(); 41 | $feed->setTitle(uniqid('phpunit-')); 42 | $feed->setLink(uniqid('phpunit-')); 43 | 44 | $this->feedManager->persist($feed); 45 | 46 | $collectionFeed = new CollectionFeed(); 47 | $collectionFeed->setCollection($collection); 48 | $collectionFeed->setFeed($feed); 49 | 50 | $this->collectionFeedManager->persist($collectionFeed); 51 | 52 | $this->assertIsInt($collectionFeed->getId()); 53 | 54 | $this->collectionFeedManager->remove($collectionFeed); 55 | 56 | $this->feedManager->remove($feed); 57 | 58 | $this->collectionManager->remove($collection); 59 | } 60 | 61 | public function testGetOne(): void 62 | { 63 | $test = $this->collectionFeedManager->getOne(['id' => 0]); 64 | $this->assertNull($test); 65 | } 66 | 67 | public function testGetList(): void 68 | { 69 | $test = $this->collectionFeedManager->getList(['id' => 0])->getResult(); 70 | $this->assertIsArray($test); 71 | $this->assertCount(0, $test); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tests/Manager/CollectionManagerTest.php: -------------------------------------------------------------------------------- 1 | collectionManager = static::getContainer()->get('App\Manager\CollectionManager'); 20 | } 21 | 22 | public function testPersist(): void 23 | { 24 | $collection = new Collection(); 25 | 26 | $this->collectionManager->persist($collection); 27 | 28 | $this->assertIsInt($collection->getId()); 29 | 30 | $this->collectionManager->remove($collection); 31 | } 32 | 33 | public function testGetOne(): void 34 | { 35 | $test = $this->collectionManager->getOne(['id' => 0]); 36 | $this->assertNull($test); 37 | } 38 | 39 | public function testGetList(): void 40 | { 41 | $test = $this->collectionManager->getList(['id' => 0])->getResult(); 42 | $this->assertIsArray($test); 43 | $this->assertCount(0, $test); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/Manager/ConnectionManagerTest.php: -------------------------------------------------------------------------------- 1 | memberManager = static::getContainer()->get('App\Manager\MemberManager'); 24 | 25 | $this->connectionManager = static::getContainer()->get('App\Manager\ConnectionManager'); 26 | } 27 | 28 | public function testPersist(): void 29 | { 30 | $member = new Member(); 31 | $member->setEmail(uniqid('phpunit-')); 32 | $member->setPassword(uniqid('phpunit-')); 33 | 34 | $this->memberManager->persist($member); 35 | 36 | $connection = new Connection(); 37 | $connection->setMember($member); 38 | $connection->setType(uniqid('phpunit-')); 39 | $connection->setToken(uniqid('phpunit-')); 40 | 41 | $this->connectionManager->persist($connection); 42 | 43 | $this->assertIsInt($connection->getId()); 44 | 45 | $this->connectionManager->remove($connection); 46 | 47 | $this->memberManager->remove($member); 48 | } 49 | 50 | public function testGetOne(): void 51 | { 52 | $test = $this->connectionManager->getOne(['id' => 0]); 53 | $this->assertNull($test); 54 | } 55 | 56 | public function testGetList(): void 57 | { 58 | $test = $this->connectionManager->getList(['id' => 0])->getResult(); 59 | $this->assertIsArray($test); 60 | $this->assertCount(0, $test); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/Manager/EnclosureManagerTest.php: -------------------------------------------------------------------------------- 1 | feedManager = static::getContainer()->get('App\Manager\FeedManager'); 28 | 29 | $this->itemManager = static::getContainer()->get('App\Manager\ItemManager'); 30 | 31 | $this->enclosureManager = static::getContainer()->get('App\Manager\EnclosureManager'); 32 | } 33 | 34 | public function testPersist(): void 35 | { 36 | $feed = new Feed(); 37 | $feed->setTitle(uniqid('phpunit-')); 38 | $feed->setLink(uniqid('phpunit-')); 39 | 40 | $this->feedManager->persist($feed); 41 | 42 | $item = new Item(); 43 | $item->setFeed($feed); 44 | $item->setTitle(uniqid('phpunit-')); 45 | $item->setLink(uniqid('phpunit-')); 46 | $item->setDate(new \Datetime()); 47 | 48 | $this->itemManager->persist($item); 49 | 50 | $enclosure = new Enclosure(); 51 | $enclosure->setItem($item); 52 | $enclosure->setLink(uniqid('phpunit-')); 53 | $enclosure->setType(uniqid('phpunit-')); 54 | 55 | $this->enclosureManager->persist($enclosure); 56 | 57 | $this->assertIsInt($enclosure->getId()); 58 | 59 | $this->enclosureManager->remove($enclosure); 60 | 61 | $this->itemManager->remove($item); 62 | 63 | $this->feedManager->remove($feed); 64 | } 65 | 66 | public function testGetOne(): void 67 | { 68 | $test = $this->enclosureManager->getOne(['id' => 0]); 69 | $this->assertNull($test); 70 | } 71 | 72 | public function testGetList(): void 73 | { 74 | $test = $this->enclosureManager->getList(['id' => 0])->getResult(); 75 | $this->assertIsArray($test); 76 | $this->assertCount(0, $test); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /tests/Manager/FeedCategoryManagerTest.php: -------------------------------------------------------------------------------- 1 | categoryManager = static::getContainer()->get('App\Manager\CategoryManager'); 28 | 29 | $this->feedManager = static::getContainer()->get('App\Manager\FeedManager'); 30 | 31 | $this->feedCategoryManager = static::getContainer()->get('App\Manager\FeedCategoryManager'); 32 | } 33 | 34 | public function testPersist(): void 35 | { 36 | $category = new Category(); 37 | $category->setTitle(uniqid('phpunit-')); 38 | 39 | $this->categoryManager->persist($category); 40 | 41 | $feed = new Feed(); 42 | $feed->setTitle(uniqid('phpunit-')); 43 | $feed->setLink(uniqid('phpunit-')); 44 | 45 | $this->feedManager->persist($feed); 46 | 47 | $feedCategory = new FeedCategory(); 48 | $feedCategory->setCategory($category); 49 | $feedCategory->setFeed($feed); 50 | 51 | $this->feedCategoryManager->persist($feedCategory); 52 | 53 | $this->assertIsInt($feedCategory->getId()); 54 | 55 | $this->feedCategoryManager->remove($feedCategory); 56 | 57 | $this->feedManager->remove($feed); 58 | 59 | $this->categoryManager->remove($category); 60 | } 61 | 62 | public function testGetOne(): void 63 | { 64 | $test = $this->feedCategoryManager->getOne(['id' => 0]); 65 | $this->assertNull($test); 66 | } 67 | 68 | public function testGetList(): void 69 | { 70 | $test = $this->feedCategoryManager->getList(['id' => 0])->getResult(); 71 | $this->assertIsArray($test); 72 | $this->assertCount(0, $test); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tests/Manager/FeedManagerTest.php: -------------------------------------------------------------------------------- 1 | feedManager = static::getContainer()->get('App\Manager\FeedManager'); 20 | } 21 | 22 | public function testPersist(): void 23 | { 24 | $feed = new Feed(); 25 | $feed->setTitle(uniqid('phpunit-')); 26 | $feed->setLink(uniqid('phpunit-')); 27 | 28 | $this->feedManager->persist($feed); 29 | 30 | $this->assertIsInt($feed->getId()); 31 | 32 | $this->feedManager->remove($feed); 33 | } 34 | 35 | public function testGetOne(): void 36 | { 37 | $test = $this->feedManager->getOne(['id' => 0]); 38 | $this->assertNull($test); 39 | } 40 | 41 | public function testGetList(): void 42 | { 43 | $test = $this->feedManager->getList(['id' => 0, 'sortField' => 'fed.id', 'sortDirection' => 'ASC'])->getResult(); 44 | $this->assertIsArray($test); 45 | $this->assertCount(0, $test); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/Manager/ItemManagerTest.php: -------------------------------------------------------------------------------- 1 | feedManager = static::getContainer()->get('App\Manager\FeedManager'); 24 | 25 | $this->itemManager = static::getContainer()->get('App\Manager\ItemManager'); 26 | } 27 | 28 | public function testPersist(): void 29 | { 30 | $feed = new Feed(); 31 | $feed->setTitle(uniqid('phpunit-')); 32 | $feed->setLink(uniqid('phpunit-')); 33 | 34 | $this->feedManager->persist($feed); 35 | 36 | $item = new Item(); 37 | $item->setFeed($feed); 38 | $item->setTitle(uniqid('phpunit-')); 39 | $item->setLink(uniqid('phpunit-')); 40 | $item->setDate(new \Datetime()); 41 | 42 | $this->itemManager->persist($item); 43 | 44 | $this->assertIsInt($item->getId()); 45 | 46 | $this->itemManager->remove($item); 47 | 48 | $this->feedManager->remove($feed); 49 | } 50 | 51 | public function testGetOne(): void 52 | { 53 | $test = $this->itemManager->getOne(['id' => 0]); 54 | $this->assertNull($test); 55 | } 56 | 57 | public function testGetList(): void 58 | { 59 | $test = $this->itemManager->getList(['id' => 0, 'sortField' => 'itm.id', 'sortDirection' => 'ASC'])->getResult(); 60 | $this->assertIsArray($test); 61 | $this->assertCount(0, $test); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/Manager/MemberManagerTest.php: -------------------------------------------------------------------------------- 1 | memberManager = static::getContainer()->get('App\Manager\MemberManager'); 20 | } 21 | 22 | public function testPersist(): void 23 | { 24 | $member = new Member(); 25 | $member->setEmail(uniqid('phpunit-')); 26 | $member->setPassword(uniqid('phpunit-')); 27 | 28 | $this->memberManager->persist($member); 29 | 30 | $this->assertIsInt($member->getId()); 31 | 32 | $this->memberManager->remove($member); 33 | } 34 | 35 | public function testGetOne(): void 36 | { 37 | $test = $this->memberManager->getOne(['id' => 0]); 38 | $this->assertNull($test); 39 | } 40 | 41 | public function testGetList(): void 42 | { 43 | $test = $this->memberManager->getList(['id' => 0])->getResult(); 44 | $this->assertIsArray($test); 45 | $this->assertCount(0, $test); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/Manager/MemberPasskeyManagerTest.php: -------------------------------------------------------------------------------- 1 | memberManager = static::getContainer()->get('App\Manager\MemberManager'); 24 | 25 | $this->memberPasskeyManager = static::getContainer()->get('App\Manager\MemberPasskeyManager'); 26 | } 27 | 28 | public function testPersist(): void 29 | { 30 | $member = new Member(); 31 | $member->setEmail(uniqid('phpunit-')); 32 | $member->setPassword(uniqid('phpunit-')); 33 | 34 | $this->memberManager->persist($member); 35 | 36 | $memberPasskey = new MemberPasskey(); 37 | $memberPasskey->setTitle(uniqid('phpunit-')); 38 | $memberPasskey->setCredentialId(uniqid('phpunit-')); 39 | $memberPasskey->setPublicKey(uniqid('phpunit-')); 40 | $memberPasskey->setMember($member); 41 | 42 | $this->memberPasskeyManager->persist($memberPasskey); 43 | 44 | $this->assertIsInt($memberPasskey->getId()); 45 | 46 | $this->memberPasskeyManager->remove($memberPasskey); 47 | 48 | $this->memberManager->remove($member); 49 | } 50 | 51 | public function testGetOne(): void 52 | { 53 | $test = $this->memberPasskeyManager->getOne(['id' => 0]); 54 | $this->assertNull($test); 55 | } 56 | 57 | public function testGetList(): void 58 | { 59 | $test = $this->memberPasskeyManager->getList(['id' => 0])->getResult(); 60 | $this->assertIsArray($test); 61 | $this->assertCount(0, $test); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | bootEnv(dirname(__DIR__).'/.env'); 9 | } 10 | -------------------------------------------------------------------------------- /translations/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephanediondev/feed/2f61d1922279d3d989f7b485e769b2b7da7275b2/translations/.gitignore -------------------------------------------------------------------------------- /update.sh: -------------------------------------------------------------------------------- 1 | git fetch origin 2 | git reset --hard origin/main 3 | 4 | composer install 5 | bin/console doctrine:migrations:migrate -n 6 | 7 | bin/console asset-map:compile 8 | 9 | sed -i "s/VERSION =.*/VERSION = '"$(date +%Y-%m-%d.%H-%M-%S)"';/g" public/serviceworker.js 10 | --------------------------------------------------------------------------------