├── CHANGELOG.md ├── CODE-OF-CONDUCT.md ├── DOCUMENTATION.md ├── LICENSE ├── Plugin.php ├── README.md ├── ServiceProvider.php ├── assets ├── css │ ├── redirect.css │ ├── statistics.css │ └── test-lab.css ├── images │ ├── extensions │ │ └── Vdlp.RedirectConditions.svg │ └── icon.svg └── javascript │ └── test-lab.js ├── classes ├── BrandHelper.php ├── CacheManager.php ├── OptionHelper.php ├── PublishManager.php ├── RedirectConditionManager.php ├── RedirectManager.php ├── RedirectManagerSettings.php ├── RedirectMiddleware.php ├── RedirectRule.php ├── Sparkline.php ├── StatisticsHelper.php ├── TesterBase.php ├── TesterResult.php ├── contracts │ ├── CacheManagerInterface.php │ ├── PublishManagerInterface.php │ ├── RedirectConditionInterface.php │ ├── RedirectManagerInterface.php │ └── TesterInterface.php ├── exceptions │ ├── InvalidScheme.php │ ├── NoMatchForRequest.php │ ├── NoMatchForRule.php │ ├── RulesPathNotReadable.php │ ├── RulesPathNotWritable.php │ └── UnableToLoadRules.php ├── observers │ ├── RedirectObserver.php │ ├── SettingsObserver.php │ └── traits │ │ └── CanBeDisabled.php ├── testers │ ├── RedirectCount.php │ ├── RedirectFinalDestination.php │ ├── RedirectLoop.php │ ├── RedirectMatch.php │ └── ResponseCode.php └── util │ └── Str.php ├── composer.json ├── config └── config.php ├── console └── PublishRedirectsCommand.php ├── controllers ├── Categories.php ├── Extensions.php ├── Logs.php ├── Redirects.php ├── Statistics.php ├── TestLab.php ├── categories │ ├── _list_toolbar.htm │ ├── config_form.yaml │ ├── config_list.yaml │ ├── create.htm │ ├── index.htm │ └── update.htm ├── extensions │ ├── _installed.htm │ └── index.htm ├── logs │ ├── _list_toolbar.htm │ ├── config_filter.yaml │ ├── config_list.yaml │ └── index.htm ├── redirects │ ├── _field_statistics.htm │ ├── _list_toolbar.htm │ ├── _popup_actions.htm │ ├── _redirect_test.htm │ ├── _redirect_test_result.htm │ ├── _reorder_toolbar.htm │ ├── _sparkline.htm │ ├── _status_code_info.htm │ ├── _tab_logs.htm │ ├── _warning.htm │ ├── config_filter.yaml │ ├── config_form.yaml │ ├── config_import_export.yaml │ ├── config_list.yaml │ ├── config_relation.yaml │ ├── config_reorder.yaml │ ├── create.htm │ ├── export.htm │ ├── import.htm │ ├── index.htm │ ├── reorder.htm │ ├── request-log │ │ ├── _list_toolbar.htm │ │ ├── _modal.htm │ │ ├── columns.yaml │ │ └── config_list.yaml │ └── update.htm ├── statistics │ ├── _hits-per-day.htm │ ├── _loading-indicator.htm │ ├── _redirect-hits-per-month.htm │ ├── _score-board.htm │ ├── _top-crawlers-this-month.htm │ ├── _top-redirects-this-month.htm │ └── index.htm └── testlab │ ├── _test_button.htm │ ├── _tester_failed.htm │ ├── _tester_result.htm │ ├── _tester_result_items.htm │ └── index.htm ├── lang ├── de │ └── lang.php ├── en │ └── lang.php ├── es │ └── lang.php ├── fr │ └── lang.php ├── nl │ └── lang.php ├── ru │ └── lang.php └── sv │ └── lang.php ├── models ├── Category.php ├── Client.php ├── Redirect.php ├── RedirectExport.php ├── RedirectImport.php ├── RedirectLog.php ├── Settings.php ├── category │ ├── columns.yaml │ └── fields.yaml ├── client │ ├── columns.yaml │ └── fields.yaml ├── redirect │ ├── columns.yaml │ └── fields.yaml ├── redirectexport │ └── columns.yaml ├── redirectimport │ └── columns.yaml ├── redirectlog │ └── columns.yaml └── settings │ └── fields.yaml ├── reportwidgets ├── CreateRedirect.php ├── TopTenRedirects.php ├── createredirect │ ├── fields.yaml │ └── partials │ │ └── _widget.htm └── toptenredirects │ └── partials │ └── _widget.htm ├── routes.php └── updates ├── 20180718_0001_create_tables.php ├── 20180831_0002_upgrade_from_adrenth_redirect.php ├── 20181019_0003_add_ignore_query_parameters_to_redirects_table.php ├── 20181117_0004_add_redirect_timestamp_crawler_index_on_clients_table.php ├── 20181117_0005_add_month_year_crawler_index_on_clients_table.php ├── 20190404_0006_add_description_to_redirects_table.php ├── 20190704_0007_add_timestamp_crawler_index_on_clients_table.php ├── 20200408_0008_change_column_types_from_char_to_varchar.php ├── 20200414_0009_add_ignore_case_to_redirects_table.php ├── 20200414_0010_add_ignore_trailing_slash_to_redirects_table.php ├── 20200918_0011_refactor_redirects_logs_table.php ├── 20200918_0012_add_redirect_id_to_system_request_logs_table.php ├── 20231108_0013_add_keep_querystring_to_redirects_table.php └── version.yaml /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [3.1.11] - 2023-11-30 8 | 9 | * Add option to keep query string when redirecting. 10 | 11 | ## [3.1.10] - 2023-09-28 12 | 13 | * Fixed backend filtering for MySQL >= 8.0.3. 14 | 15 | ## [3.1.9] - 2023-06-12 16 | 17 | * Added environment variables for manipulating the navigation: 18 | * `VDLP_REDIRECT_SHOW_IMPORT` 19 | * `VDLP_REDIRECT_SHOW_EXPORT` 20 | * `VDLP_REDIRECT_SHOW_SETTINGS` 21 | * `VDLP_REDIRECT_SHOW_EXTENSION` 22 | * UI optimizations 23 | 24 | ## [3.1.8] - 2023-05-25 25 | 26 | * Add German translation (#104). 27 | 28 | ## [3.1.7] - 2023-04-19 29 | 30 | * Remove use of old BrandSetting constants (#103). 31 | 32 | ## [3.1.6] - 2023-03-28 33 | 34 | * Add support for October CMS 3.3 35 | * UI optimizations 36 | 37 | ## [3.1.5] - 2023-03-20 38 | 39 | * Prevent 'Uninitialized string offset 0' error. 40 | * Updated Chart.js library to v4.2.1 41 | 42 | ## [3.1.4] - 2023-02-14 43 | 44 | * Fix target field not loading properly. 45 | 46 | ## [3.1.3] - 2023-01-27 47 | 48 | * Minor code improvements. 49 | 50 | ## [3.1.2] - 2022-12-09 51 | 52 | * Fix daily stats labels when selecting month/year. 53 | 54 | ## [3.1.1] - 2022-06-24 55 | 56 | * Add description column to Redirects overview. 57 | 58 | ## [3.1.0] - 2022-06-11 59 | 60 | * Add support for October CMS 3.x. 61 | * Drop support for October CMS 2.x. 62 | * Minimum required PHP version is now 8.0.2 63 | 64 | ## [3.0.5] - 2022-06-11 65 | 66 | * Lock to October CMS version 2.x. Support for October CMS 3 will be added in v3.1.0. 67 | 68 | ## [3.0.4] - 2022-04-19 69 | 70 | * Improved compatibility/extensibility with other Plugins (Solves issue #90). 71 | 72 | ## [3.0.3] - 2022-03-15 73 | 74 | * Update plugin dependencies. 75 | 76 | ## [3.0.2] - 2022-02-20 77 | 78 | * Change version constraint for composer/installers. 79 | 80 | ## [3.0.1] - 2022-02-20 81 | 82 | * Add support for regular expression matches in target path. 83 | 84 | ## [3.0.0] - 2022-02-19 85 | 86 | * Drop support for October CMS 1.1 and lower. 87 | * Minimum required PHP version is now 7.4 88 | * Redirect to relative paths is now enabled by default. 89 | * Add database relation to system request logs. 90 | * When updating to `3.0.0` the table `vdlp_redirect_redirect_logs` will be removed due to schema changes. 91 | * Improvements to redirect request logs. 92 | * Improvements to redirect conditions logic. 93 | * Improvements to the statistics page. 94 | * Redirect Settings: 95 | * Relative paths setting is now **enabled** by default. 96 | * Testlab (Beta) is now **disabled** by default. 97 | * Redirect logging is now **disabled** by default. 98 | * Redirect statistics is now **disabled** by default. 99 | 100 | ## [2.6.0] 101 | 102 | * Update plugin dependencies. 103 | 104 | ## [2.5.13] 105 | 106 | * Fix database error when cache is being cleared before installation of plugin. 107 | 108 | ## [2.5.12] 109 | 110 | * Fix strpos() type error. 111 | 112 | ## [2.5.11] 113 | 114 | * Remove CMS support check. 115 | * Fix bad use of import. 116 | 117 | ## [2.5.10] 118 | 119 | * Add PHP 8.0 version constraint. 120 | * Add composer/installers package. 121 | 122 | ## [2.5.9] 123 | 124 | * Fix import in Plugin file. 125 | 126 | ## [2.5.8] 127 | 128 | * Improve redirect caching management (revised). 129 | 130 | ## [2.5.7] 131 | 132 | * Improve redirect caching management. 133 | 134 | ## [2.5.6] 135 | 136 | * Prevent connection exception when accessing settings in CLI mode. 137 | 138 | ## [2.5.5] 139 | 140 | * Suppress logging when redirect rules file is empty. 141 | 142 | ## [2.5.4] 143 | 144 | * Add support for symfony/stopwatch:^5.0 (version 4.0 is still supported). 145 | * Update Spanish language (thanks to Juan David M). 146 | * Hide button "From Request log" when request logging is disabled. 147 | 148 | ## [2.5.3] 149 | 150 | * Improve / fixes redirect rule caching (thanks to Eric Pfeiffer). 151 | * Update Spanish language (thanks to Juan David M). 152 | * Update Language files (help wanted!). 153 | 154 | ## [2.5.2] 155 | 156 | * Fix bug that causes re-writing the redirect rules file when hits are updated. 157 | 158 | ## [2.5.1] 159 | 160 | * Fixes issues with redirect rules file not being present. 161 | 162 | ## [2.5.0] 163 | 164 | * Add support for using relative paths. 165 | 166 | ## [2.4.1] 167 | 168 | * Add Redirect Extensions promo page. 169 | 170 | ## [2.4.0] 171 | 172 | * Skip requests with header "X-Requested-With: XMLHttpRequest". 173 | 174 | ## [2.3.2] 175 | 176 | * Improve error handling in plugin migration process. 177 | 178 | ## [2.3.1] 179 | 180 | * Fix SQLSTATE[42S22] error when installing plugin. 181 | 182 | ## [2.3.0] 183 | 184 | * Add new Redirect options: 185 | * Ignore Case 186 | * Ignore Trailing Slashes 187 | * Fix date field timezone issue (scheduled tab). 188 | * Fix warning dialog position (when scheduled redirect is not active). 189 | * Minor translation improvements (en, nl). 190 | 191 | ## [2.2.0] 192 | 193 | * Add "Cache-Control: no-store" header. This will prevent (modern) web browsers to cache the redirects. Very convenient when testing your redirects. 194 | * Add extra tab "Event logs" to Redirect update page. This tab shows a list with the related event logs of the redirect. 195 | * UI improvements. 196 | 197 | ## [2.1.1] 198 | 199 | * Update CHANGELOG. 200 | 201 | ## [2.1.0] 202 | 203 | * Improve exception handling #52. 204 | * Add support for league/csv:9.0+. 205 | * Improve caching mechanism #54. 206 | * Suppress cache flush log message. 207 | * Skip sparkline routes from being processed. 208 | 209 | ## [2.0.2] 210 | 211 | * Force type of vdlp.redirect::log_redirect_changes #53. 212 | * Apply config check to prevent log redirect changes #53. 213 | * Convert database column types (char to varchar) #51. 214 | 215 | ## [2.0.1] 216 | 217 | * Fix Middleware not being invoked in newer PHP versions. 218 | 219 | ## [2.0.0] 220 | 221 | * Drop support for PHP 7.0, only supports PHP 7.1.3+. 222 | * Most of the classes are made final. For extending use October CMS proposed solutions. 223 | * Auto-redirect creation for CMS/Static pages has been removed from this plugin. 224 | * The following events have been removed: 225 | * `vdlp.redirects.changed` 226 | * `vdlp.redirect.beforeRedirectSave` 227 | * `vdlp.redirect.beforeRedirectUpdate` 228 | * `vdlp.redirect.afterRedirectUpdate` 229 | * New events: 230 | * `vdlp.redirect.changed` 231 | * `vdlp.redirect.changed` 232 | -------------------------------------------------------------------------------- /CODE-OF-CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at octobercms@vdlp.nl. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /DOCUMENTATION.md: -------------------------------------------------------------------------------- 1 | # Vdlp.Redirect documentation 2 | 3 | This plugin should be easy to understand if you are familiar with the basics of the web. If you have issues setting up some redirects, please do not hesitate to contact me. 4 | 5 | ## Redirect types 6 | 7 | This plugins ships with two types of redirects: 8 | 9 | * **Exact**; performs an exact match on the Source path. 10 | * **Placeholders**; matches placeholders like {id} or {category} (like the defined routes in Symfony or Laravel framework). 11 | * **Regular expression**; Use regular expressions to match multiple patterns at once (advanced). 12 | 13 | ## Redirect target types 14 | 15 | This plugin allows you to redirect to the following types: 16 | 17 | * An internal path 18 | * An internal CMS Page 19 | * An internal Static Page (`RainLab.Pages` plugin) 20 | * An external URL 21 | 22 | ## Relative vs. Absolute URLs 23 | 24 | Both types are supported by the Redirect plugin. Absolute URLs are generated by default. 25 | This setting can be changed at the Redirect Settings page. 26 | 27 | In some cases it is necessary to use Relative URLs only. When using a reverse proxy for example. Or when you are 28 | hosting multiple domains on one single codebase. 29 | 30 | Example when Absolute URLs are enabled (default): 31 | 32 | ``` 33 | Source path: /path/from 34 | Target path: /path/to 35 | Result path when matched: https://example.com/path/to 36 | ``` 37 | 38 | Example when Relative URLs are enabled: 39 | 40 | ``` 41 | Source path: /path/from 42 | Target path: /path/to 43 | Result path when matched: /path/to 44 | ``` 45 | 46 | ## Scheme matching 47 | 48 | This plugin allows you to match requests from a `http://` scheme to a `https://` scheme and vice versa. 49 | 50 | ## Placeholders 51 | 52 | Every placeholder can be attached to a requirement. A requirement consists of a `placeholder`, `requirement` and an optional `replacement` value. 53 | 54 | Example: 55 | 56 | ``` 57 | Input path: 58 | /blog.php?category=cat&id=145 59 | 60 | Source path: 61 | /blog.php?category={category}&id={id} 62 | 63 | Target path: 64 | /blog/{category}/{id} 65 | 66 | Result path: 67 | /blog/cat/145 68 | ``` 69 | 70 | * The requirement for `{category}` would be: `[a-zA-Z]` or could be more specific like `(dog|cat|mouse)`. 71 | * The requirement for `{id}` would be: `[0-9]+`. 72 | 73 | **Replacement value** 74 | 75 | A requirement can also contain a replacement value. Provide this replacement value if you need to rewrite a certain placeholder to a static value. 76 | 77 | Example: 78 | 79 | The requirement for `{category}` is `(dog|cat|mouse)`, with replacement value `animals`. 80 | 81 | ``` 82 | Input path: 83 | /blog.php?category=mouse&id=1337 84 | 85 | Source path: 86 | /blog.php?category={category}&id={id} 87 | 88 | Target path: 89 | /blog/{category}/{id} 90 | 91 | Result: 92 | /blog/animals/1337 93 | ``` 94 | 95 | ![](https://i.imgur.com/928z7pI.png) 96 | 97 | Result in TestLab: 98 | 99 | ![](https://i.imgur.com/BswnUAo.png) 100 | 101 | ## Regular expression (advanced) 102 | 103 | For advanced users there's the Regular Expression matching logic. Please refer to the PHP.net `preg_match` manual. 104 | 105 | The actual `$matches` result from the `preg_match($sourcePath, $url, $matches)` function can be used in the target path and will be replaced with the matched value. 106 | 107 | Example (with matches replacement): 108 | 109 | ``` 110 | Input path: /foo/my-match 111 | Source Path: @/foo/(.*)?@ 112 | Target Path: /bar/{1} 113 | Result: /bar/my-match 114 | ``` 115 | 116 | ## Redirect Target 117 | 118 | You can select a CMS Page as a Redirect target. Placeholders are supported. Let's assume there is a page 'Blog' with the following URL: `/blog/:category/:subcategory`. 119 | 120 | It is possible to create a Redirect with placeholders that has this CMS Page as a target: 121 | 122 | ```` 123 | Redirect with: 124 | Source: `/blog.php?cat={category}&subcat={subcategory}` 125 | Placeholders: {category}, {subcategory} 126 | Target: CMS Page `Blog` 127 | 128 | Request path: /blog.php?cat=news&subcat=general 129 | Result: /blog/news/general 130 | ```` 131 | 132 | ## Events 133 | 134 | ### Fires events 135 | 136 | | Event | Payload | Description | 137 | | --- | --- | --- | 138 | | `vdlp.redirect.match` | none | When a request matched, right before the redirect response. 139 | | `vdlp.redirect.changed` | int[] $redirectId | When one or more redirects are changed. 140 | 141 | ### Listens to events 142 | 143 | | Event | Payload | Description | 144 | | --- | --- | --- | 145 | | `vdlp.redirect.toUrlChanged` | `string $oldUrl, string $newUrl` | Can be fired from a third-party plugin. 146 | 147 | ## Commands 148 | 149 | | Command | Description | 150 | | --- | --- | 151 | | `vdlp:redirect:publish-redirects` | Publish all redirects. | 152 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

Vdlp.Redirect

4 |

5 | 6 |

7 | Manage all your HTTP redirects with an easy to use GUI. This is an essential SEO plugin. 8 |

9 | 10 |

11 | 12 | 13 | 14 | 15 | 16 | 17 |

18 | 19 | ## The #1 Redirect plugin for October CMS 20 | 21 | This is the best Redirect-plugin for October CMS. With this plugin installed you can manage redirects directly from October CMS' beautiful interface. Many webmasters and SEO specialists use redirects to optimise their website for search engines. This plugin allows you to manage such redirects with a nice and user-friendly interface. 22 | 23 | ## History 24 | 25 | This plugin was originally build in 2016 by Alwin Drenth a Software Engineer at Van der Let & Partners. 26 | As of 2018 this plugin is re-distributed to the October CMS Marketplace with vendor name Vdlp.Redirect (formerly known as Adrenth.Redirect). 27 | 28 | The Redirect plugin will now be maintained by Van der Let & Partners and You (the open source community). 29 | 30 | ## What does this plugin offer? 31 | 32 | This plugin adds a 'Redirects' section to the main menu of October CMS. This plugin has a unique and fast matching algorithm to match your redirects before your website is being rendered. 33 | 34 | ## Features 35 | 36 | * **Quick** matching algorithm 37 | * A **test** utility for redirects 38 | * Matching using **placeholders** (dynamic paths) 39 | * Matching using **regular expressions** 40 | * **Exact** path matching 41 | * **Importing** and **exporting** redirect rules 42 | * **Schedule** redirects (e.g. active for 2 months) 43 | * Redirect to **external** URLs 44 | * Redirect to **internal** CMS pages 45 | * Redirect to relative or absolute URLs 46 | * Redirect **log** 47 | * **Categorize** redirects 48 | * **Statistics** 49 | * Hits per redirect 50 | * Popular redirects per month (top 10) 51 | * Popular crawlers per month (top 10) 52 | * Number of redirects per month 53 | * And more... 54 | * Multilingual ***(Need help translating! Contact us at octobercms@vdlp.nl)*** 55 | * Supports MySQL, SQLite and Postgres 56 | * HTTP status codes 301, 302, 303, 404, 410 57 | * Caching 58 | 59 | ## Supported database platforms 60 | 61 | * MySQL 62 | * Postgres 63 | * SQLite 64 | 65 | ## Requirements 66 | 67 | * October CMS 3 68 | * PHP version 8.0.2 or higher. 69 | * PHP extensions: `ext-curl` and `ext-json`. 70 | 71 | ## Supported HTTP status codes 72 | 73 | * `HTTP/1.1 301 Moved Permanently` 74 | * `HTTP/1.1 302 Found` 75 | * `HTTP/1.1 303 See Other` 76 | * `HTTP/1.1 404 Not Found` 77 | * `HTTP/1.1 410 Gone` 78 | 79 | ## Supported HTTP request methods 80 | 81 | * `GET` 82 | * `POST` 83 | * `HEAD` 84 | 85 | ## Performance 86 | 87 | All redirects are stored in the database and will be automatically "published" to a file which the internal redirect mechanism uses to determine if a certain request needs to be redirected. This is way faster than querying a database. 88 | 89 | This plugin is designed to be fast and should have no negative effect on the performance of your website. 90 | 91 | To gain maximum performance with this plugin: 92 | 93 | * Enable redirect caching using a "in-memory" caching method (see Caching). 94 | * Maintain your redirects frequently to keep the number of redirects as low as possible. 95 | * Try to use placeholders to keep your number of redirect low (less redirects is better performance). 96 | 97 | ## Caching 98 | 99 | If your website has a lot of redirects it is recommended to enable redirect caching. You can enable redirect caching in the settings panel of this plugin. 100 | 101 | Only cache drivers which support tagged cache are supported. So driver `file` and `database` are not supported. For this plugin database and file caching do not increase performance, but can actually have a negative influence on performance. So it is recommended to use an in-memory caching solution like `memcached` or `redis`. 102 | 103 | ### How caching works 104 | 105 | If caching is enabled (and supported) every request which is handled by this plugin will be cached. It will be stored with tag `Vdlp.Redirect`. 106 | 107 | When you modify a redirect all redirect cache will be invalidated automatically. It is also possible to manually clear the cache using the 'Clear cache' button in the Backend. 108 | 109 | ## Placeholders 110 | 111 | This plugin makes advantage of the `symfony/routing` package. So if you need more info on how to make placeholder requirements for your redirection URLs, please go to: https://symfony.com/doc/current/components/routing/introduction.html#usage 112 | 113 | ## Contribution 114 | 115 | Please feel free to [contribute](https://github.com/vdlp/oc-redirect-plugin) to this awesome plugin. 116 | 117 | ## Questions? Need help? 118 | 119 | If you have any question about how to use this plugin, please don't hesitate to contact us at octobercms@vdlp.nl. We're happy to help you. You can also visit the support forum and drop your questions/issues there. 120 | 121 | --- 122 | 123 | > If you love this quality plugin as much as we do, please [**rate our plugin**](http://octobercms.com/plugin/vdlp-redirect). 124 | 125 | --- 126 | -------------------------------------------------------------------------------- /ServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->bind(Contracts\RedirectManagerInterface::class, RedirectManager::class); 22 | $this->app->bind(Contracts\PublishManagerInterface::class, PublishManager::class); 23 | $this->app->bind(Contracts\CacheManagerInterface::class, CacheManager::class); 24 | 25 | $this->app->singleton(RedirectManager::class); 26 | $this->app->singleton(PublishManager::class); 27 | $this->app->singleton(CacheManager::class); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /assets/css/redirect.css: -------------------------------------------------------------------------------- 1 | .modal-content h3 { 2 | color: #da5700; 3 | margin-bottom: 8px; 4 | } 5 | 6 | #Form-field-Redirect-status_code-group .help-block.before-field { 7 | margin-bottom: 0; 8 | color: #2a3e51; 9 | } 10 | 11 | .status-code-info { 12 | cursor: pointer; 13 | margin-bottom: 10px; 14 | color: #da5700; 15 | } 16 | 17 | tr.special td { 18 | font-style: italic; 19 | } 20 | 21 | div.actions .btn { 22 | text-align: center; 23 | } 24 | 25 | .sparkline { 26 | width: 100px; 27 | height: 30px; 28 | background-size: 100px 30px; 29 | background-repeat: no-repeat; 30 | } 31 | 32 | .sparkline img { 33 | width: 100px; 34 | height: 30px; 35 | opacity: .8; 36 | } 37 | 38 | .form-sidebar .sparkline { 39 | width: 100%; 40 | height: 120px; 41 | background-size: 260px 120px; 42 | background-repeat: no-repeat; 43 | margin-top: 4px; 44 | } 45 | 46 | .form-sidebar .sparkline img { 47 | width: 100%; 48 | height: 120px; 49 | opacity: .8; 50 | } 51 | 52 | .scoreboard-item.title-value p { 53 | font-size: 20px; 54 | } 55 | 56 | ul.nav li a:focus, table.data tr:focus { 57 | outline:0 !important; 58 | box-shadow: none !important; 59 | } 60 | -------------------------------------------------------------------------------- /assets/css/statistics.css: -------------------------------------------------------------------------------- 1 | .row { 2 | margin-bottom: 30px; 3 | } 4 | 5 | .row .report-widget { 6 | position: relative; 7 | min-height: 155px; 8 | } 9 | 10 | .row .report-widget h3 { 11 | padding-bottom: 5px; 12 | border-bottom: 1px solid #cae1e2; 13 | margin-bottom: 15px; 14 | } 15 | 16 | .report-widget.redirect-hits-per-day { 17 | min-height: 375px; 18 | } 19 | 20 | .report-widget.current-active-redirects { 21 | min-height: 375px; 22 | } 23 | 24 | .report-widget.general { 25 | min-height: 400px; 26 | } 27 | 28 | .report-widget div[data-control=toolbar] { 29 | height: 125px; 30 | } 31 | 32 | .report-widget .loading-indicator-container { 33 | position: absolute; 34 | left: 0; 35 | top: 0; 36 | width: 100%; 37 | height: 100%; 38 | } 39 | 40 | .report-widget .loading-indicator { 41 | background: transparent; 42 | } 43 | -------------------------------------------------------------------------------- /assets/css/test-lab.css: -------------------------------------------------------------------------------- 1 | .test-lab { 2 | height: 100%; 3 | width: 100%; 4 | position: absolute; 5 | top: 0; 6 | } 7 | 8 | #testerResults { 9 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace; 10 | font-size: 12px; 11 | position: relative; 12 | top: 0; 13 | overflow: scroll; 14 | height: 85%; 15 | outline: none; 16 | } 17 | 18 | #testButton { 19 | text-align: center; 20 | } 21 | 22 | /* 23 | * Progress Bar 24 | **********************************************************************************************************************/ 25 | 26 | .progress { 27 | height: 35.7891px; 28 | margin-bottom: 4px; 29 | } 30 | 31 | .progress-bar { 32 | 33 | } 34 | 35 | /* 36 | * Tester Results 37 | **********************************************************************************************************************/ 38 | 39 | #testerResults a { 40 | outline: none; 41 | } 42 | 43 | #testerResults .failed i, 44 | #testerResults .failed .test { 45 | color: #ff0000; 46 | } 47 | 48 | #testerResults .passed i, 49 | #testerResults .passed .test { 50 | color: #009900; 51 | } 52 | 53 | #testerResults .row.heading .toolbar { 54 | font-family: sans-serif; 55 | } 56 | 57 | #testerResults .row.heading .toolbar .toolbar-item { 58 | margin-left: 4px; 59 | font-size: 12px; 60 | } 61 | 62 | #testerResults .row.heading { 63 | margin-bottom: 4px; 64 | } 65 | 66 | #testerResults .tester-result { 67 | background-color: rgb(233, 237, 243); 68 | border: 1px solid #fafbfd; 69 | border-radius: 4px; 70 | padding: 8px; 71 | margin-bottom: 20px; 72 | } 73 | 74 | /* 75 | * Loading Indicator 76 | **********************************************************************************************************************/ 77 | 78 | #testerResults .loading-indicator { 79 | font-size: 12px; 80 | } 81 | 82 | #testerResults .loading-indicator-container { 83 | min-height: 34px; 84 | } 85 | 86 | #testerResults .loading-indicator > span { 87 | background-size: 20px 20px; 88 | right: 0; 89 | left: auto; 90 | } 91 | 92 | #testerResults .loading-indicator-container .loading-indicator > div { 93 | right: 0; 94 | margin-right: 40px; 95 | } 96 | 97 | /* 98 | * Popup Loading Indicator 99 | **********************************************************************************************************************/ 100 | 101 | .popup-backdrop .popup-loading-indicator { 102 | width: 200px; 103 | height: 200px; 104 | margin-left: -100px; 105 | } 106 | 107 | .popup-backdrop .popup-loading-indicator:after { 108 | margin-left: 75px; 109 | } 110 | 111 | .popup-backdrop .popup-loading-indicator .btn { 112 | position: absolute; 113 | width: 100px; 114 | left: 50%; 115 | margin-left: -50px; 116 | text-align: center; 117 | top: 150px; 118 | } 119 | 120 | .popup-backdrop .popup-loading-indicator div { 121 | position: absolute; 122 | width: 200px; 123 | left: 50%; 124 | margin-left: -100px; 125 | text-align: center; 126 | top: 110px; 127 | } 128 | -------------------------------------------------------------------------------- /assets/images/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Tile 8 | 9 | 10 | A 11 | B 12 | 13 | -------------------------------------------------------------------------------- /assets/javascript/test-lab.js: -------------------------------------------------------------------------------- 1 | var testerShouldStop = false; 2 | 3 | function testerExecute(offset, total, button) { 4 | if (testerShouldStop) { 5 | testerDone(); 6 | testerShouldStop = false; 7 | return; 8 | } 9 | 10 | $.request('onTest', { 11 | data: { 12 | offset: offset 13 | }, 14 | success: function (data) { 15 | if (data.result === '' || typeof data.result === 'undefined') { 16 | testerDone(); 17 | updateStatusBar(total, total); 18 | return; 19 | } 20 | 21 | $('#testerResults').prepend(data.result); 22 | 23 | updateStatusBar(total, offset); 24 | 25 | if (offset + 1 !== total) { 26 | testerExecute(offset + 1, total, button); 27 | } 28 | }, 29 | error: function() { 30 | if (offset + 1 !== total) { 31 | testerExecute(offset + 1, total, button); 32 | } 33 | } 34 | }); 35 | } 36 | 37 | function testerDone() { 38 | $('#testButton').prop('disabled', false); 39 | 40 | var loader = $('#loader'); 41 | loader.removeClass('loading'); 42 | 43 | setTimeout(function () { 44 | loader.addClass('hidden'); 45 | }, 500); 46 | } 47 | 48 | function testerStart(button) { 49 | updateStatusBar(0); 50 | 51 | $('#testerResults').html(''); 52 | 53 | button.prop('disabled', true); 54 | 55 | var loader = $('#loader'); 56 | loader.removeClass('hidden'); 57 | loader.addClass('loading'); 58 | 59 | testerExecute(0, $('#redirectCount').val(), button); 60 | } 61 | 62 | function testerStop() { 63 | testerShouldStop = true; 64 | } 65 | 66 | function updateStatusBar(total, offset) { 67 | var width = 0; 68 | 69 | if (total > 0) { 70 | width = Math.ceil(100 / total * offset); 71 | } 72 | 73 | var progress = $('#progress'); 74 | progress.html(width + '% complete (' + offset + ' of ' + total + ')'); 75 | 76 | var progressBar = $('#progressBar'); 77 | progressBar.attr('aria-valuenow', width); 78 | progressBar.css('width', width + '%'); 79 | 80 | if (width === 0) { 81 | progress.html(progress.data('initial')); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /classes/BrandHelper.php: -------------------------------------------------------------------------------- 1 | 3.3 22 | if (method_exists($brandSettings, 'getPaletteColors')) { 23 | $colorPalette = BrandSetting::get('color_palette'); 24 | $colorMode = BrandSetting::getColorMode(); 25 | 26 | $this->primaryColor = $colorPalette[$colorMode]['primary'] ?? '#6a6cf7'; 27 | $this->secondaryColor = $colorPalette[$colorMode]['secondary'] ?? '#72809d'; 28 | // October CMS <3.2 29 | } else { 30 | $this->primaryColor = $brandSettings->get('primary_color'); 31 | $this->secondaryColor = $brandSettings->get('secondary_color'); 32 | } 33 | } 34 | 35 | public function getPrimaryColor(): string 36 | { 37 | return $this->primaryColor; 38 | } 39 | 40 | public function getSecondaryColor(): string 41 | { 42 | return $this->secondaryColor; 43 | } 44 | 45 | public function getPrimaryOrSecondaryColor(bool $flag): string 46 | { 47 | return $flag ? $this->getPrimaryColor() : $this->getSecondaryColor(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /classes/CacheManager.php: -------------------------------------------------------------------------------- 1 | cache->tags(self::CACHE_TAG_MATCHES) 30 | ->get($key); 31 | } 32 | 33 | public function forget(string $key): bool 34 | { 35 | return $this->cache->tags(self::CACHE_TAG_MATCHES) 36 | ->forget($key); 37 | } 38 | 39 | public function has(string $key): bool 40 | { 41 | return $this->cache->tags(self::CACHE_TAG_MATCHES) 42 | ->has($key); 43 | } 44 | 45 | public function cacheKey(string $requestPath, string $scheme): string 46 | { 47 | // Most caching backend have no limits on key lengths. 48 | // But to be sure I chose to MD5 hash the cache key. 49 | return md5($requestPath . $scheme); 50 | } 51 | 52 | public function flush(): void 53 | { 54 | $this->cache->tags([self::CACHE_TAG, self::CACHE_TAG_RULES, self::CACHE_TAG_MATCHES]) 55 | ->flush(); 56 | 57 | if ((bool) config('vdlp.redirect::log_redirect_changes', false) === true) { 58 | $this->log->info('Vdlp.Redirect: Redirect cache has been flushed.'); 59 | } 60 | } 61 | 62 | public function putRedirectRules(array $redirectRules): void 63 | { 64 | $this->cache->tags(self::CACHE_TAG_RULES) 65 | ->forever('rules', $redirectRules); 66 | } 67 | 68 | public function getRedirectRules(): array 69 | { 70 | if (!$this->cache->tags(self::CACHE_TAG_RULES)->has('rules')) { 71 | $publishManager = resolve(PublishManagerInterface::class); 72 | $publishManager->publish(); 73 | } 74 | 75 | $data = $this->cache->tags(self::CACHE_TAG_RULES) 76 | ->get('rules', []); 77 | 78 | if (is_array($data)) { 79 | return $data; 80 | } 81 | 82 | return []; 83 | } 84 | 85 | public function putMatch(string $cacheKey, ?RedirectRule $matchedRule = null): ?RedirectRule 86 | { 87 | if ($matchedRule === null) { 88 | $this->cache->tags(self::CACHE_TAG_MATCHES) 89 | ->forever($cacheKey, false); 90 | 91 | return null; 92 | } 93 | 94 | $matchedRuleToDate = $matchedRule->getToDate(); 95 | 96 | if ($matchedRuleToDate instanceof Carbon) { 97 | $minutes = $matchedRuleToDate->diffInMinutes(Carbon::now()); 98 | 99 | $this->cache->tags(self::CACHE_TAG_MATCHES) 100 | ->put($cacheKey, $matchedRule, $minutes); 101 | } else { 102 | $this->cache->tags(self::CACHE_TAG_MATCHES) 103 | ->forever($cacheKey, $matchedRule); 104 | } 105 | 106 | return $matchedRule; 107 | } 108 | 109 | public function cachingEnabledAndSupported(): bool 110 | { 111 | if (!Settings::isCachingEnabled()) { 112 | return false; 113 | } 114 | 115 | try { 116 | $this->cache->tags(self::CACHE_TAG); 117 | } catch (Throwable) { 118 | return false; 119 | } 120 | 121 | return true; 122 | } 123 | 124 | public function cachingEnabledButNotSupported(): bool 125 | { 126 | if (!Settings::isCachingEnabled()) { 127 | return false; 128 | } 129 | 130 | try { 131 | $this->cache->tags(self::CACHE_TAG); 132 | } catch (Throwable) { 133 | return true; 134 | } 135 | 136 | return false; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /classes/OptionHelper.php: -------------------------------------------------------------------------------- 1 | 'vdlp.redirect::lang.redirect.target_type_none', 20 | ]; 21 | } 22 | 23 | return [ 24 | Redirect::TARGET_TYPE_PATH_URL => 'vdlp.redirect::lang.redirect.target_type_path_or_url', 25 | Redirect::TARGET_TYPE_CMS_PAGE => 'vdlp.redirect::lang.redirect.target_type_cms_page', 26 | Redirect::TARGET_TYPE_STATIC_PAGE => 'vdlp.redirect::lang.redirect.target_type_static_page', 27 | ]; 28 | } 29 | 30 | public static function getCmsPageOptions(): array 31 | { 32 | return ['' => '-- ' . e(trans('vdlp.redirect::lang.redirect.none')) . ' --' ] + Page::getNameList(); 33 | } 34 | 35 | public static function getStaticPageOptions(): array 36 | { 37 | $options = ['' => '-- ' . e(trans('vdlp.redirect::lang.redirect.none')) . ' --' ]; 38 | 39 | $hasPagesPlugin = PluginManager::instance()->hasPlugin('RainLab.Pages'); 40 | 41 | if (!$hasPagesPlugin) { 42 | return $options; 43 | } 44 | 45 | $pages = \RainLab\Pages\Classes\Page::listInTheme(Theme::getActiveTheme()); 46 | 47 | /** @var \RainLab\Pages\Classes\Page $page */ 48 | foreach ($pages as $page) { 49 | if (array_key_exists('title', $page->viewBag)) { 50 | $options[$page->getBaseFileName()] = $page->viewBag['title']; 51 | } 52 | } 53 | 54 | return $options; 55 | } 56 | 57 | public static function getCategoryOptions(): array 58 | { 59 | return Category::query() 60 | ->get(['id', 'name']) 61 | ->pluck('name', 'key') 62 | ->toArray(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /classes/PublishManager.php: -------------------------------------------------------------------------------- 1 | where('is_enabled', 1) 49 | ->orderBy('sort_order') 50 | ->get($columns); 51 | 52 | if ($this->cacheManager->cachingEnabledAndSupported()) { 53 | $this->publishToCache($redirects->toArray()); 54 | } else { 55 | $this->publishToFilesystem($columns, $redirects->toArray()); 56 | } 57 | 58 | $count = $redirects->count(); 59 | 60 | if ((bool) config('vdlp.redirect::log_redirect_changes', false) === true) { 61 | $this->log->info(sprintf( 62 | 'Vdlp.Redirect: Redirect engine has been updated with %s redirects.', 63 | $count 64 | )); 65 | } 66 | 67 | return $count; 68 | } 69 | 70 | private function publishToFilesystem(array $columns, array $redirects): void 71 | { 72 | $redirectsFile = config('vdlp.redirect::rules_path'); 73 | 74 | if (file_exists($redirectsFile)) { 75 | unlink($redirectsFile); 76 | } 77 | 78 | try { 79 | $writer = Writer::createFromPath($redirectsFile, 'w+'); 80 | $writer->insertOne($columns); 81 | 82 | foreach ($redirects as $row) { 83 | if (isset($row['requirements'])) { 84 | $row['requirements'] = json_encode($row['requirements'], JSON_THROW_ON_ERROR); 85 | } 86 | 87 | $writer->insertOne($row); 88 | } 89 | } catch (Throwable $throwable) { 90 | touch($redirectsFile); 91 | 92 | $this->log->error($throwable); 93 | } 94 | } 95 | 96 | private function publishToCache(array $redirects): void 97 | { 98 | foreach ($redirects as &$redirect) { 99 | if (!isset($redirect['requirements'])) { 100 | continue; 101 | } 102 | 103 | try { 104 | $redirect['requirements'] = json_encode($redirect['requirements'], JSON_THROW_ON_ERROR); 105 | } catch (JsonException) { 106 | // @ignoreException 107 | } 108 | } 109 | 110 | unset($redirect); 111 | 112 | $this->cacheManager->flush(); 113 | $this->cacheManager->putRedirectRules($redirects); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /classes/RedirectConditionManager.php: -------------------------------------------------------------------------------- 1 | redirectManager->getConditions(); 26 | 27 | if (count($conditions) === 0) { 28 | return $enabledConditions; 29 | } 30 | 31 | $conditionCodes = \Vdlp\RedirectConditions\Models\ConditionParameter::query() 32 | ->where('redirect_id', '=', $rule->getId()) 33 | ->whereNotNull('is_enabled') 34 | ->get(['condition_code']) 35 | ->pluck('condition_code') 36 | ->toArray(); 37 | 38 | if (count($conditionCodes) === 0) { 39 | return $enabledConditions; 40 | } 41 | 42 | foreach ($conditions as $condition) { 43 | /** @var RedirectConditionInterface $condition */ 44 | $condition = resolve($condition); 45 | 46 | if (!in_array($condition->getCode(), $conditionCodes, true)) { 47 | continue; 48 | } 49 | 50 | $enabledConditions[] = $condition; 51 | } 52 | 53 | return $enabledConditions; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /classes/RedirectManagerSettings.php: -------------------------------------------------------------------------------- 1 | loggingEnabled; 30 | } 31 | 32 | public function isStatisticsEnabled(): bool 33 | { 34 | return $this->statisticsEnabled; 35 | } 36 | 37 | public function isRelativePathsEnabled(): bool 38 | { 39 | return $this->relativePathsEnabled; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /classes/RedirectMiddleware.php: -------------------------------------------------------------------------------- 1 | isXmlHttpRequest() 38 | || !in_array($request->method(), self::$supportedMethods, true) 39 | || Str::startsWith($request->getRequestUri(), '/vdlp/redirect/sparkline/') 40 | ) { 41 | return $next($request); 42 | } 43 | 44 | if ($request->header('X-Vdlp-Redirect') === 'Tester') { 45 | $this->redirectManager->setSettings(new RedirectManagerSettings( 46 | false, 47 | false, 48 | Settings::isRelativePathsEnabled() 49 | )); 50 | } 51 | 52 | $rule = false; 53 | 54 | $requestUri = str_replace($request->getBasePath(), '', $request->getRequestUri()); 55 | 56 | try { 57 | if ( 58 | $this->cacheManager->cachingEnabledAndSupported() 59 | && method_exists($this->redirectManager, 'matchCached') 60 | ) { 61 | $rule = $this->redirectManager->matchCached($requestUri, $request->getScheme()); 62 | } else { 63 | $rule = $this->redirectManager->match($requestUri, $request->getScheme()); 64 | } 65 | } catch (NoMatchForRequest | UnableToLoadRules | InvalidScheme) { 66 | // @ignoreException 67 | $rule = false; 68 | } catch (Throwable $throwable) { 69 | $this->log->error(sprintf( 70 | 'Vdlp.Redirect: Could not perform redirect for %s (scheme: %s): %s', 71 | $requestUri, 72 | $request->getScheme(), 73 | $throwable->getMessage() 74 | )); 75 | } 76 | 77 | if ($rule === false || $rule === null) { 78 | return $next($request); 79 | } 80 | 81 | /* 82 | * Extensibility: 83 | * 84 | * At this point a positive match was made based on the request URI. 85 | */ 86 | $this->dispatcher->fire('vdlp.redirect.match', [$rule, $requestUri]); 87 | 88 | /* 89 | * Extensibility: 90 | * 91 | * Developers can add their own conditions. If a condition does not pass the redirect will be ignored. 92 | */ 93 | foreach ($this->redirectConditionManager->getEnabledConditions($rule) as $condition) { 94 | if (!$condition->passes($rule, $requestUri)) { 95 | return $next($request); 96 | } 97 | } 98 | 99 | $this->redirectManager->redirectWithRule($rule, $requestUri); 100 | 101 | return $next($request); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /classes/Sparkline.php: -------------------------------------------------------------------------------- 1 | colorHexToRGB($color); 18 | 19 | $baseRed = $baseGreen = $baseBlue = 255; 20 | 21 | $red = (int) floor(($baseRed + $red) / 2); 22 | $green = (int) floor(($baseGreen + $green) / 2); 23 | $blue = (int) floor(($baseBlue + $blue) / 2); 24 | 25 | $this->setFillColorRGB($red, $green, $blue); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /classes/TesterBase.php: -------------------------------------------------------------------------------- 1 | testUrl = url($testPath, [], $secure); 32 | } 33 | 34 | final public function execute(): TesterResult 35 | { 36 | $stopwatch = new Stopwatch(); 37 | 38 | $stopwatch->start(__FUNCTION__); 39 | 40 | $result = $this->test(); 41 | 42 | $event = $stopwatch->stop(__FUNCTION__); 43 | 44 | $result->setDuration((int) $event->getDuration()); 45 | 46 | return $result; 47 | } 48 | 49 | public function getTestPath(): string 50 | { 51 | return $this->testPath; 52 | } 53 | 54 | public function getTestUrl(): string 55 | { 56 | return $this->testUrl; 57 | } 58 | 59 | abstract protected function test(): TesterResult; 60 | 61 | protected function setDefaultCurlOptions(CurlHandle $curlHandle): void 62 | { 63 | curl_setopt($curlHandle, CURLOPT_MAXREDIRS, self::MAX_REDIRECTS); 64 | curl_setopt($curlHandle, CURLOPT_CONNECTTIMEOUT, self::CONNECTION_TIMEOUT); 65 | curl_setopt($curlHandle, CURLOPT_AUTOREFERER, true); 66 | 67 | // This constant is not available when open_basedir or safe_mode are enabled. 68 | curl_setopt($curlHandle, CURLOPT_FOLLOWLOCATION, true); 69 | 70 | /** @noinspection CurlSslServerSpoofingInspection */ 71 | curl_setopt($curlHandle, CURLOPT_SSL_VERIFYHOST, false); 72 | /** @noinspection CurlSslServerSpoofingInspection */ 73 | curl_setopt($curlHandle, CURLOPT_SSL_VERIFYPEER, false); 74 | 75 | if (defined('CURLOPT_SSL_VERIFYSTATUS')) { 76 | curl_setopt($curlHandle, CURLOPT_SSL_VERIFYSTATUS, false); 77 | } 78 | 79 | curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, true); 80 | curl_setopt($curlHandle, CURLOPT_VERBOSE, false); 81 | curl_setopt($curlHandle, CURLOPT_HTTPHEADER, [ 82 | 'X-Vdlp-Redirect: Tester', 83 | ]); 84 | } 85 | 86 | protected function getRedirectManager(): RedirectManagerInterface 87 | { 88 | /** @var RedirectManagerInterface $manager */ 89 | $manager = resolve(RedirectManagerInterface::class); 90 | 91 | return $manager->setSettings(new RedirectManagerSettings( 92 | false, 93 | false, 94 | Settings::isRelativePathsEnabled() 95 | )); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /classes/TesterResult.php: -------------------------------------------------------------------------------- 1 | duration = 0; 16 | } 17 | 18 | public function isPassed(): bool 19 | { 20 | return $this->passed; 21 | } 22 | 23 | public function getMessage(): string 24 | { 25 | return $this->message; 26 | } 27 | 28 | public function setDuration(int $duration): TesterResult 29 | { 30 | $this->duration = $duration; 31 | 32 | return $this; 33 | } 34 | 35 | public function getDuration(): int 36 | { 37 | return $this->duration; 38 | } 39 | 40 | public function getStatusCssClass(): string 41 | { 42 | return $this->passed ? 'passed' : 'failed'; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /classes/contracts/CacheManagerInterface.php: -------------------------------------------------------------------------------- 1 | getId(), 20 | $requestPath, 21 | $scheme ?: '(no scheme)' 22 | )); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /classes/exceptions/RulesPathNotReadable.php: -------------------------------------------------------------------------------- 1 | logChange($model, 'created'); 34 | 35 | $this->dispatcher->dispatch('vdlp.redirect.changed', [ 36 | 'redirectIds' => Arr::wrap($model->getKey()) 37 | ]); 38 | } 39 | 40 | /** 41 | * @param Models\Redirect $model 42 | * @return void 43 | */ 44 | public function updated(Models\Redirect $model): void 45 | { 46 | if (!self::canHandleChanges()) { 47 | return; 48 | } 49 | 50 | $this->logChange($model, 'updated'); 51 | 52 | $this->dispatcher->dispatch('vdlp.redirect.changed', [ 53 | 'redirectIds' => Arr::wrap($model->getKey()) 54 | ]); 55 | } 56 | 57 | /** 58 | * @param Models\Redirect $model 59 | * @return void 60 | */ 61 | public function deleted(Models\Redirect $model): void 62 | { 63 | if (!self::canHandleChanges()) { 64 | return; 65 | } 66 | 67 | $this->logChange($model, 'deleted'); 68 | 69 | $this->dispatcher->dispatch('vdlp.redirect.changed', [ 70 | 'redirectIds' => Arr::wrap($model->getKey()) 71 | ]); 72 | } 73 | 74 | private function logChange(Models\Redirect $model, string $typeOfChange): void 75 | { 76 | if ((bool) config('vdlp.redirect::log_redirect_changes', false) === false) { 77 | return; 78 | } 79 | 80 | $this->log->info(sprintf( 81 | 'Vdlp.Redirect: Redirect %d has been %s.', 82 | $model->getKey(), 83 | $typeOfChange 84 | ), $model->getDirty()); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /classes/observers/SettingsObserver.php: -------------------------------------------------------------------------------- 1 | publishManager->publish(); 21 | } catch (Throwable) { 22 | // @ignoreException 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /classes/observers/traits/CanBeDisabled.php: -------------------------------------------------------------------------------- 1 | testUrl); 15 | 16 | if ($curlHandle === false) { 17 | return new TesterResult(false, e(trans('vdlp.redirect::lang.test_lab.test_error'))); 18 | } 19 | 20 | $this->setDefaultCurlOptions($curlHandle); 21 | 22 | $error = null; 23 | 24 | if (curl_exec($curlHandle) === false) { 25 | $error = curl_error($curlHandle); 26 | } 27 | 28 | if ($error !== null) { 29 | return new TesterResult(false, e(trans('vdlp.redirect::lang.test_lab.result_request_failed'))); 30 | } 31 | 32 | $statusCode = (int) curl_getinfo($curlHandle, CURLINFO_HTTP_CODE); 33 | $redirectCount = (int) curl_getinfo($curlHandle, CURLINFO_REDIRECT_COUNT); 34 | 35 | curl_close($curlHandle); 36 | 37 | return new TesterResult( 38 | $redirectCount === 1 || ($redirectCount === 0 && $statusCode > 400), 39 | e(trans('vdlp.redirect::lang.test_lab.redirects_followed', ['count' => $redirectCount, 'limit' => 10])) 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /classes/testers/RedirectFinalDestination.php: -------------------------------------------------------------------------------- 1 | testUrl); 15 | 16 | if ($curlHandle === false) { 17 | return new TesterResult(false, e(trans('vdlp.redirect::lang.test_lab.test_error'))); 18 | } 19 | 20 | $this->setDefaultCurlOptions($curlHandle); 21 | 22 | curl_setopt($curlHandle, CURLOPT_FOLLOWLOCATION, false); 23 | 24 | $error = null; 25 | 26 | if (curl_exec($curlHandle) === false) { 27 | $error = e(trans('vdlp.redirect::lang.test_lab.not_determinate_destination_url')); 28 | } 29 | 30 | $finalDestination = curl_getinfo($curlHandle, CURLINFO_REDIRECT_URL); 31 | $statusCode = (int) curl_getinfo($curlHandle, CURLINFO_HTTP_CODE); 32 | 33 | curl_close($curlHandle); 34 | 35 | if (empty($finalDestination) && $statusCode > 400) { 36 | $message = $error ?? e(trans('vdlp.redirect::lang.test_lab.no_destination_url')); 37 | } else { 38 | $finalDestination = sprintf( 39 | '%s', 40 | e($finalDestination), 41 | e($finalDestination) 42 | ); 43 | 44 | $message = $error 45 | ?? trans('vdlp.redirect::lang.test_lab.final_destination_is', ['destination' => $finalDestination]); 46 | } 47 | 48 | return new TesterResult($error === null, $message); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /classes/testers/RedirectLoop.php: -------------------------------------------------------------------------------- 1 | testUrl); 15 | 16 | if ($curlHandle === false) { 17 | return new TesterResult(false, e(trans('vdlp.redirect::lang.test_lab.test_error'))); 18 | } 19 | 20 | $this->setDefaultCurlOptions($curlHandle); 21 | 22 | curl_setopt($curlHandle, CURLOPT_MAXREDIRS, 20); 23 | 24 | $error = null; 25 | 26 | if ( 27 | curl_exec($curlHandle) === false 28 | && curl_errno($curlHandle) === CURLE_TOO_MANY_REDIRECTS 29 | ) { 30 | $error = e(trans('vdlp.redirect::lang.test_lab.possible_loop')); 31 | } 32 | 33 | curl_close($curlHandle); 34 | 35 | $message = $error ?? e(trans('vdlp.redirect::lang.test_lab.no_loop')); 36 | 37 | return new TesterResult($error === null, $message); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /classes/testers/RedirectMatch.php: -------------------------------------------------------------------------------- 1 | getRedirectManager(); 20 | 21 | try { 22 | $match = $manager->match( 23 | $this->testPath, 24 | $this->secure ? Redirect::SCHEME_HTTPS : Redirect::SCHEME_HTTP 25 | ); 26 | } catch (NoMatchForRequest | InvalidScheme | UnableToLoadRules) { 27 | $match = false; 28 | } 29 | 30 | if ($match === false) { 31 | return new TesterResult(false, e(trans('vdlp.redirect::lang.test_lab.not_match_redirect'))); 32 | } 33 | 34 | $message = sprintf( 35 | '%s %s.', 36 | e(trans('vdlp.redirect::lang.test_lab.matched')), 37 | Backend::url('vdlp/redirect/redirects/update/' . $match->getId()), 38 | e(trans('vdlp.redirect::lang.test_lab.redirect')) 39 | ); 40 | 41 | return new TesterResult(true, $message); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /classes/testers/ResponseCode.php: -------------------------------------------------------------------------------- 1 | testUrl); 27 | 28 | if ($curlHandle === false) { 29 | return new TesterResult(false, e(trans('vdlp.redirect::lang.test_lab.test_error'))); 30 | } 31 | 32 | $this->setDefaultCurlOptions($curlHandle); 33 | 34 | curl_setopt($curlHandle, CURLOPT_FOLLOWLOCATION, false); 35 | 36 | $error = null; 37 | 38 | if (curl_exec($curlHandle) === false) { 39 | $error = curl_error($curlHandle); 40 | } 41 | 42 | if ($error !== null) { 43 | return new TesterResult(false, e(trans('vdlp.redirect::lang.test_lab.result_request_failed'))); 44 | } 45 | 46 | $statusCode = (int) curl_getinfo($curlHandle, CURLINFO_HTTP_CODE); 47 | 48 | curl_close($curlHandle); 49 | 50 | $manager = $this->getRedirectManager(); 51 | 52 | try { 53 | $match = $manager->match( 54 | $this->testPath, 55 | $this->secure ? Redirect::SCHEME_HTTPS : Redirect::SCHEME_HTTP 56 | ); 57 | } catch (NoMatchForRequest | InvalidScheme | UnableToLoadRules) { 58 | $match = false; 59 | } 60 | 61 | if ($match !== false && $match->getStatusCode() !== $statusCode) { 62 | $message = e(trans('vdlp.redirect::lang.test_lab.matched_not_http_code', [ 63 | 'expected' => $match->getStatusCode(), 64 | 'received' => $statusCode, 65 | ])); 66 | 67 | return new TesterResult(false, $message); 68 | } 69 | 70 | if ($match !== false && $match->getStatusCode() === $statusCode) { 71 | $message = e(trans('vdlp.redirect::lang.test_lab.matched_http_code', [ 72 | 'code' => $statusCode, 73 | ])); 74 | 75 | return new TesterResult(true, $message); 76 | } 77 | 78 | // Should be a 301, 302, 303, 404, 410, ... 79 | if (!array_key_exists($statusCode, Redirect::$statusCodes)) { 80 | return new TesterResult( 81 | false, 82 | e(trans('vdlp.redirect::lang.test_lab.response_http_code_should_be')) 83 | . ' ' 84 | . implode(', ', array_keys(Redirect::$statusCodes)) 85 | ); 86 | } 87 | 88 | return new TesterResult( 89 | true, 90 | e(trans('vdlp.redirect::lang.test_lab.response_http_code')) . ': ' . $statusCode 91 | ); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /classes/util/Str.php: -------------------------------------------------------------------------------- 1 | [ 17 | 18 | 'publish_redirects' => env('VDLP_REDIRECT_CRON_PUBLISH_REDIRECTS', '00:00'), 19 | 20 | ], 21 | 22 | /* 23 | |-------------------------------------------------------------------------- 24 | | Logging 25 | |-------------------------------------------------------------------------- 26 | | 27 | | Enable or disable specific logging information. Commonly used for 28 | | debugging purposes. 29 | | 30 | */ 31 | 32 | 'log_redirect_changes' => (bool) env('VDLP_REDIRECT_LOG_REDIRECT_CHANGES', false), 33 | 34 | /* 35 | |-------------------------------------------------------------------------- 36 | | Redirect Rules Path 37 | |-------------------------------------------------------------------------- 38 | | 39 | | The path of the redirect rules. Make sure the path is writable. 40 | | 41 | */ 42 | 43 | 'rules_path' => env('VDLP_REDIRECT_RULES_PATH', storage_path('app/redirects.csv')), 44 | 45 | /* 46 | |-------------------------------------------------------------------------- 47 | | Navigation 48 | |-------------------------------------------------------------------------- 49 | */ 50 | 51 | 'navigation' => [ 52 | 'show_import' => env('VDLP_REDIRECT_SHOW_IMPORT', true), 53 | 'show_export' => env('VDLP_REDIRECT_SHOW_EXPORT', true), 54 | 'show_settings' => env('VDLP_REDIRECT_SHOW_SETTINGS', true), 55 | 'show_extensions' => env('VDLP_REDIRECT_SHOW_EXTENSION', true), 56 | ], 57 | 58 | ]; 59 | -------------------------------------------------------------------------------- /console/PublishRedirectsCommand.php: -------------------------------------------------------------------------------- 1 | signature = 'vdlp:redirect:publish-redirects'; 15 | $this->description = 'Publish all redirects.'; 16 | 17 | parent::__construct(); 18 | } 19 | 20 | public function handle(PublishManager $publishManager): void 21 | { 22 | $publishManager->publish(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /controllers/Categories.php: -------------------------------------------------------------------------------- 1 | addCss('/plugins/vdlp/redirect/assets/css/redirect.css'); 31 | 32 | NavigationManager::instance()->setContext('Vdlp.Redirect', 'redirect', 'categories'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /controllers/Extensions.php: -------------------------------------------------------------------------------- 1 | setContext('Vdlp.Redirect', 'redirect', 'extensions'); 29 | 30 | $this->addCss('/plugins/vdlp/redirect/assets/css/redirect.css'); 31 | 32 | $this->pageTitle = 'Redirect Extensions (new)'; 33 | } 34 | 35 | public function index(): void 36 | { 37 | $this->vars['extensions'] = []; 38 | 39 | foreach (self::$extensions as $extension) { 40 | $this->vars['extensions'][$extension] = PluginManager::instance()->hasPlugin($extension) 41 | && !PluginManager::instance()->isDisabled($extension); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /controllers/Logs.php: -------------------------------------------------------------------------------- 1 | setContext('Vdlp.Redirect', 'redirect', 'logs'); 40 | 41 | $this->addCss('/plugins/vdlp/redirect/assets/css/redirect.css'); 42 | } 43 | 44 | public function onRefresh(): array 45 | { 46 | return $this->listRefresh(); 47 | } 48 | 49 | public function onEmptyLog(): array 50 | { 51 | try { 52 | RedirectLog::query()->truncate(); 53 | $this->flash->success($this->translator->trans('vdlp.redirect::lang.flash.truncate_success')); 54 | } catch (Throwable $e) { 55 | $this->log->warning($e); 56 | } 57 | 58 | return $this->listRefresh(); 59 | } 60 | 61 | public function onDelete(): array 62 | { 63 | if (($checkedIds = $this->request->get('checked', [])) 64 | && is_array($checkedIds) 65 | && count($checkedIds) 66 | ) { 67 | foreach ($checkedIds as $recordId) { 68 | try { 69 | /** @var RedirectLog $record */ 70 | $record = RedirectLog::query()->findOrFail($recordId); 71 | $record->delete(); 72 | } catch (Throwable $e) { 73 | $this->log->warning($e); 74 | } 75 | } 76 | 77 | $this->flash->success($this->translator->trans('vdlp.redirect::lang.flash.delete_selected_success')); 78 | } else { 79 | $this->flash->error($this->translator->trans('backend::lang.list.delete_selected_empty')); 80 | } 81 | 82 | return $this->listRefresh(); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /controllers/Statistics.php: -------------------------------------------------------------------------------- 1 | setContext('Vdlp.Redirect', 'redirect', 'statistics'); 29 | 30 | $this->pageTitle = 'vdlp.redirect::lang.title.statistics'; 31 | 32 | $this->addCss('/plugins/vdlp/redirect/assets/css/redirect.css'); 33 | $this->addCss('/plugins/vdlp/redirect/assets/css/statistics.css'); 34 | 35 | $this->helper = new StatisticsHelper(); 36 | } 37 | 38 | public function index(): void 39 | { 40 | } 41 | 42 | /** 43 | * @throws SystemException|JsonException|InvalidFormatException 44 | */ 45 | public function onLoadHitsPerDay(): array 46 | { 47 | $today = Carbon::today(); 48 | 49 | $postValue = post('period-month-year', $today->month . '_' . $today->year); 50 | 51 | [$month, $year] = explode('_', $postValue); 52 | 53 | return [ 54 | '#hitsPerDay' => $this->makePartial('hits-per-day', [ 55 | 'dataSets' => json_encode([ 56 | $this->getHitsPerDayAsDataSet((int) $month, (int) $year, true), 57 | $this->getHitsPerDayAsDataSet((int) $month, (int) $year, false), 58 | ], JSON_THROW_ON_ERROR), 59 | 'labels' => json_encode($this->getLabels((int) $month, (int) $year), JSON_THROW_ON_ERROR), 60 | 'monthYearOptions' => $this->helper->getMonthYearOptions(), 61 | 'monthYearSelected' => $month . '_' . $year, 62 | ]), 63 | ]; 64 | } 65 | 66 | /** 67 | * @throws InvalidFormatException 68 | * @throws JsonException 69 | * @throws SystemException 70 | */ 71 | public function onSelectPeriodMonthYear(): array 72 | { 73 | return $this->onLoadHitsPerDay(); 74 | } 75 | 76 | /** 77 | * @throws SystemException 78 | */ 79 | public function onLoadTopRedirectsThisMonth(): array 80 | { 81 | return [ 82 | '#topRedirectsThisMonth' => $this->makePartial('top-redirects-this-month', [ 83 | 'topTenRedirectsThisMonth' => $this->helper->getTopRedirectsThisMonth(), 84 | ]), 85 | ]; 86 | } 87 | 88 | /** 89 | * @throws SystemException 90 | */ 91 | public function onLoadTopCrawlersThisMonth(): array 92 | { 93 | return [ 94 | '#topCrawlersThisMonth' => $this->makePartial('top-crawlers-this-month', [ 95 | 'topTenCrawlersThisMonth' => $this->helper->getTopTenCrawlersThisMonth(), 96 | ]), 97 | ]; 98 | } 99 | 100 | /** 101 | * @throws SystemException 102 | */ 103 | public function onLoadRedirectHitsPerMonth(): array 104 | { 105 | return [ 106 | '#redirectHitsPerMonth' => $this->makePartial('redirect-hits-per-month', [ 107 | 'redirectHitsPerMonth' => $this->helper->getRedirectHitsPerMonth(), 108 | ]), 109 | ]; 110 | } 111 | 112 | /** 113 | * @throws SystemException 114 | */ 115 | public function onLoadScoreBoard(): array 116 | { 117 | return [ 118 | '#scoreBoard' => $this->makePartial('score-board', [ 119 | 'redirectHitsPerMonth' => $this->helper->getRedirectHitsPerMonth(), 120 | 'totalActiveRedirects' => $this->helper->getTotalActiveRedirects(), 121 | 'activeRedirects' => $this->helper->getActiveRedirects(), 122 | 'totalRedirectsServed' => $this->helper->getTotalRedirectsServed(), 123 | 'totalThisMonth' => $this->helper->getTotalThisMonth(), 124 | 'totalLastMonth' => $this->helper->getTotalLastMonth(), 125 | 'latestClient' => $this->helper->getLatestClient(), 126 | ]), 127 | ]; 128 | } 129 | 130 | private function getLabels(int $month, int $year): array 131 | { 132 | $labels = []; 133 | 134 | $dates = Carbon::create($year, $month) 135 | ->firstOfMonth() 136 | ->daysUntil(Carbon::create($year, $month)->endOfMonth()); 137 | 138 | foreach ($dates as $date) { 139 | $labels[] = $date->isoFormat('LL'); 140 | } 141 | 142 | return $labels; 143 | } 144 | 145 | /** 146 | * @throws InvalidFormatException 147 | */ 148 | private function getHitsPerDayAsDataSet(int $month, int $year, bool $crawler): array 149 | { 150 | $today = Carbon::createFromDate($year, $month, 1); 151 | 152 | $data = $this->helper->getRedirectHitsPerDay($month, $year, $crawler); 153 | 154 | for ($i = $today->firstOfMonth()->day; $i <= $today->lastOfMonth()->day; $i++) { 155 | if (!array_key_exists($i, $data)) { 156 | $data[$i] = ['hits' => 0]; 157 | } 158 | } 159 | 160 | ksort($data); 161 | 162 | $color = BrandHelper::instance()->getPrimaryOrSecondaryColor($crawler); 163 | 164 | [$r, $g, $b] = sscanf($color, "#%02x%02x%02x"); 165 | 166 | return [ 167 | 'label' => $crawler 168 | ? e(trans('vdlp.redirect::lang.statistics.crawler_hits')) 169 | : e(trans('vdlp.redirect::lang.statistics.visitor_hits')), 170 | 'backgroundColor' => sprintf('rgb(%d, %d, %d, 0.5)', $r, $g, $b), 171 | 'borderColor' => sprintf('rgb(%d, %d, %d, 1)', $r, $g, $b), 172 | 'borderWidth' => 1, 173 | 'data' => data_get($data, '*.hits'), 174 | ]; 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /controllers/TestLab.php: -------------------------------------------------------------------------------- 1 | bodyClass = 'layout-relative'; 33 | 34 | parent::__construct(); 35 | 36 | NavigationManager::instance()->setContext('Vdlp.Redirect', 'redirect', 'test_lab'); 37 | 38 | $this->loadRedirects(); 39 | } 40 | 41 | public function index(): void 42 | { 43 | $this->pageTitle = 'vdlp.redirect::lang.title.test_lab'; 44 | 45 | $this->addCss('/plugins/vdlp/redirect/assets/css/redirect.css'); 46 | $this->addCss('/plugins/vdlp/redirect/assets/css/test-lab.css'); 47 | $this->addJs('/plugins/vdlp/redirect/assets/javascript/test-lab.js'); 48 | 49 | $this->vars['redirectCount'] = $this->getRedirectCount(); 50 | } 51 | 52 | private function loadRedirects(): void 53 | { 54 | /** @var Collection $redirects */ 55 | $this->redirects = array_values(Redirect::enabled() 56 | ->testLabEnabled() 57 | ->orderBy('sort_order') 58 | ->get() 59 | ->filter(static function (Redirect $redirect): bool { 60 | return $redirect->isActiveOnDate(Carbon::today()); 61 | }) 62 | ->all()); 63 | } 64 | 65 | private function offsetGetRedirect(int $offset): ?Redirect 66 | { 67 | return $this->redirects[$offset] ?? null; 68 | } 69 | 70 | public function onTest(): string 71 | { 72 | $offset = (int) $this->request->get('offset'); 73 | 74 | $redirect = $this->offsetGetRedirect($offset); 75 | 76 | if ($redirect === null) { 77 | return ''; 78 | } 79 | 80 | try { 81 | $partial = (string) $this->makePartial('tester_result', [ 82 | 'redirect' => $redirect, 83 | 'testPath' => $this->getTestPath($redirect), 84 | 'testResults' => $this->getTestResults($redirect, $this->request->secure()), 85 | ]); 86 | } catch (Throwable $e) { 87 | $partial = (string) $this->makePartial('tester_failed', [ 88 | 'redirect' => $redirect, 89 | 'message' => $e->getMessage(), 90 | ]); 91 | } 92 | 93 | return $partial; 94 | } 95 | 96 | /** 97 | * @throws ModelNotFoundException 98 | */ 99 | public function onReRun(): array 100 | { 101 | /** @var Redirect $redirect */ 102 | $redirect = Redirect::query()->findOrFail($this->request->get('id')); 103 | 104 | $this->flash->success(trans('vdlp.redirect::lang.test_lab.flash_test_executed')); 105 | 106 | return [ 107 | '#testerResult' . $redirect->getKey() => $this->makePartial( 108 | 'tester_result_items', 109 | $this->getTestResults($redirect, $this->request->secure()) 110 | ), 111 | ]; 112 | } 113 | 114 | /** 115 | * @throws ModelNotFoundException 116 | * @throws SystemException 117 | */ 118 | public function onExclude(): array 119 | { 120 | /** @var Redirect $redirect */ 121 | $redirect = Redirect::query()->findOrFail($this->request->get('id')); 122 | $redirect->update(['test_lab' => false]); 123 | 124 | $this->flash->success(trans('vdlp.redirect::lang.test_lab.flash_redirect_excluded')); 125 | 126 | return [ 127 | '#testButtonWrapper' => $this->makePartial('test_button', [ 128 | 'redirectCount' => $this->getRedirectCount(), 129 | ]), 130 | ]; 131 | } 132 | 133 | public function getTestPath(Redirect $redirect): string 134 | { 135 | $testPath = '/'; 136 | 137 | if ($redirect->isMatchTypeExact()) { 138 | $testPath = (string) $redirect->getAttribute('from_url'); 139 | } elseif ($redirect->getAttribute('test_lab_path')) { 140 | $testPath = (string) $redirect->getAttribute('test_lab_path'); 141 | } 142 | 143 | return $testPath; 144 | } 145 | 146 | public function getTestResults(Redirect $redirect, bool $secure): array 147 | { 148 | $testPath = $this->getTestPath($redirect); 149 | 150 | return [ 151 | 'maxRedirectsResult' => (new Testers\RedirectLoop($testPath, $secure))->execute(), 152 | 'matchedRedirectResult' => (new Testers\RedirectMatch($testPath, $secure))->execute(), 153 | 'responseCodeResult' => (new Testers\ResponseCode($testPath, $secure))->execute(), 154 | 'redirectCountResult' => (new Testers\RedirectCount($testPath, $secure))->execute(), 155 | 'finalDestinationResult' => (new Testers\RedirectFinalDestination($testPath, $secure))->execute(), 156 | ]; 157 | } 158 | 159 | private function getRedirectCount(): int 160 | { 161 | return count($this->redirects); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /controllers/categories/_list_toolbar.htm: -------------------------------------------------------------------------------- 1 |
2 | 4 | 5 | 6 |
7 | -------------------------------------------------------------------------------- /controllers/categories/config_form.yaml: -------------------------------------------------------------------------------- 1 | # =================================== 2 | # Form Behavior Config 3 | # =================================== 4 | 5 | # Record name 6 | name: vdlp.redirect::lang.redirect.category 7 | 8 | # Model Form Field configuration 9 | form: $/vdlp/redirect/models/category/fields.yaml 10 | 11 | # Model Class name 12 | modelClass: Vdlp\Redirect\Models\Category 13 | 14 | # Default redirect location 15 | defaultRedirect: vdlp/redirect/categories 16 | 17 | # Create page 18 | create: 19 | title: vdlp.redirect::lang.title.create_category 20 | redirect: vdlp/redirect/categories/update/:id 21 | redirectClose: vdlp/redirect/categories 22 | 23 | # Update page 24 | update: 25 | title: vdlp.redirect::lang.title.edit_category 26 | redirect: vdlp/redirect/categories 27 | redirectClose: vdlp/redirect/categories 28 | -------------------------------------------------------------------------------- /controllers/categories/config_list.yaml: -------------------------------------------------------------------------------- 1 | # =================================== 2 | # List Behavior Config 3 | # =================================== 4 | 5 | # Model List Column configuration 6 | list: $/vdlp/redirect/models/category/columns.yaml 7 | 8 | # Model Class name 9 | modelClass: Vdlp\Redirect\Models\Category 10 | 11 | # List Title 12 | title: vdlp.redirect::lang.title.categories 13 | 14 | # Link URL for each record 15 | recordUrl: vdlp/redirect/categories/update/:id 16 | 17 | # Message to display if the list is empty 18 | noRecordsMessage: backend::lang.list.no_records 19 | 20 | # Records to display per page 21 | recordsPerPage: 20 22 | 23 | # Displays the list column set up button 24 | # showSetup: true 25 | 26 | # Displays the sorting link on each column 27 | showSorting: true 28 | 29 | # Default sorting column 30 | # defaultSort: 31 | # column: created_at 32 | # direction: desc 33 | 34 | # Display checkboxes next to each record 35 | # showCheckboxes: true 36 | 37 | # Toolbar widget configuration 38 | toolbar: 39 | # Partial for toolbar buttons 40 | buttons: list_toolbar 41 | 42 | # Search widget configuration 43 | search: 44 | prompt: backend::lang.list.search_prompt 45 | -------------------------------------------------------------------------------- /controllers/categories/create.htm: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | fatalError): ?> 9 | 10 | 'layout']); ?> 11 | 12 |
13 | formRender(); ?> 14 |
15 | 16 |
17 |
18 | 26 | 35 | 36 | 37 | 38 |
39 |
40 | 41 | 42 | 43 | 44 | 45 |

fatalError); ?>

46 |

47 | 48 | 49 | -------------------------------------------------------------------------------- /controllers/categories/index.htm: -------------------------------------------------------------------------------- 1 | 2 | listRender(); ?> 3 | -------------------------------------------------------------------------------- /controllers/categories/update.htm: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | fatalError): ?> 9 | 10 | 'layout']); ?> 11 | 12 |
13 | formRender(); ?> 14 |
15 | 16 |
17 |
18 | 27 | 36 | 43 | 44 | 45 | 46 |
47 |
48 | 49 | 50 | 51 | 52 | 53 |

fatalError); ?>

54 |

55 | 56 | 57 | -------------------------------------------------------------------------------- /controllers/extensions/_installed.htm: -------------------------------------------------------------------------------- 1 |
2 | 3 | installed 4 | 5 | not installed 6 | 7 |
8 | -------------------------------------------------------------------------------- /controllers/logs/_list_toolbar.htm: -------------------------------------------------------------------------------- 1 |
3 | 7 | 8 | 9 | 14 | 15 | 16 | 28 |
29 | -------------------------------------------------------------------------------- /controllers/logs/config_filter.yaml: -------------------------------------------------------------------------------- 1 | # =================================== 2 | # Filter Scope Definitions 3 | # =================================== 4 | 5 | scopes: 6 | updated_at: 7 | label: vdlp.redirect::lang.redirect.date 8 | type: date 9 | conditions: updated_at >= ':filtered' 10 | status_code: 11 | label: vdlp.redirect::lang.redirect.status_code 12 | type: group 13 | modelClass: Vdlp\Redirect\Models\Redirect 14 | options: filterStatusCodeOptions 15 | conditions: status_code in (:filtered) 16 | -------------------------------------------------------------------------------- /controllers/logs/config_list.yaml: -------------------------------------------------------------------------------- 1 | # =================================== 2 | # List Behavior Config 3 | # =================================== 4 | 5 | # Model List Column configuration 6 | list: $/vdlp/redirect/models/redirectlog/columns.yaml 7 | 8 | filter: config_filter.yaml 9 | 10 | # Model Class name 11 | modelClass: Vdlp\Redirect\Models\RedirectLog 12 | 13 | # List Title 14 | title: vdlp.redirect::lang.title.view_redirect_log 15 | 16 | # Link URL for each record 17 | recordUrl: vdlp/redirect/redirects/update/:redirect_id 18 | 19 | # Message to display if the list is empty 20 | noRecordsMessage: backend::lang.list.no_records 21 | 22 | # Records to display per page 23 | recordsPerPage: 20 24 | 25 | # Displays the list column set up button 26 | showSetup: true 27 | 28 | # Displays the sorting link on each column 29 | showSorting: true 30 | 31 | # Default sorting column 32 | defaultSort: 33 | column: updated_at 34 | direction: desc 35 | 36 | # Display checkboxes next to each record 37 | showCheckboxes: true 38 | 39 | # Toolbar widget configuration 40 | toolbar: 41 | # Partial for toolbar buttons 42 | buttons: list_toolbar 43 | 44 | # Search widget configuration 45 | search: 46 | prompt: backend::lang.list.search_prompt 47 | -------------------------------------------------------------------------------- /controllers/logs/index.htm: -------------------------------------------------------------------------------- 1 | 2 | listRender(); ?> 3 | -------------------------------------------------------------------------------- /controllers/redirects/_field_statistics.htm: -------------------------------------------------------------------------------- 1 | formGetModel(); 3 | $redirectId = (int) $redirect->getKey(); 4 | $latestClient = $statisticsHelper->getLatestClient($redirectId); 5 | ?> 6 | 7 |
8 |
9 |
10 |

11 |
12 | <?= e(trans('vdlp.redirect::lang.statistics.activity_last_three_months')); ?> 14 |
15 |
16 |
17 |
18 | 19 |
20 |
21 |
22 |

23 |

getTotalThisMonth($redirectId), 0, '', '.') ?>

24 |

: getTotalLastMonth($redirectId), 0, '', '.') ?>

25 |
26 |
27 |
28 | 29 |
30 |
31 |
32 |

33 |

hits, 0, '', '.') ?>

34 |

35 |
36 |
37 |
38 | 39 | last_used_at, ['formatAlias' => 'dateTimeMin']) ?> 40 | 41 |
42 |
43 |
44 |

45 |

46 |

47 |
48 |
49 |
50 | 51 | updated_at, ['formatAlias' => 'dateTimeMin']) ?> 52 | 53 |
54 |
55 |
56 |

57 |

58 |

59 |
60 |
61 |
62 | 63 | systemRequestLog): ?> 64 |
65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /controllers/redirects/_list_toolbar.htm: -------------------------------------------------------------------------------- 1 |
2 | 4 | 5 | 6 | 7 | 8 | 14 | 15 | 16 | 17 | 18 | 20 | 21 | 22 | 23 |
24 | 36 | 47 | 58 |
59 | 70 | 77 |
78 | -------------------------------------------------------------------------------- /controllers/redirects/_popup_actions.htm: -------------------------------------------------------------------------------- 1 | 8 | 54 | 60 | -------------------------------------------------------------------------------- /controllers/redirects/_redirect_test.htm: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 7 | 8 |
9 |
    10 |
  • >http
  • 11 |
  • >https
  • 12 |
13 | 14 |
15 |
16 |
17 | 20 | 26 |

27 |
28 |
29 |
30 |
31 | makePartial('redirect_test_result'); ?> 32 |
33 |
34 | 37 |
42 |
43 | 44 | 49 |
50 |
51 |

52 |
53 | 63 |
64 |
65 | 80 | -------------------------------------------------------------------------------- /controllers/redirects/_redirect_test_result.htm: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 | 6 |
7 |
8 | 9 |
10 | 11 |
12 | (getStatusCode(); ?>) 13 |
14 |
15 | 16 | -------------------------------------------------------------------------------- /controllers/redirects/_reorder_toolbar.htm: -------------------------------------------------------------------------------- 1 |
2 | 4 | 5 | 6 |
7 | -------------------------------------------------------------------------------- /controllers/redirects/_sparkline.htm: -------------------------------------------------------------------------------- 1 |
2 | <?= e(trans('vdlp.redirect::lang.redirect.sparkline_30d')); ?> 6 |
7 | -------------------------------------------------------------------------------- /controllers/redirects/_status_code_info.htm: -------------------------------------------------------------------------------- 1 | 5 | 6 |
7 |

301 (Moved Permanently)

8 |

The 301 (Moved Permanently) status code indicates that the target 9 | resource has been assigned a new permanent URI and any future 10 | references to this resource ought to use one of the enclosed URIs. 11 | Clients with link-editing capabilities ought to automatically re-link 12 | references to the effective request URI to one or more of the new 13 | references sent by the server, where possible.

14 | 15 |

302 (Found)

16 |

The 302 (Found) status code indicates that the target resource 17 | resides temporarily under a different URI. Since the redirection 18 | might be altered on occasion, the client ought to continue to use the 19 | effective request URI for future requests. 20 |

21 | 22 |

303 (See Other)

23 |

The 303 (See Other) status code indicates that the server is 24 | redirecting the user agent to a different resource, as indicated by a 25 | URI in the Location header field, which is intended to provide an 26 | indirect response to the original request. A user agent can perform 27 | a retrieval request targeting that URI (a GET or HEAD request if 28 | using HTTP), which might also be redirected, and present the eventual 29 | result as an answer to the original request. Note that the new URI 30 | in the Location header field is not considered equivalent to the 31 | effective request URI.

32 | 33 |

404 (Not Found)

34 |

The 404 (Not Found) status code indicates that the origin server did 35 | not find a current representation for the target resource or is not 36 | willing to disclose that one exists. A 404 status code does not 37 | indicate whether this lack of representation is temporary or 38 | permanent; the 410 (Gone) status code is preferred over 404 if the 39 | origin server knows, presumably through some configurable means, that 40 | the condition is likely to be permanent.

41 | 42 |

410 (Gone)

43 |

The 410 (Gone) status code indicates that access to the target 44 | resource is no longer available at the origin server and that this 45 | condition is likely to be permanent. If the origin server does not 46 | know, or has no facility to determine, whether or not the condition 47 | is permanent, the status code 404 (Not Found) ought to be used 48 | instead.

49 |
50 | -------------------------------------------------------------------------------- /controllers/redirects/_tab_logs.htm: -------------------------------------------------------------------------------- 1 | relationRender('logs') ?> 2 | -------------------------------------------------------------------------------- /controllers/redirects/_warning.htm: -------------------------------------------------------------------------------- 1 |
2 | 6 |
7 | 8 |

9 |

10 |
11 |
12 | -------------------------------------------------------------------------------- /controllers/redirects/config_filter.yaml: -------------------------------------------------------------------------------- 1 | scopes: 2 | system: 3 | label: vdlp.redirect::lang.list.switch_system 4 | type: switch 5 | conditions: 6 | - "`system` <> true" 7 | - "`system` = true" 8 | is_enabled: 9 | label: vdlp.redirect::lang.list.switch_is_enabled 10 | type: switch 11 | conditions: 12 | - is_enabled <> true 13 | - is_enabled = true 14 | match_type: 15 | label: vdlp.redirect::lang.redirect.match_type 16 | type: group 17 | modelClass: Vdlp\Redirect\Models\Redirect 18 | options: filterMatchTypeOptions 19 | conditions: match_type in (:filtered) 20 | target_type: 21 | label: vdlp.redirect::lang.redirect.target_type 22 | type: group 23 | modelClass: Vdlp\Redirect\Models\Redirect 24 | options: filterTargetTypeOptions 25 | conditions: target_type in (:filtered) 26 | status_code: 27 | label: vdlp.redirect::lang.redirect.status_code 28 | type: group 29 | modelClass: Vdlp\Redirect\Models\Redirect 30 | options: filterStatusCodeOptions 31 | conditions: status_code in (:filtered) 32 | category: 33 | label: vdlp.redirect::lang.redirect.category 34 | modelClass: Vdlp\Redirect\Models\Category 35 | conditions: category_id in (:filtered) 36 | nameFrom: name 37 | hits: 38 | label: vdlp.redirect::lang.redirect.has_hits 39 | type: switch 40 | conditions: 41 | - hits = 0 42 | - hits <> 0 43 | minimum_hits: 44 | label: vdlp.redirect::lang.redirect.minimum_hits 45 | type: number 46 | conditions: hits >= ':filtered' 47 | -------------------------------------------------------------------------------- /controllers/redirects/config_form.yaml: -------------------------------------------------------------------------------- 1 | # =================================== 2 | # Form Behavior Config 3 | # =================================== 4 | 5 | # Record name 6 | name: vdlp.redirect::lang.redirect.redirect 7 | 8 | # Model Form Field configuration 9 | form: $/vdlp/redirect/models/redirect/fields.yaml 10 | 11 | # Model Class name 12 | modelClass: Vdlp\Redirect\Models\Redirect 13 | 14 | # Default redirect location 15 | defaultRedirect: vdlp/redirect/redirects 16 | 17 | # Create page 18 | create: 19 | title: vdlp.redirect::lang.title.create_redirect 20 | redirect: vdlp/redirect/redirects/update/:id 21 | redirectClose: vdlp/redirect/redirects 22 | 23 | # Update page 24 | update: 25 | title: vdlp.redirect::lang.title.edit_redirect 26 | redirect: vdlp/redirect/redirects 27 | redirectClose: vdlp/redirect/redirects 28 | -------------------------------------------------------------------------------- /controllers/redirects/config_import_export.yaml: -------------------------------------------------------------------------------- 1 | # =================================== 2 | # Import/Export Behavior Config 3 | # =================================== 4 | 5 | import: 6 | title: vdlp.redirect::lang.title.import 7 | modelClass: Vdlp\Redirect\Models\RedirectImport 8 | list: $/vdlp/redirect/models/redirectimport/columns.yaml 9 | redirect: vdlp/redirect/redirects 10 | export: 11 | title: vdlp.redirect::lang.title.export 12 | modelClass: Vdlp\Redirect\Models\RedirectExport 13 | list: $/vdlp/redirect/models/redirectexport/columns.yaml 14 | redirect: vdlp/redirect/redirects 15 | filename: redirects.csv 16 | -------------------------------------------------------------------------------- /controllers/redirects/config_list.yaml: -------------------------------------------------------------------------------- 1 | # =================================== 2 | # List Behavior Config 3 | # =================================== 4 | 5 | # Model List Column configuration 6 | list: $/vdlp/redirect/models/redirect/columns.yaml 7 | 8 | # Model Class name 9 | modelClass: Vdlp\Redirect\Models\Redirect 10 | 11 | # List Title 12 | title: vdlp.redirect::lang.title.redirects 13 | 14 | # Link URL for each record 15 | recordUrl: vdlp/redirect/redirects/update/:id 16 | 17 | # Message to display if the list is empty 18 | noRecordsMessage: vdlp.redirect::lang.list.no_records 19 | 20 | # Records to display per page 21 | recordsPerPage: 20 22 | 23 | # Displays the list column set up button 24 | showSetup: true 25 | 26 | # Displays the sorting link on each column 27 | showSorting: true 28 | 29 | # Default sorting column 30 | defaultSort: 31 | column: sort_order 32 | direction: asc 33 | 34 | # Display checkboxes next to each record 35 | showCheckboxes: true 36 | 37 | # Toolbar widget configuration 38 | toolbar: 39 | # Partial for toolbar buttons 40 | buttons: list_toolbar 41 | 42 | # Search widget configuration 43 | search: 44 | prompt: backend::lang.list.search_prompt 45 | 46 | filter: config_filter.yaml 47 | -------------------------------------------------------------------------------- /controllers/redirects/config_relation.yaml: -------------------------------------------------------------------------------- 1 | # =================================== 2 | # Relation Behavior Config 3 | # =================================== 4 | 5 | logs: 6 | label: Log 7 | view: 8 | list: $/vdlp/redirect/models/redirectlog/columns.yaml 9 | toolbarButtons: delete 10 | showSearch: true 11 | showSorting: true 12 | recordsPerPage: 10 13 | defaultSort: updated_at 14 | -------------------------------------------------------------------------------- /controllers/redirects/config_reorder.yaml: -------------------------------------------------------------------------------- 1 | # =================================== 2 | # Reorder Behavior Config 3 | # =================================== 4 | 5 | # Reorder Title 6 | title: vdlp.redirect::lang.buttons.reorder_redirects 7 | 8 | # Attribute name 9 | nameFrom: from_url 10 | 11 | # Model Class name 12 | modelClass: Vdlp\Redirect\Models\Redirect 13 | 14 | # Toolbar widget configuration 15 | toolbar: 16 | # Partial for toolbar buttons 17 | buttons: reorder_toolbar 18 | -------------------------------------------------------------------------------- /controllers/redirects/create.htm: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | fatalError): ?> 9 | 10 | 'layout']); ?> 11 | 12 |
13 | formRender(); ?> 14 |
15 | 16 |
17 |
18 | 27 | 35 | 44 | 45 | 46 | 47 |
48 |
49 | 50 | 51 | 52 | 53 | 54 |

fatalError); ?>

55 |

56 | 57 | 58 | -------------------------------------------------------------------------------- /controllers/redirects/export.htm: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 'layout']); ?> 9 |
10 | exportRender(); ?> 11 |
12 |
13 |
14 | 22 |
23 |
24 | 25 | -------------------------------------------------------------------------------- /controllers/redirects/import.htm: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 'layout']); ?> 9 |
10 | importRender(); ?> 11 |
12 |
13 | 21 |
22 | 23 | -------------------------------------------------------------------------------- /controllers/redirects/index.htm: -------------------------------------------------------------------------------- 1 | 2 | makePartial('warning', ['warningMessage' => $warningMessage]); ?> 3 | 4 | 5 | listRender(); ?> 6 | -------------------------------------------------------------------------------- /controllers/redirects/reorder.htm: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | reorderRender(); ?> 9 | -------------------------------------------------------------------------------- /controllers/redirects/request-log/_list_toolbar.htm: -------------------------------------------------------------------------------- 1 | 14 | 24 |
25 | 37 |
38 | -------------------------------------------------------------------------------- /controllers/redirects/request-log/_modal.htm: -------------------------------------------------------------------------------- 1 | 5 | 8 | -------------------------------------------------------------------------------- /controllers/redirects/request-log/columns.yaml: -------------------------------------------------------------------------------- 1 | # =================================== 2 | # Column Definitions 3 | # =================================== 4 | 5 | columns: 6 | url: 7 | label: system::lang.request_log.url 8 | searchable: yes 9 | cssClass: column-break-word 10 | 11 | status_code: 12 | label: system::lang.request_log.status_code 13 | searchable: yes 14 | width: 100px 15 | 16 | count: 17 | label: system::lang.request_log.count 18 | width: 150px 19 | -------------------------------------------------------------------------------- /controllers/redirects/request-log/config_list.yaml: -------------------------------------------------------------------------------- 1 | # =================================== 2 | # List Behavior Config 3 | # =================================== 4 | 5 | title: system::lang.request_log.menu_label 6 | list: $/vdlp/redirect/controllers/redirects/request-log/columns.yaml 7 | modelClass: System\Models\RequestLog 8 | noRecordsMessage: backend::lang.list.no_records 9 | recordsPerPage: 10 10 | showSetup: true 11 | showCheckboxes: true 12 | defaultSort: 13 | column: count 14 | direction: desc 15 | 16 | toolbar: 17 | buttons: request-log/list_toolbar 18 | search: 19 | prompt: backend::lang.list.search_prompt 20 | -------------------------------------------------------------------------------- /controllers/redirects/update.htm: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | fatalError): ?> 9 | 10 | 11 |
12 | 13 |
14 | 15 | makePartial('warning', ['warningMessage' => $warningMessage]); ?> 16 | 17 | formRenderOutsideFields() ?> 18 | formRenderPrimaryTabs() ?> 19 |
20 | 21 |
22 |
23 | 32 | 41 | 48 | 49 | 50 | 51 |
52 |
53 |
54 | 55 | 56 | 57 | 58 |
59 | formRenderSecondaryTabs() ?> 60 |
61 | 62 | 63 | 64 | 'layout stretch']) ?> 65 | makeLayout('form-with-sidebar') ?> 66 | 67 | 68 | 69 | 70 | 71 |
72 | 73 |
74 |
75 |

fatalError); ?>

76 |

77 |
78 | 79 | 80 | -------------------------------------------------------------------------------- /controllers/statistics/_hits-per-day.htm: -------------------------------------------------------------------------------- 1 | 30 |
31 |
32 |

33 |
34 |
35 | 50 |
51 |
52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /controllers/statistics/_loading-indicator.htm: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | -------------------------------------------------------------------------------- /controllers/statistics/_redirect-hits-per-month.htm: -------------------------------------------------------------------------------- 1 |

2 | 3 |
4 | 13 |
14 | 15 |

16 | 17 | -------------------------------------------------------------------------------- /controllers/statistics/_score-board.htm: -------------------------------------------------------------------------------- 1 |
2 |
3 |
7 |
    8 | $activeRedirect): ?> 9 |
  • 10 |
  • 11 | 12 |
13 |
14 |
15 |

16 |

17 |

18 |
19 |
20 |

21 |

22 |
23 |
24 |

25 |

26 |

:

27 |
28 | 29 |
30 |

31 |

redirect->from_url) ?>

32 |

timestamp) ?>

33 |
34 | 35 |
36 |
37 | -------------------------------------------------------------------------------- /controllers/statistics/_top-crawlers-this-month.htm: -------------------------------------------------------------------------------- 1 |

10])); ?>

2 | 3 |
4 | 13 |
14 | 15 |

16 | 17 | -------------------------------------------------------------------------------- /controllers/statistics/_top-redirects-this-month.htm: -------------------------------------------------------------------------------- 1 |

10])); ?>

2 | 3 |
4 | 17 |
18 | 19 |

20 | 21 | -------------------------------------------------------------------------------- /controllers/statistics/index.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 |
6 | makePartial('loading-indicator'); ?> 7 | 14 |
15 |
16 |
17 | 18 |
19 |
20 |
21 | makePartial('loading-indicator'); ?> 22 | 23 |
24 |
25 |
26 | 27 |
28 |
29 |
30 | makePartial('loading-indicator'); ?> 31 | 32 |
33 |
34 |
35 |
36 | makePartial('loading-indicator'); ?> 37 | 38 |
39 |
40 |
41 |
42 | makePartial('loading-indicator'); ?> 43 | 44 |
45 |
46 |
47 | -------------------------------------------------------------------------------- /controllers/testlab/_test_button.htm: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /controllers/testlab/_tester_failed.htm: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | 7 |
8 |
9 | -------------------------------------------------------------------------------- /controllers/testlab/_tester_result.htm: -------------------------------------------------------------------------------- 1 |
2 |
3 | to_url)): ?> 4 | 5 | to_url) ?> 6 | 7 |
8 |
9 |
10 |
11 | id); ?> 12 | 14 | 15 | 16 | 22 | 30 |
31 |
32 |
33 |
34 |
35 | makePartial('tester_result_items', $testResults); ?> 36 |
37 | -------------------------------------------------------------------------------- /controllers/testlab/_tester_result_items.htm: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 1. 5 |
6 |
getMessage(); ?>
7 |
getDuration(); ?> ms
8 |
9 |
10 |
11 | 12 | 2. 13 |
14 |
getMessage(); ?>
15 |
getDuration(); ?> ms
16 |
17 |
18 |
19 | 20 | 3. 21 |
22 |
getMessage(); ?>
23 |
getDuration(); ?> ms
24 |
25 |
26 |
27 | 28 | 4. 29 |
30 |
getMessage(); ?>
31 |
getDuration(); ?> ms
32 |
33 |
34 |
35 | 36 | 5. 37 |
38 |
getMessage(); ?>
39 |
getDuration(); ?> ms
40 |
41 | -------------------------------------------------------------------------------- /controllers/testlab/index.htm: -------------------------------------------------------------------------------- 1 | 'layout']); ?> 2 | 3 | 4 | 5 |
6 | 7 |
8 |
9 |
16 |
17 |
18 |

19 | 20 |

21 |
22 |
23 | makePartial('test_button', ['redirectCount' => $redirectCount]); ?> 24 |
25 |
26 |
27 | 28 |
29 |
30 | 35 |
36 |

37 |
38 |
39 |
40 | 41 |
42 | 43 | 44 | 45 | 53 | -------------------------------------------------------------------------------- /models/Category.php: -------------------------------------------------------------------------------- 1 | Redirect::class, 13 | ]; 14 | 15 | public $timestamps = false; 16 | 17 | protected $table = 'vdlp_redirect_clients'; 18 | 19 | protected $guarded = []; 20 | } 21 | -------------------------------------------------------------------------------- /models/RedirectExport.php: -------------------------------------------------------------------------------- 1 | get() 17 | ->toArray(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /models/RedirectImport.php: -------------------------------------------------------------------------------- 1 | 'required', 19 | 'match_type' => 'required|in:exact,placeholders,regex', 20 | 'target_type' => 'required|in:path_or_url,cms_page,static_page,none', 21 | 'status_code' => 'required|in:301,302,303,404,410', 22 | ]; 23 | 24 | private static array $nullableAttributes = [ 25 | 'category_id', 26 | 'from_date', 27 | 'to_date', 28 | 'last_used_at', 29 | 'to_url', 30 | 'test_url', 31 | 'cms_page', 32 | 'static_page', 33 | 'requirements', 34 | 'test_lab_path', 35 | ]; 36 | 37 | protected $table = 'vdlp_redirect_redirects'; 38 | 39 | public function importData($results, $sessionKey = null) 40 | { 41 | foreach ((array) $results as $row => $data) { 42 | try { 43 | $source = Redirect::make(); 44 | 45 | $except = ['id']; 46 | 47 | foreach (array_except($data, $except) as $attribute => $value) { 48 | if ($attribute === 'requirements') { 49 | $value = json_decode($value, true, 512, JSON_THROW_ON_ERROR); 50 | } elseif (empty($value) && in_array($attribute, self::$nullableAttributes, true)) { 51 | $value = null; 52 | } 53 | 54 | $source->setAttribute($attribute, $value); 55 | } 56 | 57 | $source->save(); 58 | 59 | $this->logCreated(); 60 | } catch (Throwable $e) { 61 | $this->logError($row, $e->getMessage()); 62 | } 63 | } 64 | 65 | $createCount = $this->resultStats['created'] ?? 0; 66 | 67 | if ($createCount === 0) { 68 | return; 69 | } 70 | 71 | /** @var PublishManager $publishManager */ 72 | $publishManager = resolve(PublishManager::class); 73 | $publishManager->publish(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /models/RedirectLog.php: -------------------------------------------------------------------------------- 1 | Redirect::class, 13 | ]; 14 | 15 | protected $table = 'vdlp_redirect_redirect_logs'; 16 | 17 | protected $guarded = []; 18 | } 19 | -------------------------------------------------------------------------------- /models/Settings.php: -------------------------------------------------------------------------------- 1 | implement = [SettingsModel::class]; 30 | 31 | parent::__construct($attributes); 32 | } 33 | 34 | public static function isLoggingEnabled(): bool 35 | { 36 | try { 37 | return (bool) (new self())->get('logging_enabled', false); 38 | } catch (Throwable) { 39 | return false; 40 | } 41 | } 42 | 43 | public static function isStatisticsEnabled(): bool 44 | { 45 | try { 46 | return (bool) (new self())->get('statistics_enabled', false); 47 | } catch (Throwable) { 48 | return false; 49 | } 50 | } 51 | 52 | public static function isTestLabEnabled(): bool 53 | { 54 | try { 55 | return (bool) (new self())->get('test_lab_enabled', false); 56 | } catch (Throwable) { 57 | return false; 58 | } 59 | } 60 | 61 | public static function isCachingEnabled(): bool 62 | { 63 | try { 64 | return (bool) (new self())->get('caching_enabled', false); 65 | } catch (Throwable) { 66 | return false; 67 | } 68 | } 69 | 70 | public static function isRelativePathsEnabled(): bool 71 | { 72 | try { 73 | return (bool) (new self())->get('relative_paths_enabled', true); 74 | } catch (Throwable) { 75 | return true; 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /models/category/columns.yaml: -------------------------------------------------------------------------------- 1 | # =================================== 2 | # List Column Definitions 3 | # =================================== 4 | 5 | columns: 6 | name: 7 | label: vdlp.redirect::lang.redirect.name 8 | searchable: true 9 | -------------------------------------------------------------------------------- /models/category/fields.yaml: -------------------------------------------------------------------------------- 1 | # =================================== 2 | # Form Field Definitions 3 | # =================================== 4 | 5 | fields: 6 | name: 7 | label: vdlp.redirect::lang.redirect.name 8 | span: left 9 | -------------------------------------------------------------------------------- /models/client/columns.yaml: -------------------------------------------------------------------------------- 1 | # =================================== 2 | # List Column Definitions 3 | # =================================== 4 | 5 | columns: 6 | id: 7 | label: ID 8 | searchable: true -------------------------------------------------------------------------------- /models/client/fields.yaml: -------------------------------------------------------------------------------- 1 | # =================================== 2 | # Form Field Definitions 3 | # =================================== 4 | 5 | fields: 6 | id: 7 | label: ID 8 | disabled: true 9 | -------------------------------------------------------------------------------- /models/redirect/columns.yaml: -------------------------------------------------------------------------------- 1 | # =================================== 2 | # List Column Definitions 3 | # =================================== 4 | 5 | columns: 6 | from_url: 7 | label: vdlp.redirect::lang.redirect.from_url 8 | type: redirect_from_url 9 | searchable: true 10 | target_type: 11 | label: vdlp.redirect::lang.redirect.target_type 12 | type: redirect_target_type 13 | status_code: 14 | label: vdlp.redirect::lang.redirect.status_code 15 | type: redirect_status_code 16 | match_type: 17 | label: vdlp.redirect::lang.redirect.match_type 18 | type: redirect_match_type 19 | category: 20 | label: vdlp.redirect::lang.redirect.category 21 | relation: category 22 | select: name 23 | default: '-' 24 | description: 25 | label: vdlp.redirect::lang.redirect.description 26 | invisible: true 27 | default: '-' 28 | hits: 29 | label: vdlp.redirect::lang.redirect.hits 30 | type: number 31 | last_used_at: 32 | label: vdlp.redirect::lang.redirect.last_used_at 33 | type: datetime 34 | sparkline: 35 | label: vdlp.redirect::lang.redirect.sparkline_30d 36 | sortable: false 37 | searchable: false 38 | cssClass: column-button 39 | type: partial 40 | sort_order: 41 | label: vdlp.redirect::lang.redirect.priority 42 | is_enabled: 43 | label: vdlp.redirect::lang.redirect.enabled 44 | type: redirect_switch_color 45 | system: 46 | label: vdlp.redirect::lang.redirect.type 47 | width: 16px 48 | type: redirect_system 49 | sortable: false 50 | invisible: true 51 | updated_at: 52 | label: vdlp.redirect::lang.redirect.modified_at 53 | type: datetime 54 | invisible: true 55 | created_at: 56 | label: vdlp.redirect::lang.redirect.created_at 57 | type: datetime 58 | invisible: true 59 | -------------------------------------------------------------------------------- /models/redirectexport/columns.yaml: -------------------------------------------------------------------------------- 1 | columns: 2 | id: ID 3 | match_type: vdlp.redirect::lang.import_export.match_type 4 | category_id: vdlp.redirect::lang.import_export.category_id 5 | target_type: vdlp.redirect::lang.import_export.target_type 6 | from_url: vdlp.redirect::lang.import_export.from_url 7 | from_scheme: vdlp.redirect::lang.import_export.from_scheme 8 | to_url: vdlp.redirect::lang.import_export.to_url 9 | to_scheme: vdlp.redirect::lang.import_export.to_scheme 10 | test_url: vdlp.redirect::lang.import_export.test_url 11 | cms_page: vdlp.redirect::lang.import_export.cms_page 12 | static_page: vdlp.redirect::lang.import_export.static_page 13 | requirements: vdlp.redirect::lang.import_export.requirements 14 | status_code: vdlp.redirect::lang.import_export.status_code 15 | hits: vdlp.redirect::lang.import_export.hits 16 | from_date: vdlp.redirect::lang.import_export.from_date 17 | to_date: vdlp.redirect::lang.import_export.to_date 18 | sort_order: vdlp.redirect::lang.import_export.sort_order 19 | ignore_query_parameters: vdlp.redirect::lang.import_export.ignore_query_parameters 20 | ignore_case: vdlp.redirect::lang.import_export.ignore_case 21 | ignore_trailing_slash: vdlp.redirect::lang.import_export.ignore_trailing_slash 22 | is_enabled: vdlp.redirect::lang.import_export.is_enabled 23 | test_lab: vdlp.redirect::lang.import_export.test_lab 24 | test_lab_path: vdlp.redirect::lang.import_export.test_lab_path 25 | system: vdlp.redirect::lang.import_export.system 26 | description: vdlp.redirect::lang.import_export.description 27 | last_used_at: vdlp.redirect::lang.import_export.last_used_at 28 | created_at: vdlp.redirect::lang.import_export.created_at 29 | updated_at: vdlp.redirect::lang.import_export.updated_at 30 | -------------------------------------------------------------------------------- /models/redirectimport/columns.yaml: -------------------------------------------------------------------------------- 1 | columns: 2 | id: ID 3 | match_type: vdlp.redirect::lang.import_export.match_type 4 | category_id: vdlp.redirect::lang.import_export.category_id 5 | target_type: vdlp.redirect::lang.import_export.target_type 6 | from_url: vdlp.redirect::lang.import_export.from_url 7 | from_scheme: vdlp.redirect::lang.import_export.from_scheme 8 | to_url: vdlp.redirect::lang.import_export.to_url 9 | to_scheme: vdlp.redirect::lang.import_export.to_scheme 10 | test_url: vdlp.redirect::lang.import_export.test_url 11 | cms_page: vdlp.redirect::lang.import_export.cms_page 12 | static_page: vdlp.redirect::lang.import_export.static_page 13 | requirements: vdlp.redirect::lang.import_export.requirements 14 | status_code: vdlp.redirect::lang.import_export.status_code 15 | hits: vdlp.redirect::lang.import_export.hits 16 | from_date: vdlp.redirect::lang.import_export.from_date 17 | to_date: vdlp.redirect::lang.import_export.to_date 18 | sort_order: vdlp.redirect::lang.import_export.sort_order 19 | ignore_query_parameters: vdlp.redirect::lang.import_export.ignore_query_parameters 20 | ignore_case: vdlp.redirect::lang.import_export.ignore_case 21 | ignore_trailing_slash: vdlp.redirect::lang.import_export.ignore_trailing_slash 22 | is_enabled: vdlp.redirect::lang.import_export.is_enabled 23 | test_lab: vdlp.redirect::lang.import_export.test_lab 24 | test_lab_path: vdlp.redirect::lang.import_export.test_lab_path 25 | system: vdlp.redirect::lang.import_export.system 26 | description: vdlp.redirect::lang.import_export.description 27 | last_used_at: vdlp.redirect::lang.import_export.last_used_at 28 | created_at: vdlp.redirect::lang.import_export.created_at 29 | updated_at: vdlp.redirect::lang.import_export.updated_at 30 | -------------------------------------------------------------------------------- /models/redirectlog/columns.yaml: -------------------------------------------------------------------------------- 1 | # =================================== 2 | # List Column Definitions 3 | # =================================== 4 | 5 | columns: 6 | from_url: 7 | label: vdlp.redirect::lang.redirect.from_url 8 | type: redirect_from_url 9 | searchable: true 10 | to_url: 11 | label: vdlp.redirect::lang.redirect.to_url 12 | searchable: true 13 | status_code: 14 | label: vdlp.redirect::lang.redirect.status_code 15 | type: redirect_status_code 16 | searchable: true 17 | hits: 18 | label: vdlp.redirect::lang.redirect.hits 19 | updated_at: 20 | label: vdlp.redirect::lang.redirect.date_time 21 | type: datetime 22 | -------------------------------------------------------------------------------- /models/settings/fields.yaml: -------------------------------------------------------------------------------- 1 | # =================================== 2 | # Settings Form Field Definitions 3 | # =================================== 4 | 5 | fields: 6 | redirect_settings_section: 7 | label: vdlp.redirect::lang.settings.menu_label 8 | comment: vdlp.redirect::lang.settings.menu_description 9 | type: section 10 | span: left 11 | relative_paths_enabled: 12 | label: vdlp.redirect::lang.settings.relative_paths_enabled_label 13 | comment: vdlp.redirect::lang.settings.relative_paths_enabled_command 14 | span: left 15 | type: switch 16 | default: true 17 | logging_enabled: 18 | label: vdlp.redirect::lang.settings.logging_enabled_label 19 | comment: vdlp.redirect::lang.settings.logging_enabled_comment 20 | span: left 21 | type: switch 22 | default: false 23 | statistics_enabled: 24 | label: vdlp.redirect::lang.settings.statistics_enabled_label 25 | comment: vdlp.redirect::lang.settings.statistics_enabled_comment 26 | span: left 27 | type: switch 28 | default: false 29 | test_lab_enabled: 30 | label: vdlp.redirect::lang.settings.test_lab_enabled_label 31 | comment: vdlp.redirect::lang.settings.test_lab_enabled_comment 32 | span: left 33 | type: switch 34 | default: false 35 | caching_enabled: 36 | label: vdlp.redirect::lang.settings.caching_enabled_label 37 | comment: vdlp.redirect::lang.settings.caching_enabled_comment 38 | span: left 39 | type: switch 40 | default: false 41 | 42 | -------------------------------------------------------------------------------- /reportwidgets/CreateRedirect.php: -------------------------------------------------------------------------------- 1 | alias = 'redirectCreateRedirect'; 25 | 26 | parent::__construct($controller, $properties); 27 | 28 | $this->redirect = resolve(Redirector::class); 29 | } 30 | 31 | /** 32 | * @noinspection PhpMissingParentCallCommonInspection 33 | */ 34 | public function render() 35 | { 36 | $widgetConfig = $this->makeConfig('~/plugins/vdlp/redirect/reportwidgets/createredirect/fields.yaml'); 37 | $widgetConfig->model = new Redirect; 38 | $widgetConfig->alias = $this->alias . 'Redirect'; 39 | 40 | $this->vars['formWidget'] = $this->makeWidget(Form::class, $widgetConfig); 41 | 42 | return $this->makePartial('widget'); 43 | } 44 | 45 | public function onSubmit(): RedirectResponse 46 | { 47 | $redirect = Redirect::create([ 48 | 'match_type' => Redirect::TYPE_EXACT, 49 | 'target_type' => Redirect::TARGET_TYPE_PATH_URL, 50 | 'from_url' => post('from_url'), 51 | 'from_scheme' => Redirect::SCHEME_AUTO, 52 | 'to_url' => post('to_url'), 53 | 'to_scheme' => Redirect::SCHEME_AUTO, 54 | 'test_url' => post('from_url'), 55 | 'requirements' => null, 56 | 'status_code' => 302, 57 | ]); 58 | 59 | return $this->redirect->to(Backend::url('vdlp/redirect/redirects/update/' . $redirect->getKey())); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /reportwidgets/TopTenRedirects.php: -------------------------------------------------------------------------------- 1 | alias = 'redirectTopTenRedirects'; 19 | 20 | parent::__construct($controller, $properties); 21 | } 22 | 23 | /** 24 | * @noinspection PhpMissingParentCallCommonInspection 25 | */ 26 | public function render() 27 | { 28 | $helper = new StatisticsHelper(); 29 | 30 | return $this->makePartial('widget', [ 31 | 'topTenRedirectsThisMonth' => $helper->getTopRedirectsThisMonth(), 32 | ]); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /reportwidgets/createredirect/fields.yaml: -------------------------------------------------------------------------------- 1 | # =================================== 2 | # Form Field Definitions 3 | # =================================== 4 | 5 | fields: 6 | from_url: 7 | label: vdlp.redirect::lang.redirect.from_url 8 | placeholder: vdlp.redirect::lang.redirect.from_url_placeholder 9 | type: text 10 | span: left 11 | comment: vdlp.redirect::lang.redirect.from_url_comment 12 | required: true 13 | attributes: 14 | autofocus: '' 15 | to_url: 16 | label: vdlp.redirect::lang.redirect.to_url 17 | placeholder: vdlp.redirect::lang.redirect.to_url_placeholder 18 | type: text 19 | span: right 20 | comment: vdlp.redirect::lang.redirect.to_url_comment 21 | -------------------------------------------------------------------------------- /reportwidgets/createredirect/partials/_widget.htm: -------------------------------------------------------------------------------- 1 |
2 |

3 | 4 | render() ?> 5 | 10 | 11 |
12 | -------------------------------------------------------------------------------- /reportwidgets/toptenredirects/partials/_widget.htm: -------------------------------------------------------------------------------- 1 |
2 |

10])) ?>

3 | 4 |
5 | 18 |
19 | 20 |

21 | 22 |
23 | -------------------------------------------------------------------------------- /routes.php: -------------------------------------------------------------------------------- 1 | ['web']], static function (): void { 14 | Route::get('vdlp/redirect/sparkline/{redirectId}', static function (Request $request, $redirectId) { 15 | if (!BackendAuth::check()) { 16 | return response('Forbidden', 403); 17 | } 18 | 19 | $crawler = $request->has('crawler'); 20 | 21 | $preset = $request->get('preset', '30d-small'); 22 | 23 | $properties = [ 24 | 'format' => '200x60', 25 | 'lineThickness' => 3, 26 | 'days' => 30, 27 | ]; 28 | 29 | if ($preset === '3m-large') { 30 | $properties = [ 31 | 'format' => '520x120', 32 | 'lineThickness' => 2, 33 | 'days' => 90, 34 | ]; 35 | } 36 | 37 | $cacheKey = sprintf('vdlp_redirect_%s_%d_%d', $preset, (int) $redirectId, (int) $crawler); 38 | 39 | $data = Cache::remember($cacheKey, 5 * 60, static function () use ($redirectId, $crawler, $properties) { 40 | return (new StatisticsHelper())->getRedirectHitsSparkline((int) $redirectId, $crawler, $properties['days']); 41 | }); 42 | 43 | // TODO: Generate fallback image data if generating image fails. 44 | $imageData = Cache::remember($cacheKey . '_image', 5 * 60, static function () use ($crawler, $data, $properties) { 45 | $primaryColor = BrandHelper::instance() 46 | ->getPrimaryOrSecondaryColor($crawler); 47 | 48 | $sparkline = new Sparkline(); 49 | $sparkline->setFormat($properties['format']); 50 | $sparkline->setPadding('2 0 0 2'); 51 | $sparkline->setData($data); 52 | $sparkline->setLineThickness($properties['lineThickness']); 53 | $sparkline->setLineColorHex($primaryColor); 54 | $sparkline->setFillColorHex($primaryColor); 55 | $sparkline->deactivateBackgroundColor(); 56 | 57 | return $sparkline->toBase64(); 58 | }); 59 | 60 | // TODO: Leverage Browser Caching 61 | header('Content-Type: image/png'); 62 | header('Content-Disposition: inline; filename="' . $cacheKey . '.png"'); 63 | header('Accept-Ranges: none'); 64 | 65 | echo base64_decode($imageData, true); 66 | 67 | exit(0); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /updates/20180831_0002_upgrade_from_adrenth_redirect.php: -------------------------------------------------------------------------------- 1 | getSchemaBuilder(); 27 | 28 | if (!$schema->hasTable('adrenth_redirect_redirects')) { 29 | // Skip upgrade migration. 30 | $log->info('No upgrade of Vdlp.Redirect needed. Fresh installation.'); 31 | 32 | return; 33 | } 34 | 35 | try { 36 | $database->transaction(function () use ($database): void { 37 | $this->disableForeignKeyCheck($database); 38 | 39 | $mapping = [ 40 | 'adrenth_redirect_categories' => 'vdlp_redirect_categories', 41 | 'adrenth_redirect_redirects' => 'vdlp_redirect_redirects', 42 | 'adrenth_redirect_redirect_logs' => 'vdlp_redirect_redirect_logs', 43 | 'adrenth_redirect_clients' => 'vdlp_redirect_clients', 44 | ]; 45 | 46 | // language=ignore 47 | foreach ($mapping as $from => $to) { 48 | // Make sure newly created tables are empty. 49 | $database->table($to)->delete(); 50 | 51 | // Move data from old tables to new ones. 52 | $database->statement("INSERT INTO `$to` SELECT * FROM `$from`;"); 53 | } 54 | 55 | // Migrate plugin settings. 56 | $database->table('system_settings') 57 | ->where('item', '=', 'vdlp_redirect_settings') 58 | ->delete(); 59 | 60 | // language=ignore 61 | $database->statement( 62 | 'INSERT INTO `system_settings` ' 63 | . "SELECT NULL, 'vdlp_redirect_settings', `value` " 64 | . 'FROM `system_settings` ' 65 | . "WHERE `item` = 'adrenth_redirect_settings';" 66 | ); 67 | 68 | $this->enableForeignKeyCheck($database); 69 | }); 70 | } catch (Throwable $e) { 71 | $log->error(sprintf( 72 | 'Vdlp.Redirect: Could not upgrade plugin Vdlp.Redirect from Adrenth.Redirect: %s', 73 | $e->getMessage() 74 | )); 75 | } 76 | } 77 | 78 | public function down(): void 79 | { 80 | // No migrations to reverse. 81 | } 82 | 83 | private function disableForeignKeyCheck(DatabaseManager $database): void 84 | { 85 | if ($database->getDriverName() === 'sqlite') { 86 | $database->raw('PRAGMA foreign_keys = OFF;'); 87 | } 88 | 89 | if ($database->getDriverName() === 'mysql') { 90 | $database->raw('SET FOREIGN_KEY_CHECKS = 0;'); 91 | } 92 | 93 | if ($database->getDriverName() === 'pgsql') { 94 | $database->raw('SET CONSTRAINTS ALL DEFERRED;'); 95 | } 96 | } 97 | 98 | private function enableForeignKeyCheck(DatabaseManager $database): void 99 | { 100 | if ($database->getDriverName() === 'sqlite') { 101 | $database->raw('PRAGMA foreign_keys = ON;'); 102 | } 103 | 104 | if ($database->getDriverName() === 'mysql') { 105 | $database->raw('SET FOREIGN_KEY_CHECKS = 1;'); 106 | } 107 | 108 | if ($database->getDriverName() === 'pgsql') { 109 | $database->raw('PRAGMA foreign_keys = ON;'); 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /updates/20181019_0003_add_ignore_query_parameters_to_redirects_table.php: -------------------------------------------------------------------------------- 1 | boolean('ignore_query_parameters') 22 | ->default(false) 23 | ->after('sort_order'); 24 | }); 25 | } 26 | 27 | public function down(): void 28 | { 29 | try { 30 | Schema::table('vdlp_redirect_redirects', static function (Blueprint $table): void { 31 | $table->dropColumn('ignore_query_parameters'); 32 | }); 33 | } catch (Throwable $e) { 34 | /** @var LoggerInterface $logger */ 35 | $logger = resolve(LoggerInterface::class); 36 | $logger->error(sprintf( 37 | 'Vdlp.Redirect: Unable to drop column `%s` from table `%s`: %s', 38 | 'ignore_query_parameters', 39 | 'vdlp_redirect_redirects', 40 | $e->getMessage() 41 | )); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /updates/20181117_0004_add_redirect_timestamp_crawler_index_on_clients_table.php: -------------------------------------------------------------------------------- 1 | index( 19 | [ 20 | 'redirect_id', 21 | 'timestamp', 22 | 'crawler', 23 | ], 24 | 'redirect_timestamp_crawler' 25 | ); 26 | }); 27 | } 28 | 29 | public function down(): void 30 | { 31 | try { 32 | Schema::table('vdlp_redirect_clients', static function (Blueprint $table): void { 33 | $table->dropIndex('redirect_timestamp_crawler'); 34 | }); 35 | } catch (Throwable $e) { 36 | /** @var LoggerInterface $logger */ 37 | $logger = resolve(LoggerInterface::class); 38 | $logger->error(sprintf( 39 | 'Vdlp.Redirect: Unable to drop index `%s` from table `%s`: %s', 40 | 'redirect_timestamp_crawler', 41 | 'vdlp_redirect_clients', 42 | $e->getMessage() 43 | )); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /updates/20181117_0005_add_month_year_crawler_index_on_clients_table.php: -------------------------------------------------------------------------------- 1 | index( 19 | [ 20 | 'month', 21 | 'year', 22 | 'crawler', 23 | ], 24 | 'month_year_crawler' 25 | ); 26 | 27 | $table->index( 28 | [ 29 | 'month', 30 | 'year', 31 | ], 32 | 'month_year' 33 | ); 34 | }); 35 | } 36 | 37 | public function down(): void 38 | { 39 | try { 40 | Schema::table('vdlp_redirect_clients', static function (Blueprint $table): void { 41 | $table->dropIndex('month_year_crawler'); 42 | $table->dropIndex('month_year'); 43 | }); 44 | } catch (Throwable $e) { 45 | /** @var LoggerInterface $logger */ 46 | $logger = resolve(LoggerInterface::class); 47 | $logger->error(sprintf( 48 | 'Vdlp.Redirect: Unable to drop index `%s`, `%s` from table `%s`: %s', 49 | 'month_year_crawler', 50 | 'month_year', 51 | 'vdlp_redirect_clients', 52 | $e->getMessage() 53 | )); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /updates/20190404_0006_add_description_to_redirects_table.php: -------------------------------------------------------------------------------- 1 | string('description') 19 | ->nullable() 20 | ->after('system'); 21 | }); 22 | } 23 | 24 | public function down(): void 25 | { 26 | try { 27 | Schema::table('vdlp_redirect_redirects', static function (Blueprint $table): void { 28 | $table->dropColumn('description'); 29 | }); 30 | } catch (Throwable $e) { 31 | /** @var LoggerInterface $logger */ 32 | $logger = resolve(LoggerInterface::class); 33 | $logger->error(sprintf( 34 | 'Vdlp.Redirect: Unable to drop index `%s` from table `%s`: %s', 35 | 'description', 36 | 'vdlp_redirect_redirects', 37 | $e->getMessage() 38 | )); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /updates/20190704_0007_add_timestamp_crawler_index_on_clients_table.php: -------------------------------------------------------------------------------- 1 | index( 19 | [ 20 | 'timestamp', 21 | 'crawler', 22 | ], 23 | 'timestamp_crawler' 24 | ); 25 | }); 26 | } 27 | 28 | public function down(): void 29 | { 30 | try { 31 | Schema::table('vdlp_redirect_clients', static function (Blueprint $table): void { 32 | $table->dropIndex('timestamp_crawler'); 33 | }); 34 | } catch (Throwable $e) { 35 | /** @var LoggerInterface $logger */ 36 | $logger = resolve(LoggerInterface::class); 37 | $logger->error(sprintf( 38 | 'Vdlp.Redirect: Unable to drop index `%s` from table `%s`: %s', 39 | 'timestamp_crawler', 40 | 'vdlp_redirect_clients', 41 | $e->getMessage() 42 | )); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /updates/20200408_0008_change_column_types_from_char_to_varchar.php: -------------------------------------------------------------------------------- 1 | getDriverName() === 'pgsql') { 18 | $database->statement(implode(' ', [ 19 | 'ALTER TABLE vdlp_redirect_redirects', 20 | 'ALTER COLUMN match_type TYPE VARCHAR(12),', 21 | 'ALTER COLUMN target_type TYPE VARCHAR(12),', 22 | 'ALTER COLUMN from_scheme TYPE VARCHAR(5),', 23 | 'ALTER COLUMN to_scheme TYPE VARCHAR(5),', 24 | 'ALTER COLUMN status_code TYPE VARCHAR(3);', 25 | ])); 26 | 27 | $database->statement(implode(' ', [ 28 | 'ALTER TABLE vdlp_redirect_redirects', 29 | "ALTER COLUMN target_type SET DEFAULT 'path_or_url',", 30 | "ALTER COLUMN from_scheme SET DEFAULT 'auto',", 31 | "ALTER COLUMN to_scheme SET DEFAULT 'auto';" 32 | ])); 33 | 34 | $database->statement(implode(' ', [ 35 | 'ALTER TABLE vdlp_redirect_redirect_logs', 36 | 'ALTER COLUMN status_code TYPE VARCHAR(3);', 37 | ])); 38 | } 39 | 40 | if ($database->getDriverName() === 'mysql') { 41 | $database->statement('ALTER TABLE `vdlp_redirect_redirects` CHANGE `match_type` `match_type` VARCHAR(12) NULL DEFAULT NULL;'); 42 | $database->statement("ALTER TABLE `vdlp_redirect_redirects` CHANGE `target_type` `target_type` VARCHAR(12) NOT NULL DEFAULT 'path_or_url';"); 43 | $database->statement("ALTER TABLE `vdlp_redirect_redirects` CHANGE `from_scheme` `from_scheme` VARCHAR(5) NOT NULL DEFAULT 'auto';"); 44 | $database->statement("ALTER TABLE `vdlp_redirect_redirects` CHANGE `to_scheme` `to_scheme` VARCHAR(5) NOT NULL DEFAULT 'auto';"); 45 | $database->statement("ALTER TABLE `vdlp_redirect_redirects` CHANGE `status_code` `status_code` VARCHAR(3) NOT NULL DEFAULT '';"); 46 | $database->statement("ALTER TABLE `vdlp_redirect_redirect_logs` CHANGE `status_code` `status_code` VARCHAR(3) NOT NULL DEFAULT '';"); 47 | } 48 | 49 | // 'sqlite' does not support the char type, so it doesn't need to be altered. 50 | } 51 | 52 | public function down(): void 53 | { 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /updates/20200414_0009_add_ignore_case_to_redirects_table.php: -------------------------------------------------------------------------------- 1 | boolean('ignore_case') 15 | ->default(false) 16 | ->after('ignore_query_parameters'); 17 | }); 18 | } 19 | 20 | public function down(): void 21 | { 22 | try { 23 | Schema::table('vdlp_redirect_redirects', static function (Blueprint $table): void { 24 | $table->dropColumn('ignore_case'); 25 | }); 26 | } catch (Throwable $e) { 27 | /** @var LoggerInterface $logger */ 28 | $logger = resolve(LoggerInterface::class); 29 | $logger->error(sprintf( 30 | 'Vdlp.Redirect: Unable to drop column `%s` from table `%s`: %s', 31 | 'ignore_case', 32 | 'vdlp_redirect_redirects', 33 | $e->getMessage() 34 | )); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /updates/20200414_0010_add_ignore_trailing_slash_to_redirects_table.php: -------------------------------------------------------------------------------- 1 | boolean('ignore_trailing_slash') 15 | ->default(false) 16 | ->after('ignore_case'); 17 | }); 18 | } 19 | 20 | public function down(): void 21 | { 22 | try { 23 | Schema::table('vdlp_redirect_redirects', static function (Blueprint $table): void { 24 | $table->dropColumn('ignore_trailing_slash'); 25 | }); 26 | } catch (Throwable $e) { 27 | /** @var LoggerInterface $logger */ 28 | $logger = resolve(LoggerInterface::class); 29 | $logger->error(sprintf( 30 | 'Vdlp.Redirect: Unable to drop column `%s` from table `%s`: %s', 31 | 'ignore_trailing_slash', 32 | 'vdlp_redirect_redirects', 33 | $e->getMessage() 34 | )); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /updates/20200918_0011_refactor_redirects_logs_table.php: -------------------------------------------------------------------------------- 1 | getMessage(), PHP_EOL; 18 | echo 'Database table `vdlp_redirect_redirect_logs` could not be removed.', PHP_EOL; 19 | echo 'Please remove it manually and try running the database migrations again.', PHP_EOL; 20 | 21 | return; 22 | } 23 | 24 | Schema::create('vdlp_redirect_redirect_logs', static function (Blueprint $table): void { 25 | // Table MySQL configuration 26 | $table->engine = 'InnoDB'; 27 | 28 | // Columns 29 | $table->increments('id'); 30 | $table->unsignedInteger('redirect_id'); 31 | $table->string('from_to_hash', 40); 32 | $table->string('status_code', 3); 33 | $table->mediumText('from_url'); 34 | $table->mediumText('to_url'); 35 | $table->unsignedInteger('hits') 36 | ->default(0); 37 | $table->timestamps(); 38 | 39 | // Foreign keys 40 | $table->foreign('redirect_id', 'vdlp_redirect_log') 41 | ->references('id') 42 | ->on('vdlp_redirect_redirects') 43 | ->onDelete('cascade'); 44 | 45 | // Indexes 46 | $table->unique([ 47 | 'redirect_id', 48 | 'from_to_hash', 49 | 'status_code', 50 | ], 'redirect_log_unique'); 51 | }); 52 | } 53 | 54 | public function down(): void 55 | { 56 | try { 57 | Schema::disableForeignKeyConstraints(); 58 | Schema::dropIfExists('vdlp_redirect_redirect_logs'); 59 | Schema::enableForeignKeyConstraints(); 60 | } catch (Throwable $throwable) { 61 | echo 'Migration error: ' . $throwable->getMessage(), PHP_EOL; 62 | echo 'Database table `vdlp_redirect_redirect_logs` could not be removed.', PHP_EOL; 63 | echo 'Please remove it manually and try running the database migrations again.', PHP_EOL; 64 | 65 | return; 66 | } 67 | 68 | Schema::create('vdlp_redirect_redirect_logs', static function (Blueprint $table): void { 69 | // Table MySQL configuration 70 | $table->engine = 'InnoDB'; 71 | 72 | // Columns 73 | $table->increments('id'); 74 | $table->unsignedInteger('redirect_id'); 75 | $table->mediumText('from_url'); 76 | $table->mediumText('to_url'); 77 | $table->string('status_code', 3); 78 | $table->unsignedTinyInteger('day'); 79 | $table->unsignedTinyInteger('month'); 80 | $table->unsignedSmallInteger('year'); 81 | $table->dateTime('date_time'); 82 | 83 | // Indexes 84 | $table->index(['redirect_id', 'day', 'month', 'year'], 'redirect_log_dmy'); 85 | $table->index(['redirect_id', 'month', 'year'], 'redirect_log_my'); 86 | 87 | // Foreign keys 88 | $table->foreign('redirect_id', 'vdlp_redirect_log') 89 | ->references('id') 90 | ->on('vdlp_redirect_redirects') 91 | ->onDelete('cascade'); 92 | }); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /updates/20200918_0012_add_redirect_id_to_system_request_logs_table.php: -------------------------------------------------------------------------------- 1 | unsignedInteger('vdlp_redirect_redirect_id') 18 | ->nullable() 19 | ->after('id'); 20 | 21 | $table->foreign('vdlp_redirect_redirect_id', 'vdlp_redirect_request_log') 22 | ->references('id') 23 | ->on('vdlp_redirect_redirects') 24 | ->onDelete('set null'); 25 | }); 26 | } 27 | 28 | public function down(): void 29 | { 30 | if (Schema::hasColumn('system_request_logs', 'vdlp_redirect_redirect_id')) { 31 | Schema::table('system_request_logs', static function (Blueprint $table): void { 32 | $table->dropForeign('vdlp_redirect_request_log'); 33 | $table->dropColumn('vdlp_redirect_redirect_id'); 34 | }); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /updates/20231108_0013_add_keep_querystring_to_redirects_table.php: -------------------------------------------------------------------------------- 1 | boolean('keep_querystring') 18 | ->default(false) 19 | ->after('ignore_trailing_slash'); 20 | }); 21 | } 22 | 23 | public function down(): void 24 | { 25 | if (Schema::hasColumn('vdlp_redirect_redirects', 'keep_querystring')) { 26 | Schema::table('vdlp_redirect_redirects', static function (Blueprint $table): void { 27 | $table->dropColumn('keep_querystring'); 28 | }); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /updates/version.yaml: -------------------------------------------------------------------------------- 1 | v1.0.0: 2 | - "Transfer vendor ownership. Adrenth.Redirect -> Vdlp.Redirect" 3 | - 20180718_0001_create_tables.php 4 | - 20180831_0002_upgrade_from_adrenth_redirect.php 5 | v1.1.0: "Redirect rules are now being cached (if caching enabled) -- See: https://github.com/vdlp/oc-redirect-plugin/issues/1" 6 | v1.2.0: "Add extra filters to the Redirects overview -- See: https://github.com/vdlp/oc-redirect-plugin/pull/5" 7 | v1.3.0: 8 | - "Add support for ignoring query parameters -- See: https://github.com/vdlp/oc-redirect-plugin/issues/6" 9 | - 20181019_0003_add_ignore_query_parameters_to_redirects_table.php 10 | v1.4.0: "Code dusting and improvements to the redirect list view" 11 | v1.4.1: 12 | - "Add extra statistics database index to improve performance" 13 | - 20181117_0004_add_redirect_timestamp_crawler_index_on_clients_table.php 14 | v1.4.2: 15 | - "Add extra statistics database index to improve performance (Statistics dashboard)" 16 | - 20181117_0005_add_month_year_crawler_index_on_clients_table.php 17 | v1.4.3: "Fix thrown BindingResolutionException on redirecting" 18 | v1.4.4: "Fixes a redirect loop bug which might occur after renaming content pages" 19 | v1.4.5: "Fixes critical issue with ignoring query parameters" 20 | v1.5.0: "Bugfixes and added more extensibility support -- See: https://github.com/vdlp/oc-redirect-plugin/releases/tag/1.5.0" 21 | v1.6.0: 22 | - "Minor UI additions and added new Match Type: Regular Expressions! -- See: https://github.com/vdlp/oc-redirect-plugin/releases/tag/1.6.0" 23 | - 20190404_0006_add_description_to_redirects_table.php 24 | v1.7.0: "Bugfixes and code improvements -- See: https://github.com/vdlp/oc-redirect-plugin/releases/tag/1.7.0" 25 | v1.8.0: "Add CLI command for publishing redirects -- See: https://github.com/vdlp/oc-redirect-plugin/releases/tag/1.8.0" 26 | v1.8.1: "Fix critical issue regarding regular expression redirects" 27 | v1.9.0: "Add setting for enabling/disabling automatic creation of redirects -- See: https://github.com/vdlp/oc-redirect-plugin/releases/tag/1.9.0" 28 | v1.10.0: 29 | - "Improve statistics performance -- See: https://github.com/vdlp/oc-redirect-plugin/releases/tag/1.10.0" 30 | - 20190704_0007_add_timestamp_crawler_index_on_clients_table.php 31 | v1.10.1: "This fixes an issue where redirects will fail to work when the redirects.csv file does not have the write permission -- See: https://github.com/vdlp/oc-redirect-plugin/releases/tag/1.10.1" 32 | v1.10.2: "Fixes a fatal error when running TestLab -- See: https://github.com/vdlp/oc-redirect-plugin/releases/tag/1.10.2" 33 | v1.10.3: "Fix support for Postgres -- See: https://github.com/vdlp/oc-redirect-plugin/releases/tag/1.10.3" 34 | v1.10.4: "Fixes reported issues #41 and #43 -- See: https://github.com/vdlp/oc-redirect-plugin/releases/tag/1.10.4" 35 | v1.10.5: "Fixes reported issues #46 and #49 -- See: https://github.com/vdlp/oc-redirect-plugin/releases/tag/1.10.5" 36 | v2.0.0: "Supports PHP 7.1.3 and higher. Read CHANGELOG.md -- See: https://github.com/vdlp/oc-redirect-plugin/releases/tag/2.0.0" 37 | v2.0.1: "Fix Middleware not being invoked in newer PHP versions -- See: https://github.com/vdlp/oc-redirect-plugin/releases/tag/2.0.1" 38 | v2.0.2: 39 | - "Minor database and configuration fixes -- See: https://github.com/vdlp/oc-redirect-plugin/releases/tag/2.0.2" 40 | - 20200408_0008_change_column_types_from_char_to_varchar.php 41 | v2.1.0: "Added support for October CMS L6 build, improved caching and more -- See: https://github.com/vdlp/oc-redirect-plugin/releases/tag/2.1.0" 42 | v2.1.1: "Update CHANGELOG" 43 | v2.2.0: "Add cache control header and UI improvements -- See: https://github.com/vdlp/oc-redirect-plugin/releases/tag/2.2.0" 44 | v2.3.0: 45 | - "Add new redirect options (ignore case and ignore trailing slash) -- See: https://github.com/vdlp/oc-redirect-plugin/releases/tag/2.3.0" 46 | - 20200414_0009_add_ignore_case_to_redirects_table.php 47 | - 20200414_0010_add_ignore_trailing_slash_to_redirects_table.php 48 | v2.3.1: "Fix SQLSTATE[42S22] error when installing plugin -- See: https://github.com/vdlp/oc-redirect-plugin/releases/tag/2.3.1" 49 | v2.3.2: "Improve error handling in plugin migration process -- See: https://github.com/vdlp/oc-redirect-plugin/releases/tag/2.3.2" 50 | v2.4.0: "Skip requests with header 'X-Requested-With: XMLHttpRequest' -- See: https://github.com/vdlp/oc-redirect-plugin/releases/tag/2.4.0" 51 | v2.4.1: "Add Redirect Extensions promo page -- See: https://github.com/vdlp/oc-redirect-plugin/releases/tag/2.4.1" 52 | v2.5.0: "Add support for using relative paths -- See: https://github.com/vdlp/oc-redirect-plugin/releases/tag/2.5.0" 53 | v2.5.1: "Fixes issues with redirect rules file not being present -- See: https://github.com/vdlp/oc-redirect-plugin/releases/tag/2.5.1" 54 | v2.5.2: "Fix bug that causes re-writing the redirect rules file when hits are updated -- See: https://github.com/vdlp/oc-redirect-plugin/releases/tag/2.5.2" 55 | v2.5.3: "Improve / fixes redirect rule caching -- See: https://github.com/vdlp/oc-redirect-plugin/releases/tag/2.5.3" 56 | v2.5.4: "Add support for symfony/stopwatch:^5.0 (version 4.0 is still supported) -- See: https://github.com/vdlp/oc-redirect-plugin/releases/tag/2.5.4" 57 | v2.5.5: "Suppress logging when redirect rules file is empty -- See: https://github.com/vdlp/oc-redirect-plugin/releases/tag/2.5.5" 58 | v2.5.6: "Prevent connection exception when accessing settings in CLI mode -- See: https://github.com/vdlp/oc-redirect-plugin/releases/tag/2.5.6" 59 | v2.5.7: "Improve redirect caching management -- See: https://github.com/vdlp/oc-redirect-plugin/releases/tag/2.5.7" 60 | v2.5.8: "Improve redirect caching management (revised) -- See: https://github.com/vdlp/oc-redirect-plugin/releases/tag/2.5.8" 61 | v2.5.9: "Fix import in Plugin file" 62 | v2.5.10: "Add PHP 8.0 version constraint and composer/installers package" 63 | v2.5.11: "Minor fixes -- See: https://github.com/vdlp/oc-redirect-plugin/releases/tag/2.5.11" 64 | v2.5.12: "Fix strpos() type error" 65 | v2.5.13: "Fix database error when cache is being cleared before installation of plugin." 66 | v2.6.0: "Update plugin dependencies." 67 | v3.0.0: 68 | - "Drop support for October CMS 1.1 and lower. See: https://github.com/vdlp/oc-redirect-plugin/releases/tag/3.0.0" 69 | - 20200918_0011_refactor_redirects_logs_table.php 70 | - 20200918_0012_add_redirect_id_to_system_request_logs_table.php 71 | v3.0.1: "Add support for regular expression matches in target path." 72 | v3.0.2: "Change version constraint for composer/installers." 73 | v3.0.3: "Update plugin dependencies" 74 | v3.0.4: "Improved compatibility/extensibility with other Plugins." 75 | v3.0.5: "Lock to October CMS version 2.x. Support for October CMS 3 will be added in v3.1.0." 76 | v3.1.0: "Add support for October CMS 3.x. Drop support for October CMS 2.x." 77 | v3.1.1: "Add description column to Redirects overview." 78 | v3.1.2: "Fix daily stats labels when selecting month/year." 79 | v3.1.3: "Maintenance release" 80 | v3.1.4: "Fix target field not loading properly. See: https://github.com/vdlp/oc-redirect-plugin/releases/tag/3.1.4" 81 | v3.1.5: "Minor improvements. See: https://github.com/vdlp/oc-redirect-plugin/releases/tag/3.1.5" 82 | v3.1.6: "Add support for October CMS 3.3." 83 | v3.1.7: "Remove use of old BrandSetting constants." 84 | v3.1.8: "Add German translation." 85 | v3.1.9: "Added environment variables for manipulating the navigation." 86 | v3.1.10: "Fixed backend filtering for MySQL >= 8.0.3." 87 | v3.1.11: 88 | - "Add option to keep query string when redirecting." 89 | - 20231108_0013_add_keep_querystring_to_redirects_table.php 90 | --------------------------------------------------------------------------------