├── .dockerignore ├── skins ├── test │ ├── templates │ │ ├── Page_Footer.html.twig │ │ ├── Page_Header.html.twig │ │ ├── System_Message.html.twig │ │ ├── Access_Denied.twig │ │ ├── Piwik.twig │ │ ├── Error_Page.html.twig │ │ ├── Warning_Page.html.twig │ │ ├── Page_Not_Found.html.twig │ │ ├── Membership_Application_Confirmation.html.twig │ │ ├── Form_Utils.twig │ │ ├── CreditCardPaymentNotification.twig │ │ ├── Donation_Cancellation_Confirmation.html.twig │ │ ├── Membership_Application_Cancellation_Confirmation.html.twig │ │ ├── Comment_List.html.twig │ │ ├── Comment_List.rss.twig │ │ └── Membership_Application.html.twig │ └── README.md └── laika │ └── templates │ ├── Error_Page.html.twig │ ├── Comment_List.html.twig │ ├── Contact_Form.html.twig │ ├── Donation_Form.html.twig │ ├── Frequent_Questions.html.twig │ ├── Funds_Usage.html.twig │ ├── Comment_Ticker.html.twig │ ├── System_Message.html.twig │ ├── Update_Address.html.twig │ ├── Donation_Confirmation.html.twig │ ├── Membership_Application.html.twig │ ├── Membership_Fee_Change.html.twig │ ├── AddressUpdateSuccess.html.twig │ ├── page_layouts │ ├── supporters.html.twig │ └── base.html.twig │ ├── Membership_Application_Confirmation.html.twig │ ├── Subscription_Confirmation.twig │ ├── Pattern_Library.html.twig │ ├── matomo.html.twig │ ├── Page_Not_Found.html.twig │ ├── Access_Denied.twig │ └── Comment_List.rss.twig ├── tests ├── Data │ ├── files │ │ ├── Banned_Cats.txt │ │ ├── addSubscriptionResponse.js │ │ └── emptyCommentList.rss │ ├── GeneratedMailTemplates │ │ ├── Contact_Confirm_to_User.txt │ │ ├── Admin_Moderation.donation.txt │ │ ├── Admin_Moderation.membership.txt │ │ ├── Subscription_Request.txt │ │ ├── Contact_Forward_to_Operator.txt │ │ ├── Subscription_Confirmation.txt │ │ ├── Donation_Cancellation_Confirmation.txt │ │ ├── Membership_Application_Cancellation_Confirmation.txt │ │ ├── Membership_Application_Confirmation.moderated_other_reason.txt │ │ ├── Membership_Application_Confirmation.moderated_amount_too_high.txt │ │ ├── Donation_Confirmation.direct_debit_unmoderated_recurring.txt │ │ ├── Donation_Confirmation.direct_debit_unmoderated_non_recurring.txt │ │ ├── Donation_Confirmation.deposit_unmoderated_recurring.txt │ │ ├── Donation_Confirmation.deposit_unmoderated_non_recurring.txt │ │ ├── Donation_Confirmation.paypal_unmoderated_non_recurring.txt │ │ ├── Donation_Confirmation.paypal_unmoderated_recurring.txt │ │ ├── Donation_Confirmation.sofort_unmoderated_non_recurring.txt │ │ ├── Donation_Confirmation.credit_card_unmoderated_recurring.txt │ │ ├── Donation_Confirmation.micropayment_unmoderated_recurring.txt │ │ ├── Membership_Application_Confirmation.bank_transfer_active_yearly.txt │ │ ├── Membership_Application_Confirmation.direct_debit_active_yearly.txt │ │ ├── Membership_Application_Confirmation.paypal_sustaining_monthly.txt │ │ ├── Membership_Application_Confirmation.direct_debit_sustaining_quarterly.txt │ │ └── Membership_Application_Confirmation.direct_debit_active_yearly_receipt_optout.txt │ └── ValidLocation.php ├── Fixtures │ ├── FakeDonationEvent.php │ ├── FakeMembershipEvent.php │ ├── FailedValidationResult.php │ ├── FeatureReaderStub.php │ ├── LoggerSpy.php │ ├── FakeEnvironmentSetup.php │ ├── EntityManagerSpy.php │ ├── FixedPaymentDelayCalculator.php │ ├── SuccessfulMembershipAuthorizer.php │ ├── MailContentProviderStub.php │ ├── LogWriterSpy.php │ ├── FixedTokenGenerator.php │ ├── FailingPayPalVerificationService.php │ ├── SucceedingEmailValidator.php │ ├── UrlAuthenticatorStub.php │ ├── ThrowingCampaignConfigurationLoader.php │ ├── FakeBucketLoggingEvent.php │ ├── FailingVerificationServiceFactory.php │ ├── SucceedingVerificationServiceFactory.php │ ├── FakeTranslator.php │ ├── ErrorThrowingTemplateBasedMailer.php │ ├── EventDispatcherSpy.php │ ├── FakeUrlGenerator.php │ ├── InMemoryTranslator.php │ ├── BucketLoggerSpy.php │ ├── DonationUrlAuthenticationLoaderStub.php │ ├── BucketSelectionSpy.php │ └── InMemoryTokenRepository.php ├── Unit │ ├── Factories │ │ ├── TwigFactoryTestImplementation.php │ │ └── TwigFactoryTest.php │ ├── Presentation │ │ └── HonorificsTest.php │ ├── Infrastructure │ │ ├── EventHandling │ │ │ ├── DonationEventEmitterTest.php │ │ │ └── MembershipEventEmitterTest.php │ │ └── Mail │ │ │ └── BasicMailSubjectRendererTest.php │ ├── Cli │ │ └── FeatureToggleParserTest.php │ └── BucketTesting │ │ └── BucketTest.php ├── EdgeToEdge │ ├── Routes │ │ ├── GetApplicationVarsTrait.php │ │ └── DefaultRouteTest.php │ ├── TrackingDataTest.php │ ├── BucketVariableTest.php │ └── MatomoTest.php ├── TestEnvironmentBootstrapper.php ├── RebuildDatabaseSchemaTrait.php ├── SchemaCreator.php └── Integration │ └── MailTemplateFilenameTraversableTest.php ├── .docker └── app │ ├── mailhog.ini │ └── msmtprc ├── web ├── skins │ └── README.md ├── res │ ├── img │ │ └── banner │ │ │ ├── banner1.gif │ │ │ ├── banner2.gif │ │ │ ├── banner3.gif │ │ │ └── banner4.gif │ ├── css │ │ └── asset_state.css │ └── js │ │ └── matomo-loader.js ├── robots.txt └── index.php ├── config ├── packages │ ├── prod │ │ ├── routing.yaml │ │ └── monolog.yaml │ ├── uat │ │ ├── routing.yaml │ │ └── monolog.yaml │ ├── test │ │ └── framework.yaml │ ├── dev │ │ └── monolog.yaml │ ├── routing.yaml │ ├── cache.yaml │ └── framework.yaml ├── routes │ └── dev │ │ └── framework.yaml ├── preload.php ├── services_prod.yml ├── bundles.php └── services_test.yaml ├── .github └── CODEOWNERS ├── doc ├── images │ ├── php_server.png │ ├── phpunit_config.png │ ├── php_interpreter.png │ ├── remote_interpreter.png │ └── remote_interpreter_config.png ├── adr │ ├── 001_Use_ADRs.md │ └── images │ │ └── ADR27_ER_Diagram_Custom.mmd ├── Common_Practices_Typescript.md ├── HOWTO_Run_Drone_Locally.md ├── HOWTO_Import_Geodata.md └── donation_state.dot ├── app ├── CookieNames.php ├── config │ ├── campaigns.dev.yml │ ├── campaigns.test.yml │ └── migrations.php ├── MailTemplateFixtures │ ├── TemplateVariant.php │ ├── TemplateSettingsGenerator.php │ ├── TemplateSettings.php │ ├── SimpleSettingsGenerator.php │ └── VariantSettingsGenerator.php ├── mail_templates │ ├── Contact_Forward_to_Operator.txt.twig │ ├── Contact_Confirm_to_User.txt.twig │ ├── Admin_Moderation.txt.twig │ ├── Subscription_Confirmation.txt.twig │ └── Subscription_Request.txt.twig ├── Controllers │ ├── PageNotFoundController.php │ ├── Membership │ │ ├── MembershipFeeUpgradeFrontendFlag.php │ │ ├── ShowMembershipFeeChangeController.php │ │ └── ShowMembershipConfirmationController.php │ ├── StaticContent │ │ ├── ShowUseOfFundsController.php │ │ ├── ShowPatternLibraryController.php │ │ ├── ShowFaqController.php │ │ └── ShowContactFormController.php │ ├── API │ │ └── Donation │ │ │ ├── AbstractApiController.php │ │ │ └── FrameworkAddCommentRequest.php │ ├── Donation │ │ ├── CommentTickerController.php │ │ └── ViewCommentController.php │ ├── Subscription │ │ └── ConfirmSubscriptionController.php │ ├── Validation │ │ ├── FindCitiesController.php │ │ ├── FindStreetsController.php │ │ ├── ValidationController.php │ │ └── ValidateIbanController.php │ ├── Payment │ │ └── BankDataToIbanController.php │ └── AddressChange │ │ └── ShowAddressChangeSuccessController.php ├── AccessDeniedException.php ├── Migrations │ ├── Version20210427124520.php │ ├── Version20221118093850.php │ └── Version20230823082138.php ├── EventHandlers │ ├── PrettifyJsonResponse.php │ └── StoreBucketSelection.php └── UrlGeneratorAdapter.php ├── src ├── RefactoringException.php ├── Factories │ ├── UnknownChoiceDefinition.php │ ├── EnvironmentSetup │ │ ├── EnvironmentSetup.php │ │ └── EnvironmentSetupException.php │ ├── ChoiceFactory.php │ └── DoctrineFactory.php ├── Authentication │ ├── TokenGenerator.php │ ├── AuthenticationBoundedContext.php │ ├── OldStyleTokens │ │ ├── TokenRepository.php │ │ └── LenientAuthorizationChecker.php │ ├── RandomTokenGenerator.php │ ├── MembershipUrlAuthenticationLoader.php │ ├── DonationUrlAuthenticationLoader.php │ ├── Token.php │ └── AuthenticationContextFactory.php ├── BucketTesting │ ├── Logging │ │ ├── LoggingError.php │ │ ├── LogWriter.php │ │ ├── LoggingEvent.php │ │ ├── BucketLogger.php │ │ ├── NullBucketLogger.php │ │ ├── Events │ │ │ ├── DonationCreated.php │ │ │ └── MembershipApplicationCreated.php │ │ └── BestEffortBucketLogger.php │ ├── FeatureToggle.php │ ├── Domain │ │ ├── BucketLoggingRepository.php │ │ └── Model │ │ │ ├── BucketLogBucket.php │ │ │ ├── CampaignDate.php │ │ │ └── Bucket.php │ ├── CampaignConfigurationLoaderInterface.php │ ├── BucketSelectionStrategy.php │ ├── BucketTestingContextFactory.php │ ├── Validation │ │ ├── Rule │ │ │ ├── CampaignValidationRuleInterface.php │ │ │ ├── MinBucketCountRule.php │ │ │ ├── StartAndEndTimeRule.php │ │ │ ├── DefaultBucketRule.php │ │ │ └── UniqueBucketRule.php │ │ └── CampaignErrorCollection.php │ ├── DoorkeeperFeatureToggle.php │ ├── RandomBucketSelection.php │ ├── DataAccess │ │ └── DoctrineBucketLogRepository.php │ ├── InactiveCampaignBucketSelection.php │ ├── ParameterBucketSelection.php │ └── SelectDefaultWhenParamsAreMissingSelection.php ├── Infrastructure │ ├── EventHandling │ │ ├── DomainEventEmitter.php │ │ ├── DonationEventEmitter.php │ │ ├── MembershipEventEmitter.php │ │ └── EventDispatcher.php │ ├── Mail │ │ ├── MailSubjectRendererInterface.php │ │ ├── MailerException.php │ │ ├── Message.php │ │ ├── NullMailer.php │ │ ├── TemplateMailerInterface.php │ │ ├── GetInTouchMailerInterface.php │ │ ├── BasicMailSubjectRenderer.php │ │ ├── OperatorMailer.php │ │ ├── DonationConfirmationMailerAdapter.php │ │ ├── AdminDonationModerationMailerAdapter.php │ │ ├── MailTemplateFilenameTraversable.php │ │ ├── MembershipMailerAdapter.php │ │ ├── MailFormatter.php │ │ ├── ErrorHandlingMailerDecorator.php │ │ ├── DonationConfirmationMailSubjectRenderer.php │ │ └── MembershipConfirmationMailSubjectRenderer.php │ ├── Translation │ │ ├── TranslatorInterface.php │ │ └── JsonTranslator.php │ ├── Validation │ │ ├── NullDomainNameValidator.php │ │ ├── InternetDomainNameValidator.php │ │ └── ValidationErrorLogger.php │ ├── UrlGenerator.php │ ├── TrackingDataSelector.php │ ├── UserDataKeyGenerator.php │ ├── GetConfigCacheKey.php │ ├── WordListFileReader.php │ ├── TranslationsCollector.php │ └── SubmissionRateLimit.php ├── FeatureToggle │ ├── FeatureReader.php │ └── Feature.php ├── Presentation │ ├── ContentPage │ │ ├── PageNotFoundException.php │ │ ├── ContentNotFoundException.php │ │ └── PageSelector.php │ ├── Presenters │ │ ├── ExceptionHtmlPresenterInterface.php │ │ ├── PageNotFoundPresenter.php │ │ ├── ErrorPageHtmlPresenter.php │ │ ├── DonationFormPresenter │ │ │ └── ImpressionCounts.php │ │ ├── InternalErrorHtmlPresenter.php │ │ ├── BankDataPresenter.php │ │ ├── DevelopmentInternalErrorHtmlPresenter.php │ │ ├── ConfirmSubscriptionHtmlPresenter.php │ │ ├── CommentListJsonPresenter.php │ │ ├── CommentListHtmlPresenter.php │ │ └── AddSubscriptionHtmlPresenter.php │ ├── CampaignPropertyExtractor.php │ ├── ActiveFeatureRenderer.php │ ├── BucketPropertyExtractor.php │ ├── Honorifics.php │ ├── TwigTemplate.php │ └── Salutations.php ├── Autocomplete │ ├── AutocompleteContextFactory.php │ ├── Domain │ │ └── LocationRepository.php │ └── UseCases │ │ ├── FindCitiesUseCase.php │ │ └── FindStreetsUseCase.php ├── Validation │ └── IsCustomAmountValidator.php └── UseCases │ └── GetInTouch │ └── GetInTouchRequest.php ├── .env.test ├── cli └── ApplicationConfigValidation │ ├── ConfigValidationException.php │ ├── ValidationErrorRenderer.php │ └── SchemaLoader.php ├── .env.dev ├── phpcs.xml ├── phpstan.neon ├── bin ├── download_assets.sh └── doctrine └── .gitignore /.dockerignore: -------------------------------------------------------------------------------- 1 | vendor 2 | skins/laika/node_modules -------------------------------------------------------------------------------- /skins/test/templates/Page_Footer.html.twig: -------------------------------------------------------------------------------- 1 | page footer -------------------------------------------------------------------------------- /skins/test/templates/Page_Header.html.twig: -------------------------------------------------------------------------------- 1 | page header -------------------------------------------------------------------------------- /tests/Data/files/Banned_Cats.txt: -------------------------------------------------------------------------------- 1 | Nyan 2 | Garfield 3 | Felix da House -------------------------------------------------------------------------------- /skins/test/templates/System_Message.html.twig: -------------------------------------------------------------------------------- 1 | {$ message | raw $} 2 | -------------------------------------------------------------------------------- /.docker/app/mailhog.ini: -------------------------------------------------------------------------------- 1 | [mail function] 2 | sendmail_path = "/usr/bin/msmtp -t" 3 | -------------------------------------------------------------------------------- /web/skins/README.md: -------------------------------------------------------------------------------- 1 | OK, content in here is copied from the build system --Gabriel -------------------------------------------------------------------------------- /.docker/app/msmtprc: -------------------------------------------------------------------------------- 1 | account default 2 | host mailhog 3 | port 1025 4 | auto_from on 5 | -------------------------------------------------------------------------------- /tests/Data/files/addSubscriptionResponse.js: -------------------------------------------------------------------------------- 1 | /**/test({ 2 | "status": "OK" 3 | }); -------------------------------------------------------------------------------- /skins/test/templates/Access_Denied.twig: -------------------------------------------------------------------------------- 1 | 2 | {$ message|default( 'access_denied' ) $} 3 | -------------------------------------------------------------------------------- /config/packages/prod/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | strict_requirements: null 4 | -------------------------------------------------------------------------------- /config/packages/uat/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | strict_requirements: null 4 | -------------------------------------------------------------------------------- /skins/test/templates/Piwik.twig: -------------------------------------------------------------------------------- 1 | 2 | {$ piwik.baseUrl $} 3 | idsite={$ piwik.siteId $} 4 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # This marks the default owner group of all files in this repository 2 | * @wmde/funtech-core 3 | -------------------------------------------------------------------------------- /doc/images/php_server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wmde/fundraising-application/main/doc/images/php_server.png -------------------------------------------------------------------------------- /skins/test/templates/Error_Page.html.twig: -------------------------------------------------------------------------------- 1 | {% if message is defined %} 2 | Internal Error: {$ message $} 3 | {% endif %} -------------------------------------------------------------------------------- /skins/test/templates/Warning_Page.html.twig: -------------------------------------------------------------------------------- 1 | {% if message is defined %} 2 | Warning: {$ message $} 3 | {% endif %} -------------------------------------------------------------------------------- /doc/images/phpunit_config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wmde/fundraising-application/main/doc/images/phpunit_config.png -------------------------------------------------------------------------------- /doc/images/php_interpreter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wmde/fundraising-application/main/doc/images/php_interpreter.png -------------------------------------------------------------------------------- /web/res/img/banner/banner1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wmde/fundraising-application/main/web/res/img/banner/banner1.gif -------------------------------------------------------------------------------- /web/res/img/banner/banner2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wmde/fundraising-application/main/web/res/img/banner/banner2.gif -------------------------------------------------------------------------------- /web/res/img/banner/banner3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wmde/fundraising-application/main/web/res/img/banner/banner3.gif -------------------------------------------------------------------------------- /web/res/img/banner/banner4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wmde/fundraising-application/main/web/res/img/banner/banner4.gif -------------------------------------------------------------------------------- /doc/images/remote_interpreter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wmde/fundraising-application/main/doc/images/remote_interpreter.png -------------------------------------------------------------------------------- /config/routes/dev/framework.yaml: -------------------------------------------------------------------------------- 1 | _errors: 2 | resource: '@FrameworkBundle/Resources/config/routing/errors.xml' 3 | prefix: /_error 4 | -------------------------------------------------------------------------------- /doc/images/remote_interpreter_config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wmde/fundraising-application/main/doc/images/remote_interpreter_config.png -------------------------------------------------------------------------------- /config/packages/test/framework.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | test: true 3 | session: 4 | storage_factory_id: session.storage.factory.mock_file 5 | -------------------------------------------------------------------------------- /skins/test/templates/Page_Not_Found.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'Base_Layout.html.twig' %} 2 | 3 | {% block main %} 4 | {$ message|default( site_metadata.page_not_found )|raw $} 5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /app/CookieNames.php: -------------------------------------------------------------------------------- 1 | {$ errors[field_name] $} 4 | {% endif %} 5 | {% endmacro %} 6 | -------------------------------------------------------------------------------- /src/Authentication/TokenGenerator.php: -------------------------------------------------------------------------------- 1 | ['all' => true], 7 | Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], 8 | ]; 9 | -------------------------------------------------------------------------------- /src/BucketTesting/FeatureToggle.php: -------------------------------------------------------------------------------- 1 | 8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /skins/test/templates/Donation_Cancellation_Confirmation.html.twig: -------------------------------------------------------------------------------- 1 | 2 | Donation ID: {$ donationId $} 3 | Cancellation status: {% if cancellationSuccessful %}successful{% else %}failed{% endif %} 4 | 5 | Mail delivery status: {% if mailDeliveryFailed %}failed{% else %}successful{% endif %} 6 | -------------------------------------------------------------------------------- /tests/Data/GeneratedMailTemplates/Contact_Confirm_to_User.txt: -------------------------------------------------------------------------------- 1 | contact_confirm_to_user 2 | 3 | ------------------------------------- 4 | address_of_organization 5 | 6 | phone 7 | www.wikimedia.de 8 | 9 | wikimedia_vision 10 | 11 | 12 | tax_id_organization 13 | -------------------------------------------------------------------------------- /config/packages/dev/monolog.yaml: -------------------------------------------------------------------------------- 1 | monolog: 2 | handlers: 3 | main: 4 | type: error_log 5 | level: INFO 6 | channels: [ '!event', '!console' ] 7 | console: 8 | type: console 9 | process_psr_3_messages: false 10 | channels: [ '!event', '!doctrine', '!console' ] -------------------------------------------------------------------------------- /config/packages/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | utf8: true 4 | 5 | # Configure how to generate URLs in non-HTTP contexts, such as CLI commands. 6 | # See https://symfony.com/doc/current/routing.html#generating-urls-in-commands 7 | #default_uri: http://localhost 8 | -------------------------------------------------------------------------------- /skins/laika/templates/Comment_List.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'Base_Layout.html.twig' %} 2 | 3 | {% block title %}{$ page_title( 'comment_list_page_title' ) $}{% endblock %} 4 | 5 | {% block scripts %} 6 | {$ parent() $} 7 | 8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /skins/laika/templates/Contact_Form.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'Base_Layout.html.twig' %} 2 | 3 | {% block title %}{$ page_title( 'contact_form_page_title' ) $}{% endblock %} 4 | 5 | {% block scripts %} 6 | {$ parent() $} 7 | 8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /skins/laika/templates/Donation_Form.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'Base_Layout.html.twig' %} 2 | 3 | {% block title %}{$ page_title( 'donation_form_page_title' ) $}{% endblock %} 4 | 5 | {% block scripts %} 6 | {$ parent() $} 7 | 8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /skins/laika/templates/Frequent_Questions.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'Base_Layout.html.twig' %} 2 | 3 | {% block title %}{$ page_title( 'faq_page_title' ) $}{% endblock %} 4 | 5 | {% block scripts %} 6 | {$ parent() $} 7 | 8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /skins/laika/templates/Funds_Usage.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'Base_Layout.html.twig' %} 2 | 3 | {% block title %}{$ page_title( 'use_of_funds_page_title' ) $}{% endblock %} 4 | 5 | {% block scripts %} 6 | {$ parent() $} 7 | 8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /skins/laika/templates/Comment_Ticker.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'Base_Layout.html.twig' %} 2 | 3 | {% block title %}{$ page_title( 'comment_ticker_page_title' ) $}{% endblock %} 4 | 5 | {% block scripts %} 6 | {$ parent() $} 7 | 8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /skins/laika/templates/System_Message.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'Base_Layout.html.twig' %} 2 | 3 | {% block title %}{$ page_title( 'system_message_page_title' ) $}{% endblock %} 4 | 5 | {% block scripts %} 6 | {$ parent() $} 7 | 8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /skins/laika/templates/Update_Address.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'Base_Layout.html.twig' %} 2 | 3 | {% block title %}{$ page_title( 'update_address_page_title' ) $}{% endblock %} 4 | 5 | {% block scripts %} 6 | {$ parent() $} 7 | 8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /tests/Data/GeneratedMailTemplates/Admin_Moderation.donation.txt: -------------------------------------------------------------------------------- 1 | Hallo, 2 | 3 | eine Spende wurde automatisch moderiert, da der Betrag verdächtig hoch ist ( 7.777.777,77 Euro ). 4 | 5 | Zum Überprüfen der Moderation bitte https://backend.wikimedia.de/backend/donation/list aufrufen und dort nach ID 42 suchen. 6 | 7 | -------------------------------------------------------------------------------- /tests/Data/GeneratedMailTemplates/Admin_Moderation.membership.txt: -------------------------------------------------------------------------------- 1 | Hallo, 2 | 3 | ein Mitgliedschaftsantrag wurde automatisch moderiert, da der Betrag verdächtig hoch ist ( 90.000 Euro ). 4 | 5 | Zum Überprüfen der Moderation bitte https://backend.wikimedia.de/backend/member/list aufrufen und dort nach ID 1 suchen. 6 | 7 | -------------------------------------------------------------------------------- /tests/Data/GeneratedMailTemplates/Subscription_Request.txt: -------------------------------------------------------------------------------- 1 | subscription_request 2 | 3 | name_head_of_fundraising 4 | 5 | donation_hint 6 | 7 | ------------------------------------- 8 | address_of_organization 9 | 10 | phone 11 | email 12 | 13 | bank_account 14 | 15 | tax_id_organization 16 | 17 | unsubscribe_hint 18 | -------------------------------------------------------------------------------- /skins/laika/templates/Donation_Confirmation.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'Base_Layout.html.twig' %} 2 | 3 | {% block title %}{$ page_title( 'donation_confirmation_page_title' ) $}{% endblock %} 4 | 5 | {% block scripts %} 6 | {$ parent() $} 7 | 8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /skins/laika/templates/Membership_Application.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'Base_Layout.html.twig' %} 2 | 3 | {% block title %}{$ page_title( 'membership_application_page_title' ) $}{% endblock %} 4 | 5 | {% block scripts %} 6 | {$ parent() $} 7 | 8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /skins/laika/templates/Membership_Fee_Change.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'Base_Layout.html.twig' %} 2 | 3 | {% block title %}{$ page_title( 'membership_fee_upgrade_page_title' ) $}{% endblock %} 4 | 5 | {% block scripts %} 6 | {$ parent() $} 7 | 8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /skins/test/templates/Membership_Application_Cancellation_Confirmation.html.twig: -------------------------------------------------------------------------------- 1 | {% if cancellationSuccessful %} 2 | {$ 'membership-cancellation-header' $} 3 | {$ 'membership-cancellation-text'| raw $} 4 | {% else %} 5 | {$ 'membership-cancellation-failed-header' $} 6 | {$ 'membership-cancellation-failed-text' | raw $} 7 | {% endif %} 8 | -------------------------------------------------------------------------------- /.env.dev: -------------------------------------------------------------------------------- 1 | KERNEL_CLASS='WMDE\Fundraising\Frontend\App\Kernel' 2 | APP_SECRET='$ecretf0rt3st' 3 | APP_DEBUG=0 4 | SYMFONY_DEPRECATIONS_HELPER=999999 5 | PANTHER_APP_ENV=panther 6 | 7 | PAYPAL_API_CLIENT_ID=test_paypal_client_id 8 | PAYPAL_API_CLIENT_SECRET=test_paypal_client_secret 9 | PAYPAL_API_URL=https://api-m.sandbox.paypal.com/ 10 | -------------------------------------------------------------------------------- /app/MailTemplateFixtures/TemplateVariant.php: -------------------------------------------------------------------------------- 1 | 8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /tests/Data/GeneratedMailTemplates/Contact_Forward_to_Operator.txt: -------------------------------------------------------------------------------- 1 | Über das Kontaktformular wurde eine Anfrage gesendet. 2 | Folgende Daten wurden übermittelt: 3 | 4 | Vorname: John 5 | Nachname: Doe 6 | E-Mail-Adresse: j.doe808@example.com 7 | Betreff: Missing Link 8 | Referenz: 123456 9 | Thema: Other 10 | Anfrage: Please advise 11 | -------------------------------------------------------------------------------- /src/BucketTesting/Logging/LogWriter.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | public function getMetaData(): array; 13 | 14 | public function getName(): string; 15 | } 16 | -------------------------------------------------------------------------------- /src/BucketTesting/Logging/BucketLogger.php: -------------------------------------------------------------------------------- 1 | 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /skins/test/templates/Comment_List.html.twig: -------------------------------------------------------------------------------- 1 | 2 |

Die {$ comments |length |e $} neuesten Spenderkommentare

3 | {% for comment in comments %} 4 |
5 | {$ comment.amount |number_format(2, ',', '.') |e $}€ von {$ comment.author |e $} am {$ comment.publicationDate |e $}
6 | {$ comment.text |e $} 7 |
8 | {% endfor %} 9 | -------------------------------------------------------------------------------- /app/mail_templates/Contact_Forward_to_Operator.txt.twig: -------------------------------------------------------------------------------- 1 | Über das Kontaktformular wurde eine Anfrage gesendet. 2 | Folgende Daten wurden übermittelt: 3 | 4 | Vorname: {$ firstName $} 5 | Nachname: {$ lastName $} 6 | E-Mail-Adresse: {$ emailAddress $} 7 | Betreff: {$ subject $} 8 | Referenz: {$ donationNumber $} 9 | Thema: {$ category $} 10 | Anfrage: {$ message $} -------------------------------------------------------------------------------- /skins/laika/templates/Membership_Application_Confirmation.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'Base_Layout.html.twig' %} 2 | 3 | {% block title %}{$ page_title( 'membership_application_confirmation_page_title' ) $}{% endblock %} 4 | 5 | {% block scripts %} 6 | {$ parent() $} 7 | 8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /skins/laika/templates/Subscription_Confirmation.twig: -------------------------------------------------------------------------------- 1 | {% extends 'Base_Layout.html.twig' %} 2 | 3 | {% block title %}{$ page_title( 'subscription_confirmation_page_title' ) $}{% endblock %} 4 | 5 | {% block scripts %} 6 | {$ parent() $} 7 | 8 | {% endblock %} 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/BucketTesting/Domain/BucketLoggingRepository.php: -------------------------------------------------------------------------------- 1 | name}"; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Infrastructure/Mail/MailSubjectRendererInterface.php: -------------------------------------------------------------------------------- 1 | $templateArguments 11 | */ 12 | public function render( array $templateArguments = [] ): string; 13 | 14 | } 15 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | app 5 | src/ 6 | tests/ 7 | cli/ 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Infrastructure/Mail/MailerException.php: -------------------------------------------------------------------------------- 1 | 11 | 12 | {$ mail_content('tax_id_organization') $} -------------------------------------------------------------------------------- /src/BucketTesting/CampaignConfigurationLoaderInterface.php: -------------------------------------------------------------------------------- 1 | $parameters 12 | */ 13 | public function trans( string $messageKey, array $parameters = [] ): string; 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/BucketTesting/BucketSelectionStrategy.php: -------------------------------------------------------------------------------- 1 | 8 | {% endblock %} 9 | 10 | {% block styles %} 11 | 12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /tests/Data/files/emptyCommentList.rss: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Spenden an Wikimedia 5 | /list-comments.html 6 | Öffentliche Liste der über das Online-Formular der Wikimedia Fördergesellschaft getätigter Spenden 7 | de-de 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /web/res/css/asset_state.css: -------------------------------------------------------------------------------- 1 | body::after { 2 | position: fixed; 3 | bottom: 0; 4 | right: 0; 5 | display: block; 6 | padding: 0 16px; 7 | border-radius: 20px; 8 | margin: 5px; 9 | opacity: 0.5; 10 | } 11 | 12 | .external-assets::after { 13 | content: "External assets"; 14 | background: orange; 15 | } 16 | 17 | .no-external-assets::after{ 18 | content: "Builtin assets"; 19 | background: green; 20 | } 21 | -------------------------------------------------------------------------------- /tests/Fixtures/FeatureReaderStub.php: -------------------------------------------------------------------------------- 1 | getLogCalls()->getFirstCall(); 10 | if ( $logCall === null ) { 11 | throw new \RuntimeException( "Log call is null" ); 12 | } 13 | return $logCall; 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/Infrastructure/Validation/NullDomainNameValidator.php: -------------------------------------------------------------------------------- 1 | template->render( [] ); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/Authentication/OldStyleTokens/TokenRepository.php: -------------------------------------------------------------------------------- 1 | $error 11 | */ 12 | public static function render( array $error ): string { 13 | return sprintf( 14 | 'Error in JSON value "%s": %s', 15 | $error['pointer'], 16 | $error['message'] 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /skins/laika/templates/matomo.html.twig: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tests/Fixtures/FakeEnvironmentSetup.php: -------------------------------------------------------------------------------- 1 | entity = $object; 14 | } 15 | 16 | public function getEntity(): ?object { 17 | return $this->entity; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - vendor/phpstan/phpstan-phpunit/extension.neon 3 | - vendor/phpstan/phpstan-phpunit/rules.neon 4 | - vendor/phpstan/phpstan-doctrine/extension.neon 5 | - vendor/phpstan/phpstan-doctrine/rules.neon 6 | - phpstan-baseline.neon 7 | 8 | parameters: 9 | excludePaths: 10 | - src/Presentation/DonationMembershipApplicationAdapter.php 11 | - src/Presentation/Presenters/MembershipFormViolationPresenter.php 12 | - tests/EdgeToEdge/PayPalRequestLoggerTest.php 13 | -------------------------------------------------------------------------------- /src/BucketTesting/Validation/Rule/CampaignValidationRuleInterface.php: -------------------------------------------------------------------------------- 1 | subject; 17 | } 18 | 19 | public function getMessageBody(): string { 20 | return $this->messageBody; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/BucketTesting/DoorkeeperFeatureToggle.php: -------------------------------------------------------------------------------- 1 | doorkeeper->grantsAccessTo( $featureId ); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /tests/Fixtures/FixedPaymentDelayCalculator.php: -------------------------------------------------------------------------------- 1 | fixedDate; 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /app/AccessDeniedException.php: -------------------------------------------------------------------------------- 1 | $templateArguments 14 | */ 15 | public function sendMail( EmailAddress $recipient, array $templateArguments = [] ): void { 16 | // Does nothing on purpose 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/MailTemplateFixtures/TemplateSettingsGenerator.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | public function getTemplateSettings(): iterable; 14 | 15 | public function getTemplateName(): string; 16 | } 17 | -------------------------------------------------------------------------------- /src/Autocomplete/Domain/LocationRepository.php: -------------------------------------------------------------------------------- 1 | 12 | --------------------------------------------------------------------------- 13 | 14 | address_of_organization 15 | 16 | tax_id_organization_verbose 17 | -------------------------------------------------------------------------------- /app/Controllers/StaticContent/ShowUseOfFundsController.php: -------------------------------------------------------------------------------- 1 | getUseOfFundsRenderer(); 14 | return new Response( $renderUseOfFunds() ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/BucketTesting/RandomBucketSelection.php: -------------------------------------------------------------------------------- 1 | getBuckets()[array_rand( $campaign->getBuckets() )]; 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/Presentation/CampaignPropertyExtractor.php: -------------------------------------------------------------------------------- 1 | getUrlKey(); 17 | }, 18 | $campaigns 19 | ); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /tests/Fixtures/SuccessfulMembershipAuthorizer.php: -------------------------------------------------------------------------------- 1 | $message, 'errors' => $errors ], $responseCode ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Infrastructure/Mail/TemplateMailerInterface.php: -------------------------------------------------------------------------------- 1 | $templateArguments 16 | */ 17 | public function sendMail( EmailAddress $recipient, array $templateArguments = [] ): void; 18 | } 19 | -------------------------------------------------------------------------------- /app/Controllers/Donation/CommentTickerController.php: -------------------------------------------------------------------------------- 1 | getLayoutTemplate( 13 | 'Comment_Ticker.html.twig' 14 | ); 15 | 16 | return new Response( $template->render( [] ) ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Presentation/ActiveFeatureRenderer.php: -------------------------------------------------------------------------------- 1 | active ) { 17 | $ids[] = $feature->getId(); 18 | } 19 | } 20 | return $ids; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Infrastructure/EventHandling/DonationEventEmitter.php: -------------------------------------------------------------------------------- 1 | dispatcher->dispatch( $event ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Validation/IsCustomAmountValidator.php: -------------------------------------------------------------------------------- 1 | getEuroCents() === 0 ) { 19 | return false; 20 | } 21 | return !in_array( $amount, $this->validAmounts ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Infrastructure/EventHandling/MembershipEventEmitter.php: -------------------------------------------------------------------------------- 1 | dispatcher->dispatch( $event ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Infrastructure/Mail/GetInTouchMailerInterface.php: -------------------------------------------------------------------------------- 1 | $templateArguments Context parameters to use while rendering the template 14 | */ 15 | public function sendMail( EmailAddress $recipient, array $templateArguments = [] ): void; 16 | } 17 | # 18 | -------------------------------------------------------------------------------- /src/Presentation/Presenters/ErrorPageHtmlPresenter.php: -------------------------------------------------------------------------------- 1 | template->render( [ 'message' => $errorMessage ] ); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /bin/download_assets.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ASSET_BRANCH=${1:-main} 4 | TMP=$(mktemp -d) 5 | echo "Downloading assets ..." 6 | curl -sSL "https://gitlab.com/api/v4/projects/fun-tech%2ffundraising-app-frontend/jobs/artifacts/$ASSET_BRANCH/download?job=build-artifacts" > $TMP/assets.zip 7 | 8 | echo "... removing old assets ..." 9 | rm -rf web/skins/dist web/skins/laika 10 | 11 | echo "... extracting assets ..." 12 | unzip -q $TMP/assets.zip -d web/skins -x "*.html" 13 | mv web/skins/dist web/skins/laika 14 | 15 | echo "... cleanup ..." 16 | rm -rf $TMP 17 | 18 | echo "... finished." 19 | -------------------------------------------------------------------------------- /tests/Fixtures/MailContentProviderStub.php: -------------------------------------------------------------------------------- 1 | 12 | --------------------------------------------------------------------------- 13 | 14 | address_of_organization 15 | 16 | phone 17 | www.wikimedia.de 18 | 19 | tax_id_organization 20 | -------------------------------------------------------------------------------- /app/Controllers/StaticContent/ShowPatternLibraryController.php: -------------------------------------------------------------------------------- 1 | getLayoutTemplate( 'Pattern_Library.html.twig' ); 13 | return new Response( $template->render( [ 'pattern' => $pattern, ] ) ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/Fixtures/LogWriterSpy.php: -------------------------------------------------------------------------------- 1 | entries; 21 | } 22 | 23 | public function write( string $logEntry ): void { 24 | $this->entries[] = $logEntry; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/Presentation/ContentPage/PageSelector.php: -------------------------------------------------------------------------------- 1 | $config pageId => slug 11 | */ 12 | public function __construct( private readonly array $config ) { 13 | } 14 | 15 | public function getPageId( string $slug ): string { 16 | $pageId = array_search( $slug, $this->config ); 17 | 18 | if ( $pageId === false ) { 19 | throw new PageNotFoundException; 20 | } 21 | 22 | return $pageId; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /doc/adr/001_Use_ADRs.md: -------------------------------------------------------------------------------- 1 | # Use Architectural Design Decision Records (ADR) 2 | 3 | Date: 2018-06-22 4 | 5 | ## Status 6 | 7 | Accepted 8 | 9 | ## Context 10 | 11 | We need to record the architectural decisions made on this project. 12 | 13 | ## Decision 14 | 15 | We will use Architecture Decision Records, as described by Michael Nygard in 16 | [this article](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions). 17 | 18 | We will keep the ADRs in Markdown files starting with three-digit number, counting up. 19 | 20 | ## Consequences 21 | 22 | See Michael Nygard's article, linked above. -------------------------------------------------------------------------------- /src/Presentation/BucketPropertyExtractor.php: -------------------------------------------------------------------------------- 1 | getId(); 21 | }, 22 | $buckets 23 | ); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/Factories/ChoiceFactory.php: -------------------------------------------------------------------------------- 1 | getLayoutTemplate( 'Frequent_Questions.html.twig' ); 14 | return new Response( $template->render( 15 | [ 16 | 'faq_content' => $ffFactory->getFaqContent(), 17 | ] 18 | ) ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/Unit/Presentation/HonorificsTest.php: -------------------------------------------------------------------------------- 1 | 'None', 'Dr.' => 'Dr.' ] ); 16 | $this->assertSame( [ '', 'Dr.' ], $honorifics->getKeys() ); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /tests/Data/GeneratedMailTemplates/Membership_Application_Confirmation.moderated_other_reason.txt: -------------------------------------------------------------------------------- 1 | mail_introduction_male_lastname_informal 2 | 3 | membership_application_confirmation_sustaining/moderation_other_reason 4 | 5 | name_head_of_organization 6 | title_head_of_organization 7 | address_of_organization 8 | 9 | --------------------------------------------------------------------------- 10 | wikimedia_vision 11 | 12 | --------------------------------------------------------------------------- 13 | 14 | address_of_organization 15 | 16 | phone 17 | www.wikimedia.de 18 | 19 | tax_id_organization 20 | -------------------------------------------------------------------------------- /app/mail_templates/Admin_Moderation.txt.twig: -------------------------------------------------------------------------------- 1 | {% if amount is defined %} 2 | {%- set formattedAmount = amount |format_number(locale=locale) ~ ' Euro' -%} 3 | {% elseif membershipFee is defined %} 4 | {%- set formattedAmount = membershipFee |format_number(locale=locale) ~ ' Euro' -%} 5 | {% else %} 6 | {% set formattedAmount = "-- ! Fehler im E-Mail-Template, bitte Templates überprüfen. ! --" %} 7 | {% endif %} 8 | Hallo, 9 | 10 | {$ itemType $} wurde automatisch moderiert, da der Betrag verdächtig hoch ist ( {$ formattedAmount $} ). 11 | 12 | Zum Überprüfen der Moderation bitte {$ focURL $} aufrufen und dort nach ID {$ id $} suchen. 13 | -------------------------------------------------------------------------------- /tests/Data/GeneratedMailTemplates/Membership_Application_Confirmation.moderated_amount_too_high.txt: -------------------------------------------------------------------------------- 1 | mail_introduction_male_lastname_informal 2 | 3 | membership_application_confirmation_sustaining/moderation_amount_too_high 4 | 5 | name_head_of_organization 6 | title_head_of_organization 7 | address_of_organization 8 | 9 | --------------------------------------------------------------------------- 10 | wikimedia_vision 11 | 12 | --------------------------------------------------------------------------- 13 | 14 | address_of_organization 15 | 16 | phone 17 | www.wikimedia.de 18 | 19 | tax_id_organization 20 | -------------------------------------------------------------------------------- /src/Presentation/Presenters/DonationFormPresenter/ImpressionCounts.php: -------------------------------------------------------------------------------- 1 | totalImpressionCount; 16 | } 17 | 18 | public function getSingleBannerImpressionCount(): int { 19 | return $this->singleBannerImpressionCount; 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /tests/Fixtures/FixedTokenGenerator.php: -------------------------------------------------------------------------------- 1 | token = $token ?? Token::fromHex( self::DEFAULT_TOKEN ); 16 | } 17 | 18 | public function generateToken(): Token { 19 | return $this->token; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Infrastructure/UrlGenerator.php: -------------------------------------------------------------------------------- 1 | $parameters 15 | */ 16 | public function generateAbsoluteUrl( string $routeName, array $parameters = [] ): string; 17 | 18 | /** 19 | * @param string $routeName 20 | * @param array $parameters 21 | */ 22 | public function generateRelativeUrl( string $routeName, array $parameters = [] ): string; 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/Presentation/Presenters/InternalErrorHtmlPresenter.php: -------------------------------------------------------------------------------- 1 | template = $template; 18 | } 19 | 20 | public function present( \Throwable $exception ): string { 21 | return $this->template->render( [] ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/Fixtures/FailingPayPalVerificationService.php: -------------------------------------------------------------------------------- 1 | errorMessage ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/Fixtures/SucceedingEmailValidator.php: -------------------------------------------------------------------------------- 1 | newConfirmSubscriptionUseCase(); 14 | $result = $useCase->confirmSubscription( $confirmationCode ); 15 | return new Response( $ffFactory->newConfirmSubscriptionHtmlPresenter()->present( $result ) ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/mail_templates/Subscription_Confirmation.txt.twig: -------------------------------------------------------------------------------- 1 | {$ mail_content('subscription_confirmation/body') $} 2 | 3 | {$ mail_content('name_head_of_fundraising') $} 4 | 5 | {$ mail_content('donation_hint', { 'piwiki_keyword': 'confirmed' }) $} 6 | 7 | ------------------------------------- 8 | {$ mail_content('address_of_organization') $} 9 | 10 | {$ mail_content('phone') $} 11 | {$ mail_content('email') $} 12 | 13 | {$ mail_content('bank_account') $} 14 | 15 | {$ mail_content('tax_id_organization') $} 16 | 17 | {$ mail_content('unsubscribe_hint', { 18 | 'subscription_email': subscription.email, 19 | 'privacy_protection_link': url( 'page', { pageName: 'Datenschutz' } ) 20 | }) $} -------------------------------------------------------------------------------- /src/Autocomplete/UseCases/FindCitiesUseCase.php: -------------------------------------------------------------------------------- 1 | locationRepository->getCitiesForPostcode( $alphanumericPostCode ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/Controllers/Validation/FindCitiesController.php: -------------------------------------------------------------------------------- 1 | newFindCitiesUseCase() 16 | ->getCitiesForPostcode( $request->get( 'postcode', '' ) ); 17 | 18 | return new JsonResponse( $cities ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Autocomplete/UseCases/FindStreetsUseCase.php: -------------------------------------------------------------------------------- 1 | locationRepository->getStreetsForPostcode( $alphanumericPostCode ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Presentation/Presenters/BankDataPresenter.php: -------------------------------------------------------------------------------- 1 | '', 'bic' => '', 'bankname' => '' ]; 8 | 9 | /** 10 | * @param array $bankData 11 | * @return array{iban?:string,bic?:string,bankname?:string} 12 | */ 13 | public static function getBankDataArray( array $bankData ): array { 14 | $filteredData = array_intersect_key( $bankData, self::BANK_DATA_KEYS ); 15 | $stringData = array_map( 'strval', $filteredData ); 16 | return array_merge( self::BANK_DATA_KEYS, $stringData ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/Fixtures/UrlAuthenticatorStub.php: -------------------------------------------------------------------------------- 1 | newFindStreetsUseCase() 16 | ->getStreetsForPostcode( $request->get( 'postcode', '' ) ); 17 | 18 | return new JsonResponse( $streets ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/Fixtures/ThrowingCampaignConfigurationLoader.php: -------------------------------------------------------------------------------- 1 | exception; 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/Presentation/Honorifics.php: -------------------------------------------------------------------------------- 1 | $honorifics name => display name pairs 14 | */ 15 | public function __construct( private readonly array $honorifics ) { 16 | } 17 | 18 | /** 19 | * @return string[] 20 | */ 21 | public function getList(): array { 22 | return $this->honorifics; 23 | } 24 | 25 | /** 26 | * @return string[] 27 | */ 28 | public function getKeys(): array { 29 | return array_keys( $this->honorifics ); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/BucketTesting/DataAccess/DoctrineBucketLogRepository.php: -------------------------------------------------------------------------------- 1 | entityManager->persist( $bucketLog ); 18 | $this->entityManager->flush(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Infrastructure/Mail/BasicMailSubjectRenderer.php: -------------------------------------------------------------------------------- 1 | $templateArguments 19 | */ 20 | public function render( array $templateArguments = [] ): string { 21 | return $this->translator->trans( $this->subjectKey ); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | !.* 3 | 4 | composer.phar 5 | npm-debug.log 6 | coverage.clover 7 | coverage.xml 8 | 9 | vendor/ 10 | node_modules/ 11 | var/ 12 | coverage/ 13 | 14 | .env 15 | .env.*.local 16 | .idea/ 17 | .floo 18 | .flooignore 19 | 20 | .phpunit.result.cache 21 | .phpunit.cache 22 | 23 | app/config/ 24 | 25 | app/fundraising-frontend-content/ 26 | 27 | res/blz.lut2f 28 | 29 | web/res/js/wmde.js 30 | web/resources 31 | web/skins 32 | 33 | deployment/deployment.retry 34 | 35 | tmp/ 36 | 37 | /.docker/database/* 38 | !/.docker/database/01.Database_Schema.sql 39 | !/.docker/database/02.Default_Data.sql 40 | 41 | # This is a file with lots of errors that we could copy 'ignore' rules from 42 | phpstan-baseline-source.neon -------------------------------------------------------------------------------- /src/BucketTesting/Validation/Rule/MinBucketCountRule.php: -------------------------------------------------------------------------------- 1 | getBuckets() ) < 2 ) { 14 | $errorLogger->addError( 'Campaigns must have at least two buckets', $campaign ); 15 | return false; 16 | } 17 | return true; 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/Infrastructure/TrackingDataSelector.php: -------------------------------------------------------------------------------- 1 | 0 ? array_shift( $nonEmptyValues ) : ''; 15 | } 16 | 17 | public static function concatTrackingFromVarTuple( string $campaign, string $keyword ): string { 18 | if ( $campaign !== '' ) { 19 | return strtolower( implode( '/', array_filter( [ $campaign, $keyword ] ) ) ); 20 | } 21 | 22 | return ''; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Presentation/Presenters/DevelopmentInternalErrorHtmlPresenter.php: -------------------------------------------------------------------------------- 1 | template = $template; 15 | } 16 | 17 | public function present( \Throwable $exception ): string { 18 | return $this->template->render( [ 19 | 'message' => $exception->getMessage(), 20 | 'trace' => $exception->getTrace(), 21 | ] ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/Fixtures/FakeBucketLoggingEvent.php: -------------------------------------------------------------------------------- 1 | $metadata 13 | */ 14 | public function __construct( private readonly array $metadata = [ 'id' => 123, 'some_fact' => 'water_is_wet' ] ) { 15 | } 16 | 17 | /** 18 | * @return array 19 | */ 20 | public function getMetaData(): array { 21 | return $this->metadata; 22 | } 23 | 24 | public function getName(): string { 25 | return 'testEventLogged'; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /doc/Common_Practices_Typescript.md: -------------------------------------------------------------------------------- 1 | # Common practices for client-side code 2 | 3 | These guidelines are agreed-upon practices by the FUN team and reflect their current understanding of Vue.js and TypeScript. 4 | 5 | * We collect all new types in the `types.ts` file. When it becomes sufficiently large, we'll have enough code "examples" to refactor them into smaller cohesive units/modules. 6 | * When writing a Vuex store module, don't split mutations, actions, getters, etc in separate files. Doing this makes the code harder to navigate. If a Vuex module file becomes too big, ask the team members if splitting is ok. 7 | * In Vue.js files, use TypeScript 8 | * Don't use [vue-class-component decorator](https://github.com/vuejs/vue-class-component) 9 | -------------------------------------------------------------------------------- /src/Authentication/RandomTokenGenerator.php: -------------------------------------------------------------------------------- 1 | length < 1 ) { 17 | throw new \InvalidArgumentException( 'Token length must be a positive integer' ); 18 | } 19 | 20 | return new Token( random_bytes( $this->length ) ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/Fixtures/FailingVerificationServiceFactory.php: -------------------------------------------------------------------------------- 1 | errorMessage ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/Fixtures/SucceedingVerificationServiceFactory.php: -------------------------------------------------------------------------------- 1 | 11 | Show pink fluffy unicorns dancing on rainbows to select donors. 12 | 13 | This campaign is used for testing the parameter passing on redirects 14 | See AddDonationRouteTest. 15 | start: "2023-08-08" 16 | end: "2038-01-18" 17 | buckets: 18 | - "default" 19 | - "fluffy" 20 | - "pink" 21 | default_bucket: "default" 22 | url_key: pfu 23 | active: true 24 | param_only: true 25 | -------------------------------------------------------------------------------- /src/BucketTesting/Validation/Rule/StartAndEndTimeRule.php: -------------------------------------------------------------------------------- 1 | getStartTimestamp() >= $campaign->getEndTimestamp() ) { 14 | $errorLogger->addError( 'Start date must be before end date.', $campaign ); 15 | return false; 16 | } 17 | return true; 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /tests/EdgeToEdge/Routes/GetApplicationVarsTrait.php: -------------------------------------------------------------------------------- 1 | filter( '#appdata' )->getNode( 0 ); 12 | $decodedApplicationVars = json_decode( $appElement->getAttribute( 'data-application-vars' ) ); 13 | if ( !( $decodedApplicationVars instanceof \stdClass ) ) { 14 | throw new \RuntimeException( 'Could not decode application vars as an object' ); 15 | } 16 | return $decodedApplicationVars; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/Data/GeneratedMailTemplates/Donation_Confirmation.direct_debit_unmoderated_recurring.txt: -------------------------------------------------------------------------------- 1 | mail_introduction_female_lastname_informal 2 | 3 | donation_confirmation/payment_introdonation_confirmation/paymenttype_directdebit 4 | 5 | donation_confirmation/receipt 6 | 7 | donation_confirmation/receipt_support 8 | 9 | donation_confirmation/greetings 10 | 11 | name_head_of_organization 12 | title_head_of_organization 13 | 14 | --------------------------------------------------------------------------- 15 | wikimedia_vision 16 | 17 | --------------------------------------------------------------------------- 18 | 19 | address_of_organization 20 | 21 | tax_id_organization_verbose 22 | 23 | donation_confirmation/number 24 | -------------------------------------------------------------------------------- /app/Controllers/Validation/ValidationController.php: -------------------------------------------------------------------------------- 1 | getEmailValidator()->validate( $request->request->get( 'email', '' ) ); 16 | return new JsonResponse( [ 17 | 'status' => $validationResult->isSuccessful() ? 'OK' : 'ERR' 18 | ] ); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/BucketTesting/Validation/Rule/DefaultBucketRule.php: -------------------------------------------------------------------------------- 1 | getDefaultBucket(); 15 | } catch ( \LogicException $e ) { 16 | $errorLogger->addError( 'Must have a valid default bucket.', $campaign ); 17 | return false; 18 | } 19 | return true; 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/Presentation/TwigTemplate.php: -------------------------------------------------------------------------------- 1 | $context 15 | */ 16 | public function __construct( 17 | private readonly Environment $twig, 18 | private readonly string $templatePath, 19 | private readonly array $context = [] 20 | ) { 21 | } 22 | 23 | /** 24 | * @param array $arguments 25 | */ 26 | public function render( array $arguments ): string { 27 | return $this->twig->render( $this->templatePath, $arguments + $this->context ); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /tests/Data/GeneratedMailTemplates/Donation_Confirmation.direct_debit_unmoderated_non_recurring.txt: -------------------------------------------------------------------------------- 1 | mail_introduction_female_lastname_informal 2 | 3 | donation_confirmation/payment_introdonation_confirmation/paymenttype_directdebit 4 | 5 | donation_confirmation/receipt_support donation_confirmation/receipt_opted_out 6 | 7 | donation_confirmation/greetings 8 | 9 | name_head_of_organization 10 | title_head_of_organization 11 | 12 | --------------------------------------------------------------------------- 13 | wikimedia_vision 14 | 15 | --------------------------------------------------------------------------- 16 | 17 | address_of_organization 18 | 19 | tax_id_organization_verbose 20 | 21 | donation_confirmation/number 22 | -------------------------------------------------------------------------------- /tests/Data/ValidLocation.php: -------------------------------------------------------------------------------- 1 | getLayoutTemplate( 'Contact_Form.html.twig' ); 14 | $templateContext = [ 15 | 'contact_categories' => $ffFactory->getGetInTouchCategories(), 16 | 'contactFormValidationPatterns' => $ffFactory->getValidationRules()->contactForm, 17 | ]; 18 | return new Response( $template->render( $templateContext ) ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/Controllers/Validation/ValidateIbanController.php: -------------------------------------------------------------------------------- 1 | newCheckIbanUseCase(); 16 | $checkIbanResponse = $useCase->ibanIsValid( $request->query->get( 'iban', '' ) ); 17 | return new JsonResponse( $ffFactory->newIbanPresenter()->present( $checkIbanResponse ) ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/BucketTesting/Logging/Events/DonationCreated.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | private array $metadata; 15 | 16 | public function __construct( int $donationId ) { 17 | $this->metadata = [ 18 | 'id' => $donationId 19 | ]; 20 | } 21 | 22 | /** 23 | * @return array 24 | */ 25 | public function getMetaData(): array { 26 | return $this->metadata; 27 | } 28 | 29 | public function getName(): string { 30 | return 'donationCreated'; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /tests/Data/GeneratedMailTemplates/Donation_Confirmation.deposit_unmoderated_recurring.txt: -------------------------------------------------------------------------------- 1 | mail_introduction_female_lastname_informal 2 | 3 | donation_confirmation/paymenttype_banktransfer 4 | 5 | donation_confirmation/paymenttype_banktransfer_interval 6 | donation_confirmation/receipt_support donation_confirmation/receipt_opted_out 7 | 8 | donation_confirmation/greetings 9 | 10 | name_head_of_organization 11 | title_head_of_organization 12 | 13 | --------------------------------------------------------------------------- 14 | wikimedia_vision 15 | 16 | --------------------------------------------------------------------------- 17 | 18 | address_of_organization 19 | 20 | tax_id_organization_verbose 21 | 22 | donation_confirmation/number 23 | -------------------------------------------------------------------------------- /tests/Data/GeneratedMailTemplates/Donation_Confirmation.deposit_unmoderated_non_recurring.txt: -------------------------------------------------------------------------------- 1 | mail_introduction_female_lastname_informal 2 | 3 | donation_confirmation/paymenttype_banktransfer 4 | 5 | donation_confirmation/paymenttype_banktransfer_once 6 | 7 | donation_confirmation/receipt 8 | 9 | donation_confirmation/receipt_support 10 | 11 | donation_confirmation/greetings 12 | 13 | name_head_of_organization 14 | title_head_of_organization 15 | 16 | --------------------------------------------------------------------------- 17 | wikimedia_vision 18 | 19 | --------------------------------------------------------------------------- 20 | 21 | address_of_organization 22 | 23 | tax_id_organization_verbose 24 | 25 | donation_confirmation/number 26 | -------------------------------------------------------------------------------- /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/framework.yaml: -------------------------------------------------------------------------------- 1 | # see https://symfony.com/doc/current/reference/configuration/framework.html 2 | framework: 3 | secret: '%env(APP_SECRET)%' 4 | #csrf_protection: true 5 | http_method_override: false 6 | 7 | # Enables session support. Note that the session will ONLY be started if you read or write from it. 8 | # Remove or comment this section to explicitly disable session support. 9 | session: 10 | handler_id: null 11 | cookie_secure: auto 12 | cookie_samesite: lax 13 | 14 | #esi: true 15 | #fragments: true 16 | php_errors: 17 | log: true 18 | 19 | mailer: 20 | dsn: 'native://default' 21 | 22 | handle_all_throwables: true 23 | 24 | validation: 25 | email_validation_mode: html5 26 | -------------------------------------------------------------------------------- /tests/Data/GeneratedMailTemplates/Donation_Confirmation.paypal_unmoderated_non_recurring.txt: -------------------------------------------------------------------------------- 1 | mail_introduction_female_lastname_informal 2 | 3 | donation_confirmation/payment_introdonation_confirmation/paymenttype_externaldonation_confirmation/payment_interval_once 4 | 5 | donation_confirmation/receipt_support donation_confirmation/receipt_opted_out 6 | 7 | donation_confirmation/greetings 8 | 9 | name_head_of_organization 10 | title_head_of_organization 11 | 12 | --------------------------------------------------------------------------- 13 | wikimedia_vision 14 | 15 | --------------------------------------------------------------------------- 16 | 17 | address_of_organization 18 | 19 | tax_id_organization_verbose 20 | 21 | donation_confirmation/number 22 | -------------------------------------------------------------------------------- /tests/Data/GeneratedMailTemplates/Donation_Confirmation.paypal_unmoderated_recurring.txt: -------------------------------------------------------------------------------- 1 | mail_introduction_female_lastname_informal 2 | 3 | donation_confirmation/payment_introdonation_confirmation/paymenttype_externaldonation_confirmation/payment_interval_recurring 4 | 5 | donation_confirmation/receipt_support donation_confirmation/receipt_opted_out 6 | 7 | donation_confirmation/greetings 8 | 9 | name_head_of_organization 10 | title_head_of_organization 11 | 12 | --------------------------------------------------------------------------- 13 | wikimedia_vision 14 | 15 | --------------------------------------------------------------------------- 16 | 17 | address_of_organization 18 | 19 | tax_id_organization_verbose 20 | 21 | donation_confirmation/number 22 | -------------------------------------------------------------------------------- /tests/Data/GeneratedMailTemplates/Donation_Confirmation.sofort_unmoderated_non_recurring.txt: -------------------------------------------------------------------------------- 1 | mail_introduction_female_lastname_informal 2 | 3 | donation_confirmation/payment_introdonation_confirmation/paymenttype_externaldonation_confirmation/payment_interval_once 4 | 5 | donation_confirmation/receipt_support donation_confirmation/receipt_opted_out 6 | 7 | donation_confirmation/greetings 8 | 9 | name_head_of_organization 10 | title_head_of_organization 11 | 12 | --------------------------------------------------------------------------- 13 | wikimedia_vision 14 | 15 | --------------------------------------------------------------------------- 16 | 17 | address_of_organization 18 | 19 | tax_id_organization_verbose 20 | 21 | donation_confirmation/number 22 | -------------------------------------------------------------------------------- /src/BucketTesting/InactiveCampaignBucketSelection.php: -------------------------------------------------------------------------------- 1 | isActive() || $campaign->isExpired( $this->now ) ) { 18 | return $campaign->getDefaultBucket(); 19 | } 20 | return null; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /skins/laika/templates/Page_Not_Found.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'Base_Layout.html.twig' %} 2 | 3 | {% block title %}{$ page_title( 'page_not_found_page_title' ) $}{% endblock %} 4 | 5 | {% block main %} 6 |
7 | 11 |
12 | 18 | {% endblock %} 19 | 20 | {% block scripts %} 21 | {$ parent() $} 22 | 23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /src/Presentation/Presenters/ConfirmSubscriptionHtmlPresenter.php: -------------------------------------------------------------------------------- 1 | isSuccessful() ) { 18 | $contextVariables['error_message'] = $confirmationResponse->getValidationErrors()[0]->getMessageIdentifier(); 19 | } 20 | return $this->template->render( $contextVariables ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/MailTemplateFixtures/TemplateSettings.php: -------------------------------------------------------------------------------- 1 | $templateData Data passed to the template engine 14 | */ 15 | public function __construct( 16 | public readonly string $templateName, 17 | public readonly string $id, 18 | public readonly array $templateData = [] 19 | ) { 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/Fixtures/FakeTranslator.php: -------------------------------------------------------------------------------- 1 | messageWrapper, $messageKey ); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /app/MailTemplateFixtures/SimpleSettingsGenerator.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | public function getTemplateSettings(): iterable { 17 | yield new TemplateSettings( $this->templateName, $this->getId(), $this->templateData ); 18 | } 19 | 20 | private function getId(): string { 21 | return basename( $this->templateName, '.txt.twig' ); 22 | } 23 | 24 | public function getTemplateName(): string { 25 | return $this->templateName; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /doc/HOWTO_Run_Drone_Locally.md: -------------------------------------------------------------------------------- 1 | # How to run the DRONE CI pipeline on your local machine 2 | 3 | ## Prerequisites 4 | 5 | You need to have the `drone` command line interface installed on your 6 | local machine. See https://docs.drone.io/cli/install/ for download links 7 | and installation instructions. 8 | 9 | You need to have the `pass` password manager installed, configured and be 10 | able to access the Fundraising Tech password repository. You can try out 11 | your setup by running `pass wmde-fun/github-composer-ci-token`. After 12 | entering your GPG password you should then see the GitHub token. 13 | 14 | As the last item on the list, you should be able to use `docker` commands 15 | on your local machine. 16 | 17 | ## Running Drone CI pipeline on your local machine 18 | 19 | 20 | make drone-ci 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/BucketTesting/ParameterBucketSelection.php: -------------------------------------------------------------------------------- 1 | $parameters 14 | */ 15 | public function __construct( private readonly array $parameters ) { 16 | } 17 | 18 | public function selectBucketForCampaign( Campaign $campaign ): ?Bucket { 19 | $urlKey = $campaign->getUrlKey(); 20 | if ( !isset( $this->parameters[$urlKey] ) ) { 21 | return null; 22 | } 23 | return $campaign->getBucketByIndex( $this->parameters[$urlKey] ); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/BucketTesting/Validation/CampaignErrorCollection.php: -------------------------------------------------------------------------------- 1 | getName() . ': ' . $error; 19 | } 20 | $this->errors[] = $error; 21 | } 22 | 23 | /** 24 | * @return string[] 25 | */ 26 | public function getErrors(): array { 27 | return $this->errors; 28 | } 29 | 30 | public function hasErrors(): bool { 31 | return !empty( $this->getErrors() ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Authentication/MembershipUrlAuthenticationLoader.php: -------------------------------------------------------------------------------- 1 | $parameters 18 | * @return array 19 | */ 20 | public function addMembershipAuthorizationParameters( int $membershipId, array $parameters ): array; 21 | } 22 | -------------------------------------------------------------------------------- /src/BucketTesting/SelectDefaultWhenParamsAreMissingSelection.php: -------------------------------------------------------------------------------- 1 | $params 14 | */ 15 | public function __construct( private readonly array $params ) { 16 | } 17 | 18 | public function selectBucketForCampaign( Campaign $campaign ): ?Bucket { 19 | if ( $campaign->isOnlyActiveWithUrlKey() && !isset( $this->params[$campaign->getUrlKey()] ) ) { 20 | return $campaign->getDefaultBucket(); 21 | } 22 | return null; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /web/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /list-comments.* 3 | Disallow: /spenden/list.* 4 | Disallow: /spenden/rss.* 5 | Disallow: /spenden/json.* 6 | 7 | Disallow: /donation/add 8 | Disallow: /donation/cancel 9 | Disallow: /donation/update 10 | Disallow: /validate-email 11 | Disallow: /validate-payment-data 12 | Disallow: /validate-address 13 | Disallow: /validate-fee 14 | Disallow: /check-iban 15 | Disallow: /generate-iban 16 | Disallow: /add-comment 17 | Disallow: /show-membership-confirmation 18 | Disallow: /change-membership-fee 19 | Disallow: /show-donation-confirmation 20 | Disallow: /cancel-membership-application 21 | Disallow: /sofort-payment-notification 22 | Disallow: /handle-paypal-payment-notification 23 | Disallow: /handle-paypal-membership-fee-payments 24 | Disallow: /handle-creditcard-payment-notification 25 | -------------------------------------------------------------------------------- /doc/HOWTO_Import_Geodata.md: -------------------------------------------------------------------------------- 1 | # How to Import the Third Party Geodata 2 | 3 | The Geodata zip contains 2 sql files, a small one with community names, and a large one which includes streets. 4 | 5 | 1. Copy the smaller sql file into /.docker/database. 6 | 2. Rename it to `00.Geodata.sql`. This is needed because Docker runs the files in order, and the Geodata import needs to run before the Schema. 7 | 3. Remove the database volume with the command: 8 | 9 | ```$ docker volume rm APP_DIRECTORY_db-storage``` 10 | 11 | APP_DIRECTORY is the directory name where you have checked out the `fundraising-application` code. You can check for the volume name by listing all docker volumes with the command `$ docker volume ls`. 12 | 4. Run `$ docker compose up`. When importing the Geotata this might take several minutes as it imports. 13 | -------------------------------------------------------------------------------- /app/mail_templates/Subscription_Request.txt.twig: -------------------------------------------------------------------------------- 1 | {$ mail_content('subscription_request', { 2 | 'confirm_subscription_link': url( 'confirm-subscription', { confirmationCode: subscription.confirmationCode } ), 3 | 'day_of_the_week': day_of_the_week 4 | }) $} 5 | 6 | {$ mail_content('name_head_of_fundraising') $} 7 | 8 | {$ mail_content('donation_hint', { 'piwiki_keyword': 'requested' }) $} 9 | 10 | ------------------------------------- 11 | {$ mail_content('address_of_organization') $} 12 | 13 | {$ mail_content('phone') $} 14 | {$ mail_content('email') $} 15 | 16 | {$ mail_content('bank_account') $} 17 | 18 | {$ mail_content('tax_id_organization') $} 19 | 20 | {$ mail_content('unsubscribe_hint', { 21 | 'subscription_email': subscription.email, 22 | 'privacy_protection_link': url( 'page', { pageName: 'Datenschutz' } ) 23 | }) $} -------------------------------------------------------------------------------- /doc/donation_state.dot: -------------------------------------------------------------------------------- 1 | digraph G { 2 | "New" -> "Moderation" [label="Add comment with bad words"] 3 | "New" -> "Cancelled" [label="Cancelled by user or in backend"] 4 | "Promise" -> "Moderation" [label="Add comment with bad words"] 5 | "External_Incomplete" -> "Moderation" [label="Add comment with bad words"] 6 | "External_Incomplete" -> "External_Booked" [label="Book donation"] 7 | "Moderation" -> "External_Incomplete" [label="Moderate OK in backend"] 8 | "Moderation" -> "External_Booked" [label="Moderate OK in backend"] 9 | "External_Booked" -> "Moderation" [label="Add comment with bad words"] 10 | "New" [shape=circle,peripheries=2] 11 | "Promise" [shape=circle,peripheries=2] 12 | "External_Incomplete" [shape=circle,peripheries=2] 13 | "External_Booked" [shape=circle] 14 | "Moderation" [shape=circle] 15 | } 16 | -------------------------------------------------------------------------------- /src/BucketTesting/Logging/Events/MembershipApplicationCreated.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | private array $metadata; 15 | 16 | public function __construct( int $membershipApplicationId ) { 17 | $this->metadata = [ 18 | 'id' => $membershipApplicationId 19 | ]; 20 | } 21 | 22 | /** 23 | * @return array 24 | */ 25 | public function getMetaData(): array { 26 | return $this->metadata; 27 | } 28 | 29 | public function getName(): string { 30 | return 'membershipApplicationCreated'; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /tests/Fixtures/ErrorThrowingTemplateBasedMailer.php: -------------------------------------------------------------------------------- 1 | $templateArguments 20 | */ 21 | public function sendMail( EmailAddress $recipient, array $templateArguments = [] ): void { 22 | throw new \RuntimeException( self::ERROR_MESSAGE, 0, $this->previous ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/Controllers/Payment/BankDataToIbanController.php: -------------------------------------------------------------------------------- 1 | newGenerateBankDataFromGermanLegacyBankDataUseCase()->generateIban( 16 | $request->query->get( 'accountNumber', '' ), 17 | $request->query->get( 'bankCode', '' ) 18 | ); 19 | return new JsonResponse( $ffFactory->newIbanPresenter()->present( $generateIbanResponse ) ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Authentication/DonationUrlAuthenticationLoader.php: -------------------------------------------------------------------------------- 1 | $parameters 18 | * @return array 19 | */ 20 | public function addDonationAuthorizationParameters( int $donationId, array $parameters ): array; 21 | } 22 | -------------------------------------------------------------------------------- /tests/Fixtures/EventDispatcherSpy.php: -------------------------------------------------------------------------------- 1 | listeners ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/Controllers/API/Donation/FrameworkAddCommentRequest.php: -------------------------------------------------------------------------------- 1 | comment, 22 | isPublic: $this->isPublic, 23 | isAnonymous: !$this->withName, 24 | donationId: $this->donationId, 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/Data/GeneratedMailTemplates/Donation_Confirmation.credit_card_unmoderated_recurring.txt: -------------------------------------------------------------------------------- 1 | mail_introduction_female_lastname_informal 2 | 3 | donation_confirmation/payment_introdonation_confirmation/paymenttype_externaldonation_confirmation/payment_interval_recurring 4 | 5 | donation_confirmation/receipt_support donation_confirmation/receipt_opted_out 6 | 7 | donation_confirmation/greetings 8 | 9 | name_head_of_organization 10 | title_head_of_organization 11 | 12 | donation_confirmation/creditcard_interval_notice 13 | 14 | --------------------------------------------------------------------------- 15 | wikimedia_vision 16 | 17 | --------------------------------------------------------------------------- 18 | 19 | address_of_organization 20 | 21 | tax_id_organization_verbose 22 | 23 | donation_confirmation/number 24 | -------------------------------------------------------------------------------- /tests/Data/GeneratedMailTemplates/Donation_Confirmation.micropayment_unmoderated_recurring.txt: -------------------------------------------------------------------------------- 1 | mail_introduction_female_lastname_informal 2 | 3 | donation_confirmation/payment_introdonation_confirmation/paymenttype_externaldonation_confirmation/payment_interval_recurring 4 | 5 | donation_confirmation/receipt_support donation_confirmation/receipt_opted_out 6 | 7 | donation_confirmation/greetings 8 | 9 | name_head_of_organization 10 | title_head_of_organization 11 | 12 | donation_confirmation/creditcard_interval_notice 13 | 14 | --------------------------------------------------------------------------- 15 | wikimedia_vision 16 | 17 | --------------------------------------------------------------------------- 18 | 19 | address_of_organization 20 | 21 | tax_id_organization_verbose 22 | 23 | donation_confirmation/number 24 | -------------------------------------------------------------------------------- /tests/Data/GeneratedMailTemplates/Membership_Application_Confirmation.bank_transfer_active_yearly.txt: -------------------------------------------------------------------------------- 1 | mail_introduction_male_informal 2 | 3 | membership_application_confirmation_active/intro_start 4 | 5 | membership_application_confirmation_active/intro_ueb 6 | 7 | membership_application_confirmation_active/intro_end 8 | 9 | membership_application_confirmation_active/contact 10 | 11 | membership_application_confirmation_active/outro 12 | 13 | name_head_of_organization 14 | title_head_of_organization 15 | address_of_organization 16 | 17 | --------------------------------------------------------------------------- 18 | wikimedia_vision 19 | 20 | --------------------------------------------------------------------------- 21 | 22 | address_of_organization 23 | 24 | phone 25 | www.wikimedia.de 26 | 27 | tax_id_organization 28 | -------------------------------------------------------------------------------- /tests/Data/GeneratedMailTemplates/Membership_Application_Confirmation.direct_debit_active_yearly.txt: -------------------------------------------------------------------------------- 1 | mail_introduction_male_informal 2 | 3 | membership_application_confirmation_active/intro_start 4 | 5 | membership_application_confirmation_active/intro_bez 6 | 7 | membership_application_confirmation_active/intro_end 8 | 9 | membership_application_confirmation_active/contact 10 | 11 | membership_application_confirmation_active/outro 12 | 13 | name_head_of_organization 14 | title_head_of_organization 15 | address_of_organization 16 | 17 | --------------------------------------------------------------------------- 18 | wikimedia_vision 19 | 20 | --------------------------------------------------------------------------- 21 | 22 | address_of_organization 23 | 24 | phone 25 | www.wikimedia.de 26 | 27 | tax_id_organization 28 | -------------------------------------------------------------------------------- /tests/EdgeToEdge/Routes/DefaultRouteTest.php: -------------------------------------------------------------------------------- 1 | modifyConfiguration( [ 'skin' => 'laika' ] ); 15 | $client = $this->createClient(); 16 | 17 | $client->request( 'GET', '/' ); 18 | 19 | $this->assertMatchesRegularExpression( 20 | '/ 23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /skins/test/templates/Comment_List.rss.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Spenden an Wikimedia 5 | {$ basepath $}/list-comments.html 6 | Öffentliche Liste der über das Online-Formular der Wikimedia Fördergesellschaft getätigter Spenden 7 | de-de 8 | {$ rssPublicationData|e $} 9 | 10 | {% for comment in comments %} 11 | {$ comment.amount |number_format(2, ',', '.') |e $} Euro von {$ comment.author |e $} 12 | {$ comment.text |e $} 13 | {$ comment.publicationDate |e $} 14 | {$ basepath $}/list-comments.html 15 | {% endfor %} 16 | 17 | -------------------------------------------------------------------------------- /src/BucketTesting/Validation/Rule/UniqueBucketRule.php: -------------------------------------------------------------------------------- 1 | getBuckets() as $bucket ) { 16 | if ( in_array( $bucket->getName(), $buckets ) ) { 17 | $valid = false; 18 | $errorLogger->addError( 'Duplicate bucket ' . $bucket->getName(), $campaign ); 19 | } 20 | $buckets[] = $bucket->getName(); 21 | } 22 | return $valid; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /tests/Data/GeneratedMailTemplates/Membership_Application_Confirmation.paypal_sustaining_monthly.txt: -------------------------------------------------------------------------------- 1 | mail_introduction_male_lastname_informal 2 | 3 | membership_application_confirmation_sustaining/intro_start 4 | 5 | membership_application_confirmation_sustaining/intro_bez 6 | 7 | membership_application_confirmation_sustaining/intro_end 8 | 9 | membership_application_confirmation_sustaining/contact 10 | 11 | membership_application_confirmation_sustaining/outro 12 | 13 | name_head_of_organization 14 | title_head_of_organization 15 | address_of_organization 16 | 17 | --------------------------------------------------------------------------- 18 | wikimedia_vision 19 | 20 | --------------------------------------------------------------------------- 21 | 22 | address_of_organization 23 | 24 | phone 25 | www.wikimedia.de 26 | 27 | tax_id_organization 28 | -------------------------------------------------------------------------------- /skins/laika/templates/Comment_List.rss.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Spenden an Wikimedia 5 | {$ basepath $}/list-comments.html 6 | Öffentliche Liste der über das Online-Formular der Wikimedia Fördergesellschaft getätigter Spenden 7 | de-de 8 | {$ rssPublicationData|e $} 9 | 10 | {% for comment in comments %} 11 | {$ comment.amount |number_format(2, ',', '.') |e $} Euro von {$ comment.author |e $} 12 | {$ comment.text |e $} 13 | {$ comment.publicationDate |e $} 14 | {$ basepath $}/list-comments.html 15 | {% endfor %} 16 | 17 | -------------------------------------------------------------------------------- /src/Infrastructure/Validation/InternetDomainNameValidator.php: -------------------------------------------------------------------------------- 1 | isDomainBanned( $domain ) && $this->domainHasDnsEntry( $domain ); 17 | } 18 | 19 | private function isDomainBanned( string $domain ): bool { 20 | return in_array( strtolower( trim( $domain ) ), self::BANNED_DOMAINS ); 21 | } 22 | 23 | private function domainHasDnsEntry( string $domain ): bool { 24 | return checkdnsrr( $domain, 'MX' ) || checkdnsrr( $domain, 'A' ) || checkdnsrr( $domain, 'SOA' ); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /tests/Data/GeneratedMailTemplates/Membership_Application_Confirmation.direct_debit_sustaining_quarterly.txt: -------------------------------------------------------------------------------- 1 | mail_introduction_male_lastname_informal 2 | 3 | membership_application_confirmation_sustaining/intro_start 4 | 5 | membership_application_confirmation_sustaining/intro_bez 6 | 7 | membership_application_confirmation_sustaining/intro_end 8 | 9 | membership_application_confirmation_sustaining/contact 10 | 11 | membership_application_confirmation_sustaining/outro 12 | 13 | name_head_of_organization 14 | title_head_of_organization 15 | address_of_organization 16 | 17 | --------------------------------------------------------------------------- 18 | wikimedia_vision 19 | 20 | --------------------------------------------------------------------------- 21 | 22 | address_of_organization 23 | 24 | phone 25 | www.wikimedia.de 26 | 27 | tax_id_organization 28 | -------------------------------------------------------------------------------- /src/Infrastructure/GetConfigCacheKey.php: -------------------------------------------------------------------------------- 1 | 0 ? md5( $fileStats ) : ''; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Presentation/Salutations.php: -------------------------------------------------------------------------------- 1 | >> $salutations 11 | */ 12 | public function __construct( private readonly array $salutations ) { 13 | } 14 | 15 | /** 16 | * @return array>> 17 | */ 18 | public function getList(): array { 19 | return $this->salutations; 20 | } 21 | 22 | /** 23 | * @return array>|null 24 | */ 25 | public function getSalutation( string $value ): ?array { 26 | $data = array_filter( $this->salutations, static fn ( $salutation ) => $salutation['value'] == $value ); 27 | 28 | if ( count( $data ) === 0 ) { 29 | return null; 30 | } 31 | 32 | return reset( $data ); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/Infrastructure/Mail/OperatorMailer.php: -------------------------------------------------------------------------------- 1 | $templateArguments 22 | */ 23 | public function sendMailToOperator( EmailAddress $replyToAddress, string $subject, array $templateArguments = [] ): void { 24 | $this->messenger->sendMessageToOperator( 25 | new Message( 26 | $subject, 27 | $this->template->render( $templateArguments ) 28 | ), 29 | $replyToAddress 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/Data/GeneratedMailTemplates/Membership_Application_Confirmation.direct_debit_active_yearly_receipt_optout.txt: -------------------------------------------------------------------------------- 1 | mail_introduction_male_informal 2 | 3 | membership_application_confirmation_active/intro_start 4 | 5 | membership_application_confirmation_active/intro_bez 6 | 7 | membership_application_confirmation_active/intro_end 8 | 9 | membership_application_confirmation_active/contact membership_application_confirmation_active/receipt 10 | 11 | membership_application_confirmation_active/outro 12 | 13 | name_head_of_organization 14 | title_head_of_organization 15 | address_of_organization 16 | 17 | --------------------------------------------------------------------------- 18 | wikimedia_vision 19 | 20 | --------------------------------------------------------------------------- 21 | 22 | address_of_organization 23 | 24 | phone 25 | www.wikimedia.de 26 | 27 | tax_id_organization 28 | -------------------------------------------------------------------------------- /web/index.php: -------------------------------------------------------------------------------- 1 | bootEnv( __DIR__ . '/../.env' ); 14 | 15 | $env = $_SERVER['APP_ENV'] ?? 'dev'; 16 | $debug = $_SERVER['APP_DEBUG'] ? (bool)$_SERVER['APP_DEBUG'] : false; 17 | 18 | umask( 0002 ); 19 | if ( $debug ) { 20 | umask( 0000 ); 21 | Debug::enable(); 22 | } 23 | 24 | $kernel = new Kernel( $env, $debug ); 25 | $request = Request::createFromGlobals(); 26 | $response = $kernel->handle( $request ); 27 | $response->send(); 28 | $kernel->terminate( $request, $response ); 29 | 30 | -------------------------------------------------------------------------------- /src/Infrastructure/Mail/DonationConfirmationMailerAdapter.php: -------------------------------------------------------------------------------- 1 | templateMailer->sendMail( $recipient, [ 18 | 'recipient' => $templateArguments->recipient, 19 | 'donation' => get_object_vars( $templateArguments->donation ) 20 | ] ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/EventHandlers/PrettifyJsonResponse.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | public static function getSubscribedEvents(): array { 18 | return [ 19 | KernelEvents::RESPONSE => 'onKernelResponse' 20 | ]; 21 | } 22 | 23 | public function onKernelResponse( ResponseEvent $event ): void { 24 | $response = $event->getResponse(); 25 | if ( $response instanceof JsonResponse ) { 26 | $response->setEncodingOptions( JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ); 27 | } 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /tests/TestEnvironmentBootstrapper.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | private array $configurationOverride = []; 16 | 17 | /** 18 | * @param array $config 19 | */ 20 | public function overrideConfiguration( array $config ): void { 21 | $this->configurationOverride = $config; 22 | } 23 | 24 | /** 25 | * @return string[] 26 | */ 27 | protected function getConfiguration(): array { 28 | $config = parent::getConfiguration(); 29 | if ( $this->configurationOverride ) { 30 | $config = \array_replace_recursive( $config, $this->configurationOverride ); 31 | } 32 | return $config; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /tests/EdgeToEdge/TrackingDataTest.php: -------------------------------------------------------------------------------- 1 | modifyConfiguration( [ 'skin' => 'laika' ] ); 18 | $client = $this->createClient(); 19 | $client->request( 'get', '/', [ 20 | self::PARAM_NAME_CAMPAIGN => 'campaign', 21 | self::PARAM_NAME_KEYWORD => 'keyword' 22 | ] ); 23 | 24 | $this->assertEquals( 'campaign/keyword', $client->getRequest()->get( 'trackingCode' ) ); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/Authentication/Token.php: -------------------------------------------------------------------------------- 1 | tokenBytes ); 23 | } 24 | 25 | public function getRawBytes(): string { 26 | return $this->tokenBytes; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /doc/adr/images/ADR27_ER_Diagram_Custom.mmd: -------------------------------------------------------------------------------- 1 | erDiagram 2 | 3 | SPENDEN ||--|| DONOR : has 4 | 5 | DONOR { 6 | string donor_type 7 | bool receipt 8 | bool mailing_list 9 | string email_address 10 | } 11 | 12 | COMPANY_DONOR { 13 | string company_name 14 | } 15 | 16 | SCRUBBED_DONOR { 17 | string original_donor_type 18 | string original_salutation 19 | } 20 | 21 | DONOR_NAME { 22 | string firstname 23 | string lastname 24 | string salutation 25 | string title 26 | } 27 | 28 | DONOR |o--|| COMPANY_DONOR : inherits_from 29 | DONOR |o--|| SCRUBBED_DONOR : inherits_from 30 | 31 | DONOR |o--|| DONOR_ADDRESS : has 32 | DONOR |o--|| DONOR_NAME : has 33 | 34 | DONOR_ADDRESS { 35 | string street_name 36 | string house_number 37 | string postcode 38 | string city 39 | string country_code 40 | } -------------------------------------------------------------------------------- /skins/test/templates/Membership_Application.html.twig: -------------------------------------------------------------------------------- 1 | {% set showMembershipTypeOption = showMembershipTypeOption is defined ? showMembershipTypeOption : 'true' %} 2 | {% set formSlideOpen = false %} 3 | {% set initialPaymentType = paymentTypes|length == 1 ? paymentTypes|first : '' %} 4 | 5 |
6 | 7 | {% if showMembershipTypeOption == 'true' %} 8 | 9 | {% else %} 10 | 11 | 12 | {% endif %} 13 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /src/Infrastructure/Mail/AdminDonationModerationMailerAdapter.php: -------------------------------------------------------------------------------- 1 | templateMailer->sendMail( 18 | $recipient, 19 | [ 20 | 'id' => $templateArguments->donationId, 21 | 'moderationFlags' => $templateArguments->moderationFlags, 22 | 'amount' => $templateArguments->amount 23 | ] 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Infrastructure/Validation/ValidationErrorLogger.php: -------------------------------------------------------------------------------- 1 | > $validationErrors 18 | */ 19 | public function logViolations( string $message, array $fields, array $validationErrors ): void { 20 | if ( $this->hasMoreErrorsThan( 4, $fields ) ) { 21 | return; 22 | } 23 | 24 | $this->logger->warning( $message, $validationErrors ); 25 | } 26 | 27 | /** 28 | * @param int $number 29 | * @param string[] $violations 30 | */ 31 | private function hasMoreErrorsThan( int $number, array $violations ): bool { 32 | return count( $violations ) > $number; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/RebuildDatabaseSchemaTrait.php: -------------------------------------------------------------------------------- 1 | getEntityManager(); 14 | $schemaCreator = new SchemaCreator( $entityManager ); 15 | 16 | try { 17 | $schemaCreator->dropSchema(); 18 | } catch ( \Exception $ex ) { 19 | } 20 | 21 | $schemaCreator->createSchema(); 22 | $entityManager->persist( new PaymentId() ); 23 | $entityManager->persist( new DonationId() ); 24 | $entityManager->persist( new MembershipId() ); 25 | $entityManager->flush(); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/BucketTesting/Logging/BestEffortBucketLogger.php: -------------------------------------------------------------------------------- 1 | caughtException !== null ) { 22 | return; 23 | } 24 | try { 25 | $this->bucketLogger->writeEvent( $event, ...$buckets ); 26 | } catch ( LoggingError $error ) { 27 | $this->caughtException = $error; 28 | $this->errorLogging->error( $error->getMessage(), [ 'exception' => $error ] ); 29 | } 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /tests/SchemaCreator.php: -------------------------------------------------------------------------------- 1 | getSchemaTool()->createSchema( $this->getClassMetaData() ); 18 | } 19 | 20 | public function dropSchema(): void { 21 | $this->getSchemaTool()->dropSchema( $this->getClassMetaData() ); 22 | } 23 | 24 | private function getSchemaTool(): SchemaTool { 25 | return new SchemaTool( $this->entityManager ); 26 | } 27 | 28 | /** 29 | * @return list 30 | * @phpstan-ignore-next-line 31 | */ 32 | private function getClassMetaData(): array { 33 | return $this->entityManager->getMetadataFactory()->getAllMetadata(); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /tests/Unit/Infrastructure/EventHandling/DonationEventEmitterTest.php: -------------------------------------------------------------------------------- 1 | createMock( EventDispatcher::class ); 19 | $dispatcher->expects( $this->once() )->method( 'dispatch' )->with( $event ); 20 | $emitter = new DonationEventEmitter( $dispatcher ); 21 | 22 | $emitter->emit( $event ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Authentication/OldStyleTokens/LenientAuthorizationChecker.php: -------------------------------------------------------------------------------- 1 | createMock( EventDispatcher::class ); 19 | $dispatcher->expects( $this->once() )->method( 'dispatch' )->with( $event ); 20 | $emitter = new MembershipEventEmitter( $dispatcher ); 21 | 22 | $emitter->emit( $event ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /cli/ApplicationConfigValidation/SchemaLoader.php: -------------------------------------------------------------------------------- 1 | fileFetcher->fetchFile( $schema ); 18 | } catch ( FileFetchingException $e ) { 19 | throw new ConfigValidationException( $e->getMessage(), $e->getCode(), $e ); 20 | } 21 | $schema = json_decode( $schemaString ); 22 | if ( $schema === null ) { 23 | throw new ConfigValidationException( 'Error parsing the schema file: ' . json_last_error_msg() ); 24 | } 25 | if ( !is_object( $schema ) ) { 26 | throw new ConfigValidationException( 'Schema must be a JSON object.' ); 27 | } 28 | return $schema; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/BucketTesting/Domain/Model/BucketLogBucket.php: -------------------------------------------------------------------------------- 1 | id; 26 | } 27 | 28 | public function getBucketLog(): BucketLog { 29 | return $this->bucketLog; 30 | } 31 | 32 | public function getName(): string { 33 | return $this->name; 34 | } 35 | 36 | public function getCampaign(): string { 37 | return $this->campaign; 38 | } 39 | 40 | public function setBucketLog( BucketLog $bucketLog ): BucketLogBucket { 41 | $this->bucketLog = $bucketLog; 42 | return $this; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/UrlGeneratorAdapter.php: -------------------------------------------------------------------------------- 1 | $parameters 17 | */ 18 | public function generateAbsoluteUrl( string $routeName, array $parameters = [] ): string { 19 | return $this->urlGenerator->generate( 20 | $routeName, 21 | $parameters, 22 | UrlGeneratorInterface::ABSOLUTE_URL 23 | ); 24 | } 25 | 26 | public function generateRelativeUrl( string $routeName, array $parameters = [] ): string { 27 | return $this->urlGenerator->generate( 28 | $routeName, 29 | $parameters, 30 | UrlGeneratorInterface::ABSOLUTE_PATH 31 | ); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /app/Migrations/Version20221118093850.php: -------------------------------------------------------------------------------- 1 | getTable( 'bucket_log' ); 18 | $table->modifyColumn( 'event_name', [ 'length' => 64 ] ); 19 | } 20 | 21 | public function postUp( Schema $schema ): void { 22 | $this->connection->executeStatement( 'UPDATE bucket_log SET event_name = "membershipApplicationCreated" WHERE event_name = "membershipApplicationCre"' ); 23 | } 24 | 25 | public function down( Schema $schema ): void { 26 | $table = $schema->getTable( 'bucket_log' ); 27 | $table->modifyColumn( 'event_name', [ 'length' => 24 ] ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Infrastructure/Mail/MailTemplateFilenameTraversable.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class MailTemplateFilenameTraversable implements \IteratorAggregate { 11 | 12 | public function __construct( private readonly string $mailTemplatePath ) { 13 | } 14 | 15 | /** 16 | * @return \Iterator 17 | */ 18 | public function getIterator(): \Iterator { 19 | $glob = glob( $this->mailTemplatePath . '/*\.twig' ); 20 | 21 | // we can't reliably trigger glob() returning `false` on Linux systems 22 | // @codeCoverageIgnoreStart 23 | if ( $glob === false ) { 24 | throw new \RuntimeException( sprintf( "Failed to find path name: %s", 25 | var_export( $this->mailTemplatePath . '/*\.twig', true ) ) ); 26 | } 27 | 28 | // @codeCoverageIgnoreEnd 29 | 30 | foreach ( $glob as $fileName ) { 31 | yield basename( $fileName ); 32 | } 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/Infrastructure/Mail/MembershipMailerAdapter.php: -------------------------------------------------------------------------------- 1 | templateMailer->sendMail( $recipient, get_object_vars( $templateArguments ) ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /config/packages/prod/monolog.yaml: -------------------------------------------------------------------------------- 1 | monolog: 2 | handlers: 3 | main: 4 | type: fingers_crossed 5 | action_level: notice 6 | handler: group 7 | excluded_http_codes: [ 404, 405 ] 8 | buffer_size: 50 # How many messages should be saved? Prevent memory leaks 9 | group: 10 | type: group 11 | members: ['logfile', 'errbit_wrapper'] 12 | logfile: 13 | type: stream 14 | path: "%kernel.logs_dir%/application.log" 15 | level: info 16 | errbit_wrapper: 17 | type: fallbackgroup 18 | members: [ 'errbit', 'errbit_errors' ] 19 | errbit: 20 | type: service 21 | id: "app.errbit_logger" 22 | # Level must be configured on service until https://github.com/symfony/monolog-bundle/issues/322 is implemented 23 | errbit_errors: 24 | type: stream 25 | path: "%kernel.logs_dir%/errbit_errors.log" 26 | 27 | console: 28 | type: console 29 | process_psr_3_messages: false 30 | channels: [ '!event', '!doctrine', '!console' ] -------------------------------------------------------------------------------- /config/packages/uat/monolog.yaml: -------------------------------------------------------------------------------- 1 | monolog: 2 | handlers: 3 | main: 4 | type: fingers_crossed 5 | action_level: notice 6 | handler: group 7 | excluded_http_codes: [ 404, 405 ] 8 | buffer_size: 50 # How many messages should be saved? Prevent memory leaks 9 | group: 10 | type: group 11 | members: ['logfile', 'errbit_wrapper'] 12 | logfile: 13 | type: stream 14 | path: "%kernel.logs_dir%/application.log" 15 | level: info 16 | errbit_wrapper: 17 | type: fallbackgroup 18 | members: [ 'errbit', 'errbit_errors' ] 19 | errbit: 20 | type: service 21 | id: "app.errbit_logger" 22 | # Level must be configured on service until https://github.com/symfony/monolog-bundle/issues/322 is implemented 23 | errbit_errors: 24 | type: stream 25 | path: "%kernel.logs_dir%/errbit_errors.log" 26 | 27 | console: 28 | type: console 29 | process_psr_3_messages: false 30 | channels: [ '!event', '!doctrine', '!console' ] -------------------------------------------------------------------------------- /skins/laika/templates/page_layouts/base.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'Base_Layout.html.twig' %} 2 | {% set page_works_without_js = true %} 3 | {% set title = site_metadata.page_titles[page_id] %} 4 | {% set content = web_content("pages/#{page_id}") %} 5 | 6 | {% block title %}{$ page_title( page_id ) $}{% endblock %} 7 | 8 | {% block main %} 9 |
10 | 14 |
15 | 24 | 25 | {% endblock %} 26 | 27 | {% block scripts %} 28 | {$ parent() $} 29 | 30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /web/res/js/matomo-loader.js: -------------------------------------------------------------------------------- 1 | var matomoSettings = document.getElementById('matomo-loader'); 2 | var initialPaymentType = matomoSettings.dataset.initialPaymentType || ''; 3 | var siteId = matomoSettings.dataset.siteId; 4 | 5 | var _paq = _paq || []; 6 | _paq.push(['requireCookieConsent']); 7 | _paq.push( ['setCustomDimension', 2, 'laika' ] ); 8 | _paq.push( [ 'trackPageView' ] ); 9 | _paq.push( [ 'enableLinkTracking' ] ); 10 | _paq.push( [ 'trackVisibleContentImpressions' ] ); 11 | if ( initialPaymentType ) { 12 | _paq.push( [ 'setCustomDimension', 1, initialPaymentType ] ); 13 | } 14 | 15 | ( function () { 16 | var u = matomoSettings.dataset.matomoBaseUrl; 17 | _paq.push( [ 'setTrackerUrl', u + 'piwik.php' ] ); 18 | _paq.push( [ 'setSiteId', siteId ] ); 19 | var d = document, 20 | g = d.createElement( 'script' ), 21 | s = d.getElementsByTagName( 'script' )[ 0 ]; 22 | g.type = 'text/javascript'; 23 | g.async = true; 24 | g.defer = true; 25 | g.src = u + 'piwik.js'; 26 | s.parentNode.insertBefore( g, s ); 27 | } ) (); 28 | -------------------------------------------------------------------------------- /app/Controllers/Membership/ShowMembershipFeeChangeController.php: -------------------------------------------------------------------------------- 1 | query->get( 'uuid', '' ); 19 | 20 | $htmlPresenter = $ffFactory->newMembershipFeeChangeHTMLPresenter(); 21 | 22 | $feeUpgradeUseCase = $ffFactory->newMembershipFeeUpgradeUseCase(); 23 | $feeUpgradeUseCase->showFeeChange( $uuidFromRequest, $htmlPresenter ); 24 | 25 | return $htmlPresenter->getHTMLResponse(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Infrastructure/WordListFileReader.php: -------------------------------------------------------------------------------- 1 | fileName === '' ) { 24 | return []; 25 | } 26 | 27 | try { 28 | $content = $this->fileFetcher->fetchFile( $this->fileName ); 29 | } catch ( FileFetchingException $e ) { 30 | // The error will be logged and we don't want to bring down the application with missing files 31 | $content = ''; 32 | } 33 | 34 | return array_values( array_filter( array_map( 'trim', explode( "\n", $content ) ) ) ); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/Infrastructure/Mail/MailFormatter.php: -------------------------------------------------------------------------------- 1 | getName() !== self::TIMEZONE ) { 18 | throw new \InvalidArgumentException( sprintf( 'CampaignDates must have time zone "%s".', self::TIMEZONE ) ); 19 | } 20 | parent::__construct( $time, new DateTimeZone( self::TIMEZONE ) ); 21 | } 22 | 23 | public static function createFromString( string $time, ?DateTimeZone $timezone = null ): self { 24 | if ( $timezone === null ) { 25 | return new self( $time ); 26 | } 27 | $instance = new \DateTime( $time, $timezone ); 28 | $instance->setTimezone( new DateTimeZone( self::TIMEZONE ) ); 29 | return new self( $instance->format( \DateTime::ISO8601 ) ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/EdgeToEdge/BucketVariableTest.php: -------------------------------------------------------------------------------- 1 | modifyConfiguration( [ 'skin' => 'laika' ] ); 21 | $client = $this->createClient(); 22 | 23 | $client->request( 'GET', '/' ); 24 | 25 | $applicationVars = $this->getDataApplicationVars( $client->getCrawler() ); 26 | $this->assertContains( 'campaigns.show_unicorns.default', $applicationVars->selectedBuckets ); 27 | $this->assertContains( 'pfu', $applicationVars->allowedCampaignParameters ); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/BucketTesting/Domain/Model/Bucket.php: -------------------------------------------------------------------------------- 1 | name; 21 | } 22 | 23 | public function getId(): string { 24 | return 'campaigns.' . $this->getCampaign()->getName() . '.' . $this->getName(); 25 | } 26 | 27 | public function getCampaign(): Campaign { 28 | return $this->campaign; 29 | } 30 | 31 | public function isDefaultBucket(): bool { 32 | return $this->defaultBucket; 33 | } 34 | 35 | /** 36 | * @return array 37 | */ 38 | public function getParameters(): array { 39 | return [ $this->campaign->getUrlKey() => $this->campaign->getIndexByBucket( $this ) ]; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/Presentation/Presenters/CommentListJsonPresenter.php: -------------------------------------------------------------------------------- 1 | > 14 | */ 15 | public function present( CommentList $commentList ): array { 16 | // TODO Translate keys into English once old skins are phased out 17 | return array_map( 18 | static function ( Comment $comment ) { 19 | return [ 20 | 'betrag' => $comment->donationAmount, 21 | 'spender' => $comment->authorName, 22 | 'kommentar' => $comment->commentText, 23 | 'datum' => $comment->donationTime->format( 'r' ), 24 | 'lokalisiertes_datum' => $comment->donationTime->format( 'd.m.Y \u\m H:i \U\h\r' ), 25 | 'id' => $comment->donationId, 26 | ]; 27 | }, 28 | $commentList->toArray() 29 | ); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /tests/Fixtures/BucketLoggerSpy.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | private array $events = []; 17 | 18 | public function writeEvent( LoggingEvent $event, Bucket ...$buckets ): void { 19 | $this->events[] = [ 20 | 'event' => $event, 21 | 'buckets' => $buckets 22 | ]; 23 | } 24 | 25 | public function getEventCount(): int { 26 | return count( $this->events ); 27 | } 28 | 29 | public function getFirstEvent(): LoggingEvent { 30 | return $this->events[0]['event']; 31 | } 32 | 33 | /** 34 | * @return Bucket[] 35 | */ 36 | public function getFirstBuckets(): array { 37 | return $this->events[0]['buckets']; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/Fixtures/DonationUrlAuthenticationLoaderStub.php: -------------------------------------------------------------------------------- 1 | $authorizationParameters 13 | */ 14 | public function __construct( private readonly array $authorizationParameters = [] ) { 15 | } 16 | 17 | public function getDonationUrlAuthenticator( int $donationId ): URLAuthenticator { 18 | return new UrlAuthenticatorStub(); 19 | } 20 | 21 | /** 22 | * @param int $donationId 23 | * @param array $parameters 24 | * 25 | * @return array 26 | */ 27 | public function addDonationAuthorizationParameters( int $donationId, array $parameters ): array { 28 | return [ 29 | ...$parameters, 30 | ...$this->authorizationParameters 31 | ]; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Infrastructure/Translation/JsonTranslator.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | private array $messages; 15 | 16 | public function __construct( private readonly FileFetcher $fileFetcher ) { 17 | $this->messages = []; 18 | } 19 | 20 | public function addFile( string $fileName ): self { 21 | $contents = $this->fileFetcher->fetchFile( $fileName ); 22 | $this->messages = array_merge( $this->messages, json_decode( $contents, true ) ); 23 | return $this; 24 | } 25 | 26 | public function trans( string $messageKey, array $parameters = [] ): string { 27 | if ( !isset( $this->messages[$messageKey] ) ) { 28 | throw new \InvalidArgumentException( "Unknown translation key: $messageKey" ); 29 | } 30 | return str_replace( array_keys( $parameters ), array_values( $parameters ), $this->messages[$messageKey] ); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/Factories/DoctrineFactory.php: -------------------------------------------------------------------------------- 1 | entityManager = null; 22 | } 23 | 24 | public function getConnection(): Connection { 25 | return $this->connection; 26 | } 27 | 28 | public function getEntityManager(): EntityManager { 29 | if ( $this->entityManager === null ) { 30 | $connection = $this->getConnection(); 31 | $this->entityManager = new EntityManager( $connection, $this->config, new EventManager() ); 32 | $this->contextFactories->registerCustomTypes( $connection ); 33 | } 34 | return $this->entityManager; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/Controllers/Donation/ViewCommentController.php: -------------------------------------------------------------------------------- 1 | getLayoutTemplate( 15 | 'Donation_Comment.html.twig' 16 | ); 17 | 18 | return new Response( 19 | $template->render( 20 | [ 21 | 'donationId' => (int)$request->query->get( 'donationId', '' ), 22 | 'updateToken' => $request->query->get( 'updateToken', '' ), 23 | 'cancelUrl' => $ffFactory->getUrlGenerator()->generateRelativeUrl( 24 | 'show-donation-confirmation', 25 | [ 26 | 'id' => (int)$request->query->get( 'donationId', '' ), 27 | 'accessToken' => $request->query->get( 'accessToken', '' ) 28 | ] 29 | ) 30 | ] 31 | ) 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Presentation/Presenters/CommentListHtmlPresenter.php: -------------------------------------------------------------------------------- 1 | template->render( [ 18 | 'comments' => array_map( 19 | static function ( Comment $comment ) { 20 | return [ 21 | 'amount' => $comment->donationAmount, 22 | 'author' => $comment->authorName, 23 | 'text' => $comment->commentText, 24 | 'publicationDate' => $comment->donationTime->format( 'r' ), 25 | ]; 26 | }, 27 | $commentList->toArray() 28 | ), 29 | 'page' => $pageNumber, 30 | 'max_pages' => 100 31 | ] ); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /app/EventHandlers/StoreBucketSelection.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | public static function getSubscribedEvents(): array { 23 | return [ 24 | KernelEvents::REQUEST => [ 'setSelectedBuckets', self::PRIORITY ], 25 | ]; 26 | } 27 | 28 | public function setSelectedBuckets( RequestEvent $event ): void { 29 | $request = $event->getRequest(); 30 | $selector = $this->factory->getBucketSelector(); 31 | $this->factory->setSelectedBuckets( $selector->selectBuckets( $request->query->all() ) ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Infrastructure/TranslationsCollector.php: -------------------------------------------------------------------------------- 1 | translationFiles[] = $transFile; 22 | } 23 | 24 | /** 25 | * @return array 26 | */ 27 | public function collectTranslations(): array { 28 | $result = []; 29 | 30 | foreach ( $this->translationFiles as $transFile ) { 31 | $jsonFileContent = json_decode( $this->fileFetcher->fetchFile( $transFile ), true ); 32 | if ( !is_array( $jsonFileContent ) ) { 33 | throw new RuntimeException( 'The file must contain a key,value data structure.' ); 34 | } 35 | $result = array_merge( $result, $jsonFileContent ); 36 | } 37 | 38 | return $result; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/Unit/Factories/TwigFactoryTest.php: -------------------------------------------------------------------------------- 1 | true ], '/tmp' ); 18 | 19 | $cache = $factory->getCache(); 20 | 21 | $this->assertInstanceOf( FilesystemCache::class, $cache ); 22 | $this->assertStringStartsWith( '/tmp/', $cache->generateKey( 'foo', '' ) ); 23 | } 24 | 25 | public function testDisabledCacheReturnsNullCache(): void { 26 | $factory = new TwigFactoryTestImplementation( [ 'enable-cache' => false ], '/tmp' ); 27 | 28 | $cache = $factory->getCache(); 29 | 30 | $this->assertInstanceOf( NullCache::class, $cache ); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /tests/Integration/MailTemplateFilenameTraversableTest.php: -------------------------------------------------------------------------------- 1 | get( FunFunFactory::class ); 20 | $mailTemplatePaths = $funFunFactory->newMailTemplateFilenameTraversable(); 21 | 22 | $pathArray = iterator_to_array( $mailTemplatePaths ); 23 | 24 | $this->assertContains( 'Contact_Confirm_to_User.txt.twig', $pathArray ); 25 | $this->assertContains( 'Subscription_Request.txt.twig', $pathArray ); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /tests/Fixtures/BucketSelectionSpy.php: -------------------------------------------------------------------------------- 1 | bucketSelections = []; 19 | } 20 | 21 | public function selectBucketForCampaign( Campaign $campaign ): Bucket { 22 | $bucket = $this->bucketSelector->selectBucketForCampaign( $campaign ); 23 | if ( $bucket === null ) { 24 | throw new \LogicException( 'Wrapped bucket selector must return a bucket' ); 25 | } 26 | $this->bucketSelections[] = $bucket; 27 | return $bucket; 28 | } 29 | 30 | public function bucketWasSelected(): bool { 31 | return count( $this->bucketSelections ) > 0; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/MailTemplateFixtures/VariantSettingsGenerator.php: -------------------------------------------------------------------------------- 1 | variants = $variants; 15 | } 16 | 17 | /** 18 | * @return iterable 19 | */ 20 | public function getTemplateSettings(): iterable { 21 | foreach ( $this->variants as $variant ) { 22 | $templateData = array_merge( $this->templateData, $variant->additionalTemplateData ); 23 | yield new TemplateSettings( $this->templateName, $this->getId( $variant->name ), $templateData ); 24 | } 25 | } 26 | 27 | public function getId( string $variantName ): string { 28 | return basename( $this->templateName, '.txt.twig' ) . '.' . $variantName; 29 | } 30 | 31 | public function getTemplateName(): string { 32 | return $this->templateName; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /app/Controllers/Membership/ShowMembershipConfirmationController.php: -------------------------------------------------------------------------------- 1 | getTranslationCollector()->addTranslationFile( $ffFactory->getI18nDirectory() . '/messages/paymentTypes.json' ); 16 | $presenter = $ffFactory->newMembershipApplicationConfirmationHtmlPresenter(); 17 | 18 | $useCase = $ffFactory->newMembershipApplicationConfirmationUseCase( 19 | $presenter, 20 | $request->query->get( 'accessToken', '' ) 21 | ); 22 | 23 | $useCase->showConfirmation( new ShowAppConfirmationRequest( (int)$request->query->get( 'id', 0 ) ) ); 24 | return new Response( $presenter->getHtml() ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Presentation/Presenters/AddSubscriptionHtmlPresenter.php: -------------------------------------------------------------------------------- 1 | $formData 22 | */ 23 | public function present( ValidationResponse $subscriptionResponse, array $formData ): string { 24 | $errors = []; 25 | /** @var ConstraintViolation $constraintViolation */ 26 | foreach ( $subscriptionResponse->getValidationErrors() as $constraintViolation ) { 27 | $errors[$constraintViolation->getSource()] = $constraintViolation->getMessageIdentifier(); 28 | } 29 | return $this->template->render( array_merge( $formData, [ 'errors' => $errors ] ) ); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /tests/Unit/Infrastructure/Mail/BasicMailSubjectRendererTest.php: -------------------------------------------------------------------------------- 1 | value; 18 | $this->assertSame( 19 | 'mail_subject_getintouch', 20 | $this->newDonationConfirmationMailSubjectRenderer()->render( $templateArguments ) 21 | ); 22 | } 23 | 24 | public function newDonationConfirmationMailSubjectRenderer(): BasicMailSubjectRenderer { 25 | return new BasicMailSubjectRenderer( 26 | new FakeTranslator(), 27 | 'mail_subject_getintouch' 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /bin/doctrine: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | bootEnv( __DIR__ . '/../.env' ); 21 | 22 | $kernel = new Kernel( $_SERVER['APP_ENV'], false ); 23 | $kernel->boot(); 24 | $factory = $kernel->getContainer()->get( FunFunFactory::class ); 25 | 26 | $cli = ConsoleRunner::createApplication( 27 | new SingleManagerProvider( $factory->getEntityManager() ), 28 | $factory->newDoctrineMigrationCommands() 29 | ); 30 | $cli->run(); 31 | -------------------------------------------------------------------------------- /src/Infrastructure/Mail/ErrorHandlingMailerDecorator.php: -------------------------------------------------------------------------------- 1 | $templateArguments 21 | */ 22 | public function sendMail( EmailAddress $recipient, array $templateArguments = [] ): void { 23 | try { 24 | $this->templateBasedMailer->sendMail( $recipient, $templateArguments ); 25 | } catch ( \RuntimeException $e ) { 26 | $message = $e->getMessage(); 27 | $exception = $e; 28 | $previous = $e->getPrevious(); 29 | if ( $previous !== null ) { 30 | $message .= ' ' . $previous->getMessage(); 31 | $exception = $previous; 32 | } 33 | $this->logger->error( $message, [ 'exception' => $exception ] ); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Authentication/AuthenticationContextFactory.php: -------------------------------------------------------------------------------- 1 | registerAuthenticationDomain( $connection ); 21 | } 22 | 23 | private function registerAuthenticationDomain( Connection $connection ): void { 24 | static $isRegistered = false; 25 | if ( $isRegistered ) { 26 | return; 27 | } 28 | Type::addType( 'AuthenticationBoundedContext', 'WMDE\Fundraising\Frontend\Authentication\DataAccess\AuthenticationBoundedContext' ); 29 | $connection->getDatabasePlatform()->registerDoctrineTypeMapping( 'AuthenticationBoundedContext', 'AuthenticationBoundedContext' ); 30 | $isRegistered = true; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Infrastructure/EventHandling/EventDispatcher.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | protected array $listeners = []; 16 | 17 | /** 18 | * @param class-string $eventClassName 19 | * @param callable $listener 20 | */ 21 | public function addEventListener( string $eventClassName, callable $listener ): self { 22 | if ( empty( $this->listeners[$eventClassName] ) ) { 23 | $this->listeners[$eventClassName] = []; 24 | } 25 | $this->listeners[$eventClassName][] = $listener; 26 | return $this; 27 | } 28 | 29 | public function dispatch( DonationEvent|MembershipEvent $event ): void { 30 | $eventName = get_class( $event ); 31 | if ( empty( $this->listeners[$eventName] ) ) { 32 | return; 33 | } 34 | array_map( static fn ( $handler ) => \call_user_func( $handler, $event ), $this->listeners[$eventName] ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/Unit/Cli/FeatureToggleParserTest.php: -------------------------------------------------------------------------------- 1 | assertSame( 18 | [ 19 | 'campaigns.test_campaign.test_bucket_a', 20 | 'campaigns.test_campaign.test_bucket_b', 21 | 'campaigns.another_test_campaign.test_bucket_c', 22 | 'campaigns.another_test_campaign.test_bucket_d', 23 | 'campaigns.another_test_campaign.test_bucket_e', 24 | 'campaigns.another_test_campaign.test_bucket_c', 25 | 'campaigns.another_test_campaign.test_bucket_d', 26 | ], 27 | FeatureToggleParser::getFeatureToggleChecks( self::CHOICE_FACTORY_LOCATION ) 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Infrastructure/Mail/DonationConfirmationMailSubjectRenderer.php: -------------------------------------------------------------------------------- 1 | $templateArguments 21 | */ 22 | public function render( array $templateArguments = [] ): string { 23 | if ( 24 | is_array( $templateArguments['donation'] ) && 25 | isset( $templateArguments['donation']['paymentType'] ) && 26 | $templateArguments['donation']['paymentType'] === PaymentType::BankTransfer->value 27 | ) { 28 | return $this->translator->trans( $this->bankTransferSubjectKey ); 29 | } 30 | return $this->translator->trans( $this->defaultSubjectKey ); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /app/config/migrations.php: -------------------------------------------------------------------------------- 1 | [ 6 | 'table_name' => 'doctrine_migration_versions', 7 | 'version_column_name' => 'version', 8 | 'version_column_length' => 250, 9 | 'executed_at_column_name' => 'executed_at', 10 | 'execution_time_column_name' => 'execution_time', 11 | ], 12 | 13 | 'migrations_paths' => [ 14 | 'WMDE\Fundraising\Frontend\App\Migrations' => './app/Migrations', 15 | 'WMDE\Fundraising\AddressChangeContext\DataAccess\Migrations' => './vendor/wmde/fundraising-address-change/src/DataAccess/Migrations', 16 | 'WMDE\Fundraising\DonationContext\DataAccess\Migrations' => './vendor/wmde/fundraising-donations/src/DataAccess/Migrations', 17 | 'WMDE\Fundraising\MembershipContext\DataAccess\Migrations' => './vendor/wmde/fundraising-memberships/src/DataAccess/Migrations', 18 | 'WMDE\Fundraising\PaymentContext\DataAccess\Migrations' => './vendor/wmde/fundraising-payments/src/DataAccess/Migrations', 19 | ], 20 | 21 | 'all_or_nothing' => true, 22 | 'transactional' => false, 23 | 'check_database_platform' => true, 24 | 'organize_migrations' => 'none', 25 | 'connection' => null, 26 | 'em' => null, 27 | ]; 28 | -------------------------------------------------------------------------------- /src/Infrastructure/Mail/MembershipConfirmationMailSubjectRenderer.php: -------------------------------------------------------------------------------- 1 | activeMembershipSubject = $activeMembershipSubject; 20 | } 21 | 22 | /** 23 | * @param array $templateArguments 24 | */ 25 | public function render( array $templateArguments = [] ): string { 26 | if ( $templateArguments['membershipType'] === MembershipApplication::ACTIVE_MEMBERSHIP ) { 27 | return $this->translator->trans( $this->activeMembershipSubject ); 28 | } 29 | return $this->translator->trans( $this->sustainingMembershipSubject ); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /tests/EdgeToEdge/MatomoTest.php: -------------------------------------------------------------------------------- 1 | modifyConfiguration( [ 'skin' => 'laika' ] ); 20 | } 21 | 22 | public function testMatomoScriptGetsEmbedded(): void { 23 | $client = $this->createClient(); 24 | $client->request( 'GET', '/' ); 25 | $this->assertStringContainsString( '', $client->getResponse()->getContent() ?: '' ); 26 | } 27 | 28 | public function testConfigParametersAreUsed(): void { 29 | $client = $this->createClient(); 30 | $client->request( 'GET', '/' ); 31 | 32 | $this->assertStringContainsString( 'tracking.wikimedia.de', $client->getResponse()->getContent() ?: '' ); 33 | $this->assertStringContainsString( 'idsite=1234', $client->getResponse()->getContent() ?: '' ); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/Infrastructure/SubmissionRateLimit.php: -------------------------------------------------------------------------------- 1 | get( $this->sessionKey, null ); 20 | if ( !( $lastSubmission instanceof \DateTimeImmutable ) ) { 21 | return true; 22 | } 23 | 24 | $minNextTimestamp = $lastSubmission->add( $this->intervalBetweenSubmissions ); 25 | if ( $minNextTimestamp > new \DateTime() ) { 26 | return false; 27 | } 28 | 29 | return true; 30 | } 31 | 32 | public function setRateLimitCookie( SessionInterface $session ): void { 33 | $lastSubmission = $session->get( $this->sessionKey, null ); 34 | if ( !( $lastSubmission instanceof \DateTimeImmutable ) ) { 35 | $session->set( $this->sessionKey, new \DateTimeImmutable() ); 36 | } 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/UseCases/GetInTouch/GetInTouchRequest.php: -------------------------------------------------------------------------------- 1 | firstName; 22 | } 23 | 24 | public function getLastName(): string { 25 | return $this->lastName; 26 | } 27 | 28 | public function getEmailAddress(): string { 29 | return $this->emailAddress; 30 | } 31 | 32 | public function getDonationNumber(): string { 33 | return $this->donationNumber; 34 | } 35 | 36 | public function getSubject(): string { 37 | return $this->subject; 38 | } 39 | 40 | public function getCategory(): string { 41 | return $this->category; 42 | } 43 | 44 | public function getMessageBody(): string { 45 | return $this->messageBody; 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /tests/Fixtures/InMemoryTokenRepository.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | private array $tokens = []; 16 | 17 | public function __construct( AuthenticationToken ...$tokens ) { 18 | foreach ( $tokens as $token ) { 19 | $this->storeToken( $token ); 20 | } 21 | } 22 | 23 | public function storeToken( AuthenticationToken $token ): void { 24 | $this->tokens[$token->id . $token->authenticationBoundedContext->value] = $token; 25 | } 26 | 27 | public function getTokenById( int $id, AuthenticationBoundedContext $authenticationDomain ): AuthenticationToken { 28 | return $this->tokens[$id . $authenticationDomain->value] ?? new NullToken( $id, $authenticationDomain ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/Controllers/AddressChange/ShowAddressChangeSuccessController.php: -------------------------------------------------------------------------------- 1 | get( 'addressToken', '' ); 16 | if ( $addressToken === '' ) { 17 | throw new AccessDeniedException( 'address_change_no_token_in_request' ); 18 | } 19 | 20 | $addressChangeRepository = $ffFactory->getAddressChangeRepository(); 21 | $addressChange = $addressChangeRepository->getAddressChangeByUuids( $addressToken, $addressToken ); 22 | 23 | return new Response( $ffFactory->getLayoutTemplate( 'AddressUpdateSuccess.html.twig' )->render( [ 24 | 'message' => $request->attributes->get( 'successMessage' ), 25 | 'addressToken' => $addressToken, 26 | 'receipt' => $addressChange->isOptedIntoDonationReceipt() 27 | ] ) ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Unit/BucketTesting/BucketTest.php: -------------------------------------------------------------------------------- 1 | newCampaign(); 28 | $bucketA = new Bucket( 'a', $campaign, Bucket::DEFAULT ); 29 | $bucketB = new Bucket( 'b', $campaign, Bucket::NON_DEFAULT ); 30 | 31 | $campaign->addBucket( $bucketA )->addBucket( $bucketB ); 32 | 33 | $this->assertEquals( [ 't1' => 0 ], $bucketA->getParameters() ); 34 | $this->assertEquals( [ 't1' => 1 ], $bucketB->getParameters() ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/Migrations/Version20230823082138.php: -------------------------------------------------------------------------------- 1 | createTable( 'legacy_auth_tokens' ); 17 | $table->addColumn( 'id', 'integer', [ 'unsigned' => true ] ); 18 | $table->addColumn( 'authentication_context', 'string', [ 'length' => 16 ] ); 19 | $table->addColumn( 'access_token', 'string', [ 'length' => 64 ] ); 20 | $table->addColumn( 'update_token', 'string', [ 'length' => 64 ] ); 21 | $table->addColumn( 'update_token_expiry', 'datetime', [ 'notnull' => false ] ); 22 | $table->setPrimaryKey( [ 'id', 'authentication_context' ] ); 23 | $table->addIndex( [ 'access_token' ], 'access_token_idx' ); 24 | $table->addIndex( [ 'update_token' ], 'update_token_idx' ); 25 | } 26 | 27 | public function down( Schema $schema ): void { 28 | $schema->dropTable( 'legacy_auth_tokens' ); 29 | } 30 | } 31 | --------------------------------------------------------------------------------