diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..5ccc87c --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: spatie diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..b26e2ec --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,14 @@ +blank_issues_enabled: false +contact_links: + - name: Ask a question + url: https://github.com/spatie/lighthouse-php/discussions/new?category=q-a + about: Ask the community for help + - name: Request a feature + url: https://github.com/spatie/lighthouse-php/discussions/new?category=ideas + about: Share ideas for new features + - name: Report a security issue + url: https://github.com/spatie/lighthouse-php/security/policy + about: Learn how to notify us for sensitive bugs + - name: Report a bug + url: https://github.com/spatie/lighthouse-php/issues/new + about: Report a reproducable bug diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..30c8a49 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +# Please see the documentation for all configuration options: +# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + labels: + - "dependencies" \ No newline at end of file diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml new file mode 100644 index 0000000..e7e28b9 --- /dev/null +++ b/.github/workflows/dependabot-auto-merge.yml @@ -0,0 +1,32 @@ +name: dependabot-auto-merge +on: pull_request_target + +permissions: + pull-requests: write + contents: write + +jobs: + dependabot: + runs-on: ubuntu-latest + if: ${{ github.actor == 'dependabot[bot]' }} + steps: + + - name: Dependabot metadata + id: metadata + uses: dependabot/fetch-metadata@v1.3.5 + with: + github-token: "${{ secrets.GITHUB_TOKEN }}" + + - name: Auto-merge Dependabot PRs for semver-minor updates + if: ${{steps.metadata.outputs.update-type == 'version-update:semver-minor'}} + run: gh pr merge --auto --merge "$PR_URL" + env: + PR_URL: ${{github.event.pull_request.html_url}} + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} + + - name: Auto-merge Dependabot PRs for semver-patch updates + if: ${{steps.metadata.outputs.update-type == 'version-update:semver-patch'}} + run: gh pr merge --auto --merge "$PR_URL" + env: + PR_URL: ${{github.event.pull_request.html_url}} + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} diff --git a/.github/workflows/php-cs-fixer.yml b/.github/workflows/fix-php-code-style-issues.yml similarity index 52% rename from .github/workflows/php-cs-fixer.yml rename to .github/workflows/fix-php-code-style-issues.yml index 5811f0c..150750c 100644 --- a/.github/workflows/php-cs-fixer.yml +++ b/.github/workflows/fix-php-code-style-issues.yml @@ -1,21 +1,22 @@ -name: Check & fix styling +name: Fix PHP code style issues -on: [push] +on: + push: + paths: + - '**.php' jobs: - php-cs-fixer: + php-code-styling: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: ref: ${{ github.head_ref }} - - name: Run PHP CS Fixer - uses: docker://oskarstark/php-cs-fixer-ga - with: - args: --config=.php-cs-fixer.php --allow-risky=yes + - name: Fix PHP code style issues + uses: aglipanci/laravel-pint-action@1.0.0 - name: Commit changes uses: stefanzweifel/git-auto-commit-action@v4 diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index ffe3e86..9cbb4ff 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -6,31 +6,32 @@ jobs: test: runs-on: ${{ matrix.os }} strategy: - fail-fast: true + fail-fast: false matrix: - os: [ubuntu-latest, windows-latest] - php: [8.1, 8.0, 7.4] - stability: [prefer-lowest, prefer-stable] + os: [ubuntu-latest] + php: [8.2, 8.1] + stability: [prefer-stable] name: P${{ matrix.php }} - ${{ matrix.stability }} - ${{ matrix.os }} steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo coverage: none - name: Setup problem matchers run: | echo "::add-matcher::${{ runner.tool_cache }}/php.json" echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" + - name: Install dependencies run: composer update --${{ matrix.stability }} --prefer-dist --no-interaction - name: Execute tests - run: vendor/bin/phpunit + run: composer test diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php deleted file mode 100644 index ea229df..0000000 --- a/.php-cs-fixer.php +++ /dev/null @@ -1,35 +0,0 @@ -in([ - __DIR__ . '/src', - __DIR__ . '/tests', - ]) - ->name('*.php') - ->notName('*.blade.php') - ->ignoreDotFiles(true) - ->ignoreVCS(true); - -return (new PhpCsFixer\Config()) - ->setRules([ - '@PSR12' => true, - 'array_syntax' => ['syntax' => 'short'], - 'ordered_imports' => ['sort_algorithm' => 'alpha'], - 'no_unused_imports' => true, - 'not_operator_with_successor_space' => true, - 'trailing_comma_in_multiline' => true, - 'phpdoc_scalar' => true, - 'unary_operator_spaces' => true, - 'binary_operator_spaces' => true, - 'blank_line_before_statement' => [ - 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], - ], - 'phpdoc_single_line_var_spacing' => true, - 'phpdoc_var_without_name' => true, - 'method_argument_space' => [ - 'on_multiline' => 'ensure_fully_multiline', - 'keep_multiple_spaces_after_comma' => true, - ], - 'single_trait_insert_per_statement' => true, - ]) - ->setFinder($finder); diff --git a/README.md b/README.md index 1691802..307c2b4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,3 @@ - -[](https://supportukrainenow.org) - # Create secured URLs with a limited lifetime [![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/url-signer.svg?style=flat-square)](https://packagist.org/packages/spatie/url-signer) @@ -16,7 +13,7 @@ $urlSigner = new MD5UrlSigner('randomkey'); $urlSigner->sign('https://myapp.com', 30); -// => The generated url will be valid for 30 days +// => The generated url will be valid for 30 seconds ``` This will output an URL that looks like `https://myapp.com/?expires=xxxx&signature=xxxx`. @@ -28,8 +25,6 @@ your application can validate it with: $urlSigner->validate('https://myapp.com/?expires=xxxx&signature=xxxx'); ``` -Spatie is a webdesign agency in Antwerp, Belgium. You'll find an overview of all our open source projects [on our website](https://spatie.be/opensource). - ## Support us [](https://spatie.be/github-ad-click/url-signer) @@ -58,9 +53,9 @@ composer require spatie/url-signer A signer-object can sign URLs and validate signed URLs. A secret key is used to generate signatures. ```php -use Spatie\UrlSigner\MD5UrlSigner; +use Spatie\UrlSigner\Md5UrlSigner; -$urlSigner = new MD5UrlSigner('mysecretkey'); +$urlSigner = new Md5UrlSigner('mysecretkey'); ``` ### Generating URLs @@ -75,12 +70,12 @@ $urlSigner->sign('https://myapp.com', $expirationDate); // => The generated url will be valid for 10 days ``` -If an integer is provided as expiration date, the url will be valid for that amount of days. +If an integer is provided as expiration date, the url will be valid for that amount of seconds. ```php $urlSigner->sign('https://myapp.com', 30); -// => The generated url will be valid for 30 days +// => The generated url will be valid for 30 seconds ``` ### Validating URLs @@ -98,6 +93,7 @@ $urlSigner->validate('https://myapp.com/?expires=1439223344&signature=2d42f65bd0 ``` ## Writing custom signers + This packages provides a signer that uses md5 to generate signature. You can create your own signer by implementing the `Spatie\UrlSigner\UrlSigner`-interface. If you let your signer extend `Spatie\UrlSigner\BaseUrlSigner` you'll only need to provide the `createSignature`-method. @@ -107,10 +103,11 @@ signer by implementing the `Spatie\UrlSigner\UrlSigner`-interface. If you let yo The tests can be run with: ``` -$ vendor/bin/phpspec run +composer test ``` ## Integrations + To get started quickly in Laravel you can use the [spatie/laravel-url-signer](https://github.com/spatie/laravel-url-signer) package. ## Changelog @@ -127,6 +124,7 @@ If you've found a bug regarding security please mail [security@spatie.be](mailto ## Credits +- [Freek Van der Herten](https://github.com/freekmurze) - [Sebastian De Deyne](https://github.com/sebastiandedeyne) - [All Contributors](../../contributors) diff --git a/UPGRADING.md b/UPGRADING.md new file mode 100644 index 0000000..3450892 --- /dev/null +++ b/UPGRADING.md @@ -0,0 +1,4 @@ +## From v1 to v2 + +- the expiration passed to `sign` and `validate` is now in seconds instead of days +- all the rest of the API has stayed the same 👍 \ No newline at end of file diff --git a/composer.json b/composer.json index 53260a0..3c7deb2 100644 --- a/composer.json +++ b/composer.json @@ -11,6 +11,12 @@ "homepage": "https://github.com/spatie/url-signer", "license": "MIT", "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://github.com/freekmurze", + "role": "Developer" + }, { "name": "Sebastian De Deyne", "email": "sebastian@spatie.be", @@ -19,24 +25,33 @@ } ], "require": { - "php": "^7.4|^8.0", - "league/uri": "^6.0", - "league/uri-components": "^2.2" + "php": "^8.1" }, "require-dev": { - "phpunit/phpunit": "^9.5" + "pestphp/pest": "^1.22" }, "autoload": { "psr-4": { "Spatie\\UrlSigner\\": "src" } }, + "autoload-dev": { + "psr-4": { + "Spatie\\UrlSigner\\Tests\\": "tests" + } + }, "scripts": { - "test": "phpunit" + "test": "vendor/bin/pest", + "test-coverage": "vendor/bin/pest --coverage", + "format": "vendor/bin/pint" }, - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" + "config": { + "sort-packages": true, + "allow-plugins": { + "phpstan/extension-installer": true, + "pestphp/pest-plugin": true } - } + }, + "minimum-stability": "dev", + "prefer-stable": true } diff --git a/phpspec.yml b/phpspec.yml deleted file mode 100644 index 7fcfac7..0000000 --- a/phpspec.yml +++ /dev/null @@ -1,5 +0,0 @@ -suites: - urlsigner_suite: - namespace: Spatie\UrlSigner - psr4_prefix: Spatie\UrlSigner - src_path: src diff --git a/src/BaseUrlSigner.php b/src/BaseUrlSigner.php index 740ebad..58d342f 100644 --- a/src/BaseUrlSigner.php +++ b/src/BaseUrlSigner.php @@ -3,244 +3,128 @@ namespace Spatie\UrlSigner; use DateTime; -use League\Uri\Http; -use League\Uri\QueryString; -use Psr\Http\Message\UriInterface; use Spatie\UrlSigner\Exceptions\InvalidExpiration; use Spatie\UrlSigner\Exceptions\InvalidSignatureKey; +use Spatie\UrlSigner\Support\Url; abstract class BaseUrlSigner implements UrlSigner { - /** - * The key that is used to generate secure signatures. - * - * @var string - */ - protected $signatureKey; - - /** - * The URL's query parameter name for the expiration. - * - * @var string - */ - protected $expiresParameter; - - /** - * The URL's query parameter name for the signature. - * - * @var string - */ - protected $signatureParameter; - - /** - * @param string $signatureKey - * @param string $expiresParameter - * @param string $signatureParameter - * - * @throws InvalidSignatureKey - */ - public function __construct($signatureKey, $expiresParameter = 'expires', $signatureParameter = 'signature') - { - if ($signatureKey == '') { - throw new InvalidSignatureKey('The signature key is empty'); + public function __construct( + protected string $defaultSignatureKey, + protected string $expiresParameterName = 'expires', + protected string $signatureParameterName = 'signature' + ) { + if ($this->defaultSignatureKey == '') { + throw InvalidSignatureKey::signatureEmpty(); } - - $this->signatureKey = $signatureKey; - $this->expiresParameter = $expiresParameter; - $this->signatureParameter = $signatureParameter; } - /** - * Get a secure URL to a controller action. - * - * @param string $url - * @param \DateTime|int $expiration - * - * @throws InvalidExpiration - * - * @return string - */ - public function sign($url, $expiration) - { - $url = Http::createFromString($url); + abstract protected function createSignature( + string $url, + string $expiration, + string $signatureKey, + ): string; + + public function sign( + string $url, + int|DateTime $expiration, + string $signatureKey = null, + ): string { + $signatureKey ??= $this->defaultSignatureKey; $expiration = $this->getExpirationTimestamp($expiration); - $signature = $this->createSignature((string) $url, $expiration); - return (string) $this->signUrl($url, $expiration, $signature); + $signature = $this->createSignature($url, $expiration, $signatureKey); + + return $this->signUrl($url, $expiration, $signature); } - /** - * Add expiration and signature query parameters to an url. - * - * @param UriInterface $url - * @param string $expiration - * @param string $signature - * - * @return \League\Url\UrlImmutable - */ - protected function signUrl(UriInterface $url, $expiration, $signature) + protected function signUrl(string $url, string $expiration, $signature): string { - $query = QueryString::extract($url->getQuery()); - - $query[$this->expiresParameter] = $expiration; - $query[$this->signatureParameter] = $signature; - - return $url->withQuery($this->buildQueryStringFromArray($query)); + return Url::addQueryParameters($url, [ + $this->expiresParameterName => $expiration, + $this->signatureParameterName => $signature, + ]); } - /** - * Validate a signed url. - * - * @param string $url - * - * @return bool - */ - public function validate($url) + public function validate(string $url, string $signatureKey = null): bool { - $url = Http::createFromString($url); - - $query = QueryString::extract($url->getQuery()); + $signatureKey ??= $this->defaultSignatureKey; - if ($this->isMissingAQueryParameter($query)) { + $queryParameters = Url::queryParameters($url); + if ($this->isMissingAQueryParameter($queryParameters)) { return false; } - $expiration = $query[$this->expiresParameter]; + $expiration = $queryParameters[$this->expiresParameterName]; if (! $this->isFuture($expiration)) { return false; } - if (! $this->hasValidSignature($url)) { + if (! $this->hasValidSignature($url, $signatureKey)) { return false; } return true; } - /** - * Generate a token to identify the secure action. - * - * @param UriInterface|string $url - * @param string $expiration - * - * @return string - */ - abstract protected function createSignature($url, string $expiration); - - /** - * Check if a query is missing a necessary parameter. - * - * @param array $query - * - * @return bool - */ - protected function isMissingAQueryParameter(array $query) + protected function isMissingAQueryParameter(array $query): bool { - if (! isset($query[$this->expiresParameter])) { + if (! isset($query[$this->expiresParameterName])) { return true; } - if (! isset($query[$this->signatureParameter])) { + if (! isset($query[$this->signatureParameterName])) { return true; } return false; } - /** - * Check if a timestamp is in the future. - * - * @param int $timestamp - * - * @return bool - */ - protected function isFuture($timestamp) + protected function isFuture(int $timestamp): bool { - return ((int) $timestamp) >= (new DateTime())->getTimestamp(); + return $timestamp >= (new DateTime())->getTimestamp(); } - /** - * Retrieve the intended URL by stripping off the UrlSigner specific parameters. - * - * @param UriInterface $url - * - * @return UriInterface - */ - protected function getIntendedUrl(UriInterface $url) + protected function getIntendedUrl(string $url): string { - $intendedQuery = QueryString::extract($url->getQuery()); - - unset($intendedQuery[$this->expiresParameter]); - unset($intendedQuery[$this->signatureParameter]); - - return $url->withQuery($this->buildQueryStringFromArray($intendedQuery) ?? ''); + return Url::withoutParameters($url, [ + $this->expiresParameterName, + $this->signatureParameterName, + ]); } - /** - * Retrieve the expiration timestamp for a link based on an absolute DateTime or a relative number of days. - * - * @param \DateTime|int $expiration The expiration date of this link. - * - DateTime: The value will be used as expiration date - * - int: The expiration time will be set to X days from now - * - * @throws \Spatie\UrlSigner\Exceptions\InvalidExpiration - * - * @return string - */ - protected function getExpirationTimestamp($expiration) + protected function getExpirationTimestamp(DateTime|int $expirationInSeconds): string { - if (is_int($expiration)) { - $expiration = (new DateTime())->modify((int) $expiration.' days'); + if (is_int($expirationInSeconds)) { + $expirationInSeconds = (new DateTime())->modify($expirationInSeconds.' seconds'); } - if (! $expiration instanceof DateTime) { - throw new InvalidExpiration('Expiration date must be an instance of DateTime or an integer'); + if (! $expirationInSeconds instanceof DateTime) { + throw InvalidExpiration::wrongType(); } - if (! $this->isFuture($expiration->getTimestamp())) { - throw new InvalidExpiration('Expiration date must be in the future'); + if (! $this->isFuture($expirationInSeconds->getTimestamp())) { + throw InvalidExpiration::isInPast(); } - return (string) $expiration->getTimestamp(); + return (string) $expirationInSeconds->getTimestamp(); } - /** - * Determine if the url has a forged signature. - * - * @param UriInterface $url - * - * @return bool - */ - protected function hasValidSignature(UriInterface $url) - { - $query = QueryString::extract($url->getQuery()); + protected function hasValidSignature( + string $url, + string $signatureKey, + ): bool { + $queryParameters = Url::queryParameters($url); - $expiration = $query[$this->expiresParameter]; - $providedSignature = $query[$this->signatureParameter]; + $expiration = $queryParameters[$this->expiresParameterName]; + $providedSignature = $queryParameters[$this->signatureParameterName]; $intendedUrl = $this->getIntendedUrl($url); - $validSignature = $this->createSignature($intendedUrl, $expiration); + $validSignature = $this->createSignature($intendedUrl, $expiration, $signatureKey); return hash_equals($validSignature, $providedSignature); } - - /** - * Turn a key => value associate array into a query string. - * - * @param array $query - * - * @return string|null - */ - protected function buildQueryStringFromArray(array $query) - { - $buildQuery = []; - foreach ($query as $key => $value) { - $buildQuery[] = [$key, $value]; - } - - return QueryString::build($buildQuery); - } } diff --git a/src/Exceptions/InvalidExpiration.php b/src/Exceptions/InvalidExpiration.php index 1d49edc..b41ae4a 100644 --- a/src/Exceptions/InvalidExpiration.php +++ b/src/Exceptions/InvalidExpiration.php @@ -2,6 +2,17 @@ namespace Spatie\UrlSigner\Exceptions; -class InvalidExpiration extends \Exception +use Exception; + +class InvalidExpiration extends Exception { + public static function isInPast(): self + { + return new self('Expiration date must be in the future'); + } + + public static function wrongType(): self + { + return new self('Expiration date must be an instance of DateTime or an integer'); + } } diff --git a/src/Exceptions/InvalidSignatureKey.php b/src/Exceptions/InvalidSignatureKey.php index dd7f8bb..3bf0b10 100644 --- a/src/Exceptions/InvalidSignatureKey.php +++ b/src/Exceptions/InvalidSignatureKey.php @@ -2,6 +2,12 @@ namespace Spatie\UrlSigner\Exceptions; -class InvalidSignatureKey extends \Exception +use Exception; + +class InvalidSignatureKey extends Exception { + public static function signatureEmpty(): self + { + return new self('The signature key is empty'); + } } diff --git a/src/MD5UrlSigner.php b/src/MD5UrlSigner.php deleted file mode 100644 index ac5e54a..0000000 --- a/src/MD5UrlSigner.php +++ /dev/null @@ -1,21 +0,0 @@ -signatureKey}"); - } -} diff --git a/src/Md5UrlSigner.php b/src/Md5UrlSigner.php new file mode 100644 index 0000000..72bb144 --- /dev/null +++ b/src/Md5UrlSigner.php @@ -0,0 +1,14 @@ +assertInstanceOf(MD5UrlSigner::class, $urlSigner); - } - - /** @test */ - public function it_will_throw_an_exception_for_an_empty_signatureKey() - { - $this->expectException(InvalidSignatureKey::class); - - $urlSigner = new MD5UrlSigner(''); - } - - /** @test */ - public function it_returns_false_when_validating_a_forged_url() - { - $signedUrl = 'http://myapp.com/somewhereelse/?expires=4594900544&signature=41d5c3a92c6ef94e73cb70c7dcda0859'; - $urlSigner = new MD5UrlSigner('random_monkey'); - - $this->assertFalse($urlSigner->validate($signedUrl)); - } - - /** @test */ - public function it_returns_false_when_validating_an_expired_url() - { - $signedUrl = 'http://myapp.com/?expires=1123690544&signature=93e02326d7572632dd6edfa2665f2743'; - $urlSigner = new MD5UrlSigner('random_monkey'); - - $this->assertFalse($urlSigner->validate($signedUrl)); - } - - /** @test */ - public function it_returns_true_when_validating_an_non_expired_url() - { - $url = 'http://myapp.com'; - $expiration = 10000; - $urlSigner = new MD5UrlSigner('random_monkey'); - $signedUrl = $urlSigner->sign($url, $expiration); - - $this->assertTrue($urlSigner->validate($signedUrl)); - } - - public function unsignedUrlProvider() - { - return [ - ['http://myapp.com/?expires=4594900544'], - ['http://myapp.com/?signature=41d5c3a92c6ef94e73cb70c7dcda0859'], - ]; - } - - /** - * @test - * @dataProvider unsignedUrlProvider - */ - public function it_returns_false_when_validating_an_unsigned_url($unsignedUrl) - { - $urlSigner = new MD5UrlSigner('random_monkey'); - - $this->assertFalse($urlSigner->validate($unsignedUrl)); - } - - /** @test */ - public function it_does_a_strict_check_on_expirations() - { - $url = 'http://myapp.com'; - $expiration = '30'; - $urlSigner = new MD5UrlSigner('random_monkey'); - - $this->expectException(InvalidExpiration::class); - - $urlSigner->sign($url, $expiration); - } - - public function pastExpirationProvider() - { - return [ - [DateTime::createFromFormat('d/m/Y H:i:s', '10/08/2005 18:15:44')], - [-10], - ]; - } - - /** - * @test - * @dataProvider pastExpirationProvider - */ - public function it_doesnt_allow_expirations_in_the_past($pastExpiration) - { - $url = 'http://myapp.com'; - $urlSigner = new MD5UrlSigner('random_monkey'); - - $this->expectException(InvalidExpiration::class); - - $urlSigner->sign($url, $pastExpiration); - } - - /** @test */ - public function it_keeps_the_urls_query_parameters_intact() - { - $url = 'https://myapp.com/?foo=bar&baz=qux'; - $expiration = DateTime::createFromFormat( - 'd/m/Y H:i:s', - '10/08/2115 18:15:44', - new DateTimeZone('Europe/Brussels') - ); - $expected = 'https://myapp.com/?foo=bar&baz=qux&expires=4594900544&signature=728971d9fd0682793d2a1e96b734d949'; - - $urlSigner = new MD5UrlSigner('random_monkey'); - $signedUrl = $urlSigner->sign($url, $expiration); - - $this->assertStringContainsString('?foo=bar&baz=qux', $signedUrl); - $this->assertTrue($urlSigner->validate($signedUrl)); - } -} diff --git a/tests/Md5UrlSignerTest.php b/tests/Md5UrlSignerTest.php new file mode 100644 index 0000000..1cb27f9 --- /dev/null +++ b/tests/Md5UrlSignerTest.php @@ -0,0 +1,85 @@ +urlSigner = new Md5UrlSigner('random_monkey'); +}); + +it('can be initialized', function () { + expect($this->urlSigner)->toBeInstanceOf(UrlSigner::class); +}); + +it('will throw an exception fro an empty signature key', function () { + new Md5UrlSigner(''); +})->throws(InvalidSignatureKey::class); + +it('returns false when validating a forged url', function () { + $signedUrl = 'http://myapp.com/somewhereelse/?expires=4594900544&signature=41d5c3a92c6ef94e73cb70c7dcda0859'; + + expect($this->urlSigner->validate($signedUrl))->toBeFalse(); +}); + +it('returns false when validating an expired url', function () { + $signedUrl = 'http://myapp.com/?expires=1123690544&signature=93e02326d7572632dd6edfa2665f2743'; + + expect($this->urlSigner->validate($signedUrl))->toBeFalse(); +}); + +it('returns true when validating a non-expired url', function () { + $url = 'http://myapp.com'; + + $expiration = 10000; + $signedUrl = $this->urlSigner->sign($url, $expiration); + + expect($this->urlSigner->validate($signedUrl))->toBeTrue(); +}); + +it('returns false when validating an unsigned url', function (string $unsignedUrl) { + expect($this->urlSigner->validate($unsignedUrl))->toBeFalse(); +})->with('unsignedUrls'); + +it('does not allow expirations in the past', function ($pastExpiration) { + $url = 'http://myapp.com'; + + $this->urlSigner->sign($url, $pastExpiration); +})->with([ + [DateTime::createFromFormat('d/m/Y H:i:s', '10/08/2005 18:15:44')], + [-10], +])->throws(InvalidExpiration::class); + +it('will keep url query parameters intact', function () { + $url = 'https://myapp.com/?foo=bar&baz=qux'; + $expiration = DateTime::createFromFormat( + 'd/m/Y H:i:s', + '10/08/2115 18:15:44', + new DateTimeZone('Europe/Brussels') + ); + + $signedUrl = $this->urlSigner->sign($url, $expiration); + + expect($signedUrl)->toContain('?foo=bar&baz=qux'); + expect($this->urlSigner->validate($signedUrl))->toBeTrue(); +}); + +dataset('unsignedUrls', [ + ['http://myapp.com/?expires=4594900544'], + ['http://myapp.com/?signature=41d5c3a92c6ef94e73cb70c7dcda0859'], +]); + +it('using a custom key results in a different signed url', function () { + $signedUsingRegularKey = $this->urlSigner->sign('https://spatie.be', 5); + $signedUsingCustomKey = $this->urlSigner->sign('https://spatie.be', 5, 'custom-key'); + + expect($signedUsingRegularKey)->not()->toBe($signedUsingCustomKey); +}); + +it('can sign and validate urls with a custom key', function () { + $signedUsingCustomKey = $this->urlSigner->sign('https://spatie.be', 5, 'custom-key'); + + expect($this->urlSigner->validate($signedUsingCustomKey, 'custom-key'))->toBeTrue(); + expect($this->urlSigner->validate($signedUsingCustomKey, 'wrong-custom-key'))->toBeFalse(); +}); diff --git a/tests/Support/StrTest.php b/tests/Support/StrTest.php new file mode 100644 index 0000000..570c5dd --- /dev/null +++ b/tests/Support/StrTest.php @@ -0,0 +1,28 @@ +toBe($expected); +})->with([ + ['https://spatie.be?hey', '?', 'hey'], + ['https://spatie.be', '?', ''], + ['https://spatie.be?', '?', ''], + ['https://?spatie.be?', '?', 'spatie.be?'], + ['https://?spatie.be?', '!', ''], +]); + +it('can get the string before a string', function (string $string, string $after, string $expected) { + $actual = Str::before($string, $after); + + expect($actual)->toBe($expected); +})->with([ + ['https://spatie.be?hey', '?', 'https://spatie.be'], + ['https://spatie.be', '?', 'https://spatie.be'], + ['https://?spatie.be?', '?', 'https://'], + ['https://?spatie.be?', '!', 'https://?spatie.be?'], + ['?https://spatie.be?hey', '?', ''], + +]); diff --git a/tests/Support/UrlTest.php b/tests/Support/UrlTest.php new file mode 100644 index 0000000..eeebd24 --- /dev/null +++ b/tests/Support/UrlTest.php @@ -0,0 +1,20 @@ +toBe($actualParameters); +})->with([ + ['spatie.be?a=1&b=2', ['a' => '1', 'b' => '2']], + //['spatie.be', []], +]); + +it('can add query parameters to a URL', function (string $url, array $add, string $expectedUrl) { + $actualUrl = Url::addQueryParameters($url, $add); + + expect($expectedUrl)->toBe($actualUrl); +})->with([ + ['spatie.be', ['a' => 1, 'b' => 2], 'spatie.be?a=1&b=2'], +]);