I'm writing PHPUnit tests and running coverage tests. I admit its very difficult to have 100% coverage, however, I'd like to get as close as possible. In a following scenario, how to mock variables in a clause in order to test the code block?
class CalendarClientService
{
/** #var array SCOPES */
public const SCOPES = [Google_Service_Calendar::CALENDAR];
/** #var string ACCESS_TYPE */
public const ACCESS_TYPE = "offline";
/** #var string CALENDAR_ID */
public const CALENDAR_ID = "primary";
/** #var int MAX_RESULTS */
public const MAX_RESULTS = 25;
/** #var string ORDER_BY */
public const ORDER_BY = "startTime";
/** #var bool SINGLE_EVENTS */
public const SINGLE_EVENTS = true;
/** #var string|null TIME_MIN */
public const TIME_MIN = null;
/** #var bool CACHE_TIME_TO_LIVE */
public const CACHE_TIME_TO_LIVE = 604800;
/** #var string */
public string $clientSecretPath = "";
/** #var StorageAdapterFactoryInterface */
protected StorageAdapterFactoryInterface $storageAdapterFactory;
/** #var StorageInterface */
protected StorageInterface $storageInterfaceCache;
/**
* CalendarClientService constructor.
* #param string $clientSecretPath
* #param StorageAdapterFactoryInterface $storageAdapterFactory
* #param StorageInterface $storageInterfaceCache
*/
public function __construct(
string $clientSecretPath,
StorageAdapterFactoryInterface $storageAdapterFactory,
StorageInterface $storageInterfaceCache
) {
$this->clientSecretPath = $clientSecretPath;
$this->storageAdapterFactory = $storageAdapterFactory;
$this->storageInterfaceCache = $storageInterfaceCache;
}
/** #return string */
public function getClientSecretPath()
{
return $this->clientSecretPath;
}
/** #param string $secretFile */
public function setClientSecretPath(string $secretFile)
{
$this->clientSecretPath = $secretFile;
}
/**
* #param array
* #return Google_Service_Calendar_Event
*/
public function getGoogleServiceCalendarEvent($eventData)
{
return new Google_Service_Calendar_Event($eventData);
}
/**
* #param string
* #return Google_Service_Calendar_EventDateTime
*/
public function getGoogleServiceCalendarEventDateTime($dateTime)
{
$eventDateTime = new Google_Service_Calendar_EventDateTime();
$eventDateTime->setDateTime(Carbon::parse($dateTime)->toW3cString());
$eventDateTime->setTimeZone(Carbon::parse($dateTime)->timezone->getName());
return $eventDateTime;
}
/**
* #param Google_Client $client
* #return Events
*/
public function getGoogleServiceCalendarResourceEvents(Google_Client $client)
{
$service = new Google_Service_Calendar($client);
return $service->events;
}
/**
* #param int
* #return array
* #throws Exception
* #throws ExceptionInterface
*/
public function getEventData($id)
{
$client = $this->getClient();
if (!$this->authenticateClient($client)) {
return [
"error" => "authentication",
"url" => filter_var($client->createAuthUrl(), FILTER_SANITIZE_URL),
];
}
$service = $this->getGoogleServiceCalendarResourceEvents($client);
return ["event" => $service->get(self::CALENDAR_ID, $id)];
}
/**
* #return Google_Client
* #throws Exception
*/
public function getClient()
{
$client = new Google_Client();
$client->setApplicationName(Module::MODULE_NAME);
$client->setScopes(self::SCOPES);
$client->setAuthConfig($this->clientSecretPath);
$client->setAccessType(self::ACCESS_TYPE);
return $client;
}
/**
* #param Google_Client $client
* #return bool
* #throws ExceptionInterface
*/
public function authenticateClient(Google_Client $client)
{
if ($this->storageInterfaceCache->hasItem("api_access_token")) {
$accessToken = json_decode($this->storageInterfaceCache->getItem("api_access_token"), true);
if ($accessToken["error"] == "invalid_grant" || empty($accessToken)) {
$this->storageInterfaceCache->removeItem("api_access_token");
} else {
$this->storageInterfaceCache->setItem("api_access_token", json_encode($accessToken));
$client->setAccessToken($accessToken);
}
}
if ($client->isAccessTokenExpired()) {
$tokenValid = false;
if ($client->getRefreshToken()) {
$client->fetchAccessTokenWithRefreshToken($client->getRefreshToken());
$accessToken = $client->getAccessToken();
$this->storageInterfaceCache->setItem("api_access_token", json_encode($accessToken));
$tokenValid = true;
} else {
$helper = new Helper();
if(!$helper->verifyAuthCode($_GET["code"])){
return $tokenValid;
}
$authCode = $_GET["code"];
$accessToken = $client->fetchAccessTokenWithAuthCode($authCode);
if ($accessToken["error"] == "invalid_grant" || empty($accessToken)) {
$this->storageInterfaceCache->removeItem("api_access_token");
} else {
$this->storageInterfaceCache->setItem("api_access_token", json_encode($accessToken));
$client->setAccessToken($accessToken);
$tokenValid = true;
}
}
} else {
$tokenValid = true;
}
return isset($tokenValid) ? $tokenValid : false;
}
I want to test this 6th line from top in authenticateClient method and want to mock this clause $accessToken["error"] == "invalid_grant" || empty($accessToken).
Now how to go about it?
Edit: Here's a test that I've written. Now whatever value I'm mocking in the $this->storageInterfaceCacheMock->method("getItem"), it always returns empty $accessToken. I've also attached the image for better understanding of what's happening and what I want.
public function testGetEventDataReturnsArrayOnSuccessfulAuthenticateClientThroughCache()
{
$this->storageInterfaceCacheMock->method("hasItem")->willReturn(true);
$this->storageInterfaceCacheMock->method("getItem")->willReturn(json_encode('{"access_token":"ya29.a0ARrdaM99pJTf1XzmD1ngxAH3XJud8lvHb0aTaOOABYdfdhsdfgsdfgVD9OoH4heiKoskDF7DMkHj1_aPuWIO5TE14KHJidFf66xwn_pTCkkSow6Kg4lRHwGrNQBQGI8sPlgnFO5U5hJvYdqgxDMHEqw1TER2w","expires_in":3599,"scope":"https:\/\/www.googleapis.com\/auth\/calendar","token_type":"Bearer","created":1637312218,"refresh_token":"1\/\/03psr5omKiljUCgYFDHDGJHGSHSNwF-L9Iraor5zcfe-h3BeCHSFGSDFGDGJHjy4UnEtKj974LXthS5bWexQcjviVGfJsdfGHSHgIrDn6Yk"}'));
$this->assertIsArray($this->calendarClientService->getEventData(1));
}
Another test which isn't performing as per required is mentioned below. (also visible in the screenshot)
public function testAccessTokenIsExpiredAndGotRefreshToken()
{
$this->googleClientMock->method("isAccessTokenExpired")->willReturn(true);
$this->googleClientMock->method("getRefreshToken")->willReturn(true);
$this->googleClientMock->method("fetchAccessTokenWithRefreshToken")->willReturnSelf();
$this->googleClientMock->method("getAccessToken")->willReturnSelf();
$this->assertTrue($this->calendarClientService->authenticateClient($this->googleClientMock));
}
I believe you want json_decode you don't need double encode here:
$this->storageInterfaceCacheMock->method("getItem")->willReturn(json_encode('{"access_token":"...."}'));
just
$this->storageInterfaceCacheMock->method("getItem")->willReturn('{"access_token":"...."}');
Here's how I resolved the issue and got the test to be successful.
I declared a dummy ACCESS_TOKEN as below and then used in the test method.
class CalendarControllerTest extends AbstractApplicationTestCase
{
/** #var string CLIENT_SECRET */
public const CLIENT_SECRET = __DIR__ . "/../_fixtures/config/client_secret.json";
/** #var string CLIENT_SECRET */
public const ACCESS_TOKEN = [
"access_token" => "test-data",
"expires_in" => 3592,
"scope" => "https://www.googleapis.com/auth/calendar",
"token_type" => "Bearer",
"created" => 1640858809,
];
...
...
...
}
public function setUp(): void
{
parent::setUp();
$this->googleClientMock = $this->getMockBuilder(Google_Client::class)
->disableOriginalConstructor()
->onlyMethods(
[
"isAccessTokenExpired",
"setAuthConfig",
"getRefreshToken",
"fetchAccessTokenWithRefreshToken",
"fetchAccessTokenWithAuthCode",
"getAccessToken",
"setAccessToken",
]
)
->getMock();
$this->googleServiceCalendarResourceEventsMock = $this->getMockBuilder(Events::class)
->disableOriginalConstructor()
->onlyMethods(["get"])
->getMock();
$this->googleServiceCalendarEventMock = $this->getMockBuilder(Event::class)
->disableOriginalConstructor()
->getMock();
$this->storageInterfaceCacheMock = $this->getMockForAbstractClass(StorageInterface::class);
$this->container->setAllowOverride(true);
$this->container->setService(Google_Client::class, $this->googleClientMock);
$this->container->setService(Events::class, $this->googleServiceCalendarResourceEventsMock);
$this->container->setService(Event::class, $this->googleServiceCalendarEventMock);
$this->container->setService(StorageInterface::class, $this->storageInterfaceCacheMock);
$this->container->setAllowOverride(true);
$this->googleClientMock->method("setAuthConfig")->willReturn(true);
$this->calendarClientService = $this->container->get("ServiceManager")->get(CalendarClientService::class);
$this->calendarClientService->setClientSecretPath(CalendarControllerTest::CLIENT_SECRET);
}
/** #throws ExceptionInterface */
public function testAccessTokenIsExpiredAndFailureToRefreshTokenWillGenerateNewAccessToken()
{
$this->calendarClientService->setAuthCode(CalendarControllerTest::DEFAULT_TESTING_VALUE);
$this->googleClientMock->method("isAccessTokenExpired")->willReturn(true);
$this->googleClientMock->method("getRefreshToken")->willReturn(false);
$this->googleClientMock->method("fetchAccessTokenWithAuthCode")->willReturn(
CalendarControllerTest::ACCESS_TOKEN
);
$this->storageInterfaceCacheMock->method("setItem")->willReturn(true);
$this->googleClientMock->method("setAccessToken")->willReturnSelf();
$this->assertTrue($this->calendarClientService->authenticateClient($this->googleClientMock));
}
Related
I have created this resource in order to request a password reset token:
POST {{api_base_url}}/users/password/reset-request
Payload example
{
"email" :"user#example.com"
}
Everything works fine but the response doesn't serialize ResetPasswordRequestOutput to show the result. I got this response instead (as User type)
{
"#context": {
"#vocab": "http://dev.api.xxxxxxxxxx.com/docs.jsonld#",
"hydra": "http://www.w3.org/ns/hydra/core#",
"token": "ResetPasswordRequestOutput/token"
},
"#type": "User",
"#id": "/users/00000000-0000-0000-0000-000000000002",
"token": "ORGOsGh01sr8R91J"
}
Expected output: (of type: ResetPasswordRequestOutput)
{
"#type": "ResetPasswordRequestOutput",
"token": "ORGOsGh01sr8R91J"
}
Code:
The controller class of the custom operation:
final class ResetPasswordRequest {
private $validator;
private $entityManager;
public function __construct(ValidatorInterface $validator, EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
$this->validator = $validator;
}
public function __invoke(ResetPasswordRequestInput $data)
{
$this->validator->validate($data);
$userRepository = $this->entityManager->getRepository(User::class);
/** #var User $user */
$user = $userRepository->findOneBy(['email' => $data->email]);
if (!$user) {
throw new ItemNotFoundException("invalid token");
}
if ($user->getStatus() !== UserStatus::ACTIVE) {
throw new AccessDeniedException("user is inactive");
}
$generator = (new Factory())->getGenerator(new Strength(Strength::MEDIUM));
$token = $generator->generateString(16, 'BCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789');
$user->setToken($token);
return $user;
}
}
The data transofrmer that converts User object to ResetPasswordRequestOutput
namespace App\DataTransformer;
final class ResetPasswordRequestOutputDataTransformer implements DataTransformerInterface
{
private $normalizer;
public function __construct(NormalizerInterface $normalizer)
{
$this->normalizer = $normalizer;
}
/**
* {#inheritdoc}
*/
public function transform($data, string $to, array $context = [])
{
$output = new ResetPasswordRequestOutput();
$output->token = $data->getToken();
return $output;
}
/**
* {#inheritdoc}
*/
public function supportsTransformation($data, string $to, array $context = []): bool
{
return ResetPasswordRequestOutput::class === $to && $data instanceof User;
}
DTO object that holds input data
<?php
namespace App\Dto;
final class ResetPasswordRequestInput
{
/**
* #var string
*
* #Assert\Email
* #Groups({"user:write"})
*/
public $email;
}
DTO object that holds output data
namespace App\Dto;
final class ResetPasswordRequestOutput
{
/**
* #var string
*
* #Assert\NotNull()
* #Groups({"user:read"})
*/
public $token;
}
User Entity class with ApiResource annotation (partial) action=reset_password_request
/**
* #ApiResource(
* normalizationContext={"groups"={"user", "user:read"}},
* denormalizationContext={"groups"={"user", "user:write"}},
* collectionOperations={
* "get",
* "post",
* "reset_password_request" = {
* "method"= "POST",
* "read"=false,
* "path"="/users/password/reset-request",
* "controller"=ResetPasswordRequest::class,
* "status"=200,
* "input"=ResetPasswordRequestInput::class,
* "output"=ResetPasswordRequestOutput::class,
* "normalization_context"={"jsonld_embed_context"=false}
* },
My database schema consists mainly of the following entities: user and auth_token. Each user can have multiple auth_token's.
The problem: when selecting the currently authenticated user in a controller, the string field saltedPasswordHash is empty (""), although a value is set in the database. Getting saltedPasswordHash in the ApiKeyAuthenticator.php works (please have a look at the two TODO comments).
For whatever reason, selecting the email (string) or created (datetime) field works. Persisting new user entities with a saltedPasswordHash or selecting any other user item works fine.
An APIKeyAuthenticator is handling the authorization. When disabling firewall and authentication, everything works as expected. I included the source files below.
I'm using PHP 7.2.15-1 with mysql Ver 15.1 Distrib 10.3.13-MariaDB.
Security/ApiKeyAuthenticator.php
namespace App\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticatedToken;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Authentication\SimplePreAuthenticatorInterface;
class ApiKeyAuthenticator implements SimplePreAuthenticatorInterface
{
public function createToken(Request $request, $providerKey)
{
$apiKey = $request->headers->get('authToken');
if (!$apiKey) {
throw new BadCredentialsException();
// or to just skip api key authentication
// return null;
}
return new PreAuthenticatedToken(
'anon.',
$apiKey,
$providerKey
);
}
public function supportsToken(TokenInterface $token, $providerKey)
{
return $token instanceof PreAuthenticatedToken && $token->getProviderKey() === $providerKey;
}
public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey)
{
if (!$userProvider instanceof ApiKeyUserProvider) {
throw new \InvalidArgumentException(
sprintf(
'The user provider must be an instance of ApiKeyUserProvider (%s was given).',
get_class($userProvider)
)
);
}
$apiKey = $token->getCredentials();
$username = $userProvider->getUsernameForApiKey($apiKey);
if (!$username) {
// CAUTION: this message will be returned to the client
// (so don't put any un-trusted messages / error strings here)
throw new BadCredentialsException(
sprintf('API Key "%s" does not exist.', $apiKey)
);
}
$user = $userProvider->loadUserByAuthToken($apiKey);
if (!isset($user)) {
throw new BadCredentialsException(
sprintf('API Key "%s" does not exist.', $apiKey)
);
}
// TODO: HERE, THE $user->getSaltedPasswordHash() RETURNS THE CORRECT VALUE!
return new PreAuthenticatedToken(
$user, // TODO: with "new User()" instead, it works!
$apiKey,
$providerKey,
$user->getRoles()
);
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
return new Response(
// this contains information about *why* authentication failed
// use it, or return your own message
strtr($exception->getMessageKey(), $exception->getMessageData()),
401
);
}
}
Security/ApiKeyUserProvider.php
namespace App\Security;
use App\Entity\AuthToken;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\User;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
class ApiKeyUserProvider implements UserProviderInterface
{
/**
* #var EntityManagerInterface
*/
private $em;
/**
* ApiKeyUserProvider constructor.
* #param EntityManagerInterface $em
*/
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
public function getUsernameForApiKey($apiKey)
{
return $apiKey;
}
public function loadUserByUsername($username)
{
// TODO: Implement loadUserByUsername() method.
}
/**
* Auth token is used as username
*
* #param string $authToken
* #return null|UserInterface
*/
public function loadUserByAuthToken($authToken): ?UserInterface
{
if (!isset($authToken)) {
return null;
}
$token = $this->em
->getRepository(AuthToken::class)
->findOneBy(['id' => AuthToken::hex2dec($authToken)]);
if (!isset($token)) {
return null;
}
return $token->getUser();
}
public
function refreshUser(UserInterface $user)
{
// this is used for storing authentication in the session
// but in this example, the token is sent in each request,
// so authentication can be stateless. Throwing this exception
// is proper to make things stateless
throw new UnsupportedUserException();
}
public
function supportsClass($class)
{
return User::class === $class;
}
}
Entity/User.php
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\EquatableInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Validator\Constraints as Assert;
/**
* #ORM\Entity(repositoryClass="App\Repository\UserRepository")
*/
class User implements UserInterface, EquatableInterface
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* This needs to be nullable because the email includes the id of newly created users, which can only be obtained after inserting the new record.
* #ORM\Column(type="string", length=255, nullable=true, unique=true)
* #Assert\Length(max=255)
* #Assert\NotBlank()
*/
private $email;
/**
* Set null to disable login
* #ORM\Column(type="string", length=255, nullable=true)
* #Assert\Length(max=255)
* #Assert\NotBlank()
*/
private $saltedPasswordHash;
/**
* #ORM\Column(type="datetime")
*/
private $created;
// ...
/**
* #ORM\OneToMany(targetEntity="App\Entity\AuthToken", mappedBy="user", fetch="LAZY")
*/
private $authTokens;
/**
* #ORM\Column(type="string", length=5)
*/
private $role;
// ...
public function __construct()
{
$this->role = 'user';
$this->saltedPasswordHash = null;
}
public function getId()
{
return $this->id;
}
public function getSaltedPasswordHash(): ?string
{
return $this->saltedPasswordHash;
}
public function setSaltedPasswordHash(?string $saltedPasswordHash): self
{
$this->saltedPasswordHash = $saltedPasswordHash;
return $this;
}
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(?string $email): self
{
$this->email = $email;
return $this;
}
public function getCreated(): ?\DateTimeInterface
{
return $this->created;
}
public function setCreated(\DateTimeInterface $created): self
{
$this->created = $created;
return $this;
}
/**
* #return ArrayCollection
*/
public function getAuthTokens()
{
return $this->authTokens;
}
/**
* #param ArrayCollection $authTokens
* #return User
*/
public function setAuthTokens(ArrayCollection $authTokens): User
{
$this->authTokens = $authTokens;
return $this;
}
/**
* #param AuthToken $authToken
* #return User
*/
public function addAuthToken(AuthToken $authToken): User
{
$this->authTokens->add($authToken);
return $this;
}
/**
* #param AuthToken $authToken
* #return User
*/
public function removeAuthToken(AuthToken $authToken): User
{
$this->authTokens->removeElement($authToken);
return $this;
}
// ...
/**
* Returns the password used to authenticate the user.
*
* This should be the encoded password. On authentication, a plain-text
* password will be salted, encoded, and then compared to this value.
*
* #return string The password
*/
public function getPassword()
{
return $this->getSaltedPasswordHash();
}
/**
* Returns the salt that was originally used to encode the password.
*
* This can return null if the password was not encoded using a salt.
*
* #return string|null The salt
*/
public function getSalt()
{
// TODO: Implement getSalt() method.
}
/**
* Returns the username used to authenticate the user.
*
* #return string The username
*/
public function getUsername()
{
return $this->getEmail();
}
/**
* Removes sensitive data from the user.
*
* This is important if, at any given point, sensitive information like
* the plain-text password is stored on this object.
*/
public function eraseCredentials()
{
$this->setSaltedPasswordHash('');
}
/**
* #return mixed
*/
public function getRole()
{
return $this->role;
}
/**
* #param mixed $role
* #return User
*/
public function setRole($role): User
{
$this->role = $role;
return $this;
}
/**
* #return string
*/
public function __toString()
{
return "User " . $this->email;
}
/**
* The equality comparison should neither be done by referential equality
* nor by comparing identities (i.e. getId() === getId()).
*
* However, you do not need to compare every attribute, but only those that
* are relevant for assessing whether re-authentication is required.
*
* Also implementation should consider that $user instance may implement
* the extended user interface `AdvancedUserInterface`.
*
* https://stackoverflow.com/a/39884792/6144818
*
* #param UserInterface $user
* #return bool
*/
public function isEqualTo(UserInterface $user)
{
return (
$this->getUsername() == $user->getUsername()
) && (
$this->getRoles() == $user->getRoles()
);
}
// ...
}
Entitiy/AuthToken.php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="App\Repository\AuthTokenRepository")
*/
class AuthToken
{
/**
* #ORM\Id()
* #ORM\Column(type="decimal", precision=32, scale=0, options={"unsigned": true})
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\User", inversedBy="authTokens")
*/
private $user;
/**
* #ORM\Column(type="datetime")
*/
private $added;
/**
* #ORM\Column(type="datetime", nullable=true)
*/
private $lastSeen;
/**
* #ORM\Column(type="string", length=12, nullable=true)
*/
private $apiVersion;
/**
* #return string
*/
public function getId(): string
{
return $this->id;
}
/**
* #return string
*/
public function getHexId(): string
{
return $this->dec2hex($this->id);
}
/**
* #param mixed $id
*/
public function setId($id): void
{
$this->id = $id;
}
/**
* #param mixed $id
* #throws \Exception
*/
public function generateId(): void
{
$length = 32;
$str = "";
$characters = range('0', '9');
$max = count($characters) - 1;
for ($i = 0; $i < $length; $i++) {
$rand = random_int(0, $max);
$str .= $characters[$rand];
}
$this->id = $str;
}
/**
* #return mixed
*/
public function getUser()
{
return $this->user;
}
/**
* #param mixed $user
*/
public function setUser($user): void
{
$this->user = $user;
}
/**
* #return mixed
*/
public function getAdded()
{
return $this->added;
}
/**
* #param mixed $added
*/
public function setAdded($added): void
{
$this->added = $added;
}
/**
* #return mixed
*/
public function getLastSeen()
{
return $this->lastSeen;
}
/**
* #param mixed $lastSeen
*/
public function setLastSeen($lastSeen): void
{
$this->lastSeen = $lastSeen;
}
public function getApiVersion(): ?string
{
return $this->apiVersion;
}
public function setApiVersion(string $apiVersion): self
{
$this->apiVersion = $apiVersion;
return $this;
}
public static function dec2hex(string $dec): string
{
$hex = '';
do {
$last = bcmod($dec, 16);
$hex = dechex($last) . $hex;
$dec = bcdiv(bcsub($dec, $last), 16);
} while ($dec > 0);
return $hex;
}
public static function hex2dec($hex)
{
$dec = '0';
$len = strlen($hex);
for ($i = 1; $i <= $len; $i++)
$dec = bcadd($dec, bcmul(strval(hexdec($hex[$i - 1])), bcpow('16', strval($len - $i))));
return $dec;
}
/**
* #return string
*/
public function __toString()
{
return "AuthToken " . $this->id . " (" . $this->user . ")";
}
}
This function of your Entity/User.php is the reason of your behavior:
/**
* Removes sensitive data from the user.
*
* This is important if, at any given point, sensitive information like
* the plain-text password is stored on this object.
*/
public function eraseCredentials()
{
$this->setSaltedPasswordHash('');
}
When authentication comes in play on Symfony, after the authentication, the AuthenticationProviderManager would call that eraseCredentials function, so it doesn't leak sensible information, or even worse, the sensible information does not end up in your session.
Just try to comment the setter in that function and you should have what you expect.
It just my thought i just want to do like the same, I haven't try this because of i haven't any idea about this means how it will be done.
For Example:
I have - mysite.com/view?user_id=12 , I don't want to show url's parameters (?user_id=12) on url, On the place of this, I want to show like
mysite.com/view?_signature=encryptedparameter
(like : mysite.com/view?_signature=853ef7a0ff5aea6f24152b1d1ed4d771)
853ef7a0ff5aea6f24152b1d1ed4d771 will denotes user_id=12.
If i have multiple parameters it would be like the same and i will have a single encrypted string and i will get all parameters with that string.
Yes but i am using in Laravel 5.1
assume if i have "mysite.com/view?user_id=12"
And i want "mysite.com/view?_signature=encryptedparameter"
Please fallow some steps
laravel>packages>myfolder>core
+=>folder
-=>file
+packages
+Folder
+core
+src
+Faundation
-Application.php
+Routing
-RoutingServiceProvider.php
-UriGenerator.php
+Support
+Middleware
-UrlParamRevealerMiddleware.php
+ServiceProvider
-CoreServiceProvider.php
-UriParamProtector.php
<<IN APPLICATION.PHP>>
<?php
namespace Folder\Core\Foundation;
use Illuminate\Events\EventServiceProvider;
use Folder\Core\Routing\RoutingServiceProvider;
use Illuminate\Foundation\Application as BaseApplication;
use Folder\Core\Support\ServiceProvider\CoreServiceProvider;
class Application extends BaseApplication
{
/**
* Register all of the base service providers.
*
* #return void
*/
protected function registerBaseServiceProviders()
{
$this->register(new EventServiceProvider($this));
$this->register(new CoreServiceProvider($this));
$this->register(new RoutingServiceProvider($this));
}
}
<<//IN APPLICATION.PHP>>
<<-RoutingServiceProvider>>
<?php
namespace Folder\Core\Routing;
use Folder\Core\Routing\UrlGenerator;
use Illuminate\Routing\RoutingServiceProvider as BaseServiceProvider;
class RoutingServiceProvider extends BaseServiceProvider
{
/**
* Register the URL generator service.
*
* #return void
*/
protected function registerUrlGenerator()
{
$this->app['url'] = $this->app->share(function ($app) {
$routes = $app['router']->getRoutes();
// The URL generator needs the route collection that exists on the router.
// Keep in mind this is an object, so we're passing by references here
// and all the registered routes will be available to the generator.
$app->instance('routes', $routes);
$url = $this->getUrlGenerator($routes);
$url->setSessionResolver(function () {
return $this->app['session'];
});
// If the route collection is "rebound", for example, when the routes stay
// cached for the application, we will need to rebind the routes on the
// URL generator instance so it has the latest version of the routes.
$app->rebinding('routes', function ($app, $routes) {
$app['url']->setRoutes($routes);
});
return $url;
});
}
/**
* Get the url generator instance.
*
* #param \Illuminate\Routing\RouteCollection $routes
* #return \Folder\Core\Routing\UrlGenerator
*/
protected function getUrlGenerator($routes)
{
$url = new UrlGenerator(
$routes,
$this->app->rebinding(
'request',
$this->requestRebinder()
)
);
return $url;
}
}
<</-RoutingServiceProvider>>
<<UriGenerator.php>>
<?php
namespace Folder\Core\Routing;
use ErrorException;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Auth;
use Illuminate\Routing\UrlGenerator as BaseUrlGenerator;
class UrlGenerator extends BaseUrlGenerator
{
/**
* Get the URL to a named route.
*
* #param string $name
* #param mixed $parameters
* #param bool $absolute
* #return string
*
* #throws \InvalidArgumentException
*/
public function route($name, $parameters = [], $absolute = true)
{
$encryptedParameters = $parameters;
if (Auth::guest() === false) {
$encryptedParameters = $this->encrypt($name, $parameters);
}
return parent::route($name, $encryptedParameters, $absolute);
}
/**
* Get the cryptic engine.
*
* #return \Folder\Core\Support\UrlParamEncrypter
*
* #throws \ErrorException
*/
protected function getCrypt()
{
$app = App::getInstance();
if (isset($app['urlencryptor'])) {
return $app['urlencryptor'];
}
throw new ErrorException('URL Encryptor was not found.');
}
/**
* Get the protector engine.
*
* #return #return \Folder\Core\Support\UrlParamProtector
*
* #throws \ErrorException
*/
protected function getProtector()
{
$app = App::getInstance();
if (isset($app['urlprotector'])) {
return $app['urlprotector'];
}
throw new ErrorException('URL Protector was not found.');
}
/**
* Encrypts the parameter passed as querystring in URL.
*
* #param array $parameters
* #return array
*/
protected function encrypt($routeName, $parameters = [])
{
if (! is_array($parameters)) {
return $parameters;
}
if (count($parameters) === 0) {
return [];
}
if (Auth::guest() === true) {
return $parameters;
}
$protected = $this->getProtector()->protect($routeName, $parameters);
return ['__signature' => $protected];
}
}
<<//UriGenerator.php>>
<<UrlParamRevealerMiddleware.php>>
<?php
namespace Folder\Core\Support\Middleware;
use Closure;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Auth;
use Folder\Core\Support\UrlParamProtector;
class UrlParamRevealerMiddleware
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
(! Auth::guest()) && App::make(UrlParamProtector::class)->reveal($request);
return $next($request);
}
}
<<//UrlParamRevealerMiddleware.php>>
<<CoreServiceProvider.php>>
<?php
namespace Folder\Core\Support\ServiceProvider;
use Illuminate\Support\ServiceProvider;
use Folder\Core\Support\UrlParamProtector;
class CoreServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* #return void
*/
public function boot()
{
$this->app->make('Illuminate\Contracts\Http\Kernel')
->pushMiddleware('Folder\Core\Support\Middleware\UrlParamRevealerMiddleware');
}
/**
* Register the service provider.
*
* #return void
*/
public function register()
{
$this->registerUrlProtector();
}
/**
* Register UrlProtector class.
*/
protected function registerUrlProtector()
{
$this->app->singleton('urlprotector', function () {
return new UrlParamProtector();
});
}
}
<<//CoreServiceProvider.php>>
<<-UriParamProtector.php>>
<?php
namespace Folder\Core\Support;
use Illuminate\Support\Str;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Session;
class UrlParamProtector
{
/**
* Session key.
*
* #var string
*/
protected $sessionKey = '__url_protector__';
/**
* Request class.
*
* #var \Illuminate\Http\Request
*/
protected $request;
/**
* Values those needs to be merged in request object.
*
* #var array
*/
protected $valuesToBeMerged;
/**
* Create and returns VALID RFC 4211 COMPLIANT
* Universally Unique IDentifiers (UUID) version 4.
*
* #return string
*/
protected function getNewGuid()
{
if (function_exists('com_create_guid') === true) {
return trim(com_create_guid(), '{}');
}
return sprintf(
'%04X%04X-%04X-%04X-%04X-%04X%04X%04X',
mt_rand(0, 65535),
mt_rand(0, 65535),
mt_rand(0, 65535),
mt_rand(16384, 20479),
mt_rand(32768, 49151),
mt_rand(0, 65535),
mt_rand(0, 65535),
mt_rand(0, 65535)
);
}
/**
* Create the route key.
*
* #param string $routeName
* #param array $parameters
* #return string
*/
protected function getRouteKey($routeName, array $parameters = [])
{
if (count($parameters) <= 0) {
$paramToString = '';
} else {
$paramToString = implode('-', array_map(
function ($k, $v) {
return $k . '-' . $v;
},
array_keys($parameters),
array_values($parameters)
));
}
$routeKey = 'route__' . $routeName . (empty($paramToString) ? '' : '-' . $paramToString);
return $routeKey;
}
/**
* Returns a GUID for a URL parameter.
*
* #param string $routeName
* #param array $parameters
* #return string
*/
public function protect($routeName, array $parameters)
{
$routeKey = $this->getRouteKey($routeName, $parameters);
if (Session::has($this->sessionKey . '.' . $routeKey) === false) {
$guid = Str::lower($this->getNewGuid());
Session::set($this->sessionKey . '.' . $routeKey, [
'guid' => $guid,
'loggedin_user_id' => (Auth::guest() ? 0 : Auth::user()->id),
'params' => $parameters,
]);
} else {
$guid = Session::get($this->sessionKey . '.' . $routeKey . '.guid');
}
return $guid;
}
/**
* Check whether guid passed is a valid one or not.
*
* #param string $guid
* #return boolean
*/
protected function isValidGuid($guid)
{
foreach (Session::get($this->sessionKey) as $key => $value) {
if (!isset($value['guid'])) {
list($innerKey, $val) = each($value);
} else {
$val = $value;
}
if ($val['guid'] === $guid) {
$this->valuesToBeMerged = $val['params'];
return true;
}
}
return false;
}
/**
* Check whether guid passed is a valid one or not.
*
* #param string $guid
* #return boolean
*/
public function isValidGuidForPost($guid)
{
foreach (Session::get($this->sessionKey) as $key => $value) {
if ($value['guid'] === $guid && Auth::user()->id === $value['loggedin_user_id']) {
$this->valuesToBeMerged = $value['params'];
return true;
}
}
return false;
}
/**
* Merge the request with our revealed values.
*/
protected function mergeRequest()
{
$this->request->merge($this->valuesToBeMerged);
}
/**
* Check whether a "__signature" is correct or not.
*
* #param \Illuminate\Http\Request $request
* #return boolean
*/
public function reveal(Request &$request)
{
$this->request = $request;
$guid = ($this->request->query('__signature') ? : false);
if ($guid === false) {
return false;
}
if ($this->isValidGuid($guid) === false) {
App::abort(400);
}
$this->mergeRequest();
}
}
<<//-UriParamProtector.php>>
==============================================
after that in "bootstrap>App.php"
use this=>
$app = new Folder\Core\Foundation\Application(
realpath(__DIR__.'/../')
);
instad of =>
$app = new Illuminate\Foundation\Application(
realpath(__DIR__.'/../')
);
in any link use "data-href"
<a data-href="route('modify_lead_status',['user_id' => $users->id])" href="javascript:void(0)"><i class=\"fa fa-random\"></i></a>"
and after that any controller,Routing,middleware will be same.....
>>composer auto-dump
<<Please check spealing be sure use same>>
i hope this is help full....thanks
I want to allow user creating oEmbed content to my website.
There is a lot of information how to implement it but none about creating a enpoint.
Quick question: is there some ready php code samples for creating (returning) oEmbed response for third party websites?
If no, how to create it? Ex youtube link looks like this:
http://www.youtube.com/oembed?url=http%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DiwGFalTRHDA
and the response is :
{
"html": "<iframe width=\"459\" height=\"344\" src=\"https://www.youtube.com/embed/iwGFalTRHDA?feature=oembed\" frameborder=\"0\" allowfullscreen></iframe>",
"thumbnail_height": 360,
"thumbnail_width": 480,
"provider_name": "YouTube",
"author_url": "https://www.youtube.com/user/KamoKatt",
"thumbnail_url": "https://i.ytimg.com/vi/iwGFalTRHDA/hqdefault.jpg",
"author_name": "KamoKatt",
"provider_url": "https://www.youtube.com/",
"type": "video",
"version": "1.0",
"width": 459,
"title": "Trololo",
"height": 344
}
My question is: how they know which video it is? They are using regexp to parse videoID?
What is 'best practice' for this kind of request? Should it be created as Controller, Service, Provider or how?
Finally I have created it like this:
I have created a service:
namespace AppBundle\Service;
use AppBundle\Entity\PlaceMarker;
use Doctrine\ORM\EntityManager;
use Symfony\Component\Config\Definition\Exception\Exception;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception as Ex;
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
class EmbedService
{
CONST THUMBNAIL_WIDTH = 400;
CONST THUMBNAIL_HEIGHT = 300;
CONST HTML_WIDTH = 459;
CONST HTML_HEIGHT = 344;
/**
* #var string
*/
protected $baseUrl;
/**
* #var array
*/
private $params = [];
/**
* #var EntityManager
*/
private $entityManager;
/** #var \Twig_Environment */
private $twig;
public function __construct($baseUrl, EntityManager $entityManager, \Twig_Environment $twig)
{
$this->baseUrl = strtolower($baseUrl);
$this->entityManager = $entityManager;
$this->twig = $twig;
}
/**
*
* #param Request $request
* #param null $format
* #throws \Exception
*/
public function handleRequest(Request $request, $format = null)
{
$this->params = [];
// $baseUrl = $request->getSchemeAndHttpHost();
/** handle url parameter */
$this->params['url'] = $request->query->get('url');
if (!$this->params['url']) {
throw new Ex\BadRequestHttpException('A URL parameter must be provided');
}
/** check if url matches site url */
$this->params['path'] = substr($request->query->get('url'), strlen($this->baseUrl));
if (substr(strtolower($request->query->get('url')), 0, strlen($this->baseUrl)) !== strtolower($this->baseUrl)) {
throw new Ex\BadRequestHttpException('This is not a valid URL parameter');
}
/** handle format parameters */
$this->params['format'] = $format;
if (null === $format) {
$this->params['format'] = $request->query->get('format', 'json');
}
if (!in_array($this->params['format'], ['json', 'xml'])) {
throw new \Exception('Disallowed format parameter \'' . $this->params['format'] . '\'');
}
$maxWidth = $request->query->get('maxwidth', null);
if (is_numeric($maxWidth) || is_null($maxWidth)) {
$this->params['maxWidth'] = $maxWidth;
}
$maxHeight = $request->query->get('maxheight', null);
if (is_numeric($maxHeight) || is_null($maxHeight)) {
$this->params['maxHeight'] = $maxHeight;
}
$this->params['attr'] = $this->matchRoute($this->params['path']);
}
public function matchRoute($path)
{
$route1 = new Route('/place-marker/{id}/show', ['controller' => 'PlaceMarkerController']);
$routes = new RouteCollection();
$routes->add('place-marker', $route1);
$context = new RequestContext('/');
$matcher = new UrlMatcher($routes, $context);
try {
return $matcher->match($path);
} catch (\Exception $ex) {
throw new Ex\NotFoundHttpException('No implemented!');
}
}
/**
* #return array
*/
public function create()
{
if (!$this->params) {
throw new Exception('Unable to create oEmbed. Did you use handleRequest() first?');
}
$oEmbed = [
'version' => '1.0',
'provider_url' => $this->baseUrl,
'provider_name' => 'Pixdor.com'
];
switch ($this->params['attr']['_route']) {
case 'place-marker':
/**
* #var PlaceMarker $pMarker
*/
$pMarker = $this->entityManager->getRepository(PlaceMarker::class)->find($this->params['attr']['id']);
$oEmbed['title'] = $pMarker->getName();
$oEmbed['url'] = $pMarker->getUrl();
$oEmbed['description'] = $pMarker->getDescription();
$oEmbed['caption'] = $pMarker->getCaption();
$oEmbed['html'] = $this->twig->render(':place_marker:embed.html.twig');
$oEmbed['width'] = self::HTML_WIDTH;
$oEmbed['height'] = self::HTML_HEIGHT;
$oEmbed['rich'] = 'photo';
break;
}
$oEmbed['thumbnail_width'] = isset($this->params['maxWidth']) ? $this->params['maxWidth'] : self::THUMBNAIL_WIDTH;
$oEmbed['thumbnail_height'] = isset($this->params['thumbnail_height']) ? $this->params['thumbnail_height'] : self::THUMBNAIL_HEIGHT;
$oEmbed['thumbnail_url'] = sprintf('http://placehold.it/%dx%d', $oEmbed['thumbnail_width'], $oEmbed['thumbnail_height']);
return $oEmbed;
}
/**
* Returns format type (json|xml)
* #return string
*/
public function getFormat()
{
if (!$this->params) {
throw new Exception('Unknown format type. Did you use handleRequest() first?');
}
return $this->params['format'];
}
}
services.yml
app.embed:
class: AppBundle\Service\EmbedService
arguments: ["%base_url%", "#doctrine.orm.entity_manager", "#twig"]
parameters.yml
base_url: "http://project.local"
next in the controller:
/**
* #Route("/oembed.{_format}",
* name="embed",
* defaults={"_format": null}
* )
* #param Request $request
* #param $_format
* #return \Symfony\Component\HttpFoundation\Response
* #throws \Exception
*/
public function generateAction(Request $request, $_format)
{
$oEmbed = $this->get('app.embed');
$oEmbed->handleRequest($request, $_format);
$view = View::create()
->setFormat($oEmbed->getFormat())
->setStatusCode(200)
->setData($oEmbed->create());
return $this->handleView($view);
}
you call it like this:
http://project.local/oembed?url=http://project.local/place-marker/4/show
I am using symfony 3. I already created some events and they work fine. But this new event is different. I must somehow send some additional parameters to subscriber. What is the best way to do that? I created phenstalk service to perform job. The controller dispatch an event like:
$dispatcher = $this->container->get('event_dispatcher');
$dispatcher->dispatch(Events::POWERPLANT_GET_DATA, new PowerPlantEvent($this->getPowerPlantAction($id)));
Call like this will not work, cuz I am missing some parameters.
and I have action in subscriber:
public function onPowerPlantGetData(PowerPlantEvent $event, $startDate, $endDate, $measurement, $field)
How to do that?
If you event uses optional parameters, you can make them optional in the constructor.
use Symfony\Component\EventDispatcher\Event;
final class PowerPlantEvent extends Event
{
/**
* #var PowerPlantAction
*/
private $powerPlantAction;
/**
* #var \DateTime
*/
private $startDate;
/**
* #var \DateTime
*/
private $endDate;
/**
* #var int
*/
private $measurement;
/**
* #var string
*/
private $field;
public function __construct(
$powerPlantAction,
$startDate = null,
$endDate = null,
$measurement = null,
$field = null
) {
$this->powerPlantAction = $powerPlantAction;
$this->startDate = $startDate;
$this->endDate = $endDate;
$this->measurement = $measurement;
$this->field = $field;
}
public function getPowerPlantAction()
{
return $this->powerPlantAction;
}
public function getStartDate()
{
return $this->startDate;
}
public function getEndDate()
{
return $this->endDate;
}
public function getMeasurement()
{
return $this->measurement;
}
public function getField()
{
return $this->field;
}
}
Then dispatching like this:
$this->eventDispatcher->dispatch(
Events::POWERPLANT_GET_DATA,
new PowerPlantEvent($this->getPowerPlantAction($id))
);
Or:
$this->eventDispatcher->dispatch(
Events::POWERPLANT_GET_DATA,
new PowerPlantEvent(
$this->getPowerPlantAction($id),
$startDate,
$endDate,
$measurement,
$field
)
);
In your subscriber just use those getters:
public function onPowerPlantGetData(PowerPlantEvent $powerPlantEvent)
{
$powerPlantEvent->getStartDate();
// ...
}
You should put all necessary data into Event itself and then you can access them in subscriber
For simplicity I made properties public and added all to __construct but it's up to you how you implement it. You might want to create new Event class for that
class PowerPlantEvent extends Event
{
/**
* #var \DateTime
*/
public $startDate;
/**
* #var \DateTime
*/
public $endDate;
/**
* #var Something
*/
public $measurement;
public $field;
/**
* PowerPlantEvent constructor.
*
* #param \DateTime $startDate
* #param \DateTime $endDate
* #param Something $measurement
* #param $field
*/
public function __construct(\DateTime $startDate, \DateTime $endDate, Something $measurement, $field)
{
$this->startDate = $startDate;
$this->endDate = $endDate;
$this->measurement = $measurement;
$this->field = $field;
}
}
and call it with
$dispatcher = $this->container->get('event_dispatcher');
$dispatcher->dispatch(
Events::POWERPLANT_GET_DATA,
new PowerPlantEvent($startDate, $endDate, $measurement, $field)
);
and access in subscriber (or event listener)
public function onPowerPlantGetData(PowerPlantEvent $event)
{
$startDate = $event->startDate; // you might want to implement getters for those
}