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.
{{ "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}}
{{ "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}}
{{ "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}}
{{ "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}}
";
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 | [](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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------