├── .gitignore ├── templates ├── index.twig ├── widgets │ ├── RetourWidget_Settings.twig │ └── RetourWidget_Body.twig ├── welcome.twig ├── settings.twig ├── fields │ ├── RetourFieldType_Settings.twig │ └── RetourFieldType.twig ├── statistics.twig ├── _edit.twig └── redirects.twig ├── resources ├── cancel.png ├── css │ ├── images │ │ ├── sort_asc.png │ │ ├── sort_both.png │ │ ├── sort_desc.png │ │ ├── sort_asc_disabled.png │ │ └── sort_desc_disabled.png │ ├── Retour.css │ ├── widgets │ │ └── RetourWidget.css │ ├── fields │ │ └── RetourFieldType.css │ ├── RetourTables.css │ └── datatables.min.css ├── screenshots │ ├── retour01.png │ ├── retour02.png │ └── retour03.png ├── js │ ├── Retour.js │ ├── widgets │ │ └── RetourWidget.js │ └── fields │ │ └── RetourFieldType.js ├── icon.svg └── icon-mask.svg ├── composer.json ├── translations └── en.php ├── models ├── Retour_RedirectsFieldModel.php ├── Retour_StatsModel.php └── Retour_RedirectsModel.php ├── migrations ├── m170710_000000_retour_increaseReferrerUrlColumnMaxLength.php ├── m160426_020311_retour_FixIndexes.php ├── m160514_000000_retour_convertToElementId.php ├── m160427_000000_retour_addHandledStats.php └── m160704_000000_retour_addReferrerStats.php ├── LICENSE.txt ├── config.php ├── records ├── Retour_StatsRecord.php ├── Retour_StaticRedirectsRecord.php └── Retour_RedirectsRecord.php ├── variables └── RetourVariable.php ├── widgets └── RetourWidget.php ├── fieldtypes └── RetourFieldType.php ├── CHANGELOG.md ├── controllers └── RetourController.php ├── RetourPlugin.php ├── releases.json ├── README.md └── services └── RetourService.php /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | .DS_Store 3 | .idea 4 | -------------------------------------------------------------------------------- /templates/index.twig: -------------------------------------------------------------------------------- 1 | {% redirect url('retour/redirects') %} -------------------------------------------------------------------------------- /resources/cancel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nystudio107/retour/master/resources/cancel.png -------------------------------------------------------------------------------- /resources/css/images/sort_asc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nystudio107/retour/master/resources/css/images/sort_asc.png -------------------------------------------------------------------------------- /resources/css/images/sort_both.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nystudio107/retour/master/resources/css/images/sort_both.png -------------------------------------------------------------------------------- /resources/css/images/sort_desc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nystudio107/retour/master/resources/css/images/sort_desc.png -------------------------------------------------------------------------------- /resources/screenshots/retour01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nystudio107/retour/master/resources/screenshots/retour01.png -------------------------------------------------------------------------------- /resources/screenshots/retour02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nystudio107/retour/master/resources/screenshots/retour02.png -------------------------------------------------------------------------------- /resources/screenshots/retour03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nystudio107/retour/master/resources/screenshots/retour03.png -------------------------------------------------------------------------------- /resources/css/images/sort_asc_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nystudio107/retour/master/resources/css/images/sort_asc_disabled.png -------------------------------------------------------------------------------- /resources/css/images/sort_desc_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nystudio107/retour/master/resources/css/images/sort_desc_disabled.png -------------------------------------------------------------------------------- /resources/js/Retour.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Retour plugin for Craft CMS 3 | * 4 | * Retour JS 5 | * 6 | * @author Andrew Welch 7 | * @copyright Copyright (c) 2016 nystudio107 8 | * @link http://nystudio107.com 9 | * @package Retour 10 | * @since 1.0.0 11 | */ 12 | -------------------------------------------------------------------------------- /resources/js/widgets/RetourWidget.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Retour plugin for Craft CMS 3 | * 4 | * RetourWidget JS 5 | * 6 | * @author nystudio107 7 | * @copyright Copyright (c) 2016 nystudio107 8 | * @link http://nystudio107.com 9 | * @package Retour 10 | * @since 1.0.0 11 | */ 12 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nystudio107/retour", 3 | "description": "Retour allows you to intelligently redirect legacy URLs, so that you don't lose SEO value when rebuilding & restructuring a website.", 4 | "require": { 5 | "composer/installers": "~1.0" 6 | }, 7 | "type": "craft-plugin" 8 | } -------------------------------------------------------------------------------- /translations/en.php: -------------------------------------------------------------------------------- 1 | 'To this', 16 | ); 17 | -------------------------------------------------------------------------------- /resources/css/Retour.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Retour plugin for Craft CMS 3 | * 4 | * Retour CSS 5 | * 6 | * @author Andrew Welch 7 | * @copyright Copyright (c) 2016 nystudio107 8 | * @link http://nystudio107.com 9 | * @package Retour 10 | * @since 1.0.0 11 | */ 12 | 13 | div.retour-htaccess-container { 14 | display: inline-block; 15 | margin-right: 10px; 16 | } 17 | 18 | .inputfile { 19 | width: 0.1px; 20 | height: 0.1px; 21 | opacity: 0; 22 | overflow: hidden; 23 | position: absolute; 24 | z-index: -1; 25 | } -------------------------------------------------------------------------------- /templates/widgets/RetourWidget_Settings.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * Retour plugin for Craft CMS 4 | * 5 | * RetourWidget Settings 6 | * 7 | * @author nystudio107 8 | * @copyright Copyright (c) 2016 nystudio107 9 | * @link http://nystudio107.com 10 | * @package Retour 11 | * @since 1.0.0 12 | */ 13 | #} 14 | 15 | {% import "_includes/forms" as forms %} 16 | 17 | {{ forms.textField({ 18 | label: 'Statistics for Days', 19 | instructions: 'Number of days to display statistics from', 20 | id: 'numberOfDays', 21 | name: 'numberOfDays', 22 | value: settings['numberOfDays']}) 23 | }} 24 | -------------------------------------------------------------------------------- /models/Retour_RedirectsFieldModel.php: -------------------------------------------------------------------------------- 1 | array(AttributeType::Bool, 'default' => 1), 27 | )); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /migrations/m170710_000000_retour_increaseReferrerUrlColumnMaxLength.php: -------------------------------------------------------------------------------- 1 | alterColumn('retour_stats', 'referrerUrl', array(ColumnType::Varchar, 'maxLength' => 2000)); 18 | 19 | RetourPlugin::log('The max length of column referrerUrl has been increased to 2000 ', LogLevel::Info, true); 20 | 21 | return true; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /resources/css/widgets/RetourWidget.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Retour plugin for Craft CMS 3 | * 4 | * RetourWidget CSS 5 | * 6 | * @author nystudio107 7 | * @copyright Copyright (c) 2016 nystudio107 8 | * @link http://nystudio107.com 9 | * @package Retour 10 | * @since 1.0.0 11 | */ 12 | 13 | table.retour td.retour-large-stat { 14 | text-align: center; 15 | font-size: 4vw; 16 | border: none; 17 | line-height: normal; 18 | vertical-align: top; 19 | width: 50%; 20 | } 21 | 22 | table.retour th.retour-handled, 23 | table.retour td.retour-handled { 24 | color: green; 25 | } 26 | 27 | table.retour th.retour-not-handled, 28 | table.retour td.retour-not-handled { 29 | color: red; 30 | } 31 | 32 | table.retour td.retour-stats { 33 | vertical-align: top; 34 | width: 50%; 35 | } 36 | 37 | table.retour .centered { 38 | text-align: center; 39 | } 40 | 41 | table.retour .rightered { 42 | text-align: right; 43 | } -------------------------------------------------------------------------------- /migrations/m160426_020311_retour_FixIndexes.php: -------------------------------------------------------------------------------- 1 | db->createCommand()->dropIndex('retour_redirects', 'redirectSrcUrl', true); 20 | craft()->db->createCommand()->createIndex('retour_redirects', 'redirectSrcUrlParsed', true); 21 | 22 | craft()->db->createCommand()->dropIndex('retour_static_redirects', 'redirectSrcUrl', true); 23 | craft()->db->createCommand()->createIndex('retour_static_redirects', 'redirectSrcUrlParsed', true); 24 | 25 | RetourPlugin::log("Updated Indexes for retour_redirects & retour_static_redirects", LogLevel::Info, true); 26 | 27 | return true; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /resources/css/fields/RetourFieldType.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Retour plugin for Craft CMS 3 | * 4 | * RetourFieldType CSS 5 | * 6 | * @author Andrew Welch 7 | * @copyright Copyright (c) 2016 nystudio107 8 | * @link http://nystudio107.com 9 | * @package Retour 10 | * @since 1.0.0 11 | */ 12 | 13 | div.retour-field { 14 | border: 1px solid #e3e5e8; 15 | margin-bottom: 10px; 16 | padding: 7px 14px 14px; 17 | border-radius: 3px; 18 | background: #f9fafa; 19 | } 20 | 21 | div.retour-field-title { 22 | background: #eef0f1; 23 | color: #8f98a3; 24 | margin: -7px -14px 14px; 25 | padding: 7px 14px 7px 14px; 26 | width: calc(100% + 28px); 27 | -webkit-box-sizing: border-box; 28 | -moz-box-sizing: border-box; 29 | box-sizing: border-box; 30 | border-radius: 2px 2px 0 0; 31 | overflow: hidden; 32 | white-space: nowrap; 33 | text-overflow: ellipsis; 34 | word-wrap: normal; 35 | cursor: default; 36 | } 37 | 38 | img.retour-field-icon { 39 | display: inline-block; 40 | width: 16px; 41 | height: auto; 42 | padding-right: 7px; 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The Retour License 2 | Copyright (c) 2016 nystudio107 3 | 4 | Permission is hereby granted, free of charge, to any person or entity obtaining a copy of this software and associated documentation files (the "Software"), to use the software in any capacity, including commercial and for-profit use. Permission is also granted to alter, modify, or extend the Software for your own use, or commission a third-party to perform modifications for you. 5 | 6 | Permission is NOT granted to create derivative works, sublicense, and/or sell copies of the Software. This is not FOSS software. 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /migrations/m160514_000000_retour_convertToElementId.php: -------------------------------------------------------------------------------- 1 | addForeignKey('retour_redirects', 'associatedElementId', 'elements', 'id', 'CASCADE', 'CASCADE'); 30 | 31 | // return true and let craft know its done 32 | return true; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /models/Retour_StatsModel.php: -------------------------------------------------------------------------------- 1 | array(AttributeType::String, 'default' => ''), 35 | 'referrerUrl' => array(AttributeType::String, 'default' => '', 'maxLength' => 2000), 36 | 'hitCount' => array(AttributeType::Number, 'default' => 0), 37 | 'hitLastTime' => array(AttributeType::DateTime, 'default' => DateTimeHelper::currentTimeForDb()), 38 | 'handledByRetour' => array(AttributeType::Bool, 'default' => false), 39 | )); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /config.php: -------------------------------------------------------------------------------- 1 | true, 19 | 20 | /** 21 | * How many stats should be stored 22 | */ 23 | "statsStoredLimit" => 1000, 24 | 25 | /** 26 | * How many stats to display in the Admin CP 27 | */ 28 | "statsDisplayLimit" => 1000, 29 | 30 | /** 31 | * How many static redirects to display in the Admin CP 32 | */ 33 | "staticRedirectDisplayLimit" => 100, 34 | 35 | /** 36 | * How many dynamic redirects to display in the Admin CP 37 | */ 38 | "dynamicRedirectDisplayLimit" => 100, 39 | 40 | /** 41 | * Should the query string be stripped from the saved statistics source URLs? 42 | */ 43 | "stripQueryStringFromStats" => true, 44 | 45 | /** 46 | * Should the query string be stripped from all 404 URLs before their evaluation? 47 | */ 48 | "alwaysStripQueryString" => false, 49 | 50 | ); 51 | -------------------------------------------------------------------------------- /resources/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /resources/icon-mask.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /models/Retour_RedirectsModel.php: -------------------------------------------------------------------------------- 1 | array(AttributeType::String, 'default' => ''), 27 | 'redirectSrcUrlParsed' => array(AttributeType::String, 'default' => ''), 28 | 'redirectMatchType' => array(AttributeType::String, 'default' => 'match'), 29 | 'redirectDestUrl' => array(AttributeType::String, 'default' => ''), 30 | 'redirectHttpCode' => array(AttributeType::Number, 'default' => 301), 31 | 'hitCount' => array(AttributeType::Number, 'default' => 0), 32 | 'hitLastTime' => array(AttributeType::DateTime, 'default' => DateTimeHelper::currentTimeForDb()), 33 | 'locale' => array(AttributeType::String, 'default' => ''), 34 | 'associatedElementId' => array(AttributeType::Number, 'default' => 0), 35 | )); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /resources/js/fields/RetourFieldType.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Retour plugin for Craft CMS 3 | * 4 | * RetourFieldType JS 5 | * 6 | * @author Andrew Welch 7 | * @copyright Copyright (c) 2016 nystudio107 8 | * @link http://nystudio107.com 9 | * @package Retour 10 | * @since 1.0.0 11 | */ 12 | 13 | ;(function ( $, window, document, undefined ) { 14 | 15 | var pluginName = "RetourFieldType", 16 | defaults = { 17 | }; 18 | 19 | // Plugin constructor 20 | function Plugin( element, options ) { 21 | this.element = element; 22 | 23 | this.options = $.extend( {}, defaults, options) ; 24 | 25 | this._defaults = defaults; 26 | this._name = pluginName; 27 | 28 | this.init(); 29 | } 30 | 31 | Plugin.prototype = { 32 | 33 | init: function(id) { 34 | var _this = this; 35 | 36 | $(function () { 37 | 38 | /* -- _this.options gives us access to the $jsonVars that our FieldType passed down to us */ 39 | 40 | }); 41 | } 42 | }; 43 | 44 | // A really lightweight plugin wrapper around the constructor, 45 | // preventing against multiple instantiations 46 | $.fn[pluginName] = function ( options ) { 47 | return this.each(function () { 48 | if (!$.data(this, "plugin_" + pluginName)) { 49 | $.data(this, "plugin_" + pluginName, 50 | new Plugin( this, options )); 51 | } 52 | }); 53 | }; 54 | 55 | })( jQuery, window, document ); 56 | -------------------------------------------------------------------------------- /records/Retour_StatsRecord.php: -------------------------------------------------------------------------------- 1 | array('hitCount', 'id')), 33 | array('columns' => array('redirectSrcUrl'), 'unique' => true), 34 | ); 35 | } 36 | 37 | /** 38 | * @return array 39 | */ 40 | public function defineRelations() 41 | { 42 | return array(); 43 | } 44 | 45 | /** 46 | * @access protected 47 | * @return array 48 | */ 49 | protected function defineAttributes() 50 | { 51 | return array( 52 | 'redirectSrcUrl' => array(AttributeType::String, 'default' => ''), 53 | 'referrerUrl' => array(AttributeType::String, 'default' => ''), 54 | 'hitCount' => array(AttributeType::Number, 'default' => 0), 55 | 'hitLastTime' => array(AttributeType::DateTime, 'default' => DateTimeHelper::currentTimeForDb()), 56 | 'handledByRetour' => array(AttributeType::Bool, 'default' => false), 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /variables/RetourVariable.php: -------------------------------------------------------------------------------- 1 | retour->getAllEntryRedirects($limit); 27 | } 28 | 29 | /** 30 | * @param null $limit 31 | * 32 | * @return mixed 33 | */ 34 | public function getStaticRedirects($limit = null) 35 | { 36 | return craft()->retour->getAllStaticRedirects($limit); 37 | } 38 | 39 | /** 40 | * @return mixed 41 | */ 42 | public function getStatistics() 43 | { 44 | return craft()->retour->getAllStatistics(); 45 | } 46 | 47 | /** 48 | * @param $days 49 | * @param $handled 50 | * 51 | * @return mixed 52 | */ 53 | public function getRecentStatistics($days, $handled) 54 | { 55 | return craft()->retour->getRecentStatistics($days, $handled); 56 | } 57 | 58 | /** 59 | * @return mixed 60 | */ 61 | public function getMatchesList() 62 | { 63 | return craft()->retour->getMatchesList(); 64 | } 65 | 66 | /** 67 | * @return mixed 68 | */ 69 | public function getPluginName() 70 | { 71 | return craft()->retour->getPluginName(); 72 | } 73 | 74 | /** 75 | * @return int 76 | */ 77 | public function getHttpStatus() 78 | { 79 | return http_response_code(); 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /templates/welcome.twig: -------------------------------------------------------------------------------- 1 | {% extends '_layouts/cp' %} 2 | {% set title = 'Welcome to Retour!' %} 3 | 4 | {% includeCssResource "retour/css/Retour.css" %} 5 | {% includeJsResource "retour/js/Retour.js" %} 6 | 7 | {% set linkGetStarted = url('retour/redirects') %} 8 | {% set docsUrl = "https://github.com/nystudio107/retour/blob/master/README.md" %} 9 | 10 | {% set crumbs = [ 11 | { label: craft.retour.getPluginName(), url: url('retour') }, 12 | { label: "Welcome"|t, url: url('retour/Welcome') }, 13 | ] %} 14 | 15 | {% set content %} 16 |
17 | 18 |

Thanks for using Retour!

19 |

Retour allows you to intelligently redirect legacy URLs, so that you don't lose SEO value when rebuilding & restructuring a website.

20 |

In addition to supporting traditional exact and RegEx matching of URL patterns, Retour also has a Retour Redirect FieldType that you can add to your entries. This allows you to have dynamic entry redirects that have access to the data in your entries when matching URL patterns.

21 |

Retour is written to be performant. There is no impact on your website's performance until a 404 exception happens; and even then the resulting matching happens with minimal impact.

22 |

Don't just rebuild a website. Transition it with Retour.

23 |

24 |   25 |

26 |

27 | 28 |

29 |
30 |
31 |

32 | Brought to you by nystudio107 33 |

34 |
35 | {% endset %} -------------------------------------------------------------------------------- /records/Retour_StaticRedirectsRecord.php: -------------------------------------------------------------------------------- 1 | array('locale', 'id')), 34 | array('columns' => array('redirectSrcUrlParsed'), 'unique' => true), 35 | ); 36 | } 37 | 38 | /** 39 | * @access protected 40 | * @return array 41 | */ 42 | protected function defineAttributes() 43 | { 44 | return array( 45 | 'redirectSrcUrl' => array(AttributeType::String, 'default' => ''), 46 | 'redirectSrcUrlParsed' => array(AttributeType::String, 'default' => ''), 47 | 'redirectMatchType' => array(AttributeType::String, 'default' => 'match'), 48 | 'redirectDestUrl' => array(AttributeType::String, 'default' => ''), 49 | 'redirectHttpCode' => array(AttributeType::Number, 'default' => 301), 50 | 'hitCount' => array(AttributeType::Number, 'default' => 0), 51 | 'hitLastTime' => array(AttributeType::DateTime, 'default' => DateTimeHelper::currentTimeForDb()), 52 | 'locale' => array(AttributeType::Locale, 'required' => true), 53 | 'associatedElementId' => array(AttributeType::Number, 'default' => 0), 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /migrations/m160427_000000_retour_addHandledStats.php: -------------------------------------------------------------------------------- 1 | ColumnType::Bool, 21 | ); 22 | 23 | $this->_addColumnsAfter("retour_stats", $newColumns, "hitLastTime"); 24 | 25 | // return true and let craft know its done 26 | return true; 27 | } 28 | 29 | private function _addColumnsAfter($tableName, $newColumns, $afterColumnHandle) 30 | { 31 | 32 | // this is a foreach loop, enough said 33 | foreach ($newColumns as $columnName => $columnType) { 34 | // check if the column does NOT exist 35 | if (!craft()->db->columnExists($tableName, $columnName)) { 36 | $this->addColumnAfter( 37 | $tableName, 38 | $columnName, 39 | array( 40 | 'column' => $columnType, 41 | 'null' => false, 42 | ), 43 | $afterColumnHandle 44 | ); 45 | 46 | // log that we created the new column 47 | RetourPlugin::log("Created the `$columnName` in the `$tableName` table.", LogLevel::Info, true); 48 | } // if the column already exists in the table 49 | else { 50 | // tell craft that we couldn't create the column as it alredy exists. 51 | RetourPlugin::log("Column `$columnName` already exists in the `$tableName` table.", LogLevel::Info, true); 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /migrations/m160704_000000_retour_addReferrerStats.php: -------------------------------------------------------------------------------- 1 | ColumnType::Varchar, 21 | ); 22 | 23 | $this->_addColumnsAfter("retour_stats", $newColumns, "redirectSrcUrl"); 24 | 25 | // return true and let craft know its done 26 | return true; 27 | } 28 | 29 | private function _addColumnsAfter($tableName, $newColumns, $afterColumnHandle) 30 | { 31 | 32 | // this is a foreach loop, enough said 33 | foreach ($newColumns as $columnName => $columnType) { 34 | // check if the column does NOT exist 35 | if (!craft()->db->columnExists($tableName, $columnName)) { 36 | $this->addColumnAfter( 37 | $tableName, 38 | $columnName, 39 | array( 40 | 'column' => $columnType, 41 | 'null' => false, 42 | ), 43 | $afterColumnHandle 44 | ); 45 | 46 | // log that we created the new column 47 | RetourPlugin::log("Created the `$columnName` in the `$tableName` table.", LogLevel::Info, true); 48 | } // if the column already exists in the table 49 | else { 50 | // tell craft that we couldn't create the column as it alredy exists. 51 | RetourPlugin::log("Column `$columnName` already exists in the `$tableName` table.", LogLevel::Info, true); 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /widgets/RetourWidget.php: -------------------------------------------------------------------------------- 1 | retour->getPluginName() . " " . Craft::t("Stats"); 24 | } 25 | 26 | /** 27 | * @return mixed 28 | */ 29 | public function getBodyHtml() 30 | { 31 | // Include our Javascript & CSS 32 | craft()->templates->includeCssResource('retour/css/widgets/RetourWidget.css'); 33 | craft()->templates->includeJsResource('retour/js/widgets/RetourWidget.js'); 34 | 35 | // Variables to pass down to our rendered template 36 | $variables = array(); 37 | $variables['settings'] = $this->getSettings(); 38 | 39 | return craft()->templates->render('retour/widgets/RetourWidget_Body', $variables); 40 | } 41 | 42 | /** 43 | * @return string 44 | */ 45 | public function getIconPath() 46 | { 47 | return craft()->path->getPluginsPath() . 'retour/resources/icon.svg'; 48 | } 49 | 50 | /** 51 | * @return mixed 52 | */ 53 | public function getSettingsHtml() 54 | { 55 | // Variables to pass down to our rendered template 56 | $variables = array(); 57 | $variables['settings'] = $this->getSettings(); 58 | 59 | return craft()->templates->render('retour/widgets/RetourWidget_Settings', $variables); 60 | } 61 | 62 | /** 63 | * @param mixed $settings The Widget's settings 64 | * 65 | * @return mixed 66 | */ 67 | public function prepSettings($settings) 68 | { 69 | // Modify $settings here... 70 | return $settings; 71 | } 72 | 73 | /** 74 | * @return array 75 | */ 76 | protected function defineSettings() 77 | { 78 | return array( 79 | 'numberOfDays' => array(AttributeType::String, 'label' => 'Some Setting', 'default' => '3'), 80 | ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /records/Retour_RedirectsRecord.php: -------------------------------------------------------------------------------- 1 | array('locale', 'associatedElementId')), 33 | array('columns' => array('redirectSrcUrlParsed'), 'unique' => true), 34 | ); 35 | } 36 | 37 | /** 38 | * @return array 39 | */ 40 | public function defineRelations() 41 | { 42 | return array( 43 | 'locale' => array(static::BELONGS_TO, 'LocaleRecord', 'locale', 'required' => true, 'onDelete' => static::CASCADE, 'onUpdate' => static::CASCADE), 44 | 'associatedElement' => array(static::BELONGS_TO, 'ElementRecord', 'required' => true, 'onDelete' => static::CASCADE), 45 | ); 46 | } 47 | 48 | /** 49 | * @access protected 50 | * @return array 51 | */ 52 | protected function defineAttributes() 53 | { 54 | return array( 55 | 'redirectSrcUrl' => array(AttributeType::String, 'column' => ColumnType::Text), 56 | 'redirectSrcUrlParsed' => array(AttributeType::String, 'default' => ''), 57 | 'redirectMatchType' => array(AttributeType::String, 'default' => 'match'), 58 | 'redirectDestUrl' => array(AttributeType::String, 'default' => ''), 59 | 'redirectHttpCode' => array(AttributeType::Number, 'default' => 301), 60 | 'hitCount' => array(AttributeType::Number, 'default' => 0), 61 | 'hitLastTime' => array(AttributeType::DateTime, 'default' => DateTimeHelper::currentTimeForDb()), 62 | 'locale' => array(AttributeType::Locale, 'required' => true) 63 | /* defined in defineRelations() 64 | 'associatedElementId' => array(AttributeType::Number, 'default' => 0), 65 | */ 66 | ); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /templates/settings.twig: -------------------------------------------------------------------------------- 1 | {% import "_includes/forms" as forms %} 2 | 3 | {% extends "_layouts/cp" %} 4 | {% import '_includes/forms' as forms %} 5 | 6 | {% includeCssResource "retour/css/Retour.css" %} 7 | {% includeJsResource "retour/js/Retour.js" %} 8 | 9 | {% set title = craft.retour.getPluginName() %} 10 | 11 | {% set fullPageForm = true %} 12 | 13 | {% set docsUrl = "https://github.com/nystudio107/retour/blob/master/README.md" %} 14 | 15 | {% set retourSections = { 16 | redirects: { label: "Redirects"|t, url: url('retour/redirects') }, 17 | statistics: { label: "Statistics"|t, url: url('retour/statistics') }, 18 | settings: { label: "Settings"|t, url: url('retour/settings') }, 19 | } %} 20 | 21 | {% set crumbs = [ 22 | { label: craft.retour.getPluginName(), url: url('retour') }, 23 | { label: "Settings"|t, url: url('retour/settings') }, 24 | ] %} 25 | 26 | {% if craft.app.version < 2.5 %} 27 | {% set tabs = retourSections %} 28 | {% set selectedTab = 'settings' %} 29 | {% else %} 30 | {% set subnav = retourSections %} 31 | {% set selectedSubnavItem = 'settings' %} 32 | {% endif %} 33 | 34 | {% set content %} 35 | 36 | 37 | 38 | {% if craft.app.version < 2.5 %} 39 |
40 | {% endif %} 41 | 42 | 43 | 44 | 45 | {{ getCsrfInput() }} 46 | 47 | 48 | 49 | {{ forms.textField({ 50 | label: "Plugin Name"|t, 51 | instructions: "The plugin name as you'd like it to be displayed in the AdminCP."|t, 52 | id: 'pluginNameOverride', 53 | name: 'settings[pluginNameOverride]', 54 | value: settings.pluginNameOverride, 55 | autofocus: true, 56 | first: true, 57 | }) }} 58 | 59 | 60 | 61 | {% if craft.app.version < 2.5 %} 62 |
63 |
64 |
65 | 66 |
67 |
68 |
69 | {% endif %} 70 | 71 | 72 | 73 | 74 | {% if craft.app.version < 2.5 %} 75 |
76 | {% endif %} 77 | 78 | {% endset %} 79 | -------------------------------------------------------------------------------- /templates/fields/RetourFieldType_Settings.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * Retour plugin for Craft CMS 4 | * 5 | * RetourFieldType_Settings HTML 6 | * 7 | * @author Andrew Welch 8 | * @copyright Copyright (c) 2016 nystudio107 9 | * @link http://nystudio107.com 10 | * @package Retour 11 | * @since 1.0.0 12 | */ 13 | #} 14 | 15 | {% import "_includes/forms" as forms %} 16 | 17 |
18 | 19 |
20 |
21 |

{{ "Retour will look for 404 (Not Found) URLs that match the Legacy URL Pattern below, and redirect them to this entry's URL. Here you can set the default values for the Retour Redirect FieldType." |t |raw}}

22 |
23 |
24 |
25 |
26 | 27 |

{{ "Enter the URL pattern that Retour should match. This matches against the path, the part of the URL after the domain name. You can include tags that output entry properties, such as {title} or {myCustomField} in the text field below. e.g.: Exact Match: /recipes/{recipeid} or RegEx Match: /.*RecipeID={recipeid}$ where {recipeid} is a field handle to a field in this entry." |t |raw}}

28 |
29 | {{ forms.textField({ 30 | fieldClass: 'nomarginfield', 31 | id: 'defaultRedirectSrcUrl', 32 | class: 'nicetext', 33 | name: 'defaultRedirectSrcUrl', 34 | value: settings.defaultRedirectSrcUrl, 35 | required: true, 36 | }) }} 37 |
38 | 39 |
40 |
41 | 42 |

{{ "Set what type of pattern matching should be set as a default for this field. If a plugin provides a custom matching function, you can select it here." |t |raw}}

43 |
44 | {{ forms.selectField({ 45 | fieldClass: 'nomarginfield', 46 | class: 'selectField', 47 | id: 'defaultRedirectMatchType', 48 | options: matchList, 49 | name: 'defaultRedirectMatchType', 50 | value: settings.defaultRedirectMatchType, 51 | }) }} 52 |
53 | 54 |
55 |
56 | 57 |

{{ "Select whether the redirect should be permanent or temporary." |t |raw}}

58 |
59 | {{ forms.selectField({ 60 | fieldClass: 'nomarginfield', 61 | class: 'selectField', 62 | id: "defaultRedirectHttpCode", 63 | options: { 64 | "301": "301 - Permanent"|t, 65 | "302": "302 - Temporary"|t, 66 | "410": "410 - Gone"|t, 67 | }, 68 | name: "defaultRedirectHttpCode", 69 | value: settings.defaultRedirectHttpCode, 70 | }) }} 71 |
72 | 73 | {{ forms.lightswitchField({ 74 | label: "Redirect Changeable"|t, 75 | name: 'redirectChangeable', 76 | instructions: "Whether to allow the user to change the redirect while editing the entry"|t, 77 | on: settings.redirectChangeable, 78 | }) }} 79 | 80 |
81 | -------------------------------------------------------------------------------- /templates/fields/RetourFieldType.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * Retour plugin for Craft CMS 4 | * 5 | * RetourFieldType HTML 6 | * 7 | * @author Andrew Welch 8 | * @copyright Copyright (c) 2016 nystudio107 9 | * @link http://nystudio107.com 10 | * @package Retour 11 | * @since 1.0.0 12 | */ 13 | #} 14 | 15 | {% import "_includes/forms" as forms %} 16 | 17 | {% set disableFields = values.redirectChangeable ? false : true %} 18 | {% set locale = craft.isLocalized() ? (element ? element.locale : craft.locale) %} 19 | 20 |
21 | 22 |
23 |  Retour Redirect 24 |
25 | 26 |
27 |
28 |

{{ "Retour will look for 404 (Not Found) URLs that match the Legacy URL Pattern below, and redirect them to this entry's URL." |t |raw}}

29 |
30 |
31 | 32 |
33 |
34 | 35 |

{{ "Enter the URL pattern that Retour should match. This matches against the path, the part of the URL after the domain name. You can include tags that output entry properties, such as {title} or {myCustomField} in the text field below. e.g.: Exact Match: /recipes/{recipeid} or RegEx Match: .*RecipeID={recipeid}$ where {recipeid} is a field handle to a field in this entry." |t |raw}}

36 |
37 | {{ forms.textField({ 38 | fieldClass: 'nomarginfield', 39 | id: id ~ "redirectSrcUrl", 40 | class: 'nicetext', 41 | name: name ~ "[redirectSrcUrl]", 42 | value: values.redirectSrcUrl, 43 | required: true, 44 | disabled: disableFields, 45 | locale: field.translatable ? locale, 46 | }) }} 47 |
48 | 49 |
50 |
51 | 52 |

{{ "What type of matching should be done with the Legacy URL Pattern. Details on RegEx matching can be found at regexr.com If a plugin provides a custom matching function, you can select it here." |t |raw}}

53 |
54 | {{ forms.selectField({ 55 | fieldClass: 'nomarginfield', 56 | class: 'selectField', 57 | id: id ~ "redirectMatchType", 58 | options: matchList, 59 | name: name ~ "[redirectMatchType]", 60 | value: values.redirectMatchType, 61 | disabled: disableFields, 62 | locale: field.translatable ? locale, 63 | }) }} 64 |
65 | 66 |
67 |
68 | 69 |

{{ "Select whether the redirect should be permanent or temporary." |t |raw}}

70 |
71 | {{ forms.selectField({ 72 | fieldClass: 'nomarginfield', 73 | class: 'selectField', 74 | id: id ~ "redirectHttpCode", 75 | options: { 76 | "301": "301 - Permanent"|t, 77 | "302": "302 - Temporary"|t, 78 | "410": "410 - Gone"|t, 79 | }, 80 | name: name ~ "[redirectHttpCode]", 81 | value: values.redirectHttpCode, 82 | disabled: disableFields, 83 | locale: field.translatable ? locale, 84 | }) }} 85 |
86 | 87 |
88 | -------------------------------------------------------------------------------- /templates/widgets/RetourWidget_Body.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /** 3 | * Retour plugin for Craft CMS 4 | * 5 | * RetourWidget Body 6 | * 7 | * @author nystudio107 8 | * @copyright Copyright (c) 2016 nystudio107 9 | * @link http://nystudio107.com 10 | * @package Retour 11 | * @since 1.0.0 12 | */ 13 | #} 14 | 15 | {% set handled = craft.retour.getRecentStatistics(settings.numberOfDays, true) %} 16 | {% set nothandled = craft.retour.getRecentStatistics(settings.numberOfDays, false) %} 17 | {% set outputRows = 5 %} 18 | 19 |

404 Redirects in the last {{ settings.numberOfDays }} {% if settings.numberOfDays == 1 %}day{% else %}days{% endif %}

20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 32 | 35 | 36 | 37 | 56 | 74 | 75 | 76 |
✔ Handled:✖ Not Handled:
30 | {{ handled |length }} 31 | 33 | {{ nothandled |length }} 34 |
38 | 39 | 40 | 41 | 42 | 43 | 44 | {% set displayOutput = true %} 45 | {% for stat in handled %} 46 | {% if loop.index0 < outputRows %} 47 | 48 | 49 | 50 | 51 | {% endif %} 52 | {% endfor %} 53 | 54 |
{{ "URL"|t }}{{ "Hits"|t }}
{{ stat.redirectSrcUrl }}{{ stat.hitCount }}
55 |
57 | 58 | 59 | 60 | 61 | 62 | 63 | {% for stat in nothandled %} 64 | {% if loop.index0 < outputRows %} 65 | 66 | 67 | 68 | 69 | {% endif %} 70 | {% endfor %} 71 | 72 |
{{ "URL"|t }}{{ "Hits"|t }}
{{ stat.redirectSrcUrl }}{{ stat.hitCount }}
73 |
77 |
78 | -------------------------------------------------------------------------------- /templates/statistics.twig: -------------------------------------------------------------------------------- 1 | {% extends "_layouts/cp" %} 2 | {% import '_includes/forms' as forms %} 3 | 4 | {% includeCssResource "retour/css/Retour.css" %} 5 | {% includeCssResource "retour/css/RetourTables.css" %} 6 | {% includeJsResource "retour/js/Retour.js" %} 7 | {% includeJsResource "retour/js/datatables.min.js" %} 8 | 9 | {% set title = craft.retour.getPluginName() %} 10 | 11 | {% set docsUrl = "https://github.com/nystudio107/retour/blob/master/README.md" %} 12 | 13 | {% set retourSections = { 14 | redirects: { label: "Redirects"|t, url: url('retour/redirects') }, 15 | statistics: { label: "Statistics"|t, url: url('retour/statistics') }, 16 | settings: { label: "Settings"|t, url: url('retour/settings') }, 17 | } %} 18 | 19 | {% set crumbs = [ 20 | { label: craft.retour.getPluginName(), url: url('retour') }, 21 | { label: "Statistics"|t, url: url('retour/statistics') }, 22 | ] %} 23 | 24 | {% if craft.app.version < 2.5 %} 25 | {% set tabs = retourSections %} 26 | {% set selectedTab = 'statistics' %} 27 | {% else %} 28 | {% set subnav = retourSections %} 29 | {% set selectedSubnavItem = 'statistics' %} 30 | {% endif %} 31 | 32 | {% set extraPageHeaderHtml %} 33 |
34 | {{ "Clear Stats"|t }} 35 |
36 | {% endset %} 37 | 38 | {% set content %} 39 | 40 | 41 | 42 |

{{ "Retour Statistics" |t }}

43 | 44 | {% set stats = craft.retour.getStatistics() %} 45 | {% if stats|length %} 46 |
47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | {% for stat in stats %} 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | {% endfor %} 68 | 69 | 70 |
{{ "404 File Not Found URL"|t }}{{ "Last Referrer URL"|t }}{{ "Hits"|t }}{{ "Last Hit"|t }}{{ "Handled"|t }} 
{{ stat.redirectSrcUrl }}{% if stat.referrerUrl %}{{ stat.referrerUrl }}{% endif %}{{ stat.hitCount }}{{ stat.hitLastTime |datetime }}{% if stat.handledByRetour %}{% else %}{% endif %}{% if not stat.handledByRetour %}{% endif %}
71 |
72 | {% else %} 73 |

You have no 404s yet.

74 | {% endif %} 75 | 76 | {% endset %} 77 | 78 | {% set js %} 79 | /* -- Initialize the datatable */ 80 | 81 | $('#statistics').dataTable({ 82 | "sDom": '<"top"ilpf<"clear">>rt<"bottom"ilp<"clear">>', 83 | "sPaginationType": "full_numbers", 84 | "aaSorting": [[ 2, "desc" ]], 85 | "aoColumns": [ null, null, { "sType": "num" }, { "sType": "date" }, null, null], 86 | "bLengthChange": false, 87 | "iDisplayLength": 50, 88 | "bInfo": true, 89 | "oLanguage": { 90 | "sSearch": "Statistics Filter ", 91 | "oPaginate": { 92 | "sFirst": "«", 93 | "sLast": "»", 94 | "sNext": "›", 95 | "sPrevious": "‹" 96 | } 97 | }, 98 | "bAutoWidth": false }); 99 | 100 | /* -- Add our cancel button to the table */ 101 | 102 | {% set cancelButton = resourceUrl('retour/cancel.png') %} 103 | var sOnClick = "onclick=\"$(document).ready(function() {var oTable = $('#statistics').dataTable();oTable.fnFilter('');});\""; 104 | var sMyElem = "
"; 105 | 106 | $( sMyElem ).insertBefore( "#statistics_filter" ); 107 | {% endset %} 108 | {% includeJs js %} 109 | -------------------------------------------------------------------------------- /templates/_edit.twig: -------------------------------------------------------------------------------- 1 | {% extends "_layouts/cp" %} 2 | {% import '_includes/forms' as forms %} 3 | 4 | {% includeCssResource "retour/css/Retour.css" %} 5 | {% includeJsResource "retour/js/Retour.js" %} 6 | 7 | {% set title = craft.retour.getPluginName() %} 8 | 9 | {% set fullPageForm = true %} 10 | 11 | {% set docsUrl = "https://github.com/nystudio107/retour/blob/master/README.md" %} 12 | 13 | {% set retourSections = { 14 | redirects: { label: "Redirects"|t, url: url('retour/redirects') }, 15 | statistics: { label: "Statistics"|t, url: url('retour/statistics') }, 16 | settings: { label: "Settings"|t, url: url('retour/settings') }, 17 | } %} 18 | 19 | {% set crumbs = [ 20 | { label: craft.retour.getPluginName(), url: url('retour') }, 21 | { label: "Redirects"|t, url: url('retour/redirects') }, 22 | { label: "Edit"|t, url: url(craft.request.url) }, 23 | ] %} 24 | 25 | {% if craft.app.version < 2.5 %} 26 | {% set tabs = retourSections %} 27 | {% set selectedTab = 'redirects' %} 28 | {% else %} 29 | {% set subnav = retourSections %} 30 | {% set selectedSubnavItem = 'redirects' %} 31 | {% endif %} 32 | 33 | {% set content %} 34 | 35 | 36 | 37 | {% if craft.app.version < 2.5 %} 38 |
39 | {% endif %} 40 | 41 | 42 | 43 | {{ getCsrfInput() }} 44 | 45 | 46 | 47 | {{ forms.hidden({ 48 | id: "redirectId", 49 | name: "redirectId", 50 | value: redirectId, 51 | }) }} 52 | 53 |
54 |
55 |

{{ "Retour will look for 404 (Not Found) URLs that match the Legacy URL Pattern below, and redirect them to Destination URL." |t |raw}}

56 |
57 |
58 | 59 |
60 |
61 | 62 |

{{ "Enter the URL pattern that Retour should match. This matches against the path, the part of the URL after the domain name. e.g.: Exact Match: /recipes/ or RegEx Match: .*RecipeID=(.*)" |t |raw}}

63 |
64 | {{ forms.textField({ 65 | fieldClass: 'nomarginfield', 66 | id: "redirectSrcUrl", 67 | class: 'nicetext', 68 | name: "redirectSrcUrl", 69 | value: values.redirectSrcUrl, 70 | maxlength: 255, 71 | showCharsLeft: true, 72 | required: true, 73 | }) }} 74 |
75 | 76 |
77 |
78 | 79 |

{{ "Enter the destination URL that should be redirected to. This can either be a fully qualified URL or a relative URL. e.g.: Exact Match: /new-recipes/ or RegEx Match: /new-recipes/$1" |t |raw}}

80 |
81 | {{ forms.textField({ 82 | fieldClass: 'nomarginfield', 83 | id: "redirectDestUrl", 84 | class: 'nicetext', 85 | name: "redirectDestUrl", 86 | value: values.redirectDestUrl, 87 | maxlength: 255, 88 | showCharsLeft: true, 89 | required: true, 90 | }) }} 91 |
92 | 93 |
94 |
95 | 96 |

{{ "What type of matching should be done with the Legacy URL Pattern. Details on RegEx matching can be found at regexr.com If a plugin provides a custom matching function, you can select it here." |t |raw}}

97 |
98 | {{ forms.selectField({ 99 | fieldClass: 'nomarginfield', 100 | class: 'selectField', 101 | id: "redirectMatchType", 102 | options: matchList, 103 | name: "redirectMatchType", 104 | value: values.redirectMatchType, 105 | }) }} 106 |
107 | 108 |
109 |
110 | 111 |

{{ "Select whether the redirect should be permanent or temporary." |t |raw}}

112 |
113 | {{ forms.selectField({ 114 | fieldClass: 'nomarginfield', 115 | class: 'selectField', 116 | id: "redirectHttpCode", 117 | options: { 118 | "301": "301 - Permanent"|t, 119 | "302": "302 - Temporary"|t, 120 | "410": "410 - Gone"|t, 121 | }, 122 | name: "redirectHttpCode", 123 | value: values.redirectHttpCode, 124 | }) }} 125 |
126 | 127 | 128 | 129 | {% if craft.app.version < 2.5 %} 130 |
131 |
132 |
133 | 134 |
135 |
136 |
137 | {% endif %} 138 | 139 | 140 | 141 | 142 | {% if craft.app.version < 2.5 %} 143 |
144 | {% endif %} 145 | 146 | {% endset %} 147 | -------------------------------------------------------------------------------- /resources/css/RetourTables.css: -------------------------------------------------------------------------------- 1 | input[type=search]::-webkit-search-cancel-button, 2 | input[type=search]::-webkit-search-decoration, 3 | input[type=search]::-webkit-search-results-button, 4 | input[type=search]::-webkit-search-results-decoration { 5 | -webkit-appearance:none; 6 | } 7 | 8 | div.top { 9 | padding-bottom: 10px; 10 | } 11 | 12 | div.bottom { 13 | padding-top: 10px; 14 | } 15 | 16 | /* -- The cancel button that appears after the datatable_filter div */ 17 | 18 | .filter-cancel-button { 19 | float: right; 20 | padding-top: 12px; 21 | padding-right: 10px; 22 | cursor: pointer; 23 | z-index: 10; 24 | clear: both; 25 | position: relative; 26 | height: 16px; 27 | width: 16px; 28 | } 29 | 30 | .dataTables_wrapper { 31 | padding: 0; 32 | width: 100%; 33 | } 34 | 35 | /* -- Override the default datatable styles */ 36 | 37 | 38 | table.dataTable td { 39 | padding-top: 10px; 40 | padding-bottom: 10px; 41 | padding-left: 10px; 42 | padding-right: 10px; 43 | } 44 | 45 | table.dataTable th { 46 | padding-top: 10px; 47 | padding-bottom: 5px; 48 | padding-left: 10px; 49 | padding-right: 10px; 50 | outline: 0; 51 | } 52 | 53 | table.dataTable tr.odd { 54 | } 55 | 56 | table.dataTable tr.even { 57 | } 58 | 59 | table.dataTable tr.odd td.sorting_1 { 60 | } 61 | 62 | table.dataTable tr.odd td.sorting_2 { 63 | } 64 | 65 | table.dataTable tr.odd td.sorting_3 { 66 | } 67 | 68 | table.dataTable tr.even td.sorting_1 { 69 | } 70 | 71 | table.dataTable tr.even td.sorting_2 { 72 | } 73 | 74 | table.dataTable tr.even td.sorting_3 { 75 | } 76 | 77 | table.dataTable thead th { 78 | } 79 | 80 | .dataTables_wrapper { 81 | position: relative; 82 | clear: both; 83 | *zoom: 1; 84 | zoom: 1; 85 | } 86 | 87 | 88 | table.dataTable thead th.sorting > span.sort-label { 89 | padding-right: 15px; 90 | margin-right: -15px; 91 | background: url('images/sort_both.png') no-repeat center right; 92 | background-size: 16px 16px; 93 | cursor: pointer; 94 | } 95 | 96 | table.dataTable thead th.sorting { 97 | } 98 | 99 | table.dataTable thead th.sorting_desc { 100 | } 101 | 102 | table.dataTable thead th.sorting_asc { 103 | } 104 | 105 | table.dataTable thead th.sorting_desc > span.sort-label { 106 | padding-right: 15px; 107 | margin-right: -15px; 108 | background: url('images/sort_desc.png') no-repeat center right; 109 | background-size: 16px 16px; 110 | cursor: pointer; 111 | } 112 | 113 | table.dataTable thead th.sorting_asc > span.sort-label { 114 | padding-right: 15px; 115 | margin-right: -15px; 116 | background: url('images/sort_asc.png') no-repeat center right; 117 | background-size: 16px 16px; 118 | cursor: pointer; 119 | } 120 | 121 | div.dataTables_length { 122 | float: left; 123 | padding: 6px; 124 | } 125 | 126 | .dataTables_length select { 127 | display: inline; 128 | width: 100px; 129 | } 130 | 131 | div.dataTables_filter { 132 | float: right; 133 | margin-right: -22px; 134 | padding: 6px; 135 | padding-right: 0px; 136 | } 137 | 138 | .dataTables_filter input { 139 | display: inline; 140 | width: 150px; 141 | outline: none; 142 | border: 1px solid #aaa; 143 | padding: 6px 4px; 144 | border-radius: 15px; 145 | -moz-border-radius: 15px; 146 | -webkit-border-radius: 15px; 147 | padding-left: 10px; 148 | padding-right: 0px; 149 | -webkit-appearance:none; 150 | } 151 | 152 | div.dataTables_info { 153 | float: left; 154 | text-transform: uppercase; 155 | font-size: 1em; 156 | font-family: sans-serif; 157 | color: #AAA; 158 | } 159 | 160 | .dataTables_filter label { 161 | text-transform: lowercase; 162 | color: #444444; 163 | font-size: 1em; 164 | font-family: sans-serif; 165 | font-weight: normal; 166 | } 167 | 168 | 169 | /* -- pagination */ 170 | .dataTables_paginate { 171 | float: right; 172 | text-align: right; 173 | padding-bottom: 5px; 174 | } 175 | 176 | /* -- vertical center align on search page */ 177 | .center-vertical-search { 178 | line-height: 32px; 179 | display: table-cell; 180 | vertical-align: middle; 181 | } 182 | 183 | /* -- The first, last, next, previous, and regulate data tables paginating buttons */ 184 | 185 | a.paginate_button { 186 | text-transform: lowercase; 187 | background-color: rgba(255, 255, 255, 0.2); 188 | display: inline-block; 189 | text-decoration: none; 190 | border-radius: 4px; 191 | border-color: rgba(51, 51, 51, 0.3); 192 | border-style: solid; 193 | border-width: thin; 194 | cursor: pointer; 195 | color: #999; 196 | padding: 1px 5px; 197 | margin: 0 4px; 198 | min-width: 6px; 199 | } 200 | 201 | /* -- The currently on paginate button */ 202 | 203 | a.paginate_active { 204 | background-color: rgba(151, 151, 151, 0.1); 205 | text-decoration: underline; 206 | } 207 | 208 | a.paginate_button.current { 209 | background-color: rgba(151, 151, 151, 0.1)!important; 210 | text-decoration: underline!important; 211 | } 212 | 213 | .paging_full_numbers a:active { 214 | text-transform: lowercase; 215 | background-color: rgba(255, 255, 255, 0.2); 216 | display: inline-block; 217 | text-decoration: none; 218 | border-radius: 4px; 219 | border-color: rgba(151, 151, 151, 0.3); 220 | border-style: solid; 221 | border-width: thin; 222 | cursor: pointer; 223 | color: #999; 224 | padding: 1px 5px; 225 | margin: 0 4px; 226 | min-width: 6px; 227 | } 228 | 229 | .paging_full_numbers a.paginate_active { 230 | text-transform: lowercase; 231 | background-color: #ffffff; 232 | display: inline-block; 233 | text-decoration: underline; 234 | border-radius: 4px; 235 | border-color: rgba(151, 151, 151, 0.3); 236 | border-style: solid; 237 | border-width: thin; 238 | cursor: pointer; 239 | color: #999; 240 | padding: 1px 5px; 241 | margin: 0 4px; 242 | min-width: 6px; 243 | } 244 | 245 | .paging_full_numbers a.paginate_button { 246 | text-transform: lowercase; 247 | background-color: rgba(255, 255, 255, 0.2); 248 | display: inline-block; 249 | text-decoration: none; 250 | border-radius: 4px; 251 | border-color: rgba(151, 151, 151, 0.3); 252 | border-style: solid; 253 | border-width: thin; 254 | cursor: pointer; 255 | color: #999; 256 | padding: 1px 5px; 257 | margin: 0 4px; 258 | min-width: 6px; 259 | } 260 | 261 | .paging_full_numbers a.paginate_button:hover { 262 | background-color: rgba(151, 151, 151, 0.1); 263 | text-decoration: none; 264 | } 265 | -------------------------------------------------------------------------------- /fieldtypes/RetourFieldType.php: -------------------------------------------------------------------------------- 1 | templates->formatInputId($name); 50 | $namespacedId = craft()->templates->namespaceInputId($id); 51 | 52 | // Include our Javascript & CSS 53 | craft()->templates->includeCssResource('retour/css/fields/RetourFieldType.css'); 54 | craft()->templates->includeJsResource('retour/js/fields/RetourFieldType.js'); 55 | 56 | // Variables to pass down to our field.js 57 | $jsonVars = array( 58 | 'id' => $id, 59 | 'name' => $name, 60 | 'namespace' => $namespacedId, 61 | 'prefix' => craft()->templates->namespaceInputId(""), 62 | ); 63 | 64 | $jsonVars = json_encode($jsonVars); 65 | craft()->templates->includeJs("$('#{$namespacedId}').RetourFieldType(" . $jsonVars . ");"); 66 | 67 | // Get the list of matches 68 | $matchList = craft()->retour->getMatchesList(); 69 | 70 | // Variables to pass down to our rendered template 71 | $variables = array( 72 | 'id' => $id, 73 | 'name' => $name, 74 | 'namespaceId' => $namespacedId, 75 | 'matchList' => $matchList, 76 | 'element' => $this->element, 77 | 'field' => $this->model, 78 | 'values' => $value, 79 | ); 80 | 81 | return craft()->templates->render('retour/fields/RetourFieldType.twig', $variables); 82 | } 83 | 84 | /** 85 | * Render the field settings 86 | * 87 | * @return none 88 | */ 89 | public function getSettingsHtml() 90 | { 91 | // Get the list of matches 92 | $matchList = craft()->retour->getMatchesList(); 93 | 94 | return craft()->templates->render('retour/fields/RetourFieldType_Settings', array( 95 | 'matchList' => $matchList, 96 | 'settings' => $this->getSettings(), 97 | )); 98 | } 99 | 100 | /** 101 | * @inheritDoc IFieldType::onAfterElementSave() 102 | */ 103 | public function onAfterElementSave() 104 | { 105 | $fieldHandle = $this->model->handle; 106 | $attributes = $this->element->content->attributes; 107 | $retourModel = null; 108 | if (isset($attributes[$fieldHandle])) { 109 | $retourModel = $attributes[$fieldHandle]; 110 | } 111 | $value = $this->prepValueFromPost($retourModel); 112 | 113 | if ($value) { 114 | RetourPlugin::log("Resaving Retour field data", LogLevel::Info, false); 115 | 116 | // If the redirectSrcUrl is empty, don't save it, and delete any existing record 117 | if ($value->redirectSrcUrl == "") { 118 | craft()->retour->deleteRedirectByElementId($value->associatedElementId, $value->locale); 119 | } else { 120 | $error = craft()->cache->flush(); 121 | RetourPlugin::log("Cache flushed: " . print_r($error, true), LogLevel::Info, false); 122 | craft()->retour->saveRedirect($value); 123 | } 124 | } 125 | 126 | parent::onAfterElementSave(); 127 | } 128 | 129 | /** 130 | * Returns the input value as it should be saved to the database. 131 | * 132 | * @param mixed $value 133 | * 134 | * @return mixed 135 | */ 136 | public function prepValueFromPost($value) 137 | { 138 | $result = null; 139 | 140 | if (empty($value)) { 141 | $result = $this->prepValue($value); 142 | } else { 143 | $result = new Retour_RedirectsFieldModel($value); 144 | } 145 | $urlParts = parse_url($this->element->url); 146 | $url = $urlParts['path'] ? $urlParts['path'] : $this->element->url; 147 | if (craft()->config->get('addTrailingSlashesToUrls')) { 148 | $url = rtrim($url, '/') . '/'; 149 | } 150 | $result->redirectDestUrl = $url; 151 | $result->associatedElementId = $this->element->id; 152 | if ($this->model->translatable) { 153 | $locale = $this->element->locale; 154 | } else { 155 | $locale = craft()->language; 156 | } 157 | $result->locale = $locale; 158 | if ($result->redirectMatchType == "exactmatch" && $result->redirectSrcUrl !== '') { 159 | $result->redirectSrcUrl = '/' . ltrim($result->redirectSrcUrl, '/'); 160 | } 161 | 162 | // Restore the default fields we don't let the user edit 163 | $oldRecord = craft()->retour->getRedirectByElementId($this->element->id, $locale); 164 | 165 | if ($oldRecord) { 166 | $result->hitCount = $oldRecord->hitCount; 167 | $result->hitLastTime = $oldRecord->hitLastTime; 168 | } 169 | 170 | try { 171 | $result->redirectSrcUrlParsed = craft()->templates->renderObjectTemplate($result->redirectSrcUrl, $this->element); 172 | } catch (Exception $e) { 173 | RetourPlugin::log("Template error in the `redirectSrcUrl` field.", LogLevel::Info, true); 174 | } 175 | 176 | return $result; 177 | } 178 | 179 | /** 180 | * Prepares the field's value for use. 181 | * 182 | * @param mixed $value 183 | * 184 | * @return mixed 185 | */ 186 | public function prepValue($value) 187 | { 188 | if (!$value) { 189 | $value = new Retour_RedirectsFieldModel(); 190 | if ($this->model->translatable) { 191 | $locale = $this->element->locale; 192 | } else { 193 | $locale = craft()->language; 194 | } 195 | $value->locale = $locale; 196 | $result = craft()->retour->getRedirectByElementId($this->element->id, $locale); 197 | if ($result) { 198 | $value->setAttributes($result->getAttributes(), false); 199 | } else { 200 | $value->redirectSrcUrl = $this->getSettings()->defaultRedirectSrcUrl; 201 | $value->redirectMatchType = $this->getSettings()->defaultRedirectMatchType; 202 | $value->redirectHttpCode = $this->getSettings()->defaultRedirectHttpCode; 203 | } 204 | } 205 | 206 | $value->redirectChangeable = $this->getSettings()->redirectChangeable; 207 | 208 | return $value; 209 | } 210 | 211 | /** 212 | * Define our FieldType's settings 213 | * 214 | * @return array 215 | */ 216 | protected function defineSettings() 217 | { 218 | return array( 219 | 'defaultRedirectSrcUrl' => array(AttributeType::String, 'default' => ''), 220 | 'defaultRedirectMatchType' => array(AttributeType::String, 'default' => 'match'), 221 | 'defaultRedirectHttpCode' => array(AttributeType::Number, 'default' => 301), 222 | 'redirectChangeable' => array(AttributeType::Bool, 'default' => 1), 223 | ); 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Retour Changelog 2 | 3 | ## 1.0.22 - 2017.10.02 4 | ### Changed 5 | * Fixed an issue in the `Retour_StatsModel` 6 | * Exact matches are now checked before RegEx matches for static redirects 7 | * Fixed the documentation that was incorrect about being able to add multiple FieldTypes to a single entry 8 | * Replace 4-byte UTF-8 characters in `Retour_Statistics` before saving them, to avoid db errors 9 | 10 | ## 1.0.21 - 2017.08.31 11 | ### Changed 12 | * Fixed a hardcoded table name in the `referrerUrl` migration 13 | 14 | ## 1.0.20 - 2017.08.30 15 | ### Added 16 | * Retour will automatically trim the `retour_stats` table to the last 10,000 redirects, sorted by date (configurable via `statsStoredLimit` in `config.php`) 17 | * Add the URL to the stats title attribute, for cases where the display is truncated 18 | * Respect `addTrailingSlashesToUrls` in the URLs returned from `getLocalizedrUls()` 19 | * Updated README.md to note that importing `.htaccess` ignores `RewriteRule` 20 | * Added a `alwaysStripQueryString` setting to `config.php` (defaults to `false`) 21 | * Added a `stripQueryStringFromStats` setting to `config.php` (defaults to `true`) 22 | 23 | ### Changed 24 | * Refactored and cleaned up the code 25 | * Fixed some deprecated meta information in `RetourPlugin.php` 26 | * The `referrerUrl` has a maximum length of 2000 now 27 | * Fixed some typos in `README.md` 28 | 29 | ## 1.0.19 - 2017.02.10 30 | 31 | * [Added] Added a referrer column in the Stats table 32 | * [Added] Added additional logging in `devMode` 33 | * [Improved] No more default value for `redirectSrcUrl` column (could cause SQL exceptions in newer versions of MySQL) 34 | * [Improved] Updated CHANGELOG.md 35 | 36 | ## 1.0.18 - 2017.01.20 37 | 38 | * [Fixed] The `addTrailingSlashesToUrls` is now respected for dynamic entry redirects 39 | * [Improved] Merged pull request 'Fix retourMatch hook' 40 | * [Added] Added a `statsDisplayLimit` setting to `config.php` to control how many stats should be displayed in the AdminCP 41 | * [Improved] Merged pull request 'Limit returned results to template' 42 | * [Improved] Merged pull request 'Allow handling of 404s from { exit } tags encountered while rendering templates' 43 | * [Added] Added a config.php setting `createUriChangeRedirects` so that the URI-change redirects can be disabled on a per-environment basis 44 | * [Improved] Don't redirect to the welcome page if we're being installed via Console command 45 | * [Improved] Moved the changelog to CHANGELOG.md 46 | 47 | ## 1.0.17 - 2016.08.31 48 | 49 | * [Improved] Query strings are now stripped from the incoming URI before redirect detection is done 50 | * [Improved] Updated the README.md 51 | 52 | ## 1.0.16 - 2016.08.30 53 | 54 | * [Fixed] FieldTypes in multi-locale setups that are not translatable are now handled properly 55 | * [Fixed] Fixed missing locale prefix for localized entries in the FieldType 56 | * [Fixed] Fixed an issue where FieldType redirects had an errant / prepended to them 57 | * [Improved] Better importing of `.htaccess` files 58 | * [Improved] Better error handling when importing malformed `.htaccess` files 59 | * [Fixed] Trailing /'s are no longer stripped from URLs added via the `+` icon from the Statistics page 60 | * [Fixed] Fixed an issue that would prevent RegEx's from matching as they should in FieldTypes 61 | * [Improved] Updated the README.md 62 | 63 | ## 1.0.15 - 2016.07.12 64 | 65 | * [Added] Added the ability to import the redirects from a `.htaccess` file into Retour 66 | * [Fixed] Fixed a statics db error with empty referrers 67 | * [Improved] Updated the README.md 68 | 69 | ## 1.0.14 - 2016.07.10 70 | 71 | * [Added] The Statistics and Redirects tables are now dynamically searchable and sortable 72 | * [Fixed] Fixed an issue that caused redirects created via the `+` from Statistics page to not save 73 | * [Improved] Updated the README.md 74 | 75 | ## 1.0.13 - 2016.07.06 76 | 77 | * [Added] Adds support for locales in the automatic redirect that is created when a slug is changed for an entry 78 | * [Improved] Retour will no longer let you save a static redirect with an empty destinationURL 79 | * [Fixed] Fixed a typo in the Retour_StatsModel 80 | * [Improved] Added a rant about `.htaccess` to the docs 81 | * [Improved] Updated the README.md 82 | 83 | ## 1.0.12 - 2016.07.04 84 | 85 | * [Added] If you hover over a 404 File Not Found URL on the Statistics page, you'll now see the last referrer for the 404 URL 86 | * [Added] Added a + button on the Statistics page that lets you quickly add a 404'd URL as a redirect 87 | * [Improved] We now store the destination for redirects in the FieldType as a URI rather than a URL, so that it's more portable across environments 88 | * [Added] Structure entries that have Retour FieldTypes in them now have the destinationURL updated when the structure elements are moved 89 | * [Improved] The widget now handles very long URLs more gracefully 90 | * [Improved] Updated the README.md 91 | 92 | ## 1.0.11 - 2016.06.21 93 | 94 | * [Fixed] Fixed an issue with URLs that have umlauts in them 95 | * [Fixed] Fixed an issue with URLs that are longer than 255 characters for the redirect statistics 96 | * [Improved] Statistics are now limited to the top 1,000 hits 97 | * [Improved] Updated the README.md 98 | 99 | ## 1.0.10 - 2016.06.15 100 | 101 | * [Added] Retour will attempt to prevent redirect loops when saving a new redirect by deleting any existing redirects that have the destUrl as their srcUrl 102 | * [Added] Added a 410 - Gone redirect http code for permanently removed resources 103 | * [Improved] Updated the README.md 104 | 105 | ## 1.0.9 - 2016.06.04 106 | 107 | * [Added] Retour will now automatically create a static redirect for you if you rename an entry's slug 108 | * [Improved] Retour checks to ensure that no two redirects have the same redirectSrcUrl 109 | * [Improved] The Statistics page handles really long URLs better now 110 | * [Improved] If you save a redirect, either static or dynamic, with an empty Legacy URL Pattern, retour now deletes it 111 | * [Improved] Updated the README.md 112 | 113 | ## 1.0.8 - 2016.05.30 114 | 115 | * [Improved] Revamped Retour to key off of the ElementID rather than the EntryID 116 | * [Fixed] Fixed an issue with Retour and MySQL running in strict mode (which is the default in 5.7+) 117 | * [Fixed] Retour will no longer try to save a record with a null id (caused a CDbCommand exception) 118 | * [Fixed] A '/' isn't prepended to empty src URLs anymore 119 | * [Improved] Updated the README.md 120 | 121 | ## 1.0.7 - 2016.05.07 122 | 123 | * [Improved] getRequestUri() is now explicitly used, and we immediately terminate the request upon redirect 124 | * [Improved] We now pass in 0 instead of null for the cache duration 125 | * [Improved] We now explicitly check for CHttpException 126 | * [Improved] Updated the README.md 127 | 128 | ## 1.0.6 - 2016.04.29 129 | 130 | * [Fixed] Fixed a Javascript error with the FieldType Javascript 131 | * [Fixed] Fixed a visual display glitch with the tabs on Craft 2.4.x 132 | * [Improved] Updated the README.md 133 | 134 | ## 1.0.5 - 2016.04.28 135 | 136 | * [Added] Added a 'Clear Statistics' button to the Statistics page 137 | * [Fixed] Fixed a bug when using RegEx for static redirects that would cause them to not work 138 | * [Fixed] Fixed an issue with Craft 2.4.x 139 | * [Improved] Updated the README.md 140 | 141 | ## 1.0.4 - 2016.04.28 142 | 143 | * [Added] The tables in the Statistics and Redirects pages are now sortable by any column 144 | * [Improved] Fixed up the localization support for the FieldType 145 | * [Improved] Minor changes/fixes to the plugin 146 | * [Improved] Updated the README.md 147 | 148 | ## 1.0.3 - 2016.04.27 149 | 150 | * [Added] Added a Retour Stats widget 151 | * [Added] Added information on the Statistics tab as to whether Retour handled the 404 or not 152 | * [Improved] Updated the README.md 153 | 154 | ## 1.0.2 - 2016.04.26 155 | 156 | * [Fixed] Fixed faulty indexes that could cause Retour Redirect FieldTypes to not work properly 157 | * [Improved] Spiffy new icon 158 | * [Improved] Changing the display name of the plugin is now more globally applied 159 | * [Improved] Updated the README.md 160 | 161 | ## 1.0.1 - 2016.04.26 162 | 163 | * [Added] Implemented a caching layer so that once a redirect has been determined, subsequent redirects are cached and immediately returned 164 | * [Added] Added the ability to delete static redirects 165 | * [Added] Added Composer support 166 | * [Improved] Updated the README.md 167 | 168 | ## 1.0.0 - 2016.04.25 169 | 170 | * Initial release 171 | 172 | Brought to you by [nystudio107](http://nystudio107.com) -------------------------------------------------------------------------------- /controllers/RetourController.php: -------------------------------------------------------------------------------- 1 | getTempName(); 27 | $handle = @fopen($filename, "r"); 28 | if ($handle) { 29 | $skippingRule = false; 30 | while (($line = fgets($handle)) !== false) { 31 | $redirectType = ""; 32 | RetourPlugin::log("parsing line: " . $line, LogLevel::Info, false); 33 | $line = ltrim($line); 34 | $line = preg_replace('/\s+/', ' ', $line); 35 | $redirectParts = explode(" ", $line); 36 | RetourPlugin::log("line parts: " . print_r($redirectParts, true), LogLevel::Info, false); 37 | array_shift($redirectParts); 38 | 39 | if ((!empty($redirectParts[0])) && (!empty($redirectParts[1])) && (!empty($redirectParts[2]))) { 40 | if (strpos($line, 'RedirectMatch') === 0) { 41 | $redirectType = "regexmatch"; 42 | $srcUrl = $redirectParts[1]; 43 | $destUrl = $redirectParts[2]; 44 | $redirectCode = $redirectParts[0]; 45 | } elseif (strpos($line, 'Redirect') === 0) { 46 | $redirectType = "exactmatch"; 47 | $srcUrl = $redirectParts[1]; 48 | $destUrl = $redirectParts[2]; 49 | $redirectCode = $redirectParts[0]; 50 | } 51 | } 52 | 53 | /* We should just ignore RewriteRule's completely 54 | if (strpos($line, 'RewriteRule') === 0) 55 | { 56 | $srcUrl = $redirectParts[0]; 57 | $destUrl = $redirectParts[1]; 58 | $redirectCode = $redirectParts[2]; 59 | $pos = strpos($redirectCode, 'R='); 60 | if ($pos !== false) 61 | { 62 | $redirectType = "regexmatch"; 63 | $redirectCode = substr($redirectCode, $pos + 2, 3); 64 | } 65 | } 66 | 67 | if (strpos($line, 'RewriteCond') === 0) 68 | $skippingRule = true; 69 | 70 | if (strpos($line, 'RewriteEngine') === 0) 71 | $skippingRule = false; 72 | */ 73 | if (($redirectType != "") && (!$skippingRule)) { 74 | $record = new Retour_StaticRedirectsRecord; 75 | 76 | $record->locale = craft()->language; 77 | $record->redirectMatchType = $redirectType; 78 | $record->redirectSrcUrl = $srcUrl; 79 | if (($record->redirectMatchType == "exactmatch") && ($record->redirectSrcUrl != "")) { 80 | $record->redirectSrcUrl = '/' . ltrim($record->redirectSrcUrl, '/'); 81 | } 82 | $record->redirectSrcUrlParsed = $record->redirectSrcUrl; 83 | $record->redirectDestUrl = $destUrl; 84 | $record->redirectHttpCode = $redirectCode; 85 | $record->hitLastTime = DateTimeHelper::currentUTCDateTime(); 86 | $record->associatedElementId = 0; 87 | 88 | $result = craft()->retour->saveStaticRedirect($record); 89 | } 90 | } 91 | if (!feof($handle)) { 92 | craft()->userSession->setError(Craft::t('Error: unexpected fgets() fail.')); 93 | } 94 | fclose($handle); 95 | } 96 | } else { 97 | craft()->userSession->setError(Craft::t('Please upload a file.')); 98 | } 99 | } 100 | 101 | /** 102 | * @param array $variables 103 | */ 104 | public function actionEditRedirect(array $variables = array()) 105 | { 106 | // Give us something to edit 107 | $redirectModel = new Retour_RedirectsModel(); 108 | $redirectModel->redirectSrcUrl = craft()->request->getParam('defaultRedirectSrcUrl'); 109 | $redirectId = 0; 110 | if (!empty($variables['redirectId'])) { 111 | $redirectId = $variables['redirectId']; 112 | $record = craft()->retour->getRedirectById($redirectId); 113 | if ($record) { 114 | $redirectModel->setAttributes($record->getAttributes(), false); 115 | } 116 | } 117 | 118 | // Get the list of matches 119 | $matchList = craft()->retour->getMatchesList(); 120 | 121 | // Display the edit template 122 | $this->renderTemplate('retour/_edit', array( 123 | 'values' => $redirectModel, 124 | 'matchList' => $matchList, 125 | 'redirectId' => $redirectId, 126 | )); 127 | } 128 | 129 | /** 130 | * @param array $variables 131 | */ 132 | public function actionSaveRedirect(array $variables = array()) 133 | { 134 | $this->requirePostRequest(); 135 | 136 | $redirectId = craft()->request->getPost('redirectId'); 137 | 138 | if ($redirectId) { 139 | $record = craft()->retour->getRedirectById($redirectId); 140 | } else { 141 | $record = new Retour_StaticRedirectsRecord; 142 | } 143 | 144 | // Set the record attributes, defaulting to the existing values for whatever is missing from the post data 145 | $record->locale = craft()->language; 146 | $record->redirectMatchType = craft()->request->getPost('redirectMatchType', $record->redirectMatchType); 147 | $record->redirectSrcUrl = craft()->request->getPost('redirectSrcUrl', $record->redirectSrcUrl); 148 | if (($record->redirectMatchType == "exactmatch") && ($record->redirectSrcUrl != "")) { 149 | $record->redirectSrcUrl = '/' . ltrim($record->redirectSrcUrl, '/'); 150 | } 151 | $record->redirectSrcUrlParsed = $record->redirectSrcUrl; 152 | $record->redirectDestUrl = craft()->request->getPost('redirectDestUrl', $record->redirectDestUrl); 153 | $record->redirectHttpCode = craft()->request->getPost('redirectHttpCode', $record->redirectHttpCode); 154 | $record->hitLastTime = DateTimeHelper::currentUTCDateTime(); 155 | $record->associatedElementId = 0; 156 | 157 | $result = craft()->retour->saveStaticRedirect($record); 158 | if ($result === "" || $result === -1) { 159 | $this->redirectToPostedUrl($record); 160 | } else { 161 | // Send the record back to the template 162 | craft()->urlManager->setRouteVariables(array( 163 | 'values' => $record, 164 | )); 165 | } 166 | } 167 | 168 | /** 169 | */ 170 | public function actionDeleteRedirect() 171 | { 172 | $this->requirePostRequest(); 173 | $this->requireAjaxRequest(); 174 | 175 | $id = craft()->request->getRequiredPost('id'); 176 | 177 | $affectedRows = craft()->db->createCommand()->delete('retour_static_redirects', array( 178 | 'id' => $id, 179 | )); 180 | 181 | RetourPlugin::log("Deleted Redirected: " . $id, LogLevel::Info, false); 182 | $error = craft()->cache->flush(); 183 | RetourPlugin::log("Cache flushed: " . print_r($error, true), LogLevel::Info, false); 184 | 185 | $this->returnJson(array('success' => true)); 186 | } 187 | 188 | /** 189 | */ 190 | public function actionEditSettings() 191 | { 192 | $retourPlugin = craft()->plugins->getPlugin('retour'); 193 | $settings = $retourPlugin->getSettings(); 194 | 195 | $this->renderTemplate('retour/settings', array( 196 | 'settings' => $settings, 197 | )); 198 | } 199 | 200 | /** 201 | */ 202 | public function actionClearStatistics() 203 | { 204 | 205 | $error = craft()->retour->clearStatistics(); 206 | RetourPlugin::log("Statistics cleared: " . print_r($error, true), LogLevel::Info, false); 207 | 208 | $error = craft()->cache->flush(); 209 | RetourPlugin::log("Cache flushed: " . print_r($error, true), LogLevel::Info, false); 210 | 211 | craft()->userSession->setNotice(Craft::t('Statistics Cleared.')); 212 | craft()->request->redirect('statistics'); 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /templates/redirects.twig: -------------------------------------------------------------------------------- 1 | {% extends "_layouts/cp" %} 2 | {% import '_includes/forms' as forms %} 3 | 4 | {% includeCssResource "retour/css/Retour.css" %} 5 | {% includeCssResource "retour/css/RetourTables.css" %} 6 | {% includeJsResource "retour/js/Retour.js" %} 7 | {% includeJsResource "retour/js/datatables.min.js" %} 8 | 9 | {% set title = craft.retour.getPluginName() %} 10 | 11 | {% set docsUrl = "https://github.com/nystudio107/retour/blob/master/README.md" %} 12 | 13 | {% set retourSections = { 14 | redirects: { label: "Redirects"|t, url: url('retour/redirects') }, 15 | statistics: { label: "Statistics"|t, url: url('retour/statistics') }, 16 | settings: { label: "Settings"|t, url: url('retour/settings') }, 17 | } %} 18 | 19 | {% set crumbs = [ 20 | { label: craft.retour.getPluginName(), url: url('retour') }, 21 | { label: "Redirects"|t, url: url('retour/redirects') }, 22 | ] %} 23 | 24 | {% if craft.app.version < 2.5 %} 25 | {% set tabs = retourSections %} 26 | {% set selectedTab = 'redirects' %} 27 | {% else %} 28 | {% set subnav = retourSections %} 29 | {% set selectedSubnavItem = 'redirects' %} 30 | {% endif %} 31 | 32 | {% set extraPageHeaderHtml %} 33 |
34 |
35 |
36 | {{ getCsrfInput() }} 37 | 38 | 39 | 40 |
41 |
42 | {{ "New Static Redirect"|t }} 43 |
44 | {% endset %} 45 | 46 | {% set content %} 47 | 48 | {% set matchesList = craft.retour.getMatchesList() %} 49 | 50 | 51 | 52 |

{{ "Static Redirects" |t }}

53 | 54 | {% set redirects = craft.retour.getStaticRedirects(craft.config.get('staticRedirectDisplayLimit', 'retour')) %} 55 | {% if redirects|length %} 56 |
57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | {% for redir in redirects %} 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | {% endfor %} 80 | 81 | 82 |
{{ "Legacy URL Pattern"|t }}{{ "Redirect To"|t }}{{ "Pattern Match Type"|t }}{{ "Redirect Type"|t }}{{ "Hits"|t }}{{ "Last Hit"|t }} 
{{ redir.redirectSrcUrl }}{{ redir.redirectDestUrl }}{{ matchesList[redir.redirectMatchType] }}{{ redir.redirectHttpCode }}{{ redir.hitCount }}{{ redir.hitLastTime |datetime }}
83 |
84 | {% else %} 85 |

You have no Static Redirects. You can create clicking on the + New Static Redirect button.

86 | {% endif %} 87 | 88 |

89 | 90 | 91 | 92 |

{{ "Dynamic Entry Redirects" |t }}

93 | 94 | {% set redirects = craft.retour.getEntryRedirects(craft.config.get('dynamicRedirectDisplayLimit', 'retour')) %} 95 | {% if redirects|length %} 96 |
97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | {% for redir in redirects %} 109 | {% set associatedEntry = craft.entries.id(redir.associatedElementId).locale(redir.locale).first %} 110 | {% if associatedEntry %} 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | {% endif %} 120 | {% endfor %} 121 | 122 | 123 |
{{ "Legacy URL Pattern"|t }}{{ "Redirect To"|t }}{{ "Pattern Match Type"|t }}{{ "Redirect Type"|t }}{{ "Hits"|t }}{{ "Last Hit"|t }}
{{ redir.redirectSrcUrl }}{{ redir.redirectDestUrl }}{{ matchesList[redir.redirectMatchType] }}{{ redir.redirectHttpCode }}{{ redir.hitCount }}{{ redir.hitLastTime |datetime }}
124 |
125 | {% else %} 126 |

You have no Dynamic Entry Redirects. You can create them by adding a Retour Redirect FieldType to your Section Entry Types.

127 | {% endif %} 128 | 129 | {% endset %} 130 | 131 | {% set js %} 132 | 133 | /* -- Make it a Craft Admin table */ 134 | 135 | new Craft.AdminTable({ 136 | tableSelector: '#static-redirects', 137 | noObjectsSelector: '#no-static-redirects', 138 | deleteAction: 'retour/deleteRedirect' 139 | }); 140 | 141 | /* -- Initialize the datatable */ 142 | 143 | $('#static-redirects').dataTable({ 144 | "sDom": '<"top"ilpf<"clear">>rt<"bottom"ilp<"clear">>', 145 | "sPaginationType": "full_numbers", 146 | "aaSorting": [[ 4, "desc" ]], 147 | "aoColumns": [ null, null, null, { "sType": "num" }, { "sType": "num" }, { "sType": "date" }, null], 148 | "bLengthChange": false, 149 | "iDisplayLength": 20, 150 | "bInfo": true, 151 | "oLanguage": { 152 | "sSearch": "Redirects Filter ", 153 | "oPaginate": { 154 | "sFirst": "«", 155 | "sLast": "»", 156 | "sNext": "›", 157 | "sPrevious": "‹" 158 | } 159 | }, 160 | "bAutoWidth": false }); 161 | 162 | /* -- Add our cancel button to the table */ 163 | 164 | {% set cancelButton = resourceUrl('retour/cancel.png') %} 165 | var sOnClick = "onclick=\"$(document).ready(function() {var oTable = $('#static-redirects').dataTable();oTable.fnFilter('');});\""; 166 | var sMyElem = "
"; 167 | 168 | $( sMyElem ).insertBefore( "#static-redirects_filter" ); 169 | 170 | /* -- Initialize the datatable */ 171 | 172 | $('#entry-redirects').dataTable({ 173 | "sDom": '<"top"ilpf<"clear">>rt<"bottom"ilp<"clear">>', 174 | "sPaginationType": "full_numbers", 175 | "aaSorting": [[ 4, "desc" ]], 176 | "aoColumns": [ null, null, null, { "sType": "num" }, { "sType": "num" }, { "sType": "date" }], 177 | "bLengthChange": false, 178 | "iDisplayLength": 20, 179 | "bInfo": true, 180 | "oLanguage": { 181 | "sSearch": "Redirects Filter ", 182 | "oPaginate": { 183 | "sFirst": "«", 184 | "sLast": "»", 185 | "sNext": "›", 186 | "sPrevious": "‹" 187 | } 188 | }, 189 | "bAutoWidth": false }); 190 | 191 | /* -- Add our cancel button to the table */ 192 | 193 | {% set cancelButton = resourceUrl('retour/cancel.png') %} 194 | var sOnClick = "onclick=\"$(document).ready(function() {var oTable = $('#entry-redirects').dataTable();oTable.fnFilter('');});\""; 195 | var sMyElem = "
"; 196 | 197 | $( sMyElem ).insertBefore( "#entry-redirects_filter" ); 198 | 199 | {% endset %} 200 | {% includeJs js %} 201 | -------------------------------------------------------------------------------- /RetourPlugin.php: -------------------------------------------------------------------------------- 1 | onException = function (\CExceptionEvent $event) { 30 | if ((($event->exception instanceof \CHttpException) && ($event->exception->statusCode == 404)) || 31 | (($event->exception->getPrevious() instanceof \CHttpException) && ($event->exception->getPrevious()->statusCode == 404))) { 32 | RetourPlugin::log("A 404 exception occurred", LogLevel::Info, false); 33 | if (craft()->request->isSiteRequest() && !craft()->request->isLivePreview()) { 34 | // See if we should redirect 35 | $url = urldecode(craft()->request->getRequestUri()); 36 | // Strip the query string if `alwaysStripQueryString` is set 37 | if (craft()->config->get("alwaysStripQueryString", "retour")) { 38 | $url = UrlHelper::stripQueryString($url); 39 | } 40 | $noQueryUrl = UrlHelper::stripQueryString($url); 41 | RetourPlugin::log("404 URL: " . $url, LogLevel::Info, false); 42 | 43 | // Redirect if we find a match, otherwise let Craft handle it 44 | $redirect = craft()->retour->findRedirectMatch($url); 45 | 46 | if (isset($redirect)) { 47 | craft()->retour->incrementStatistics($url, true); 48 | $event->handled = true; 49 | RetourPlugin::log("Redirecting " . $url . " to " . $redirect['redirectDestUrl'], LogLevel::Info, false); 50 | craft()->request->redirect($redirect['redirectDestUrl'], true, $redirect['redirectHttpCode']); 51 | } else { 52 | // Now try it without the query string, too, otherwise let Craft handle it 53 | $redirect = craft()->retour->findRedirectMatch($noQueryUrl); 54 | 55 | if (isset($redirect)) { 56 | craft()->retour->incrementStatistics($url, true); 57 | $event->handled = true; 58 | RetourPlugin::log("Redirecting " . $url . " to " . $redirect['redirectDestUrl'], LogLevel::Info, false); 59 | craft()->request->redirect($redirect['redirectDestUrl'], true, $redirect['redirectHttpCode']); 60 | } else { 61 | craft()->retour->incrementStatistics($url, false); 62 | } 63 | } 64 | } 65 | } 66 | }; 67 | 68 | // Listen for structure changes so we can regenerated our FieldType's URLs 69 | craft()->on('structures.onMoveElement', function (Event $e) { 70 | $element = $e->params['element']; 71 | $elemType = $element->getElementType(); 72 | if ($element) { 73 | if ($elemType == ElementType::Entry) { 74 | // Check the field layout, so that we only do this for FieldLayouts that have our Retour fieldtype 75 | $fieldLayouts = $element->fieldLayout->getFields(); 76 | foreach ($fieldLayouts as $fieldLayout) { 77 | $field = craft()->fields->getFieldById($fieldLayout->fieldId); 78 | if ($field->type == "Retour") { 79 | craft()->elements->saveElement($element); 80 | RetourPlugin::log("Resaved moved structure element", LogLevel::Info, false); 81 | break; 82 | } 83 | } 84 | } 85 | } 86 | }); 87 | 88 | // Listen for entries whose slug changes 89 | craft()->on('entries.onBeforeSaveEntry', function (Event $e) { 90 | $this->originalUris = array(); 91 | if (!$e->params['isNewEntry'] && craft()->config->get("createUriChangeRedirects", "retour")) { 92 | $entry = $e->params['entry']; 93 | 94 | $thisSection = $entry->getSection(); 95 | if ($thisSection->hasUrls) { 96 | $this->originalUris = craft()->retour->getLocalizedUris($entry); 97 | } 98 | } 99 | }); 100 | 101 | craft()->on('entries.onSaveEntry', function (Event $e) { 102 | if (!$e->params['isNewEntry'] && craft()->config->get("createUriChangeRedirects", "retour")) { 103 | $entry = $e->params['entry']; 104 | $newUris = craft()->retour->getLocalizedUris($entry); 105 | 106 | foreach ($newUris as $newUri) { 107 | $oldUri = current($this->originalUris); 108 | next($this->originalUris); 109 | if ((strcmp($oldUri, $newUri) != 0) && ($oldUri != "")) { 110 | $record = new Retour_StaticRedirectsRecord; 111 | 112 | if (craft()->config->get('addTrailingSlashesToUrls')) { 113 | $oldUri = rtrim($oldUri, '/') . '/'; 114 | $newUri = rtrim($newUri, '/') . '/'; 115 | } 116 | 117 | // Set the record attributes for our new auto-redirect 118 | $record->locale = $entry->locale; 119 | $record->redirectMatchType = 'exactmatch'; 120 | $record->redirectSrcUrl = $oldUri; 121 | if (($record->redirectMatchType == "exactmatch") && ($record->redirectSrcUrl != "")) { 122 | $record->redirectSrcUrl = '/' . ltrim($record->redirectSrcUrl, '/'); 123 | } 124 | $record->redirectSrcUrlParsed = $record->redirectSrcUrl; 125 | $record->redirectDestUrl = $newUri; 126 | if (($record->redirectMatchType == "exactmatch") && ($record->redirectDestUrl != "")) { 127 | $record->redirectDestUrl = '/' . ltrim($record->redirectDestUrl, '/'); 128 | } 129 | $record->redirectHttpCode = '301'; 130 | $record->hitLastTime = DateTimeHelper::currentUTCDateTime(); 131 | $record->associatedElementId = 0; 132 | 133 | $result = craft()->retour->saveStaticRedirect($record); 134 | } 135 | } 136 | } 137 | }); 138 | } 139 | 140 | /** 141 | * Returns the user-facing name. 142 | * 143 | * @return mixed 144 | */ 145 | public function getName() 146 | { 147 | $pluginNameOverride = $this->getSettings()->getAttribute('pluginNameOverride'); 148 | 149 | return empty($pluginNameOverride) ? Craft::t('Retour') : $pluginNameOverride; 150 | } 151 | 152 | /** 153 | * @return mixed 154 | */ 155 | public function getDescription() 156 | { 157 | return Craft::t("Intelligently redirect legacy URLs, so that you don't lose SEO value when rebuilding & restructuring a website."); 158 | } 159 | 160 | /** 161 | * @return string 162 | */ 163 | public function getDocumentationUrl() 164 | { 165 | return 'https://github.com/nystudio107/retour/blob/master/README.md'; 166 | } 167 | 168 | /** 169 | * @return string 170 | */ 171 | public function getReleaseFeedUrl() 172 | { 173 | return 'https://raw.githubusercontent.com/nystudio107/retour/master/releases.json'; 174 | } 175 | 176 | /** 177 | * Returns the version number. 178 | * 179 | * @return string 180 | */ 181 | public function getVersion() 182 | { 183 | return '1.0.22'; 184 | } 185 | 186 | /** 187 | * @return string 188 | */ 189 | public function getSchemaVersion() 190 | { 191 | return '1.0.5'; 192 | } 193 | 194 | /** 195 | * @return string 196 | */ 197 | public function getDeveloper() 198 | { 199 | return 'nystudio107'; 200 | } 201 | 202 | /** 203 | * @return string 204 | */ 205 | public function getDeveloperUrl() 206 | { 207 | return 'http://nystudio107.com'; 208 | } 209 | 210 | /** 211 | * @return bool 212 | */ 213 | public function hasCpSection() 214 | { 215 | return true; 216 | } 217 | 218 | public function registerCpRoutes() 219 | { 220 | return array( 221 | 'retour/settings' => array('action' => 'retour/editSettings'), 222 | 'retour/clearStats' => array('action' => 'retour/clearStatistics'), 223 | 'retour/new' => array('action' => 'retour/editRedirect'), 224 | 'retour/edit/(?P\d+)' => array('action' => 'retour/editRedirect'), 225 | 'retour/htaccess' => array('action' => 'retour/importHtaccess'), 226 | ); 227 | } 228 | 229 | /** 230 | */ 231 | public function onBeforeInstall() 232 | { 233 | } 234 | 235 | /** 236 | */ 237 | public function onAfterInstall() 238 | { 239 | // Show our "Welcome to Retour" message 240 | if (!craft()->isConsole()) { 241 | craft()->request->redirect(UrlHelper::getCpUrl('retour/welcome')); 242 | } 243 | } 244 | 245 | /** 246 | */ 247 | public function onBeforeUninstall() 248 | { 249 | } 250 | 251 | /** 252 | */ 253 | public function onAfterUninstall() 254 | { 255 | } 256 | 257 | /** 258 | * @return array 259 | */ 260 | protected function defineSettings() 261 | { 262 | return array( 263 | 'pluginNameOverride' => AttributeType::String, 264 | ); 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /resources/css/datatables.min.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This combined file was created by the DataTables downloader builder: 3 | * https://datatables.net/download 4 | * 5 | * To rebuild or modify this file with the latest versions of the included 6 | * software please visit: 7 | * https://datatables.net/download/#dt/dt-1.10.12 8 | * 9 | * Included libraries: 10 | * DataTables 1.10.12 11 | */ 12 | 13 | table.dataTable{width:100%;margin:0 auto;clear:both;border-collapse:separate;border-spacing:0}table.dataTable thead th,table.dataTable tfoot th{font-weight:bold}table.dataTable thead th,table.dataTable thead td{padding:10px 18px;border-bottom:1px solid #111}table.dataTable thead th:active,table.dataTable thead td:active{outline:none}table.dataTable tfoot th,table.dataTable tfoot td{padding:10px 18px 6px 18px;border-top:1px solid #111}table.dataTable thead .sorting,table.dataTable thead .sorting_asc,table.dataTable thead .sorting_desc{cursor:pointer;*cursor:hand}table.dataTable thead .sorting,table.dataTable thead .sorting_asc,table.dataTable thead .sorting_desc,table.dataTable thead .sorting_asc_disabled,table.dataTable thead .sorting_desc_disabled{background-repeat:no-repeat;background-position:center right}table.dataTable thead .sorting{background-image:url("DataTables-1.10.12/images/sort_both.png")}table.dataTable thead .sorting_asc{background-image:url("DataTables-1.10.12/images/sort_asc.png")}table.dataTable thead .sorting_desc{background-image:url("DataTables-1.10.12/images/sort_desc.png")}table.dataTable thead .sorting_asc_disabled{background-image:url("DataTables-1.10.12/images/sort_asc_disabled.png")}table.dataTable thead .sorting_desc_disabled{background-image:url("DataTables-1.10.12/images/sort_desc_disabled.png")}table.dataTable tbody tr{background-color:#ffffff}table.dataTable tbody tr.selected{background-color:#B0BED9}table.dataTable tbody th,table.dataTable tbody td{padding:8px 10px}table.dataTable.row-border tbody th,table.dataTable.row-border tbody td,table.dataTable.display tbody th,table.dataTable.display tbody td{border-top:1px solid #ddd}table.dataTable.row-border tbody tr:first-child th,table.dataTable.row-border tbody tr:first-child td,table.dataTable.display tbody tr:first-child th,table.dataTable.display tbody tr:first-child td{border-top:none}table.dataTable.cell-border tbody th,table.dataTable.cell-border tbody td{border-top:1px solid #ddd;border-right:1px solid #ddd}table.dataTable.cell-border tbody tr th:first-child,table.dataTable.cell-border tbody tr td:first-child{border-left:1px solid #ddd}table.dataTable.cell-border tbody tr:first-child th,table.dataTable.cell-border tbody tr:first-child td{border-top:none}table.dataTable.stripe tbody tr.odd,table.dataTable.display tbody tr.odd{background-color:#f9f9f9}table.dataTable.stripe tbody tr.odd.selected,table.dataTable.display tbody tr.odd.selected{background-color:#acbad4}table.dataTable.hover tbody tr:hover,table.dataTable.display tbody tr:hover{background-color:#f6f6f6}table.dataTable.hover tbody tr:hover.selected,table.dataTable.display tbody tr:hover.selected{background-color:#aab7d1}table.dataTable.order-column tbody tr>.sorting_1,table.dataTable.order-column tbody tr>.sorting_2,table.dataTable.order-column tbody tr>.sorting_3,table.dataTable.display tbody tr>.sorting_1,table.dataTable.display tbody tr>.sorting_2,table.dataTable.display tbody tr>.sorting_3{background-color:#fafafa}table.dataTable.order-column tbody tr.selected>.sorting_1,table.dataTable.order-column tbody tr.selected>.sorting_2,table.dataTable.order-column tbody tr.selected>.sorting_3,table.dataTable.display tbody tr.selected>.sorting_1,table.dataTable.display tbody tr.selected>.sorting_2,table.dataTable.display tbody tr.selected>.sorting_3{background-color:#acbad5}table.dataTable.display tbody tr.odd>.sorting_1,table.dataTable.order-column.stripe tbody tr.odd>.sorting_1{background-color:#f1f1f1}table.dataTable.display tbody tr.odd>.sorting_2,table.dataTable.order-column.stripe tbody tr.odd>.sorting_2{background-color:#f3f3f3}table.dataTable.display tbody tr.odd>.sorting_3,table.dataTable.order-column.stripe tbody tr.odd>.sorting_3{background-color:whitesmoke}table.dataTable.display tbody tr.odd.selected>.sorting_1,table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_1{background-color:#a6b4cd}table.dataTable.display tbody tr.odd.selected>.sorting_2,table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_2{background-color:#a8b5cf}table.dataTable.display tbody tr.odd.selected>.sorting_3,table.dataTable.order-column.stripe tbody tr.odd.selected>.sorting_3{background-color:#a9b7d1}table.dataTable.display tbody tr.even>.sorting_1,table.dataTable.order-column.stripe tbody tr.even>.sorting_1{background-color:#fafafa}table.dataTable.display tbody tr.even>.sorting_2,table.dataTable.order-column.stripe tbody tr.even>.sorting_2{background-color:#fcfcfc}table.dataTable.display tbody tr.even>.sorting_3,table.dataTable.order-column.stripe tbody tr.even>.sorting_3{background-color:#fefefe}table.dataTable.display tbody tr.even.selected>.sorting_1,table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_1{background-color:#acbad5}table.dataTable.display tbody tr.even.selected>.sorting_2,table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_2{background-color:#aebcd6}table.dataTable.display tbody tr.even.selected>.sorting_3,table.dataTable.order-column.stripe tbody tr.even.selected>.sorting_3{background-color:#afbdd8}table.dataTable.display tbody tr:hover>.sorting_1,table.dataTable.order-column.hover tbody tr:hover>.sorting_1{background-color:#eaeaea}table.dataTable.display tbody tr:hover>.sorting_2,table.dataTable.order-column.hover tbody tr:hover>.sorting_2{background-color:#ececec}table.dataTable.display tbody tr:hover>.sorting_3,table.dataTable.order-column.hover tbody tr:hover>.sorting_3{background-color:#efefef}table.dataTable.display tbody tr:hover.selected>.sorting_1,table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_1{background-color:#a2aec7}table.dataTable.display tbody tr:hover.selected>.sorting_2,table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_2{background-color:#a3b0c9}table.dataTable.display tbody tr:hover.selected>.sorting_3,table.dataTable.order-column.hover tbody tr:hover.selected>.sorting_3{background-color:#a5b2cb}table.dataTable.no-footer{border-bottom:1px solid #111}table.dataTable.nowrap th,table.dataTable.nowrap td{white-space:nowrap}table.dataTable.compact thead th,table.dataTable.compact thead td{padding:4px 17px 4px 4px}table.dataTable.compact tfoot th,table.dataTable.compact tfoot td{padding:4px}table.dataTable.compact tbody th,table.dataTable.compact tbody td{padding:4px}table.dataTable th.dt-left,table.dataTable td.dt-left{text-align:left}table.dataTable th.dt-center,table.dataTable td.dt-center,table.dataTable td.dataTables_empty{text-align:center}table.dataTable th.dt-right,table.dataTable td.dt-right{text-align:right}table.dataTable th.dt-justify,table.dataTable td.dt-justify{text-align:justify}table.dataTable th.dt-nowrap,table.dataTable td.dt-nowrap{white-space:nowrap}table.dataTable thead th.dt-head-left,table.dataTable thead td.dt-head-left,table.dataTable tfoot th.dt-head-left,table.dataTable tfoot td.dt-head-left{text-align:left}table.dataTable thead th.dt-head-center,table.dataTable thead td.dt-head-center,table.dataTable tfoot th.dt-head-center,table.dataTable tfoot td.dt-head-center{text-align:center}table.dataTable thead th.dt-head-right,table.dataTable thead td.dt-head-right,table.dataTable tfoot th.dt-head-right,table.dataTable tfoot td.dt-head-right{text-align:right}table.dataTable thead th.dt-head-justify,table.dataTable thead td.dt-head-justify,table.dataTable tfoot th.dt-head-justify,table.dataTable tfoot td.dt-head-justify{text-align:justify}table.dataTable thead th.dt-head-nowrap,table.dataTable thead td.dt-head-nowrap,table.dataTable tfoot th.dt-head-nowrap,table.dataTable tfoot td.dt-head-nowrap{white-space:nowrap}table.dataTable tbody th.dt-body-left,table.dataTable tbody td.dt-body-left{text-align:left}table.dataTable tbody th.dt-body-center,table.dataTable tbody td.dt-body-center{text-align:center}table.dataTable tbody th.dt-body-right,table.dataTable tbody td.dt-body-right{text-align:right}table.dataTable tbody th.dt-body-justify,table.dataTable tbody td.dt-body-justify{text-align:justify}table.dataTable tbody th.dt-body-nowrap,table.dataTable tbody td.dt-body-nowrap{white-space:nowrap}table.dataTable,table.dataTable th,table.dataTable td{-webkit-box-sizing:content-box;box-sizing:content-box}.dataTables_wrapper{position:relative;clear:both;*zoom:1;zoom:1}.dataTables_wrapper .dataTables_length{float:left}.dataTables_wrapper .dataTables_filter{float:right;text-align:right}.dataTables_wrapper .dataTables_filter input{margin-left:0.5em}.dataTables_wrapper .dataTables_info{clear:both;float:left;padding-top:0.755em}.dataTables_wrapper .dataTables_paginate{float:right;text-align:right;padding-top:0.25em}.dataTables_wrapper .dataTables_paginate .paginate_button{box-sizing:border-box;display:inline-block;min-width:1.5em;padding:0.5em 1em;margin-left:2px;text-align:center;text-decoration:none !important;cursor:pointer;*cursor:hand;color:#333 !important;border:1px solid transparent;border-radius:2px}.dataTables_wrapper .dataTables_paginate .paginate_button.current,.dataTables_wrapper .dataTables_paginate .paginate_button.current:hover{color:#333 !important;border:1px solid #979797;background-color:white;background:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #fff), color-stop(100%, #dcdcdc));background:-webkit-linear-gradient(top, #fff 0%, #dcdcdc 100%);background:-moz-linear-gradient(top, #fff 0%, #dcdcdc 100%);background:-ms-linear-gradient(top, #fff 0%, #dcdcdc 100%);background:-o-linear-gradient(top, #fff 0%, #dcdcdc 100%);background:linear-gradient(to bottom, #fff 0%, #dcdcdc 100%)}.dataTables_wrapper .dataTables_paginate .paginate_button.disabled,.dataTables_wrapper .dataTables_paginate .paginate_button.disabled:hover,.dataTables_wrapper .dataTables_paginate .paginate_button.disabled:active{cursor:default;color:#666 !important;border:1px solid transparent;background:transparent;box-shadow:none}.dataTables_wrapper .dataTables_paginate .paginate_button:hover{color:white !important;border:1px solid #111;background-color:#585858;background:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #585858), color-stop(100%, #111));background:-webkit-linear-gradient(top, #585858 0%, #111 100%);background:-moz-linear-gradient(top, #585858 0%, #111 100%);background:-ms-linear-gradient(top, #585858 0%, #111 100%);background:-o-linear-gradient(top, #585858 0%, #111 100%);background:linear-gradient(to bottom, #585858 0%, #111 100%)}.dataTables_wrapper .dataTables_paginate .paginate_button:active{outline:none;background-color:#2b2b2b;background:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #2b2b2b), color-stop(100%, #0c0c0c));background:-webkit-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:-moz-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:-ms-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:-o-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%);background:linear-gradient(to bottom, #2b2b2b 0%, #0c0c0c 100%);box-shadow:inset 0 0 3px #111}.dataTables_wrapper .dataTables_paginate .ellipsis{padding:0 1em}.dataTables_wrapper .dataTables_processing{position:absolute;top:50%;left:50%;width:100%;height:40px;margin-left:-50%;margin-top:-25px;padding-top:20px;text-align:center;font-size:1.2em;background-color:white;background:-webkit-gradient(linear, left top, right top, color-stop(0%, rgba(255,255,255,0)), color-stop(25%, rgba(255,255,255,0.9)), color-stop(75%, rgba(255,255,255,0.9)), color-stop(100%, rgba(255,255,255,0)));background:-webkit-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);background:-moz-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);background:-ms-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);background:-o-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);background:linear-gradient(to right, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%)}.dataTables_wrapper .dataTables_length,.dataTables_wrapper .dataTables_filter,.dataTables_wrapper .dataTables_info,.dataTables_wrapper .dataTables_processing,.dataTables_wrapper .dataTables_paginate{color:#333}.dataTables_wrapper .dataTables_scroll{clear:both}.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody{*margin-top:-1px;-webkit-overflow-scrolling:touch}.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody th,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody td{vertical-align:middle}.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody th>div.dataTables_sizing,.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody td>div.dataTables_sizing{height:0;overflow:hidden;margin:0 !important;padding:0 !important}.dataTables_wrapper.no-footer .dataTables_scrollBody{border-bottom:1px solid #111}.dataTables_wrapper.no-footer div.dataTables_scrollHead table,.dataTables_wrapper.no-footer div.dataTables_scrollBody table{border-bottom:none}.dataTables_wrapper:after{visibility:hidden;display:block;content:"";clear:both;height:0}@media screen and (max-width: 767px){.dataTables_wrapper .dataTables_info,.dataTables_wrapper .dataTables_paginate{float:none;text-align:center}.dataTables_wrapper .dataTables_paginate{margin-top:0.5em}}@media screen and (max-width: 640px){.dataTables_wrapper .dataTables_length,.dataTables_wrapper .dataTables_filter{float:none;text-align:center}.dataTables_wrapper .dataTables_filter{margin-top:0.5em}} 14 | 15 | 16 | -------------------------------------------------------------------------------- /releases.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "version": "1.0.22", 4 | "downloadUrl": "https://github.com/nystudio107/retour/archive/master.zip", 5 | "date": "2017-10-02T12:00:00-05:00", 6 | "notes": [ 7 | "[Fixed] Fixed an issue in the `Retour_StatsModel`", 8 | "[Fixed] Exact matches are now checked before RegEx matches for static redirects", 9 | "[Improved] Fixed the documentation that was incorrect about being able to add multiple FieldTypes to a single entry", 10 | "[Fixed] Replace 4-byte UTF-8 characters in `Retour_Statistics` before saving them, to avoid db errors" 11 | ] 12 | }, 13 | { 14 | "version": "1.0.21", 15 | "downloadUrl": "https://github.com/nystudio107/retour/archive/master.zip", 16 | "date": "2017-08-31T12:00:00-05:00", 17 | "notes": [ 18 | "[Fixed] Fixed a hardcoded table name in the `referrerUrl` migration" 19 | ] 20 | }, 21 | { 22 | "version": "1.0.20", 23 | "downloadUrl": "https://github.com/nystudio107/retour/archive/master.zip", 24 | "date": "2017-08-30T12:00:00-05:00", 25 | "notes": [ 26 | "[Added] Retour will automatically trim the `retour_stats` table to the last 10,000 redirects, sorted by date (configurable via `statsStoredLimit` in `config.php`)", 27 | "[Added] Add the URL to the stats title attribute, for cases where the display is truncated", 28 | "[Added] Respect `addTrailingSlashesToUrls` in the URLs returned from `getLocalizedUrls()`", 29 | "[Added] Respect `addTrailingSlashesToUrls` in the redirects", 30 | "[Added] Added a `alwaysStripQueryString` setting to `config.php` (defaults to `false`)", 31 | "[Added] Added a `stripQueryStringFromStats` setting to `config.php` (defaults to `true`)", 32 | "[Improved] Updated README.md to note that importing `.htaccess` ignores `RewriteRule`", 33 | "[Improved] Refactored and cleaned up the code", 34 | "[Fixed] Fixed some deprecated meta information in `RetourPlugin.php`", 35 | "[Improved] The `referrerUrl` has a maximum length of 2000 now", 36 | "[Fixed] Fixed some typos in `README.md`" 37 | ] 38 | }, 39 | { 40 | "version": "1.0.19", 41 | "downloadUrl": "https://github.com/nystudio107/retour/archive/master.zip", 42 | "date": "2017-02-10T12:00:00-05:00", 43 | "notes": [ 44 | "[Added] Added a referrer column in the Stats table", 45 | "[Added] Added additional logging in `devMode`", 46 | "[Improved] No more default value for `redirectSrcUrl` column (could cause SQL exceptions in newer versions of MySQL)", 47 | "[Improved] Updated CHANGELOG.md" 48 | ] 49 | }, 50 | { 51 | "version": "1.0.18", 52 | "downloadUrl": "https://github.com/nystudio107/retour/archive/master.zip", 53 | "date": "2017-01-20T12:00:00-05:00", 54 | "notes": [ 55 | "[Fixed] The `addTrailingSlashesToUrls` is now respected for dynamic entry redirects", 56 | "[Improved] Merged pull request 'Fix retourMatch hook'", 57 | "[Added] Added a `statsDisplayLimit` setting to `config.php` to control how many stats should be displayed in the AdminCP", 58 | "[Improved] Merged pull request 'Limit returned results to template'", 59 | "[Improved] Merged pull request 'Allow handling of 404s from { exit } tags encountered while rendering templates'", 60 | "[Added] Added a config.php setting `createUriChangeRedirects` so that the URI-change redirects can be disabled on a per-environment basis", 61 | "[Improved] Don't redirect to the welcome page if we're being installed via Console command", 62 | "[Improved] Moved the changelog to CHANGELOG.md" 63 | ] 64 | }, 65 | { 66 | "version": "1.0.17", 67 | "downloadUrl": "https://github.com/nystudio107/retour/archive/master.zip", 68 | "date": "2016-08-31T12:00:00-05:00", 69 | "notes": [ 70 | "[Improved] Query strings are now stripped from the incoming URI before redirect detection is done", 71 | "[Improved] Updated the README.md" 72 | ] 73 | }, 74 | { 75 | "version": "1.0.16", 76 | "downloadUrl": "https://github.com/nystudio107/retour/archive/master.zip", 77 | "date": "2016-08-30T12:00:00-05:00", 78 | "notes": [ 79 | "[Fixed] FieldTypes in multi-locale setups that are not translatable are now handled properly", 80 | "[Fixed] Fixed missing locale prefix for localized entries in the FieldType", 81 | "[Fixed] Fixed an issue where FieldType redirects had an errant / prepended to them", 82 | "[Improved] Better importing of `.htaccess` files", 83 | "[Improved] Better error handling when importing malformed `.htaccess` files", 84 | "[Fixed] Trailing /'s are no longer stripped from URLs added via the `+` icon from the Statistics page", 85 | "[Fixed] Fixed an issue that would prevent RegEx's from matching as they should in FieldTypes", 86 | "[Improved] Updated the README.md" 87 | ] 88 | }, 89 | { 90 | "version": "1.0.15", 91 | "downloadUrl": "https://github.com/nystudio107/retour/archive/master.zip", 92 | "date": "2016-07-12T12:00:00-05:00", 93 | "notes": [ 94 | "[Added] Added the ability to import the redirects from a `.htaccess` file into Retour", 95 | "[Fixed] Fixed a statics db error with empty referrers", 96 | "[Improved] Updated the README.md" 97 | ] 98 | }, 99 | { 100 | "version": "1.0.14", 101 | "downloadUrl": "https://github.com/nystudio107/retour/archive/master.zip", 102 | "date": "2016-07-10T12:00:00-05:00", 103 | "notes": [ 104 | "[Added] The Statistics and Redirects tables are now dynamically searchable and sortable", 105 | "[Fixed] Fixed an issue that caused redirects created via the `+` from Statistics page to not save", 106 | "[Improved] Updated the README.md" 107 | ] 108 | }, 109 | { 110 | "version": "1.0.13", 111 | "downloadUrl": "https://github.com/nystudio107/retour/archive/master.zip", 112 | "date": "2016-07-06T12:00:00-05:00", 113 | "notes": [ 114 | "[Added] Adds support for locales in the automatic redirect that is created when a slug is changed for an entry", 115 | "[Improved] Retour will no longer let you save a static redirect with an empty destinationURL", 116 | "[Fixed] Fixed a typo in the Retour_StatsModel", 117 | "[Improved] Added a rant about `.htaccess` to the docs", 118 | "[Improved] Updated the README.md" 119 | ] 120 | }, 121 | { 122 | "version": "1.0.12", 123 | "downloadUrl": "https://github.com/nystudio107/retour/archive/master.zip", 124 | "date": "2016-07-04T12:00:00-05:00", 125 | "notes": [ 126 | "[Added] If you hover over a 404 File Not Found URL on the Statistics page, you'll now see the last referrer for the 404 URL", 127 | "[Added] Added a + button on the Statistics page that lets you quickly add a 404'd URL as a redirect", 128 | "[Improved] We now store the destination for redirects in the FieldType as a URI rather than a URL, so that it's more portable across environments", 129 | "[Added] Structure entries that have Retour FieldTypes in them now have the destinationURL updated when the structure elements are moved", 130 | "[Improved] The widget now handles very long URLs more gracefully", 131 | "[Improved] Updated the README.md" 132 | ] 133 | }, 134 | { 135 | "version": "1.0.11", 136 | "downloadUrl": "https://github.com/nystudio107/retour/archive/master.zip", 137 | "date": "2016-06-21T12:00:00-05:00", 138 | "notes": [ 139 | "[Fixed] Fixed an issue with URLs that have umlauts in them", 140 | "[Fixed] Fixed an issue with URLs that are longer than 255 characters for the redirect statistics", 141 | "[Improved] Statistics are now limited to the top 1,000 hits", 142 | "[Improved] Updated the README.md" 143 | ] 144 | }, 145 | { 146 | "version": "1.0.10", 147 | "downloadUrl": "https://github.com/nystudio107/retour/archive/master.zip", 148 | "date": "2016-06-15T12:00:00-05:00", 149 | "notes": [ 150 | "[Added] Retour will attempt to prevent redirect loops when saving a new redirect by deleting any existing redirects that have the destUrl as their srcUrl", 151 | "[Added] Added a 410 - Gone redirect http code for permanently removed resources", 152 | "[Improved] Updated the README.md" 153 | ] 154 | }, 155 | { 156 | "version": "1.0.9", 157 | "downloadUrl": "https://github.com/nystudio107/retour/archive/master.zip", 158 | "date": "2016-06-04T12:00:00-05:00", 159 | "notes": [ 160 | "[Added] Retour will now automatically create a static redirect for you if you rename an entry's slug", 161 | "[Improved] Retour checks to ensure that no two redirects have the same redirectSrcUrl", 162 | "[Improved] The Statistics page handles really long URLs better now", 163 | "[Improved] If you save a redirect, either static or dynamic, with an empty Legacy URL Pattern, retour now deletes it", 164 | "[Improved] Updated the README.md" 165 | ] 166 | }, 167 | { 168 | "version": "1.0.8", 169 | "downloadUrl": "https://github.com/nystudio107/retour/archive/master.zip", 170 | "date": "2016-05-30T12:00:00-05:00", 171 | "notes": [ 172 | "[Improved] Revamped Retour to key off of the ElementID rather than the EntryID", 173 | "[Fixed] Fixed an issue with Retour and MySQL running in strict mode (which is the default in 5.7+)", 174 | "[Fixed] Retour will no longer try to save a record with a null id (caused a CDbCommand exception)", 175 | "[Fixed] A '/' isn't prepended to empty src URLs anymore", 176 | "[Improved] Updated the README.md" 177 | ] 178 | }, 179 | { 180 | "version": "1.0.7", 181 | "downloadUrl": "https://github.com/nystudio107/retour/archive/master.zip", 182 | "date": "2016-05-06T12:00:00-05:00", 183 | "notes": [ 184 | "[Improved] getRequestUri() is now explicitly used, and we immediately terminate the request upon redirect", 185 | "[Improved] We now pass in 0 instead of null for the cache duration", 186 | "[Improved] We now explicitly check for CHttpException", 187 | "[Improved] Updated the README.md" 188 | ] 189 | }, 190 | { 191 | "version": "1.0.6", 192 | "downloadUrl": "https://github.com/nystudio107/retour/archive/master.zip", 193 | "date": "2016-04-29T12:00:00-05:00", 194 | "notes": [ 195 | "[Fixed] Fixed a Javascript error with the FieldType Javascript", 196 | "[Fixed] Fixed a visual display glitch with the tabs on Craft 2.4.x", 197 | "[Improved] Updated the README.md" 198 | ] 199 | }, 200 | { 201 | "version": "1.0.5", 202 | "downloadUrl": "https://github.com/nystudio107/retour/archive/master.zip", 203 | "date": "2016-04-28T12:00:00-05:00", 204 | "notes": [ 205 | "[Added] Added a 'Clear Statistics' button to the Statistics page", 206 | "[Fixed] Fixed a bug when using RegEx for static redirects that would cause them to not work", 207 | "[Fixed] Fixed an issue with Craft 2.4.x", 208 | "[Improved] Updated the README.md" 209 | ] 210 | }, 211 | { 212 | "version": "1.0.4", 213 | "downloadUrl": "https://github.com/nystudio107/retour/archive/master.zip", 214 | "date": "2016-04-28T11:00:00-05:00", 215 | "notes": [ 216 | "[Added] The tables in the Statistics and Redirects pages are now sortable by any column", 217 | "[Improved] Fixed up the localization support for the FieldType", 218 | "[Improved] Minor changes/fixes to the plugin", 219 | "[Improved] Updated the README.md" 220 | ] 221 | }, 222 | { 223 | "version": "1.0.3", 224 | "downloadUrl": "https://github.com/nystudio107/retour/archive/master.zip", 225 | "date": "2016-04-27T20:16:52.434Z", 226 | "notes": [ 227 | "[Added] Added a Retour Stats widget", 228 | "[Added] Added information on the Statistics tab as to whether Retour handled the 404 or not", 229 | "[Improved] Updated the README.md" 230 | ] 231 | }, 232 | { 233 | "version": "1.0.2", 234 | "downloadUrl": "https://github.com/nystudio107/retour/archive/master.zip", 235 | "date": "2016-04-26T20:16:52.434Z", 236 | "notes": [ 237 | "[Fixed] Fixed faulty indexes that could cause Retour Redirect FieldTypes to not work properly", 238 | "[Improved] Spiffy new icon", 239 | "[Improved] Changing the display name of the plugin is now more globally applied", 240 | "[Improved] Updated the README.md" 241 | ] 242 | }, 243 | { 244 | "version": "1.0.1", 245 | "downloadUrl": "https://github.com/nystudio107/retour/archive/master.zip", 246 | "date": "2016-04-26T18:16:52.434Z", 247 | "notes": [ 248 | "[Added] Implemented a caching layer so that once a redirect has been determined, subsequent redirects are cached and immediately returned", 249 | "[Added] Added the ability to delete static redirects", 250 | "[Added] Added Composer support", 251 | "[Improved] Updated the README.md" 252 | ] 253 | }, 254 | { 255 | "version": "1.0.0", 256 | "downloadUrl": "https://github.com/nystudio107/retour/archive/master.zip", 257 | "date": "2016-04-25T18:16:52.434Z", 258 | "notes": [ 259 | "[Added] Initial release" 260 | ] 261 | } 262 | ] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![No Maintenance Intended](http://unmaintained.tech/badge.svg)](http://unmaintained.tech/) 2 | 3 | # DEPRECATED 4 | 5 | This Craft CMS 2.x plugin is no longer supported, but it is fully functional, and you may continue to use it as you see fit. The license also allows you to fork it and make changes as needed for legacy support reasons. 6 | 7 | The Craft CMS 3.x version of this plugin can be found here: [craft-retour](https://github.com/nystudio107/craft-retour) and can also be installed via the Craft Plugin Store in the Craft CP. 8 | 9 | # Retour plugin for Craft CMS 10 | 11 | Retour allows you to intelligently redirect legacy URLs, so that you don't lose SEO value when rebuilding & restructuring a website. 12 | 13 | ![Screenshot](resources/screenshots/retour01.png) 14 | 15 | Related: [Retour for Craft 3.x](https://github.com/nystudio107/craft-retour) 16 | 17 | ## Installation 18 | 19 | To install Retour, follow these steps: 20 | 21 | 1. Download & unzip the file and place the `retour` directory into your `craft/plugins` directory 22 | 2. -OR- do a `git clone https://github.com/nystudio107/retour.git` directly into your `craft/plugins` folder. You can then update it with `git pull` 23 | 3. -OR- install with Composer via `composer require nystudio107/retour` 24 | 4. Install plugin in the Craft Control Panel under Settings > Plugins 25 | 5. The plugin folder should be named `retour` for Craft to see it. GitHub recently started appending `-master` (the branch name) to the name of the folder for zip file downloads. 26 | 27 | Retour works on Craft 2.4.x, Craft 2.5.x, and Craft 2.6.x. 28 | 29 | ## Retour Overview 30 | 31 | Retour allows you to intelligently redirect legacy URLs, so that you don't lose SEO value when rebuilding & restructuring a website. 32 | 33 | In addition to supporting traditional exact and RegEx matching of URL patterns, Retour also has a Retour Redirect FieldType that you can add to your entries. This allows you to have dynamic entry redirects that have access to the data in your entries when matching URL patterns. 34 | 35 | Retour will also automatically create a redirect for you if you change an entry's slug, or move an entry around in a Structure. 36 | 37 | Retour is written to be performant. There is no impact on your website's performance until a 404 exception happens; and even then the resulting matching happens with minimal impact. 38 | 39 | Don't just rebuild a website. Transition it with Retour. 40 | 41 | ## Why Use a Plugin for Redirects? 42 | 43 | If you have just a few static redirects, then your best bet is to put them in your `.htaccess` file, or better yet, in your `.conf` file for your virtual host. However, there are a number of cases where using a plugin to handle it is a **better** solution: 44 | 45 | 1. If you have a large number of redirects, it will slow down every single request your web server handles unnecessarily if they are in `.htaccess` or `.conf` 46 | 2. Often the URL patterns from the legacy website do not match the new website URLs in a deterministic way, which makes creating redirects difficult 47 | 3. Sometimes you don't have access to the server config files, or you want to give your client the ability to manage redirects easily 48 | 49 | Retour solves these problems: 50 | 51 | 1. Retour only attempts to do a redirect after the web server has already thrown a 404 exception. Once a redirect mapping is successfully determined, it also caches the result for speedy resolution of the next redirect request. 52 | 2. Retour also gives you the ability to do Dynamic Entry Redirects that allow you to import a piece of legacy data into your entries to use as a key for determining the new URL mapping. In this way, utterly dissimilar URLs can be mapped for redirection effectively. 53 | 3. It provides an easy to use GUI that the client can use from Craft's AdminCP, and keeps statistics on the 404 hits (and misses) 54 | 55 | ### A Word about .htaccess 56 | 57 | People using the Apache webserver are familiar with the `.htaccess` file, and may even be using it for redirects. It's very likely that you should not be using `.htaccess` at all; instead you should disable `.htaccess` via `AllowOverride none` and make your configuration changes in your webserver configuration files. From [Apache HTTP Server Tutorial: .htaccess files](https://httpd.apache.org/docs/current/howto/htaccess.html) 58 | 59 | There are two main reasons to avoid the use of .htaccess files. 60 | 61 | The first of these is performance. When AllowOverride is set to allow the 62 | use of .htaccess files, httpd will look in every directory for .htaccess 63 | files. Thus, permitting .htaccess files causes a performance hit, whether or 64 | not you actually even use them! Also, the .htaccess file is loaded every 65 | time a document is requested. 66 | 67 | Further note that httpd must look for .htaccess files in all higher-level 68 | directories, in order to have a full complement of directives that it must 69 | apply. (See section on how directives are applied.) Thus, if a file is 70 | requested out of a directory /www/htdocs/example, httpd must look for the 71 | following files: 72 | 73 | /.htaccess 74 | /www/.htaccess 75 | /www/htdocs/.htaccess 76 | /www/htdocs/example/.htaccess 77 | 78 | And so, for each file access out of that directory, there are 4 additional 79 | file-system accesses, even if none of those files are present. (Note that 80 | this would only be the case if .htaccess files were enabled for /, which is 81 | not usually the case.) 82 | 83 | In the case of RewriteRule directives, in .htaccess context these regular 84 | expressions must be re-compiled with every request to the directory, whereas 85 | in main server configuration context they are compiled once and cached. 86 | Additionally, the rules themselves are more complicated, as one must work 87 | around the restrictions that come with per-directory context and 88 | mod_rewrite. Consult the Rewrite Guide for more detail on this subject. 89 | 90 | As you can see, avoiding the use of `.htaccess` completely is best if at all possible, and especially avoid it for `RewriteRule` directives, such as 404 rewrites. 91 | 92 | ## Dynamic Entry Redirects 93 | 94 | Retour implements a Retour Redirect FieldType that you can add to your Entry Types. Retour will look for 404 (Not Found) URLs that match the Legacy URL Pattern, and redirect them to this entry's URL. 95 | 96 | You also get the context of the `entry` that you can use when matching legacy URLs; so if you've imported a field called `recipeid` into your new website, you can the Retour Redirect FieldType look for it in your Legacy URL Pattern, e.g.: `/old-recipes/{recipeid}` 97 | 98 | This allows you to key off of a piece of legacy data that was imported, for the cases when the new URL patterns don't look anything like the Legacy URL Patterns, or follow any pattern that RegEx is useful for matching. 99 | 100 | ![Screenshot](resources/screenshots/retour02.png) 101 | 102 | ### Creating a Retour Redirect Field 103 | 104 | Create a Retour Redirect field as you would any other field; then set the default values for it. For new entries, it will default to the values entered here, so you can put your matching pattern in once, rather than having to do it for each entry. 105 | 106 | * **Default Legacy URL Pattern** - Enter the URL pattern that Retour should match. This matches against the path, the part of the URL after the domain name. You can include tags that output entry properties, such as `{title}` or `{myCustomField}` in the text field below. e.g.: Exact Match: `/recipes/{recipeid}` or RegEx Match: `.*RecipeID={recipeid}$` where `{recipeid}` is a field handle to a field in this entry. 107 | * **Default Pattern Match Type** - What type of matching should be done with the Legacy URL Pattern. Details on RegEx matching can be found at [regexr.com](http://regexr.com). If a plugin provides a custom matching function, you can select it here. 108 | * **Default Redirect Type** - Select whether the redirect should be permanent or temporary. 109 | * **Redirect Changeable** - Whether to allow the user to change the redirect while editing the entry. 110 | 111 | ### Configuring a Retour Redirect Field 112 | 113 | 114 | * **Legacy URL Pattern** - Enter the URL pattern that Retour should match. This matches against the path, the part of the URL after the domain name. You can include tags that output entry properties, such as `{title}` or `{myCustomField}` in the text field below. e.g.: Exact Match: `/recipes/{recipeid}` or RegEx Match: `.*RecipeID={recipeid}$` where `{recipeid}` is a field handle to a field in this entry. 115 | * **Pattern Match Type** - What type of matching should be done with the Legacy URL Pattern. Details on RegEx matching can be found at [regexr.com](http://regexr.com). If a plugin provides a custom matching function, you can select it here. 116 | * **Redirect Type** - Select whether the redirect should be permanent or temporary. 117 | 118 | **Note:** if you add a Retour Redirect FieldType to an existing Section, or you import data from a foreign source into a Section with a Retour Redirect FieldType, the default values you set for the Retour Redirect FieldType will not be propagated to the entry yet. To cause that to happen, go to **Settings->Sections** then click on the Section to edit it, and hit **Save**. This will cause all of the entries in that section to be re-saved, and Retour will fill in the default field values. 119 | 120 | ## Static Redirects 121 | 122 | ### Manually Creating Static Redirects 123 | 124 | Static Redirects are useful when the Legacy URL Patterns and the new URL patterns are deterministic. You can create them by clicking on **Retour->Redirects** and then clicking on the **+ New Static Redirect** button. 125 | 126 | * **Legacy URL Pattern** - Enter the URL pattern that Retour should match. This matches against the path, the part of the URL after the domain name. e.g.: Exact Match: `/recipes/` or RegEx Match: `.*RecipeID=(.*)` 127 | * **Destination URL** - Enter the destination URL that should be redirected to. This can either be a fully qualified URL or a relative URL. e.g.: Exact Match: `/new-recipes/` or RegEx Match: `/new-recipes/$1` 128 | * **Pattern Match Type** - What type of matching should be done with the Legacy URL Pattern. Details on RegEx matching can be found at [regexr.com](http://regexr.com). If a plugin provides a custom matching function, you can select it here. 129 | * **Redirect Type** - Select whether the redirect should be permanent or temporary. 130 | 131 | ### Importing an Existing .htaccess file 132 | 133 | Retour also allows you to import an existing `.htaccess` file and all of the redirects it contains into Retour by clicking on **Retour->Redirects** and then clicking on the **Import .htaccess File** button. 134 | 135 | It will import redirects from `Redirect` and `RedirectMatch` directives in the file. It will **ignore** `RewriteRule`s because they don't necessarily have a 1:1 mapping, you can have several `RewriteRule`s that are strung together to figure out the final redirect. 136 | 137 | It asks your browser to look for only `text` files to upload; if the `.htaccess` file you have isn't a `.txt` file, you can force it to allow you to upload it by choosing **Format: All Files**. 138 | 139 | ### Renamed Slug Redirects 140 | 141 | If you rename an entry's `slug` (and the Section the entry is in has URLs), Retour will automatically create a static redirect for you to keep traffic going to the right place. It will also automatically create a static redirect if you move an entry around in a Structure. 142 | 143 | It will appear listed under the "Static Redirects" section like any other static redirect. 144 | 145 | ## Retour Statistics 146 | 147 | Retour keeps track of every 404 your website receives. You can view them by clicking on **Retour->Statistics**. 148 | 149 | Only one record is saved per URL Pattern, so the database won't get clogged with a ton of records. 150 | 151 | ### Retour Widget 152 | 153 | If you'd like to see an overview of the Retour Statistics in your dashboard, you can add a Retour widget to your Dashboard: 154 | 155 | ![Screenshot](resources/screenshots/retour03.png) 156 | 157 | It displays the total number of handled and not handled 404s, and the 5 most recent 404 URLs in each category right in your dashboard. 158 | 159 | ## Developer Info 160 | 161 | ### Custom Match Functions via Plugin 162 | 163 | Retour allows you to implement a custom matching function via plugin, if the Exact and RegEx matching are not sufficient for your purposes. 164 | 165 | In your main plugin class file, simply add this function: 166 | 167 | /** 168 | * retourMatch gives your plugin a chance to use whatever custom logic is needed for URL redirection. You are passed 169 | * in an array that contains the details of the redirect. Do whatever matching logic, then return true if is a 170 | * matched, false if it is not. 171 | * 172 | * You can alter the 'redirectDestUrl' to change what URL they should be redirected to, as well as the 'redirectHttpCode' 173 | * to change the type of redirect. None of the changes made are saved in the database. 174 | * 175 | * @param mixed An array of arguments that define the redirect 176 | * $args = array( 177 | * 'redirect' => array( 178 | * 'id' => the id of the redirect record in the retour_redirects table 179 | * 'associatedElementId' => the id of the entry if this is a Dynamic Entry Redirect; 0 otherwise 180 | * 'redirectSrcUrl' => the legacy URL as entered by the user 181 | * 'redirectSrcUrlParsed' => the redirectSrcUrl after it has been parsed as a micro template for {variables} 182 | * via renderObjectTemplate(). This is typically what you would want to match against. 183 | * 'redirectMatchType' => the type of match; this will be set to your plugin's ClassHandle 184 | * 'redirectDestUrl' => the destination URL for the entry this redirect is associated with, or the 185 | * destination URL that was manually entered by the user 186 | * 'redirectHttpCode' => the redirect HTTP code (typically 301 or 302) 187 | * 'hitCount' => the number of times this redirect has been matched, and the redirect done in the browser 188 | * 'hitLastTime' => the date and time of the when this redirect was matched 189 | * 'locale' => the locale of this redirect 190 | * ) 191 | * ); 192 | * @return bool Return true if it's a match, false otherwise 193 | */ 194 | public function retourMatch($args) 195 | { 196 | return true; 197 | } 198 | 199 | Your plugin will then appear in the list of Pattern Match Types that can be chosen from via Retour->Redirects or via the Retour Redirect FieldType. 200 | 201 | ### Utility Functions 202 | 203 | `craft.retour.getHttpStatus` in your templates will return the HTTP Status code for the current template, so you can display a special message for people who end up on a page via a `301` or `302` redirect. 204 | 205 | ## Retour Roadmap 206 | 207 | Some things to do, and ideas for potential features: 208 | 209 | * Craft 3 compatibility 210 | * Add the ability to mass-import redirects from a CSV file 211 | 212 | Brought to you by [nystudio107](http://nystudio107.com) 213 | -------------------------------------------------------------------------------- /services/RetourService.php: -------------------------------------------------------------------------------- 1 | cachedStatistics)) { 31 | return $this->cachedStatistics; 32 | } 33 | 34 | $result = craft()->db->createCommand() 35 | ->select('*') 36 | ->from('retour_stats') 37 | ->order('hitCount DESC') 38 | ->limit(craft()->config->get("statsDisplayLimit", "retour")) 39 | ->queryAll(); 40 | 41 | $this->cachedStatistics = $result; 42 | 43 | return $result; 44 | } 45 | 46 | /** 47 | * @param int $days The number of days to get 48 | * @param int $handled 49 | * 50 | * @return array Recent statistics 51 | */ 52 | public function getRecentStatistics($days = 1, $handled = 0) 53 | { 54 | 55 | $handled = (int)$handled; 56 | 57 | if (!$handled) { 58 | $handled = 0; 59 | } 60 | $result = craft()->db->createCommand() 61 | ->select('*') 62 | ->from('retour_stats') 63 | ->where("hitLastTime >= ( CURDATE() - INTERVAL '$days' DAY )") 64 | ->andWhere('handledByRetour =' . $handled) 65 | ->order('hitLastTime DESC') 66 | ->queryAll(); 67 | 68 | return $result; 69 | } 70 | 71 | /** 72 | */ 73 | public function clearStatistics() 74 | { 75 | $result = craft()->db->createCommand() 76 | ->truncateTable('retour_stats'); 77 | 78 | return $result; 79 | } 80 | 81 | /** 82 | * @param string $url the url to match 83 | * 84 | * @return mixed the redirect array 85 | */ 86 | public function findRedirectMatch($url) 87 | { 88 | $result = null; 89 | 90 | // Check the cache first 91 | $redirect = $this->getRedirectFromCache($url); 92 | if ($redirect) { 93 | $error = $this->incrementRedirectHitCount($redirect); 94 | $this->saveRedirectToCache($url, $redirect); 95 | RetourPlugin::log("[cached] " . $redirect['redirectMatchType'] . " result: " . print_r($error, true), LogLevel::Info, false); 96 | 97 | return $redirect; 98 | } 99 | 100 | // Look up the entry redirects first 101 | $redirects = null; 102 | $redirects = $this->getAllEntryRedirects(); 103 | $result = $this->lookupRedirect($url, $redirects); 104 | if ($result) { 105 | return $result; 106 | } 107 | 108 | // Look up the static redirects next 109 | $redirects = null; 110 | $redirects = $this->getAllStaticRedirects(); 111 | $result = $this->lookupRedirect($url, $redirects); 112 | if ($result) { 113 | return $result; 114 | } 115 | 116 | return $result; 117 | } 118 | 119 | /** 120 | * @param $url 121 | * 122 | * @return mixed The redirect 123 | */ 124 | public function getRedirectFromCache($url) 125 | { 126 | $cacheKey = "retour_cache_" . md5($url); 127 | $result = craft()->cache->get($cacheKey); 128 | RetourPlugin::log("Cached Redirect hit: " . print_r($result, true), LogLevel::Info, false); 129 | 130 | return $result; 131 | } 132 | 133 | /** 134 | * @param Retour_RedirectsModel The redirect to create 135 | */ 136 | public function incrementRedirectHitCount(&$redirect) 137 | { 138 | if (isset($redirect)) { 139 | $redirect['hitCount'] = $redirect['hitCount'] + 1; 140 | $redirect['hitLastTime'] = DateTimeHelper::currentTimeForDb(); 141 | 142 | if ($redirect['associatedElementId']) { 143 | $table = 'retour_redirects'; 144 | } else { 145 | $table = 'retour_static_redirects'; 146 | } 147 | $result = craft()->db->createCommand() 148 | ->update($table, array( 149 | 'hitCount' => $redirect['hitCount'], 150 | 'hitLastTime' => $redirect['hitLastTime'], 151 | ), 'id=:id', array(':id' => $redirect['id'])); 152 | } 153 | } 154 | 155 | /** 156 | * @param string $url The input URL 157 | * @param mixed $redirect The redirect 158 | */ 159 | public function saveRedirectToCache($url, $redirect) 160 | { 161 | $cacheKey = "retour_cache_" . md5($url); 162 | $error = craft()->cache->set($cacheKey, $redirect, 0); 163 | RetourPlugin::log("Cached Redirect saved: " . print_r($error, true), LogLevel::Info, false); 164 | } 165 | 166 | /** 167 | * @return Array All of the entry redirects 168 | */ 169 | public function getAllEntryRedirects($limit = null) 170 | { 171 | 172 | // Cache it in our class; no need to fetch it more than once 173 | if (isset($this->cachedEntryRedirects)) { 174 | return $this->cachedEntryRedirects; 175 | } 176 | 177 | $result = craft()->db->createCommand() 178 | ->select('*') 179 | ->from('retour_redirects') 180 | ->order('hitCount DESC'); 181 | 182 | if ($limit) { 183 | $result = $result->limit($limit); 184 | } else { 185 | $this->cachedEntryRedirects = $result; 186 | } 187 | 188 | return $result->queryAll(); 189 | } 190 | 191 | /** 192 | * @param string $url the url to match 193 | * @param mixed $redirects an array of redirects to look through 194 | * 195 | * @return mixed the redirect array 196 | */ 197 | public function lookupRedirect($url, $redirects) 198 | { 199 | $result = null; 200 | foreach ($redirects as $redirect) { 201 | $redirectMatchType = isset($redirect['redirectMatchType']) ? $redirect['redirectMatchType'] : null; 202 | switch ($redirectMatchType) { 203 | // Do a straight up match 204 | case "exactmatch": 205 | if (strcasecmp($redirect['redirectSrcUrlParsed'], $url) === 0) { 206 | $error = $this->incrementRedirectHitCount($redirect); 207 | RetourPlugin::log($redirectMatchType . " result: " . print_r($error, true), LogLevel::Info, false); 208 | $this->saveRedirectToCache($url, $redirect); 209 | 210 | return $redirect; 211 | } 212 | break; 213 | 214 | // Do a regex match 215 | case "regexmatch": 216 | $matchRegEx = "`" . $redirect['redirectSrcUrlParsed'] . "`i"; 217 | if (preg_match($matchRegEx, $url) === 1) { 218 | $error = $this->incrementRedirectHitCount($redirect); 219 | RetourPlugin::log($redirectMatchType . " result: " . print_r($error, true), LogLevel::Info, false); 220 | 221 | // If we're not associated with an EntryID, handle capture group replacement 222 | if ($redirect['associatedElementId'] == 0) { 223 | $redirect['redirectDestUrl'] = preg_replace($matchRegEx, $redirect['redirectDestUrl'], $url); 224 | } 225 | $this->saveRedirectToCache($url, $redirect); 226 | 227 | return $redirect; 228 | } 229 | break; 230 | 231 | // Otherwise try to look up a plugin's method by and call it for the match 232 | default: 233 | $plugin = $redirectMatchType ? craft()->plugins->getPlugin($redirectMatchType) : null; 234 | if ($plugin) { 235 | if (method_exists($plugin, "retourMatch")) { 236 | $args = array( 237 | array( 238 | 'redirect' => &$redirect, 239 | ), 240 | ); 241 | $result = call_user_func_array(array($plugin, "retourMatch"), $args); 242 | if ($result) { 243 | $error = $this->incrementRedirectHitCount($redirect); 244 | RetourPlugin::log($redirectMatchType . " result: " . print_r($error, true), LogLevel::Info, false); 245 | $this->saveRedirectToCache($url, $redirect); 246 | 247 | return $redirect; 248 | } 249 | } 250 | } 251 | break; 252 | } 253 | } 254 | RetourPlugin::log("Not handled: " . $url, LogLevel::Info, false); 255 | 256 | return $result; 257 | } 258 | 259 | /** 260 | * @return array All of the static redirects 261 | */ 262 | public function getAllStaticRedirects($limit = null) 263 | { 264 | 265 | // Cache it in our class; no need to fetch it more than once 266 | if (isset($this->cachedStaticRedirects)) { 267 | return $this->cachedStaticRedirects; 268 | } 269 | 270 | $result = craft()->db->createCommand() 271 | ->select('*') 272 | ->from('retour_static_redirects') 273 | ->order('redirectMatchType ASC, hitCount DESC'); 274 | 275 | if ($limit) { 276 | $result = $result->limit($limit); 277 | } else { 278 | $this->cachedStaticRedirects = $result; 279 | } 280 | 281 | return $result->queryAll(); 282 | } 283 | 284 | /** 285 | * @param $record 286 | * 287 | * @return bool|int|string 288 | */ 289 | public function saveStaticRedirect($record) 290 | { 291 | $error = ""; 292 | 293 | if (isset($record)) { 294 | if (($record->redirectSrcUrl == "") || ($record->redirectDestUrl == "")) { 295 | $id = $record->id; 296 | $affectedRows = craft()->db->createCommand()->delete('retour_static_redirects', array( 297 | 'id' => $id, 298 | )); 299 | 300 | RetourPlugin::log("Deleted Redirected: " . $id, LogLevel::Info, false); 301 | $error = craft()->cache->flush(); 302 | RetourPlugin::log("Cache flushed: " . print_r($error, true), LogLevel::Info, false); 303 | $error = -1; 304 | } else { 305 | if ($record->save()) { 306 | $error = craft()->cache->flush(); 307 | RetourPlugin::log("Cache flushed: " . print_r($error, true), LogLevel::Info, false); 308 | craft()->userSession->setNotice(Craft::t('Retour Redirect saved.')); 309 | $error = ""; 310 | 311 | // To prevent redirect loops, see if any static redirects have our destUrl as their srcUrl 312 | $redir = $this->getRedirectByRedirectSrcUrl($record->redirectDestUrl, $record->locale); 313 | if ($redir) { 314 | $id = $redir->id; 315 | $affectedRows = craft()->db->createCommand()->delete('retour_static_redirects', array( 316 | 'id' => $id, 317 | )); 318 | } 319 | } else { 320 | $error = $record->getErrors(); 321 | RetourPlugin::log(print_r($error, true), LogLevel::Info, false); 322 | craft()->userSession->setError(Craft::t('Couldn’t save Retour Redirect.')); 323 | } 324 | } 325 | } 326 | 327 | return $error; 328 | } 329 | 330 | /** 331 | * @param string $srcUrl the redirect's redirectSrcUrl 332 | * @param string $locale The locale 333 | * 334 | * @return Mixed The resulting Redirect 335 | */ 336 | public function getRedirectByRedirectSrcUrl($srcUrl, $locale) 337 | { 338 | $result = Retour_RedirectsRecord::model()->findByAttributes(array('redirectSrcUrlParsed' => $srcUrl, 'locale' => $locale)); 339 | 340 | return $result; 341 | } 342 | 343 | /** 344 | * @param $url The 404 url 345 | * @param bool $handled 346 | */ 347 | public function incrementStatistics($url, $handled = false) 348 | { 349 | 350 | $handled = (int)$handled; 351 | $url = substr($url, 0, 255); 352 | $referrer = craft()->request->getUrlReferrer(); 353 | if (is_null($referrer)) { 354 | $referrer = ""; 355 | } 356 | 357 | // Strip the query string if `stripQueryStringFromStats` is set 358 | if (craft()->config->get("stripQueryStringFromStats", "retour")) { 359 | $url = UrlHelper::stripQueryString($url); 360 | } 361 | 362 | // Make sure the referrerUrl does not exceed the max length of its table column. 363 | $attrConfigs = Retour_RedirectsRecord::model()->getAttributeConfigs(); 364 | $maxLength = isset($attrConfigs['referrerUrl']['maxLength']) ? $attrConfigs['referrerUrl']['maxLength'] : 255; 365 | $trimMarker = '...'; 366 | $referrer = mb_strimwidth($referrer, 0, ($maxLength-strlen($trimMarker)), $trimMarker, craft()->charset); 367 | 368 | // See if a stats record exists already 369 | $result = craft()->db->createCommand() 370 | ->select('*') 371 | ->from('retour_stats') 372 | ->where('redirectSrcUrl =' . craft()->db->quoteValue($url)) 373 | ->queryAll(); 374 | 375 | if (empty($result)) { 376 | $stats = new Retour_StatsRecord; 377 | $stats->redirectSrcUrl = StringHelper::encodeMb4($url); 378 | $stats->referrerUrl = StringHelper::encodeMb4($referrer); 379 | $stats->hitCount = 1; 380 | $stats->hitLastTime = DateTimeHelper::currentUTCDateTime(); 381 | $stats->handledByRetour = $handled; 382 | $stats->save(); 383 | } else { 384 | // Update the stats table 385 | foreach ($result as $stat) { 386 | $stat['hitCount'] = $stat['hitCount'] + 1; 387 | $stat['hitLastTime'] = DateTimeHelper::currentTimeForDb(); 388 | $stat['referrerUrl'] = $referrer; 389 | 390 | $result = craft()->db->createCommand() 391 | ->update('retour_stats', array( 392 | 'hitCount' => $stat['hitCount'], 393 | 'hitLastTime' => $stat['hitLastTime'], 394 | 'handledByRetour' => $handled, 395 | 'referrerUrl' => $stat['referrerUrl'], 396 | ), 'id=:id', array(':id' => $stat['id'])); 397 | } 398 | } 399 | 400 | // After incrementing a statistic, trim the retour_stats db table 401 | $this->trimStatistics(); 402 | } 403 | 404 | /** 405 | * Trim the retour_stats db table based on the statsStoredLimit config.php 406 | * setting 407 | * 408 | * @return void 409 | */ 410 | public function trimStatistics() 411 | { 412 | $affectedRows = 0; 413 | $table = craft()->db->addTablePrefix('retour_stats'); 414 | $quotedTable = craft()->db->quoteTableName($table); 415 | $limit = craft()->config->get("statsStoredLimit", "retour"); 416 | 417 | // As per https://stackoverflow.com/questions/578867/sql-query-delete-all-records-from-the-table-except-latest-n 418 | if (!empty($limit) && $limit) { 419 | $affectedRows = craft()->db->createCommand(" 420 | DELETE FROM $quotedTable 421 | WHERE id NOT IN ( 422 | SELECT id 423 | FROM ( 424 | SELECT id 425 | FROM $quotedTable 426 | ORDER BY hitLastTime DESC 427 | LIMIT $limit 428 | ) foo 429 | ) 430 | ")->execute(); 431 | RetourPlugin::log("Trimmed " . $affectedRows . " from retour_stats table", LogLevel::Info, false); 432 | } 433 | } 434 | 435 | /** 436 | * @param int $id The redirect's id 437 | * 438 | * @return Mixed The resulting Redirect 439 | */ 440 | public function getRedirectById($id) 441 | { 442 | $result = Retour_StaticRedirectsRecord::model()->findByAttributes(array('id' => $id)); 443 | 444 | return $result; 445 | } 446 | 447 | /** 448 | * @param Retour_RedirectsModel The redirect to save 449 | */ 450 | public function saveRedirect($redirectsModel) 451 | { 452 | if (isset($redirectsModel)) { 453 | $result = $this->getRedirectByElementId($redirectsModel->associatedElementId, $redirectsModel->locale); 454 | if ($result) { 455 | $result->setAttributes($redirectsModel->getAttributes(), false); 456 | $error = $result->save(); 457 | } else { 458 | $error = $this->createRedirect($redirectsModel); 459 | } 460 | RetourPlugin::log(print_r($error, true), LogLevel::Info, false); 461 | } 462 | } 463 | 464 | /** 465 | * @param int $elementId The associated elementId 466 | * @param string $locale The locale 467 | * 468 | * @return Mixed The resulting Redirect 469 | */ 470 | public function getRedirectByElementId($elementId, $locale) 471 | { 472 | $result = Retour_RedirectsRecord::model()->findByAttributes(array('associatedElementId' => $elementId, 'locale' => $locale)); 473 | 474 | return $result; 475 | } 476 | 477 | /** 478 | * @param Retour_RedirectsModel The redirect to create 479 | */ 480 | public function createRedirect($redirectsModel) 481 | { 482 | if (isset($redirectsModel)) { 483 | // Don't try to create a redirect if one already exists for the redirectSrcUrlParsed, or if empty 484 | if ($redirectsModel->redirectSrcUrlParsed && !$this->getRedirectByRedirectSrcUrl($redirectsModel->redirectSrcUrlParsed, $redirectsModel->locale)) { 485 | $result = new Retour_RedirectsRecord; 486 | $result->setAttributes($redirectsModel->getAttributes(), false); 487 | $result->save(); 488 | $error = $result->getErrors(); 489 | RetourPlugin::log(print_r($error, true), LogLevel::Info, false); 490 | } 491 | } 492 | } 493 | 494 | /** 495 | * @param int $elementId The associated elementId 496 | * @param string $locale The locale 497 | */ 498 | public function deleteRedirectByElementId($elementId, $locale) 499 | { 500 | $result = $this->getRedirectByElementId($elementId, $locale); 501 | if ($result) { 502 | $result->delete(); 503 | } 504 | } 505 | 506 | /** 507 | * Returns a list of localized URIs for the passed in element 508 | * 509 | * @param null $element 510 | * 511 | * @return array an array of paths 512 | */ 513 | public function getLocalizedUris($element = null) 514 | { 515 | $localizedUris = array(); 516 | if ($element) { 517 | if (craft()->isLocalized()) { 518 | $unsortedLocalizedUris = array(); 519 | $_rows = craft()->db->createCommand() 520 | ->select('locale') 521 | ->addSelect('uri') 522 | ->from('elements_i18n') 523 | ->where(array('elementId' => $element->id, 'enabled' => 1)) 524 | ->queryAll(); 525 | 526 | foreach ($_rows as $row) { 527 | $path = ($row['uri'] == '__home__') ? '' : $row['uri']; 528 | $url = UrlHelper::getSiteUrl($path, null, null, $row['locale']); 529 | if (craft()->config->get('addTrailingSlashesToUrls')) { 530 | $url = rtrim($url, '/') . '/'; 531 | } 532 | $unsortedLocalizedUrls[$row['locale']] = $url; 533 | } 534 | 535 | $locales = craft()->i18n->getSiteLocales(); 536 | foreach ($locales as $locale) { 537 | $localeId = $locale->getId(); 538 | if (isset($unsortedLocalizedUris[$localeId])) { 539 | $urlParts = parse_url($unsortedLocalizedUris[$localeId]); 540 | 541 | array_push($localizedUris, "/" . $urlParts['path']); 542 | } 543 | } 544 | } else { 545 | array_push($localizedUris, "/" . $element->uri); 546 | } 547 | } 548 | 549 | return $localizedUris; 550 | } 551 | 552 | /** 553 | * @return string The name of the plugin 554 | */ 555 | public function getPluginName() 556 | { 557 | $retourPlugin = craft()->plugins->getPlugin('retour'); 558 | $result = $retourPlugin->getName(); 559 | 560 | return $result; 561 | } 562 | 563 | /** 564 | * @return mixed Returns the list of matching schemes 565 | */ 566 | public function getMatchesList() 567 | { 568 | $result = array( 569 | 'exactmatch' => Craft::t('Exact Match'), 570 | 'regexmatch' => Craft::t('RegEx Match'), 571 | ); 572 | 573 | // Add any plugins that offer the retourMatch() method 574 | foreach (craft()->plugins->getPlugins() as $plugin) { 575 | if (method_exists($plugin, "retourMatch")) { 576 | $result[$plugin->getClassHandle()] = $plugin->getName() . Craft::t(" Match"); 577 | } 578 | } 579 | 580 | return $result; 581 | } 582 | } 583 | --------------------------------------------------------------------------------