├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .travis.yml ├── LICENSE.md ├── Makefile ├── README.md ├── composer.json ├── index.php ├── phpcs.xml.dist ├── phpstan.neon ├── phpunit.xml.dist ├── src ├── ApacheModRewriteGenerator.php ├── Engine.php ├── Exceptions │ ├── AmbiguousRelativeHostException.php │ ├── GenerationException.php │ └── UnhandledUrlException.php ├── GeneratorInterface.php ├── OctothorpeCommentTrait.php └── RewriteTypes.php └── tests └── tests └── ApacheIntegrationTest.php /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "composer" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | - package-ecosystem: "github-actions" 13 | directory: "/" 14 | schedule: 15 | interval: monthly 16 | time: "11:00" 17 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | - pull_request 3 | - push 4 | 5 | name: CI 6 | 7 | jobs: 8 | run: 9 | name: Tests 10 | 11 | strategy: 12 | matrix: 13 | operating-system: [ubuntu-latest] 14 | php-versions: ['8.1', '8.2', '8.3', '8.4'] 15 | 16 | runs-on: ${{ matrix.operating-system }} 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | 22 | - name: Install PHP 23 | uses: shivammathur/setup-php@v2 24 | with: 25 | php-version: ${{ matrix.php-versions }} 26 | 27 | - name: Install dependencies with composer 28 | run: composer install 29 | 30 | - name: Run tests 31 | run: make test 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | 3 | /vendor/ 4 | /clover.xml 5 | /composer.lock 6 | *.cache 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | sudo: false 3 | 4 | php: 5 | - 7.1 6 | - 7.2 7 | - 7.3 8 | - nightly 9 | - hhvm 10 | 11 | matrix: 12 | allow_failures: 13 | - php: nightly 14 | - php: hhvm 15 | 16 | install: composer install 17 | script: vendor/bin/phpunit --coverage-clover=coverage.clover 18 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright (c) 2015 Jesse Donat 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | test: cs phpstan 3 | ./vendor/bin/phpunit 4 | 5 | .PHONY: cs 6 | cs: 7 | ./vendor/bin/phpcs -s 8 | 9 | .PHONY: cbf 10 | cbf: 11 | ./vendor/bin/phpcbf 12 | 13 | .PHONY: phpstan 14 | phpstan: 15 | ./vendor/bin/phpstan 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mod Rewrite Rule Generator 2 | 3 | ![CI](https://github.com/donatj/RewriteRule-Generator/workflows/CI/badge.svg) 4 | [![Latest Stable Version](https://poser.pugx.org/donatj/rewrite-generator/v/stable)](https://packagist.org/packages/donatj/rewrite-generator) 5 | [![License](https://poser.pugx.org/donatj/rewrite-generator/license)](https://packagist.org/packages/donatj/rewrite-generator) 6 | 7 | Web Frontend: https://donatstudios.com/RewriteRule_Generator 8 | 9 | ## What it is 10 | 11 | * A simple builder of RewriteCond / RewriteRule's handling GET strings in any order. 12 | * Free and open source 13 | 14 | ## What it is not 15 | 16 | * Perfect 17 | 18 | ## Todo: 19 | 20 | * [ ] Nginx Option 21 | * This is proving to be more difficult than initially anticipated. Handling the GET parameters in **any order** may require the use of 22 | if statements which is frowned upon in the Nginx community and has performance overheads. 23 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "donatj/rewrite-generator", 3 | "description": "Server Rewrite Generator", 4 | "keywords": [ 5 | "apache", 6 | "rewrites" 7 | ], 8 | "type": "library", 9 | "license": "MIT", 10 | "authors": [ 11 | { 12 | "name": "Jesse Donat", 13 | "email": "donatj@gmail.com" 14 | } 15 | ], 16 | "require": { 17 | "php": ">=8.1" 18 | }, 19 | "autoload": { 20 | "psr-4": { 21 | "donatj\\RewriteGenerator\\": "src/" 22 | } 23 | }, 24 | "require-dev": { 25 | "corpus/coding-standard": "^0.8.0", 26 | "donatj/drop": "^1.0", 27 | "phpstan/extension-installer": "^1.4", 28 | "phpstan/phpstan": "^2.1", 29 | "phpstan/phpstan-phpunit": "^2.0", 30 | "phpunit/phpunit": "~7.5|~9.3", 31 | "squizlabs/php_codesniffer": "^3.5" 32 | }, 33 | "config": { 34 | "allow-plugins": { 35 | "dealerdirect/phpcodesniffer-composer-installer": true, 36 | "phpstan/extension-installer": true 37 | }, 38 | "sort-packages": true 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | https://donatstudios.com/RewriteRule_Generator 8 | * 9 | */ 10 | 11 | use donatj\RewriteGenerator\ApacheModRewriteGenerator; 12 | use donatj\RewriteGenerator\Engine; 13 | use donatj\RewriteGenerator\RewriteTypes; 14 | 15 | if( file_exists(__DIR__ . '/vendor/autoload.php') ) { 16 | require __DIR__ . '/vendor/autoload.php'; 17 | } 18 | 19 | // Avoiding the composer autoloader momentarily for backwards compatibility 20 | spl_autoload_register(function ( string $className ) { 21 | $parts = explode('\\', $className); 22 | array_shift($parts); 23 | array_shift($parts); 24 | $path = implode(DIRECTORY_SEPARATOR, $parts); 25 | 26 | require "src/{$path}.php"; 27 | }); 28 | 29 | $paramRewrites = $_POST['tabbed_rewrites'] ?? <<generate($paramRewrites, $paramType, $paramComments); 42 | 43 | ?> 44 | 45 |
46 |
47 | 53 | 56 | 57 |
58 | 59 |
60 | 61 |
62 | 63 |
64 |
65 | -------------------------------------------------------------------------------- /phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | src 5 | tests 6 | index.php 7 | 8 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: max 3 | paths: 4 | - src 5 | - tests 6 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | tests 6 | 7 | 8 | 9 | 10 | 11 | ./src 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/ApacheModRewriteGenerator.php: -------------------------------------------------------------------------------- 1 | escapeSubstitution($prefix . ltrim($toPath, '/')) . '?' . $toQuery; 68 | 69 | return match ($type) { 70 | RewriteTypes::SERVER_REWRITE => "{$output}&%{QUERY_STRING}", 71 | RewriteTypes::PERMANENT_REDIRECT => "{$output} [L,R=301]", 72 | default => throw new InvalidArgumentException("Unhandled RewriteType: {$type}", $type), 73 | }; 74 | } 75 | 76 | private function escapeSubstitution( string $input ) : string { 77 | $result = preg_replace('/[-\s%$\\\\]/', '\\\\$0', $input); 78 | if( $result === null ) { 79 | throw new \RuntimeException('preg_replace failed - ' . preg_last_error()); 80 | } 81 | 82 | return $result; 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /src/Engine.php: -------------------------------------------------------------------------------- 1 | generator->comment('ERROR: Malformed Line Skipped: ' . $line); 37 | $output .= "\n\n"; 38 | $errors++; 39 | 40 | continue; 41 | } 42 | 43 | try { 44 | if( $comments ) { 45 | $output .= $this->generator->lineComment($explodedLine[0], $explodedLine[1], $type); 46 | $output .= "\n"; 47 | } 48 | 49 | $output .= $this->generator->generateRewrite($explodedLine[0], $explodedLine[1], $type); 50 | $output .= "\n\n"; 51 | } catch( GenerationException $e ) { 52 | $output .= $this->generator->comment('ERROR: ' . $e->getMessage() . ': ' . $line); 53 | $output .= "\n"; 54 | $errors++; 55 | } 56 | } 57 | } 58 | 59 | if( $errors > 0 ) { 60 | $output = $this->generator->comment("WARNING: Input contained {$errors} error(s)") . "\n\n{$output}"; 61 | } 62 | 63 | $this->lastErrorCount = $errors; 64 | 65 | return rtrim($output) . "\n"; 66 | } 67 | 68 | public function getLastErrorCount() : int { 69 | return $this->lastErrorCount; 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/Exceptions/AmbiguousRelativeHostException.php: -------------------------------------------------------------------------------- 1 | %s', RewriteTypes::name($type), $from, $to); 12 | } 13 | 14 | /** 15 | * Generate a comment as a string 16 | * 17 | * @param string $text 18 | * @return string 19 | */ 20 | public function comment( string $text ) : string { 21 | return "# {$text}"; 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/RewriteTypes.php: -------------------------------------------------------------------------------- 1 | 'Rewrite', 13 | self::PERMANENT_REDIRECT => '301', 14 | default => throw new \InvalidArgumentException('invalid type', $type), 15 | }; 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /tests/tests/ApacheIntegrationTest.php: -------------------------------------------------------------------------------- 1 | generate($input, RewriteTypes::PERMANENT_REDIRECT, true); 20 | $this->assertSame(0, $engine->getLastErrorCount()); 21 | $this->assertSame($output301, $given); 22 | 23 | $given = $engine->generate($input, RewriteTypes::SERVER_REWRITE, true); 24 | $this->assertSame(0, $engine->getLastErrorCount()); 25 | $this->assertSame($outputRewrite, $given); 26 | } 27 | 28 | public function exampleProvider() : Generator { 29 | yield [ 30 | <<<'TAG' 31 | http://www.test.com/test.html http://www.test.com/spiders.html 32 | http://www.test.com/faq.html?faq=13&layout=bob http://www.test2.com/faqs.html 33 | http://www.test3.com/faq.html?faq=13&layout=bob bbq.html 34 | text/faq.html?faq=20 helpdesk/kb.php 35 | TAG 36 | , <<<'TAG' 37 | # 301 --- http://www.test.com/test.html => http://www.test.com/spiders.html 38 | RewriteRule ^test\.html$ /spiders.html? [L,R=301] 39 | 40 | # 301 --- http://www.test.com/faq.html?faq=13&layout=bob => http://www.test2.com/faqs.html 41 | RewriteCond %{HTTP_HOST} ^www\.test\.com$ 42 | RewriteCond %{QUERY_STRING} (?:^|&)faq\=13(?:$|&) 43 | RewriteCond %{QUERY_STRING} (?:^|&)layout\=bob(?:$|&) 44 | RewriteRule ^faq\.html$ http://www.test2.com/faqs.html? [L,R=301] 45 | 46 | # 301 --- http://www.test3.com/faq.html?faq=13&layout=bob => bbq.html 47 | RewriteCond %{QUERY_STRING} (?:^|&)faq\=13(?:$|&) 48 | RewriteCond %{QUERY_STRING} (?:^|&)layout\=bob(?:$|&) 49 | RewriteRule ^faq\.html$ /bbq.html? [L,R=301] 50 | 51 | # 301 --- text/faq.html?faq=20 => helpdesk/kb.php 52 | RewriteCond %{QUERY_STRING} (?:^|&)faq\=20(?:$|&) 53 | RewriteRule ^text/faq\.html$ /helpdesk/kb.php? [L,R=301] 54 | 55 | TAG 56 | , <<<'TAG' 57 | # Rewrite --- http://www.test.com/test.html => http://www.test.com/spiders.html 58 | RewriteRule ^test\.html$ /spiders.html?&%{QUERY_STRING} 59 | 60 | # Rewrite --- http://www.test.com/faq.html?faq=13&layout=bob => http://www.test2.com/faqs.html 61 | RewriteCond %{HTTP_HOST} ^www\.test\.com$ 62 | RewriteCond %{QUERY_STRING} (?:^|&)faq\=13(?:$|&) 63 | RewriteCond %{QUERY_STRING} (?:^|&)layout\=bob(?:$|&) 64 | RewriteRule ^faq\.html$ http://www.test2.com/faqs.html?&%{QUERY_STRING} 65 | 66 | # Rewrite --- http://www.test3.com/faq.html?faq=13&layout=bob => bbq.html 67 | RewriteCond %{QUERY_STRING} (?:^|&)faq\=13(?:$|&) 68 | RewriteCond %{QUERY_STRING} (?:^|&)layout\=bob(?:$|&) 69 | RewriteRule ^faq\.html$ /bbq.html?&%{QUERY_STRING} 70 | 71 | # Rewrite --- text/faq.html?faq=20 => helpdesk/kb.php 72 | RewriteCond %{QUERY_STRING} (?:^|&)faq\=20(?:$|&) 73 | RewriteRule ^text/faq\.html$ /helpdesk/kb.php?&%{QUERY_STRING} 74 | 75 | TAG 76 | , 77 | 78 | ]; 79 | 80 | yield [ 81 | 'http://www.site.ru/index.php/images/images/file/images/images/index.php /test/', 82 | '# 301 --- http://www.site.ru/index.php/images/images/file/images/images/index.php => /test/ 83 | RewriteRule ^index\.php/images/images/file/images/images/index\.php$ /test/? [L,R=301] 84 | ', 85 | '# Rewrite --- http://www.site.ru/index.php/images/images/file/images/images/index.php => /test/ 86 | RewriteRule ^index\.php/images/images/file/images/images/index\.php$ /test/?&%{QUERY_STRING} 87 | ', 88 | ]; 89 | 90 | yield [ 91 | 'http://foo.html http://bar.html', 92 | <<<'TAG' 93 | # 301 --- http://foo.html => http://bar.html 94 | RewriteCond %{HTTP_HOST} ^foo\.html$ 95 | RewriteRule ^$ http://bar.html/? [L,R=301] 96 | 97 | TAG 98 | , 99 | <<<'TAG' 100 | # Rewrite --- http://foo.html => http://bar.html 101 | RewriteCond %{HTTP_HOST} ^foo\.html$ 102 | RewriteRule ^$ http://bar.html/?&%{QUERY_STRING} 103 | 104 | TAG 105 | , 106 | ]; 107 | 108 | yield [ 109 | <<<'TAG' 110 | foo%20bar.html baz%20qux.html 111 | fo-o%2Fa%5Cbf~%3F-o%25o%2Aba%20r.ht%24ml boo%09berry.html 112 | TAG 113 | , 114 | <<<'TAG' 115 | # 301 --- foo%20bar.html => baz%20qux.html 116 | RewriteRule ^foo\ bar\.html$ /baz\ qux.html? [L,R=301] 117 | 118 | # 301 --- fo-o%2Fa%5Cbf~%3F-o%25o%2Aba%20r.ht%24ml => boo%09berry.html 119 | RewriteRule ^fo\-o/a\\bf~\?\-o%o\*ba\ r\.ht\$ml$ /boo\ berry.html? [L,R=301] 120 | 121 | TAG 122 | , 123 | <<<'TAG' 124 | # Rewrite --- foo%20bar.html => baz%20qux.html 125 | RewriteRule ^foo\ bar\.html$ /baz\ qux.html?&%{QUERY_STRING} 126 | 127 | # Rewrite --- fo-o%2Fa%5Cbf~%3F-o%25o%2Aba%20r.ht%24ml => boo%09berry.html 128 | RewriteRule ^fo\-o/a\\bf~\?\-o%o\*ba\ r\.ht\$ml$ /boo\ berry.html?&%{QUERY_STRING} 129 | 130 | TAG 131 | , 132 | ]; 133 | 134 | } 135 | 136 | /** 137 | * @dataProvider failureProvider 138 | */ 139 | public function test_failures( string $input, string $output, int $errorCount ) : void { 140 | $engine = new Engine(new ApacheModRewriteGenerator); 141 | 142 | $given = $engine->generate($input, RewriteTypes::PERMANENT_REDIRECT, true); 143 | $this->assertSame($errorCount, $engine->getLastErrorCount()); 144 | $this->assertSame($output, $given); 145 | } 146 | 147 | public function failureProvider() : Generator { 148 | yield [ 149 | 'a b c', 150 | <<<'TAG' 151 | # WARNING: Input contained 1 error(s) 152 | 153 | # ERROR: Malformed Line Skipped: a b c 154 | 155 | TAG 156 | , 1, 157 | ]; 158 | 159 | yield [ 160 | 'a.html http://bar.html', 161 | <<<'TAG' 162 | # WARNING: Input contained 1 error(s) 163 | 164 | # 301 --- a.html => http://bar.html 165 | # ERROR: Unclear relative host. When the "FROM" URI specifies a HOST the "TO" MUST specify a HOST as well.: a.html http://bar.html 166 | 167 | TAG 168 | , 1, 169 | ]; 170 | 171 | yield [ 172 | <<<'TAG' 173 | foo.html bar.html 174 | 175 | baz.html boo bar.html 176 | 177 | this line is just silly! 178 | 179 | is ok 180 | BAD 181 | 182 | is fine 183 | TAG 184 | , 185 | <<<'TAG' 186 | # WARNING: Input contained 3 error(s) 187 | 188 | # 301 --- foo.html => bar.html 189 | RewriteRule ^foo\.html$ /bar.html? [L,R=301] 190 | 191 | # ERROR: Malformed Line Skipped: baz.html boo bar.html 192 | 193 | # ERROR: Malformed Line Skipped: this line is just silly! 194 | 195 | # 301 --- is => ok 196 | RewriteRule ^is$ /ok? [L,R=301] 197 | 198 | # ERROR: Malformed Line Skipped: BAD 199 | 200 | # 301 --- is => fine 201 | RewriteRule ^is$ /fine? [L,R=301] 202 | 203 | TAG 204 | , 3, 205 | ]; 206 | 207 | yield [ 208 | <<<'TAG' 209 | foo.html#funk bar.html 210 | foo.html bar.html#fresh 211 | TAG 212 | , 213 | <<<'TAG' 214 | # WARNING: Input contained 2 error(s) 215 | 216 | # 301 --- foo.html#funk => bar.html 217 | # ERROR: "FROM" URI fragments cannot be handled - fragments are not sent in the request to the server.: foo.html#funk bar.html 218 | # 301 --- foo.html => bar.html#fresh 219 | # ERROR: "TO" URI fragments are not supported at this time.: foo.html bar.html#fresh 220 | 221 | TAG 222 | , 2, 223 | ]; 224 | } 225 | 226 | } 227 | --------------------------------------------------------------------------------