[
  {
    "path": ".blackfire.yaml",
    "content": "scenarios: |\n    #!blackfire-player\n\n    group login\n        visit url('/login')\n        submit button(\"Sign in\")\n            param username \"admin\"\n            param password \"admin\"\n            expect status_code() == 302\n\n    scenario\n        name \"Submit a comment on the Amsterdam conference page\"\n        include login\n        visit url('/fr/conference/amsterdam-2019')\n            expect status_code() == 200\n        submit button(\"Submit\")\n            param comment_form[author] 'Fabien'\n            param comment_form[email] 'me@example.com'\n            param comment_form[text] 'Such a good conference!'\n            param comment_form[photo] file(fake('image', '/tmp', 400, 300, 'cats'), 'awesome-cat.jpg')\n            expect status_code() == 302\n        follow\n            expect status_code() == 200\n            expect not(body() matches \"/Such a good conference/\")\n            # Wait for the workflow to validate the submissions\n            wait 5000\n        when env != \"prod\"\n            visit url(webmail_url ~ '/messages')\n                expect status_code() == 200\n                set message_ids json(\"[*].id\")\n            with message_id in message_ids\n                visit url(webmail_url ~ '/messages/' ~ message_id ~ '.html')\n                    expect status_code() == 200\n                    set accept_url css(\"table a\").first().attr(\"href\")\n                visit url(accept_url)\n                    # we don't check the status code as we can deal\n                    # with \"old\" messages which do not exist anymore\n                    # in the DB (would be a 404 then)\n        when env == \"prod\"\n            visit url('/admin/?entity=Comment&action=list')\n                expect status_code() == 200\n                set comment_ids css('table.table tbody tr').extract('data-id')\n            with id in comment_ids\n                visit url('/admin/comment/review/' ~ id)\n                    # we don't check the status code as we scan all comments,\n                    # including the ones already reviewed\n        visit url('/fr/')\n            wait 5000\n        visit url('/fr/conference/amsterdam-2019')\n            expect body() matches \"/Such a good conference/\"\n"
  },
  {
    "path": ".gitignore",
    "content": "/public/uploads\n\n###> symfony/framework-bundle ###\n/.env.local\n/.env.local.php\n/.env.*.local\n/config/secrets/prod/prod.decrypt.private.php\n/public/bundles/\n/var/\n/vendor/\n###< symfony/framework-bundle ###\n\n###> symfony/phpunit-bridge ###\n.phpunit\n.phpunit.result.cache\n/phpunit.xml\n###< symfony/phpunit-bridge ###\n\n###> symfony/webpack-encore-bundle ###\n/node_modules/\n/public/build/\nnpm-debug.log\nyarn-error.log\n###< symfony/webpack-encore-bundle ###\n"
  },
  {
    "path": ".symfony/config.vcl",
    "content": "acl profile {\n   # Authorize the local IP address (replace with the IP found above)\n   \"a.b.c.d\";\n   # Authorize Blackfire servers\n   \"46.51.168.2\";\n   \"54.75.240.245\";\n}\n\nsub vcl_recv {\n    set req.backend_hint = application.backend();\n    set req.http.Surrogate-Capability = \"abc=ESI/1.0\";\n\n    if (req.method == \"PURGE\") {\n        if (req.http.x-purge-token != \"PURGE_NOW\") {\n            return(synth(405));\n        }\n        return (purge);\n    }\n\n    # Don't profile ESI requests\n    if (req.esi_level > 0) {\n        unset req.http.X-Blackfire-Query;\n    }\n\n    # Bypass Varnish when the profile request comes from a known IP\n    if (req.http.X-Blackfire-Query && client.ip ~ profile) {\n        return (pass);\n    }\n}\n\nsub vcl_backend_response {\n    if (beresp.http.Surrogate-Control ~ \"ESI/1.0\") {\n        unset beresp.http.Surrogate-Control;\n        set beresp.do_esi = true;\n    }\n}\n"
  },
  {
    "path": ".symfony/routes.yaml",
    "content": "\"https://spa.{all}/\": { type: upstream, upstream: \"spa:http\" }\n\"http://spa.{all}/\": { type: redirect, to: \"https://spa.{all}/\" }\n\n\"https://{all}/\": { type: upstream, upstream: \"varnish:http\", cache: { enabled: false } }\n\"http://{all}/\": { type: redirect, to: \"https://{all}/\" }\n"
  },
  {
    "path": ".symfony/services.yaml",
    "content": "db:\n    type: postgresql:11\n    disk: 1024\n    size: S\n\nrediscache:\n    type: redis:5.0\n\nqueue:\n    type: rabbitmq:3.7\n    disk: 1024\n    size: S\n\nvarnish:\n    type: varnish:6.0\n    relationships:\n        application: 'app:http'\n    configuration:\n        vcl: !include\n            type: string\n            path: config.vcl\n\nfiles:\n    type: network-storage:1.0\n    disk: 256\n"
  },
  {
    "path": ".symfony.cloud.yaml",
    "content": "name: app\n\ntype: php:7.3\n\nruntime:\n    extensions:\n        - blackfire\n        - xsl\n        - amqp\n        - redis\n        - pdo_pgsql\n        - apcu\n        - mbstring\n        - sodium\n        - ctype\n        - iconv\n        \n\nbuild:\n    flavor: none\n\nrelationships:\n    database: \"db:postgresql\"\n    redis: \"rediscache:redis\"\n    rabbitmq: \"queue:rabbitmq\"\n\nweb:\n    locations:\n        \"/\":\n            root: \"public\"\n            expires: 1h\n            passthru: \"/index.php\"\n\ndisk: 512\n\nmounts:\n    \"/var\": { source: local, source_path: var }\n    \"/public/uploads\": { source: service, service: files, source_path: uploads }\n\nhooks:\n    build: |\n        set -x -e\n\n        curl -s https://get.symfony.com/cloud/configurator | (>&2 bash)\n        (>&2 symfony-build)\n\n    deploy: |\n        set -x -e\n\n        (>&2 symfony-deploy)\n\ncrons:\n    comment_cleanup:\n        # Cleanup every night at 11.50 pm (UTC).\n        spec: '50 23 * * *'\n        cmd: |\n            if [ \"$SYMFONY_BRANCH\" = \"master\" ]; then\n                croncape symfony console app:comment:cleanup\n            fi\n\nworkers:\n    messages:\n        commands:\n            start: |\n                set -x -e\n\n                (>&2 symfony-deploy)\n                php bin/console messenger:consume async -vv --time-limit 3600 --memory-limit=128M\n"
  },
  {
    "path": "Makefile",
    "content": "SHELL := /bin/bash\n\ntests:\n\tsymfony console doctrine:fixtures:load -n\n\tsymfony run bin/phpunit\n.PHONY: tests\n"
  },
  {
    "path": "assets/css/_variables.scss",
    "content": "\n// Colors\n\n$white: #fff;\n$gray-100: #f5f5f5;\n$gray-200: #e9ecef;\n$gray-300: #ddd;\n$gray-400: #ced4da;\n$gray-500: #adb5bd;\n$gray-600: #868e96;\n$gray-700: #495057;\n$gray-800: #343a40;\n$gray-900: #212529;\n$black: #000;\n\n$blue: #175fc9;\n$pink: #ff737c;\n$red: #d9534f;\n$yellow: #fcee60;\n$orange: #ffae73;\n$green: #02B875;\n$teal: #55e7cc;\n$cyan: #8ce6ff;\n\n$primary: $blue;\n$secondary: #666;\n$success: $green;\n$info: $cyan;\n$warning: $yellow;\n$danger: $red;\n$light: $gray-100;\n$dark: $gray-800;\n\n$theme-colors: (\n    \"blue\": $blue,\n    \"soft-blue\": rgba(23, 95, 201, 0.15),\n    \"pink\": $pink,\n    \"yellow\": $yellow,\n    \"orange\": $orange,\n    \"soft-orange\": rgba(255, 174, 115, 0.15),\n    \"teal\": $teal,\n    \"cyan\": $cyan,\n);\n\n$yiq-contrasted-threshold: 190;\n\n$text-muted: #999;\n\n// Shadow\n\n$box-shadow: 0 15px 30px 0 rgba(0, 0, 0, 0.1);\n\n// Grid\n\n$spacer: 1rem;\n$spacers: (\n    0: 0,\n    1: ($spacer * .25),\n    2: ($spacer * .5),\n    3: $spacer,\n    4: ($spacer * 1.5),\n    5: ($spacer * 3),\n    6: ($spacer * 4.5),\n    7: ($spacer * 6)\n);\n\n// Body\n\n$body-bg: $white;\n$body-color: $black;\n\n// Fonts\n\n$font-family-base: 'Open Sans', -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n$font-family-title: Georgia, Times, 'New Roman', serif;\n\n$font-size-base: 1rem;\n\n$font-weight-normal: 400;\n$font-weight-medium: 500;\n$font-weight-bold: 600;\n\n// Headings\n\n$headings-font-family: $font-family-title;\n$headings-font-weight: $font-weight-normal;\n$headings-color: $gray-900;\n\n// Navbar\n\n$navbar-light-brand-color: $black;\n$navbar-light-brand-hover-color: $primary;\n$navbar-light-hover-color: $primary;\n\n// Dropdown\n\n$dropdown-link-hover-bg: $gray-200;\n\n// Tables\n\n$table-border-color: rgba(0, 0, 0, 0.1);\n\n// Forms and buttons\n\n$input-border-color: rgba(0, 0, 0, .1);\n$input-group-addon-bg: $gray-200;\n$btn-font-weight: $font-weight-bold;\n$custom-control-indicator-size: 1.3rem;\n\n// Tooltips\n\n$tooltip-font-size: 11px;\n\n// Badges\n\n$badge-font-weight: normal;\n$badge-padding-y: 0.6em;\n$badge-padding-x: 1.2em;\n\n// Alerts\n\n$alert-border-width: 0;\n\n// Cards\n\n$card-border-width: 0;\n"
  },
  {
    "path": "assets/css/app.scss",
    "content": "@import './variables';\n@import '~bootstrap/scss/bootstrap';\n\nbody {\n    display: flex;\n    flex-direction: column;\n    height: 100vh;\n}\nmain {\n    flex: 1;\n}\n\n// Darken buttons\n@each $color, $value in $theme-colors {\n    .btn-#{$color} {\n        @include button-variant($value, $value, darken($value, 10%));\n    }\n}\n\n.navbar-brand {\n    font-family: $font-family-title;\n    font-weight: $font-weight-normal;\n    font-size: 1.8rem;\n}\n\n.navbar-brand, .nav-link {\n    transition: all .15s;\n}\n\n.nav-conference {\n    display: inline-block;\n    padding: 5px 10px;\n    color: #666;\n    text-transform: uppercase;\n    font-weight: bold;\n    font-size: 0.8rem;\n}\n\n.lift {\n    transition: box-shadow .25s ease,transform .25s ease;\n\n    &:focus, &:hover {\n        box-shadow: 0 1rem 2.5rem rgba(22,28,45,.1),0 .5rem 1rem -.75rem rgba(22,28,45,.1)!important;\n        transform: translate3d(0, -4px, 0);\n    }\n}\n\n.comment-img {\n    width: 250px;\n    height: 150px;\n\n    img {\n        max-width: 250px;\n        max-height: 150px;\n    }\n}\n\n.comment-text {\n    font-size: 12px;\n    line-height: 15px;\n}\n\nfooter {\n    background: #18171b;\n}\n"
  },
  {
    "path": "assets/js/app.js",
    "content": "/*\n * Welcome to your app's main JavaScript file!\n *\n * We recommend including the built version of this JavaScript file\n * (and its CSS file) in your base layout (base.html.twig).\n */\n\n// any CSS you require will output into a single css file (app.css in this case)\nimport '../css/app.scss';\nimport 'bootstrap';\nimport bsCustomFileInput from 'bs-custom-file-input';\n\nbsCustomFileInput.init();\n"
  },
  {
    "path": "bin/console",
    "content": "#!/usr/bin/env php\n<?php\n\nuse App\\Kernel;\nuse Symfony\\Bundle\\FrameworkBundle\\Console\\Application;\nuse Symfony\\Component\\Console\\Input\\ArgvInput;\nuse Symfony\\Component\\ErrorHandler\\Debug;\n\nif (false === in_array(\\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true)) {\n    echo 'Warning: The console should be invoked via the CLI version of PHP, not the '.\\PHP_SAPI.' SAPI'.\\PHP_EOL;\n}\n\nset_time_limit(0);\n\nrequire dirname(__DIR__).'/vendor/autoload.php';\n\nif (!class_exists(Application::class)) {\n    throw new RuntimeException('You need to add \"symfony/framework-bundle\" as a Composer dependency.');\n}\n\n$input = new ArgvInput();\nif (null !== $env = $input->getParameterOption(['--env', '-e'], null, true)) {\n    putenv('APP_ENV='.$_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = $env);\n}\n\nif ($input->hasParameterOption('--no-debug', true)) {\n    putenv('APP_DEBUG='.$_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = '0');\n}\n\nrequire dirname(__DIR__).'/config/bootstrap.php';\n\nif ($_SERVER['APP_DEBUG']) {\n    umask(0000);\n\n    if (class_exists(Debug::class)) {\n        Debug::enable();\n    }\n}\n\n$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);\n$application = new Application($kernel);\n$application->run($input);\n"
  },
  {
    "path": "bin/phpunit",
    "content": "#!/usr/bin/env php\n<?php\n\nif (!file_exists(dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php')) {\n    echo \"Unable to find the `simple-phpunit.php` script in `vendor/symfony/phpunit-bridge/bin/`.\\n\";\n    exit(1);\n}\n\nif (false === getenv('SYMFONY_PHPUNIT_DIR')) {\n    putenv('SYMFONY_PHPUNIT_DIR='.__DIR__.'/.phpunit');\n}\n\nrequire dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php';\n"
  },
  {
    "path": "composer.json",
    "content": "{\n    \"type\": \"project\",\n    \"license\": \"proprietary\",\n    \"require\": {\n        \"php\": \"^7.2.5\",\n        \"ext-ctype\": \"*\",\n        \"ext-iconv\": \"*\",\n        \"api-platform/api-pack\": \"^1.2\",\n        \"easycorp/easyadmin-bundle\": \"^2.3\",\n        \"imagine/imagine\": \"^1.2\",\n        \"sensio/framework-extra-bundle\": \"^5.5\",\n        \"symfony/cache\": \"5.0.*\",\n        \"symfony/console\": \"5.0.*\",\n        \"symfony/dotenv\": \"5.0.*\",\n        \"symfony/flex\": \"^1.3.1\",\n        \"symfony/framework-bundle\": \"5.0.*\",\n        \"symfony/http-client\": \"5.0.*\",\n        \"symfony/mailer\": \"5.0.*\",\n        \"symfony/messenger\": \"5.0.*\",\n        \"symfony/monolog-bundle\": \"^3.5\",\n        \"symfony/notifier\": \"5.0.*\",\n        \"symfony/orm-pack\": \"^1.0\",\n        \"symfony/process\": \"5.0.*\",\n        \"symfony/security-bundle\": \"5.0.*\",\n        \"symfony/slack-notifier\": \"5.0.*\",\n        \"symfony/string\": \"5.0.*\",\n        \"symfony/test-pack\": \"^1.0\",\n        \"symfony/translation\": \"5.0.*\",\n        \"symfony/twig-pack\": \"^1.0\",\n        \"symfony/webpack-encore-bundle\": \"^1.7\",\n        \"symfony/workflow\": \"5.0.*\",\n        \"symfony/yaml\": \"5.0.*\",\n        \"twig/cssinliner-extra\": \"^3.0\",\n        \"twig/inky-extra\": \"^3.0\",\n        \"twig/intl-extra\": \"^3.0\",\n        \"twig/string-extra\": \"^3.0\"\n    },\n    \"require-dev\": {\n        \"dama/doctrine-test-bundle\": \"^6.3\",\n        \"doctrine/doctrine-fixtures-bundle\": \"^3.3\",\n        \"symfony/browser-kit\": \"5.0.*\",\n        \"symfony/debug-pack\": \"^1.0\",\n        \"symfony/maker-bundle\": \"^1.14\",\n        \"symfony/profiler-pack\": \"^1.0\"\n    },\n    \"config\": {\n        \"preferred-install\": {\n            \"*\": \"dist\"\n        },\n        \"sort-packages\": true\n    },\n    \"autoload\": {\n        \"psr-4\": {\n            \"App\\\\\": \"src/\"\n        }\n    },\n    \"autoload-dev\": {\n        \"psr-4\": {\n            \"App\\\\Tests\\\\\": \"tests/\"\n        }\n    },\n    \"replace\": {\n        \"paragonie/random_compat\": \"2.*\",\n        \"symfony/polyfill-ctype\": \"*\",\n        \"symfony/polyfill-iconv\": \"*\",\n        \"symfony/polyfill-php72\": \"*\",\n        \"symfony/polyfill-php71\": \"*\",\n        \"symfony/polyfill-php70\": \"*\",\n        \"symfony/polyfill-php56\": \"*\"\n    },\n    \"scripts\": {\n        \"auto-scripts\": {\n            \"cache:clear\": \"symfony-cmd\",\n            \"assets:install %PUBLIC_DIR%\": \"symfony-cmd\"\n        },\n        \"post-install-cmd\": [\n            \"@auto-scripts\"\n        ],\n        \"post-update-cmd\": [\n            \"@auto-scripts\"\n        ]\n    },\n    \"conflict\": {\n        \"symfony/symfony\": \"*\"\n    },\n    \"extra\": {\n        \"symfony\": {\n            \"allow-contrib\": true,\n            \"require\": \"5.0.*\"\n        }\n    }\n}\n"
  },
  {
    "path": "config/bootstrap.php",
    "content": "<?php\n\nuse Symfony\\Component\\Dotenv\\Dotenv;\n\nrequire dirname(__DIR__).'/vendor/autoload.php';\n\n// Load cached env vars if the .env.local.php file exists\n// Run \"composer dump-env prod\" to create it (requires symfony/flex >=1.2)\nif (is_array($env = @include dirname(__DIR__).'/.env.local.php') && ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? $env['APP_ENV']) === $env['APP_ENV']) {\n    foreach ($env as $k => $v) {\n        $_ENV[$k] = $_ENV[$k] ?? (isset($_SERVER[$k]) && 0 !== strpos($k, 'HTTP_') ? $_SERVER[$k] : $v);\n    }\n} elseif (!class_exists(Dotenv::class)) {\n    throw new RuntimeException('Please run \"composer require symfony/dotenv\" to load the \".env\" files configuring the application.');\n} else {\n    // load all the .env files\n    (new Dotenv(false))->loadEnv(dirname(__DIR__).'/.env');\n}\n\n$_SERVER += $_ENV;\n$_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? null) ?: 'dev';\n$_SERVER['APP_DEBUG'] = $_SERVER['APP_DEBUG'] ?? $_ENV['APP_DEBUG'] ?? 'prod' !== $_SERVER['APP_ENV'];\n$_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = (int) $_SERVER['APP_DEBUG'] || filter_var($_SERVER['APP_DEBUG'], FILTER_VALIDATE_BOOLEAN) ? '1' : '0';\n"
  },
  {
    "path": "config/bundles.php",
    "content": "<?php\n\nreturn [\n    Symfony\\Bundle\\FrameworkBundle\\FrameworkBundle::class => ['all' => true],\n    Symfony\\Bundle\\TwigBundle\\TwigBundle::class => ['all' => true],\n    Symfony\\Bundle\\WebProfilerBundle\\WebProfilerBundle::class => ['dev' => true, 'test' => true],\n    Symfony\\Bundle\\MonologBundle\\MonologBundle::class => ['all' => true],\n    Symfony\\Bundle\\DebugBundle\\DebugBundle::class => ['dev' => true, 'test' => true],\n    Symfony\\Bundle\\MakerBundle\\MakerBundle::class => ['dev' => true],\n    Sensio\\Bundle\\FrameworkExtraBundle\\SensioFrameworkExtraBundle::class => ['all' => true],\n    Doctrine\\Bundle\\DoctrineBundle\\DoctrineBundle::class => ['all' => true],\n    Doctrine\\Bundle\\MigrationsBundle\\DoctrineMigrationsBundle::class => ['all' => true],\n    Symfony\\Bundle\\SecurityBundle\\SecurityBundle::class => ['all' => true],\n    EasyCorp\\Bundle\\EasyAdminBundle\\EasyAdminBundle::class => ['all' => true],\n    Twig\\Extra\\TwigExtraBundle\\TwigExtraBundle::class => ['all' => true],\n    Doctrine\\Bundle\\FixturesBundle\\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true],\n    DAMA\\DoctrineTestBundle\\DAMADoctrineTestBundle::class => ['test' => true],\n    Symfony\\WebpackEncoreBundle\\WebpackEncoreBundle::class => ['all' => true],\n    Nelmio\\CorsBundle\\NelmioCorsBundle::class => ['all' => true],\n    ApiPlatform\\Core\\Bridge\\Symfony\\Bundle\\ApiPlatformBundle::class => ['all' => true],\n];\n"
  },
  {
    "path": "config/packages/api_platform.yaml",
    "content": "api_platform:\n    mapping:\n        paths: ['%kernel.project_dir%/src/Entity']\n    patch_formats:\n        json: ['application/merge-patch+json']\n    swagger:\n        versions: [3]\n"
  },
  {
    "path": "config/packages/assets.yaml",
    "content": "framework:\n    assets:\n        json_manifest_path: '%kernel.project_dir%/public/build/manifest.json'\n"
  },
  {
    "path": "config/packages/cache.yaml",
    "content": "framework:\n    cache:\n        # Unique name of your app: used to compute stable namespaces for cache keys.\n        #prefix_seed: your_vendor_name/app_name\n\n        # The \"app\" cache stores to the filesystem by default.\n        # The data in this cache should persist between deploys.\n        # Other options include:\n\n        # Redis\n        #app: cache.adapter.redis\n        #default_redis_provider: redis://localhost\n\n        # APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues)\n        #app: cache.adapter.apcu\n\n        # Namespaced pools use the above \"app\" backend by default\n        #pools:\n            #my.dedicated.cache: null\n"
  },
  {
    "path": "config/packages/dev/debug.yaml",
    "content": "debug:\n    # Forwards VarDumper Data clones to a centralized server allowing to inspect dumps on CLI or in your browser.\n    # See the \"server:dump\" command to start a new server.\n    dump_destination: \"tcp://%env(VAR_DUMPER_SERVER)%\"\n"
  },
  {
    "path": "config/packages/dev/easy_log_handler.yaml",
    "content": "services:\n    EasyCorp\\EasyLog\\EasyLogHandler:\n        public: false\n        arguments: ['%kernel.logs_dir%/%kernel.environment%.log']\n\n#// FIXME: How to add this configuration automatically without messing up with the monolog configuration?\n#monolog:\n#    handlers:\n#        buffered:\n#            type:     buffer\n#            handler:  easylog\n#            channels: ['!event']\n#            level:    debug\n#        easylog:\n#            type: service\n#            id:   EasyCorp\\EasyLog\\EasyLogHandler\n"
  },
  {
    "path": "config/packages/dev/monolog.yaml",
    "content": "monolog:\n    handlers:\n        main:\n            type: stream\n            path: \"%kernel.logs_dir%/%kernel.environment%.log\"\n            level: debug\n            channels: [\"!event\"]\n        # uncomment to get logging in your browser\n        # you may have to allow bigger header sizes in your Web server configuration\n        #firephp:\n        #    type: firephp\n        #    level: info\n        #chromephp:\n        #    type: chromephp\n        #    level: info\n        console:\n            type: console\n            process_psr_3_messages: false\n            channels: [\"!event\", \"!doctrine\", \"!console\"]\n"
  },
  {
    "path": "config/packages/dev/web_profiler.yaml",
    "content": "web_profiler:\n    toolbar: true\n    intercept_redirects: false\n\nframework:\n    profiler: { only_exceptions: false }\n"
  },
  {
    "path": "config/packages/doctrine.yaml",
    "content": "doctrine:\n    dbal:\n        url: '%env(resolve:DATABASE_URL)%'\n\n        # IMPORTANT: You MUST configure your server version,\n        # either here or in the DATABASE_URL env var (see .env file)\n        #server_version: '5.7'\n    orm:\n        auto_generate_proxy_classes: true\n        naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware\n        auto_mapping: true\n        mappings:\n            App:\n                is_bundle: false\n                type: annotation\n                dir: '%kernel.project_dir%/src/Entity'\n                prefix: 'App\\Entity'\n                alias: App\n"
  },
  {
    "path": "config/packages/doctrine_migrations.yaml",
    "content": "doctrine_migrations:\n    dir_name: '%kernel.project_dir%/src/Migrations'\n    # namespace is arbitrary but should be different from App\\Migrations\n    # as migrations classes should NOT be autoloaded\n    namespace: DoctrineMigrations\n"
  },
  {
    "path": "config/packages/easy_admin.yaml",
    "content": "easy_admin:\n    site_name: Conference Guestbook\n\n    design:\n        menu:\n            - { route: 'homepage', label: 'Back to the website', icon: 'home' }\n            - { entity: 'Conference', label: 'Conferences', icon: 'map-marker' }\n            - { entity: 'Comment', label: 'Comments', icon: 'comments' }\n\n    entities:\n        Conference:\n            class: App\\Entity\\Conference\n\n        Comment:\n            class: App\\Entity\\Comment\n            list:\n                fields:\n                    - author\n                    - { property: 'email', type: 'email' }\n                    - { property: 'photoFilename', type: 'image', 'base_path': \"/uploads/photos\", label: 'Photo' }\n                    - state\n                    - { property: 'createdAt', type: 'datetime' }\n                sort: ['createdAt', 'ASC']\n                filters: ['conference']\n            edit:\n                fields:\n                    - { property: 'conference' }\n                    - { property: 'createdAt', type: datetime, type_options: { attr: { readonly: true } } }\n                    - 'author'\n                    - { property: 'state' }\n                    - { property: 'email', type: 'email' }\n                    - text\n"
  },
  {
    "path": "config/packages/framework.yaml",
    "content": "framework:\n    secret: '%env(APP_SECRET)%'\n    #csrf_protection: true\n    #http_method_override: true\n\n    # Enables session support. Note that the session will ONLY be started if you read or write from it.\n    # Remove or comment this section to explicitly disable session support.\n    session:\n        handler_id: '%env(REDIS_URL)%'\n        cookie_secure: auto\n        cookie_samesite: lax\n\n    esi: true\n    #fragments: true\n    php_errors:\n        log: true\n\n    ide: vscode\n"
  },
  {
    "path": "config/packages/mailer.yaml",
    "content": "framework:\n    mailer:\n        dsn: '%env(MAILER_DSN)%'\n        envelope:\n            sender: \"%env(string:default:default_admin_email:ADMIN_EMAIL)%\"\n"
  },
  {
    "path": "config/packages/messenger.yaml",
    "content": "framework:\n    messenger:\n        # Uncomment this (and the failed transport below) to send failed messages to this transport for later handling.\n        # failure_transport: failed\n\n        transports:\n            # https://symfony.com/doc/current/messenger.html#transport-configuration\n            async:\n                dsn: '%env(RABBITMQ_DSN)%'\n                retry_strategy:\n                    max_retries: 3\n                    multiplier: 2\n\n            failed: 'doctrine://default?queue_name=failed'\n            # sync: 'sync://'\n\n        failure_transport: failed\n\n        routing:\n            # Route your messages to the transports\n            App\\Message\\CommentMessage: async\n            Symfony\\Component\\Mailer\\Messenger\\SendEmailMessage: async\n            Symfony\\Component\\Notifier\\Message\\ChatMessage: async\n            Symfony\\Component\\Notifier\\Message\\SmsMessage: async\n"
  },
  {
    "path": "config/packages/nelmio_cors.yaml",
    "content": "nelmio_cors:\n    defaults:\n        origin_regex: true\n        allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']\n        allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']\n        allow_headers: ['Content-Type', 'Authorization']\n        expose_headers: ['Link']\n        max_age: 3600\n    paths:\n        '^/': null\n"
  },
  {
    "path": "config/packages/notifier.yaml",
    "content": "framework:\n    notifier:\n        chatter_transports:\n            slack: '%env(SLACK_DSN)%'\n        #    telegram: '%env(TELEGRAM_DSN)%'\n        #texter_transports:\n        #    twilio: '%env(TWILIO_DSN)%'\n        #    nexmo: '%env(NEXMO_DSN)%'\n        channel_policy:\n            # use chat/slack, chat/telegram, sms/twilio or sms/nexmo\n            urgent: ['email']\n            high: ['email']\n            medium: ['email']\n            low: ['email']\n        admin_recipients:\n            - { email: \"%env(string:default:default_admin_email:ADMIN_EMAIL)%\" }\n"
  },
  {
    "path": "config/packages/prod/doctrine.yaml",
    "content": "doctrine:\n    orm:\n        auto_generate_proxy_classes: false\n        metadata_cache_driver:\n            type: pool\n            pool: doctrine.system_cache_pool\n        query_cache_driver:\n            type: pool\n            pool: doctrine.system_cache_pool\n        result_cache_driver:\n            type: pool\n            pool: doctrine.result_cache_pool\n\nframework:\n    cache:\n        pools:\n            doctrine.result_cache_pool:\n                adapter: cache.app\n            doctrine.system_cache_pool:\n                adapter: cache.system\n"
  },
  {
    "path": "config/packages/prod/monolog.yaml",
    "content": "monolog:\n    handlers:\n        main:\n            type: fingers_crossed\n            action_level: error\n            handler: nested\n            excluded_http_codes: [404, 405]\n        nested:\n            type: stream\n            path: \"%kernel.logs_dir%/%kernel.environment%.log\"\n            level: debug\n        console:\n            type: console\n            process_psr_3_messages: false\n            channels: [\"!event\", \"!doctrine\"]\n        deprecation:\n            type: stream\n            path: \"%kernel.logs_dir%/%kernel.environment%.deprecations.log\"\n        deprecation_filter:\n            type: filter\n            handler: deprecation\n            max_level: info\n            channels: [\"php\"]\n"
  },
  {
    "path": "config/packages/prod/routing.yaml",
    "content": "framework:\n    router:\n        strict_requirements: null\n"
  },
  {
    "path": "config/packages/prod/webpack_encore.yaml",
    "content": "#webpack_encore:\n    # Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes)\n    # Available in version 1.2\n    #cache: true\n"
  },
  {
    "path": "config/packages/routing.yaml",
    "content": "framework:\n    router:\n        utf8: true\n"
  },
  {
    "path": "config/packages/security.yaml",
    "content": "security:\n    encoders:\n        App\\Entity\\Admin:\n            algorithm: auto\n\n    # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers\n    providers:\n        # used to reload user from session & other features (e.g. switch_user)\n        app_user_provider:\n            entity:\n                class: App\\Entity\\Admin\n                property: username\n    firewalls:\n        dev:\n            pattern: ^/(_(profiler|wdt)|css|images|js)/\n            security: false\n        main:\n            anonymous: lazy\n            guard:\n                authenticators:\n                    - App\\Security\\AppAuthenticator\n            logout:\n                path: app_logout\n                # where to redirect after logout\n                # target: app_any_route\n\n            # activate different ways to authenticate\n            # https://symfony.com/doc/current/security.html#firewalls-authentication\n\n            # https://symfony.com/doc/current/security/impersonating_user.html\n            # switch_user: true\n\n    # Easy way to control access for large sections of your site\n    # Note: Only the *first* access control that matches will be used\n    access_control:\n        - { path: ^/admin, roles: ROLE_ADMIN }\n        # - { path: ^/profile, roles: ROLE_USER }\n"
  },
  {
    "path": "config/packages/sensio_framework_extra.yaml",
    "content": "sensio_framework_extra:\n    router:\n        annotations: false\n"
  },
  {
    "path": "config/packages/test/dama_doctrine_test_bundle.yaml",
    "content": "dama_doctrine_test:\n    enable_static_connection: true\n    enable_static_meta_data_cache: true\n    enable_static_query_cache: true\n"
  },
  {
    "path": "config/packages/test/framework.yaml",
    "content": "framework:\n    test: true\n    session:\n        storage_id: session.storage.mock_file\n"
  },
  {
    "path": "config/packages/test/monolog.yaml",
    "content": "monolog:\n    handlers:\n        main:\n            type: stream\n            path: \"%kernel.logs_dir%/%kernel.environment%.log\"\n            level: debug\n            channels: [\"!event\"]\n"
  },
  {
    "path": "config/packages/test/twig.yaml",
    "content": "twig:\n    strict_variables: true\n"
  },
  {
    "path": "config/packages/test/validator.yaml",
    "content": "framework:\n    validation:\n        not_compromised_password: false\n"
  },
  {
    "path": "config/packages/test/web_profiler.yaml",
    "content": "web_profiler:\n    toolbar: false\n    intercept_redirects: false\n\nframework:\n    profiler: { collect: false }\n"
  },
  {
    "path": "config/packages/test/webpack_encore.yaml",
    "content": "#webpack_encore:\n#    strict_mode: false\n"
  },
  {
    "path": "config/packages/translation.yaml",
    "content": "framework:\n    default_locale: en\n    translator:\n        default_path: '%kernel.project_dir%/translations'\n        fallbacks:\n            - en\n"
  },
  {
    "path": "config/packages/twig.yaml",
    "content": "twig:\n    form_themes: ['bootstrap_4_layout.html.twig']\n"
  },
  {
    "path": "config/packages/validator.yaml",
    "content": "framework:\n    validation:\n        email_validation_mode: html5\n\n        # Enables validator auto-mapping support.\n        # For instance, basic validation constraints will be inferred from Doctrine's metadata.\n        #auto_mapping:\n        #    App\\Entity\\: []\n"
  },
  {
    "path": "config/packages/webpack_encore.yaml",
    "content": "webpack_encore:\n    # The path where Encore is building the assets - i.e. Encore.setOutputPath()\n    output_path: '%kernel.project_dir%/public/build'\n    # If multiple builds are defined (as shown below), you can disable the default build:\n    # output_path: false\n\n    # if using Encore.enableIntegrityHashes() and need the crossorigin attribute (default: false, or use 'anonymous' or 'use-credentials')\n    # crossorigin: 'anonymous'\n\n    # preload all rendered script and link tags automatically via the http2 Link header\n    # preload: true\n\n    # Throw an exception if the entrypoints.json file is missing or an entry is missing from the data\n    # strict_mode: false\n\n    # if you have multiple builds:\n    # builds:\n        # pass \"frontend\" as the 3rg arg to the Twig functions\n        # {{ encore_entry_script_tags('entry1', null, 'frontend') }}\n\n        # frontend: '%kernel.project_dir%/public/frontend/build'\n\n    # Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes)\n    # Put in config/packages/prod/webpack_encore.yaml\n    # cache: true\n"
  },
  {
    "path": "config/packages/workflow.yaml",
    "content": "framework:\n    workflows:\n        comment:\n            type: state_machine\n            audit_trail:\n                enabled: \"%kernel.debug%\"\n            marking_store:\n                type: 'method'\n                property: 'state'\n            supports:\n                - App\\Entity\\Comment\n            initial_marking: submitted\n            places:\n                - submitted\n                - ham\n                - potential_spam\n                - spam\n                - rejected\n                - ready\n                - published\n            transitions:\n                accept:\n                    from: submitted\n                    to:   ham\n                might_be_spam:\n                    from: submitted\n                    to:   potential_spam\n                reject_spam:\n                    from: submitted\n                    to:   spam\n                publish:\n                    from: potential_spam\n                    to:   ready\n                reject:\n                    from: potential_spam\n                    to:   rejected\n                publish_ham:\n                    from: ham\n                    to:   ready\n                reject_ham:\n                    from: ham\n                    to:   rejected\n                optimize:\n                    from: ready\n                    to:   published\n"
  },
  {
    "path": "config/routes/annotations.yaml",
    "content": "controllers:\n    resource: ../../src/Controller/\n    type: annotation\n"
  },
  {
    "path": "config/routes/api_platform.yaml",
    "content": "api_platform:\n    resource: .\n    type: api_platform\n    prefix: /api\n"
  },
  {
    "path": "config/routes/dev/framework.yaml",
    "content": "_errors:\n    resource: '@FrameworkBundle/Resources/config/routing/errors.xml'\n    prefix: /_error\n"
  },
  {
    "path": "config/routes/dev/web_profiler.yaml",
    "content": "web_profiler_wdt:\n    resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml'\n    prefix: /_wdt\n\nweb_profiler_profiler:\n    resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml'\n    prefix: /_profiler\n"
  },
  {
    "path": "config/routes/easy_admin.yaml",
    "content": "easy_admin_bundle:\n    resource: '@EasyAdminBundle/Controller/EasyAdminController.php'\n    prefix: /admin\n    type: annotation\n"
  },
  {
    "path": "config/routes.yaml",
    "content": "#index:\n#    path: /\n#    controller: App\\Controller\\DefaultController::index\n"
  },
  {
    "path": "config/secrets/dev/dev.AKISMET_KEY.ca01fb.php",
    "content": "<?php // dev.AKISMET_KEY.ca01fb on Tue, 07 Jan 2020 09:15:06 +0100\n\nreturn \"\\x9B\\xAA\\xDB\\xF7\\x9E\\x1C\\x8D\\x9F\\x7B\\xDBm8d\\xA3q\\x0F\\x81-\\x9C\\x25\\x0C5y\\x3F\\x8D\\xC8\\xF9\\xD2\\x8BjU5\\xF61\\xEB\\xC8\\x40i\\x28\\xA2\\x11\\x14\\x07U\\xBF8\\x0A\\xB9\\x19\\x1F\\xB6\\x0876\\xFF\\x7B\\xE4\\xA0Eko\\xA0\\xB8X\\x7D\";\n"
  },
  {
    "path": "config/secrets/dev/dev.SLACK_DSN.b2b579.php",
    "content": "<?php // dev.SLACK_DSN.b2b579 on Tue, 07 Jan 2020 09:22:47 +0100\n\nreturn \"\\x97\\x10E\\x88\\x0Fc\\xCA\\x850\\x9F\\xB6\\x18Q\\x1E\\x82\\xAB\\xDE\\xA9\\xA9\\x17t\\xB6\\xCB\\xB6\\xBA.m\\xC0\\x82\\x3C\\x24i\\x07\\xCA\\xE2\\x95\\x0C\\xB6\\x2B\\xD2qZR\\x7C\\x87\\xB5\\x9C\\xD5\\x3B\\x91\\x8B\\x3C\\xDF\\x8C\\x8B\\x10\\x9F\\x8E\\x06\\x5D\\x19\\xF3\\x93M\\xE8\\x80\\xDBx\\x5E\\xF5\\x24\\x18\\xD3x\\x40\\xBCt\\xFD\\xDA\\x60\\xF2I\\x99\\x8A\\xEEe\\x83\\x23D\\xA7y\\x0F\";\n"
  },
  {
    "path": "config/secrets/dev/dev.decrypt.private.php",
    "content": "<?php // dev.decrypt.private on Tue, 07 Jan 2020 09:15:06 +0100\n\nreturn \"z7\\x80\\x9E\\x0E\\x22\\xD2\\xBE-\\xD9\\x3Co\\x08B\\x0D\\xE5\\xF2\\xD5\\xB9D\\xEB\\x8D\\x04p\\x97\\xE2\\x11\\xF1\\x0F\\x24\\xC8\\x40\\x15\\x2A\\x18\\xCC\\x11\\x04\\x60\\x16\\x8D\\x26\\xC1\\xA2\\x3A\\x9CB\\xD9\\x9C\\xD9\\xDD\\xC8\\xD2TIp\\x9A\\xC5\\x80\\xF5\\x80\\x8BB~\";\n"
  },
  {
    "path": "config/secrets/dev/dev.encrypt.public.php",
    "content": "<?php // dev.encrypt.public on Tue, 07 Jan 2020 09:15:06 +0100\n\nreturn \"\\x15\\x2A\\x18\\xCC\\x11\\x04\\x60\\x16\\x8D\\x26\\xC1\\xA2\\x3A\\x9CB\\xD9\\x9C\\xD9\\xDD\\xC8\\xD2TIp\\x9A\\xC5\\x80\\xF5\\x80\\x8BB~\";\n"
  },
  {
    "path": "config/secrets/dev/dev.list.php",
    "content": "<?php\n\nreturn array (\n  'AKISMET_KEY' => NULL,\n  'SLACK_DSN' => NULL,\n);\n"
  },
  {
    "path": "config/secrets/prod/prod.AKISMET_KEY.ca01fb.php",
    "content": "<?php // prod.AKISMET_KEY.ca01fb on Tue, 07 Jan 2020 09:15:07 +0100\n\nreturn \"\\x89\\x8E\\x7C\\x94\\xE7\\x90\\xDE77\\xEB\\xFB\\xE0\\x9C\\xDC\\x19\\xF7\\xBA\\x7D\\x81\\xF1\\x24\\xD8\\x07\\xDF\\x03\\xB0\\x7Cj\\xCEd\\xCF\\x1C\\x13\\x01\\xBB\\x0A\\x8F\\x5C-\\x80C\\x16\\xC0\\xAA\\xBBG\\xB7\\xE1\\x91\\xB9F\\xCDt\\xB2\";\n"
  },
  {
    "path": "config/secrets/prod/prod.SLACK_DSN.b2b579.php",
    "content": "<?php // prod.SLACK_DSN.b2b579 on Tue, 07 Jan 2020 09:22:47 +0100\n\nreturn \"\\x0D_U\\xDBV\\xC4\\xF7\\x0Az\\xB2\\xBC\\xDB\\x5BXMd\\x17\\xE4\\x17Z\\x8E\\xE4\\x06V\\xDFUY\\x3A\\xE2v\\xE7R\\xCA\\xD3\\x23\\xBFzDj\\x9F\\x0A\\x10\\xF1\\x0F\\x90G\\x0CBf\\x04\\x5D\\x8Er\\x20\\x1C\\xF1\\xA9WJ\\xFD\\x9C\\x98j\\x60\\xD9_\\xEB\\x8A_\\xA6\\xAEaf\\xC6\\xAA\\x133k\\x3B\\xFB\\xA6\\x9E\\x25\\xD6\\x84\\xB3\\x85A\\xC9\\x11O\\xF5\";\n"
  },
  {
    "path": "config/secrets/prod/prod.encrypt.public.php",
    "content": "<?php // prod.encrypt.public on Tue, 07 Jan 2020 09:15:06 +0100\n\nreturn \"\\xA7\\xFDO\\xCB\\xA8Y\\x85\\xCE\\xA8E\\xCBT\\xAEK\\xEF\\xAD\\x91\\x5E\\x06\\xF1\\xC22i\\x9E\\x04\\x14\\x99\\x02O\\x20\\x07E\";\n"
  },
  {
    "path": "config/secrets/prod/prod.list.php",
    "content": "<?php\n\nreturn array (\n  'AKISMET_KEY' => NULL,\n  'SLACK_DSN' => NULL,\n);\n"
  },
  {
    "path": "config/secrets/test/test.AKISMET_KEY.ca01fb.php",
    "content": "<?php // test.AKISMET_KEY.ca01fb on Tue, 07 Jan 2020 09:15:56 +0100\n\nreturn \"\\xE1\\x85\\x91\\x878\\xAD\\xBAu\\x84\\x7F\\xC8\\xAA2\\x29\\xE7\\xAE\\xBB\\x05a-\\x29\\xF9uZ\\x80\\x904\\x96\\xE7\\x0C\\x40\\x19\\xB1\\xE6b\\xAB\\xD6Z\\xC8\\xB0\\x7C\\xFDGf\\x9DH\\x8A\\xC3\\x08\\x3D\\x11\\xE8vO\\x7CZGz\\xF5\\x0F\\xF9\\xEEw\\xDAd\";\n"
  },
  {
    "path": "config/secrets/test/test.decrypt.private.php",
    "content": "<?php // test.decrypt.private on Tue, 07 Jan 2020 09:15:56 +0100\n\nreturn \"\\x8DV1\\x26\\x29o\\xD6\\xF1\\xA8\\xF0\\x3Dz\\xE8\\xB1\\x82\\xE8\\x10\\xF5\\x09\\xE8\\x16\\xB5c\\xCBn\\xD8R~\\x3B\\xC3\\xCA\\x25\\x80\\xD2\\xF5\\x1A7j\\xE4w\\xDF\\x7Bw\\xA4\\x60\\x5CUO\\x5Bv\\xE9j\\x1B\\xAD\\x0D\\x13q\\x19\\xC7\\x9E\\xAB\\xFA\\x9F\\x7B\";\n"
  },
  {
    "path": "config/secrets/test/test.encrypt.public.php",
    "content": "<?php // test.encrypt.public on Tue, 07 Jan 2020 09:15:56 +0100\n\nreturn \"\\x80\\xD2\\xF5\\x1A7j\\xE4w\\xDF\\x7Bw\\xA4\\x60\\x5CUO\\x5Bv\\xE9j\\x1B\\xAD\\x0D\\x13q\\x19\\xC7\\x9E\\xAB\\xFA\\x9F\\x7B\";\n"
  },
  {
    "path": "config/secrets/test/test.list.php",
    "content": "<?php\n\nreturn array (\n  'AKISMET_KEY' => NULL,\n);\n"
  },
  {
    "path": "config/services.yaml",
    "content": "# This file is the entry point to configure your own services.\n# Files in the packages/ subdirectory configure your dependencies.\n\n# Put parameters here that don't need to change on each machine where the app is deployed\n# https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration\nparameters:\n    default_admin_email: admin@example.com\n    default_domain: '127.0.0.1'\n    default_scheme: 'http'\n    app.supported_locales: 'en|fr'\n\n    router.request_context.host: '%env(default:default_domain:SYMFONY_DEFAULT_ROUTE_HOST)%'\n    router.request_context.scheme: '%env(default:default_scheme:SYMFONY_DEFAULT_ROUTE_SCHEME)%'\n\nservices:\n    # default configuration for services in *this* file\n    _defaults:\n        autowire: true      # Automatically injects dependencies in your services.\n        autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.\n        bind:\n            $photoDir: \"%kernel.project_dir%/public/uploads/photos\"\n            $akismetKey: \"%env(AKISMET_KEY)%\"\n            $adminEmail: \"%env(string:default:default_admin_email:ADMIN_EMAIL)%\"\n\n    # makes classes in src/ available to be used as services\n    # this creates a service per class whose id is the fully-qualified class name\n    App\\:\n        resource: '../src/*'\n        exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}'\n\n    # controllers are imported separately to make sure services can be injected\n    # as action arguments even if you don't extend any base controller class\n    App\\Controller\\:\n        resource: '../src/Controller'\n        tags: ['controller.service_arguments']\n\n    # add more service definitions when explicit configuration is needed\n    # please note that last definitions always *replace* previous ones\n    App\\EntityListener\\ConferenceEntityListener:\n        tags:\n            - { name: 'doctrine.orm.entity_listener', event: 'prePersist', entity: 'App\\Entity\\Conference'}\n            - { name: 'doctrine.orm.entity_listener', event: 'preUpdate', entity: 'App\\Entity\\Conference'}\n"
  },
  {
    "path": "docker-compose.yaml",
    "content": "version: '3'\n\nservices:\n    database:\n        image: postgres:11-alpine\n        environment:\n            POSTGRES_USER: main\n            POSTGRES_PASSWORD: main\n            POSTGRES_DB: main\n        ports: [5432]\n\n    redis:\n        image: redis:5-alpine\n        ports: [6379]\n\n    rabbitmq:\n        image: rabbitmq:3.7-management\n        ports: [5672, 15672]\n\n    mailcatcher:\n        image: schickling/mailcatcher\n        ports: [1025, 1080]\n\n    blackfire:\n        image: blackfire/blackfire\n        env_file: .env.local\n        ports: [8707]\n"
  },
  {
    "path": "package.json",
    "content": "{\n    \"devDependencies\": {\n        \"@symfony/webpack-encore\": \"^0.28.2\",\n        \"bootstrap\": \"^4.4.1\",\n        \"bs-custom-file-input\": \"^1.3.2\",\n        \"core-js\": \"^3.0.0\",\n        \"jquery\": \"^3.4.1\",\n        \"node-sass\": \"^4.13.0\",\n        \"popper.js\": \"^1.16.0\",\n        \"regenerator-runtime\": \"^0.13.2\",\n        \"sass-loader\": \"^7.0.1\",\n        \"webpack-notifier\": \"^1.6.0\"\n    },\n    \"license\": \"UNLICENSED\",\n    \"private\": true,\n    \"scripts\": {\n        \"dev-server\": \"encore dev-server\",\n        \"dev\": \"encore dev\",\n        \"watch\": \"encore dev --watch\",\n        \"build\": \"encore production --progress\"\n    }\n}\n"
  },
  {
    "path": "php.ini",
    "content": "allow_url_include=off\nassert.active=off\ndisplay_errors=off\ndisplay_startup_errors=off\nmax_execution_time=30\nsession.use_strict_mode=On\nrealpath_cache_ttl=3600\nzend.detect_unicode=Off\n\n[blackfire]\n# use php_blackfire.dll on Windows\nextension=blackfire.so\n"
  },
  {
    "path": "phpunit.xml.dist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<!-- https://phpunit.readthedocs.io/en/latest/configuration.html -->\n<phpunit xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:noNamespaceSchemaLocation=\"bin/.phpunit/phpunit.xsd\"\n         backupGlobals=\"false\"\n         colors=\"true\"\n         bootstrap=\"config/bootstrap.php\"\n>\n    <php>\n        <ini name=\"error_reporting\" value=\"-1\" />\n        <server name=\"APP_ENV\" value=\"test\" force=\"true\" />\n        <server name=\"SHELL_VERBOSITY\" value=\"-1\" />\n        <server name=\"SYMFONY_PHPUNIT_REMOVE\" value=\"\" />\n        <server name=\"SYMFONY_PHPUNIT_VERSION\" value=\"7.5\" />\n    </php>\n\n    <testsuites>\n        <testsuite name=\"Project Test Suite\">\n            <directory>tests</directory>\n        </testsuite>\n    </testsuites>\n\n    <filter>\n        <whitelist>\n            <directory>src</directory>\n        </whitelist>\n    </filter>\n\n    <extensions>\n        <extension class=\"DAMA\\DoctrineTestBundle\\PHPUnit\\PHPUnitExtension\" />\n    </extensions>\n\n    <listeners>\n        <listener class=\"Symfony\\Bridge\\PhpUnit\\SymfonyTestsListener\" />\n    </listeners>\n</phpunit>\n"
  },
  {
    "path": "public/index.php",
    "content": "<?php\n\nuse App\\Kernel;\nuse Symfony\\Bundle\\FrameworkBundle\\HttpCache\\HttpCache;\nuse Symfony\\Component\\ErrorHandler\\Debug;\nuse Symfony\\Component\\HttpFoundation\\Request;\n\nrequire dirname(__DIR__).'/config/bootstrap.php';\n\nif ($_SERVER['APP_DEBUG']) {\n    umask(0000);\n\n    Debug::enable();\n}\n\nif ($trustedProxies = $_SERVER['TRUSTED_PROXIES'] ?? $_ENV['TRUSTED_PROXIES'] ?? false) {\n    Request::setTrustedProxies(explode(',', $trustedProxies), Request::HEADER_X_FORWARDED_ALL ^ Request::HEADER_X_FORWARDED_HOST);\n}\n\nif ($trustedHosts = $_SERVER['TRUSTED_HOSTS'] ?? $_ENV['TRUSTED_HOSTS'] ?? false) {\n    Request::setTrustedHosts([$trustedHosts]);\n}\n\n$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);\n\nif ('dev' === $kernel->getEnvironment()) {\n    $kernel = new HttpCache($kernel);\n}\n\n$request = Request::createFromGlobals();\n$response = $kernel->handle($request);\n$response->send();\n$kernel->terminate($request, $response);\n"
  },
  {
    "path": "spa/.gitignore",
    "content": "/node_modules\n/public\n/yarn-error.log\n# used later by Cordova\n/app\n"
  },
  {
    "path": "spa/.symfony.cloud.yaml",
    "content": "name: spa\n\ntype: php:7.3\nsize: S\ndisk: 256\n\nbuild:\n    flavor: none\n\ndependencies:\n    nodejs:\n        yarn: \"*\"\n\nweb:\n    commands:\n        start: sleep\n    locations:\n        \"/\":\n            root: \"public\"\n            index:\n                - \"index.html\"\n            scripts: false\n            expires: 10m\n\nhooks:\n    build: |\n        set -x -e\n\n        curl -s https://get.symfony.com/cloud/configurator | (>&2 bash)\n        yarn-install\n        npm rebuild node-sass\n        yarn encore prod\n"
  },
  {
    "path": "spa/assets/css/_variables.scss",
    "content": "\n// Colors\n\n$white: #fff;\n$gray-100: #f5f5f5;\n$gray-200: #e9ecef;\n$gray-300: #ddd;\n$gray-400: #ced4da;\n$gray-500: #adb5bd;\n$gray-600: #868e96;\n$gray-700: #495057;\n$gray-800: #343a40;\n$gray-900: #212529;\n$black: #000;\n\n$blue: #175fc9;\n$pink: #ff737c;\n$red: #d9534f;\n$yellow: #fcee60;\n$orange: #ffae73;\n$green: #02B875;\n$teal: #55e7cc;\n$cyan: #8ce6ff;\n\n$primary: $blue;\n$secondary: #666;\n$success: $green;\n$info: $cyan;\n$warning: $yellow;\n$danger: $red;\n$light: $gray-100;\n$dark: $gray-800;\n\n$theme-colors: (\n    \"blue\": $blue,\n    \"soft-blue\": rgba(23, 95, 201, 0.15),\n    \"pink\": $pink,\n    \"yellow\": $yellow,\n    \"orange\": $orange,\n    \"soft-orange\": rgba(255, 174, 115, 0.15),\n    \"teal\": $teal,\n    \"cyan\": $cyan,\n);\n\n$yiq-contrasted-threshold: 190;\n\n$text-muted: #999;\n\n// Shadow\n\n$box-shadow: 0 15px 30px 0 rgba(0, 0, 0, 0.1);\n\n// Grid\n\n$spacer: 1rem;\n$spacers: (\n    0: 0,\n    1: ($spacer * .25),\n    2: ($spacer * .5),\n    3: $spacer,\n    4: ($spacer * 1.5),\n    5: ($spacer * 3),\n    6: ($spacer * 4.5),\n    7: ($spacer * 6)\n);\n\n// Body\n\n$body-bg: $white;\n$body-color: $black;\n\n// Fonts\n\n$font-family-base: 'Open Sans', -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n$font-family-title: Georgia, Times, 'New Roman', serif;\n\n$font-size-base: 1rem;\n\n$font-weight-normal: 400;\n$font-weight-medium: 500;\n$font-weight-bold: 600;\n\n// Headings\n\n$headings-font-family: $font-family-title;\n$headings-font-weight: $font-weight-normal;\n$headings-color: $gray-900;\n\n// Navbar\n\n$navbar-light-brand-color: $black;\n$navbar-light-brand-hover-color: $primary;\n$navbar-light-hover-color: $primary;\n\n// Dropdown\n\n$dropdown-link-hover-bg: $gray-200;\n\n// Tables\n\n$table-border-color: rgba(0, 0, 0, 0.1);\n\n// Forms and buttons\n\n$input-border-color: rgba(0, 0, 0, .1);\n$input-group-addon-bg: $gray-200;\n$btn-font-weight: $font-weight-bold;\n$custom-control-indicator-size: 1.3rem;\n\n// Tooltips\n\n$tooltip-font-size: 11px;\n\n// Badges\n\n$badge-font-weight: normal;\n$badge-padding-y: 0.6em;\n$badge-padding-x: 1.2em;\n\n// Alerts\n\n$alert-border-width: 0;\n\n// Cards\n\n$card-border-width: 0;\n"
  },
  {
    "path": "spa/assets/css/app.scss",
    "content": "@import './variables';\n@import '~bootstrap/scss/bootstrap';\n\nbody {\n    display: flex;\n    flex-direction: column;\n    height: 100vh;\n}\nmain {\n    flex: 1;\n}\n\n// Darken buttons\n@each $color, $value in $theme-colors {\n    .btn-#{$color} {\n        @include button-variant($value, $value, darken($value, 10%));\n    }\n}\n\n.navbar-brand {\n    font-family: $font-family-title;\n    font-weight: $font-weight-normal;\n    font-size: 1.8rem;\n}\n\n.navbar-brand, .nav-link {\n    transition: all .15s;\n}\n\n.nav-conference {\n    display: inline-block;\n    padding: 5px 10px;\n    color: #666;\n    text-transform: uppercase;\n    font-weight: bold;\n    font-size: 0.8rem;\n}\n\n.lift {\n    transition: box-shadow .25s ease,transform .25s ease;\n\n    &:focus, &:hover {\n        box-shadow: 0 1rem 2.5rem rgba(22,28,45,.1),0 .5rem 1rem -.75rem rgba(22,28,45,.1)!important;\n        transform: translate3d(0, -4px, 0);\n    }\n}\n\n.comment-img {\n    width: 250px;\n    height: 150px;\n\n    img {\n        max-width: 250px;\n        max-height: 150px;\n    }\n}\n\n.comment-text {\n    font-size: 12px;\n    line-height: 15px;\n}\n\nfooter {\n    background: #18171b;\n}\n"
  },
  {
    "path": "spa/package.json",
    "content": "{\n  \"name\": \"spa\",\n  \"version\": \"1.0.0\",\n  \"main\": \"index.js\",\n  \"license\": \"MIT\",\n  \"dependencies\": {\n    \"@babel/core\": \"^7.7.7\",\n    \"@babel/preset-env\": \"^7.7.7\",\n    \"@symfony/webpack-encore\": \"^0.28.2\",\n    \"babel-preset-preact\": \"^2.0.0\",\n    \"bootstrap\": \"^4.4.1\",\n    \"html-webpack-plugin\": \"^3.2.0\",\n    \"node-sass\": \"^4.13.0\",\n    \"preact\": \"^10.1.1\",\n    \"preact-router\": \"^3.1.0\",\n    \"sass-loader\": \"^7.0\"\n  }\n}\n"
  },
  {
    "path": "spa/src/api/api.js",
    "content": "function fetchCollection(path) {\n    return fetch(ENV_API_ENDPOINT + path).then(resp => resp.json()).then(json => json['hydra:member']);\n}\n\nexport function findConferences() {\n    return fetchCollection('api/conferences');\n}\n\nexport function findComments(conference) {\n    return fetchCollection('api/comments?conference='+conference.id);\n}\n"
  },
  {
    "path": "spa/src/app.js",
    "content": "import '../assets/css/app.scss';\n\nimport {h, render} from 'preact';\nimport {Router, Link} from 'preact-router';\nimport {useState, useEffect} from 'preact/hooks';\n\nimport {findConferences} from './api/api';\nimport Home from './pages/home';\nimport Conference from './pages/conference';\n\nfunction App() {\n    const [conferences, setConferences] = useState(null);\n\n    useEffect(() => {\n        findConferences().then((conferences) => setConferences(conferences));\n    }, []);\n\n    if (conferences === null) {\n        return <div className=\"text-center pt-5\">Loading...</div>;\n    }\n\n    return (\n        <div>\n            <header className=\"header\">\n                <nav className=\"navbar navbar-light bg-light\">\n                    <div className=\"container\">\n                        <Link className=\"navbar-brand mr-4 pr-2\" href=\"/\">\n                            &#128217; Guestbook\n                        </Link>\n                    </div>\n                </nav>\n\n                <nav className=\"bg-light border-bottom text-center\">\n                    {conferences.map((conference) => (\n                        <Link className=\"nav-conference\" href={'/conference/'+conference.slug}>\n                            {conference.city} {conference.year}\n                        </Link>\n                    ))}\n                </nav>\n            </header>\n\n            <Router>\n                <Home path=\"/\" conferences={conferences} />\n                <Conference path=\"/conference/:slug\" conferences={conferences} />\n            </Router>\n        </div>\n    )\n}\n\nrender(<App />, document.getElementById('app'));\n"
  },
  {
    "path": "spa/src/index.ejs",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n    <meta name=\"msapplication-tap-highlight\" content=\"no\" />\n    <meta name=\"viewport\" content=\"user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width\" />\n\n    <title>Conference Guestbook application</title>\n</head>\n<body>\n    <div id=\"app\"></div>\n</body>\n</html>\n"
  },
  {
    "path": "spa/src/pages/conference.js",
    "content": "import {h} from 'preact';\nimport {findComments} from '../api/api';\nimport {useState, useEffect} from 'preact/hooks';\n\nfunction Comment({comments}) {\n    if (comments !== null && comments.length === 0) {\n        return <div className=\"text-center pt-4\">No comments yet</div>;\n    }\n\n    if (!comments) {\n        return <div className=\"text-center pt-4\">Loading...</div>;\n    }\n\n    return (\n        <div className=\"pt-4\">\n            {comments.map(comment => (\n                <div className=\"shadow border rounded-lg p-3 mb-4\">\n                    <div className=\"comment-img mr-3\">\n                        {!comment.photoFilename ? '' : (\n                            <a href={ENV_API_ENDPOINT+'uploads/photos/'+comment.photoFilename} target=\"_blank\">\n                                <img src={ENV_API_ENDPOINT+'uploads/photos/'+comment.photoFilename} />\n                            </a>\n                        )}\n                    </div>\n\n                    <h5 className=\"font-weight-light mt-3 mb-0\">{comment.author}</h5>\n                    <div className=\"comment-text\">{comment.text}</div>\n                </div>\n            ))}\n        </div>\n    );\n}\n\nexport default function Conference({conferences, slug}) {\n    const conference = conferences.find(conference => conference.slug === slug);\n    const [comments, setComments] = useState(null);\n\n    useEffect(() => {\n        findComments(conference).then(comments => setComments(comments));\n    }, [slug]);\n\n    return (\n        <div className=\"p-3\">\n            <h4>{conference.city} {conference.year}</h4>\n            <Comment comments={comments} />\n        </div>\n    );\n}\n"
  },
  {
    "path": "spa/src/pages/home.js",
    "content": "import {h} from 'preact';\nimport {Link} from 'preact-router';\n\nexport default function Home({conferences}) {\n    if (!conferences) {\n        return <div className=\"p-3 text-center\">No conferences yet</div>;\n    }\n\n    return (\n        <div className=\"p-3\">\n            {conferences.map((conference)=> (\n                <div className=\"card border shadow-sm lift mb-3\">\n                    <div className=\"card-body\">\n                        <div className=\"card-title\">\n                            <h4 className=\"font-weight-light\">\n                                {conference.city} {conference.year}\n                            </h4>\n                        </div>\n\n                        <Link className=\"btn btn-sm btn-blue stretched-link\" href={'/conference/'+conference.slug}>\n                            View\n                        </Link>\n                    </div>\n                </div>\n            ))}\n        </div>\n    );\n}\n"
  },
  {
    "path": "spa/webpack.config.js",
    "content": "const webpack = require('webpack');\nconst Encore = require('@symfony/webpack-encore');\nconst HtmlWebpackPlugin = require('html-webpack-plugin');\n\nEncore\n    .setOutputPath('public/')\n    .setPublicPath('/')\n    .cleanupOutputBeforeBuild()\n    .addEntry('app', './src/app.js')\n    .enablePreactPreset()\n    .enableSassLoader()\n    .enableSingleRuntimeChunk()\n    .addPlugin(new HtmlWebpackPlugin({ template: 'src/index.ejs', alwaysWriteToDisk: true }))\n    .addPlugin(new webpack.DefinePlugin({\n        'ENV_API_ENDPOINT': JSON.stringify(process.env.API_ENDPOINT),\n    }))\n;\n\nmodule.exports = Encore.getWebpackConfig();\n"
  },
  {
    "path": "src/Api/FilterPublishedCommentQueryExtension.php",
    "content": "<?php\n\nnamespace App\\Api;\n\nuse ApiPlatform\\Core\\Bridge\\Doctrine\\Orm\\Extension\\QueryCollectionExtensionInterface;\nuse ApiPlatform\\Core\\Bridge\\Doctrine\\Orm\\Extension\\QueryItemExtensionInterface;\nuse ApiPlatform\\Core\\Bridge\\Doctrine\\Orm\\Util\\QueryNameGeneratorInterface;\nuse App\\Entity\\Comment;\nuse Doctrine\\ORM\\QueryBuilder;\n\nclass FilterPublishedCommentQueryExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface\n{\n    public function applyToCollection(QueryBuilder $qb, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)\n    {\n        if (Comment::class === $resourceClass) {\n            $qb->andWhere(sprintf(\"%s.state = 'published'\", $qb->getRootAliases()[0]));\n        }\n    }\n\n    public function applyToItem(QueryBuilder $qb, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, string $operationName = null, array $context = [])\n    {\n        if (Comment::class === $resourceClass) {\n            $qb->andWhere(sprintf(\"%s.state = 'published'\", $qb->getRootAliases()[0]));\n        }\n    }\n}\n"
  },
  {
    "path": "src/Command/CommentCleanupCommand.php",
    "content": "<?php\n\nnamespace App\\Command;\n\nuse App\\Repository\\CommentRepository;\nuse Symfony\\Component\\Console\\Command\\Command;\nuse Symfony\\Component\\Console\\Input\\InputInterface;\nuse Symfony\\Component\\Console\\Input\\InputOption;\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\nuse Symfony\\Component\\Console\\Style\\SymfonyStyle;\n\nclass CommentCleanupCommand extends Command\n{\n    private $commentRepository;\n\n    protected static $defaultName = 'app:comment:cleanup';\n\n    public function __construct(CommentRepository $commentRepository)\n    {\n        $this->commentRepository = $commentRepository;\n\n        parent::__construct();\n    }\n\n    protected function configure()\n    {\n        $this\n            ->setDescription('Deletes rejected and spam comments from the database')\n            ->addOption('dry-run', null, InputOption::VALUE_NONE, 'Dry run')\n        ;\n    }\n\n    protected function execute(InputInterface $input, OutputInterface $output): int\n    {\n        $io = new SymfonyStyle($input, $output);\n\n        if ($input->getOption('dry-run')) {\n            $io->note('Dry mode enabled');\n\n            $count = $this->commentRepository->countOldRejected();\n        } else {\n            $count = $this->commentRepository->deleteOldRejected();\n        }\n\n        $io->success(sprintf('Deleted \"%d\" old rejected/spam comments.', $count));\n\n        return 0;\n    }\n}\n"
  },
  {
    "path": "src/Command/StepInfoCommand.php",
    "content": "<?php\n\nnamespace App\\Command;\n\nuse Symfony\\Component\\Console\\Command\\Command;\nuse Symfony\\Component\\Console\\Input\\InputInterface;\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\nuse Symfony\\Component\\Process\\Process;\nuse Symfony\\Contracts\\Cache\\CacheInterface;\n\nclass StepInfoCommand extends Command\n{\n    protected static $defaultName = 'app:step:info';\n\n    private $cache;\n\n    public function __construct(CacheInterface $cache)\n    {\n        $this->cache = $cache;\n\n        parent::__construct();\n    }\n\n    protected function execute(InputInterface $input, OutputInterface $output): int\n    {\n        $step = $this->cache->get('app.current_step', function ($item) {\n            $process = new Process(['git', 'tag', '-l', '--points-at', 'HEAD']);\n            $process->mustRun();\n            $item->expiresAfter(30);\n\n            return $process->getOutput();\n        });\n        $output->writeln($step);\n\n        return 0;\n    }\n}\n"
  },
  {
    "path": "src/Controller/.gitignore",
    "content": ""
  },
  {
    "path": "src/Controller/AdminController.php",
    "content": "<?php\n\nnamespace App\\Controller;\n\nuse App\\Entity\\Comment;\nuse App\\Message\\CommentMessage;\nuse Doctrine\\ORM\\EntityManagerInterface;\nuse Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController;\nuse Symfony\\Bundle\\FrameworkBundle\\HttpCache\\HttpCache;\nuse Symfony\\Component\\HttpFoundation\\Request;\nuse Symfony\\Component\\HttpFoundation\\Response;\nuse Symfony\\Component\\HttpKernel\\KernelInterface;\nuse Symfony\\Component\\Messenger\\MessageBusInterface;\nuse Symfony\\Component\\Routing\\Annotation\\Route;\nuse Symfony\\Component\\Routing\\Generator\\UrlGeneratorInterface;\nuse Symfony\\Component\\Workflow\\Registry;\nuse Twig\\Environment;\n\n/**\n * @Route(\"/admin\")\n */\nclass AdminController extends AbstractController\n{\n    private $twig;\n    private $entityManager;\n    private $bus;\n\n    public function __construct(Environment $twig, EntityManagerInterface $entityManager, MessageBusInterface $bus)\n    {\n        $this->twig = $twig;\n        $this->entityManager = $entityManager;\n        $this->bus = $bus;\n    }\n\n    /**\n     * @Route(\"/comment/review/{id}\", name=\"review_comment\")\n     */\n    public function reviewComment(Request $request, Comment $comment, Registry $registry)\n    {\n        $accepted = !$request->query->get('reject');\n\n        $machine = $registry->get($comment);\n        if ($machine->can($comment, 'publish')) {\n            $transition = $accepted ? 'publish' : 'reject';\n        } elseif ($machine->can($comment, 'publish_ham')) {\n            $transition = $accepted ? 'publish_ham' : 'reject_ham';\n        } else {\n            return new Response('Comment already reviewed or not in the right state.');\n        }\n\n        $machine->apply($comment, $transition);\n        $this->entityManager->flush();\n\n        if ($accepted) {\n            $reviewUrl = $this->generateUrl('review_comment', ['id' => $comment->getId()], UrlGeneratorInterface::ABSOLUTE_URL);\n            $this->bus->dispatch(new CommentMessage($comment->getId(), $reviewUrl));\n        }\n\n        return $this->render('admin/review.html.twig', [\n            'transition' => $transition,\n            'comment' => $comment,\n        ]);\n    }\n\n    /**\n     * @Route(\"/http-cache/{uri<.*>}\", methods={\"PURGE\"})\n     */\n    public function purgeHttpCache(KernelInterface $kernel, Request $request, string $uri)\n    {\n        if ('prod' === $kernel->getEnvironment()) {\n            return new Response('KO', 400);\n        }\n\n        $store = (new class($kernel) extends HttpCache {})->getStore();\n        $store->purge($request->getSchemeAndHttpHost().'/'.$uri);\n\n        return new Response('Done');\n    }\n}\n"
  },
  {
    "path": "src/Controller/ConferenceController.php",
    "content": "<?php\n\nnamespace App\\Controller;\n\nuse App\\Entity\\Comment;\nuse App\\Entity\\Conference;\nuse App\\Form\\CommentFormType;\nuse App\\Message\\CommentMessage;\nuse App\\Repository\\CommentRepository;\nuse App\\Repository\\ConferenceRepository;\nuse Doctrine\\ORM\\EntityManagerInterface;\nuse Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController;\nuse Symfony\\Component\\HttpFoundation\\File\\Exception\\FileException;\nuse Symfony\\Component\\HttpFoundation\\Request;\nuse Symfony\\Component\\HttpFoundation\\Response;\nuse Symfony\\Component\\Messenger\\MessageBusInterface;\nuse Symfony\\Component\\Notifier\\Notification\\Notification;\nuse Symfony\\Component\\Notifier\\NotifierInterface;\nuse Symfony\\Component\\Routing\\Annotation\\Route;\nuse Symfony\\Component\\Routing\\Generator\\UrlGeneratorInterface;\nuse Twig\\Environment;\n\nclass ConferenceController extends AbstractController\n{\n    private $twig;\n    private $entityManager;\n    private $bus;\n\n    public function __construct(Environment $twig, EntityManagerInterface $entityManager, MessageBusInterface $bus)\n    {\n        $this->twig = $twig;\n        $this->entityManager = $entityManager;\n        $this->bus = $bus;\n    }\n\n    /**\n     * @Route(\"/\")\n     */\n    public function indexNoLocale()\n    {\n        return $this->redirectToRoute('homepage', ['_locale' => 'en']);\n    }\n\n    /**\n     * @Route(\"/{_locale<%app.supported_locales%>}/\", name=\"homepage\")\n     */\n    public function index(ConferenceRepository $conferenceRepository)\n    {\n        $response = new Response($this->twig->render('conference/index.html.twig', [\n            'conferences' => $conferenceRepository->findAll(),\n        ]));\n        $response->setSharedMaxAge(3600);\n\n        return $response;\n    }\n\n    /**\n     * @Route(\"/{_locale<%app.supported_locales%>}/conference_header\", name=\"conference_header\")\n     */\n    public function conferenceHeader(ConferenceRepository $conferenceRepository)\n    {\n        $response = new Response($this->twig->render('conference/header.html.twig', [\n            'conferences' => $conferenceRepository->findAll(),\n        ]));\n        $response->setSharedMaxAge(3600);\n\n        return $response;\n    }\n\n    /**\n     * @Route(\"/{_locale<%app.supported_locales%>}/conference/{slug}\", name=\"conference\")\n     */\n    public function show(Request $request, Conference $conference, CommentRepository $commentRepository, NotifierInterface $notifier, string $photoDir)\n    {\n        $comment = new Comment();\n        $form = $this->createForm(CommentFormType::class, $comment);\n        $form->handleRequest($request);\n        if ($form->isSubmitted() && $form->isValid()) {\n            $comment->setConference($conference);\n            if ($photo = $form['photo']->getData()) {\n                $filename = bin2hex(random_bytes(6)).'.'.$photo->guessExtension();\n                try {\n                    $photo->move($photoDir, $filename);\n                } catch (FileException $e) {\n                    // unable to upload the photo, give up\n                }\n                $comment->setPhotoFilename($filename);\n            }\n\n            $this->entityManager->persist($comment);\n            $this->entityManager->flush();\n\n            $context = [\n                'user_ip' => $request->getClientIp(),\n                'user_agent' => $request->headers->get('user-agent'),\n                'referrer' => $request->headers->get('referer'),\n                'permalink' => $request->getUri(),\n            ];\n\n            $reviewUrl = $this->generateUrl('review_comment', ['id' => $comment->getId()], UrlGeneratorInterface::ABSOLUTE_URL);\n            $this->bus->dispatch(new CommentMessage($comment->getId(), $reviewUrl, $context));\n\n            $notifier->send(new Notification('Thank you for the feedback; your comment will be posted after moderation.', ['browser']));\n\n            return $this->redirectToRoute('conference', ['slug' => $conference->getSlug()]);\n        }\n\n        if ($form->isSubmitted()) {\n            $notifier->send(new Notification('Can you check your submission? There are some problems with it.', ['browser']));\n        }\n\n        $offset = max(0, $request->query->getInt('offset', 0));\n        $paginator = $commentRepository->getCommentPaginator($conference, $offset);\n\n        return new Response($this->twig->render('conference/show.html.twig', [\n            'conference' => $conference,\n            'comments' => $paginator,\n            'previous' => $offset - CommentRepository::PAGINATOR_PER_PAGE,\n            'next' => min(count($paginator), $offset + CommentRepository::PAGINATOR_PER_PAGE),\n            'comment_form' => $form->createView(),\n        ]));\n    }\n}\n"
  },
  {
    "path": "src/Controller/SecurityController.php",
    "content": "<?php\n\nnamespace App\\Controller;\n\nuse Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController;\nuse Symfony\\Component\\HttpFoundation\\Response;\nuse Symfony\\Component\\Routing\\Annotation\\Route;\nuse Symfony\\Component\\Security\\Http\\Authentication\\AuthenticationUtils;\n\nclass SecurityController extends AbstractController\n{\n    /**\n     * @Route(\"/login\", name=\"app_login\")\n     */\n    public function login(AuthenticationUtils $authenticationUtils): Response\n    {\n        // if ($this->getUser()) {\n        //     return $this->redirectToRoute('target_path');\n        // }\n\n        // get the login error if there is one\n        $error = $authenticationUtils->getLastAuthenticationError();\n        // last username entered by the user\n        $lastUsername = $authenticationUtils->getLastUsername();\n\n        return $this->render('security/login.html.twig', ['last_username' => $lastUsername, 'error' => $error]);\n    }\n\n    /**\n     * @Route(\"/logout\", name=\"app_logout\")\n     */\n    public function logout()\n    {\n        throw new \\Exception('This method can be blank - it will be intercepted by the logout key on your firewall');\n    }\n}\n"
  },
  {
    "path": "src/DataFixtures/AppFixtures.php",
    "content": "<?php\n\nnamespace App\\DataFixtures;\n\nuse App\\Entity\\Admin;\nuse App\\Entity\\Comment;\nuse App\\Entity\\Conference;\nuse Doctrine\\Bundle\\FixturesBundle\\Fixture;\nuse Doctrine\\Common\\Persistence\\ObjectManager;\nuse Symfony\\Component\\Security\\Core\\Encoder\\EncoderFactoryInterface;\n\nclass AppFixtures extends Fixture\n{\n    private $encoderFactory;\n\n    public function __construct(EncoderFactoryInterface $encoderFactory)\n    {\n        $this->encoderFactory = $encoderFactory;\n    }\n\n    public function load(ObjectManager $manager)\n    {\n        $amsterdam = new Conference();\n        $amsterdam->setCity('Amsterdam');\n        $amsterdam->setYear('2019');\n        $amsterdam->setIsInternational(true);\n        $manager->persist($amsterdam);\n\n        $paris = new Conference();\n        $paris->setCity('Paris');\n        $paris->setYear('2020');\n        $paris->setIsInternational(false);\n        $manager->persist($paris);\n\n        $comment1 = new Comment();\n        $comment1->setConference($amsterdam);\n        $comment1->setAuthor('Fabien');\n        $comment1->setEmail('fabien@example.com');\n        $comment1->setText('This was a great conference.');\n        $comment1->setState('published');\n        $manager->persist($comment1);\n\n        $comment2 = new Comment();\n        $comment2->setConference($amsterdam);\n        $comment2->setAuthor('Lucas');\n        $comment2->setEmail('lucas@example.com');\n        $comment2->setText('I think this one is going to be moderated.');\n        $manager->persist($comment2);\n\n        $admin = new Admin();\n        $admin->setRoles(['ROLE_ADMIN']);\n        $admin->setUsername('admin');\n        $admin->setPassword($this->encoderFactory->getEncoder(Admin::class)->encodePassword('admin', null));\n        $manager->persist($admin);\n\n        $manager->flush();\n    }\n}\n"
  },
  {
    "path": "src/Entity/.gitignore",
    "content": ""
  },
  {
    "path": "src/Entity/Admin.php",
    "content": "<?php\n\nnamespace App\\Entity;\n\nuse Doctrine\\ORM\\Mapping as ORM;\nuse Symfony\\Component\\Security\\Core\\User\\UserInterface;\n\n/**\n * @ORM\\Entity(repositoryClass=\"App\\Repository\\AdminRepository\")\n */\nclass Admin implements UserInterface\n{\n    /**\n     * @ORM\\Id()\n     * @ORM\\GeneratedValue()\n     * @ORM\\Column(type=\"integer\")\n     */\n    private $id;\n\n    /**\n     * @ORM\\Column(type=\"string\", length=180, unique=true)\n     */\n    private $username;\n\n    /**\n     * @ORM\\Column(type=\"json\")\n     */\n    private $roles = [];\n\n    /**\n     * @var string The hashed password\n     * @ORM\\Column(type=\"string\")\n     */\n    private $password;\n\n    public function getId(): ?int\n    {\n        return $this->id;\n    }\n\n    /**\n     * A visual identifier that represents this user.\n     *\n     * @see UserInterface\n     */\n    public function getUsername(): string\n    {\n        return (string) $this->username;\n    }\n\n    public function setUsername(string $username): self\n    {\n        $this->username = $username;\n\n        return $this;\n    }\n\n    /**\n     * @see UserInterface\n     */\n    public function getRoles(): array\n    {\n        $roles = $this->roles;\n        // guarantee every user at least has ROLE_USER\n        $roles[] = 'ROLE_USER';\n\n        return array_unique($roles);\n    }\n\n    public function setRoles(array $roles): self\n    {\n        $this->roles = $roles;\n\n        return $this;\n    }\n\n    public function __toString(): string\n    {\n        return $this->username;\n    }\n\n    /**\n     * @see UserInterface\n     */\n    public function getPassword(): string\n    {\n        return (string) $this->password;\n    }\n\n    public function setPassword(string $password): self\n    {\n        $this->password = $password;\n\n        return $this;\n    }\n\n    /**\n     * @see UserInterface\n     */\n    public function getSalt()\n    {\n        // not needed when using the \"bcrypt\" algorithm in security.yaml\n    }\n\n    /**\n     * @see UserInterface\n     */\n    public function eraseCredentials()\n    {\n        // If you store any temporary, sensitive data on the user, clear it here\n        // $this->plainPassword = null;\n    }\n}\n"
  },
  {
    "path": "src/Entity/Comment.php",
    "content": "<?php\n\nnamespace App\\Entity;\n\nuse ApiPlatform\\Core\\Annotation\\ApiFilter;\nuse ApiPlatform\\Core\\Annotation\\ApiResource;\nuse ApiPlatform\\Core\\Bridge\\Doctrine\\Orm\\Filter\\SearchFilter;\nuse Doctrine\\ORM\\Mapping as ORM;\nuse Symfony\\Component\\Serializer\\Annotation\\Groups;\nuse Symfony\\Component\\Validator\\Constraints as Assert;\n\n/**\n * @ORM\\Entity(repositoryClass=\"App\\Repository\\CommentRepository\")\n * @ORM\\HasLifecycleCallbacks()\n *\n * @ApiResource(\n *     collectionOperations={\"get\"={\"normalization_context\"={\"groups\"=\"comment:list\"}}},\n *     itemOperations={\"get\"={\"normalization_context\"={\"groups\"=\"comment:item\"}}},\n *     order={\"createdAt\"=\"DESC\"},\n *     paginationEnabled=false\n * )\n *\n * @ApiFilter(SearchFilter::class, properties={\"conference\": \"exact\"})\n */\nclass Comment\n{\n    /**\n     * @ORM\\Id()\n     * @ORM\\GeneratedValue()\n     * @ORM\\Column(type=\"integer\")\n     *\n     * @Groups({\"comment:list\", \"comment:item\"})\n     */\n    private $id;\n\n    /**\n     * @ORM\\Column(type=\"string\", length=255)\n     * @Assert\\NotBlank\n     *\n     * @Groups({\"comment:list\", \"comment:item\"})\n     */\n    private $author;\n\n    /**\n     * @ORM\\Column(type=\"text\")\n     * @Assert\\NotBlank\n     *\n     * @Groups({\"comment:list\", \"comment:item\"})\n     */\n    private $text;\n\n    /**\n     * @ORM\\Column(type=\"string\", length=255)\n     * @Assert\\NotBlank\n     * @Assert\\Email\n     *\n     * @Groups({\"comment:list\", \"comment:item\"})\n     */\n    private $email;\n\n    /**\n     * @ORM\\Column(type=\"datetime\")\n     *\n     * @Groups({\"comment:list\", \"comment:item\"})\n     */\n    private $createdAt;\n\n    /**\n     * @ORM\\ManyToOne(targetEntity=\"App\\Entity\\Conference\", inversedBy=\"comments\")\n     * @ORM\\JoinColumn(nullable=false)\n     *\n     * @Groups({\"comment:list\", \"comment:item\"})\n     */\n    private $conference;\n\n    /**\n     * @ORM\\Column(type=\"string\", length=255, nullable=true)\n     *\n     * @Groups({\"comment:list\", \"comment:item\"})\n     */\n    private $photoFilename;\n\n    /**\n     * @ORM\\Column(type=\"string\", length=255, options={\"default\": \"submitted\"})\n     */\n    private $state = 'submitted';\n\n    public function __toString(): string\n    {\n        return (string) $this->getEmail();\n    }\n\n    public function getId(): ?int\n    {\n        return $this->id;\n    }\n\n    public function getAuthor(): ?string\n    {\n        return $this->author;\n    }\n\n    public function setAuthor(string $author): self\n    {\n        $this->author = $author;\n\n        return $this;\n    }\n\n    public function getText(): ?string\n    {\n        return $this->text;\n    }\n\n    public function setText(string $text): self\n    {\n        $this->text = $text;\n\n        return $this;\n    }\n\n    public function getEmail(): ?string\n    {\n        return $this->email;\n    }\n\n    public function setEmail(string $email): self\n    {\n        $this->email = $email;\n\n        return $this;\n    }\n\n    public function getCreatedAt(): ?\\DateTimeInterface\n    {\n        return $this->createdAt;\n    }\n\n    public function setCreatedAt(\\DateTimeInterface $createdAt): self\n    {\n        $this->createdAt = $createdAt;\n\n        return $this;\n    }\n\n    /**\n     * @ORM\\PrePersist\n     */\n    public function setCreatedAtValue()\n    {\n        $this->createdAt = new \\DateTime();\n    }\n\n    public function getConference(): ?Conference\n    {\n        return $this->conference;\n    }\n\n    public function setConference(?Conference $conference): self\n    {\n        $this->conference = $conference;\n\n        return $this;\n    }\n\n    public function getPhotoFilename(): ?string\n    {\n        return $this->photoFilename;\n    }\n\n    public function setPhotoFilename(?string $photoFilename): self\n    {\n        $this->photoFilename = $photoFilename;\n\n        return $this;\n    }\n\n    public function getState(): ?string\n    {\n        return $this->state;\n    }\n\n    public function setState(string $state): self\n    {\n        $this->state = $state;\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "src/Entity/Conference.php",
    "content": "<?php\n\nnamespace App\\Entity;\n\nuse ApiPlatform\\Core\\Annotation\\ApiResource;\nuse Doctrine\\Common\\Collections\\ArrayCollection;\nuse Doctrine\\Common\\Collections\\Collection;\nuse Doctrine\\ORM\\Mapping as ORM;\nuse Symfony\\Bridge\\Doctrine\\Validator\\Constraints\\UniqueEntity;\nuse Symfony\\Component\\Serializer\\Annotation\\Groups;\nuse Symfony\\Component\\String\\Slugger\\SluggerInterface;\n\n/**\n * @ORM\\Entity(repositoryClass=\"App\\Repository\\ConferenceRepository\")\n * @UniqueEntity(\"slug\")\n *\n * @ApiResource(\n *     collectionOperations={\"get\"={\"normalization_context\"={\"groups\"=\"conference:list\"}}},\n *     itemOperations={\"get\"={\"normalization_context\"={\"groups\"=\"conference:item\"}}},\n *     order={\"year\"=\"DESC\", \"city\"=\"ASC\"},\n *     paginationEnabled=false\n * )\n */\nclass Conference\n{\n    /**\n     * @ORM\\Id()\n     * @ORM\\GeneratedValue()\n     * @ORM\\Column(type=\"integer\")\n     *\n     * @Groups({\"conference:list\", \"conference:item\"})\n     */\n    private $id;\n\n    /**\n     * @ORM\\Column(type=\"string\", length=255)\n     *\n     * @Groups({\"conference:list\", \"conference:item\"})\n     */\n    private $city;\n\n    /**\n     * @ORM\\Column(type=\"string\", length=4)\n     *\n     * @Groups({\"conference:list\", \"conference:item\"})\n     */\n    private $year;\n\n    /**\n     * @ORM\\Column(type=\"boolean\")\n     *\n     * @Groups({\"conference:list\", \"conference:item\"})\n     */\n    private $isInternational;\n\n    /**\n     * @ORM\\OneToMany(targetEntity=\"App\\Entity\\Comment\", mappedBy=\"conference\", orphanRemoval=true)\n     */\n    private $comments;\n\n    /**\n     * @ORM\\Column(type=\"string\", length=255, unique=true)\n     *\n     * @Groups({\"conference:list\", \"conference:item\"})\n     */\n    private $slug;\n\n    public function __construct()\n    {\n        $this->comments = new ArrayCollection();\n    }\n\n    public function __toString(): string\n    {\n        return $this->city.' '.$this->year;\n    }\n\n    public function getId(): ?int\n    {\n        return $this->id;\n    }\n\n    public function computeSlug(SluggerInterface $slugger)\n    {\n        if (!$this->slug || '-' === $this->slug) {\n            $this->slug = (string) $slugger->slug((string) $this)->lower();\n        }\n    }\n\n    public function getCity(): ?string\n    {\n        return $this->city;\n    }\n\n    public function setCity(string $city): self\n    {\n        $this->city = $city;\n\n        return $this;\n    }\n\n    public function getYear(): ?string\n    {\n        return $this->year;\n    }\n\n    public function setYear(string $year): self\n    {\n        $this->year = $year;\n\n        return $this;\n    }\n\n    public function getIsInternational(): ?bool\n    {\n        return $this->isInternational;\n    }\n\n    public function setIsInternational(bool $isInternational): self\n    {\n        $this->isInternational = $isInternational;\n\n        return $this;\n    }\n\n    /**\n     * @return Collection|Comment[]\n     */\n    public function getComments(): Collection\n    {\n        return $this->comments;\n    }\n\n    public function addComment(Comment $comment): self\n    {\n        if (!$this->comments->contains($comment)) {\n            $this->comments[] = $comment;\n            $comment->setConference($this);\n        }\n\n        return $this;\n    }\n\n    public function removeComment(Comment $comment): self\n    {\n        if ($this->comments->contains($comment)) {\n            $this->comments->removeElement($comment);\n            // set the owning side to null (unless already changed)\n            if ($comment->getConference() === $this) {\n                $comment->setConference(null);\n            }\n        }\n\n        return $this;\n    }\n\n    public function getSlug(): ?string\n    {\n        return $this->slug;\n    }\n\n    public function setSlug(string $slug): self\n    {\n        $this->slug = $slug;\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "src/EntityListener/ConferenceEntityListener.php",
    "content": "<?php\n\nnamespace App\\EntityListener;\n\nuse App\\Entity\\Conference;\nuse Doctrine\\ORM\\Event\\LifecycleEventArgs;\nuse Symfony\\Component\\String\\Slugger\\SluggerInterface;\n\nclass ConferenceEntityListener\n{\n    private $slugger;\n\n    public function __construct(SluggerInterface $slugger)\n    {\n        $this->slugger = $slugger;\n    }\n\n    public function prePersist(Conference $conference, LifecycleEventArgs $event)\n    {\n        $conference->computeSlug($this->slugger);\n    }\n\n    public function preUpdate(Conference $conference, LifecycleEventArgs $event)\n    {\n        $conference->computeSlug($this->slugger);\n    }\n}\n"
  },
  {
    "path": "src/Form/CommentFormType.php",
    "content": "<?php\n\nnamespace App\\Form;\n\nuse App\\Entity\\Comment;\nuse Symfony\\Component\\Form\\AbstractType;\nuse Symfony\\Component\\Form\\Extension\\Core\\Type\\EmailType;\nuse Symfony\\Component\\Form\\Extension\\Core\\Type\\FileType;\nuse Symfony\\Component\\Form\\Extension\\Core\\Type\\SubmitType;\nuse Symfony\\Component\\Form\\FormBuilderInterface;\nuse Symfony\\Component\\OptionsResolver\\OptionsResolver;\nuse Symfony\\Component\\Validator\\Constraints\\Image;\n\nclass CommentFormType extends AbstractType\n{\n    public function buildForm(FormBuilderInterface $builder, array $options)\n    {\n        $builder\n            ->add('author', null, [\n                'label' => 'Your name',\n            ])\n            ->add('text')\n            ->add('email', EmailType::class)\n            ->add('photo', FileType::class, [\n                'required' => false,\n                'mapped' => false,\n                'constraints' => [\n                    new Image(['maxSize' => '1024k'])\n                ],\n            ])\n            ->add('submit', SubmitType::class)\n        ;\n    }\n\n    public function configureOptions(OptionsResolver $resolver)\n    {\n        $resolver->setDefaults([\n            'data_class' => Comment::class,\n        ]);\n    }\n}\n"
  },
  {
    "path": "src/ImageOptimizer.php",
    "content": "<?php\n\nnamespace App;\n\nuse Imagine\\Gd\\Imagine;\nuse Imagine\\Image\\Box;\n\nclass ImageOptimizer\n{\n    private const MAX_WIDTH = 200;\n    private const MAX_HEIGHT = 150;\n\n    private $imagine;\n\n    public function __construct()\n    {\n        $this->imagine = new Imagine();\n    }\n\n    public function resize(string $filename): void\n    {\n        list($iwidth, $iheight) = getimagesize($filename);\n        $ratio = $iwidth / $iheight;\n        $width = self::MAX_WIDTH;\n        $height = self::MAX_HEIGHT;\n        if ($width / $height > $ratio) {\n            $width = $height * $ratio;\n        } else {\n            $height = $width / $ratio;\n        }\n\n        $photo = $this->imagine->open($filename);\n        $photo->resize(new Box($width, $height))->save($filename);\n    }\n}\n"
  },
  {
    "path": "src/Kernel.php",
    "content": "<?php\n\nnamespace App;\n\nuse Symfony\\Bundle\\FrameworkBundle\\Kernel\\MicroKernelTrait;\nuse Symfony\\Component\\Config\\Loader\\LoaderInterface;\nuse Symfony\\Component\\Config\\Resource\\FileResource;\nuse Symfony\\Component\\DependencyInjection\\ContainerBuilder;\nuse Symfony\\Component\\HttpKernel\\Kernel as BaseKernel;\nuse Symfony\\Component\\Routing\\RouteCollectionBuilder;\n\nclass Kernel extends BaseKernel\n{\n    use MicroKernelTrait;\n\n    private const CONFIG_EXTS = '.{php,xml,yaml,yml}';\n\n    public function registerBundles(): iterable\n    {\n        $contents = require $this->getProjectDir().'/config/bundles.php';\n        foreach ($contents as $class => $envs) {\n            if ($envs[$this->environment] ?? $envs['all'] ?? false) {\n                yield new $class();\n            }\n        }\n    }\n\n    public function getProjectDir(): string\n    {\n        return \\dirname(__DIR__);\n    }\n\n    protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader): void\n    {\n        $container->addResource(new FileResource($this->getProjectDir().'/config/bundles.php'));\n        $container->setParameter('container.dumper.inline_class_loader', \\PHP_VERSION_ID < 70400 || $this->debug);\n        $container->setParameter('container.dumper.inline_factories', true);\n        $confDir = $this->getProjectDir().'/config';\n\n        $loader->load($confDir.'/{packages}/*'.self::CONFIG_EXTS, 'glob');\n        $loader->load($confDir.'/{packages}/'.$this->environment.'/*'.self::CONFIG_EXTS, 'glob');\n        $loader->load($confDir.'/{services}'.self::CONFIG_EXTS, 'glob');\n        $loader->load($confDir.'/{services}_'.$this->environment.self::CONFIG_EXTS, 'glob');\n    }\n\n    protected function configureRoutes(RouteCollectionBuilder $routes): void\n    {\n        $confDir = $this->getProjectDir().'/config';\n\n        $routes->import($confDir.'/{routes}/'.$this->environment.'/*'.self::CONFIG_EXTS, '/', 'glob');\n        $routes->import($confDir.'/{routes}/*'.self::CONFIG_EXTS, '/', 'glob');\n        $routes->import($confDir.'/{routes}'.self::CONFIG_EXTS, '/', 'glob');\n    }\n}\n"
  },
  {
    "path": "src/Message/CommentMessage.php",
    "content": "<?php\n\nnamespace App\\Message;\n\nclass CommentMessage\n{\n    private $id;\n    private $reviewUrl;\n    private $context;\n\n    public function __construct(int $id, string $reviewUrl, array $context = [])\n    {\n        $this->id = $id;\n        $this->reviewUrl = $reviewUrl;\n        $this->context = $context;\n    }\n\n    public function getReviewUrl(): string\n    {\n        return $this->reviewUrl;\n    }\n\n    public function getId(): int\n    {\n        return $this->id;\n    }\n\n    public function getContext(): array\n    {\n        return $this->context;\n    }\n}\n"
  },
  {
    "path": "src/MessageHandler/CommentMessageHandler.php",
    "content": "<?php\n\nnamespace App\\MessageHandler;\n\nuse App\\ImageOptimizer;\nuse App\\Message\\CommentMessage;\nuse App\\Notification\\CommentReviewNotification;\nuse App\\Repository\\CommentRepository;\nuse App\\SpamChecker;\nuse Doctrine\\ORM\\EntityManagerInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Symfony\\Component\\Messenger\\Handler\\MessageHandlerInterface;\nuse Symfony\\Component\\Messenger\\MessageBusInterface;\nuse Symfony\\Component\\Notifier\\NotifierInterface;\nuse Symfony\\Component\\Workflow\\WorkflowInterface;\n\nclass CommentMessageHandler implements MessageHandlerInterface\n{\n    private $spamChecker;\n    private $entityManager;\n    private $commentRepository;\n    private $bus;\n    private $workflow;\n    private $notifier;\n    private $imageOptimizer;\n    private $photoDir;\n    private $logger;\n\n    public function __construct(EntityManagerInterface $entityManager, SpamChecker $spamChecker, CommentRepository $commentRepository, MessageBusInterface $bus, WorkflowInterface $commentStateMachine, NotifierInterface $notifier, ImageOptimizer $imageOptimizer, string $photoDir, LoggerInterface $logger = null)\n    {\n        $this->entityManager = $entityManager;\n        $this->spamChecker = $spamChecker;\n        $this->commentRepository = $commentRepository;\n        $this->bus = $bus;\n        $this->workflow = $commentStateMachine;\n        $this->notifier = $notifier;\n        $this->imageOptimizer = $imageOptimizer;\n        $this->photoDir = $photoDir;\n        $this->logger = $logger;\n    }\n\n    public function __invoke(CommentMessage $message)\n    {\n        $comment = $this->commentRepository->find($message->getId());\n        if (!$comment) {\n            return;\n        }\n\n\n        if ($this->workflow->can($comment, 'accept')) {\n            $score = $this->spamChecker->getSpamScore($comment, $message->getContext());\n            $transition = 'accept';\n            if (2 === $score) {\n                $transition = 'reject_spam';\n            } elseif (1 === $score) {\n                $transition = 'might_be_spam';\n            }\n            $this->workflow->apply($comment, $transition);\n            $this->entityManager->flush();\n\n            $this->bus->dispatch($message);\n        } elseif ($this->workflow->can($comment, 'publish') || $this->workflow->can($comment, 'publish_ham')) {\n            $notification = new CommentReviewNotification($comment, $message->getReviewUrl());\n            $this->notifier->send($notification, ...$this->notifier->getAdminRecipients());\n        } elseif ($this->workflow->can($comment, 'optimize')) {\n            if ($comment->getPhotoFilename()) {\n                $this->imageOptimizer->resize($this->photoDir.'/'.$comment->getPhotoFilename());\n            }\n            $this->workflow->apply($comment, 'optimize');\n            $this->entityManager->flush();\n        } elseif ($this->logger) {\n            $this->logger->debug('Dropping comment message', ['comment' => $comment->getId(), 'state' => $comment->getState()]);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Migrations/.gitignore",
    "content": ""
  },
  {
    "path": "src/Migrations/Version20200107080917.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace DoctrineMigrations;\n\nuse Doctrine\\DBAL\\Schema\\Schema;\nuse Doctrine\\Migrations\\AbstractMigration;\n\n/**\n * Auto-generated Migration: Please modify to your needs!\n */\nfinal class Version20200107080917 extends AbstractMigration\n{\n    public function getDescription() : string\n    {\n        return '';\n    }\n\n    public function up(Schema $schema) : void\n    {\n        // this up() migration is auto-generated, please modify it to your needs\n        $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \\'postgresql\\'.');\n\n        $this->addSql('CREATE SEQUENCE comment_id_seq INCREMENT BY 1 MINVALUE 1 START 1');\n        $this->addSql('CREATE SEQUENCE conference_id_seq INCREMENT BY 1 MINVALUE 1 START 1');\n        $this->addSql('CREATE TABLE comment (id INT NOT NULL, conference_id INT NOT NULL, author VARCHAR(255) NOT NULL, text TEXT NOT NULL, email VARCHAR(255) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, photo_filename VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id))');\n        $this->addSql('CREATE INDEX IDX_9474526C604B8382 ON comment (conference_id)');\n        $this->addSql('CREATE TABLE conference (id INT NOT NULL, city VARCHAR(255) NOT NULL, year VARCHAR(4) NOT NULL, is_international BOOLEAN NOT NULL, PRIMARY KEY(id))');\n        $this->addSql('ALTER TABLE comment ADD CONSTRAINT FK_9474526C604B8382 FOREIGN KEY (conference_id) REFERENCES conference (id) NOT DEFERRABLE INITIALLY IMMEDIATE');\n    }\n\n    public function down(Schema $schema) : void\n    {\n        // this down() migration is auto-generated, please modify it to your needs\n        $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \\'postgresql\\'.');\n\n        $this->addSql('CREATE SCHEMA public');\n        $this->addSql('ALTER TABLE comment DROP CONSTRAINT FK_9474526C604B8382');\n        $this->addSql('DROP SEQUENCE comment_id_seq CASCADE');\n        $this->addSql('DROP SEQUENCE conference_id_seq CASCADE');\n        $this->addSql('DROP TABLE comment');\n        $this->addSql('DROP TABLE conference');\n    }\n}\n"
  },
  {
    "path": "src/Migrations/Version20200107081222.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace DoctrineMigrations;\n\nuse Doctrine\\DBAL\\Schema\\Schema;\nuse Doctrine\\Migrations\\AbstractMigration;\n\n/**\n * Auto-generated Migration: Please modify to your needs!\n */\nfinal class Version20200107081222 extends AbstractMigration\n{\n    public function getDescription() : string\n    {\n        return '';\n    }\n\n    public function up(Schema $schema) : void\n    {\n        // this up() migration is auto-generated, please modify it to your needs\n        $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \\'postgresql\\'.');\n\n        $this->addSql('ALTER TABLE conference ADD slug VARCHAR(255)');\n        $this->addSql(\"UPDATE conference SET slug=CONCAT(LOWER(city), '-', year)\");\n        $this->addSql('ALTER TABLE conference ALTER COLUMN slug SET NOT NULL');\n    }\n\n    public function down(Schema $schema) : void\n    {\n        // this down() migration is auto-generated, please modify it to your needs\n        $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \\'postgresql\\'.');\n\n        $this->addSql('CREATE SCHEMA public');\n        $this->addSql('ALTER TABLE conference DROP slug');\n    }\n}\n"
  },
  {
    "path": "src/Migrations/Version20200107081238.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace DoctrineMigrations;\n\nuse Doctrine\\DBAL\\Schema\\Schema;\nuse Doctrine\\Migrations\\AbstractMigration;\n\n/**\n * Auto-generated Migration: Please modify to your needs!\n */\nfinal class Version20200107081238 extends AbstractMigration\n{\n    public function getDescription() : string\n    {\n        return '';\n    }\n\n    public function up(Schema $schema) : void\n    {\n        // this up() migration is auto-generated, please modify it to your needs\n        $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \\'postgresql\\'.');\n\n        $this->addSql('CREATE UNIQUE INDEX UNIQ_911533C8989D9B62 ON conference (slug)');\n    }\n\n    public function down(Schema $schema) : void\n    {\n        // this down() migration is auto-generated, please modify it to your needs\n        $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \\'postgresql\\'.');\n\n        $this->addSql('CREATE SCHEMA public');\n        $this->addSql('DROP INDEX UNIQ_911533C8989D9B62');\n    }\n}\n"
  },
  {
    "path": "src/Migrations/Version20200107081419.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace DoctrineMigrations;\n\nuse Doctrine\\DBAL\\Schema\\Schema;\nuse Doctrine\\Migrations\\AbstractMigration;\n\n/**\n * Auto-generated Migration: Please modify to your needs!\n */\nfinal class Version20200107081419 extends AbstractMigration\n{\n    public function getDescription() : string\n    {\n        return '';\n    }\n\n    public function up(Schema $schema) : void\n    {\n        // this up() migration is auto-generated, please modify it to your needs\n        $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \\'postgresql\\'.');\n\n        $this->addSql('CREATE SEQUENCE admin_id_seq INCREMENT BY 1 MINVALUE 1 START 1');\n        $this->addSql('CREATE TABLE admin (id INT NOT NULL, username VARCHAR(180) NOT NULL, roles JSON NOT NULL, password VARCHAR(255) NOT NULL, PRIMARY KEY(id))');\n        $this->addSql('CREATE UNIQUE INDEX UNIQ_880E0D76F85E0677 ON admin (username)');\n    }\n\n    public function down(Schema $schema) : void\n    {\n        // this down() migration is auto-generated, please modify it to your needs\n        $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \\'postgresql\\'.');\n\n        $this->addSql('CREATE SCHEMA public');\n        $this->addSql('DROP SEQUENCE admin_id_seq CASCADE');\n        $this->addSql('DROP TABLE admin');\n    }\n}\n"
  },
  {
    "path": "src/Migrations/Version20200107081708.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace DoctrineMigrations;\n\nuse Doctrine\\DBAL\\Schema\\Schema;\nuse Doctrine\\Migrations\\AbstractMigration;\n\n/**\n * Auto-generated Migration: Please modify to your needs!\n */\nfinal class Version20200107081708 extends AbstractMigration\n{\n    public function getDescription() : string\n    {\n        return '';\n    }\n\n    public function up(Schema $schema) : void\n    {\n        // this up() migration is auto-generated, please modify it to your needs\n        $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \\'postgresql\\'.');\n\n        $this->addSql('ALTER TABLE comment ADD state VARCHAR(255)');\n        $this->addSql(\"UPDATE comment SET state='published'\");\n        $this->addSql('ALTER TABLE comment ALTER COLUMN state SET NOT NULL');\n    }\n\n    public function down(Schema $schema) : void\n    {\n        // this down() migration is auto-generated, please modify it to your needs\n        $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \\'postgresql\\'.');\n\n        $this->addSql('CREATE SCHEMA public');\n        $this->addSql('ALTER TABLE comment DROP state');\n    }\n}\n"
  },
  {
    "path": "src/Notification/CommentReviewNotification.php",
    "content": "<?php\n\nnamespace App\\Notification;\n\nuse App\\Entity\\Comment;\nuse Symfony\\Component\\Notifier\\Bridge\\Slack\\Block\\SlackActionsBlock;\nuse Symfony\\Component\\Notifier\\Bridge\\Slack\\Block\\SlackDividerBlock;\nuse Symfony\\Component\\Notifier\\Bridge\\Slack\\Block\\SlackSectionBlock;\nuse Symfony\\Component\\Notifier\\Bridge\\Slack\\SlackOptions;\nuse Symfony\\Component\\Notifier\\Message\\ChatMessage;\nuse Symfony\\Component\\Notifier\\Message\\EmailMessage;\nuse Symfony\\Component\\Notifier\\Notification\\ChatNotificationInterface;\nuse Symfony\\Component\\Notifier\\Notification\\EmailNotificationInterface;\nuse Symfony\\Component\\Notifier\\Notification\\Notification;\nuse Symfony\\Component\\Notifier\\Recipient\\Recipient;\n\nclass CommentReviewNotification extends Notification implements EmailNotificationInterface, ChatNotificationInterface\n{\n    private $comment;\n    private $reviewUrl;\n\n    public function __construct(Comment $comment, string $reviewUrl)\n    {\n        $this->comment = $comment;\n        $this->reviewUrl = $reviewUrl;\n\n        parent::__construct('New comment posted');\n    }\n\n    public function asEmailMessage(Recipient $recipient, string $transport = null): ?EmailMessage\n    {\n        $message = EmailMessage::fromNotification($this, $recipient, $transport);\n        $message->getMessage()\n            ->htmlTemplate('emails/comment_notification.html.twig')\n            ->context(['comment' => $this->comment])\n        ;\n\n        return $message;\n    }\n\n    public function asChatMessage(Recipient $recipient, string $transport = null): ?ChatMessage\n    {\n        if ('slack' !== $transport) {\n            return null;\n        }\n\n        $message = ChatMessage::fromNotification($this, $recipient, $transport);\n        $message->subject($this->getSubject());\n        $message->options((new SlackOptions())\n            ->iconEmoji('tada')\n            ->iconUrl('https://guestbook.example.com')\n            ->username('Guestbook')\n            ->block((new SlackSectionBlock())->text($this->getSubject()))\n            ->block(new SlackDividerBlock())\n            ->block((new SlackSectionBlock())\n                ->text(sprintf('%s (%s) says: %s', $this->comment->getAuthor(), $this->comment->getEmail(), $this->comment->getText()))\n            )\n            ->block((new SlackActionsBlock())\n                ->button('Accept', $this->reviewUrl, 'primary')\n                ->button('Reject', $this->reviewUrl.'?reject=1', 'danger')\n            )\n        );\n\n        return $message;\n    }\n\n    public function getChannels(Recipient $recipient): array\n    {\n        if (preg_match('{\\b(great|awesome)\\b}i', $this->comment->getText())) {\n            return ['email', 'chat/slack'];\n        }\n\n        $this->importance(Notification::IMPORTANCE_LOW);\n\n        return ['email'];\n    }\n}\n"
  },
  {
    "path": "src/Repository/.gitignore",
    "content": ""
  },
  {
    "path": "src/Repository/AdminRepository.php",
    "content": "<?php\n\nnamespace App\\Repository;\n\nuse App\\Entity\\Admin;\nuse Doctrine\\Bundle\\DoctrineBundle\\Repository\\ServiceEntityRepository;\nuse Doctrine\\Common\\Persistence\\ManagerRegistry;\nuse Symfony\\Component\\Security\\Core\\Exception\\UnsupportedUserException;\nuse Symfony\\Component\\Security\\Core\\User\\PasswordUpgraderInterface;\nuse Symfony\\Component\\Security\\Core\\User\\UserInterface;\n\n/**\n * @method Admin|null find($id, $lockMode = null, $lockVersion = null)\n * @method Admin|null findOneBy(array $criteria, array $orderBy = null)\n * @method Admin[]    findAll()\n * @method Admin[]    findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)\n */\nclass AdminRepository extends ServiceEntityRepository implements PasswordUpgraderInterface\n{\n    public function __construct(ManagerRegistry $registry)\n    {\n        parent::__construct($registry, Admin::class);\n    }\n\n    /**\n     * Used to upgrade (rehash) the user's password automatically over time.\n     */\n    public function upgradePassword(UserInterface $user, string $newEncodedPassword): void\n    {\n        if (!$user instanceof User) {\n            throw new UnsupportedUserException(sprintf('Instances of \"%s\" are not supported.', \\get_class($user)));\n        }\n\n        $user->setPassword($newEncodedPassword);\n        $this->_em->persist($user);\n        $this->_em->flush();\n    }\n\n    // /**\n    //  * @return Admin[] Returns an array of Admin objects\n    //  */\n    /*\n    public function findByExampleField($value)\n    {\n        return $this->createQueryBuilder('a')\n            ->andWhere('a.exampleField = :val')\n            ->setParameter('val', $value)\n            ->orderBy('a.id', 'ASC')\n            ->setMaxResults(10)\n            ->getQuery()\n            ->getResult()\n        ;\n    }\n    */\n\n    /*\n    public function findOneBySomeField($value): ?Admin\n    {\n        return $this->createQueryBuilder('a')\n            ->andWhere('a.exampleField = :val')\n            ->setParameter('val', $value)\n            ->getQuery()\n            ->getOneOrNullResult()\n        ;\n    }\n    */\n}\n"
  },
  {
    "path": "src/Repository/CommentRepository.php",
    "content": "<?php\n\nnamespace App\\Repository;\n\nuse App\\Entity\\Comment;\nuse App\\Entity\\Conference;\nuse Doctrine\\Bundle\\DoctrineBundle\\Repository\\ServiceEntityRepository;\nuse Doctrine\\Common\\Persistence\\ManagerRegistry;\nuse Doctrine\\ORM\\QueryBuilder;\nuse Doctrine\\ORM\\Tools\\Pagination\\Paginator;\n\n/**\n * @method Comment|null find($id, $lockMode = null, $lockVersion = null)\n * @method Comment|null findOneBy(array $criteria, array $orderBy = null)\n * @method Comment[]    findAll()\n * @method Comment[]    findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)\n */\nclass CommentRepository extends ServiceEntityRepository\n{\n    private const DAYS_BEFORE_REJECTED_REMOVAL = 7;\n\n    public const PAGINATOR_PER_PAGE = 2;\n\n    public function __construct(ManagerRegistry $registry)\n    {\n        parent::__construct($registry, Comment::class);\n    }\n\n    public function countOldRejected(): int\n    {\n        return $this->getOldRejectedQueryBuilder()->select('COUNT(c.id)')->getQuery()->getSingleScalarResult();\n    }\n\n    public function deleteOldRejected(): int\n    {\n        return $this->getOldRejectedQueryBuilder()->delete()->getQuery()->execute();\n    }\n\n    private function getOldRejectedQueryBuilder(): QueryBuilder\n    {\n        return $this->createQueryBuilder('c')\n            ->andWhere('c.state = :state_rejected or c.state = :state_spam')\n            ->andWhere('c.createdAt < :date')\n            ->setParameters([\n                'state_rejected' => 'rejected',\n                'state_spam' => 'spam',\n                'date' => new \\DateTime(-self::DAYS_BEFORE_REJECTED_REMOVAL.' days'),\n            ])\n        ;\n    }\n\n    public function getCommentPaginator(Conference $conference, int $offset): Paginator\n    {\n        $query = $this->createQueryBuilder('c')\n            ->andWhere('c.conference = :conference')\n            ->andWhere('c.state = :state')\n            ->setParameter('conference', $conference)\n            ->setParameter('state', 'published')\n            ->orderBy('c.createdAt', 'DESC')\n            ->setMaxResults(self::PAGINATOR_PER_PAGE)\n            ->setFirstResult($offset)\n            ->getQuery()\n        ;\n\n        return new Paginator($query);\n    }\n\n    // /**\n    //  * @return Comment[] Returns an array of Comment objects\n    //  */\n    /*\n    public function findByExampleField($value)\n    {\n        return $this->createQueryBuilder('c')\n            ->andWhere('c.exampleField = :val')\n            ->setParameter('val', $value)\n            ->orderBy('c.id', 'ASC')\n            ->setMaxResults(10)\n            ->getQuery()\n            ->getResult()\n        ;\n    }\n    */\n\n    /*\n    public function findOneBySomeField($value): ?Comment\n    {\n        return $this->createQueryBuilder('c')\n            ->andWhere('c.exampleField = :val')\n            ->setParameter('val', $value)\n            ->getQuery()\n            ->getOneOrNullResult()\n        ;\n    }\n    */\n}\n"
  },
  {
    "path": "src/Repository/ConferenceRepository.php",
    "content": "<?php\n\nnamespace App\\Repository;\n\nuse App\\Entity\\Conference;\nuse Doctrine\\Bundle\\DoctrineBundle\\Repository\\ServiceEntityRepository;\nuse Doctrine\\Common\\Persistence\\ManagerRegistry;\n\n/**\n * @method Conference|null find($id, $lockMode = null, $lockVersion = null)\n * @method Conference|null findOneBy(array $criteria, array $orderBy = null)\n * @method Conference[]    findAll()\n * @method Conference[]    findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)\n */\nclass ConferenceRepository extends ServiceEntityRepository\n{\n    public function __construct(ManagerRegistry $registry)\n    {\n        parent::__construct($registry, Conference::class);\n    }\n\n    public function findAll()\n    {\n        return $this->findBy([], ['year' => 'ASC', 'city' => 'ASC']);\n    }\n\n    // /**\n    //  * @return Conference[] Returns an array of Conference objects\n    //  */\n    /*\n    public function findByExampleField($value)\n    {\n        return $this->createQueryBuilder('c')\n            ->andWhere('c.exampleField = :val')\n            ->setParameter('val', $value)\n            ->orderBy('c.id', 'ASC')\n            ->setMaxResults(10)\n            ->getQuery()\n            ->getResult()\n        ;\n    }\n    */\n\n    /*\n    public function findOneBySomeField($value): ?Conference\n    {\n        return $this->createQueryBuilder('c')\n            ->andWhere('c.exampleField = :val')\n            ->setParameter('val', $value)\n            ->getQuery()\n            ->getOneOrNullResult()\n        ;\n    }\n    */\n}\n"
  },
  {
    "path": "src/Security/AppAuthenticator.php",
    "content": "<?php\n\nnamespace App\\Security;\n\nuse App\\Entity\\Admin;\nuse Doctrine\\ORM\\EntityManagerInterface;\nuse Symfony\\Component\\HttpFoundation\\RedirectResponse;\nuse Symfony\\Component\\HttpFoundation\\Request;\nuse Symfony\\Component\\Routing\\Generator\\UrlGeneratorInterface;\nuse Symfony\\Component\\Security\\Core\\Authentication\\Token\\TokenInterface;\nuse Symfony\\Component\\Security\\Core\\Encoder\\UserPasswordEncoderInterface;\nuse Symfony\\Component\\Security\\Core\\Exception\\CustomUserMessageAuthenticationException;\nuse Symfony\\Component\\Security\\Core\\Exception\\InvalidCsrfTokenException;\nuse Symfony\\Component\\Security\\Core\\Security;\nuse Symfony\\Component\\Security\\Core\\User\\UserInterface;\nuse Symfony\\Component\\Security\\Core\\User\\UserProviderInterface;\nuse Symfony\\Component\\Security\\Csrf\\CsrfToken;\nuse Symfony\\Component\\Security\\Csrf\\CsrfTokenManagerInterface;\nuse Symfony\\Component\\Security\\Guard\\Authenticator\\AbstractFormLoginAuthenticator;\nuse Symfony\\Component\\Security\\Guard\\PasswordAuthenticatedInterface;\nuse Symfony\\Component\\Security\\Http\\Util\\TargetPathTrait;\n\nclass AppAuthenticator extends AbstractFormLoginAuthenticator implements PasswordAuthenticatedInterface\n{\n    use TargetPathTrait;\n\n    private $entityManager;\n    private $urlGenerator;\n    private $csrfTokenManager;\n    private $passwordEncoder;\n\n    public function __construct(EntityManagerInterface $entityManager, UrlGeneratorInterface $urlGenerator, CsrfTokenManagerInterface $csrfTokenManager, UserPasswordEncoderInterface $passwordEncoder)\n    {\n        $this->entityManager = $entityManager;\n        $this->urlGenerator = $urlGenerator;\n        $this->csrfTokenManager = $csrfTokenManager;\n        $this->passwordEncoder = $passwordEncoder;\n    }\n\n    public function supports(Request $request)\n    {\n        return 'app_login' === $request->attributes->get('_route')\n            && $request->isMethod('POST');\n    }\n\n    public function getCredentials(Request $request)\n    {\n        $credentials = [\n            'username' => $request->request->get('username'),\n            'password' => $request->request->get('password'),\n            'csrf_token' => $request->request->get('_csrf_token'),\n        ];\n        $request->getSession()->set(\n            Security::LAST_USERNAME,\n            $credentials['username']\n        );\n\n        return $credentials;\n    }\n\n    public function getUser($credentials, UserProviderInterface $userProvider)\n    {\n        $token = new CsrfToken('authenticate', $credentials['csrf_token']);\n        if (!$this->csrfTokenManager->isTokenValid($token)) {\n            throw new InvalidCsrfTokenException();\n        }\n\n        $user = $this->entityManager->getRepository(Admin::class)->findOneBy(['username' => $credentials['username']]);\n\n        if (!$user) {\n            // fail authentication with a custom error\n            throw new CustomUserMessageAuthenticationException('Username could not be found.');\n        }\n\n        return $user;\n    }\n\n    public function checkCredentials($credentials, UserInterface $user)\n    {\n        return $this->passwordEncoder->isPasswordValid($user, $credentials['password']);\n    }\n\n    /**\n     * Used to upgrade (rehash) the user's password automatically over time.\n     */\n    public function getPassword($credentials): ?string\n    {\n        return $credentials['password'];\n    }\n\n    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)\n    {\n        if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {\n            return new RedirectResponse($targetPath);\n        }\n\n        return new RedirectResponse($this->urlGenerator->generate('easyadmin'));\n    }\n\n    protected function getLoginUrl()\n    {\n        return $this->urlGenerator->generate('app_login');\n    }\n}\n"
  },
  {
    "path": "src/SpamChecker.php",
    "content": "<?php\n\nnamespace App;\n\nuse App\\Entity\\Comment;\nuse Symfony\\Contracts\\HttpClient\\HttpClientInterface;\n\nclass SpamChecker\n{\n    private $client;\n    private $endpoint;\n\n    public function __construct(HttpClientInterface $client, string $akismetKey)\n    {\n        $this->client = $client;\n        $this->endpoint = sprintf('https://%s.rest.akismet.com/1.1/comment-check', $akismetKey);\n    }\n\n    /**\n     * @return int Spam score: 0: not spam, 1: maybe spam, 2: blatant spam\n     *\n     * @throws \\RuntimeException if the call did not work\n     */\n    public function getSpamScore(Comment $comment, array $context): int\n    {\n        $response = $this->client->request('POST', $this->endpoint, [\n            'body' => array_merge($context, [\n                'blog' => 'https://guestbook.example.com',\n                'comment_type' => 'comment',\n                'comment_author' => $comment->getAuthor(),\n                'comment_author_email' => $comment->getEmail(),\n                'comment_content' => $comment->getText(),\n                'comment_date_gmt' => $comment->getCreatedAt()->format('c'),\n                'blog_lang' => 'en',\n                'blog_charset' => 'UTF-8',\n                'is_test' => true,\n            ]),\n        ]);\n\n        $headers = $response->getHeaders();\n        if ('discard' === ($headers['x-akismet-pro-tip'][0] ?? '')) {\n            return 2;\n        }\n\n        $content = $response->getContent();\n        if (isset($headers['x-akismet-debug-help'][0])) {\n            throw new \\RuntimeException(sprintf('Unable to check for spam: %s (%s).', $content, $headers['x-akismet-debug-help'][0]));\n        }\n\n        return 'true' === $content ? 1 : 0;\n    }\n}\n"
  },
  {
    "path": "templates/admin/review.html.twig",
    "content": "{% extends 'base.html.twig' %}\n\n{% block body %}\n    <h2>Comment reviewed, thank you!</h2>\n\n    <p>Applied transition: <strong>{{ transition }}</strong></p>\n    <p>New state: <strong>{{ comment.state }}</strong></p>\n{% endblock %}\n"
  },
  {
    "path": "templates/base.html.twig",
    "content": "<!doctype html>\n<html lang=\"en\">\n    <head>\n        <meta charset=\"utf-8\">\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n\n        <title>{% block title %}Welcome!{% endblock %}</title>\n\n        {% block stylesheets %}\n            <link href=\"https://fonts.googleapis.com/css?family=Open+Sans:300,300i,400,400i,500,500i,600,600i&display=swap\" rel=\"stylesheet\" />\n            {{ encore_entry_link_tags('app') }}\n        {% endblock %}\n    </head>\n    <body>\n        <header class=\"header\">\n            <h1 class=\"sr-only\">\n                Conference Guestbook\n            </h1>\n\n            <nav class=\"navbar navbar-expand-xl navbar-light bg-light\">\n                <div class=\"container mt-4 mb-3\">\n                    <a class=\"navbar-brand mr-4 pr-2\" href=\"{{ path('homepage') }}\">\n                        &#128217; {{ 'Conference Guestbook'|trans }}\n                    </a>\n\n                    <button class=\"navbar-toggler border-0\" type=\"button\" data-toggle=\"collapse\" data-target=\"#header-menu\" aria-controls=\"navbarSupportedContent\" aria-expanded=\"false\" aria-label=\"Afficher/Cacher la navigation\">\n                        <span class=\"navbar-toggler-icon\"></span>\n                    </button>\n\n                    <div class=\"collapse navbar-collapse\" id=\"header-menu\">\n                        <ul class=\"navbar-nav ml-auto\">\n                            <li class=\"nav-item mr-3\">\n                                <a class=\"nav-link\" href=\"{{ path('easyadmin') }}\">\n                                    Admin\n                                </a>\n                            </li>\n<li class=\"nav-item dropdown\">\n    <a class=\"nav-link dropdown-toggle\" href=\"#\" id=\"dropdown-language\" role=\"button\"\n        data-toggle=\"dropdown\" aria-haspopup=\"true\" aria-expanded=\"false\">\n        {{ app.request.locale|locale_name(app.request.locale)|u.title }}\n    </a>\n    <div class=\"dropdown-menu dropdown-menu-right\" aria-labelledby=\"dropdown-language\">\n        <a class=\"dropdown-item\" href=\"{{ path('homepage', {_locale: 'en'}) }}\">English</a>\n        <a class=\"dropdown-item\" href=\"{{ path('homepage', {_locale: 'fr'}) }}\">Français</a>\n    </div>\n</li>\n                        </ul>\n                    </div>\n                </div>\n            </nav>\n\n            <nav class=\"bg-light border-bottom\">\n                <div class=\"container\">\n                    {{ render_esi(path('conference_header')) }}\n                </div>\n            </nav>\n        </header>\n\n        <main role=\"main\" class=\"container mt-5\">\n            {% block body %}{% endblock %}\n        </main>\n\n        <footer class=\"mt-7 px-3 py-5 text-center text-muted\">\n            <p>\n                Conference Guestbook\n            </p>\n            <p>\n                <a href=\"#\" class=\"text-white\">Back to top</a>\n            </p>\n        </footer>\n\n        {% block javascripts %}\n            {{ encore_entry_script_tags('app') }}\n        {% endblock %}\n    </body>\n</html>\n"
  },
  {
    "path": "templates/conference/header.html.twig",
    "content": "{% for conference in conferences %}\n    <a class=\"nav-conference\" href=\"{{ path('conference', { slug: conference.slug }) }}\">\n        {{ conference }}\n    </a>\n{% endfor %}\n"
  },
  {
    "path": "templates/conference/index.html.twig",
    "content": "{% extends 'base.html.twig' %}\n\n{% block title %}Conference Guestbook{% endblock %}\n\n{% block body %}\n    <h2 class=\"mb-5\">\n        {{ 'Give your feedback!'|trans }}\n    </h2>\n\n    {% for row in conferences|batch(4) %}\n        <div class=\"row\">\n            {% for conference in row %}\n                <div class=\"col-12 col-md-6 col-lg-3 mb-4\">\n                    <div class=\"card border shadow lift\">\n                        <div class=\"card-body\">\n                            <div class=\"card-title\">\n                                <h4 class=\"font-weight-light\">\n                                    {{ conference }}\n                                </h4>\n                            </div>\n\n                            <a href=\"{{ path('conference', { slug: conference.slug }) }}\"\n                               class=\"btn btn-sm btn-blue stretched-link\">\n                                {{ 'View'|trans }}\n                            </a>\n                        </div>\n                    </div>\n                </div>\n            {% endfor %}\n        </div>\n    {% endfor %}\n{% endblock %}\n"
  },
  {
    "path": "templates/conference/show.html.twig",
    "content": "{% extends 'base.html.twig' %}\n\n{% block title %}Conference Guestbook - {{ conference }}{% endblock %}\n\n{% block body %}\n    {% for message in app.flashes('notification') %}\n        <div class=\"alert alert-info alert-dismissible fade show\">\n            {{ message }}\n            <button type=\"button\" class=\"close\" data-dismiss=\"alert\" aria-label=\"Close\"><span aria-hidden=\"true\">&times;</span></button>\n        </div>\n    {% endfor %}\n\n    <h2 class=\"mb-5\">\n        {{ conference }} Conference\n    </h2>\n\n    <div class=\"row\">\n        <div class=\"col-12 col-lg-8\">\n            {% if comments|length > 0 %}\n                {% for comment in comments %}\n                    <div class=\"media shadow border rounded-lg p-3 mb-4\">\n                        <div class=\"comment-img mr-3\">\n                            {% if comment.photofilename %}\n                                <a href=\"{{ asset('uploads/photos/' ~ comment.photofilename) }}\" target=\"_blank\">\n                                    <img src=\"{{ asset('uploads/photos/' ~ comment.photofilename) }}\" />\n                                </a>\n                            {% endif %}\n                        </div>\n\n                        <div class=\"media-body\">\n                            <h4 class=\"font-weight-light mb-0\">\n                                {{ comment.author }}\n                            </h4>\n\n                            <div class=\"mb-2\">\n                                <small class=\"text-muted text-uppercase\">\n                                    {{ comment.createdAt|format_datetime('medium', 'short') }}\n                                </small>\n                            </div>\n\n                            <div class=\"comment-text\">\n                                {{ comment.text|nl2br }}\n                            </div>\n                        </div>\n                    </div>\n                {% endfor %}\n                <div>{{ 'nb_of_comments'|trans({count: comments|length}) }}</div>\n                {% if previous >= 0 %}\n                    <a href=\"{{ path('conference', { slug: conference.slug, offset: previous }) }}\">Previous</a>\n                {% endif %}\n                {% if next < comments|length %}\n                    <a href=\"{{ path('conference', { slug: conference.slug, offset: next }) }}\">Next</a>\n                {% endif %}\n            {% else %}\n                <div class=\"text-center\">\n                    No comments have been posted yet for this conference.\n                </div>\n            {% endif %}\n        </div>\n        <div class=\"col-12 col-lg-4\">\n            <div class=\"bg-light shadow border rounded-lg p-4\">\n                <h3 class=\"font-weight-light\">\n                    Add your own feedback\n                </h3>\n\n                {{ form(comment_form) }}\n            </div>\n        </div>\n    </div>\n{% endblock %}\n"
  },
  {
    "path": "templates/emails/comment_notification.html.twig",
    "content": "{% extends '@email/default/notification/body.html.twig' %}\n\n{% block content %}\n    Author: {{ comment.author }}<br />\n    Email: {{ comment.email }}<br />\n    State: {{ comment.state }}<br />\n\n    <p>\n        {{ comment.text }}\n    </p>\n{% endblock %}\n\n{% block action %}\n    <spacer size=\"16\"></spacer>\n    <button href=\"{{ url('review_comment', { id: comment.id }) }}\">Accept</button>\n    <button href=\"{{ url('review_comment', { id: comment.id, reject: true }) }}\">Reject</button>\n{% endblock %}\n"
  },
  {
    "path": "templates/security/login.html.twig",
    "content": "{% extends 'base.html.twig' %}\n\n{% block title %}Log in!{% endblock %}\n\n{% block body %}\n<form method=\"post\">\n    {% if error %}\n        <div class=\"alert alert-danger\">{{ error.messageKey|trans(error.messageData, 'security') }}</div>\n    {% endif %}\n\n    {% if app.user %}\n        <div class=\"mb-3\">\n            You are logged in as {{ app.user.username }}, <a href=\"{{ path('app_logout') }}\">Logout</a>\n        </div>\n    {% endif %}\n\n    <h1 class=\"h3 mb-3 font-weight-normal\">Please sign in</h1>\n    <label for=\"inputUsername\">Username</label>\n    <input type=\"text\" value=\"{{ last_username }}\" name=\"username\" id=\"inputUsername\" class=\"form-control\" required autofocus>\n    <label for=\"inputPassword\">Password</label>\n    <input type=\"password\" name=\"password\" id=\"inputPassword\" class=\"form-control\" required>\n\n    <input type=\"hidden\" name=\"_csrf_token\"\n           value=\"{{ csrf_token('authenticate') }}\"\n    >\n\n    {#\n        Uncomment this section and add a remember_me option below your firewall to activate remember me functionality.\n        See https://symfony.com/doc/current/security/remember_me.html\n\n        <div class=\"checkbox mb-3\">\n            <label>\n                <input type=\"checkbox\" name=\"_remember_me\"> Remember me\n            </label>\n        </div>\n    #}\n\n    <button class=\"btn btn-lg btn-primary\" type=\"submit\">\n        Sign in\n    </button>\n</form>\n{% endblock %}\n"
  },
  {
    "path": "tests/.gitignore",
    "content": ""
  },
  {
    "path": "tests/Controller/ConferenceControllerTest.php",
    "content": "<?php\n\nnamespace App\\Tests\\Controller;\n\nuse App\\Repository\\CommentRepository;\nuse Doctrine\\ORM\\EntityManagerInterface;\nuse Symfony\\Bundle\\FrameworkBundle\\Test\\WebTestCase;\n\nclass ConferenceControllerTest extends WebTestCase\n{\n    public function testIndex()\n    {\n        $client = static::createClient();\n        $client->request('GET', '/en/');\n\n        $this->assertResponseIsSuccessful();\n        $this->assertSelectorTextContains('h2', 'Give your feedback');\n    }\n\n    public function testCommentSubmission()\n    {\n        $client = static::createClient();\n        $client->request('GET', '/en/conference/amsterdam-2019');\n        $client->submitForm('Submit', [\n            'comment_form[author]' => 'Fabien',\n            'comment_form[text]' => 'Some feedback from an automated functional test',\n            'comment_form[email]' => $email = 'me@automat.ed',\n            'comment_form[photo]' => dirname(__DIR__, 2).'/public/images/under-construction.gif',\n        ]);\n        $this->assertResponseRedirects();\n\n        // simulate comment validation\n        $comment = self::$container->get(CommentRepository::class)->findOneByEmail($email);\n        $comment->setState('published');\n        self::$container->get(EntityManagerInterface::class)->flush();\n\n        $client->followRedirect();\n        $this->assertSelectorExists('div:contains(\"There are 2 comments\")');\n    }\n\n    public function testConferencePage()\n    {\n        $client = static::createClient();\n        $crawler = $client->request('GET', '/en/');\n\n        $this->assertCount(2, $crawler->filter('h4'));\n\n        $client->clickLink('View');\n\n        $this->assertPageTitleContains('Amsterdam');\n        $this->assertResponseIsSuccessful();\n        $this->assertSelectorTextContains('h2', 'Amsterdam 2019');\n        $this->assertSelectorExists('div:contains(\"There is one comment\")');\n    }\n}\n"
  },
  {
    "path": "tests/SpamCheckerTest.php",
    "content": "<?php\n\nnamespace App\\Tests;\n\nuse App\\Entity\\Comment;\nuse App\\SpamChecker;\nuse PHPUnit\\Framework\\TestCase;\nuse Symfony\\Component\\HttpClient\\MockHttpClient;\nuse Symfony\\Component\\HttpClient\\Response\\MockResponse;\nuse Symfony\\Contracts\\HttpClient\\ResponseInterface;\n\nclass SpamCheckerTest extends TestCase\n{\n    public function testSpamScoreWithInvalidRequest()\n    {\n        $comment = new Comment();\n        $comment->setCreatedAtValue();\n        $context = [];\n\n        $client = new MockHttpClient([new MockResponse('invalid', ['response_headers' => ['x-akismet-debug-help: Invalid key']])]);\n        $checker = new SpamChecker($client, 'abcde');\n\n        $this->expectException(\\RuntimeException::class);\n        $this->expectExceptionMessage('Unable to check for spam: invalid (Invalid key).');\n        $checker->getSpamScore($comment, $context);\n    }\n\n    /**\n     * @dataProvider getComments\n     */\n    public function testSpamScore(int $expectedScore, ResponseInterface $response, Comment $comment, array $context)\n    {\n        $client = new MockHttpClient([$response]);\n        $checker = new SpamChecker($client, 'abcde');\n\n        $score = $checker->getSpamScore($comment, $context);\n        $this->assertSame($expectedScore, $score);\n    }\n\n    public function getComments(): iterable\n    {\n        $comment = new Comment();\n        $comment->setCreatedAtValue();\n        $context = [];\n\n        $response = new MockResponse('', ['response_headers' => ['x-akismet-pro-tip: discard']]);\n        yield 'blatant_spam' => [2, $response, $comment, $context];\n\n        $response = new MockResponse('true');\n        yield 'spam' => [1, $response, $comment, $context];\n\n        $response = new MockResponse('false');\n        yield 'ham' => [0, $response, $comment, $context];\n    }\n}\n"
  },
  {
    "path": "translations/.gitignore",
    "content": ""
  },
  {
    "path": "translations/messages+intl-icu.en.xlf",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<xliff xmlns=\"urn:oasis:names:tc:xliff:document:1.2\" version=\"1.2\">\n  <file source-language=\"en\" target-language=\"en\" datatype=\"plaintext\" original=\"file.ext\">\n    <header>\n      <tool tool-id=\"symfony\" tool-name=\"Symfony\"/>\n    </header>\n    <body>\n      <trans-unit id=\"maMQz7W\" resname=\"nb_of_comments\">\n        <source>nb_of_comments</source>\n        <target>{count, plural, =0 {There are no comments.} one {There is one comment.} other {There are # comments.}}</target>\n      </trans-unit>\n    </body>\n  </file>\n</xliff>\n"
  },
  {
    "path": "translations/messages+intl-icu.fr.xlf",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<xliff xmlns=\"urn:oasis:names:tc:xliff:document:1.2\" version=\"1.2\">\n  <file source-language=\"en\" target-language=\"fr\" datatype=\"plaintext\" original=\"file.ext\">\n    <header>\n      <tool tool-id=\"symfony\" tool-name=\"Symfony\"/>\n    </header>\n    <body>\n      <trans-unit id=\"LNAVleg\" resname=\"Give your feedback!\">\n        <source>Give your feedback!</source>\n        <target>Donnez votre avis !</target>\n      </trans-unit>\n      <trans-unit id=\"3Mg5pAF\" resname=\"View\">\n        <source>View</source>\n        <target>Sélectionner</target>\n      </trans-unit>\n      <trans-unit id=\"eOy4.6V\" resname=\"Conference Guestbook\">\n        <source>Conference Guestbook</source>\n        <target>Livre d'Or pour Conferences</target>\n      </trans-unit>\n      <trans-unit id=\"Dg2dPd6\" resname=\"nb_of_comments\">\n        <source>nb_of_comments</source>\n        <target>{count, plural, =0 {Aucun commentaire.} =1 {1 commentaire.} other {# commentaires.}}</target>\n      </trans-unit>\n    </body>\n  </file>\n</xliff>\n"
  },
  {
    "path": "webpack.config.js",
    "content": "var Encore = require('@symfony/webpack-encore');\n\n// Manually configure the runtime environment if not already configured yet by the \"encore\" command.\n// It's useful when you use tools that rely on webpack.config.js file.\nif (!Encore.isRuntimeEnvironmentConfigured()) {\n    Encore.configureRuntimeEnvironment(process.env.NODE_ENV || 'dev');\n}\n\nEncore\n    // directory where compiled assets will be stored\n    .setOutputPath('public/build/')\n    // public path used by the web server to access the output path\n    .setPublicPath('/build')\n    // only needed for CDN's or sub-directory deploy\n    //.setManifestKeyPrefix('build/')\n\n    /*\n     * ENTRY CONFIG\n     *\n     * Add 1 entry for each \"page\" of your app\n     * (including one that's included on every page - e.g. \"app\")\n     *\n     * Each entry will result in one JavaScript file (e.g. app.js)\n     * and one CSS file (e.g. app.css) if your JavaScript imports CSS.\n     */\n    .addEntry('app', './assets/js/app.js')\n    //.addEntry('page1', './assets/js/page1.js')\n    //.addEntry('page2', './assets/js/page2.js')\n\n    // When enabled, Webpack \"splits\" your files into smaller pieces for greater optimization.\n    .splitEntryChunks()\n\n    // will require an extra script tag for runtime.js\n    // but, you probably want this, unless you're building a single-page app\n    .enableSingleRuntimeChunk()\n\n    /*\n     * FEATURE CONFIG\n     *\n     * Enable & configure other features below. For a full\n     * list of features, see:\n     * https://symfony.com/doc/current/frontend.html#adding-more-features\n     */\n    .cleanupOutputBeforeBuild()\n    .enableBuildNotifications()\n    .enableSourceMaps(!Encore.isProduction())\n    // enables hashed filenames (e.g. app.abc123.css)\n    .enableVersioning(Encore.isProduction())\n\n    // enables @babel/preset-env polyfills\n    .configureBabelPresetEnv((config) => {\n        config.useBuiltIns = 'usage';\n        config.corejs = 3;\n    })\n\n    // enables Sass/SCSS support\n    .enableSassLoader()\n\n    // uncomment if you use TypeScript\n    //.enableTypeScriptLoader()\n\n    // uncomment to get integrity=\"...\" attributes on your script & link tags\n    // requires WebpackEncoreBundle 1.4 or higher\n    //.enableIntegrityHashes(Encore.isProduction())\n\n    // uncomment if you're having problems with a jQuery plugin\n    //.autoProvidejQuery()\n\n    // uncomment if you use API Platform Admin (composer req api-admin)\n    //.enableReactPreset()\n    //.addEntry('admin', './assets/js/admin.js')\n;\n\nmodule.exports = Encore.getWebpackConfig();\n"
  }
]