├── .gitignore ├── README.md ├── app ├── .htaccess ├── AppCache.php ├── AppKernel.php ├── autoload.php └── config │ ├── config.yml │ ├── config_acceptance.yml │ ├── config_dev.yml │ ├── config_prod.yml │ ├── config_test.yml │ ├── parameters.yml.dist │ ├── routing.yml │ ├── routing_dev.yml │ ├── routing_rest.yml │ ├── security.yml │ └── services.yml ├── behat.yml ├── bin ├── console └── symfony_requirements ├── composer.json ├── composer.lock ├── features └── bootstrap │ └── FeatureContext.php ├── phpunit.xml.dist ├── src ├── .htaccess └── AppBundle │ ├── AppBundle.php │ ├── Controller │ ├── FosUserRestController.php │ ├── RestLoginController.php │ ├── RestPasswordManagementController.php │ ├── RestProfileController.php │ └── RestRegistrationController.php │ ├── Entity │ └── User.php │ ├── Event │ └── Listener │ │ └── JWTCreatedListener.php │ ├── Features │ ├── Context │ │ ├── RestApiContext.php │ │ └── UserSetupContext.php │ ├── login.feature │ ├── password_change.feature │ ├── password_reset.feature │ ├── profile.feature │ └── register.feature │ └── Mailer │ └── RestMailer.php ├── tests └── AppBundle │ └── Controller │ └── DefaultControllerTest.php ├── var ├── SymfonyRequirements.php ├── cache │ └── .gitkeep ├── logs │ └── .gitkeep └── sessions │ └── .gitkeep └── web ├── .htaccess ├── app.php ├── app_acceptance.php ├── app_dev.php ├── apple-touch-icon.png ├── config.php ├── favicon.ico └── robots.txt /.gitignore: -------------------------------------------------------------------------------- 1 | /app/config/parameters.yml 2 | /build/ 3 | /phpunit.xml 4 | /var/* 5 | !/var/cache 6 | /var/cache/* 7 | !var/cache/.gitkeep 8 | !/var/logs 9 | /var/logs/* 10 | !var/logs/.gitkeep 11 | !/var/sessions 12 | /var/sessions/* 13 | !var/sessions/.gitkeep 14 | !var/SymfonyRequirements.php 15 | /vendor/ 16 | /web/bundles/ 17 | .idea 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FOS User Bundle with FOS REST Bundle integration 2 | 3 | Full course available at : https://codereviewvideos.com/course/fosuser-fosrest-lexikjwt-user-login-registration-api 4 | 5 | In this course we are going to create ourselves a RESTful user Registration and Login system using a combination of: 6 | 7 | * FOSUserBundle 8 | * FOSRESTBundle 9 | * LexikJWTBundle 10 | 11 | We will combine all three together to expose the numerous features provided by FOSUserBundle via a RESTful API. This will include user registration, user login, managing and maintaining user profiles, and updating and resetting user passwords. 12 | 13 | As we are going to make use of FOSUserBundle, we will keep all the features that make this bundle so attractive - from user management via Symfony console commands, through to well thought out email notifications, and a full range of translations. 14 | 15 | Certain parts will need tweaking, and for this we will need to override some of the functionality provided out-of-the-box by FOSUserBundle, to make it work in the way we expect. 16 | 17 | Speaking of this, we will need to ensure our RESTful user login and registration system is well tested. For this we will make use of Behat, writing a full suite of tests to cover the various flows: 18 | 19 | * Login 20 | * Password Management 21 | * Profile Updates 22 | * Registration 23 | 24 | We will also need to override part of the mail flow, so for this we will add in our own mailer service. 25 | 26 | Along the way we will enhance the output given to us by default from LexikJWTBundle. We will add in additional data to the JWT it creates for us, allowing us to customise the created token as our system requires. 27 | 28 | Whilst this system isn't the most visually appealing - it is a RESTful API after all - the functionality it provides will enable us to do some really interesting things. 29 | 30 | Immediately following this course will be a subsequent course on how to implement a React & Redux powered login / registration front-end, making full use of the code found in this course. 31 | 32 | Speaking of which, the code is immediately available. You can pull this code today and start playing with it. You can customise it, tweak it, change it to better meet your needs, or simply use it as a reference. Pull requests are always welcome, should you feel the code can be improved. 33 | 34 | Lastly, before we start, I want to make you fully aware of one thing: 35 | 36 | This is a suggested / example implementation. I am not for one moment suggesting this is the way to be approaching this problem. This code works for me and my needs. It may not work for you and yours. Please use your own careful judgement. 37 | 38 | With that said, let's start by seeing the 'finished' application in action, and then learn how it is all put together. 39 | 40 | P.s. - I say 'finished' as this is work in progress. There will very likely be extensions / bonus modules to this course which enhance the functionality. An example of this may be to add in billing with third party services, such as Stripe. 41 | -------------------------------------------------------------------------------- /app/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | Require all denied 3 | 4 | 5 | Order deny,allow 6 | Deny from all 7 | 8 | -------------------------------------------------------------------------------- /app/AppCache.php: -------------------------------------------------------------------------------- 1 | getEnvironment(), ['dev', 'test', 'acceptance'], true)) { 31 | $bundles[] = new Symfony\Bundle\DebugBundle\DebugBundle(); 32 | $bundles[] = new Symfony\Bundle\WebProfilerBundle\WebProfilerBundle(); 33 | $bundles[] = new Sensio\Bundle\DistributionBundle\SensioDistributionBundle(); 34 | $bundles[] = new Sensio\Bundle\GeneratorBundle\SensioGeneratorBundle(); 35 | 36 | $bundles[] = new Csa\Bundle\GuzzleBundle\CsaGuzzleBundle(); 37 | } 38 | 39 | return $bundles; 40 | } 41 | 42 | public function getRootDir() 43 | { 44 | return __DIR__; 45 | } 46 | 47 | public function getCacheDir() 48 | { 49 | return dirname(__DIR__).'/var/cache/'.$this->getEnvironment(); 50 | } 51 | 52 | public function getLogDir() 53 | { 54 | return dirname(__DIR__).'/var/logs'; 55 | } 56 | 57 | public function registerContainerConfiguration(LoaderInterface $loader) 58 | { 59 | $loader->load($this->getRootDir().'/config/config_'.$this->getEnvironment().'.yml'); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/autoload.php: -------------------------------------------------------------------------------- 1 | getParameterOption(['--env', '-e'], getenv('SYMFONY_ENV') ?: 'dev'); 21 | $debug = getenv('SYMFONY_DEBUG') !== '0' && !$input->hasParameterOption(['--no-debug', '']) && $env !== 'prod'; 22 | 23 | if ($debug) { 24 | Debug::enable(); 25 | } 26 | 27 | $kernel = new AppKernel($env, $debug); 28 | $application = new Application($kernel); 29 | $application->run($input); 30 | -------------------------------------------------------------------------------- /bin/symfony_requirements: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | getPhpIniConfigPath(); 9 | 10 | echo_title('Symfony Requirements Checker'); 11 | 12 | echo '> PHP is using the following php.ini file:'.PHP_EOL; 13 | if ($iniPath) { 14 | echo_style('green', ' '.$iniPath); 15 | } else { 16 | echo_style('yellow', ' WARNING: No configuration file (php.ini) used by PHP!'); 17 | } 18 | 19 | echo PHP_EOL.PHP_EOL; 20 | 21 | echo '> Checking Symfony requirements:'.PHP_EOL.' '; 22 | 23 | $messages = array(); 24 | foreach ($symfonyRequirements->getRequirements() as $req) { 25 | /** @var $req Requirement */ 26 | if ($helpText = get_error_message($req, $lineSize)) { 27 | echo_style('red', 'E'); 28 | $messages['error'][] = $helpText; 29 | } else { 30 | echo_style('green', '.'); 31 | } 32 | } 33 | 34 | $checkPassed = empty($messages['error']); 35 | 36 | foreach ($symfonyRequirements->getRecommendations() as $req) { 37 | if ($helpText = get_error_message($req, $lineSize)) { 38 | echo_style('yellow', 'W'); 39 | $messages['warning'][] = $helpText; 40 | } else { 41 | echo_style('green', '.'); 42 | } 43 | } 44 | 45 | if ($checkPassed) { 46 | echo_block('success', 'OK', 'Your system is ready to run Symfony projects'); 47 | } else { 48 | echo_block('error', 'ERROR', 'Your system is not ready to run Symfony projects'); 49 | 50 | echo_title('Fix the following mandatory requirements', 'red'); 51 | 52 | foreach ($messages['error'] as $helpText) { 53 | echo ' * '.$helpText.PHP_EOL; 54 | } 55 | } 56 | 57 | if (!empty($messages['warning'])) { 58 | echo_title('Optional recommendations to improve your setup', 'yellow'); 59 | 60 | foreach ($messages['warning'] as $helpText) { 61 | echo ' * '.$helpText.PHP_EOL; 62 | } 63 | } 64 | 65 | echo PHP_EOL; 66 | echo_style('title', 'Note'); 67 | echo ' The command console could use a different php.ini file'.PHP_EOL; 68 | echo_style('title', '~~~~'); 69 | echo ' than the one used with your web server. To be on the'.PHP_EOL; 70 | echo ' safe side, please check the requirements from your web'.PHP_EOL; 71 | echo ' server using the '; 72 | echo_style('yellow', 'web/config.php'); 73 | echo ' script.'.PHP_EOL; 74 | echo PHP_EOL; 75 | 76 | exit($checkPassed ? 0 : 1); 77 | 78 | function get_error_message(Requirement $requirement, $lineSize) 79 | { 80 | if ($requirement->isFulfilled()) { 81 | return; 82 | } 83 | 84 | $errorMessage = wordwrap($requirement->getTestMessage(), $lineSize - 3, PHP_EOL.' ').PHP_EOL; 85 | $errorMessage .= ' > '.wordwrap($requirement->getHelpText(), $lineSize - 5, PHP_EOL.' > ').PHP_EOL; 86 | 87 | return $errorMessage; 88 | } 89 | 90 | function echo_title($title, $style = null) 91 | { 92 | $style = $style ?: 'title'; 93 | 94 | echo PHP_EOL; 95 | echo_style($style, $title.PHP_EOL); 96 | echo_style($style, str_repeat('~', strlen($title)).PHP_EOL); 97 | echo PHP_EOL; 98 | } 99 | 100 | function echo_style($style, $message) 101 | { 102 | // ANSI color codes 103 | $styles = array( 104 | 'reset' => "\033[0m", 105 | 'red' => "\033[31m", 106 | 'green' => "\033[32m", 107 | 'yellow' => "\033[33m", 108 | 'error' => "\033[37;41m", 109 | 'success' => "\033[37;42m", 110 | 'title' => "\033[34m", 111 | ); 112 | $supports = has_color_support(); 113 | 114 | echo($supports ? $styles[$style] : '').$message.($supports ? $styles['reset'] : ''); 115 | } 116 | 117 | function echo_block($style, $title, $message) 118 | { 119 | $message = ' '.trim($message).' '; 120 | $width = strlen($message); 121 | 122 | echo PHP_EOL.PHP_EOL; 123 | 124 | echo_style($style, str_repeat(' ', $width).PHP_EOL); 125 | echo_style($style, str_pad(' ['.$title.']', $width, ' ', STR_PAD_RIGHT).PHP_EOL); 126 | echo_style($style, str_pad($message, $width, ' ', STR_PAD_RIGHT).PHP_EOL); 127 | echo_style($style, str_repeat(' ', $width).PHP_EOL); 128 | } 129 | 130 | function has_color_support() 131 | { 132 | static $support; 133 | 134 | if (null === $support) { 135 | if (DIRECTORY_SEPARATOR == '\\') { 136 | $support = false !== getenv('ANSICON') || 'ON' === getenv('ConEmuANSI'); 137 | } else { 138 | $support = function_exists('posix_isatty') && @posix_isatty(STDOUT); 139 | } 140 | } 141 | 142 | return $support; 143 | } 144 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chris/api.rest.user.api.dev", 3 | "description": "an example of integration between FOS REST Bundle and FOS User Bundle", 4 | "license": "proprietary", 5 | "type": "project", 6 | "autoload": { 7 | "psr-4": { 8 | "": "src/" 9 | }, 10 | "classmap": [ 11 | "app/AppKernel.php", 12 | "app/AppCache.php" 13 | ] 14 | }, 15 | "autoload-dev": { 16 | "psr-4": { 17 | "Tests\\": "tests/" 18 | } 19 | }, 20 | "require": { 21 | "php": ">=5.5.9", 22 | "symfony/symfony": "3.1.*", 23 | "doctrine/orm": "^2.5", 24 | "doctrine/doctrine-bundle": "^1.6", 25 | "doctrine/doctrine-cache-bundle": "^1.2", 26 | "symfony/swiftmailer-bundle": "^2.3", 27 | "symfony/monolog-bundle": "^2.8", 28 | "symfony/polyfill-apcu": "^1.0", 29 | "sensio/distribution-bundle": "^5.0", 30 | "sensio/framework-extra-bundle": "^3.0.2", 31 | "incenteev/composer-parameter-handler": "^2.0", 32 | 33 | "nelmio/cors-bundle": "^1.4", 34 | "nelmio/api-doc-bundle": "^2.13", 35 | "friendsofsymfony/rest-bundle": "^2.0@dev", 36 | "jms/serializer-bundle": "^1.1", 37 | "willdurand/faker-bundle": "^1.3", 38 | "knplabs/knp-paginator-bundle": "^2.5", 39 | "friendsofsymfony/user-bundle": "~2.0@dev", 40 | "lexik/jwt-authentication-bundle": "^1.7" 41 | }, 42 | "require-dev": { 43 | "sensio/generator-bundle": "^3.0", 44 | "symfony/phpunit-bridge": "^3.0", 45 | "phpspec/phpspec": "^3.0", 46 | "behat/behat": "^3.1", 47 | "behat/symfony2-extension": "^2.1", 48 | "phpunit/phpunit": "^5.5", 49 | "guzzlehttp/guzzle": "^6.2", 50 | "csa/guzzle-bundle": "^2.1" 51 | }, 52 | "scripts": { 53 | "symfony-scripts": [ 54 | "Incenteev\\ParameterHandler\\ScriptHandler::buildParameters", 55 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::buildBootstrap", 56 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::clearCache", 57 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installAssets", 58 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installRequirementsFile", 59 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::prepareDeploymentTarget" 60 | ], 61 | "post-install-cmd": [ 62 | "@symfony-scripts" 63 | ], 64 | "post-update-cmd": [ 65 | "@symfony-scripts" 66 | ] 67 | }, 68 | "extra": { 69 | "symfony-app-dir": "app", 70 | "symfony-bin-dir": "bin", 71 | "symfony-var-dir": "var", 72 | "symfony-web-dir": "web", 73 | "symfony-tests-dir": "tests", 74 | "symfony-assets-install": "relative", 75 | "incenteev-parameters": { 76 | "file": "app/config/parameters.yml" 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /features/bootstrap/FeatureContext.php: -------------------------------------------------------------------------------- 1 | doctrine = $doctrine; 25 | $this->manager = $doctrine->getManager(); 26 | $this->schemaTool = new \Doctrine\ORM\Tools\SchemaTool($this->manager); 27 | $this->classes = $this->manager->getMetadataFactory()->getAllMetadata(); 28 | } 29 | 30 | /** 31 | * @BeforeScenario 32 | */ 33 | public function createSchema() 34 | { 35 | $this->schemaTool->dropSchema($this->classes); 36 | $this->schemaTool->createSchema($this->classes); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | tests 18 | 19 | 20 | 21 | 22 | 23 | src 24 | 25 | src/*Bundle/Resources 26 | src/*/*Bundle/Resources 27 | src/*/Bundle/*Bundle/Resources 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | Require all denied 3 | 4 | 5 | Order deny,allow 6 | Deny from all 7 | 8 | -------------------------------------------------------------------------------- /src/AppBundle/AppBundle.php: -------------------------------------------------------------------------------- 1 | request->get('username'); 33 | 34 | /** @var $user UserInterface */ 35 | $user = $this->get('fos_user.user_manager')->findUserByUsernameOrEmail($username); 36 | 37 | /** @var $dispatcher EventDispatcherInterface */ 38 | $dispatcher = $this->get('event_dispatcher'); 39 | 40 | /* Dispatch init event */ 41 | $event = new GetResponseNullableUserEvent($user, $request); 42 | $dispatcher->dispatch(FOSUserEvents::RESETTING_SEND_EMAIL_INITIALIZE, $event); 43 | 44 | if (null !== $event->getResponse()) { 45 | return $event->getResponse(); 46 | } 47 | 48 | if (null === $user) { 49 | return new JsonResponse( 50 | 'User not recognised', 51 | JsonResponse::HTTP_FORBIDDEN 52 | ); 53 | } 54 | 55 | $event = new GetResponseUserEvent($user, $request); 56 | $dispatcher->dispatch(FOSUserEvents::RESETTING_RESET_REQUEST, $event); 57 | 58 | if (null !== $event->getResponse()) { 59 | return $event->getResponse(); 60 | } 61 | 62 | if ($user->isPasswordRequestNonExpired($this->container->getParameter('fos_user.resetting.token_ttl'))) { 63 | return new JsonResponse( 64 | $this->get('translator')->trans('resetting.password_already_requested', [], 'FOSUserBundle'), 65 | JsonResponse::HTTP_FORBIDDEN 66 | ); 67 | } 68 | 69 | if (null === $user->getConfirmationToken()) { 70 | /** @var $tokenGenerator \FOS\UserBundle\Util\TokenGeneratorInterface */ 71 | $tokenGenerator = $this->get('fos_user.util.token_generator'); 72 | $user->setConfirmationToken($tokenGenerator->generateToken()); 73 | } 74 | 75 | /* Dispatch confirm event */ 76 | $event = new GetResponseUserEvent($user, $request); 77 | $dispatcher->dispatch(FOSUserEvents::RESETTING_SEND_EMAIL_CONFIRM, $event); 78 | 79 | if (null !== $event->getResponse()) { 80 | return $event->getResponse(); 81 | } 82 | 83 | $this->get('fos_user.mailer')->sendResettingEmailMessage($user); 84 | $user->setPasswordRequestedAt(new \DateTime()); 85 | $this->get('fos_user.user_manager')->updateUser($user); 86 | 87 | 88 | /* Dispatch completed event */ 89 | $event = new GetResponseUserEvent($user, $request); 90 | $dispatcher->dispatch(FOSUserEvents::RESETTING_SEND_EMAIL_COMPLETED, $event); 91 | 92 | if (null !== $event->getResponse()) { 93 | return $event->getResponse(); 94 | } 95 | 96 | return new JsonResponse( 97 | $this->get('translator')->trans( 98 | 'resetting.check_email', 99 | [ '%tokenLifetime%' => floor($this->container->getParameter('fos_user.resetting.token_ttl') / 3600) ], 100 | 'FOSUserBundle' 101 | ), 102 | JsonResponse::HTTP_OK 103 | ); 104 | } 105 | 106 | /** 107 | * Reset user password 108 | * @Annotations\Post("/reset/confirm") 109 | */ 110 | public function confirmResetAction(Request $request) 111 | { 112 | $token = $request->request->get('token', null); 113 | 114 | if (null === $token) { 115 | return new JsonResponse('You must submit a token.', JsonResponse::HTTP_BAD_REQUEST); 116 | } 117 | 118 | /** @var $formFactory \FOS\UserBundle\Form\Factory\FactoryInterface */ 119 | $formFactory = $this->get('fos_user.resetting.form.factory'); 120 | /** @var $userManager \FOS\UserBundle\Model\UserManagerInterface */ 121 | $userManager = $this->get('fos_user.user_manager'); 122 | /** @var $dispatcher \Symfony\Component\EventDispatcher\EventDispatcherInterface */ 123 | $dispatcher = $this->get('event_dispatcher'); 124 | 125 | $user = $userManager->findUserByConfirmationToken($token); 126 | 127 | if (null === $user) { 128 | return new JsonResponse( 129 | // no translation provided for this in \FOS\UserBundle\Controller\ResettingController 130 | sprintf('The user with "confirmation token" does not exist for value "%s"', $token), 131 | JsonResponse::HTTP_BAD_REQUEST 132 | ); 133 | } 134 | 135 | $event = new GetResponseUserEvent($user, $request); 136 | $dispatcher->dispatch(FOSUserEvents::RESETTING_RESET_INITIALIZE, $event); 137 | 138 | if (null !== $event->getResponse()) { 139 | return $event->getResponse(); 140 | } 141 | 142 | $form = $formFactory->createForm([ 143 | 'csrf_protection' => false, 144 | 'allow_extra_fields' => true, 145 | ]); 146 | $form->setData($user); 147 | $form->submit($request->request->all()); 148 | 149 | if (!$form->isValid()) { 150 | return $form; 151 | } 152 | 153 | $event = new FormEvent($form, $request); 154 | $dispatcher->dispatch(FOSUserEvents::RESETTING_RESET_SUCCESS, $event); 155 | 156 | $userManager->updateUser($user); 157 | 158 | if (null === $response = $event->getResponse()) { 159 | return new JsonResponse( 160 | $this->get('translator')->trans('resetting.flash.success', [], 'FOSUserBundle'), 161 | JsonResponse::HTTP_OK 162 | ); 163 | } 164 | 165 | // unsure if this is now needed / will work the same 166 | $dispatcher->dispatch(FOSUserEvents::RESETTING_RESET_COMPLETED, new FilterUserResponseEvent($user, $request, $response)); 167 | 168 | return new JsonResponse( 169 | $this->get('translator')->trans('resetting.flash.success', [], 'FOSUserBundle'), 170 | JsonResponse::HTTP_OK 171 | ); 172 | } 173 | 174 | 175 | /** 176 | * Change user password 177 | * 178 | * @ParamConverter("user", class="AppBundle:User") 179 | * 180 | * @Annotations\Post("/{user}/change") 181 | */ 182 | public function changeAction(Request $request, UserInterface $user) 183 | { 184 | if ($user !== $this->getUser()) { 185 | throw new AccessDeniedHttpException(); 186 | } 187 | 188 | /** @var $dispatcher \Symfony\Component\EventDispatcher\EventDispatcherInterface */ 189 | $dispatcher = $this->get('event_dispatcher'); 190 | 191 | $event = new GetResponseUserEvent($user, $request); 192 | $dispatcher->dispatch(FOSUserEvents::CHANGE_PASSWORD_INITIALIZE, $event); 193 | 194 | if (null !== $event->getResponse()) { 195 | return $event->getResponse(); 196 | } 197 | 198 | /** @var $formFactory \FOS\UserBundle\Form\Factory\FactoryInterface */ 199 | $formFactory = $this->get('fos_user.change_password.form.factory'); 200 | 201 | $form = $formFactory->createForm([ 202 | 'csrf_protection' => false 203 | ]); 204 | $form->setData($user); 205 | $form->submit($request->request->all()); 206 | 207 | if ( ! $form->isValid()) { 208 | return $form; 209 | } 210 | 211 | /** @var $userManager \FOS\UserBundle\Model\UserManagerInterface */ 212 | $userManager = $this->get('fos_user.user_manager'); 213 | 214 | $event = new FormEvent($form, $request); 215 | $dispatcher->dispatch(FOSUserEvents::CHANGE_PASSWORD_SUCCESS, $event); 216 | 217 | $userManager->updateUser($user); 218 | 219 | if (null === $response = $event->getResponse()) { 220 | return new JsonResponse( 221 | $this->get('translator')->trans('change_password.flash.success', [], 'FOSUserBundle'), 222 | JsonResponse::HTTP_OK 223 | ); 224 | } 225 | 226 | $dispatcher->dispatch(FOSUserEvents::CHANGE_PASSWORD_COMPLETED, new FilterUserResponseEvent($user, $request, $response)); 227 | 228 | return new JsonResponse( 229 | $this->get('translator')->trans('change_password.flash.success', [], 'FOSUserBundle'), 230 | JsonResponse::HTTP_OK 231 | ); 232 | } 233 | } -------------------------------------------------------------------------------- /src/AppBundle/Controller/RestProfileController.php: -------------------------------------------------------------------------------- 1 | getUser()) { 43 | throw new AccessDeniedHttpException(); 44 | } 45 | 46 | return $user; 47 | } 48 | 49 | /** 50 | * @param Request $request 51 | * @param UserInterface $user 52 | * 53 | * @ParamConverter("user", class="AppBundle:User") 54 | * 55 | * @return View|\Symfony\Component\Form\FormInterface 56 | */ 57 | public function putAction(Request $request, UserInterface $user) 58 | { 59 | return $this->updateProfile($request, true, $user); 60 | } 61 | 62 | /** 63 | * @param Request $request 64 | * @param UserInterface $user 65 | * 66 | * @ParamConverter("user", class="AppBundle:User") 67 | * 68 | * @return View|\Symfony\Component\Form\FormInterface 69 | */ 70 | public function patchAction(Request $request, UserInterface $user) 71 | { 72 | return $this->updateProfile($request, false, $user); 73 | } 74 | 75 | /** 76 | * @param Request $request 77 | * @param bool $clearMissing 78 | * @param UserInterface $user 79 | * 80 | * @return View|null|\Symfony\Component\Form\FormInterface|Response 81 | */ 82 | private function updateProfile(Request $request, $clearMissing = true, UserInterface $user) 83 | { 84 | $user = $this->getAction($user); 85 | 86 | /** @var $dispatcher \Symfony\Component\EventDispatcher\EventDispatcherInterface */ 87 | $dispatcher = $this->get('event_dispatcher'); 88 | 89 | $event = new GetResponseUserEvent($user, $request); 90 | $dispatcher->dispatch(FOSUserEvents::PROFILE_EDIT_INITIALIZE, $event); 91 | 92 | if (null !== $event->getResponse()) { 93 | return $event->getResponse(); 94 | } 95 | 96 | /** @var $formFactory \FOS\UserBundle\Form\Factory\FactoryInterface */ 97 | $formFactory = $this->get('fos_user.profile.form.factory'); 98 | 99 | $form = $formFactory->createForm(['csrf_protection' => false]); 100 | $form->setData($user); 101 | 102 | $form->submit($request->request->all(), $clearMissing); 103 | 104 | if (!$form->isValid()) { 105 | return $form; 106 | } 107 | 108 | /** @var $userManager \FOS\UserBundle\Model\UserManagerInterface */ 109 | $userManager = $this->get('fos_user.user_manager'); 110 | 111 | $event = new FormEvent($form, $request); 112 | $dispatcher->dispatch(FOSUserEvents::PROFILE_EDIT_SUCCESS, $event); 113 | 114 | $userManager->updateUser($user); 115 | 116 | // there was no override 117 | if (null === $response = $event->getResponse()) { 118 | return $this->routeRedirectView('get_profile', ['user' => $user->getId()], Response::HTTP_NO_CONTENT); 119 | } 120 | 121 | // unsure if this is now needed / will work the same 122 | $dispatcher->dispatch(FOSUserEvents::PROFILE_EDIT_COMPLETED, new FilterUserResponseEvent($user, $request, $response)); 123 | 124 | return $this->routeRedirectView('get_profile', ['user' => $user->getId()], Response::HTTP_NO_CONTENT); 125 | } 126 | } -------------------------------------------------------------------------------- /src/AppBundle/Controller/RestRegistrationController.php: -------------------------------------------------------------------------------- 1 | get('fos_user.registration.form.factory'); 30 | /** @var $userManager \FOS\UserBundle\Model\UserManagerInterface */ 31 | $userManager = $this->get('fos_user.user_manager'); 32 | /** @var $dispatcher \Symfony\Component\EventDispatcher\EventDispatcherInterface */ 33 | $dispatcher = $this->get('event_dispatcher'); 34 | 35 | $user = $userManager->createUser(); 36 | $user->setEnabled(true); 37 | 38 | $event = new GetResponseUserEvent($user, $request); 39 | $dispatcher->dispatch(FOSUserEvents::REGISTRATION_INITIALIZE, $event); 40 | 41 | if (null !== $event->getResponse()) { 42 | return $event->getResponse(); 43 | } 44 | 45 | $form = $formFactory->createForm([ 46 | 'csrf_protection' => false 47 | ]); 48 | $form->setData($user); 49 | $form->submit($request->request->all()); 50 | 51 | if ( ! $form->isValid()) { 52 | 53 | $event = new FormEvent($form, $request); 54 | $dispatcher->dispatch(FOSUserEvents::REGISTRATION_FAILURE, $event); 55 | 56 | if (null !== $response = $event->getResponse()) { 57 | return $response; 58 | } 59 | 60 | return $form; 61 | } 62 | 63 | $event = new FormEvent($form, $request); 64 | $dispatcher->dispatch(FOSUserEvents::REGISTRATION_SUCCESS, $event); 65 | 66 | if ($event->getResponse()) { 67 | return $event->getResponse(); 68 | } 69 | 70 | $userManager->updateUser($user); 71 | 72 | $response = new JsonResponse( 73 | [ 74 | 'msg' => $this->get('translator')->trans('registration.flash.user_created', [], 'FOSUserBundle'), 75 | 'token' => $this->get('lexik_jwt_authentication.jwt_manager')->create($user), // creates JWT 76 | ], 77 | Response::HTTP_CREATED, 78 | [ 79 | 'Location' => $this->generateUrl( 80 | 'get_profile', 81 | [ 'user' => $user->getId() ], 82 | UrlGeneratorInterface::ABSOLUTE_URL 83 | ) 84 | ] 85 | ); 86 | 87 | $dispatcher->dispatch( 88 | FOSUserEvents::REGISTRATION_COMPLETED, 89 | new FilterUserResponseEvent($user, $request, $response) 90 | ); 91 | 92 | return $response; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/AppBundle/Entity/User.php: -------------------------------------------------------------------------------- 1 | tokenStorage = $tokenStorage; 21 | } 22 | 23 | /** 24 | * Adds additional data to the generated JWT 25 | * 26 | * @param JWTCreatedEvent $event 27 | * 28 | * @return void 29 | */ 30 | public function onJWTCreated(JWTCreatedEvent $event) 31 | { 32 | /** @var $user \AppBundle\Entity\User */ 33 | $user = $event->getUser(); 34 | 35 | // add new data 36 | $payload['userId'] = $user->getId(); 37 | $payload['username'] = $user->getUsername(); 38 | 39 | $event->setData($payload); 40 | } 41 | } -------------------------------------------------------------------------------- /src/AppBundle/Features/Context/RestApiContext.php: -------------------------------------------------------------------------------- 1 | client = $client; 57 | } 58 | 59 | /** 60 | * Adds Basic Authentication header to next request. 61 | * 62 | * @param string $username 63 | * @param string $password 64 | * 65 | * @Given /^I am authenticating as "([^"]*)" with "([^"]*)" password$/ 66 | */ 67 | public function iAmAuthenticatingAs($username, $password) 68 | { 69 | $this->removeHeader('Authorization'); 70 | $this->authorization = base64_encode($username . ':' . $password); 71 | $this->addHeader('Authorization', 'Basic ' . $this->authorization); 72 | } 73 | 74 | /** 75 | * Adds JWT Token to Authentication header for next request 76 | * 77 | * @param string $username 78 | * @param string $password 79 | * 80 | * @Given /^I am successfully logged in with username: "([^"]*)", and password: "([^"]*)"$/ 81 | */ 82 | public function iAmSuccessfullyLoggedInWithUsernameAndPassword($username, $password) 83 | { 84 | try { 85 | 86 | $this->iSendARequest('POST', 'login', [ 87 | 'json' => [ 88 | 'username' => $username, 89 | 'password' => $password, 90 | ] 91 | ]); 92 | 93 | $this->theResponseCodeShouldBe(200); 94 | 95 | $responseBody = json_decode($this->response->getBody(), true); 96 | $this->addHeader('Authorization', 'Bearer ' . $responseBody['token']); 97 | 98 | } catch (RequestException $e) { 99 | 100 | echo Psr7\str($e->getRequest()); 101 | 102 | if ($e->hasResponse()) { 103 | echo Psr7\str($e->getResponse()); 104 | } 105 | 106 | } 107 | } 108 | 109 | /** 110 | * @When I have forgotten to set the :header 111 | */ 112 | public function iHaveForgottenToSetThe($header) 113 | { 114 | $this->addHeader($header, null); 115 | } 116 | 117 | /** 118 | * Sets a HTTP Header. 119 | * 120 | * @param string $name header name 121 | * @param string $value header value 122 | * 123 | * @Given /^I set header "([^"]*)" with value "([^"]*)"$/ 124 | */ 125 | public function iSetHeaderWithValue($name, $value) 126 | { 127 | $this->addHeader($name, $value); 128 | } 129 | 130 | /** 131 | * Sends HTTP request to specific relative URL. 132 | * 133 | * @param string $method request method 134 | * @param string $url relative url 135 | * 136 | * @When /^(?:I )?send a "([A-Z]+)" request to "([^"]+)"$/ 137 | */ 138 | public function iSendARequest($method, $url, array $data = []) 139 | { 140 | $url = $this->prepareUrl($url); 141 | $data = $this->prepareData($data); 142 | 143 | // print_r($data); 144 | 145 | try { 146 | $this->response = $this->getClient()->request($method, $url, $data); 147 | } catch (RequestException $e) { 148 | if ($e->hasResponse()) { 149 | $this->response = $e->getResponse(); 150 | } 151 | } 152 | } 153 | 154 | /** 155 | * Sends HTTP request to specific URL with field values from Table. 156 | * 157 | * @param string $method request method 158 | * @param string $url relative url 159 | * @param TableNode $post table of post values 160 | * 161 | * @When /^(?:I )?send a ([A-Z]+) request to "([^"]+)" with values:$/ 162 | */ 163 | public function iSendARequestWithValues($method, $url, TableNode $post) 164 | { 165 | $url = $this->prepareUrl($url); 166 | $fields = array(); 167 | 168 | foreach ($post->getRowsHash() as $key => $val) { 169 | $fields[$key] = $this->replacePlaceHolder($val); 170 | } 171 | 172 | $bodyOption = array( 173 | 'body' => json_encode($fields), 174 | ); 175 | $this->request = $this->getClient()->createRequest($method, $url, $bodyOption); 176 | if (!empty($this->headers)) { 177 | $this->request->addHeaders($this->headers); 178 | } 179 | 180 | $this->sendRequest(); 181 | } 182 | 183 | /** 184 | * Sends HTTP request to specific URL with raw body from PyString. 185 | * 186 | * @param string $method request method 187 | * @param string $url relative url 188 | * @param PyStringNode $string request body 189 | * 190 | * @When /^(?:I )?send a "([A-Z]+)" request to "([^"]+)" with body:$/ 191 | */ 192 | public function iSendARequestWithBody($method, $url, PyStringNode $string) 193 | { 194 | $url = $this->prepareUrl($url); 195 | $string = $this->replacePlaceHolder(trim($string)); 196 | 197 | $this->request = $this->iSendARequest( 198 | $method, 199 | $url, 200 | [ 'body' => $string, ] 201 | ); 202 | } 203 | 204 | /** 205 | * Sends HTTP request to specific URL with form data from PyString. 206 | * 207 | * @param string $method request method 208 | * @param string $url relative url 209 | * @param PyStringNode $body request body 210 | * 211 | * @When /^(?:I )?send a "([A-Z]+)" request to "([^"]+)" with form data:$/ 212 | */ 213 | public function iSendARequestWithFormData($method, $url, PyStringNode $body) 214 | { 215 | $url = $this->prepareUrl($url); 216 | $body = $this->replacePlaceHolder(trim($body)); 217 | 218 | $fields = array(); 219 | parse_str(implode('&', explode("\n", $body)), $fields); 220 | $this->request = $this->getClient()->createRequest($method, $url); 221 | /** @var \GuzzleHttp\Post\PostBodyInterface $requestBody */ 222 | $requestBody = $this->request->getBody(); 223 | foreach ($fields as $key => $value) { 224 | $requestBody->setField($key, $value); 225 | } 226 | 227 | $this->sendRequest(); 228 | } 229 | 230 | /** 231 | * @When /^(?:I )?send a multipart "([A-Z]+)" request to "([^"]+)" with form data:$/ 232 | */ 233 | public function iSendAMultipartRequestToWithFormData($method, $url, TableNode $post) 234 | { 235 | $url = $this->prepareUrl($url); 236 | 237 | $this->request = $this->getClient()->createRequest($method, $url); 238 | 239 | $data = $post->getColumnsHash()[0]; 240 | 241 | $hasFile = false; 242 | 243 | if (array_key_exists('filePath', $data)) { 244 | $filePath = $this->dummyDataPath . $data['filePath']; 245 | unset($data['filePath']); 246 | $hasFile = true; 247 | } 248 | 249 | 250 | /** @var \GuzzleHttp\Post\PostBodyInterface $requestBody */ 251 | $requestBody = $this->request->getBody(); 252 | foreach ($data as $key => $value) { 253 | $requestBody->setField($key, $value); 254 | } 255 | 256 | 257 | if ($hasFile) { 258 | $file = fopen($filePath, 'rb'); 259 | $postFile = new PostFile('uploadedFile', $file); 260 | $requestBody->addFile($postFile); 261 | } 262 | 263 | 264 | if (!empty($this->headers)) { 265 | $this->request->addHeaders($this->headers); 266 | } 267 | $this->request->setHeader('Content-Type', 'multipart/form-data'); 268 | 269 | $this->sendRequest(); 270 | } 271 | 272 | /** 273 | * Checks that response has specific status code. 274 | * 275 | * @param string $code status code 276 | * 277 | * @Then the response code should be :arg1 278 | */ 279 | public function theResponseCodeShouldBe($code) 280 | { 281 | $expected = intval($code); 282 | $actual = intval($this->response->getStatusCode()); 283 | Assertions::assertSame($expected, $actual); 284 | } 285 | 286 | /** 287 | * Checks that response body contains specific text. 288 | * 289 | * @param string $text 290 | * 291 | * @Then /^(?:the )?response should contain "((?:[^"]|\\")*)"$/ 292 | */ 293 | public function theResponseShouldContain($text) 294 | { 295 | $expectedRegexp = '/' . preg_quote($text) . '/i'; 296 | $actual = (string) $this->response->getBody(); 297 | Assertions::assertRegExp($expectedRegexp, $actual); 298 | } 299 | 300 | /** 301 | * Checks that response body doesn't contains specific text. 302 | * 303 | * @param string $text 304 | * 305 | * @Then /^(?:the )?response should not contain "([^"]*)"$/ 306 | */ 307 | public function theResponseShouldNotContain($text) 308 | { 309 | $expectedRegexp = '/' . preg_quote($text) . '/'; 310 | $actual = (string) $this->response->getBody(); 311 | Assertions::assertNotRegExp($expectedRegexp, $actual); 312 | } 313 | 314 | /** 315 | * Checks that response body contains JSON from PyString. 316 | * 317 | * Do not check that the response body /only/ contains the JSON from PyString, 318 | * 319 | * @param PyStringNode $jsonString 320 | * 321 | * @throws \RuntimeException 322 | * 323 | * @Then /^(?:the )?response should contain json:$/ 324 | */ 325 | public function theResponseShouldContainJson(PyStringNode $jsonString) 326 | { 327 | $etalon = json_decode($this->replacePlaceHolder($jsonString->getRaw()), true); 328 | $actual = json_decode($this->response->getBody(), true); 329 | 330 | if (null === $etalon) { 331 | throw new \RuntimeException( 332 | "Can not convert etalon to json:\n" . $this->replacePlaceHolder($jsonString->getRaw()) 333 | ); 334 | } 335 | 336 | Assertions::assertGreaterThanOrEqual(count($etalon), count($actual)); 337 | foreach ($etalon as $key => $needle) { 338 | Assertions::assertArrayHasKey($key, $actual); 339 | Assertions::assertEquals($etalon[$key], $actual[$key]); 340 | } 341 | } 342 | 343 | /** 344 | * Prints last response body. 345 | * 346 | * @Then print response 347 | */ 348 | public function printResponse() 349 | { 350 | $response = $this->response; 351 | 352 | echo sprintf( 353 | "%d:\n%s", 354 | $response->getStatusCode(), 355 | $response->getBody() 356 | ); 357 | } 358 | 359 | /** 360 | * @Then the response header :header should be equal to :value 361 | */ 362 | public function theResponseHeaderShouldBeEqualTo($header, $value) 363 | { 364 | $header = $this->response->getHeaders()[$header]; 365 | Assertions::assertContains($value, $header); 366 | } 367 | 368 | /** 369 | * Prepare URL by replacing placeholders and trimming slashes. 370 | * 371 | * @param string $url 372 | * 373 | * @return string 374 | */ 375 | private function prepareUrl($url) 376 | { 377 | return ltrim($this->replacePlaceHolder($url), '/'); 378 | } 379 | 380 | /** 381 | * Sets place holder for replacement. 382 | * 383 | * you can specify placeholders, which will 384 | * be replaced in URL, request or response body. 385 | * 386 | * @param string $key token name 387 | * @param string $value replace value 388 | */ 389 | public function setPlaceHolder($key, $value) 390 | { 391 | $this->placeHolders[$key] = $value; 392 | } 393 | 394 | /** 395 | * @Then I follow the link in the Location response header 396 | */ 397 | public function iFollowTheLinkInTheLocationResponseHeader() 398 | { 399 | $location = $this->response->getHeader('Location')[0]; 400 | 401 | $this->iSendARequest(Request::METHOD_GET, $location); 402 | } 403 | 404 | /** 405 | * @Then the JSON should be valid according to this schema: 406 | */ 407 | public function theJsonShouldBeValidAccordingToThisSchema(PyStringNode $schema) 408 | { 409 | $inspector = new JsonInspector('javascript'); 410 | 411 | $json = new \Sanpi\Behatch\Json\Json(json_encode($this->response->json())); 412 | 413 | $inspector->validate( 414 | $json, 415 | new JsonSchema($schema) 416 | ); 417 | } 418 | 419 | /** 420 | * Checks, that given JSON node is equal to given value 421 | * 422 | * @Then the JSON node :node should be equal to :text 423 | */ 424 | public function theJsonNodeShouldBeEqualTo($node, $text) 425 | { 426 | $json = new \Sanpi\Behatch\Json\Json(json_encode($this->response->json())); 427 | 428 | $inspector = new JsonInspector('javascript'); 429 | 430 | $actual = $inspector->evaluate($json, $node); 431 | 432 | if ($actual != $text) { 433 | throw new \Exception( 434 | sprintf("The node value is '%s'", json_encode($actual)) 435 | ); 436 | } 437 | } 438 | 439 | /** 440 | * Replaces placeholders in provided text. 441 | * 442 | * @param string $string 443 | * 444 | * @return string 445 | */ 446 | protected function replacePlaceHolder($string) 447 | { 448 | foreach ($this->placeHolders as $key => $val) { 449 | $string = str_replace($key, $val, $string); 450 | } 451 | 452 | return $string; 453 | } 454 | 455 | /** 456 | * Returns headers, that will be used to send requests. 457 | * 458 | * @return array 459 | */ 460 | protected function getHeaders() 461 | { 462 | return $this->headers; 463 | } 464 | 465 | /** 466 | * Adds header 467 | * 468 | * @param string $name 469 | * @param string $value 470 | */ 471 | protected function addHeader($name, $value) 472 | { 473 | if ( ! isset($this->headers[$name])) { 474 | $this->headers[$name] = $value; 475 | } 476 | 477 | if (!is_array($this->headers[$name])) { 478 | $this->headers[$name] = [$this->headers[$name]]; 479 | } 480 | 481 | $this->headers[$name] = $value; 482 | } 483 | 484 | /** 485 | * Removes a header identified by $headerName 486 | * 487 | * @param string $headerName 488 | */ 489 | protected function removeHeader($headerName) 490 | { 491 | if (array_key_exists($headerName, $this->headers)) { 492 | unset($this->headers[$headerName]); 493 | } 494 | } 495 | 496 | /** 497 | * @return ClientInterface 498 | */ 499 | private function getClient() 500 | { 501 | if (null === $this->client) { 502 | throw new \RuntimeException('Client has not been set in WebApiContext'); 503 | } 504 | 505 | return $this->client; 506 | } 507 | 508 | private function prepareData($data) 509 | { 510 | if (!empty($this->headers)) { 511 | $data = array_replace( 512 | $data, 513 | ["headers" => $this->headers] 514 | ); 515 | } 516 | 517 | return $data; 518 | } 519 | } 520 | -------------------------------------------------------------------------------- /src/AppBundle/Features/Context/UserSetupContext.php: -------------------------------------------------------------------------------- 1 | userManager = $userManager; 31 | $this->em = $em; 32 | } 33 | 34 | /** 35 | * @Given there are Users with the following details: 36 | */ 37 | public function thereAreUsersWithTheFollowingDetails(TableNode $users) 38 | { 39 | foreach ($users->getColumnsHash() as $key => $val) { 40 | 41 | $confirmationToken = isset($val['confirmation_token']) && $val['confirmation_token'] != '' 42 | ? $val['confirmation_token'] 43 | : null; 44 | 45 | $user = $this->userManager->createUser(); 46 | 47 | $user->setEnabled(true); 48 | $user->setUsername($val['username']); 49 | $user->setEmail($val['email']); 50 | $user->setPlainPassword($val['password']); 51 | $user->setConfirmationToken($confirmationToken); 52 | 53 | if ( ! empty($confirmationToken)) { 54 | $user->setPasswordRequestedAt(new \DateTime('now')); 55 | } 56 | 57 | $this->userManager->updateUser($user); 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /src/AppBundle/Features/login.feature: -------------------------------------------------------------------------------- 1 | Feature: Handle user login via the RESTful API 2 | 3 | In order to allow secure access to the system 4 | As a client software developer 5 | I need to be able to let users log in and out 6 | 7 | 8 | Background: 9 | Given there are Users with the following details: 10 | | id | username | email | password | 11 | | 1 | peter | peter@test.com | testpass | 12 | | 2 | john | john@test.org | johnpass | 13 | | 3 | tim | tim@blah.net | timpass | 14 | And I set header "Content-Type" with value "application/json" 15 | 16 | @this 17 | Scenario: Cannot GET Login 18 | When I send a "GET" request to "/login" 19 | Then the response code should be 405 20 | 21 | Scenario: User cannot Login with bad credentials 22 | When I send a "POST" request to "/login" with body: 23 | """ 24 | { 25 | "username": "jimmy", 26 | "password": "badpass" 27 | } 28 | """ 29 | Then the response code should be 401 30 | 31 | Scenario: User can Login with good credentials (username) 32 | When I send a "POST" request to "/login" with body: 33 | """ 34 | { 35 | "username": "peter", 36 | "password": "testpass" 37 | } 38 | """ 39 | Then the response code should be 200 40 | And the response should contain "token" 41 | 42 | Scenario: User can Login with good credentials (email) 43 | When I send a "POST" request to "/login" with body: 44 | """ 45 | { 46 | "username": "peter@test.com", 47 | "password": "testpass" 48 | } 49 | """ 50 | Then the response code should be 200 51 | And the response should contain "token" 52 | -------------------------------------------------------------------------------- /src/AppBundle/Features/password_change.feature: -------------------------------------------------------------------------------- 1 | Feature: Handle password changing via the RESTful API 2 | 3 | In order to provide a more secure system 4 | As a client software developer 5 | I need to be able to let users change their current API password 6 | 7 | 8 | Background: 9 | Given there are Users with the following details: 10 | | id | username | email | password | confirmation_token | 11 | | 1 | peter | peter@test.com | testpass | | 12 | | 2 | john | john@test.org | johnpass | some-token-string | 13 | And I set header "Content-Type" with value "application/json" 14 | 15 | 16 | Scenario: Cannot hit the change password endpoint if not logged in (missing token) 17 | When I send a "POST" request to "/password/1/change" with body: 18 | """ 19 | { 20 | "current_password": "testpass", 21 | "plainPassword": { 22 | "first": "new password", 23 | "second": "new password" 24 | } 25 | } 26 | """ 27 | Then the response code should be 401 28 | 29 | Scenario: Cannot change the password for a different user 30 | When I am successfully logged in with username: "peter", and password: "testpass" 31 | And I send a "POST" request to "/password/2/change" with body: 32 | """ 33 | { 34 | "current_password": "testpass", 35 | "plainPassword": { 36 | "first": "new password", 37 | "second": "new password" 38 | } 39 | } 40 | """ 41 | Then the response code should be 403 42 | 43 | Scenario: Can change password with valid credentials 44 | When I am successfully logged in with username: "peter", and password: "testpass" 45 | And I send a "POST" request to "/password/1/change" with body: 46 | """ 47 | { 48 | "current_password": "testpass", 49 | "plainPassword": { 50 | "first": "new password", 51 | "second": "new password" 52 | } 53 | } 54 | """ 55 | Then the response code should be 200 56 | And the response should contain "The password has been changed" 57 | 58 | Scenario: Cannot change password with bad current password 59 | When I am successfully logged in with username: "peter", and password: "testpass" 60 | And I send a "POST" request to "/password/1/change" with body: 61 | """ 62 | { 63 | "current_password": "wrong", 64 | "plainPassword": { 65 | "first": "new password", 66 | "second": "new password" 67 | } 68 | } 69 | """ 70 | Then the response code should be 400 71 | And the response should contain "This value should be your current password." 72 | 73 | Scenario: Cannot change password with mismatched new password 74 | When I am successfully logged in with username: "peter", and password: "testpass" 75 | And I send a "POST" request to "/password/1/change" with body: 76 | """ 77 | { 78 | "current_password": "testpass", 79 | "plainPassword": { 80 | "first": "new password 11", 81 | "second": "new password 22" 82 | } 83 | } 84 | """ 85 | Then the response code should be 400 86 | And the response should contain "The entered passwords don't match" 87 | 88 | Scenario: Cannot change password with missing new password field 89 | When I am successfully logged in with username: "peter", and password: "testpass" 90 | And I send a "POST" request to "/password/1/change" with body: 91 | """ 92 | { 93 | "current_password": "testpass", 94 | "plainPassword": { 95 | "second": "missing first" 96 | } 97 | } 98 | """ 99 | Then the response code should be 400 100 | And the response should contain "The entered passwords don't match" -------------------------------------------------------------------------------- /src/AppBundle/Features/password_reset.feature: -------------------------------------------------------------------------------- 1 | Feature: Handle password changing via the RESTful API 2 | 3 | In order to help users quickly regain access to their account 4 | As a client software developer 5 | I need to be able to let users request a password reset 6 | 7 | 8 | Background: 9 | Given there are Users with the following details: 10 | | uid | username | email | password | confirmation_token | 11 | | u1 | peter | peter@test.com | testpass | | 12 | | u2 | john | john@test.org | johnpass | some-token-string | 13 | And I set header "Content-Type" with value "application/json" 14 | 15 | 16 | ############################ 17 | ## Password Reset Request ## 18 | ############################ 19 | Scenario: Cannot request a password reset for an invalid username 20 | When I send a "POST" request to "/password/reset/request" with body: 21 | """ 22 | { "username": "davey" } 23 | """ 24 | Then the response code should be 403 25 | And the response should contain "User not recognised" 26 | # silly 27 | 28 | Scenario: Can request a password reset for a valid username 29 | When I send a "POST" request to "/password/reset/request" with body: 30 | """ 31 | { "username": "peter" } 32 | """ 33 | Then the response code should be 200 34 | And the response should contain "An email has been sent. It contains a link you must click to reset your password." 35 | 36 | Scenario: Cannot request another password reset for an account already requesting, but not yet actioning the reset request 37 | When I send a "POST" request to "/password/reset/request" with body: 38 | """ 39 | { "username": "john" } 40 | """ 41 | Then the response code should be 403 42 | And the response should contain "The password for this user has already been requested within the last 24 hours." 43 | 44 | 45 | 46 | ############################ 47 | ## Password Reset Confirm ## 48 | ############################ 49 | 50 | Scenario: Cannot confirm without a token 51 | When I send a "POST" request to "/password/reset/confirm" with body: 52 | """ 53 | { "bad": "data" } 54 | """ 55 | Then the response code should be 400 56 | And the response should contain "You must submit a token" 57 | 58 | Scenario: Cannot confirm with an invalid token 59 | When I send a "POST" request to "/password/reset/confirm" with body: 60 | """ 61 | { "token": "invalid token string" } 62 | """ 63 | Then the response code should be 400 64 | 65 | Scenario: Cannot confirm without a valid new password 66 | When I send a "POST" request to "/password/reset/confirm" with body: 67 | """ 68 | { 69 | "token": "some-token-string", 70 | "plainPassword": { 71 | "second": "first-is-missing" 72 | } 73 | } 74 | """ 75 | Then the response code should be 400 76 | And the response should contain "The entered passwords don't match" 77 | 78 | Scenario: Cannot confirm with a mismatched password and confirmation 79 | When I send a "POST" request to "/password/reset/confirm" with body: 80 | """ 81 | { 82 | "token": "some-token-string", 83 | "plainPassword": { 84 | "first": "some password", 85 | "second": "oops" 86 | } 87 | } 88 | """ 89 | Then the response code should be 400 90 | And the response should contain "The entered passwords don't match" 91 | 92 | Scenario: Can confirm with valid new password 93 | When I send a "POST" request to "/password/reset/confirm" with body: 94 | """ 95 | { 96 | "token": "some-token-string", 97 | "plainPassword": { 98 | "first": "new password", 99 | "second": "new password" 100 | } 101 | } 102 | """ 103 | Then the response code should be 200 104 | And the response should contain "The password has been reset successfully" 105 | And I send a "POST" request to "/login" with body: 106 | """ 107 | { 108 | "username": "john", 109 | "password": "new password" 110 | } 111 | """ 112 | Then the response code should be 200 113 | And the response should contain "token" 114 | -------------------------------------------------------------------------------- /src/AppBundle/Features/profile.feature: -------------------------------------------------------------------------------- 1 | Feature: Manage User profile data via the RESTful API 2 | 3 | In order to allow a user to keep their profile information up to date 4 | As a client software developer 5 | I need to be able to let users read and update their profile 6 | 7 | 8 | Background: 9 | Given there are Users with the following details: 10 | | id | username | email | password | 11 | | 1 | peter | peter@test.com | testpass | 12 | | 2 | john | john@test.org | johnpass | 13 | And I am successfully logged in with username: "peter", and password: "testpass" 14 | And I set header "Content-Type" with value "application/json" 15 | 16 | 17 | Scenario: Cannot view a profile with a bad JWT 18 | When I set header "Authorization" with value "Bearer bad-token-string" 19 | And I send a "GET" request to "/profile/1" 20 | Then the response code should be 401 21 | And the response should contain "Invalid JWT Token" 22 | 23 | Scenario: Can view own profile 24 | When I send a "GET" request to "/profile/1" 25 | Then the response code should be 200 26 | And the response should contain json: 27 | """ 28 | { 29 | "id": "1", 30 | "username": "peter", 31 | "email": "peter@test.com" 32 | } 33 | """ 34 | 35 | Scenario: Cannot view another user's profile 36 | When I send a "GET" request to "/profile/2" 37 | Then the response code should be 403 38 | 39 | Scenario: Must supply current password when updating profile information 40 | When I send a "PUT" request to "/profile/1" with body: 41 | """ 42 | { 43 | "email": "new_email@test.com" 44 | } 45 | """ 46 | Then the response code should be 400 47 | 48 | Scenario: Can replace their own profile 49 | When I send a "PUT" request to "/profile/1" with body: 50 | """ 51 | { 52 | "username": "peter", 53 | "email": "new_email@test.com", 54 | "current_password": "testpass" 55 | } 56 | """ 57 | Then the response code should be 204 58 | And I send a "GET" request to "/profile/1" 59 | And the response should contain json: 60 | """ 61 | { 62 | "id": "1", 63 | "username": "peter", 64 | "email": "new_email@test.com" 65 | } 66 | """ 67 | 68 | Scenario: Cannot replace another user's profile 69 | When I send a "PUT" request to "/profile/2" with body: 70 | """ 71 | { 72 | "username": "peter", 73 | "email": "new_email@test.com", 74 | "current_password": "testpass" 75 | } 76 | """ 77 | Then the response code should be 403 78 | 79 | Scenario: Can update their own profile 80 | When I send a "PATCH" request to "/profile/1" with body: 81 | """ 82 | { 83 | "email": "different_email@test.com", 84 | "current_password": "testpass" 85 | } 86 | """ 87 | Then the response code should be 204 88 | And I send a "GET" request to "/profile/1" 89 | And the response should contain json: 90 | """ 91 | { 92 | "id": "1", 93 | "username": "peter", 94 | "email": "different_email@test.com" 95 | } 96 | """ 97 | 98 | Scenario: Cannot update another user's profile 99 | When I send a "PATCH" request to "/profile/2" with body: 100 | """ 101 | { 102 | "username": "peter", 103 | "email": "new_email@test.com", 104 | "current_password": "testpass" 105 | } 106 | """ 107 | Then the response code should be 403 108 | -------------------------------------------------------------------------------- /src/AppBundle/Features/register.feature: -------------------------------------------------------------------------------- 1 | Feature: Handle user registration via the RESTful API 2 | 3 | In order to allow a user to sign up 4 | As a client software developer 5 | I need to be able to handle registration 6 | 7 | 8 | Background: 9 | Given there are Users with the following details: 10 | | id | username | email | password | 11 | | 1 | peter | peter@test.com | testpass | 12 | And I set header "Content-Type" with value "application/json" 13 | 14 | 15 | Scenario: Can register with valid data 16 | When I send a "POST" request to "/register" with body: 17 | """ 18 | { 19 | "email": "gary@test.co.uk", 20 | "username": "garold", 21 | "plainPassword": { 22 | "first": "gaz123", 23 | "second": "gaz123" 24 | } 25 | } 26 | """ 27 | Then the response code should be 201 28 | And the response should contain "The user has been created successfully" 29 | When I am successfully logged in with username: "garold", and password: "gaz123" 30 | And I send a "GET" request to "/profile/2" 31 | And the response should contain json: 32 | """ 33 | { 34 | "id": "2", 35 | "username": "garold", 36 | "email": "gary@test.co.uk" 37 | } 38 | """ 39 | 40 | Scenario: Cannot register with existing user name 41 | When I send a "POST" request to "/register" with body: 42 | """ 43 | { 44 | "email": "gary@test.co.uk", 45 | "username": "peter", 46 | "plainPassword": { 47 | "first": "gaz123", 48 | "second": "gaz123" 49 | } 50 | } 51 | """ 52 | Then the response code should be 400 53 | And the response should contain "The username is already used" 54 | 55 | Scenario: Cannot register with an existing email address 56 | When I send a "POST" request to "/register" with body: 57 | """ 58 | { 59 | "email": "peter@test.com", 60 | "username": "garold", 61 | "plainPassword": { 62 | "first": "gaz123", 63 | "second": "gaz123" 64 | } 65 | } 66 | """ 67 | Then the response code should be 400 68 | And the response should contain "The email is already used" 69 | 70 | Scenario: Cannot register with an mismatched password 71 | When I send a "POST" request to "/register" with body: 72 | """ 73 | { 74 | "email": "gary@test.co.uk", 75 | "username": "garold", 76 | "plainPassword": { 77 | "first": "gaz123", 78 | "second": "gaz456" 79 | } 80 | } 81 | """ 82 | Then the response code should be 400 83 | And the response should contain "The entered passwords don't match" -------------------------------------------------------------------------------- /src/AppBundle/Mailer/RestMailer.php: -------------------------------------------------------------------------------- 1 | mailer = $mailer; 19 | $this->router = $router; 20 | $this->twig = $twig; 21 | $this->parameters = $parameters; 22 | } 23 | 24 | public function sendConfirmationEmailMessage(UserInterface $user) 25 | { 26 | $template = $this->parameters['template']['confirmation']; 27 | $url = $this->router->generate('fos_user_registration_confirm', array('token' => $user->getConfirmationToken()), UrlGeneratorInterface::ABSOLUTE_URL); 28 | 29 | $context = array( 30 | 'user' => $user, 31 | 'confirmationUrl' => $url 32 | ); 33 | 34 | $this->sendMessage($template, $context, $this->parameters['from_email']['confirmation'], $user->getEmail()); 35 | } 36 | 37 | public function sendResettingEmailMessage(UserInterface $user) 38 | { 39 | $template = $this->parameters['template']['resetting']; 40 | 41 | $url = $this->router->generate( 42 | 'confirm_password_reset', 43 | ['token' => $user->getConfirmationToken()], 44 | UrlGeneratorInterface::ABSOLUTE_URL 45 | ); 46 | 47 | $context = [ 48 | 'user' => $user, 49 | 'confirmationUrl' => $url 50 | ]; 51 | 52 | $this->sendMessage($template, $context, $this->parameters['from_email']['resetting'], $user->getEmail()); 53 | } 54 | 55 | /** 56 | * @param string $templateName 57 | * @param array $context 58 | * @param string $fromEmail 59 | * @param string $toEmail 60 | */ 61 | protected function sendMessage($templateName, $context, $fromEmail, $toEmail) 62 | { 63 | $context = $this->twig->mergeGlobals($context); 64 | $template = $this->twig->loadTemplate($templateName); 65 | $subject = $template->renderBlock('subject', $context); 66 | $textBody = $template->renderBlock('body_text', $context); 67 | $htmlBody = $template->renderBlock('body_html', $context); 68 | 69 | $message = \Swift_Message::newInstance() 70 | ->setSubject($subject) 71 | ->setFrom($fromEmail) 72 | ->setTo($toEmail); 73 | 74 | if (!empty($htmlBody)) { 75 | $message->setBody($htmlBody, 'text/html') 76 | ->addPart($textBody, 'text/plain'); 77 | } else { 78 | $message->setBody($textBody); 79 | } 80 | 81 | $this->mailer->send($message); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /tests/AppBundle/Controller/DefaultControllerTest.php: -------------------------------------------------------------------------------- 1 | request('GET', '/'); 14 | 15 | $this->assertEquals(200, $client->getResponse()->getStatusCode()); 16 | $this->assertContains('Welcome to Symfony', $crawler->filter('#container h1')->text()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /var/SymfonyRequirements.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | /* 13 | * Users of PHP 5.2 should be able to run the requirements checks. 14 | * This is why the file and all classes must be compatible with PHP 5.2+ 15 | * (e.g. not using namespaces and closures). 16 | * 17 | * ************** CAUTION ************** 18 | * 19 | * DO NOT EDIT THIS FILE as it will be overridden by Composer as part of 20 | * the installation/update process. The original file resides in the 21 | * SensioDistributionBundle. 22 | * 23 | * ************** CAUTION ************** 24 | */ 25 | 26 | /** 27 | * Represents a single PHP requirement, e.g. an installed extension. 28 | * It can be a mandatory requirement or an optional recommendation. 29 | * There is a special subclass, named PhpIniRequirement, to check a php.ini configuration. 30 | * 31 | * @author Tobias Schultze 32 | */ 33 | class Requirement 34 | { 35 | private $fulfilled; 36 | private $testMessage; 37 | private $helpText; 38 | private $helpHtml; 39 | private $optional; 40 | 41 | /** 42 | * Constructor that initializes the requirement. 43 | * 44 | * @param bool $fulfilled Whether the requirement is fulfilled 45 | * @param string $testMessage The message for testing the requirement 46 | * @param string $helpHtml The help text formatted in HTML for resolving the problem 47 | * @param string|null $helpText The help text (when null, it will be inferred from $helpHtml, i.e. stripped from HTML tags) 48 | * @param bool $optional Whether this is only an optional recommendation not a mandatory requirement 49 | */ 50 | public function __construct($fulfilled, $testMessage, $helpHtml, $helpText = null, $optional = false) 51 | { 52 | $this->fulfilled = (bool) $fulfilled; 53 | $this->testMessage = (string) $testMessage; 54 | $this->helpHtml = (string) $helpHtml; 55 | $this->helpText = null === $helpText ? strip_tags($this->helpHtml) : (string) $helpText; 56 | $this->optional = (bool) $optional; 57 | } 58 | 59 | /** 60 | * Returns whether the requirement is fulfilled. 61 | * 62 | * @return bool true if fulfilled, otherwise false 63 | */ 64 | public function isFulfilled() 65 | { 66 | return $this->fulfilled; 67 | } 68 | 69 | /** 70 | * Returns the message for testing the requirement. 71 | * 72 | * @return string The test message 73 | */ 74 | public function getTestMessage() 75 | { 76 | return $this->testMessage; 77 | } 78 | 79 | /** 80 | * Returns the help text for resolving the problem. 81 | * 82 | * @return string The help text 83 | */ 84 | public function getHelpText() 85 | { 86 | return $this->helpText; 87 | } 88 | 89 | /** 90 | * Returns the help text formatted in HTML. 91 | * 92 | * @return string The HTML help 93 | */ 94 | public function getHelpHtml() 95 | { 96 | return $this->helpHtml; 97 | } 98 | 99 | /** 100 | * Returns whether this is only an optional recommendation and not a mandatory requirement. 101 | * 102 | * @return bool true if optional, false if mandatory 103 | */ 104 | public function isOptional() 105 | { 106 | return $this->optional; 107 | } 108 | } 109 | 110 | /** 111 | * Represents a PHP requirement in form of a php.ini configuration. 112 | * 113 | * @author Tobias Schultze 114 | */ 115 | class PhpIniRequirement extends Requirement 116 | { 117 | /** 118 | * Constructor that initializes the requirement. 119 | * 120 | * @param string $cfgName The configuration name used for ini_get() 121 | * @param bool|callback $evaluation Either a boolean indicating whether the configuration should evaluate to true or false, 122 | * or a callback function receiving the configuration value as parameter to determine the fulfillment of the requirement 123 | * @param bool $approveCfgAbsence If true the Requirement will be fulfilled even if the configuration option does not exist, i.e. ini_get() returns false. 124 | * This is helpful for abandoned configs in later PHP versions or configs of an optional extension, like Suhosin. 125 | * Example: You require a config to be true but PHP later removes this config and defaults it to true internally. 126 | * @param string|null $testMessage The message for testing the requirement (when null and $evaluation is a boolean a default message is derived) 127 | * @param string|null $helpHtml The help text formatted in HTML for resolving the problem (when null and $evaluation is a boolean a default help is derived) 128 | * @param string|null $helpText The help text (when null, it will be inferred from $helpHtml, i.e. stripped from HTML tags) 129 | * @param bool $optional Whether this is only an optional recommendation not a mandatory requirement 130 | */ 131 | public function __construct($cfgName, $evaluation, $approveCfgAbsence = false, $testMessage = null, $helpHtml = null, $helpText = null, $optional = false) 132 | { 133 | $cfgValue = ini_get($cfgName); 134 | 135 | if (is_callable($evaluation)) { 136 | if (null === $testMessage || null === $helpHtml) { 137 | throw new InvalidArgumentException('You must provide the parameters testMessage and helpHtml for a callback evaluation.'); 138 | } 139 | 140 | $fulfilled = call_user_func($evaluation, $cfgValue); 141 | } else { 142 | if (null === $testMessage) { 143 | $testMessage = sprintf('%s %s be %s in php.ini', 144 | $cfgName, 145 | $optional ? 'should' : 'must', 146 | $evaluation ? 'enabled' : 'disabled' 147 | ); 148 | } 149 | 150 | if (null === $helpHtml) { 151 | $helpHtml = sprintf('Set %s to %s in php.ini*.', 152 | $cfgName, 153 | $evaluation ? 'on' : 'off' 154 | ); 155 | } 156 | 157 | $fulfilled = $evaluation == $cfgValue; 158 | } 159 | 160 | parent::__construct($fulfilled || ($approveCfgAbsence && false === $cfgValue), $testMessage, $helpHtml, $helpText, $optional); 161 | } 162 | } 163 | 164 | /** 165 | * A RequirementCollection represents a set of Requirement instances. 166 | * 167 | * @author Tobias Schultze 168 | */ 169 | class RequirementCollection implements IteratorAggregate 170 | { 171 | private $requirements = array(); 172 | 173 | /** 174 | * Gets the current RequirementCollection as an Iterator. 175 | * 176 | * @return Traversable A Traversable interface 177 | */ 178 | public function getIterator() 179 | { 180 | return new ArrayIterator($this->requirements); 181 | } 182 | 183 | /** 184 | * Adds a Requirement. 185 | * 186 | * @param Requirement $requirement A Requirement instance 187 | */ 188 | public function add(Requirement $requirement) 189 | { 190 | $this->requirements[] = $requirement; 191 | } 192 | 193 | /** 194 | * Adds a mandatory requirement. 195 | * 196 | * @param bool $fulfilled Whether the requirement is fulfilled 197 | * @param string $testMessage The message for testing the requirement 198 | * @param string $helpHtml The help text formatted in HTML for resolving the problem 199 | * @param string|null $helpText The help text (when null, it will be inferred from $helpHtml, i.e. stripped from HTML tags) 200 | */ 201 | public function addRequirement($fulfilled, $testMessage, $helpHtml, $helpText = null) 202 | { 203 | $this->add(new Requirement($fulfilled, $testMessage, $helpHtml, $helpText, false)); 204 | } 205 | 206 | /** 207 | * Adds an optional recommendation. 208 | * 209 | * @param bool $fulfilled Whether the recommendation is fulfilled 210 | * @param string $testMessage The message for testing the recommendation 211 | * @param string $helpHtml The help text formatted in HTML for resolving the problem 212 | * @param string|null $helpText The help text (when null, it will be inferred from $helpHtml, i.e. stripped from HTML tags) 213 | */ 214 | public function addRecommendation($fulfilled, $testMessage, $helpHtml, $helpText = null) 215 | { 216 | $this->add(new Requirement($fulfilled, $testMessage, $helpHtml, $helpText, true)); 217 | } 218 | 219 | /** 220 | * Adds a mandatory requirement in form of a php.ini configuration. 221 | * 222 | * @param string $cfgName The configuration name used for ini_get() 223 | * @param bool|callback $evaluation Either a boolean indicating whether the configuration should evaluate to true or false, 224 | * or a callback function receiving the configuration value as parameter to determine the fulfillment of the requirement 225 | * @param bool $approveCfgAbsence If true the Requirement will be fulfilled even if the configuration option does not exist, i.e. ini_get() returns false. 226 | * This is helpful for abandoned configs in later PHP versions or configs of an optional extension, like Suhosin. 227 | * Example: You require a config to be true but PHP later removes this config and defaults it to true internally. 228 | * @param string $testMessage The message for testing the requirement (when null and $evaluation is a boolean a default message is derived) 229 | * @param string $helpHtml The help text formatted in HTML for resolving the problem (when null and $evaluation is a boolean a default help is derived) 230 | * @param string|null $helpText The help text (when null, it will be inferred from $helpHtml, i.e. stripped from HTML tags) 231 | */ 232 | public function addPhpIniRequirement($cfgName, $evaluation, $approveCfgAbsence = false, $testMessage = null, $helpHtml = null, $helpText = null) 233 | { 234 | $this->add(new PhpIniRequirement($cfgName, $evaluation, $approveCfgAbsence, $testMessage, $helpHtml, $helpText, false)); 235 | } 236 | 237 | /** 238 | * Adds an optional recommendation in form of a php.ini configuration. 239 | * 240 | * @param string $cfgName The configuration name used for ini_get() 241 | * @param bool|callback $evaluation Either a boolean indicating whether the configuration should evaluate to true or false, 242 | * or a callback function receiving the configuration value as parameter to determine the fulfillment of the requirement 243 | * @param bool $approveCfgAbsence If true the Requirement will be fulfilled even if the configuration option does not exist, i.e. ini_get() returns false. 244 | * This is helpful for abandoned configs in later PHP versions or configs of an optional extension, like Suhosin. 245 | * Example: You require a config to be true but PHP later removes this config and defaults it to true internally. 246 | * @param string $testMessage The message for testing the requirement (when null and $evaluation is a boolean a default message is derived) 247 | * @param string $helpHtml The help text formatted in HTML for resolving the problem (when null and $evaluation is a boolean a default help is derived) 248 | * @param string|null $helpText The help text (when null, it will be inferred from $helpHtml, i.e. stripped from HTML tags) 249 | */ 250 | public function addPhpIniRecommendation($cfgName, $evaluation, $approveCfgAbsence = false, $testMessage = null, $helpHtml = null, $helpText = null) 251 | { 252 | $this->add(new PhpIniRequirement($cfgName, $evaluation, $approveCfgAbsence, $testMessage, $helpHtml, $helpText, true)); 253 | } 254 | 255 | /** 256 | * Adds a requirement collection to the current set of requirements. 257 | * 258 | * @param RequirementCollection $collection A RequirementCollection instance 259 | */ 260 | public function addCollection(RequirementCollection $collection) 261 | { 262 | $this->requirements = array_merge($this->requirements, $collection->all()); 263 | } 264 | 265 | /** 266 | * Returns both requirements and recommendations. 267 | * 268 | * @return array Array of Requirement instances 269 | */ 270 | public function all() 271 | { 272 | return $this->requirements; 273 | } 274 | 275 | /** 276 | * Returns all mandatory requirements. 277 | * 278 | * @return array Array of Requirement instances 279 | */ 280 | public function getRequirements() 281 | { 282 | $array = array(); 283 | foreach ($this->requirements as $req) { 284 | if (!$req->isOptional()) { 285 | $array[] = $req; 286 | } 287 | } 288 | 289 | return $array; 290 | } 291 | 292 | /** 293 | * Returns the mandatory requirements that were not met. 294 | * 295 | * @return array Array of Requirement instances 296 | */ 297 | public function getFailedRequirements() 298 | { 299 | $array = array(); 300 | foreach ($this->requirements as $req) { 301 | if (!$req->isFulfilled() && !$req->isOptional()) { 302 | $array[] = $req; 303 | } 304 | } 305 | 306 | return $array; 307 | } 308 | 309 | /** 310 | * Returns all optional recommendations. 311 | * 312 | * @return array Array of Requirement instances 313 | */ 314 | public function getRecommendations() 315 | { 316 | $array = array(); 317 | foreach ($this->requirements as $req) { 318 | if ($req->isOptional()) { 319 | $array[] = $req; 320 | } 321 | } 322 | 323 | return $array; 324 | } 325 | 326 | /** 327 | * Returns the recommendations that were not met. 328 | * 329 | * @return array Array of Requirement instances 330 | */ 331 | public function getFailedRecommendations() 332 | { 333 | $array = array(); 334 | foreach ($this->requirements as $req) { 335 | if (!$req->isFulfilled() && $req->isOptional()) { 336 | $array[] = $req; 337 | } 338 | } 339 | 340 | return $array; 341 | } 342 | 343 | /** 344 | * Returns whether a php.ini configuration is not correct. 345 | * 346 | * @return bool php.ini configuration problem? 347 | */ 348 | public function hasPhpIniConfigIssue() 349 | { 350 | foreach ($this->requirements as $req) { 351 | if (!$req->isFulfilled() && $req instanceof PhpIniRequirement) { 352 | return true; 353 | } 354 | } 355 | 356 | return false; 357 | } 358 | 359 | /** 360 | * Returns the PHP configuration file (php.ini) path. 361 | * 362 | * @return string|false php.ini file path 363 | */ 364 | public function getPhpIniConfigPath() 365 | { 366 | return get_cfg_var('cfg_file_path'); 367 | } 368 | } 369 | 370 | /** 371 | * This class specifies all requirements and optional recommendations that 372 | * are necessary to run the Symfony Standard Edition. 373 | * 374 | * @author Tobias Schultze 375 | * @author Fabien Potencier 376 | */ 377 | class SymfonyRequirements extends RequirementCollection 378 | { 379 | const REQUIRED_PHP_VERSION = '5.3.3'; 380 | 381 | /** 382 | * Constructor that initializes the requirements. 383 | */ 384 | public function __construct() 385 | { 386 | /* mandatory requirements follow */ 387 | 388 | $installedPhpVersion = phpversion(); 389 | 390 | $this->addRequirement( 391 | version_compare($installedPhpVersion, self::REQUIRED_PHP_VERSION, '>='), 392 | sprintf('PHP version must be at least %s (%s installed)', self::REQUIRED_PHP_VERSION, $installedPhpVersion), 393 | sprintf('You are running PHP version "%s", but Symfony needs at least PHP "%s" to run. 394 | Before using Symfony, upgrade your PHP installation, preferably to the latest version.', 395 | $installedPhpVersion, self::REQUIRED_PHP_VERSION), 396 | sprintf('Install PHP %s or newer (installed version is %s)', self::REQUIRED_PHP_VERSION, $installedPhpVersion) 397 | ); 398 | 399 | $this->addRequirement( 400 | version_compare($installedPhpVersion, '5.3.16', '!='), 401 | 'PHP version must not be 5.3.16 as Symfony won\'t work properly with it', 402 | 'Install PHP 5.3.17 or newer (or downgrade to an earlier PHP version)' 403 | ); 404 | 405 | $this->addRequirement( 406 | is_dir(__DIR__.'/../vendor/composer'), 407 | 'Vendor libraries must be installed', 408 | 'Vendor libraries are missing. Install composer following instructions from http://getcomposer.org/. '. 409 | 'Then run "php composer.phar install" to install them.' 410 | ); 411 | 412 | $cacheDir = is_dir(__DIR__.'/../var/cache') ? __DIR__.'/../var/cache' : __DIR__.'/cache'; 413 | 414 | $this->addRequirement( 415 | is_writable($cacheDir), 416 | 'app/cache/ or var/cache/ directory must be writable', 417 | 'Change the permissions of either "app/cache/" or "var/cache/" directory so that the web server can write into it.' 418 | ); 419 | 420 | $logsDir = is_dir(__DIR__.'/../var/logs') ? __DIR__.'/../var/logs' : __DIR__.'/logs'; 421 | 422 | $this->addRequirement( 423 | is_writable($logsDir), 424 | 'app/logs/ or var/logs/ directory must be writable', 425 | 'Change the permissions of either "app/logs/" or "var/logs/" directory so that the web server can write into it.' 426 | ); 427 | 428 | if (version_compare($installedPhpVersion, '7.0.0', '<')) { 429 | $this->addPhpIniRequirement( 430 | 'date.timezone', true, false, 431 | 'date.timezone setting must be set', 432 | 'Set the "date.timezone" setting in php.ini* (like Europe/Paris).' 433 | ); 434 | } 435 | 436 | if (version_compare($installedPhpVersion, self::REQUIRED_PHP_VERSION, '>=')) { 437 | $timezones = array(); 438 | foreach (DateTimeZone::listAbbreviations() as $abbreviations) { 439 | foreach ($abbreviations as $abbreviation) { 440 | $timezones[$abbreviation['timezone_id']] = true; 441 | } 442 | } 443 | 444 | $this->addRequirement( 445 | isset($timezones[@date_default_timezone_get()]), 446 | sprintf('Configured default timezone "%s" must be supported by your installation of PHP', @date_default_timezone_get()), 447 | 'Your default timezone is not supported by PHP. Check for typos in your php.ini file and have a look at the list of deprecated timezones at http://php.net/manual/en/timezones.others.php.' 448 | ); 449 | } 450 | 451 | $this->addRequirement( 452 | function_exists('iconv'), 453 | 'iconv() must be available', 454 | 'Install and enable the iconv extension.' 455 | ); 456 | 457 | $this->addRequirement( 458 | function_exists('json_encode'), 459 | 'json_encode() must be available', 460 | 'Install and enable the JSON extension.' 461 | ); 462 | 463 | $this->addRequirement( 464 | function_exists('session_start'), 465 | 'session_start() must be available', 466 | 'Install and enable the session extension.' 467 | ); 468 | 469 | $this->addRequirement( 470 | function_exists('ctype_alpha'), 471 | 'ctype_alpha() must be available', 472 | 'Install and enable the ctype extension.' 473 | ); 474 | 475 | $this->addRequirement( 476 | function_exists('token_get_all'), 477 | 'token_get_all() must be available', 478 | 'Install and enable the Tokenizer extension.' 479 | ); 480 | 481 | $this->addRequirement( 482 | function_exists('simplexml_import_dom'), 483 | 'simplexml_import_dom() must be available', 484 | 'Install and enable the SimpleXML extension.' 485 | ); 486 | 487 | if (function_exists('apc_store') && ini_get('apc.enabled')) { 488 | if (version_compare($installedPhpVersion, '5.4.0', '>=')) { 489 | $this->addRequirement( 490 | version_compare(phpversion('apc'), '3.1.13', '>='), 491 | 'APC version must be at least 3.1.13 when using PHP 5.4', 492 | 'Upgrade your APC extension (3.1.13+).' 493 | ); 494 | } else { 495 | $this->addRequirement( 496 | version_compare(phpversion('apc'), '3.0.17', '>='), 497 | 'APC version must be at least 3.0.17', 498 | 'Upgrade your APC extension (3.0.17+).' 499 | ); 500 | } 501 | } 502 | 503 | $this->addPhpIniRequirement('detect_unicode', false); 504 | 505 | if (extension_loaded('suhosin')) { 506 | $this->addPhpIniRequirement( 507 | 'suhosin.executor.include.whitelist', 508 | create_function('$cfgValue', 'return false !== stripos($cfgValue, "phar");'), 509 | false, 510 | 'suhosin.executor.include.whitelist must be configured correctly in php.ini', 511 | 'Add "phar" to suhosin.executor.include.whitelist in php.ini*.' 512 | ); 513 | } 514 | 515 | if (extension_loaded('xdebug')) { 516 | $this->addPhpIniRequirement( 517 | 'xdebug.show_exception_trace', false, true 518 | ); 519 | 520 | $this->addPhpIniRequirement( 521 | 'xdebug.scream', false, true 522 | ); 523 | 524 | $this->addPhpIniRecommendation( 525 | 'xdebug.max_nesting_level', 526 | create_function('$cfgValue', 'return $cfgValue > 100;'), 527 | true, 528 | 'xdebug.max_nesting_level should be above 100 in php.ini', 529 | 'Set "xdebug.max_nesting_level" to e.g. "250" in php.ini* to stop Xdebug\'s infinite recursion protection erroneously throwing a fatal error in your project.' 530 | ); 531 | } 532 | 533 | $pcreVersion = defined('PCRE_VERSION') ? (float) PCRE_VERSION : null; 534 | 535 | $this->addRequirement( 536 | null !== $pcreVersion, 537 | 'PCRE extension must be available', 538 | 'Install the PCRE extension (version 8.0+).' 539 | ); 540 | 541 | if (extension_loaded('mbstring')) { 542 | $this->addPhpIniRequirement( 543 | 'mbstring.func_overload', 544 | create_function('$cfgValue', 'return (int) $cfgValue === 0;'), 545 | true, 546 | 'string functions should not be overloaded', 547 | 'Set "mbstring.func_overload" to 0 in php.ini* to disable function overloading by the mbstring extension.' 548 | ); 549 | } 550 | 551 | /* optional recommendations follow */ 552 | 553 | if (file_exists(__DIR__.'/../vendor/composer')) { 554 | require_once __DIR__.'/../vendor/autoload.php'; 555 | 556 | try { 557 | $r = new ReflectionClass('Sensio\Bundle\DistributionBundle\SensioDistributionBundle'); 558 | 559 | $contents = file_get_contents(dirname($r->getFileName()).'/Resources/skeleton/app/SymfonyRequirements.php'); 560 | } catch (ReflectionException $e) { 561 | $contents = ''; 562 | } 563 | $this->addRecommendation( 564 | file_get_contents(__FILE__) === $contents, 565 | 'Requirements file should be up-to-date', 566 | 'Your requirements file is outdated. Run composer install and re-check your configuration.' 567 | ); 568 | } 569 | 570 | $this->addRecommendation( 571 | version_compare($installedPhpVersion, '5.3.4', '>='), 572 | 'You should use at least PHP 5.3.4 due to PHP bug #52083 in earlier versions', 573 | 'Your project might malfunction randomly due to PHP bug #52083 ("Notice: Trying to get property of non-object"). Install PHP 5.3.4 or newer.' 574 | ); 575 | 576 | $this->addRecommendation( 577 | version_compare($installedPhpVersion, '5.3.8', '>='), 578 | 'When using annotations you should have at least PHP 5.3.8 due to PHP bug #55156', 579 | 'Install PHP 5.3.8 or newer if your project uses annotations.' 580 | ); 581 | 582 | $this->addRecommendation( 583 | version_compare($installedPhpVersion, '5.4.0', '!='), 584 | 'You should not use PHP 5.4.0 due to the PHP bug #61453', 585 | 'Your project might not work properly due to the PHP bug #61453 ("Cannot dump definitions which have method calls"). Install PHP 5.4.1 or newer.' 586 | ); 587 | 588 | $this->addRecommendation( 589 | version_compare($installedPhpVersion, '5.4.11', '>='), 590 | 'When using the logout handler from the Symfony Security Component, you should have at least PHP 5.4.11 due to PHP bug #63379 (as a workaround, you can also set invalidate_session to false in the security logout handler configuration)', 591 | 'Install PHP 5.4.11 or newer if your project uses the logout handler from the Symfony Security Component.' 592 | ); 593 | 594 | $this->addRecommendation( 595 | (version_compare($installedPhpVersion, '5.3.18', '>=') && version_compare($installedPhpVersion, '5.4.0', '<')) 596 | || 597 | version_compare($installedPhpVersion, '5.4.8', '>='), 598 | 'You should use PHP 5.3.18+ or PHP 5.4.8+ to always get nice error messages for fatal errors in the development environment due to PHP bug #61767/#60909', 599 | 'Install PHP 5.3.18+ or PHP 5.4.8+ if you want nice error messages for all fatal errors in the development environment.' 600 | ); 601 | 602 | if (null !== $pcreVersion) { 603 | $this->addRecommendation( 604 | $pcreVersion >= 8.0, 605 | sprintf('PCRE extension should be at least version 8.0 (%s installed)', $pcreVersion), 606 | 'PCRE 8.0+ is preconfigured in PHP since 5.3.2 but you are using an outdated version of it. Symfony probably works anyway but it is recommended to upgrade your PCRE extension.' 607 | ); 608 | } 609 | 610 | $this->addRecommendation( 611 | class_exists('DomDocument'), 612 | 'PHP-DOM and PHP-XML modules should be installed', 613 | 'Install and enable the PHP-DOM and the PHP-XML modules.' 614 | ); 615 | 616 | $this->addRecommendation( 617 | function_exists('mb_strlen'), 618 | 'mb_strlen() should be available', 619 | 'Install and enable the mbstring extension.' 620 | ); 621 | 622 | $this->addRecommendation( 623 | function_exists('iconv'), 624 | 'iconv() should be available', 625 | 'Install and enable the iconv extension.' 626 | ); 627 | 628 | $this->addRecommendation( 629 | function_exists('utf8_decode'), 630 | 'utf8_decode() should be available', 631 | 'Install and enable the XML extension.' 632 | ); 633 | 634 | $this->addRecommendation( 635 | function_exists('filter_var'), 636 | 'filter_var() should be available', 637 | 'Install and enable the filter extension.' 638 | ); 639 | 640 | if (!defined('PHP_WINDOWS_VERSION_BUILD')) { 641 | $this->addRecommendation( 642 | function_exists('posix_isatty'), 643 | 'posix_isatty() should be available', 644 | 'Install and enable the php_posix extension (used to colorize the CLI output).' 645 | ); 646 | } 647 | 648 | $this->addRecommendation( 649 | extension_loaded('intl'), 650 | 'intl extension should be available', 651 | 'Install and enable the intl extension (used for validators).' 652 | ); 653 | 654 | if (extension_loaded('intl')) { 655 | // in some WAMP server installations, new Collator() returns null 656 | $this->addRecommendation( 657 | null !== new Collator('fr_FR'), 658 | 'intl extension should be correctly configured', 659 | 'The intl extension does not behave properly. This problem is typical on PHP 5.3.X x64 WIN builds.' 660 | ); 661 | 662 | // check for compatible ICU versions (only done when you have the intl extension) 663 | if (defined('INTL_ICU_VERSION')) { 664 | $version = INTL_ICU_VERSION; 665 | } else { 666 | $reflector = new ReflectionExtension('intl'); 667 | 668 | ob_start(); 669 | $reflector->info(); 670 | $output = strip_tags(ob_get_clean()); 671 | 672 | preg_match('/^ICU version +(?:=> )?(.*)$/m', $output, $matches); 673 | $version = $matches[1]; 674 | } 675 | 676 | $this->addRecommendation( 677 | version_compare($version, '4.0', '>='), 678 | 'intl ICU version should be at least 4+', 679 | 'Upgrade your intl extension with a newer ICU version (4+).' 680 | ); 681 | 682 | if (class_exists('Symfony\Component\Intl\Intl')) { 683 | $this->addRecommendation( 684 | \Symfony\Component\Intl\Intl::getIcuDataVersion() <= \Symfony\Component\Intl\Intl::getIcuVersion(), 685 | sprintf('intl ICU version installed on your system is outdated (%s) and does not match the ICU data bundled with Symfony (%s)', \Symfony\Component\Intl\Intl::getIcuVersion(), \Symfony\Component\Intl\Intl::getIcuDataVersion()), 686 | 'To get the latest internationalization data upgrade the ICU system package and the intl PHP extension.' 687 | ); 688 | if (\Symfony\Component\Intl\Intl::getIcuDataVersion() <= \Symfony\Component\Intl\Intl::getIcuVersion()) { 689 | $this->addRecommendation( 690 | \Symfony\Component\Intl\Intl::getIcuDataVersion() === \Symfony\Component\Intl\Intl::getIcuVersion(), 691 | sprintf('intl ICU version installed on your system (%s) does not match the ICU data bundled with Symfony (%s)', \Symfony\Component\Intl\Intl::getIcuVersion(), \Symfony\Component\Intl\Intl::getIcuDataVersion()), 692 | 'To avoid internationalization data incosistencies upgrade the symfony/intl component.' 693 | ); 694 | } 695 | } 696 | 697 | $this->addPhpIniRecommendation( 698 | 'intl.error_level', 699 | create_function('$cfgValue', 'return (int) $cfgValue === 0;'), 700 | true, 701 | 'intl.error_level should be 0 in php.ini', 702 | 'Set "intl.error_level" to "0" in php.ini* to inhibit the messages when an error occurs in ICU functions.' 703 | ); 704 | } 705 | 706 | $accelerator = 707 | (extension_loaded('eaccelerator') && ini_get('eaccelerator.enable')) 708 | || 709 | (extension_loaded('apc') && ini_get('apc.enabled')) 710 | || 711 | (extension_loaded('Zend Optimizer+') && ini_get('zend_optimizerplus.enable')) 712 | || 713 | (extension_loaded('Zend OPcache') && ini_get('opcache.enable')) 714 | || 715 | (extension_loaded('xcache') && ini_get('xcache.cacher')) 716 | || 717 | (extension_loaded('wincache') && ini_get('wincache.ocenabled')) 718 | ; 719 | 720 | $this->addRecommendation( 721 | $accelerator, 722 | 'a PHP accelerator should be installed', 723 | 'Install and/or enable a PHP accelerator (highly recommended).' 724 | ); 725 | 726 | if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { 727 | $this->addRecommendation( 728 | $this->getRealpathCacheSize() > 1000, 729 | 'realpath_cache_size should be above 1024 in php.ini', 730 | 'Set "realpath_cache_size" to e.g. "1024" in php.ini* to improve performance on windows.' 731 | ); 732 | } 733 | 734 | $this->addPhpIniRecommendation('short_open_tag', false); 735 | 736 | $this->addPhpIniRecommendation('magic_quotes_gpc', false, true); 737 | 738 | $this->addPhpIniRecommendation('register_globals', false, true); 739 | 740 | $this->addPhpIniRecommendation('session.auto_start', false); 741 | 742 | $this->addRecommendation( 743 | class_exists('PDO'), 744 | 'PDO should be installed', 745 | 'Install PDO (mandatory for Doctrine).' 746 | ); 747 | 748 | if (class_exists('PDO')) { 749 | $drivers = PDO::getAvailableDrivers(); 750 | $this->addRecommendation( 751 | count($drivers) > 0, 752 | sprintf('PDO should have some drivers installed (currently available: %s)', count($drivers) ? implode(', ', $drivers) : 'none'), 753 | 'Install PDO drivers (mandatory for Doctrine).' 754 | ); 755 | } 756 | } 757 | 758 | /** 759 | * Loads realpath_cache_size from php.ini and converts it to int. 760 | * 761 | * (e.g. 16k is converted to 16384 int) 762 | * 763 | * @return int 764 | */ 765 | protected function getRealpathCacheSize() 766 | { 767 | $size = ini_get('realpath_cache_size'); 768 | $size = trim($size); 769 | $unit = strtolower(substr($size, -1, 1)); 770 | switch ($unit) { 771 | case 'g': 772 | return $size * 1024 * 1024 * 1024; 773 | case 'm': 774 | return $size * 1024 * 1024; 775 | case 'k': 776 | return $size * 1024; 777 | default: 778 | return (int) $size; 779 | } 780 | } 781 | } 782 | -------------------------------------------------------------------------------- /var/cache/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codereviewvideos/fos-rest-and-user-bundle-integration/e2d5ec7c1cac94946a8bd25f3dd52ae3e715c01d/var/cache/.gitkeep -------------------------------------------------------------------------------- /var/logs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codereviewvideos/fos-rest-and-user-bundle-integration/e2d5ec7c1cac94946a8bd25f3dd52ae3e715c01d/var/logs/.gitkeep -------------------------------------------------------------------------------- /var/sessions/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codereviewvideos/fos-rest-and-user-bundle-integration/e2d5ec7c1cac94946a8bd25f3dd52ae3e715c01d/var/sessions/.gitkeep -------------------------------------------------------------------------------- /web/.htaccess: -------------------------------------------------------------------------------- 1 | # Use the front controller as index file. It serves as a fallback solution when 2 | # every other rewrite/redirect fails (e.g. in an aliased environment without 3 | # mod_rewrite). Additionally, this reduces the matching process for the 4 | # start page (path "/") because otherwise Apache will apply the rewriting rules 5 | # to each configured DirectoryIndex file (e.g. index.php, index.html, index.pl). 6 | DirectoryIndex app.php 7 | 8 | # By default, Apache does not evaluate symbolic links if you did not enable this 9 | # feature in your server configuration. Uncomment the following line if you 10 | # install assets as symlinks or if you experience problems related to symlinks 11 | # when compiling LESS/Sass/CoffeScript assets. 12 | # Options FollowSymlinks 13 | 14 | # Disabling MultiViews prevents unwanted negotiation, e.g. "/app" should not resolve 15 | # to the front controller "/app.php" but be rewritten to "/app.php/app". 16 | 17 | Options -MultiViews 18 | 19 | 20 | 21 | RewriteEngine On 22 | 23 | # Determine the RewriteBase automatically and set it as environment variable. 24 | # If you are using Apache aliases to do mass virtual hosting or installed the 25 | # project in a subdirectory, the base path will be prepended to allow proper 26 | # resolution of the app.php file and to redirect to the correct URI. It will 27 | # work in environments without path prefix as well, providing a safe, one-size 28 | # fits all solution. But as you do not need it in this case, you can comment 29 | # the following 2 lines to eliminate the overhead. 30 | RewriteCond %{REQUEST_URI}::$1 ^(/.+)/(.*)::\2$ 31 | RewriteRule ^(.*) - [E=BASE:%1] 32 | 33 | # Sets the HTTP_AUTHORIZATION header removed by Apache 34 | RewriteCond %{HTTP:Authorization} . 35 | RewriteRule ^ - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] 36 | 37 | # Redirect to URI without front controller to prevent duplicate content 38 | # (with and without `/app.php`). Only do this redirect on the initial 39 | # rewrite by Apache and not on subsequent cycles. Otherwise we would get an 40 | # endless redirect loop (request -> rewrite to front controller -> 41 | # redirect -> request -> ...). 42 | # So in case you get a "too many redirects" error or you always get redirected 43 | # to the start page because your Apache does not expose the REDIRECT_STATUS 44 | # environment variable, you have 2 choices: 45 | # - disable this feature by commenting the following 2 lines or 46 | # - use Apache >= 2.3.9 and replace all L flags by END flags and remove the 47 | # following RewriteCond (best solution) 48 | RewriteCond %{ENV:REDIRECT_STATUS} ^$ 49 | RewriteRule ^app\.php(?:/(.*)|$) %{ENV:BASE}/$1 [R=301,L] 50 | 51 | # If the requested filename exists, simply serve it. 52 | # We only want to let Apache serve files and not directories. 53 | RewriteCond %{REQUEST_FILENAME} -f 54 | RewriteRule ^ - [L] 55 | 56 | # Rewrite all other queries to the front controller. 57 | RewriteRule ^ %{ENV:BASE}/app.php [L] 58 | 59 | 60 | 61 | 62 | # When mod_rewrite is not available, we instruct a temporary redirect of 63 | # the start page to the front controller explicitly so that the website 64 | # and the generated links can still be used. 65 | RedirectMatch 302 ^/$ /app.php/ 66 | # RedirectTemp cannot be used instead 67 | 68 | 69 | -------------------------------------------------------------------------------- /web/app.php: -------------------------------------------------------------------------------- 1 | loadClassCache(); 13 | //$kernel = new AppCache($kernel); 14 | 15 | // When using the HttpCache, you need to call the method in your front controller instead of relying on the configuration parameter 16 | //Request::enableHttpMethodParameterOverride(); 17 | $request = Request::createFromGlobals(); 18 | $response = $kernel->handle($request); 19 | $response->send(); 20 | $kernel->terminate($request, $response); 21 | -------------------------------------------------------------------------------- /web/app_acceptance.php: -------------------------------------------------------------------------------- 1 | loadClassCache(); 24 | $request = Request::createFromGlobals(); 25 | $response = $kernel->handle($request); 26 | $response->send(); 27 | $kernel->terminate($request, $response); -------------------------------------------------------------------------------- /web/app_dev.php: -------------------------------------------------------------------------------- 1 | loadClassCache(); 29 | $request = Request::createFromGlobals(); 30 | $response = $kernel->handle($request); 31 | $response->send(); 32 | $kernel->terminate($request, $response); 33 | -------------------------------------------------------------------------------- /web/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codereviewvideos/fos-rest-and-user-bundle-integration/e2d5ec7c1cac94946a8bd25f3dd52ae3e715c01d/web/apple-touch-icon.png -------------------------------------------------------------------------------- /web/config.php: -------------------------------------------------------------------------------- 1 | getFailedRequirements(); 30 | $minorProblems = $symfonyRequirements->getFailedRecommendations(); 31 | 32 | ?> 33 | 34 | 35 | 36 | 37 | 38 | Symfony Configuration Checker 39 | 40 | 41 | 122 | 123 | 124 |
125 |
126 | 129 | 130 | 150 |
151 | 152 |
153 |
154 |
155 |

Configuration Checker

156 |

157 | This script analyzes your system to check whether is 158 | ready to run Symfony applications. 159 |

160 | 161 | 162 |

Major problems

163 |

Major problems have been detected and must be fixed before continuing:

164 |
    165 | 166 |
  1. getTestMessage() ?> 167 |

    getHelpHtml() ?>

    168 |
  2. 169 | 170 |
171 | 172 | 173 | 174 |

Recommendations

175 |

176 | Additionally, toTo enhance your Symfony experience, 177 | it’s recommended that you fix the following: 178 |

179 |
    180 | 181 |
  1. getTestMessage() ?> 182 |

    getHelpHtml() ?>

    183 |
  2. 184 | 185 |
186 | 187 | 188 | hasPhpIniConfigIssue()): ?> 189 |

* 190 | getPhpIniConfigPath()): ?> 191 | Changes to the php.ini file must be done in "getPhpIniConfigPath() ?>". 192 | 193 | To change settings, create a "php.ini". 194 | 195 |

196 | 197 | 198 | 199 |

All checks passed successfully. Your system is ready to run Symfony applications.

200 | 201 | 202 | 207 |
208 |
209 |
210 |
Symfony Standard Edition
211 |
212 | 213 | 214 | -------------------------------------------------------------------------------- /web/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codereviewvideos/fos-rest-and-user-bundle-integration/e2d5ec7c1cac94946a8bd25f3dd52ae3e715c01d/web/favicon.ico -------------------------------------------------------------------------------- /web/robots.txt: -------------------------------------------------------------------------------- 1 | # www.robotstxt.org/ 2 | # www.google.com/support/webmasters/bin/answer.py?hl=en&answer=156449 3 | 4 | User-agent: * 5 | Disallow: 6 | --------------------------------------------------------------------------------