├── CHANGELOG.md
├── LICENSE
├── README.md
├── composer.json
├── docs
├── assets
│ ├── favicon.ico
│ ├── flame.svg
│ ├── github-dark-dimmed.css
│ └── hljs.js
├── commands.md
├── configuration.md
├── filters.md
├── helpers.md
├── index.md
├── installation.md
└── methods.md
├── infection.json.dist
├── mkdocs.yml
├── psalm.xml
├── psalm_autoload.php
├── src
├── Commands
│ ├── SignedUrlAlgorithms.php
│ └── SignedUrlPublish.php
├── Common.php
├── Config
│ ├── Registrar.php
│ ├── Services.php
│ └── SignedUrl.php
├── Exceptions
│ └── SignedUrlException.php
├── Filters
│ └── SignedUrl.php
├── Language
│ └── en
│ │ └── SignedUrl.php
└── SignedUrl.php
└── tests
├── CommonTest.php
└── SignedUrlTest.php
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 | All notable changes to this project will be documented in this file.
3 |
4 | ## [Unreleased]
5 |
6 | ## [2.1.0](https://github.com/michalsn/codeigniter-signed-url/compare/v1.0.0...v1.1.0) - 2023-09-08
7 |
8 | ### Enhancements
9 | - Added `token` option to set a randomly generated token of the specified length by @michalsn in #45
10 | - Added `redirectTo` option setting to set a URI path for redirection when filter URL validation fails by @michalsn in #46
11 |
12 | ## [2.0.0](https://github.com/michalsn/codeigniter-signed-url/compare/v1.1.1...v2.0.0) - 2023-08-26
13 |
14 | ### Fixes
15 | - Compatibility with CodeIgniter 4.4 by @michalsn in #38
16 |
17 | ### Enhancements
18 | - Default hashing algorithm has been changed from `sha1` to `sha256` by @michalsn in #38
19 |
20 | ## [1.1.1](https://github.com/michalsn/codeigniter-signed-url/compare/v1.1.0...v1.1.1) - 2023-04-05
21 |
22 | ### Bugs
23 | - Take `App::$indexPage` and `App::$baseURL` into consideration during URL verification by @michalsn in #22
24 |
25 | ## [1.1.0](https://github.com/michalsn/codeigniter-signed-url/compare/v1.0.0...v1.1.0) - 2022-12-31
26 |
27 | ### Enhancements
28 | - Autoload `signedurl` filter by @datamweb in #2
29 |
30 | ## [1.0.0] - 2022-12-28
31 | Initial release
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Michal Sniatala
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CodeIgniter Signed URL
2 |
3 | Prevent manual URL manipulation and auto expiry URLs.
4 |
5 | [](https://github.com/michalsn/codeigniter-signed-url/actions/workflows/phpunit.yml)
6 | [](https://github.com/michalsn/codeigniter-signed-url/actions/workflows/phpstan.yml)
7 | [](https://github.com/michalsn/codeigniter-signed-url/actions/workflows/deptrac.yml)
8 | [](https://coveralls.io/github/michalsn/codeigniter-signed-url?branch=develop)
9 |
10 |
11 | ## Installation
12 |
13 | composer require michalsn/codeigniter-signed-url
14 |
15 | ## Overview
16 |
17 | We can sign URLs very easy with two main methods that act similar to the helper functions known from CodeIgniter's URL helper.
18 |
19 | ```php
20 | echo signedurl()->siteUrl('controller/method?query=string');
21 | // https://example.com/controller/method?query=string&signature=signature-goes-here
22 | ```
23 |
24 | ```php
25 | echo signedurl()->setExpiration(DAY * 2)->urlTo('namedRoute', 12);
26 | // https://example.com/route/name/12?expiration=1671980371&signature=signature-goes-here
27 | ```
28 |
29 | ## Versions
30 |
31 | Versions are not compatible - URLs generated in one version of Signed URL will not work with another version.
32 |
33 | | CodeIgniter version | Signed URL version |
34 | |---------------------|--------------------|
35 | | `>= 4.4` | `2.*` |
36 | | `< 4.4` | `1.*` |
37 |
38 | ## Docs
39 |
40 | https://michalsn.github.io/codeigniter-signed-url
41 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "michalsn/codeigniter-signed-url",
3 | "description": "Signing URL functionality for CodeIgniter 4 framework",
4 | "license": "MIT",
5 | "type": "library",
6 | "keywords": ["codeigniter", "codeigniter4", "signed-url"],
7 | "authors": [
8 | {
9 | "name": "michalsn",
10 | "homepage": "https://github.com/michalsn",
11 | "role": "Developer"
12 | }
13 | ],
14 | "homepage": "https://github.com/michalsn/codeigniter-signed-url",
15 | "require": {
16 | "php": "^8.0"
17 | },
18 | "require-dev": {
19 | "codeigniter4/devkit": "^1.0",
20 | "codeigniter4/framework": "^4.4"
21 | },
22 | "minimum-stability": "dev",
23 | "prefer-stable": true,
24 | "autoload": {
25 | "psr-4": {
26 | "Michalsn\\CodeIgniterSignedUrl\\": "src"
27 | },
28 | "files": ["src/Common.php"]
29 | },
30 | "autoload-dev": {
31 | "psr-4": {
32 | "Tests\\": "tests"
33 | }
34 | },
35 | "config": {
36 | "allow-plugins": {
37 | "phpstan/extension-installer": true
38 | }
39 | },
40 | "scripts": {
41 | "analyze": [
42 | "phpstan analyze",
43 | "psalm",
44 | "rector process --dry-run"
45 | ],
46 | "sa": "@analyze",
47 | "ci": [
48 | "Composer\\Config::disableProcessTimeout",
49 | "@cs",
50 | "@deduplicate",
51 | "@inspect",
52 | "@analyze",
53 | "@test"
54 | ],
55 | "cs": "php-cs-fixer fix --ansi --verbose --dry-run --diff",
56 | "cs-fix": "php-cs-fixer fix --ansi --verbose --diff",
57 | "style": "@cs-fix",
58 | "deduplicate": "phpcpd app/ src/",
59 | "inspect": "deptrac analyze --cache-file=build/deptrac.cache",
60 | "mutate": "infection --threads=2 --skip-initial-tests --coverage=build/phpunit",
61 | "test": "phpunit"
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/docs/assets/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michalsn/codeigniter-signed-url/d858fba8d69baf03fd1f0ac9b544b478df1cf19e/docs/assets/favicon.ico
--------------------------------------------------------------------------------
/docs/assets/flame.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
--------------------------------------------------------------------------------
/docs/assets/github-dark-dimmed.css:
--------------------------------------------------------------------------------
1 | pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
2 | Theme: GitHub Dark Dimmed
3 | Description: Dark dimmed theme as seen on github.com
4 | Author: github.com
5 | Maintainer: @Hirse
6 | Updated: 2021-05-15
7 | Modified: 2022:12:27 by @michalsn
8 |
9 | Colors taken from GitHub's CSS
10 | */.hljs{color:#adbac7 !important;background-color:#22272e !important}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#f47067}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#dcbdfb}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#6cb6ff}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#96d0ff}.hljs-built_in,.hljs-symbol{color:#f69d50}.hljs-code,.hljs-comment,.hljs-formula{color:#768390}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#8ddb8c}.hljs-subst{color:#adbac7}.hljs-section{color:#316dca;font-weight:700}.hljs-bullet{color:#eac55f}.hljs-emphasis{color:#adbac7;font-style:italic}.hljs-strong{color:#adbac7;font-weight:700}.hljs-addition{color:#b4f1b4;background-color:#1b4721}.hljs-deletion{color:#ffd8d3;background-color:#78191b}
11 |
12 | [data-md-color-scheme="default"] {
13 | --md-default-fg-color--lightest: #575757;
14 | --md-default-fg-color--light: #959595;
15 | }
16 |
--------------------------------------------------------------------------------
/docs/assets/hljs.js:
--------------------------------------------------------------------------------
1 | window.document$.subscribe(() => {
2 | hljs.highlightAll();
3 | });
4 |
--------------------------------------------------------------------------------
/docs/commands.md:
--------------------------------------------------------------------------------
1 | # Commands
2 |
3 | Available options:
4 |
5 | - [publish](#publish)
6 | - [algorithms](#algorithms)
7 |
8 | ### publish
9 |
10 | This command will publish configuration file to the APP namespace.
11 |
12 | ```console
13 | php spark signedurl:publish
14 | ```
15 |
16 | ### algorithms
17 |
18 | This command will list all avaliable algorithms options to use with `$algorithm` config variable.
19 |
20 | ```console
21 | php spark signedurl:algorithms
22 | ```
23 |
24 | !!! warning
25 |
26 | If you're not sure what you're doing please stay with the default option.
27 |
--------------------------------------------------------------------------------
/docs/configuration.md:
--------------------------------------------------------------------------------
1 | # Configuration
2 |
3 | To make changes to the config file, we have to have our copy in the `app/Config/SignedUrl.php`. Luckily, this package comes with handy command that will make this easy.
4 |
5 | When we run:
6 |
7 | php spark signedurl:publish
8 |
9 | We will get our copy ready for modifications.
10 |
11 | ---
12 |
13 | Available options:
14 |
15 | - [$expiration](#expiration)
16 | - [$token](#token)
17 | - [$algorithm](#algorithm)
18 | - [$expirationKey](#expirationKey)
19 | - [$tokenKey](#tokenKey)
20 | - [$signatureKey](#signatureKey)
21 | - [$algorithmKey](#algorithmKey)
22 | - [$includeAlgorithmKey](#includeAlgorithmKey)
23 | - [$redirect](#redirect)
24 | - [$show404](#show404)
25 |
26 | ### $expiration
27 |
28 | This setting allows us to set a fixed time after which the signed URL will expire.
29 | It's number of seconds in unix timestamp that will be added to the current date.
30 |
31 | By default, this is set to `null`.
32 |
33 | ### $token
34 |
35 | This setting allows us to set a randomly generated token with given length.
36 | It is useful when you have very few changing parameters in the URL.
37 |
38 | By default, this is set to `null`.
39 |
40 | ### $algorithm
41 |
42 | This setting allows us to set algorithm that will be used during signing the URLs.
43 |
44 | By default, this is set to `sha256`.
45 |
46 | !!! note
47 |
48 | If you're not sure what you're doing please stay with the default option.
49 |
50 | You can see the list of all available options when running command:
51 |
52 | php spark signedurl:algorithms
53 |
54 | !!! warning
55 |
56 | When you don't include used algorithm to the query string (default), then changing algorithm will result with invalidating all the generated URLs.
57 |
58 | ### $expirationKey
59 |
60 | This is the name of the query string key, which will be responsible for storing the time after which the URL will expire.
61 |
62 | By default, this is set to `expires`.
63 |
64 | !!! note
65 |
66 | Whatever name you will choose, treat it as a restricted name and don't use it as a part of the query string in your code.
67 |
68 | ### $tokenKey
69 |
70 | This is the name of the query string key, which will be responsible for storing the token string.
71 |
72 | By default, this is set to `token`.
73 |
74 | !!! note
75 |
76 | Whatever name you will choose, treat it as a restricted name and don't use it as a part of the query string in your code.
77 |
78 | ### $signatureKey
79 |
80 | This is the name of the query string key, which will be responsible for storing the signature by which the validity of the entire URL will be checked.
81 |
82 | By default, this is set to `signature`.
83 |
84 | !!! note
85 |
86 | Whatever name you will choose, treat it as a restricted name and don't use it as a part of the query string in your code.
87 |
88 | ### $algorithmKey
89 |
90 | This is the name of the query string key, which will be responsible for storing the algorithm by which the validity of the entire URL will be checked.
91 |
92 | By default, this is set to `algorithm`.
93 |
94 | !!! note
95 |
96 | Whatever name you will choose, treat it as a restricted name and don't use it as a part of the query string in your code.
97 |
98 | ### $includeAlgorithmKey
99 |
100 | This setting determines if the algorithm will be included to the query string of the generated URL.
101 |
102 | By default, this is set to `false`.
103 |
104 | ### $redirectTo
105 |
106 | This setting is used in the Filter to determine whether we will redirect user to the given URI path with the `error`, when URL will not be valid or expired.
107 |
108 | By default, this is set to `null`.
109 |
110 | ### $redirect
111 |
112 | This setting is used in the Filter to determine whether we will redirect user to the previous page with the `error`, when URL will not be valid or expired.
113 |
114 | By default, this is set to `false`.
115 |
116 | ### $show404
117 |
118 | This setting is used in the Filter to determine whether we will show a 404 page, when URL will not be valid or expired.
119 |
120 | By default, this is set to `false`.
121 |
--------------------------------------------------------------------------------
/docs/filters.md:
--------------------------------------------------------------------------------
1 | # Filters
2 |
3 | ## Overview
4 |
5 | To validate signed URLs we can use build in filter. We can enable it in one simple step.
6 |
7 | Define when filter should be fired up. In the example below we will assume it will be used when the first segment of the url will contain `signed-urls` string.
8 |
9 | ```php
10 | // app/Config/Filters.php
11 | ['before' => ['signed-urls/*']],
22 | ];
23 | }
24 | ```
25 |
26 | ## Options
27 |
28 | By default, this filter will throw `SignedUrlException` when the URL won't be signed or will be expired. But there are other options, and we can enable them by editing the config file:
29 |
30 | * We can redirect to the previous page
31 | * Or show 404 page
32 |
33 | More info you can find in the [Configuration](configuration.md) page.
34 |
35 | !!! note
36 |
37 | Remember, that if the filter implementation doesn't suit you, you can always [create your own](https://codeigniter.com/user_guide/incoming/filters.html?highlight=filter#creating-a-filter), which will behave differently upon an error. You can also not use the filter at all and make the check in the controller.
38 |
--------------------------------------------------------------------------------
/docs/helpers.md:
--------------------------------------------------------------------------------
1 | # Helpers
2 |
3 | Available options:
4 |
5 | - [signedurl()](#signedurl)
6 |
7 | #### signedurl()
8 |
9 | This function returns the `SignedUrl` class instance.
10 |
11 | ```php
12 | signedurl()->setExpiration(DAY)->siteUrl('controller/method');
13 | ```
14 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | # CodeIgniter Signed URL Documentation
2 |
3 | This library makes it easy to sign URLs in CodeIgniter 4 framework. It can be used to **prevent manual URL manipulation** or to **auto expiry links** that have been given to the end user.
4 |
5 | ## Overview
6 |
7 | We can sign URLs very easy with two main methods that act similar to the helper functions known from CodeIgniter's URL helper.
8 |
9 | ```php
10 | echo signedurl()->siteUrl('controller/method?query=string');
11 | // https://example.com/controller/method?query=string&signature=signature-goes-here
12 | ```
13 |
14 | ```php
15 | echo signedurl()->setExpiration(DAY * 2)->urlTo('namedRoute', 12);
16 | // https://example.com/route/name/12?expiration=1671980371&signature=signature-goes-here
17 | ```
18 |
19 | ## Versions
20 |
21 | Versions are not compatible - URLs generated in one version of Signed URL will not work with another version.
22 |
23 | | CodeIgniter version | Signed URL version |
24 | |---------------------|--------------------|
25 | | `>= 4.4` | `2.*` |
26 | | `< 4.4` | `1.*` |
27 |
--------------------------------------------------------------------------------
/docs/installation.md:
--------------------------------------------------------------------------------
1 | # Installation
2 |
3 | - [Composer Installation](#composer-installation)
4 | - [Manual Installation](#manual-installation)
5 | - [Generate encryption key](#generate-encryption-key)
6 |
7 | ## Composer Installation
8 |
9 | The only thing you have to do is to run this command, and you're ready to go.
10 |
11 | ```console
12 | composer require michalsn/codeigniter-signed-url
13 | ```
14 |
15 | ## Manual Installation
16 |
17 | In the example below we will assume, that files from this project will be located in `app/ThirdParty/signed-url` directory.
18 |
19 | Download this project and then enable it by editing the `app/Config/Autoload.php` file and adding the `Michalsn\CodeIgniterSignedUrl` namespace to the `$psr4` array. You also have to add `Common.php` to the `$files` array, like in the below example:
20 |
21 | ```php
22 | APPPATH, // For custom app namespace
28 | 'Config' => APPPATH . 'Config',
29 | 'Michalsn\CodeIgniterSignedUrl' => APPPATH . 'ThirdParty/signed-url/src',
30 | ];
31 |
32 | ...
33 |
34 | public $files = [
35 | APPPATH . 'ThirdParty/signed-url/src/Common.php',
36 | ];
37 | ```
38 |
39 | ## Generate encryption key
40 |
41 | Make sure that you have generated the encryption key. If not, please run command:
42 |
43 | ```console
44 | php spark key:generate
45 | ```
46 |
47 | !!! warning
48 |
49 | Please remember that any change made to the `encryption key` after generating signed URLs will auto expire them.
50 |
51 |
52 |
--------------------------------------------------------------------------------
/docs/methods.md:
--------------------------------------------------------------------------------
1 | # Methods
2 |
3 | Available options:
4 |
5 | - [setExpiration()](#setExpiration)
6 | - [siteUrl()](#siteUrl)
7 | - [urlTo()](#urlTo)
8 | - [sign()](#sign)
9 | - [verify()](#verify)
10 |
11 | ### setExpiration()
12 |
13 | With this method we can set temporary value for expiration. The value set here will be reset when the: `siteUrl()`, `urlTo()` or `sign()` methods are called.
14 |
15 | This is number of seconds in unix timestamp that will be added to the current date.
16 |
17 | ```php
18 | service('signedurl')->setExpiration(DAY)->siteUrl('url');
19 | ```
20 |
21 | !!! note
22 |
23 | If you want the URLs to always be valid for a certain period of time, you can set time in the `$expiration` variable in the configuration file.
24 |
25 | ### siteUrl()
26 |
27 | This method is similar to the standard `site_url`, but it produces signed URL.
28 |
29 | ```php
30 | service('signedurl')->siteUrl('controller/method');
31 | ```
32 |
33 | ### urlTo()
34 |
35 | This method is similar to the standard `url_to`, but it produces signed URL.
36 |
37 | ```php
38 | service('signedurl')->urlTo('namedRoute', 'param');
39 | ```
40 |
41 | ### sign()
42 |
43 | With this method we can sign URI. Usually you won't be using this method directly, since it is used by other methods.
44 |
45 | ```php
46 | service('signedurl')->sign($uri);
47 | ```
48 |
49 | ### verify()
50 |
51 | With this method we can verify if given URL is properly signed and not expired if expiration timestamp was set during URL creation.
52 |
53 | ```php
54 | service('signedurl')->verify($request);
55 | ```
56 |
57 | The URL verification may take place automatically via Filter class, but you can also make it happen in your Controller instead. The choice is up to you.
58 |
--------------------------------------------------------------------------------
/infection.json.dist:
--------------------------------------------------------------------------------
1 | {
2 | "source": {
3 | "directories": [
4 | "src/"
5 | ],
6 | "excludes": [
7 | "Config",
8 | "Database/Migrations",
9 | "Views"
10 | ]
11 | },
12 | "logs": {
13 | "text": "build/infection.log"
14 | },
15 | "mutators": {
16 | "@default": true
17 | },
18 | "bootstrap": "vendor/codeigniter4/framework/system/Test/bootstrap.php"
19 | }
20 |
--------------------------------------------------------------------------------
/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: CodeIgniter Signed URL
2 | site_description: Documentation for Signed URL library for CodeIgniter 4 framework
3 |
4 | theme:
5 | name: material
6 | logo: assets/flame.svg
7 | favicon: assets/favicon.ico
8 | icon:
9 | repo: fontawesome/brands/github
10 | palette:
11 | - media: "(prefers-color-scheme: light)"
12 | scheme: default
13 | primary: indigo
14 | accent: indigo
15 | toggle:
16 | icon: material/brightness-7
17 | name: Switch to dark mode
18 | - media: "(prefers-color-scheme: dark)"
19 | scheme: slate
20 | primary: indigo
21 | accent: indigo
22 | toggle:
23 | icon: material/brightness-4
24 | name: Switch to light mode
25 | features:
26 | - navigation.instant
27 |
28 | extra:
29 | homepage: https://michalsn.github.io/codeigniter-signed-url
30 |
31 | social:
32 | - icon: fontawesome/brands/github
33 | link: https://github.com/michalsn/codeigniter-signed-url
34 | name: GitHub
35 |
36 | site_url: https://michalsn.github.io/codeigniter-signed-url/
37 | repo_url: https://github.com/michalsn/codeigniter-signed-url
38 | edit_uri: edit/develop/docs/
39 |
40 | markdown_extensions:
41 | - admonition
42 | - pymdownx.superfences
43 | - pymdownx.highlight:
44 | use_pygments: false
45 |
46 | extra_css:
47 | - assets/github-dark-dimmed.css
48 |
49 | extra_javascript:
50 | - https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.6.0/build/highlight.min.js
51 | - assets/hljs.js
52 |
53 | nav:
54 | - Home: index.md
55 | - Installation: installation.md
56 | - Configuration: configuration.md
57 | - Methods: methods.md
58 | - Helpers: helpers.md
59 | - Filters: filters.md
60 | - Commands: commands.md
61 |
--------------------------------------------------------------------------------
/psalm.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/psalm_autoload.php:
--------------------------------------------------------------------------------
1 | $item) {
25 | $tbody[] = [++$key, $item];
26 | }
27 |
28 | CLI::table($tbody, $thead);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Commands/SignedUrlPublish.php:
--------------------------------------------------------------------------------
1 | getNamespace('Michalsn\\CodeIgniterSignedUrl')[0];
22 |
23 | $publisher = new Publisher($source, APPPATH);
24 |
25 | try {
26 | $publisher->addPaths([
27 | 'Config/SignedUrl.php',
28 | ])->merge(false);
29 | } catch (Throwable $e) {
30 | $this->showError($e);
31 |
32 | return;
33 | }
34 |
35 | foreach ($publisher->getPublished() as $file) {
36 | $contents = file_get_contents($file);
37 | $contents = str_replace('namespace Michalsn\\CodeIgniterSignedUrl\\Config', 'namespace Config', $contents);
38 | $contents = str_replace('use CodeIgniter\\Config\\BaseConfig', 'use Michalsn\\CodeIgniterSignedUrl\\Config\\SignedUrl as BaseSignedUrl', $contents);
39 | $contents = str_replace('class SignedUrl extends BaseConfig', 'class SignedUrl extends BaseSignedUrl', $contents);
40 | file_put_contents($file, $contents);
41 | }
42 |
43 | CLI::write(CLI::color(' Published! ', 'green') . 'You can customize the configuration by editing the "app/Config/SignedUrl.php" file.');
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/Common.php:
--------------------------------------------------------------------------------
1 | [
16 | 'signedurl' => SignedUrl::class,
17 | ],
18 | ];
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/Config/Services.php:
--------------------------------------------------------------------------------
1 | verify($request);
46 | } catch (SignedUrlException $e) {
47 | if ($path = $signedUrl->shouldRedirectTo()) {
48 | return redirect()->to($path)->with('error', $e->getMessage());
49 | }
50 |
51 | if ($signedUrl->shouldRedirect()) {
52 | return redirect()->back()->with('error', $e->getMessage());
53 | }
54 |
55 | if ($signedUrl->shouldShow404()) {
56 | throw PageNotFoundException::forPageNotFound($e->getMessage());
57 | }
58 |
59 | throw $e;
60 | }
61 | }
62 |
63 | /**
64 | * We don't have anything to do here.
65 | *
66 | * @param array|null $arguments
67 | *
68 | * @return void
69 | */
70 | public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
71 | {
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/Language/en/SignedUrl.php:
--------------------------------------------------------------------------------
1 | 'Algorithm is incorrect, please run command: "php spark signedurl:algorithms" to see available options.',
5 | 'invalidAlgorithm' => 'Algorithm is invalid or not supported.',
6 | 'emptyExpirationKey' => 'Expiration key cannot be empty.',
7 | 'emptyTokenKey' => 'Token key cannot be empty.',
8 | 'emptySignatureKey' => 'Signature key cannot be empty.',
9 | 'emptyAlgorithmKey' => 'Algorithm key cannot be empty.',
10 | 'sameKeyNames' => 'Expiration, Token, Signature or Algorithm keys cannot share the same name.',
11 | 'emptyEncryptionKey' => 'Encryption key is missing, please run command: "php spark key:generate"',
12 | 'missingSignature' => 'This URL have to be signed.',
13 | 'urlNotValid' => 'This URL is not valid.',
14 | 'urlExpired' => 'This URL has expired.',
15 | ];
16 |
--------------------------------------------------------------------------------
/src/SignedUrl.php:
--------------------------------------------------------------------------------
1 | key = config('Encryption')->key;
22 |
23 | if ($this->config->algorithm === '' || $this->config->algorithm === '0' || ! in_array($this->config->algorithm, hash_hmac_algos(), true)) {
24 | throw SignedUrlException::forIncorrectAlgorithm();
25 | }
26 |
27 | if ($this->config->expirationKey === '' || $this->config->expirationKey === '0') {
28 | throw SignedUrlException::forEmptyExpirationKey();
29 | }
30 |
31 | if ($this->config->tokenKey === '' || $this->config->tokenKey === '0') {
32 | throw SignedUrlException::forEmptyTokenKey();
33 | }
34 |
35 | if ($this->config->signatureKey === '' || $this->config->signatureKey === '0') {
36 | throw SignedUrlException::forEmptySignatureKey();
37 | }
38 |
39 | if ($this->config->algorithmKey === '' || $this->config->algorithmKey === '0') {
40 | throw SignedUrlException::forEmptyAlgorithmKey();
41 | }
42 |
43 | if (count(array_unique([$this->config->expirationKey, $this->config->tokenKey, $this->config->signatureKey, $this->config->algorithmKey])) !== 4) {
44 | throw SignedUrlException::forSameKeyNames();
45 | }
46 |
47 | if (empty($this->key)) {
48 | throw SignedUrlException::forEmptyEncryptionKey();
49 | }
50 |
51 | $this->resetSettings();
52 | }
53 |
54 | /**
55 | * Reset settings between calls.
56 | */
57 | protected function resetSettings(): void
58 | {
59 | $this->tempExpiration = $this->config->expiration;
60 | }
61 |
62 | /**
63 | * Set the URL expiration time.
64 | */
65 | public function setExpiration(?int $sec): static
66 | {
67 | $this->tempExpiration = $sec;
68 |
69 | return $this;
70 | }
71 |
72 | /**
73 | * Similar to site_url() helper function but with ability of sign the URL.
74 | */
75 | public function siteUrl(array|string $relativePath): string
76 | {
77 | if (is_array($relativePath)) {
78 | $relativePath = implode('/', $relativePath);
79 | }
80 |
81 | $host = service('request')->getUri()->getHost();
82 |
83 | $config = config(App::class);
84 |
85 | $uri = new SiteURI($config, $relativePath, $host);
86 |
87 | return $this->sign($uri);
88 | }
89 |
90 | /**
91 | * Similar to url_to() helper function but with ability of sign the URL.
92 | *
93 | * @throws RouterException
94 | */
95 | public function urlTo(string $controller, int|string ...$args): string
96 | {
97 | if (! $route = route_to($controller, ...$args)) {
98 | $explode = explode('::', $controller);
99 |
100 | if (isset($explode[1])) {
101 | throw RouterException::forControllerNotFound($explode[0], $explode[1]);
102 | }
103 |
104 | throw RouterException::forInvalidRoute($controller);
105 | }
106 |
107 | return $this->siteUrl($route);
108 | }
109 |
110 | /**
111 | * Transform the URI to signed URL.
112 | */
113 | public function sign(URI $uri): string
114 | {
115 | if ($this->tempExpiration !== null) {
116 | $uri->addQuery($this->config->expirationKey, Time::now()->addSeconds($this->tempExpiration)->getTimestamp());
117 | }
118 |
119 | if ($this->config->token !== null) {
120 | helper('text');
121 | $uri->addQuery($this->config->tokenKey, random_string('alnum', $this->config->token));
122 | }
123 |
124 | if ($this->config->includeAlgorithmKey) {
125 | $uri->addQuery($this->config->algorithmKey, $this->config->algorithm);
126 | }
127 |
128 | $url = URI::createURIString($uri->getScheme(), $uri->getAuthority(), $uri->getPath(), $uri->getQuery(), $uri->getFragment());
129 | $signature = base64url_encode(hash_hmac($this->config->algorithm, $url, $this->key, true));
130 |
131 | $uri->addQuery($this->config->signatureKey, $signature);
132 |
133 | $url = URI::createURIString($uri->getScheme(), $uri->getAuthority(), $uri->getPath(), $uri->getQuery(), $uri->getFragment());
134 |
135 | $this->resetSettings();
136 |
137 | return $url;
138 | }
139 |
140 | /**
141 | * Verify if URL is properly signed
142 | *
143 | * @throws SignedUrlException
144 | */
145 | public function verify(IncomingRequest $request): bool
146 | {
147 | $querySignature = $request->getGet($this->config->signatureKey);
148 | $queryExpiration = (int) $request->getGet($this->config->expirationKey);
149 | $queryAlgorithm = $request->getGet($this->config->algorithmKey) ?? $this->config->algorithm;
150 |
151 | if (empty($querySignature)) {
152 | throw SignedUrlException::forMissingSignature();
153 | }
154 |
155 | if (empty($queryAlgorithm) || ! in_array($queryAlgorithm, hash_hmac_algos(), true)) {
156 | throw SignedUrlException::forInvalidAlgorithm();
157 | }
158 |
159 | $querySignature = base64url_decode($querySignature);
160 |
161 | $uri = $request->getUri();
162 | $uri->stripQuery($this->config->signatureKey);
163 |
164 | $url = URI::createURIString('', base_url(), $uri->getPath(), $uri->getQuery(), $uri->getFragment());
165 | $signature = hash_hmac($queryAlgorithm, $url, $this->key, true);
166 |
167 | if (! hash_equals($querySignature, $signature)) {
168 | throw SignedUrlException::forUrlNotValid();
169 | }
170 |
171 | if (! empty($queryExpiration) && Time::now()->getTimestamp() > $queryExpiration) {
172 | throw SignedUrlException::forUrlExpired();
173 | }
174 |
175 | return true;
176 | }
177 |
178 | /**
179 | * Return redirectTo config option.
180 | */
181 | public function shouldRedirectTo(): ?string
182 | {
183 | return $this->config->redirectTo;
184 | }
185 |
186 | /**
187 | * Check if redirect option is enabled.
188 | */
189 | public function shouldRedirect(): bool
190 | {
191 | return $this->config->redirect;
192 | }
193 |
194 | /**
195 | * Check if show 404 option is enabled.
196 | */
197 | public function shouldShow404(): bool
198 | {
199 | return $this->config->show404;
200 | }
201 | }
202 |
--------------------------------------------------------------------------------
/tests/CommonTest.php:
--------------------------------------------------------------------------------
1 | key = hex2bin('6ece79d55cd04503600bd97520a0138a067690112fbfb44c704b0c626a7c62a2');
23 | }
24 |
25 | public function testSignedurl(): void
26 | {
27 | $this->assertInstanceOf(SignedUrl::class, signedurl());
28 | }
29 |
30 | public function testSignedUrlSiteUrl(): void
31 | {
32 | $this->assertSame(
33 | 'https://example.com/index.php/controller/method?signature=I0a1XPGLCTlRQo5c5f3LCz9R-tKP244-6pKCRV54AEk',
34 | signedurl()->siteUrl(['controller', 'method'])
35 | );
36 | }
37 |
38 | public function testSignedUrlSiteUrlWithExpirationTime(): void
39 | {
40 | Time::setTestNow('2022-12-25 14:59:11', 'UTC');
41 |
42 | $this->assertSame(
43 | 'https://example.com/index.php/controller/method?expires=1671980361&signature=9ZKau6qjzGOPY6unRPozK7dtZB1k_5hHQ9j3pwaQmzU',
44 | signedurl()->setExpiration(SECOND * 10)->siteUrl('controller/method')
45 | );
46 | }
47 |
48 | public function testSignedUrlTo(): void
49 | {
50 | $routes = service('routes');
51 | $routes->add('path/(:num)', 'myController::goto/$1', ['as' => 'gotoPage']);
52 |
53 | $this->assertSame(
54 | 'https://example.com/index.php/path/13?signature=niwm-RgYXkGSKzuEH1semjC6TU5T8WrHs7FvEEyD8uQ',
55 | signedurl()->urlTo('gotoPage', 13)
56 | );
57 | }
58 |
59 | public function testSignedUrlToWithExpirationTime(): void
60 | {
61 | $routes = service('routes');
62 | $routes->add('path/(:num)', 'myController::goto/$1', ['as' => 'gotoPage']);
63 |
64 | Time::setTestNow('2022-12-25 14:59:11', 'UTC');
65 |
66 | $this->assertSame(
67 | 'https://example.com/index.php/path/13?expires=1671980361&signature=pHMHFrXI74G5JuQc1mUUETznuUNnpHkwhAOsjazxlUw',
68 | signedurl()->setExpiration(SECOND * 10)->urlTo('gotoPage', 13)
69 | );
70 | }
71 |
72 | public function testSignedUrlToThrowControllerNotFound(): void
73 | {
74 | $this->expectException(RouterException::class);
75 | $this->expectExceptionMessage('Controller or its method is not found: Controller::method');
76 | signedurl()->urlTo('Controller::method', 13);
77 | }
78 |
79 | public function testSignedUrlToThrowInvalidRoute(): void
80 | {
81 | $this->expectException(RouterException::class);
82 | $this->expectExceptionMessage('The route for "gotoPage" cannot be found.');
83 |
84 | signedurl()->urlTo('gotoPage', 13);
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/tests/SignedUrlTest.php:
--------------------------------------------------------------------------------
1 | config = new App();
33 | $this->config->baseURL = 'http://example.com/';
34 | $this->config->indexPage = '';
35 |
36 | $_SERVER['HTTP_HOST'] = 'example.com';
37 | $_SERVER['REQUEST_URI'] = '/';
38 | $_SERVER['SCRIPT_NAME'] = '';
39 |
40 | config('Encryption')->key = hex2bin('6ece79d55cd04503600bd97520a0138a067690112fbfb44c704b0c626a7c62a2');
41 | }
42 |
43 | private function createRequest(?App $config = null, $body = null, ?string $path = null)
44 | {
45 | $config ??= new App();
46 |
47 | $factory = new SiteURIFactory($config, new Superglobals());
48 | $uri = $factory->createFromGlobals();
49 |
50 | if ($path !== null) {
51 | $uri->setPath($path);
52 | }
53 |
54 | $request = new IncomingRequest($config, $uri, $body, new UserAgent());
55 |
56 | Factories::injectMock('config', 'App', $config);
57 |
58 | return $request;
59 | }
60 |
61 | public function testIncorrectAlgorithm(): void
62 | {
63 | $this->expectException(SignedUrlException::class);
64 | $this->expectExceptionMessage('Algorithm is incorrect, please run command: "php spark signedurl:algorithms" to see available options.');
65 |
66 | $config = new SignedUrlConfig();
67 | $config->algorithm = '';
68 | new SignedUrl($config);
69 | }
70 |
71 | public function testMissingExpirationKey(): void
72 | {
73 | $this->expectException(SignedUrlException::class);
74 | $this->expectExceptionMessage('Expiration key cannot be empty.');
75 |
76 | $config = new SignedUrlConfig();
77 | $config->expirationKey = '';
78 | new SignedUrl($config);
79 | }
80 |
81 | public function testMissingTokenKey(): void
82 | {
83 | $this->expectException(SignedUrlException::class);
84 | $this->expectExceptionMessage('Token key cannot be empty.');
85 |
86 | $config = new SignedUrlConfig();
87 | $config->tokenKey = '';
88 | new SignedUrl($config);
89 | }
90 |
91 | public function testMissingSignatureKey(): void
92 | {
93 | $this->expectException(SignedUrlException::class);
94 | $this->expectExceptionMessage('Signature key cannot be empty.');
95 |
96 | $config = new SignedUrlConfig();
97 | $config->signatureKey = '';
98 | new SignedUrl($config);
99 | }
100 |
101 | public function testMissingAlgorithmKey(): void
102 | {
103 | $this->expectException(SignedUrlException::class);
104 | $this->expectExceptionMessage('Algorithm key cannot be empty.');
105 |
106 | $config = new SignedUrlConfig();
107 | $config->algorithmKey = '';
108 | new SignedUrl($config);
109 | }
110 |
111 | public function testSameKeyNames(): void
112 | {
113 | $this->expectException(SignedUrlException::class);
114 | $this->expectExceptionMessage('Expiration, Token, Signature or Algorithm keys cannot share the same name.');
115 |
116 | $config = new SignedUrlConfig();
117 | $config->expirationKey = 'same';
118 | $config->signatureKey = 'same';
119 | new SignedUrl($config);
120 | }
121 |
122 | public function testMissingEncryptionKey(): void
123 | {
124 | $this->expectException(SignedUrlException::class);
125 | $this->expectExceptionMessage('Encryption key is missing, please run command: "php spark key:generate"');
126 |
127 | config('Encryption')->key = '';
128 |
129 | $config = new SignedUrlConfig();
130 | new SignedUrl($config);
131 | }
132 |
133 | public function testSignWithNoExpirationInConfig(): void
134 | {
135 | $expectedUrl = 'https://example.com/path?query=string';
136 | $uri = new URI($expectedUrl);
137 |
138 | $config = new SignedUrlConfig();
139 | $signedUrl = new SignedUrl($config);
140 | $url = $signedUrl->sign($uri);
141 |
142 | $expectedUrl .= '&signature=ongZW4ttfJMqN757mwNXp5kx_3snwQhaDyI6JiV-5FM';
143 |
144 | $this->assertSame($expectedUrl, $url);
145 | }
146 |
147 | public function testSignWithIncludedAlgorithm(): void
148 | {
149 | $expectedUrl = 'https://example.com/path?query=string';
150 | $uri = new URI($expectedUrl);
151 |
152 | $config = new SignedUrlConfig();
153 | $config->includeAlgorithmKey = true;
154 |
155 | $signedUrl = new SignedUrl($config);
156 | $url = $signedUrl->sign($uri);
157 |
158 | $expectedUrl .= '&algorithm=sha256&signature=IldvSUQVJqTc8Gq47i0pEvuUYNjK_oRX1PAw-ZaXyM4';
159 |
160 | $this->assertSame($expectedUrl, $url);
161 | }
162 |
163 | public function testSignWithExpirationFromConfig(): void
164 | {
165 | $expectedUrl = 'https://example.com/path?query=string';
166 | $uri = new URI($expectedUrl);
167 |
168 | Time::setTestNow('2022-12-25 14:59:11', 'UTC');
169 |
170 | $config = new SignedUrlConfig();
171 | $config->expiration = SECOND * 10;
172 | $signedUrl = new SignedUrl($config);
173 | $url = $signedUrl->sign($uri);
174 |
175 | $expectedUrl .= '&expires=1671980361&signature=qohLh7fvypmDF9vktdJ6DBXH6fiKyBezNQblosN2sbA';
176 |
177 | $this->assertSame($expectedUrl, $url);
178 | }
179 |
180 | public function testSignWithOverwritenExpirationFromConfig(): void
181 | {
182 | $expectedUrl = 'https://example.com/path?query=string';
183 | $uri = new URI($expectedUrl);
184 |
185 | Time::setTestNow('2022-12-25 14:59:11', 'UTC');
186 |
187 | $config = new SignedUrlConfig();
188 | $config->expiration = SECOND * 10;
189 | $signedUrl = new SignedUrl($config);
190 | $url = $signedUrl->setExpiration(SECOND * 20)->sign($uri);
191 |
192 | $expectedUrl .= '&expires=1671980371&signature=IzHjHhkTOOBPTayZnk8f_ut0H4-3q0YrDb11slKPWWE';
193 |
194 | $this->assertSame($expectedUrl, $url);
195 | }
196 |
197 | public function testVerifyWithIndexPage(): void
198 | {
199 | $this->config->indexPage = 'index.php';
200 | $_SERVER['SCRIPT_NAME'] = '/index.php';
201 |
202 | $_SERVER['REQUEST_URI'] = '/path?query=string&signature=joVnKjlHYIeuLtyUW5SnQ-US2FPkWkykZnSmf2D_RZY';
203 |
204 | $request = $this->createRequest($this->config);
205 |
206 | $config = new SignedUrlConfig();
207 | $signedUrl = new SignedUrl($config);
208 |
209 | Time::setTestNow('2022-12-25 14:59:11', 'UTC');
210 |
211 | $this->assertTrue($signedUrl->verify($request));
212 | }
213 |
214 | public function testVerifyWithoutExpiration(): void
215 | {
216 | $_SERVER['REQUEST_URI'] = '/path?query=string&signature=iBEmAoQ9cPafZ3N05b9jEMj906Nd5nmSsJV7rKzFZSY';
217 |
218 | $request = $this->createRequest($this->config);
219 |
220 | $config = new SignedUrlConfig();
221 | $signedUrl = new SignedUrl($config);
222 |
223 | Time::setTestNow('2022-12-25 14:59:11', 'UTC');
224 |
225 | $this->assertTrue($signedUrl->verify($request));
226 | }
227 |
228 | public function testVerifyWithExpiration(): void
229 | {
230 | $_SERVER['REQUEST_URI'] = '/path?query=string&expires=1671980371&signature=9GNwvgcsK7jJUPpXe3MK5xFbE0rb5ZBHIjKc1qqWSgU';
231 |
232 | $request = $this->createRequest($this->config);
233 |
234 | $config = new SignedUrlConfig();
235 | $signedUrl = new SignedUrl($config);
236 |
237 | Time::setTestNow('2022-12-25 14:59:11', 'UTC');
238 |
239 | $this->assertTrue($signedUrl->verify($request));
240 | }
241 |
242 | public function testVerifyThrowExceptionForMissingSignature(): void
243 | {
244 | $this->expectException(SignedUrlException::class);
245 | $this->expectExceptionMessage('This URL have to be signed.');
246 |
247 | $_SERVER['REQUEST_URI'] = '/path?query=string';
248 |
249 | $request = $this->createRequest($this->config);
250 |
251 | $config = new SignedUrlConfig();
252 | $signedUrl = new SignedUrl($config);
253 | $signedUrl->verify($request);
254 | }
255 |
256 | public function testVerifyThrowExceptionForInvalidAlgorithm(): void
257 | {
258 | $this->expectException(SignedUrlException::class);
259 | $this->expectExceptionMessage('Algorithm is invalid or not supported.');
260 |
261 | $_SERVER['REQUEST_URI'] = '/path?query=string&algorithm=fake&signature=fake';
262 |
263 | $request = $this->createRequest($this->config);
264 |
265 | $config = new SignedUrlConfig();
266 | $signedUrl = new SignedUrl($config);
267 | $signedUrl->verify($request);
268 | }
269 |
270 | public function testVerifyThrowExceptionForUrlNotValid(): void
271 | {
272 | $this->expectException(SignedUrlException::class);
273 | $this->expectExceptionMessage('URL is not valid.');
274 |
275 | $_SERVER['REQUEST_URI'] = '/path?query=string123&expires=1671980371&signature=GSU95yKkJm3DqU5t3ZyYxUpgmBI';
276 |
277 | $request = $this->createRequest($this->config);
278 |
279 | $config = new SignedUrlConfig();
280 | $signedUrl = new SignedUrl($config);
281 | $signedUrl->verify($request);
282 | }
283 |
284 | public function testVerifyThrowExceptionForExpiredUrl(): void
285 | {
286 | $this->expectException(SignedUrlException::class);
287 | $this->expectExceptionMessage('This URL has expired.');
288 |
289 | $_SERVER['REQUEST_URI'] = '/path?query=string&expires=1671980371&signature=9GNwvgcsK7jJUPpXe3MK5xFbE0rb5ZBHIjKc1qqWSgU';
290 |
291 | $request = $this->createRequest($this->config);
292 |
293 | Time::setTestNow('2022-12-25 15:59:11', 'UTC');
294 |
295 | $config = new SignedUrlConfig();
296 | $signedUrl = new SignedUrl($config);
297 | $signedUrl->verify($request);
298 | }
299 |
300 | public function testShouldRedirectTo(): void
301 | {
302 | $config = new SignedUrlConfig();
303 | $signedUrl = new SignedUrl($config);
304 | $this->assertNull($signedUrl->shouldRedirectTo());
305 | }
306 |
307 | public function testShouldRedirect(): void
308 | {
309 | $config = new SignedUrlConfig();
310 | $signedUrl = new SignedUrl($config);
311 | $this->assertFalse($signedUrl->shouldRedirect());
312 | }
313 |
314 | public function testShouldShow404(): void
315 | {
316 | $config = new SignedUrlConfig();
317 | $signedUrl = new SignedUrl($config);
318 | $this->assertFalse($signedUrl->shouldShow404());
319 | }
320 | }
321 |
--------------------------------------------------------------------------------