├── src
├── web
│ └── assets
│ │ └── dist
│ │ ├── assets
│ │ ├── retour-DnaB684y.js
│ │ ├── retour-DnaB684y.js.map
│ │ ├── import-Cap4Ox0Y.js.gz
│ │ ├── widget-BQWJDBDA.js.gz
│ │ ├── dashboard-DVAYjtqJ.js.gz
│ │ ├── purify.es-DP_WP6YH.js.gz
│ │ ├── redirects-B6OCJnQt.js.gz
│ │ ├── retour-exeLeENM.css.gz
│ │ ├── import-Cap4Ox0Y.js.map.gz
│ │ ├── shortlinks-Jev4OoWa.js.gz
│ │ ├── widget-BQWJDBDA.js.map.gz
│ │ ├── LegacyUrl-CeiszSHN.js.map.gz
│ │ ├── dashboard-DVAYjtqJ.js.map.gz
│ │ ├── purify.es-DP_WP6YH.js.map.gz
│ │ ├── redirects-B6OCJnQt.js.map.gz
│ │ ├── shortlinks-Jev4OoWa.js.map.gz
│ │ ├── vue-apexcharts-BHFWl5EO.js.gz
│ │ ├── _plugin-vue2_normalizer-BFq-tfv7.js.map
│ │ ├── vue-apexcharts-BHFWl5EO.js.map.gz
│ │ ├── LegacyUrl-CeiszSHN.js
│ │ ├── purify-DvoaRXnC.css
│ │ ├── _plugin-vue2_normalizer-BFq-tfv7.js
│ │ ├── widget-BQWJDBDA.js
│ │ ├── LegacyUrl-CeiszSHN.js.map
│ │ ├── widget-BQWJDBDA.js.map
│ │ ├── retour-exeLeENM.css
│ │ └── shortlinks-Jev4OoWa.js
│ │ ├── manifest.json.gz
│ │ ├── img
│ │ └── icon-mask.svg
│ │ └── manifest.json
├── templates
│ ├── _layouts
│ │ ├── widget-cp.twig
│ │ └── retour-cp.twig
│ ├── _includes
│ │ ├── macros.twig
│ │ └── sites-menu.twig
│ ├── _components
│ │ ├── widgets
│ │ │ ├── Retour_settings.twig
│ │ │ └── Retour_body.twig
│ │ └── fields
│ │ │ ├── ShortLink_settings.twig
│ │ │ ├── ShortLink_input.twig
│ │ │ └── ShortLink_preview.twig
│ ├── index.twig
│ ├── import
│ │ ├── errors.twig
│ │ └── index.twig
│ ├── settings
│ │ ├── index.twig
│ │ └── _includes
│ │ │ ├── general.twig
│ │ │ ├── statistics.twig
│ │ │ └── advanced.twig
│ ├── shortlinks
│ │ └── index.twig
│ ├── redirects
│ │ └── index.twig
│ └── dashboard
│ │ └── index.twig
├── helpers
│ ├── Gql.php
│ ├── Permission.php
│ ├── Version.php
│ ├── FileLog.php
│ ├── Text.php
│ ├── MultiSite.php
│ └── UrlHelper.php
├── migrations
│ ├── m190416_212500_widget_type_update.php
│ ├── m221130_224500_add_priorities_column.php
│ ├── m181013_122446_add_remote_ip.php
│ ├── m200109_144310_add_redirectSrcUrl_index.php
│ ├── m230627_141429_add_redirectMatchType_index.php
│ ├── m181018_135656_add_redirect_status.php
│ ├── m181013_202455_add_redirect_src_match.php
│ ├── m210603_221000_add_gql_schema_components.php
│ ├── m181216_043222_rebuild_indexes.php
│ ├── m181018_123901_add_stats_info.php
│ ├── m181013_171315_truncate_match_type.php
│ └── m181213_233502_add_site_id.php
├── events
│ ├── RedirectResolvedEvent.php
│ ├── RedirectEvent.php
│ └── ResolveRedirectEvent.php
├── icon-mask.svg
├── assetbundles
│ └── retour
│ │ ├── RetourAsset.php
│ │ ├── RetourImportAsset.php
│ │ ├── RetourDashboardAsset.php
│ │ ├── RetourRedirectsAsset.php
│ │ └── RetourWidgetAsset.php
├── gql
│ ├── arguments
│ │ └── RetourArguments.php
│ ├── types
│ │ ├── RetourType.php
│ │ └── generators
│ │ │ └── RetourGenerator.php
│ ├── queries
│ │ └── RetourQuery.php
│ ├── resolvers
│ │ └── RetourResolver.php
│ └── interfaces
│ │ └── RetourInterface.php
├── validators
│ ├── DbStringValidator.php
│ ├── ParsedUriValidator.php
│ └── UriValidator.php
├── models
│ ├── DbModel.php
│ ├── Stats.php
│ ├── Redirects.php
│ ├── StaticRedirects.php
│ └── Settings.php
├── controllers
│ ├── ApiController.php
│ ├── SettingsController.php
│ ├── ChartsController.php
│ └── StatisticsController.php
├── variables
│ └── RetourVariable.php
├── console
│ └── controllers
│ │ └── StatsController.php
├── config.php
├── services
│ └── ServicesTrait.php
├── widgets
│ └── RetourWidget.php
└── fields
│ └── ShortLink.php
├── phpstan.neon
├── ecs.php
├── Makefile
├── composer.json
├── LICENSE.md
└── README.md
/src/web/assets/dist/assets/retour-DnaB684y.js:
--------------------------------------------------------------------------------
1 |
2 | //# sourceMappingURL=retour-DnaB684y.js.map
3 |
--------------------------------------------------------------------------------
/src/web/assets/dist/manifest.json.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-retour/develop-v5/src/web/assets/dist/manifest.json.gz
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/retour-DnaB684y.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"retour-DnaB684y.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/import-Cap4Ox0Y.js.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-retour/develop-v5/src/web/assets/dist/assets/import-Cap4Ox0Y.js.gz
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/widget-BQWJDBDA.js.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-retour/develop-v5/src/web/assets/dist/assets/widget-BQWJDBDA.js.gz
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/dashboard-DVAYjtqJ.js.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-retour/develop-v5/src/web/assets/dist/assets/dashboard-DVAYjtqJ.js.gz
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/purify.es-DP_WP6YH.js.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-retour/develop-v5/src/web/assets/dist/assets/purify.es-DP_WP6YH.js.gz
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/redirects-B6OCJnQt.js.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-retour/develop-v5/src/web/assets/dist/assets/redirects-B6OCJnQt.js.gz
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/retour-exeLeENM.css.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-retour/develop-v5/src/web/assets/dist/assets/retour-exeLeENM.css.gz
--------------------------------------------------------------------------------
/phpstan.neon:
--------------------------------------------------------------------------------
1 | includes:
2 | - %currentWorkingDirectory%/vendor/craftcms/phpstan/phpstan.neon
3 |
4 | parameters:
5 | level: 5
6 | paths:
7 | - src
8 |
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/import-Cap4Ox0Y.js.map.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-retour/develop-v5/src/web/assets/dist/assets/import-Cap4Ox0Y.js.map.gz
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/shortlinks-Jev4OoWa.js.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-retour/develop-v5/src/web/assets/dist/assets/shortlinks-Jev4OoWa.js.gz
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/widget-BQWJDBDA.js.map.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-retour/develop-v5/src/web/assets/dist/assets/widget-BQWJDBDA.js.map.gz
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/LegacyUrl-CeiszSHN.js.map.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-retour/develop-v5/src/web/assets/dist/assets/LegacyUrl-CeiszSHN.js.map.gz
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/dashboard-DVAYjtqJ.js.map.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-retour/develop-v5/src/web/assets/dist/assets/dashboard-DVAYjtqJ.js.map.gz
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/purify.es-DP_WP6YH.js.map.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-retour/develop-v5/src/web/assets/dist/assets/purify.es-DP_WP6YH.js.map.gz
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/redirects-B6OCJnQt.js.map.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-retour/develop-v5/src/web/assets/dist/assets/redirects-B6OCJnQt.js.map.gz
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/shortlinks-Jev4OoWa.js.map.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-retour/develop-v5/src/web/assets/dist/assets/shortlinks-Jev4OoWa.js.map.gz
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/vue-apexcharts-BHFWl5EO.js.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-retour/develop-v5/src/web/assets/dist/assets/vue-apexcharts-BHFWl5EO.js.gz
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/_plugin-vue2_normalizer-BFq-tfv7.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"_plugin-vue2_normalizer-BFq-tfv7.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/vue-apexcharts-BHFWl5EO.js.map.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nystudio107/craft-retour/develop-v5/src/web/assets/dist/assets/vue-apexcharts-BHFWl5EO.js.map.gz
--------------------------------------------------------------------------------
/ecs.php:
--------------------------------------------------------------------------------
1 | paths([
8 | __DIR__ . '/src',
9 | __FILE__,
10 | ]);
11 | $ecsConfig->parallel();
12 | $ecsConfig->sets([SetList::CRAFT_CMS_4]);
13 | };
14 |
--------------------------------------------------------------------------------
/src/templates/_layouts/widget-cp.twig:
--------------------------------------------------------------------------------
1 |
2 | {% block head %}
3 | {% set tagOptions = {
4 | 'depends': [
5 | 'nystudio107\\retour\\assetbundles\\retour\\RetourAsset'
6 | ],
7 | } %}
8 | {{ craft.retour.register('src/js/Retour.js', false, tagOptions, tagOptions) }}
9 | {% endblock %}
10 |
11 | {% block content %}
12 | {% endblock %}
13 |
--------------------------------------------------------------------------------
/src/templates/_layouts/retour-cp.twig:
--------------------------------------------------------------------------------
1 | {% extends "_layouts/cp" %}
2 |
3 | {% block head %}
4 | {{ parent() }}
5 | {% set tagOptions = {
6 | 'depends': [
7 | 'nystudio107\\retour\\assetbundles\\retour\\RetourAsset'
8 | ],
9 | } %}
10 | {{ craft.retour.register('src/js/Retour.js', false, tagOptions, tagOptions) }}
11 | {% endblock %}
12 |
--------------------------------------------------------------------------------
/src/templates/_includes/macros.twig:
--------------------------------------------------------------------------------
1 | {% macro configWarning(setting, file) -%}
2 | {%- set configArray = craft.app.config.getConfigFromFile(file) -%}
3 | {%- if configArray[setting] is defined -%}
4 | {{- "This is being overridden by the `#{setting}` setting in the `config/#{file}.php` file." |raw }}
5 | {%- else -%}
6 | {{ false }}
7 | {%- endif -%}
8 | {%- endmacro %}
9 |
--------------------------------------------------------------------------------
/src/helpers/Gql.php:
--------------------------------------------------------------------------------
1 | update('{{%widgets}}', [
20 | 'type' => RetourWidget::class,
21 | ], ['type' => 'Retour']);
22 |
23 | return true;
24 | }
25 |
26 | /**
27 | * @inheritdoc
28 | */
29 | public function safeDown(): bool
30 | {
31 | echo "m190416_212500_widget_type_update cannot be reverted.\n";
32 |
33 | return false;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/events/RedirectResolvedEvent.php:
--------------------------------------------------------------------------------
1 | db->columnExists('{{%retour_static_redirects}}', 'priority')) {
19 | $this->addColumn(
20 | '{{%retour_static_redirects}}',
21 | 'priority',
22 | $this->integer()->null()->after('redirectHttpCode')->defaultValue(5)
23 | );
24 | }
25 | }
26 |
27 | /**
28 | * @inheritdoc
29 | */
30 | public function safeDown()
31 | {
32 | echo "m221130_224500_add_priorities_column cannot be reverted.\n";
33 | return false;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/migrations/m181013_122446_add_remote_ip.php:
--------------------------------------------------------------------------------
1 | db->columnExists('{{%retour_stats}}', 'remoteIp')) {
20 | $this->addColumn(
21 | '{{%retour_stats}}',
22 | 'remoteIp',
23 | $this->string(45)->after('referrerUrl')->defaultValue('')
24 | );
25 | }
26 |
27 | return true;
28 | }
29 |
30 | /**
31 | * @inheritdoc
32 | */
33 | public function safeDown(): bool
34 | {
35 | echo "m181013_122446_add_remote_ip cannot be reverted.\n";
36 | return false;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/templates/index.twig:
--------------------------------------------------------------------------------
1 | {# @var craft \craft\web\twig\variables\CraftVariable #}
2 | {#
3 | /**
4 | * Retour plugin for Craft CMS
5 | *
6 | * Retour index.twig
7 | *
8 | * @author nystudio107
9 | * @copyright Copyright (c) 2018 nystudio107
10 | * @link https://nystudio107.com/
11 | * @package Retour
12 | * @since 3.x.x
13 | */
14 | #}
15 |
16 | {% requirePermission 'accessPlugin-retour' %}
17 |
18 | {# Redirect the user to the first sub page they can access #}
19 | {% if currentUser.can('retour:dashboard') %}
20 | {% redirect cpUrl('retour/dashboard') %}
21 | {% elseif currentUser.can('retour:redirects') %}
22 | {% redirect cpUrl('retour/redirects') %}
23 | {% elseif currentUser.can('retour:shortlinks') %}
24 | {% redirect cpUrl('retour/shortlinks') %}
25 | {% elseif currentUser.can('retour:settings') %}
26 | {% redirect cpUrl('retour/settings') %}
27 | {% else %}
28 | {# ...or throw a 403 if they can't access anything at all #}
29 | {% exit 403 %}
30 | {% endif %}
31 |
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/_plugin-vue2_normalizer-BFq-tfv7.js:
--------------------------------------------------------------------------------
1 | var l=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{};function s(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}function c(e){if(Object.prototype.hasOwnProperty.call(e,"__esModule"))return e;var t=e.default;if(typeof t=="function"){var o=function n(){return this instanceof n?Reflect.construct(t,arguments,this.constructor):t.apply(this,arguments)};o.prototype=t.prototype}else o={};return Object.defineProperty(o,"__esModule",{value:!0}),Object.keys(e).forEach(function(n){var u=Object.getOwnPropertyDescriptor(e,n);Object.defineProperty(o,n,u.get?u:{enumerable:!0,get:function(){return e[n]}})}),o}function d(e,t,o,n,u,f,a,i){var r=typeof e=="function"?e.options:e;return t&&(r.render=t,r.staticRenderFns=o,r._compiled=!0),f&&(r._scopeId="data-v-"+f),{exports:e,options:r}}export{c as a,l as c,s as g,d as n};
2 | //# sourceMappingURL=_plugin-vue2_normalizer-BFq-tfv7.js.map
3 |
--------------------------------------------------------------------------------
/src/icon-mask.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
11 |
12 |
--------------------------------------------------------------------------------
/src/web/assets/dist/img/icon-mask.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
11 |
12 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | MAJOR_VERSION?=5
2 | PLUGINDEV_PROJECT_DIR?=/Users/andrew/webdev/sites/plugindev/cms_v${MAJOR_VERSION}/
3 | VENDOR?=nystudio107
4 | PROJECT_PATH?=${VENDOR}/$(shell basename $(CURDIR))
5 |
6 | .PHONY: dev docs release
7 |
8 | # Start up the buildchain dev server
9 | dev:
10 | ${MAKE} -C buildchain/ dev
11 | # Start up the docs dev server
12 | docs:
13 | ${MAKE} -C docs/ dev
14 | # Run code quality tools, tests, and build the buildchain & docs in preparation for a release
15 | release: --code-quality --code-tests --buildchain-clean-build --docs-clean-build
16 | # The internal targets used by the dev & release targets
17 | --buildchain-clean-build:
18 | ${MAKE} -C buildchain/ clean
19 | ${MAKE} -C buildchain/ image-build
20 | ${MAKE} -C buildchain/ build
21 | --code-quality:
22 | ${MAKE} -C ${PLUGINDEV_PROJECT_DIR} -- ecs check vendor/${PROJECT_PATH}/src --fix
23 | ${MAKE} -C ${PLUGINDEV_PROJECT_DIR} -- phpstan analyze -c vendor/${PROJECT_PATH}/phpstan.neon
24 | --code-tests:
25 | --docs-clean-build:
26 | ${MAKE} -C docs/ clean
27 | ${MAKE} -C docs/ image-build
28 | ${MAKE} -C docs/ fix
29 |
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/widget-BQWJDBDA.js:
--------------------------------------------------------------------------------
1 | import{A as n,a as i}from"./vue-apexcharts-BHFWl5EO.js";import{n as o}from"./_plugin-vue2_normalizer-BFq-tfv7.js";const c=e=>({baseURL:e,headers:{"X-Requested-With":"XMLHttpRequest"}}),h=(e,t,s,a)=>{e.get(t,{params:s}).then(r=>{a&&a(r.data)}).catch(r=>{console.error(r)})},p={components:{apexcharts:n},props:{title:{type:String,default:""},subTitle:{type:String,default:""},days:{type:String,default:""},apiUrl:{type:String,default:""}},data:function(){return{chartOptions:{chart:{id:"vuechart-widget",toolbar:{show:!1}},colors:["#008FFB","#DCE6EC"],labels:["404 hits","404 hits handled"]},series:[50,50]}},created:function(){this.getSeriesData()},methods:{async getSeriesData(){const e=i.create(c(this.apiUrl));await h(e,"",{days:this.days},t=>{this.series=t})}}};var d=function(){var t=this,s=t._self._c;return s("apexcharts",{attrs:{options:t.chartOptions,series:t.series,height:"200px",type:"donut",width:"100%"}})},l=[],u=o(p,d,l,!1,null,null);const f=u.exports,_=window.Vue;new _({el:"#widget-content",components:{"widget-chart":f}});
2 | //# sourceMappingURL=widget-BQWJDBDA.js.map
3 |
--------------------------------------------------------------------------------
/src/assetbundles/retour/RetourAsset.php:
--------------------------------------------------------------------------------
1 | sourcePath = '@nystudio107/retour/web/assets/dist';
34 | $this->depends = [
35 | CpAsset::class,
36 | VueAsset::class,
37 | ];
38 |
39 | parent::init();
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/assetbundles/retour/RetourImportAsset.php:
--------------------------------------------------------------------------------
1 | sourcePath = '@nystudio107/retour/web/assets/dist';
34 | $this->depends = [
35 | CpAsset::class,
36 | VueAsset::class,
37 | RetourAsset::class,
38 | ];
39 |
40 | parent::init();
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/templates/_components/fields/ShortLink_settings.twig:
--------------------------------------------------------------------------------
1 | {% import "_includes/forms" as forms %}
2 |
3 | {{ forms.selectField({
4 | label: "Legacy URL Match Type"|t("retour"),
5 | instructions: "Should the legacy URL be matched by path (e.g. `/new-recipes/`) or by full URL (e.g.: `http://example.com/de/new-recipes/`)?"|t("retour"),
6 | id: "redirectSrcMatch",
7 | name: "redirectSrcMatch",
8 | value: field.redirectSrcMatch,
9 | options: {
10 | "pathonly": "Path Only"|t("retour"),
11 | "fullurl": "Full URL"|t("retour"),
12 | },
13 | }) }}
14 |
15 | {{ forms.selectField({
16 | label: "Redirect Type"|t("retour"),
17 | instructions: "Select whether the redirect should be permanent or temporary."|t("retour"),
18 | id: "redirectHttpCode",
19 | name: "redirectHttpCode",
20 | value: field.redirectHttpCode,
21 | options: {
22 | "301": "301 - Moved Permanently"|t("retour"),
23 | "302": "302 - Found"|t("retour"),
24 | "307": "307 - Temporary Redirect"|t("retour"),
25 | "308": "308 - Permanent Redirect"|t("retour"),
26 | "410": "410 - Gone"|t("retour"),
27 | },
28 | }) }}
29 |
--------------------------------------------------------------------------------
/src/assetbundles/retour/RetourDashboardAsset.php:
--------------------------------------------------------------------------------
1 | sourcePath = '@nystudio107/retour/web/assets/dist';
34 | $this->depends = [
35 | CpAsset::class,
36 | VueAsset::class,
37 | RetourAsset::class,
38 | ];
39 |
40 | parent::init();
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/assetbundles/retour/RetourRedirectsAsset.php:
--------------------------------------------------------------------------------
1 | sourcePath = '@nystudio107/retour/web/assets/dist';
34 | $this->depends = [
35 | CpAsset::class,
36 | VueAsset::class,
37 | RetourAsset::class,
38 | ];
39 |
40 | parent::init();
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/assetbundles/retour/RetourWidgetAsset.php:
--------------------------------------------------------------------------------
1 | sourcePath = '@nystudio107/retour/web/assets/dist';
34 | $this->depends = [
35 | CpAsset::class,
36 | VueAsset::class,
37 | RetourAsset::class,
38 | ];
39 | $this->js = [
40 | ];
41 |
42 | parent::init();
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/migrations/m200109_144310_add_redirectSrcUrl_index.php:
--------------------------------------------------------------------------------
1 | createIndexes();
18 |
19 | return true;
20 | }
21 |
22 | /**
23 | * @return void
24 | */
25 | protected function createIndexes(): void
26 | {
27 | $this->createIndex(
28 | $this->db->getIndexName(),
29 | '{{%retour_static_redirects}}',
30 | 'redirectSrcUrl',
31 | false
32 | );
33 |
34 | $this->createIndex(
35 | $this->db->getIndexName(),
36 | '{{%retour_stats}}',
37 | 'redirectSrcUrl',
38 | false
39 | );
40 | }
41 |
42 | /**
43 | * @inheritdoc
44 | */
45 | public function safeDown(): bool
46 | {
47 | echo "m200109_144310_add_redirectSrcUrl_index cannot be reverted.\n";
48 |
49 | return false;
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/migrations/m230627_141429_add_redirectMatchType_index.php:
--------------------------------------------------------------------------------
1 | createIndexes();
18 |
19 | return true;
20 | }
21 |
22 | /**
23 | * @return void
24 | */
25 | protected function createIndexes(): void
26 | {
27 | $this->createIndex(
28 | $this->db->getIndexName(),
29 | '{{%retour_static_redirects}}',
30 | 'redirectMatchType',
31 | false
32 | );
33 |
34 | $this->createIndex(
35 | $this->db->getIndexName(),
36 | '{{%retour_redirects}}',
37 | 'redirectMatchType',
38 | false
39 | );
40 | }
41 |
42 | /**
43 | * @inheritdoc
44 | */
45 | public function safeDown(): bool
46 | {
47 | echo "m230627_141429_add_redirectMatchType_index cannot be reverted.\n";
48 | return false;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/migrations/m181018_135656_add_redirect_status.php:
--------------------------------------------------------------------------------
1 | db->columnExists('{{%retour_redirects}}', 'enabled')) {
18 | $this->addColumn(
19 | '{{%retour_redirects}}',
20 | 'enabled',
21 | $this->boolean()->after('associatedElementId')->defaultValue(true)
22 | );
23 | }
24 | if (!$this->db->columnExists('{{%retour_static_redirects}}', 'enabled')) {
25 | $this->addColumn(
26 | '{{%retour_static_redirects}}',
27 | 'enabled',
28 | $this->boolean()->after('associatedElementId')->defaultValue(true)
29 | );
30 | }
31 |
32 | return true;
33 | }
34 |
35 | /**
36 | * @inheritdoc
37 | */
38 | public function safeDown(): bool
39 | {
40 | echo "m181018_135656_add_redirect_status cannot be reverted.\n";
41 |
42 | return false;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/templates/_components/fields/ShortLink_input.twig:
--------------------------------------------------------------------------------
1 | {% import "_includes/forms" as forms %}
2 |
3 | {%- set class = ['nicetext', 'retour-shortlink-icon'] %}
4 |
5 | {% set config = {
6 | id: id ?? field.getInputId(),
7 | describedBy: field.describedBy,
8 | name: name,
9 | value: value,
10 | class: class,
11 | required: field.required,
12 | } %}
13 |
14 |
22 |
23 | {{ forms.text(config) }}
24 |
--------------------------------------------------------------------------------
/src/templates/_components/fields/ShortLink_preview.twig:
--------------------------------------------------------------------------------
1 | {% import "_includes/forms" as forms %}
2 |
3 | {%- set class = ['retour-shortlink-preview'] %}
4 | {%- set class = (class ?? [])|explodeClass|merge([
5 | ]|filter) %}
6 |
7 | {% set config = {
8 | name: name,
9 | text: value,
10 | class: class,
11 | readonly: true,
12 | } %}
13 |
14 |
23 |
24 | {{ tag('div', config) }}
25 |
--------------------------------------------------------------------------------
/src/migrations/m181013_202455_add_redirect_src_match.php:
--------------------------------------------------------------------------------
1 | db->columnExists('{{%retour_redirects}}', 'redirectSrcMatch')) {
18 | $this->addColumn(
19 | '{{%retour_redirects}}',
20 | 'redirectSrcMatch',
21 | $this->string(32)->after('redirectSrcUrlParsed')->defaultValue('pathonly')
22 | );
23 | }
24 | if (!$this->db->columnExists('{{%retour_static_redirects}}', 'redirectSrcMatch')) {
25 | $this->addColumn(
26 | '{{%retour_static_redirects}}',
27 | 'redirectSrcMatch',
28 | $this->string(32)->after('redirectSrcUrlParsed')->defaultValue('pathonly')
29 | );
30 | }
31 |
32 | return true;
33 | }
34 |
35 | /**
36 | * @inheritdoc
37 | */
38 | public function safeDown(): bool
39 | {
40 | echo "m181013_202455_add_redirect_src_match cannot be reverted.\n";
41 |
42 | return false;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/templates/import/errors.twig:
--------------------------------------------------------------------------------
1 | {# @var craft \craft\web\twig\variables\CraftVariable #}
2 | {#
3 | /**
4 | * Retour plugin for Craft CMS
5 | *
6 | * @author nystudio107
7 | * @copyright Copyright (c) 2018 nystudio107
8 | * @link https://nystudio107.com/
9 | * @package Retour
10 | * @since 3.2.0
11 | */
12 | #}
13 |
14 | {% requirePermission "retour:redirects" %}
15 |
16 | {% extends "retour/_layouts/retour-cp.twig" %}
17 |
18 | {% import "_includes/forms" as forms %}
19 |
20 | {% block actionButton %}
21 | {% endblock %}
22 |
23 | {% block contextMenu %}
24 | {% endblock %}
25 |
26 | {% block content %}
27 |
28 |
29 |
30 | {{ "Some errors occured importing the CSV file."|t("retour") |md }}
31 |
32 |
{{ "All CSV rows without errors were still imported."|t("retour") }}
33 |
34 |
35 |
36 | {{ errorLogContents | raw }}
37 |
38 |
39 |
40 |
41 |
42 | {% endblock %}
43 |
44 | {% block foot %}
45 | {# include our JavaScript modules #}
46 | {{ parent() }}
47 | {% endblock %}
48 |
--------------------------------------------------------------------------------
/src/migrations/m210603_221000_add_gql_schema_components.php:
--------------------------------------------------------------------------------
1 | getProjectConfig();
20 | $schemaVersion = $projectConfig->get('plugins.retour.schemaVersion', true);
21 |
22 | if (version_compare($schemaVersion, '3.0.10', '<')) {
23 | foreach ($projectConfig->get('graphql.schemas') ?? [] as $schemaUid => $schemaComponents) {
24 | if (isset($schemaComponents['scope'])) {
25 | $scope = $schemaComponents['scope'];
26 | $scope[] = 'retour.all:read';
27 |
28 | $projectConfig->set("graphql.schemas.$schemaUid.scope", $scope);
29 | }
30 | }
31 | }
32 |
33 | return true;
34 | }
35 |
36 | /**
37 | * @inheritdoc
38 | */
39 | public function safeDown(): bool
40 | {
41 | echo "m210603_221000_add_gql_schema_components cannot be reverted.\n";
42 |
43 | return false;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/events/RedirectEvent.php:
--------------------------------------------------------------------------------
1 | [
33 | 'name' => 'uri',
34 | 'type' => Type::string(),
35 | 'description' => 'The URI to resolve a redirect for.',
36 | ],
37 | 'site' => [
38 | 'name' => 'site',
39 | 'type' => Type::string(),
40 | 'description' => 'The site handle to resolve a redirect for.',
41 | ],
42 | 'siteId' => [
43 | 'name' => 'siteId',
44 | 'type' => Type::int(),
45 | 'description' => 'The siteId to resolve a redirect for.',
46 | ],
47 | ];
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/events/ResolveRedirectEvent.php:
--------------------------------------------------------------------------------
1 |
33 |
34 |
35 | {{ redirectInput("retour/settings") }}
36 |
37 | {# -- General settings -- #}
38 | {% include "retour/settings/_includes/general.twig" %}
39 |
40 | {# -- Statistics settings -- #}
41 | {% include "retour/settings/_includes/statistics.twig" %}
42 |
43 | {# -- Advanced settings -- #}
44 | {% include "retour/settings/_includes/advanced.twig" %}
45 |
46 | {# include our JavaScript modules #}
47 | {{ parent() }}
48 | {% endblock %}
49 |
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/LegacyUrl-CeiszSHN.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"LegacyUrl-CeiszSHN.js","sources":["../../../../../buildchain/src/vue/LegacyUrl.vue"],"sourcesContent":["\n \n \n\n\n"],"names":["_sfc_main","title","enabled"],"mappings":"0DAcA,MAAAA,EAAA,CACA,MAAA,CACA,QAAA,CACA,KAAA,OACA,SAAA,EACA,EACA,SAAA,CACA,KAAA,OACA,QAAA,CACA,CACA,EACA,SAAA,CACA,UAAA,UAAA,CACA,IAAAC,EAAA,GAEA,OAAAA,GAAA,KAAA,QAAA,eAEAA,CACA,EACA,gBAAA,UAAA,CACA,IAAAC,EAAA,GAEA,MAAA,CAAA,KAAA,QAAA,QACA,OAGAA,CACA,CACA,CACA"}
--------------------------------------------------------------------------------
/src/helpers/Permission.php:
--------------------------------------------------------------------------------
1 | getUser()->getIdentity()) === null) {
38 | throw new ForbiddenHttpException('Your account has no identity.');
39 | }
40 |
41 | if (!$currentUser->can($permission)) {
42 | throw new ForbiddenHttpException("Your account doesn't have permission to assign access this resource.");
43 | }
44 | }
45 |
46 | // Protected Static Methods
47 | // =========================================================================
48 | }
49 |
--------------------------------------------------------------------------------
/src/templates/_components/widgets/Retour_body.twig:
--------------------------------------------------------------------------------
1 | {# @var craft \craft\web\twig\variables\CraftVariable #}
2 | {#
3 | /**
4 | * Retour plugin for Craft CMS
5 | *
6 | * Retour Widget Body
7 | *
8 | * @author nystudio107
9 | * @copyright Copyright (c) 2018 nystudio107
10 | * @link https://nystudio107.com/
11 | * @package Retour
12 | * @since 3.0.0
13 | */
14 | #}
15 |
16 | {% extends "retour/_layouts/widget-cp.twig" %}
17 |
18 | {% set baseAssetsUrl = view.getAssetManager().getPublishedUrl('@nystudio107/retour/web/assets/dist', true) %}
19 | {% set iconUrl = baseAssetsUrl ~ '/img/Retour-icon.svg' %}
20 |
21 | {% block content %}
22 |
23 |
404 Hits
24 |
Discrete 404s hits in the last {{ numberOfDays }} days
25 |
31 |
32 |
35 |
36 |
37 | {% set tagOptions = {
38 | 'depends': [
39 | 'nystudio107\\retour\\assetbundles\\retour\\RetourAsset'
40 | ],
41 | } %}
42 | {{ craft.retour.register('src/js/Widget.js', false, tagOptions, tagOptions) }}
43 | {% endblock %}
44 |
--------------------------------------------------------------------------------
/src/validators/DbStringValidator.php:
--------------------------------------------------------------------------------
1 | max === null) {
40 | throw new InvalidConfigException('The "max" property must be set.');
41 | }
42 | }
43 |
44 | /**
45 | * @inheritdoc
46 | */
47 | public function validateAttribute($model, $attribute): void
48 | {
49 | $value = $model->$attribute;
50 | $value = TextHelper::cleanupText($value);
51 | $value = StringHelper::encodeMb4($value);
52 | $value = TextHelper::truncate($value, $this->max);
53 | $model->$attribute = $value;
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/models/DbModel.php:
--------------------------------------------------------------------------------
1 | $propValue) {
48 | if (!property_exists($class, $propName)) {
49 | unset($config[$propName]);
50 | }
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/migrations/m181216_043222_rebuild_indexes.php:
--------------------------------------------------------------------------------
1 | dropIndexes();
19 | $this->createIndexes();
20 |
21 | return true;
22 | }
23 |
24 | /**
25 | * @return void
26 | */
27 | protected function dropIndexes()
28 | {
29 | Db::dropIndexIfExists('{{%retour_static_redirects}}', 'redirectSrcUrlParsed', true, $this->db);
30 | Db::dropIndexIfExists('{{%retour_redirects}}', 'redirectSrcUrlParsed', true, $this->db);
31 | }
32 |
33 | /**
34 | * @return void
35 | */
36 | protected function createIndexes(): void
37 | {
38 | $this->createIndex(
39 | $this->db->getIndexName(),
40 | '{{%retour_static_redirects}}',
41 | 'redirectSrcUrlParsed',
42 | false
43 | );
44 |
45 | $this->createIndex(
46 | $this->db->getIndexName(),
47 | '{{%retour_redirects}}',
48 | 'redirectSrcUrlParsed',
49 | false
50 | );
51 | }
52 |
53 | /**
54 | * @inheritdoc
55 | */
56 | public function safeDown(): bool
57 | {
58 | echo "m181216_043222_rebuild_indexes cannot be reverted.\n";
59 |
60 | return false;
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/controllers/ApiController.php:
--------------------------------------------------------------------------------
1 | enableApiEndpoint) {
47 | $this->allowAnonymous = false;
48 | }
49 |
50 | return parent::beforeAction($action);
51 | }
52 |
53 | /**
54 | * @param null $siteId
55 | * @return Response
56 | */
57 | public function actionGetRedirects($siteId = null): Response
58 | {
59 | $redirects = Retour::$plugin->redirects->getAllStaticRedirects(null, $siteId);
60 |
61 | return $this->asJson($redirects);
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/variables/RetourVariable.php:
--------------------------------------------------------------------------------
1 | redirects->getMatchesList();
38 | }
39 |
40 | /**
41 | * Return the http status code
42 | *
43 | * @return int
44 | */
45 | public function getHttpStatus(): int
46 | {
47 | return http_response_code();
48 | }
49 |
50 | /**
51 | * Return whether we are running Craft 3.1 or later
52 | *
53 | * @return bool
54 | */
55 | public function craft31(): bool
56 | {
57 | return true;
58 | }
59 |
60 | /**
61 | * Return whether we are running Craft 3.2 or later
62 | *
63 | * @return bool
64 | */
65 | public function craft32(): bool
66 | {
67 | return true;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/helpers/Version.php:
--------------------------------------------------------------------------------
1 | getPrettyVersion();
46 | } catch (Throwable $e) {
47 | Craft::error($e, __METHOD__);
48 | }
49 | if ($installedVersion) {
50 | if (Semver::satisfies($installedVersion, '^8.0.0')) {
51 | $version = 8;
52 | }
53 | if (Semver::satisfies($installedVersion, '^9.0.0')) {
54 | $version = 9;
55 | }
56 | }
57 |
58 | return $version;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/validators/ParsedUriValidator.php:
--------------------------------------------------------------------------------
1 | source === null) {
52 | throw new InvalidConfigException('The "source" property must be set.');
53 | }
54 | }
55 |
56 | /**
57 | * @inheritdoc
58 | */
59 | public function validateAttribute($model, $attribute): void
60 | {
61 | // Set the attribute to be the same value as the source
62 | $srcValue = $this->source;
63 | $value = $model->$srcValue;
64 | $model->$attribute = $value;
65 | // If we're supposed to parse it, do so
66 | if ($this->parse) {
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/gql/types/RetourType.php:
--------------------------------------------------------------------------------
1 | fieldName;
48 | $result = $source[$fieldName] ?? '';
49 | // Handle the `site` virtual field
50 | if ($fieldName === 'site') {
51 | $result = null;
52 | $siteId = $source['siteId'] ?? null;
53 | if ($siteId) {
54 | $site = Craft::$app->getSites()->getSiteById($siteId);
55 | if ($site !== null) {
56 | $result = $site->handle;
57 | }
58 | }
59 | }
60 |
61 | if (empty($result)) {
62 | $result = null;
63 | }
64 |
65 | return $result;
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/migrations/m181018_123901_add_stats_info.php:
--------------------------------------------------------------------------------
1 | db->columnExists('{{%retour_stats}}', 'exceptionFileLine')) {
18 | $this->addColumn(
19 | '{{%retour_stats}}',
20 | 'exceptionFileLine',
21 | $this->integer(45)->after('remoteIp')->defaultValue(0)
22 | );
23 | }
24 | if (!$this->db->columnExists('{{%retour_stats}}', 'exceptionFilePath')) {
25 | $this->addColumn(
26 | '{{%retour_stats}}',
27 | 'exceptionFilePath',
28 | $this->string(255)->after('remoteIp')->defaultValue('')
29 | );
30 | }
31 | if (!$this->db->columnExists('{{%retour_stats}}', 'exceptionMessage')) {
32 | $this->addColumn(
33 | '{{%retour_stats}}',
34 | 'exceptionMessage',
35 | $this->string(255)->after('remoteIp')->defaultValue('')
36 | );
37 | }
38 | if (!$this->db->columnExists('{{%retour_stats}}', 'userAgent')) {
39 | $this->addColumn(
40 | '{{%retour_stats}}',
41 | 'userAgent',
42 | $this->string(255)->after('remoteIp')->defaultValue('')
43 | );
44 | }
45 |
46 | return true;
47 | }
48 |
49 | /**
50 | * @inheritdoc
51 | */
52 | public function safeDown(): bool
53 | {
54 | echo "m181018_123901_add_stats_info cannot be reverted.\n";
55 |
56 | return false;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/validators/UriValidator.php:
--------------------------------------------------------------------------------
1 | $attribute;
50 | $redirectMatchType = 'redirectMatchType';
51 | // Always remove whitespace
52 | $value = preg_replace("/\r|\n/", '', $value);
53 | $value = trim($value);
54 | $model->$attribute = $value;
55 | // Only do any kind of validation for exact match redirects
56 | if ($model->$redirectMatchType !== 'exactmatch') {
57 | return;
58 | }
59 | // Don't mess with URLs that are already full URLs
60 | if (UrlHelper::isFullUrl($value)) {
61 | return;
62 | }
63 | // Make sure there is a leading /
64 | $value = '/' . ltrim($value, '/');
65 | $model->$attribute = $value;
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/migrations/m181013_171315_truncate_match_type.php:
--------------------------------------------------------------------------------
1 | db->getIsPgsql()) {
19 | $this->alterColumn(
20 | '{{%retour_redirects}}',
21 | 'redirectMatchType',
22 | $this->string(32)
23 | );
24 | $this->alterColumn(
25 | '{{%retour_redirects}}',
26 | 'redirectMatchType',
27 | "SET DEFAULT 'exactmatch'"
28 | );
29 | $this->alterColumn(
30 | '{{%retour_static_redirects}}',
31 | 'redirectMatchType',
32 | $this->string(32)
33 | );
34 | $this->alterColumn(
35 | '{{%retour_static_redirects}}',
36 | 'redirectMatchType',
37 | "SET DEFAULT 'exactmatch'"
38 | );
39 | }
40 | if ($this->db->getIsMysql()) {
41 | $this->alterColumn(
42 | '{{%retour_redirects}}',
43 | 'redirectMatchType',
44 | $this->string(32)->defaultValue('exactmatch')
45 | );
46 | $this->alterColumn(
47 | '{{%retour_static_redirects}}',
48 | 'redirectMatchType',
49 | $this->string(32)->defaultValue('exactmatch')
50 | );
51 | }
52 |
53 | return true;
54 | }
55 |
56 | /**
57 | * @inheritdoc
58 | */
59 | public function safeDown(): bool
60 | {
61 | echo "m181013_171315_truncate_match_type cannot be reverted.\n";
62 |
63 | return false;
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/templates/shortlinks/index.twig:
--------------------------------------------------------------------------------
1 | {# @var craft \craft\web\twig\variables\CraftVariable #}
2 | {#
3 | /**
4 | * Retour plugin for Craft CMS
5 | *
6 | * Retour Redirects index.twig
7 | *
8 | * @author nystudio107
9 | * @copyright Copyright (c) 2018 nystudio107
10 | * @link https://nystudio107.com/
11 | * @package Retour
12 | * @since 3.0.0
13 | */
14 | #}
15 |
16 | {% requirePermission "retour:shortlinks" %}
17 |
18 | {% extends "retour/_layouts/retour-cp.twig" %}
19 |
20 | {% import "_includes/forms" as forms %}
21 |
22 | {% do view.registerTranslations('retour', [
23 | 'Short Link',
24 | 'Redirect To',
25 | 'Match Type',
26 | 'Sites',
27 | 'Status',
28 | 'Hits',
29 | 'Last Hit',
30 | 'Search for:',
31 | 'Reset',
32 | 'Displaying',
33 | 'to',
34 | 'of',
35 | 'items',
36 | 'Per-page:',
37 | 'Delete',
38 | 'redirect',
39 | 'redirects',
40 | ]) %}
41 |
42 | {% block contextMenu %}
43 | {% include "retour/_includes/sites-menu.twig" %}
44 | {% endblock %}
45 |
46 | {% block actionButton %}
47 |
48 |
49 | {% endblock %}
50 |
51 | {% block content %}
52 |
53 |
54 |
{{ "Short Links are a way to create a short link or alias to an Entry, so the link is easier to type or remember."|t('retour') }}
55 |
{{ "Short Links are created by adding a Short Link field to the field layout of a Section."|t('retour') }}
56 |
57 |
61 |
62 |
63 | {% endblock %}
64 |
65 | {% block foot %}
66 | {# include our JavaScript modules #}
67 | {{ parent() }}
68 | {% set tagOptions = {
69 | 'depends': [
70 | 'nystudio107\\retour\\assetbundles\\retour\\RetourAsset'
71 | ],
72 | } %}
73 | {{ craft.retour.register('src/js/Shortlinks.js', false, tagOptions, tagOptions) }}
74 | {% endblock %}
75 |
--------------------------------------------------------------------------------
/src/console/controllers/StatsController.php:
--------------------------------------------------------------------------------
1 | statistics->trimStatistics($this->limit);
68 | echo Craft::t(
69 | 'retour',
70 | 'Trimmed {rows} from retour_stats table',
71 | ['rows' => $affectedRows]
72 | ) . PHP_EOL;
73 |
74 | return 0;
75 | }
76 |
77 | // Protected Methods
78 | // =========================================================================
79 | }
80 |
--------------------------------------------------------------------------------
/src/gql/types/generators/RetourGenerator.php:
--------------------------------------------------------------------------------
1 | $typeName,
43 | 'args' => function() use ($retourArgs) {
44 | return $retourArgs;
45 | },
46 | 'fields' => function() use ($retourFields) {
47 | return $retourFields;
48 | },
49 | 'description' => 'This entity has all the Retour fields',
50 | ]));
51 |
52 | $gqlTypes[$typeName] = $retourType;
53 | TypeLoader::registerType($typeName, function() use ($retourType) {
54 | return $retourType;
55 | });
56 |
57 | return $gqlTypes;
58 | }
59 |
60 | /**
61 | * @inheritdoc
62 | */
63 | public static function getName($context = null): string
64 | {
65 | return 'RetourType';
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nystudio107/craft-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 | "type": "craft-plugin",
5 | "version": "5.0.12",
6 | "keywords": [
7 | "craftcms",
8 | "craft-plugin",
9 | "retour",
10 | "redirect",
11 | "404",
12 | "statistics"
13 | ],
14 | "support": {
15 | "docs": "https://nystudio107.com/docs/retour/",
16 | "issues": "https://nystudio107.com/plugins/retour/support",
17 | "source": "https://github.com/nystudio107/craft-retour"
18 | },
19 | "license": "proprietary",
20 | "authors": [
21 | {
22 | "name": "nystudio107",
23 | "homepage": "https://nystudio107.com/"
24 | }
25 | ],
26 | "require-dev": {
27 | },
28 | "require": {
29 | "php": "^8.2",
30 | "craftcms/cms": "^5.0.0",
31 | "nystudio107/craft-plugin-vite": "^5.0.0",
32 | "league/csv": "^8.2 || ^9.0",
33 | "jean85/pretty-package-versions": "^1.5 || ^2.0"
34 | },
35 | "require-dev": {
36 | "codeception/codeception": "^5.0.11",
37 | "codeception/module-asserts": "^3.0.0",
38 | "codeception/module-datafactory": "^3.0.0",
39 | "codeception/module-phpbrowser": "^3.0.0",
40 | "codeception/module-rest": "^3.3.2",
41 | "codeception/module-yii2": "^1.1.9",
42 | "vlucas/phpdotenv": "^5.4.0",
43 | "craftcms/ecs": "dev-main",
44 | "craftcms/phpstan": "dev-main",
45 | "craftcms/rector": "dev-main"
46 | },
47 | "scripts": {
48 | "phpstan": "phpstan --ansi --memory-limit=1G",
49 | "check-cs": "ecs check --ansi",
50 | "fix-cs": "ecs check --fix --ansi",
51 | "test": "codecept run unit --coverage-xml"
52 | },
53 | "config": {
54 | "allow-plugins": {
55 | "craftcms/plugin-installer": true,
56 | "yiisoft/yii2-composer": true
57 | },
58 | "optimize-autoloader": true,
59 | "platform": {
60 | "php": "8.2"
61 | },
62 | "platform-check": false,
63 | "sort-packages": true
64 | },
65 | "autoload": {
66 | "psr-4": {
67 | "nystudio107\\retour\\": "src/"
68 | }
69 | },
70 | "extra": {
71 | "class": "nystudio107\\retour\\Retour",
72 | "handle": "retour",
73 | "name": "Retour"
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright © nystudio107
2 |
3 | Permission is hereby granted to any person obtaining a copy of this software
4 | (the “Software”) to use, copy, modify, merge, publish and/or distribute copies
5 | of the Software, and to permit persons to whom the Software is furnished to do
6 | so, subject to the following conditions:
7 |
8 | 1. **Don’t plagiarize.** The above copyright notice and this license shall be
9 | included in all copies or substantial portions of the Software.
10 |
11 | 2. **Don’t use the same license on more than one project.** Each licensed copy
12 | of the Software shall be actively installed in no more than one production
13 | environment at a time.
14 |
15 | 3. **Don’t mess with the licensing features.** Software features related to
16 | licensing shall not be altered or circumvented in any way, including (but
17 | not limited to) license validation, payment prompts, feature restrictions,
18 | and update eligibility.
19 |
20 | 4. **Pay up.** Payment shall be made immediately upon receipt of any notice,
21 | prompt, reminder, or other message indicating that a payment is owed.
22 |
23 | 5. **Follow the law.** All use of the Software shall not violate any applicable
24 | law or regulation, nor infringe the rights of any other person or entity.
25 |
26 | Failure to comply with the foregoing conditions will automatically and
27 | immediately result in termination of the permission granted hereby. This
28 | license does not include any right to receive updates to the Software or
29 | technical support. Licensees bear all risk related to the quality and
30 | performance of the Software and any modifications made or obtained to it,
31 | including liability for actual and consequential harm, such as loss or
32 | corruption of data, and any necessary service, repair, or correction.
33 |
34 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
35 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
36 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
37 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
38 | LIABILITY, INCLUDING SPECIAL, INCIDENTAL AND CONSEQUENTIAL DAMAGES, WHETHER IN
39 | AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
40 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
41 |
--------------------------------------------------------------------------------
/src/templates/_includes/sites-menu.twig:
--------------------------------------------------------------------------------
1 | {% import "_includes/forms" as forms %}
2 |
3 | {% set baseUrl = "retour/#{controllerHandle}/" %}
4 | {% set params = [] %}
5 |
6 | {% if showSites %}
7 | {{ sitesMenuLabel }}
8 |
10 |
11 |
52 | {% endif %}
53 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://scrutinizer-ci.com/g/nystudio107/craft-retour/?branch=v5) [](https://scrutinizer-ci.com/g/nystudio107/craft-retour/build-status/v5) [](https://scrutinizer-ci.com/code-intelligence) [](https://scrutinizer-ci.com/g/nystudio107/craft-retour/?branch=v5)
2 |
3 | # Retour plugin for Craft CMS 5.x
4 |
5 | Retour allows you to intelligently redirect legacy URLs, so that you don't lose SEO value when rebuilding & restructuring a website
6 |
7 | 
8 |
9 | Related: [Retour for Craft 2.x](https://github.com/nystudio107/retour)
10 |
11 | **Note**: _The license fee for this plugin is $59.00 via the Craft Plugin Store._
12 |
13 | ### Upgrading from Retour 1.x for Craft CMS 2.x
14 |
15 | Even though this version of Retour was entirely rewritten for Craft CMS 3, it was designed to use all of the same data used by the Craft CMS 2.x version of Retour.
16 |
17 | So any existing redirects and statistics will continue to be in place.
18 |
19 | ## Used By
20 |
21 | 
22 |
23 | Retour is the redirect tool that the SEO experts at [Moz.com](https://moz.com/) and the creators of Craft CMS, Pixel & Tonic, rely on to handle their website redirects!
24 |
25 | ## Requirements
26 |
27 | This plugin requires Craft CMS 5.0.0 or later.
28 |
29 | ## Installation
30 |
31 | To install the plugin, follow these instructions.
32 |
33 | 1. Open your terminal and go to your Craft project:
34 |
35 | cd /path/to/project
36 |
37 | 2. Then tell Composer to load the plugin:
38 |
39 | composer require nystudio107/craft-retour
40 |
41 | 3. Install the plugin via `./craft install/plugin retour` via the CLI, or in the Control Panel, go to Settings → Plugins and click the “Install” button for Retour.
42 |
43 | You can also install Retour via the **Plugin Store** in the Craft Control Panel.
44 |
45 | ## Documentation
46 |
47 | Click here -> [Retour Documentation](https://nystudio107.com/plugins/retour/documentation)
48 |
49 | ## Retour Roadmap
50 |
51 | Some things to do, and ideas for potential features:
52 |
53 | * Release it
54 |
55 | Brought to you by [nystudio107](https://nystudio107.com/)
56 |
--------------------------------------------------------------------------------
/src/templates/import/index.twig:
--------------------------------------------------------------------------------
1 | {# @var craft \craft\web\twig\variables\CraftVariable #}
2 | {#
3 | /**
4 | * Retour plugin for Craft CMS
5 | *
6 | * Retour Dashboard index.twig
7 | *
8 | * @author nystudio107
9 | * @copyright Copyright (c) 2018 nystudio107
10 | * @link https://nystudio107.com/
11 | * @package Retour
12 | * @since 3.0.0
13 | */
14 | #}
15 |
16 | {% requirePermission "retour:redirects" %}
17 |
18 | {% extends "retour/_layouts/retour-cp.twig" %}
19 |
20 | {% import "_includes/forms" as forms %}
21 |
22 | {% do view.registerTranslations('retour', [
23 | 'Legacy URL Pattern',
24 | 'Redirect To',
25 | 'Match Type',
26 | 'HTTP Status',
27 | 'Site ID',
28 | 'Legacy URL Match Type',
29 | 'Hits',
30 | 'Priority',
31 | ]) %}
32 |
33 | {% block actionButton %}
34 |
35 | {% endblock %}
36 |
37 | {% block contextMenu %}
38 | {% endblock %}
39 |
40 | {% block content %}
41 |
42 | {{ csrfInput() }}
43 | {{ redirectInput("retour/redirects") }}
44 |
45 | {{ forms.hidden({
46 | id: "filename",
47 | name: "filename",
48 | value: filename,
49 | }) }}
50 |
51 |
52 |
53 |
54 |
55 | {{ "Choose the fields to import into Retour from the CSV file by dragging them in the appropriate order. Click on the `x` to delete an unused field." |t("retour") |md }}
56 |
57 |
58 |
59 |
61 |
62 |
63 |
64 | {{ "The **Match Type** field must be either `exactmatch` or `regexmatch` (case sensitive). Anything left blank will be filled in with default values." |t("retour") |md }}
65 |
66 |
67 |
68 |
69 | {% endblock %}
70 |
71 | {% block foot %}
72 | {# include our JavaScript modules #}
73 | {{ parent() }}
74 | {% set tagOptions = {
75 | 'depends': [
76 | 'nystudio107\\retour\\assetbundles\\retour\\RetourAsset'
77 | ],
78 | } %}
79 | {{ craft.retour.register('src/js/Import.js', false, tagOptions, tagOptions) }}
80 | {% endblock %}
81 |
82 |
--------------------------------------------------------------------------------
/src/web/assets/dist/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "_LegacyUrl-CeiszSHN.js": {
3 | "file": "assets/LegacyUrl-CeiszSHN.js",
4 | "name": "LegacyUrl",
5 | "imports": [
6 | "__plugin-vue2_normalizer-BFq-tfv7.js"
7 | ]
8 | },
9 | "__plugin-vue2_normalizer-BFq-tfv7.js": {
10 | "file": "assets/_plugin-vue2_normalizer-BFq-tfv7.js",
11 | "name": "_plugin-vue2_normalizer"
12 | },
13 | "_purify-DvoaRXnC.css": {
14 | "file": "assets/purify-DvoaRXnC.css",
15 | "src": "_purify-DvoaRXnC.css"
16 | },
17 | "_purify.es-DP_WP6YH.js": {
18 | "file": "assets/purify.es-DP_WP6YH.js",
19 | "name": "purify.es",
20 | "imports": [
21 | "__plugin-vue2_normalizer-BFq-tfv7.js"
22 | ],
23 | "css": [
24 | "assets/purify-DvoaRXnC.css"
25 | ]
26 | },
27 | "_vue-apexcharts-BHFWl5EO.js": {
28 | "file": "assets/vue-apexcharts-BHFWl5EO.js",
29 | "name": "vue-apexcharts",
30 | "imports": [
31 | "__plugin-vue2_normalizer-BFq-tfv7.js"
32 | ]
33 | },
34 | "src/js/Dashboard.js": {
35 | "file": "assets/dashboard-DVAYjtqJ.js",
36 | "name": "dashboard",
37 | "src": "src/js/Dashboard.js",
38 | "isEntry": true,
39 | "imports": [
40 | "_purify.es-DP_WP6YH.js",
41 | "__plugin-vue2_normalizer-BFq-tfv7.js",
42 | "_vue-apexcharts-BHFWl5EO.js"
43 | ]
44 | },
45 | "src/js/Import.js": {
46 | "file": "assets/import-Cap4Ox0Y.js",
47 | "name": "import",
48 | "src": "src/js/Import.js",
49 | "isEntry": true,
50 | "imports": [
51 | "__plugin-vue2_normalizer-BFq-tfv7.js"
52 | ]
53 | },
54 | "src/js/Redirects.js": {
55 | "file": "assets/redirects-B6OCJnQt.js",
56 | "name": "redirects",
57 | "src": "src/js/Redirects.js",
58 | "isEntry": true,
59 | "imports": [
60 | "_purify.es-DP_WP6YH.js",
61 | "_LegacyUrl-CeiszSHN.js",
62 | "__plugin-vue2_normalizer-BFq-tfv7.js"
63 | ]
64 | },
65 | "src/js/Retour.js": {
66 | "file": "assets/retour-DnaB684y.js",
67 | "name": "retour",
68 | "src": "src/js/Retour.js",
69 | "isEntry": true,
70 | "css": [
71 | "assets/retour-exeLeENM.css"
72 | ]
73 | },
74 | "src/js/Shortlinks.js": {
75 | "file": "assets/shortlinks-Jev4OoWa.js",
76 | "name": "shortlinks",
77 | "src": "src/js/Shortlinks.js",
78 | "isEntry": true,
79 | "imports": [
80 | "_purify.es-DP_WP6YH.js",
81 | "_LegacyUrl-CeiszSHN.js",
82 | "__plugin-vue2_normalizer-BFq-tfv7.js"
83 | ]
84 | },
85 | "src/js/Widget.js": {
86 | "file": "assets/widget-BQWJDBDA.js",
87 | "name": "widget",
88 | "src": "src/js/Widget.js",
89 | "isEntry": true,
90 | "imports": [
91 | "_vue-apexcharts-BHFWl5EO.js",
92 | "__plugin-vue2_normalizer-BFq-tfv7.js"
93 | ]
94 | }
95 | }
--------------------------------------------------------------------------------
/src/gql/queries/RetourQuery.php:
--------------------------------------------------------------------------------
1 | [
42 | 'type' => RetourInterface::getType(),
43 | 'args' => RetourArguments::getArguments(),
44 | 'resolve' => RetourResolver::class . '::resolve',
45 | 'description' => 'This query is used to query for Retour redirects.',
46 | 'deprecationReason' => 'This query is deprecated and will be removed in the future. You should use `retourResolveRedirect` instead.',
47 | ],
48 | 'retourResolveRedirect' => [
49 | 'type' => RetourInterface::getType(),
50 | 'args' => RetourArguments::getArguments(),
51 | 'resolve' => RetourResolver::class . '::resolve',
52 | 'description' => 'This query is used to query for Retour redirects.',
53 | ],
54 | 'retourRedirects' => [
55 | 'type' => Type::listOf(RetourInterface::getType()),
56 | 'args' => [
57 | 'site' => [
58 | 'name' => 'site',
59 | 'type' => Type::string(),
60 | 'description' => 'The site handle to list all redirects for.',
61 | ],
62 | 'siteId' => [
63 | 'name' => 'siteId',
64 | 'type' => Type::int(),
65 | 'description' => 'The siteId to list all redirects for.',
66 | ],
67 | ],
68 | 'resolve' => RetourResolver::class . '::resolveAll',
69 | 'description' => 'This query is used to query for all Retour redirects for a site.',
70 | ],
71 | ];
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/templates/redirects/index.twig:
--------------------------------------------------------------------------------
1 | {# @var craft \craft\web\twig\variables\CraftVariable #}
2 | {#
3 | /**
4 | * Retour plugin for Craft CMS
5 | *
6 | * Retour Redirects index.twig
7 | *
8 | * @author nystudio107
9 | * @copyright Copyright (c) 2018 nystudio107
10 | * @link https://nystudio107.com/
11 | * @package Retour
12 | * @since 3.0.0
13 | */
14 | #}
15 |
16 | {% requirePermission "retour:redirects" %}
17 |
18 | {% extends "retour/_layouts/retour-cp.twig" %}
19 |
20 | {% import "_includes/forms" as forms %}
21 |
22 | {% do view.registerTranslations('retour', [
23 | 'Legacy URL Pattern',
24 | 'Redirect To',
25 | 'Match Type',
26 | 'Sites',
27 | 'Status',
28 | 'Hits',
29 | 'Last Hit',
30 | 'Search for:',
31 | 'Reset',
32 | 'Displaying',
33 | 'to',
34 | 'of',
35 | 'items',
36 | 'Per-page:',
37 | 'Delete',
38 | 'redirect',
39 | 'redirects',
40 | ]) %}
41 |
42 | {% block contextMenu %}
43 | {% include "retour/_includes/sites-menu.twig" %}
44 | {% endblock %}
45 |
46 | {% block actionButton %}
47 |
64 | {% endblock %}
65 |
66 | {% block content %}
67 |
68 |
69 |
{{ "Redirects only happen if Craft throws a `404` exception for the Legacy URL Pattern."|t('retour')|md|raw }}
70 |
71 |
75 |
76 |
77 | {% endblock %}
78 |
79 | {% block foot %}
80 | {# include our JavaScript modules #}
81 | {{ parent() }}
82 | {% set tagOptions = {
83 | 'depends': [
84 | 'nystudio107\\retour\\assetbundles\\retour\\RetourAsset'
85 | ],
86 | } %}
87 | {{ craft.retour.register('src/js/Redirects.js', false, tagOptions, tagOptions) }}
88 | {% endblock %}
89 |
--------------------------------------------------------------------------------
/src/config.php:
--------------------------------------------------------------------------------
1 | 'Retour',
29 |
30 | // Controls whether Retour automatically creates static redirects when an entry's URI changes.
31 | 'createUriChangeRedirects' => true,
32 |
33 | // Should the legacy URL be matched by path (e.g. `/new-recipes/`) or by full URL (e.g.: `http://example.com/de/new-recipes/`)?
34 | 'uriChangeRedirectSrcMatch' => 'pathonly',
35 |
36 | // Should the query string be stripped from all 404 URLs before their evaluation?
37 | 'alwaysStripQueryString' => false,
38 |
39 | // Should the query string be preserved and passed along to the redirected URL?
40 | 'preserveQueryString' => false,
41 |
42 | // Should `no-cache` headers be set on the redirect response to prevent client-side caching?
43 | 'setNoCacheHeaders' => true,
44 |
45 | // Should the query string be stripped from the saved statistics source URLs?
46 | 'stripQueryStringFromStats' => true,
47 |
48 | // Should the anonymous ip address of the client causing a 404 be recorded?
49 | 'recordRemoteIp' => true,
50 |
51 | // How many stats should be stored
52 | 'statsStoredLimit' => 1000,
53 |
54 | // Dashboard data live refresh interval
55 | 'refreshIntervalSecs' => 5,
56 |
57 | // Whether statistics should be kept to track how many times a redirect has been followed
58 | 'enableStatistics' => true,
59 |
60 | // Whether the Statistics should be trimmed after each new statistic is recorded
61 | 'automaticallyTrimStatistics' => true,
62 |
63 | // The number of milliseconds required between trimming of statistics
64 | 'statisticsRateLimitMs' => 3600000,
65 |
66 | // Determines whether the Retour API endpoint should be enabled for anonymous frontend access
67 | 'enableApiEndpoint' => false,
68 |
69 | // Should Craft sites factor into determining redirect destination URLs
70 | 'resolveCraftSites' => true,
71 |
72 | // [Regular expressions](https://regexr.com/) to match URLs to exclude from tracking
73 | 'excludePatterns' => [
74 | ],
75 |
76 | // Additional headers to add to redirected requests
77 | 'additionalHeaders' => [
78 | ],
79 |
80 | // The delimiter between data column values for importing CSV files (normally `,`)
81 | 'csvColumnDelimiter' => ',',
82 | ];
83 |
--------------------------------------------------------------------------------
/src/helpers/FileLog.php:
--------------------------------------------------------------------------------
1 | $fileName,
48 | 'categories' => [$category],
49 | 'level' => LogLevel::INFO,
50 | 'logContext' => false,
51 | 'allowLineBreaks' => true,
52 | 'formatter' => new LineFormatter(
53 | format: "%datetime% [%channel%.%level_name%] [%extra.yii_category%] %message% %context% %extra%\n",
54 | dateFormat: 'Y-m-d H:i:s',
55 | allowInlineLineBreaks: true,
56 | ignoreEmptyContextAndExtra: true,
57 | ),
58 | ]);
59 | // Add the new target file target to the dispatcher
60 | Craft::getLogger()->dispatcher->targets[] = $fileTarget;
61 | }
62 |
63 | /**
64 | * Delete the passed in log file $fileName
65 | *
66 | * @param string $fileName
67 | * @return void
68 | */
69 | public static function delete(string $fileName): void
70 | {
71 | FileHelper::unlink(self::getLogFilePath($fileName));
72 | }
73 |
74 | /**
75 | * Get the contents of the log file
76 | *
77 | * @param string $fileName
78 | * @return string
79 | */
80 | public static function getContents(string $fileName): string
81 | {
82 | $contents = @file_get_contents(self::getLogFilePath($fileName));
83 |
84 | return $contents === false ? '' : $contents;
85 | }
86 |
87 | /**
88 | * Return a full path to the log file
89 | *
90 | * @param string $fileName
91 | * @return string
92 | */
93 | public static function getLogFilePath(string $fileName): string
94 | {
95 | $logfile = $fileName . "-" . date(self::FILE_PER_DAY);
96 |
97 | return Craft::getAlias("@storage/logs/$logfile.log");
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/widget-BQWJDBDA.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"widget-BQWJDBDA.js","sources":["../../../../../buildchain/src/vue/WidgetChart.vue","../../../../../buildchain/src/js/Widget.js"],"sourcesContent":["\n \n \n\n\n","import Vue from 'vue';\nimport WidgetChart from '@/vue/WidgetChart.vue';\n\n// Create our vue instance\nnew Vue({\n el: \"#widget-content\",\n components: {\n 'widget-chart': WidgetChart,\n },\n});\n\n// Accept HMR as per: https://vitejs.dev/guide/api-hmr.html\nif (import.meta.hot) {\n import.meta.hot.accept(() => {\n console.log(\"HMR\")\n });\n}\n"],"names":["configureApi","url","queryApi","api","uri","params","callback","result","error","_sfc_main","ApexCharts","chartsAPI","Axios","data","Vue","WidgetChart"],"mappings":"kHAgBA,MAAAA,EAAAC,IACA,CACA,QAAAA,EACA,QAAA,CACA,mBAAA,gBACA,CACA,GAGAC,EAAA,CAAAC,EAAAC,EAAAC,EAAAC,IAAA,CACAH,EAAA,IAAAC,EAAA,CAAA,OAAAC,CAAA,CAAA,EACA,KAAAE,GAAA,CACAD,GACAA,EAAAC,EAAA,IAAA,CAEA,CAAA,EACA,MAAAC,GAAA,CACA,QAAA,MAAAA,CAAA,CACA,CAAA,CACA,EAGAC,EAAA,CACA,WAAA,CACA,WAAAC,CACA,EACA,MAAA,CACA,MAAA,CACA,KAAA,OACA,QAAA,EACA,EACA,SAAA,CACA,KAAA,OACA,QAAA,EACA,EACA,KAAA,CACA,KAAA,OACA,QAAA,EACA,EACA,OAAA,CACA,KAAA,OACA,QAAA,EACA,CACA,EACA,KAAA,UAAA,CACA,MAAA,CACA,aAAA,CACA,MAAA,CACA,GAAA,kBACA,QAAA,CACA,KAAA,EACA,CACA,EACA,OAAA,CAAA,UAAA,SAAA,EACA,OAAA,CACA,WACA,kBACA,CACA,EACA,OAAA,CAAA,GAAA,EAAA,CACA,CACA,EACA,QAAA,UAAA,CACA,KAAA,cAAA,CACA,EACA,QAAA,CAEA,MAAA,eAAA,CACA,MAAAC,EAAAC,EAAA,OAAAZ,EAAA,KAAA,MAAA,CAAA,EACA,MAAAE,EAAAS,EAAA,GAAA,CAAA,KAAA,KAAA,IAAA,EAAAE,GAAA,CACA,KAAA,OAAAA,CACA,CAAA,CACA,CACA,CACA,4MC1FAC,EAAA,OAAA,IAIA,IAAIA,EAAI,CACN,GAAI,kBACJ,WAAY,CACV,eAAgBC,CAAA,CAEpB,CAAC"}
--------------------------------------------------------------------------------
/src/services/ServicesTrait.php:
--------------------------------------------------------------------------------
1 | = 8.2, and config() is called before __construct(),
39 | // so we can't extract it from the passed in $config
40 | $majorVersion = '5';
41 | // Dev server container name & port are based on the major version of this plugin
42 | $devPort = 3000 + (int)$majorVersion;
43 | $versionName = 'v' . $majorVersion;
44 | return [
45 | 'components' => [
46 | 'events' => Events::class,
47 | 'redirects' => Redirects::class,
48 | 'statistics' => Statistics::class,
49 | // Register the vite service
50 | 'vite' => [
51 | 'assetClass' => RetourAsset::class,
52 | 'checkDevServer' => true,
53 | 'class' => VitePluginService::class,
54 | 'devServerInternal' => 'http://craft-retour-' . $versionName . '-buildchain-dev:' . $devPort,
55 | 'devServerPublic' => 'http://localhost:' . $devPort,
56 | 'errorEntry' => 'src/js/Retour.js',
57 | 'useDevServer' => true,
58 | ],
59 | ],
60 | ];
61 | }
62 |
63 | // Public Methods
64 | // =========================================================================
65 |
66 | /**
67 | * Returns the events service
68 | *
69 | * @return Events The events service
70 | * @throws InvalidConfigException
71 | */
72 | public function getEvents(): Events
73 | {
74 | return $this->get('events');
75 | }
76 |
77 | /**
78 | * Returns the redirects service
79 | *
80 | * @return Redirects The redirects service
81 | * @throws InvalidConfigException
82 | */
83 | public function getRedirects(): Redirects
84 | {
85 | return $this->get('redirects');
86 | }
87 |
88 | /**
89 | * Returns the statistics service
90 | *
91 | * @return Statistics The statistics service
92 | * @throws InvalidConfigException
93 | */
94 | public function getStatistics(): Statistics
95 | {
96 | return $this->get('statistics');
97 | }
98 |
99 | /**
100 | * Returns the vite service
101 | *
102 | * @return VitePluginService The vite service
103 | * @throws InvalidConfigException
104 | */
105 | public function getVite(): VitePluginService
106 | {
107 | return $this->get('vite');
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/src/helpers/Text.php:
--------------------------------------------------------------------------------
1 | truncate($length, $substring);
48 | }
49 |
50 | return $result;
51 | }
52 |
53 | /**
54 | * Truncates the string to a given length, while ensuring that it does not
55 | * split words. If $substring is provided, and truncating occurs, the
56 | * string is further truncated so that the substring may be appended without
57 | * exceeding the desired length.
58 | *
59 | * @param string $string The string to truncate
60 | * @param int $length Desired length of the truncated string
61 | * @param string $substring The substring to append if it can fit
62 | *
63 | * @return string with the resulting $str after truncating
64 | */
65 | public static function truncateOnWord(string $string, int $length, string $substring = '…'): string
66 | {
67 | $result = $string;
68 |
69 | if (!empty($string)) {
70 | $string = strip_tags($string);
71 | $result = (string)Stringy::create($string)->safeTruncate($length, $substring);
72 | }
73 |
74 | return $result;
75 | }
76 |
77 | /**
78 | * Clean up the passed in text by converting it to UTF-8, stripping tags,
79 | * removing whitespace, and decoding HTML entities
80 | *
81 | * @param string $text
82 | *
83 | * @return string
84 | */
85 | public static function cleanupText(string $text): string
86 | {
87 | if (empty($text)) {
88 | return '';
89 | }
90 | // Convert to UTF-8
91 | $text = StringHelper::convertToUtf8($text);
92 | // Strip HTML tags
93 | $text = strip_tags($text);
94 | // Remove excess whitespace
95 | $text = preg_replace('/\s{2,}/u', ' ', $text);
96 | // Decode any HTML entities
97 | $text = html_entity_decode($text);
98 |
99 | return $text;
100 | }
101 |
102 | // Protected Static Methods
103 | // =========================================================================
104 | }
105 |
--------------------------------------------------------------------------------
/src/migrations/m181213_233502_add_site_id.php:
--------------------------------------------------------------------------------
1 | db->columnExists('{{%retour_redirects}}', 'locale')) {
21 | $this->dropColumn(
22 | '{{%retour_redirects}}',
23 | 'locale'
24 | );
25 | }
26 | if ($this->db->columnExists('{{%retour_static_redirects}}', 'locale')) {
27 | $this->dropColumn(
28 | '{{%retour_static_redirects}}',
29 | 'locale'
30 | );
31 | }
32 | // Add in the siteId columns
33 | if (!$this->db->columnExists('{{%retour_redirects}}', 'siteId')) {
34 | $this->addColumn(
35 | '{{%retour_redirects}}',
36 | 'siteId',
37 | $this->integer()->null()->after('uid')->defaultValue(null)
38 | );
39 | }
40 | if (!$this->db->columnExists('{{%retour_static_redirects}}', 'siteId')) {
41 | $this->addColumn(
42 | '{{%retour_static_redirects}}',
43 | 'siteId',
44 | $this->integer()->null()->after('uid')->defaultValue(null)
45 | );
46 | }
47 | if (!$this->db->columnExists('{{%retour_stats}}', 'siteId')) {
48 | $this->addColumn(
49 | '{{%retour_stats}}',
50 | 'siteId',
51 | $this->integer()->null()->after('uid')->defaultValue(null)
52 | );
53 | }
54 | // Add foreign keys
55 | $this->addForeignKeys();
56 | // Create indexes
57 | $this->createIndexes();
58 |
59 | return true;
60 | }
61 |
62 | /**
63 | * @inheritdoc
64 | */
65 | public function safeDown()
66 | {
67 | echo "m181213_233502_add_site_id cannot be reverted.\n";
68 | return false;
69 | }
70 |
71 | /**
72 | * @return void
73 | */
74 | protected function addForeignKeys(): void
75 | {
76 | $this->addForeignKey(
77 | $this->db->getForeignKeyName(),
78 | '{{%retour_static_redirects}}',
79 | 'siteId',
80 | '{{%sites}}',
81 | 'id',
82 | 'CASCADE',
83 | 'CASCADE'
84 | );
85 |
86 | $this->addForeignKey(
87 | $this->db->getForeignKeyName(),
88 | '{{%retour_stats}}',
89 | 'siteId',
90 | '{{%sites}}',
91 | 'id',
92 | 'CASCADE',
93 | 'CASCADE'
94 | );
95 | }
96 |
97 | /**
98 | * @return void
99 | */
100 | protected function createIndexes(): void
101 | {
102 | $this->createIndex(
103 | $this->db->getIndexName(),
104 | '{{%retour_redirects}}',
105 | 'siteId',
106 | false
107 | );
108 |
109 | $this->createIndex(
110 | $this->db->getIndexName(),
111 | '{{%retour_static_redirects}}',
112 | 'siteId',
113 | false
114 | );
115 |
116 | $this->createIndex(
117 | $this->db->getIndexName(),
118 | '{{%retour_stats}}',
119 | 'siteId',
120 | false
121 | );
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/src/templates/settings/_includes/general.twig:
--------------------------------------------------------------------------------
1 | {# @var craft \craft\web\twig\variables\CraftVariable #}
2 | {#
3 | /**
4 | * Retour plugin for Craft CMS
5 | *
6 | * Retour Settings index.twig
7 | *
8 | * @author nystudio107
9 | * @copyright Copyright (c) 2018 nystudio107
10 | * @link https://nystudio107.com/
11 | * @package Retour
12 | * @since 3.0.0
13 | */
14 | #}
15 |
16 | {% import "_includes/forms" as forms %}
17 | {% from "retour/_includes/macros.twig" import configWarning %}
18 |
19 |
20 | {% namespace "settings" %}
21 | {{ forms.textField({
22 | label: "Plugin name"|t("retour"),
23 | instructions: "The public-facing name of the plugin"|t("retour"),
24 | id: "pluginName",
25 | name: "pluginName",
26 | value: settings.pluginName,
27 | warning: configWarning("pluginName", "retour"),
28 | errors: settings.getErrors("pluginName"),
29 | }) }}
30 |
31 | {{ forms.lightswitchField({
32 | label: "Create Entry Redirects"|t("retour"),
33 | instructions: "Controls whether Retour automatically creates static redirects when an entry's URI changes."|t("retour"),
34 | id: "createUriChangeRedirects",
35 | name: "createUriChangeRedirects",
36 | on: settings.createUriChangeRedirects,
37 | warning: configWarning("createUriChangeRedirects", "retour"),
38 | errors: settings.getErrors("createUriChangeRedirects"),
39 | }) }}
40 |
41 | {{ forms.selectField({
42 | label: "Entry Redirects URL Match Type"|t("retour"),
43 | instructions: "Should the automatically created Entry Redirects be matched by path (e.g. `/new-recipes/`) or by full URL (e.g.: `http://example.com/de/new-recipes/`)?"|t("retour"),
44 | id: "uriChangeRedirectSrcMatch",
45 | name: "uriChangeRedirectSrcMatch",
46 | value: settings.uriChangeRedirectSrcMatch ?? "pathonly",
47 | options: {
48 | "pathonly": "Path Only"|t("retour"),
49 | "fullurl": "Full URL"|t("retour"),
50 | },
51 | errors: settings.getErrors("uriChangeRedirectSrcMatch"),
52 | }) }}
53 |
54 | {{ forms.lightswitchField({
55 | label: "Strip Query String from 404s"|t("retour"),
56 | instructions: "Should the query string be stripped from all 404 URLs before their evaluation?"|t("retour"),
57 | id: "alwaysStripQueryString",
58 | name: "alwaysStripQueryString",
59 | on: settings.alwaysStripQueryString,
60 | warning: configWarning("alwaysStripQueryString", "retour"),
61 | errors: settings.getErrors("alwaysStripQueryString"),
62 | }) }}
63 |
64 | {{ forms.lightswitchField({
65 | label: "Preserve Query String"|t("retour"),
66 | instructions: "Should the query string be preserved and passed along to the redirected URL?"|t("retour"),
67 | id: "preserveQueryString",
68 | name: "preserveQueryString",
69 | on: settings.preserveQueryString,
70 | warning: configWarning("preserveQueryString", "retour"),
71 | errors: settings.getErrors("preserveQueryString"),
72 | }) }}
73 |
74 | {{ forms.lightswitchField({
75 | label: "Set No-Cache Headers"|t("retour"),
76 | instructions: "Should `no-cache` headers be set on the redirect response to prevent client-side caching?"|t("retour"),
77 | id: "setNoCacheHeaders",
78 | name: "setNoCacheHeaders",
79 | on: settings.setNoCacheHeaders,
80 | warning: configWarning("setNoCacheHeaders", "retour"),
81 | errors: settings.getErrors("setNoCacheHeaders"),
82 | }) }}
83 |
84 | {% endnamespace %}
85 |
86 |
--------------------------------------------------------------------------------
/src/widgets/RetourWidget.php:
--------------------------------------------------------------------------------
1 | pluginName;
50 | }
51 |
52 | /**
53 | * @inheritdoc
54 | */
55 | public static function iconPath(): bool|string
56 | {
57 | return Craft::getAlias("@nystudio107/retour/web/assets/dist/img/icon-mask.svg");
58 | }
59 |
60 | /**
61 | * @inheritdoc
62 | */
63 | public static function maxColspan(): ?int
64 | {
65 | return 1;
66 | }
67 |
68 | // Public Methods
69 | // =========================================================================
70 |
71 | /**
72 | * @inheritdoc
73 | */
74 | public function rules(): array
75 | {
76 | $rules = parent::rules();
77 | $rules = array_merge(
78 | $rules,
79 | [
80 | ['numberOfDays', 'integer', 'min' => 1],
81 | ['numberOfDays', 'default', 'value' => 30],
82 | ]
83 | );
84 | return $rules;
85 | }
86 |
87 | /**
88 | * @inheritdoc
89 | */
90 | public function getSettingsHtml(): null|string
91 | {
92 | try {
93 | return Craft::$app->getView()->renderTemplate(
94 | 'retour/_components/widgets/Retour_settings',
95 | [
96 | 'widget' => $this,
97 | ]
98 | );
99 | } catch (LoaderError $e) {
100 | Craft::error($e->getMessage(), __METHOD__);
101 | } catch (Exception $e) {
102 | Craft::error($e->getMessage(), __METHOD__);
103 | }
104 |
105 | return null;
106 | }
107 |
108 | /**
109 | * @inheritdoc
110 | */
111 | public function getBodyHtml(): null|string
112 | {
113 | try {
114 | Craft::$app->getView()->registerAssetBundle(RetourWidgetAsset::class);
115 | } catch (InvalidConfigException $e) {
116 | Craft::error($e->getMessage(), __METHOD__);
117 | }
118 |
119 | try {
120 | return Craft::$app->getView()->renderTemplate(
121 | 'retour/_components/widgets/Retour_body',
122 | [
123 | 'numberOfDays' => $this->numberOfDays,
124 | ]
125 | );
126 | } catch (LoaderError $e) {
127 | Craft::error($e->getMessage(), __METHOD__);
128 | } catch (Exception $e) {
129 | Craft::error($e->getMessage(), __METHOD__);
130 | }
131 |
132 | return null;
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/src/templates/settings/_includes/statistics.twig:
--------------------------------------------------------------------------------
1 | {# @var craft \craft\web\twig\variables\CraftVariable #}
2 | {#
3 | /**
4 | * Retour plugin for Craft CMS
5 | *
6 | * Retour Settings index.twig
7 | *
8 | * @author nystudio107
9 | * @copyright Copyright (c) 2018 nystudio107
10 | * @link https://nystudio107.com/
11 | * @package Retour
12 | * @since 3.0.0
13 | */
14 | #}
15 |
16 | {% import "_includes/forms" as forms %}
17 | {% from "retour/_includes/macros.twig" import configWarning %}
18 |
19 |
20 | {% namespace "settings" %}
21 | {{ forms.lightswitchField({
22 | label: "Strip Query String from Statistics"|t("retour"),
23 | instructions: "Should the query string be stripped from the saved statistics source URLs?"|t("retour"),
24 | id: "stripQueryStringFromStats",
25 | name: "stripQueryStringFromStats",
26 | on: settings.stripQueryStringFromStats,
27 | warning: configWarning("stripQueryStringFromStats", "retour"),
28 | errors: settings.getErrors("stripQueryStringFromStats"),
29 | }) }}
30 |
31 | {{ forms.lightswitchField({
32 | label: "Record Remote IP"|t("retour"),
33 | instructions: "Should the anonymous ip address of the client causing a 404 be recorded?"|t("retour"),
34 | id: "recordRemoteIp",
35 | name: "recordRemoteIp",
36 | on: settings.recordRemoteIp,
37 | warning: configWarning("recordRemoteIp", "retour"),
38 | errors: settings.getErrors("recordRemoteIp"),
39 | }) }}
40 |
41 | {{ forms.textField({
42 | label: "Statistics to Store"|t("retour"),
43 | instructions: "How many unique 404 statistics should be stored before they are trimmed."|t("retour"),
44 | id: "statsStoredLimit",
45 | name: "statsStoredLimit",
46 | size: 7,
47 | maxlength: 7,
48 | value: settings.statsStoredLimit,
49 | warning: configWarning("statsStoredLimit", "retour"),
50 | errors: settings.getErrors("statsStoredLimit"),
51 | }) }}
52 |
53 | {{ forms.selectField({
54 | label: "Dashboard Refresh Interval"|t("retour"),
55 | instructions: "Dashboard data live refresh interval."|t("retour"),
56 | id: "refreshIntervalSecs",
57 | name: "refreshIntervalSecs",
58 | options: {
59 | 0: "Never"|t("retour"),
60 | 5: "5 seconds"|t("retour"),
61 | 10: "10 seconds"|t("retour"),
62 | 30: "30 seconds"|t("retour"),
63 | 60: "60 seconds"|t("retour"),
64 | },
65 | size: 7,
66 | maxlength: 7,
67 | value: settings.refreshIntervalSecs,
68 | warning: configWarning("refreshIntervalSecs", "retour"),
69 | errors: settings.getErrors("refreshIntervalSecs"),
70 | }) }}
71 |
72 | {{ forms.lightswitchField({
73 | label: "Automatically Trim Statistics"|t("retour"),
74 | instructions: "Whether the Statistics should be trimmed after each new statistic is recorded."|t("retour"),
75 | id: "automaticallyTrimStatistics",
76 | name: "automaticallyTrimStatistics",
77 | on: settings.automaticallyTrimStatistics,
78 | warning: configWarning("automaticallyTrimStatistics", "retour"),
79 | errors: settings.getErrors("automaticallyTrimStatistics"),
80 | }) }}
81 |
82 | {{ forms.selectField({
83 | label: "Statistics Trimming Rate Limit"|t("retour"),
84 | instructions: "The amount of time required between trimming of statistics."|t("retour"),
85 | id: "statisticsRateLimitMs",
86 | name: "statisticsRateLimitMs",
87 | options: {
88 | 3600000: "Once per hour"|t("retour"),
89 | 86400000: "Once per day"|t("retour"),
90 | 604800000: "Once per week"|t("retour"),
91 | },
92 | value: settings.statisticsRateLimitMs,
93 | warning: configWarning("statisticsRateLimitMs", "retour"),
94 | errors: settings.getErrors("statisticsRateLimitMs"),
95 | }) }}
96 |
97 | {% endnamespace %}
98 |
99 |
--------------------------------------------------------------------------------
/src/models/Stats.php:
--------------------------------------------------------------------------------
1 | null],
99 | [
100 | [
101 | 'redirectSrcUrl',
102 | 'userAgent',
103 | 'exceptionMessage',
104 | 'exceptionFilePath',
105 | ],
106 | DbStringValidator::class,
107 | 'max' => 255,
108 | ],
109 | [
110 | [
111 | 'redirectSrcUrl',
112 | 'userAgent',
113 | 'exceptionMessage',
114 | 'exceptionFilePath',
115 | ],
116 | 'string',
117 | ],
118 | [
119 | [
120 | 'redirectSrcUrl',
121 | 'userAgent',
122 | 'exceptionMessage',
123 | 'exceptionFilePath',
124 | ],
125 | 'default',
126 | 'value' => '',
127 | ],
128 | ['exceptionFileLine', 'integer'],
129 | ['exceptionFileLine', 'default', 'value' => 0],
130 | ['referrerUrl', DbStringValidator::class, 'max' => 2000],
131 | ['referrerUrl', 'string'],
132 | ['referrerUrl', 'default', 'value' => ''],
133 | ['remoteIp', DbStringValidator::class, 'max' => 45],
134 | ['remoteIp', 'string'],
135 | ['remoteIp', 'default', 'value' => ''],
136 | ['hitCount', 'integer'],
137 | ['hitCount', 'default', 'value' => 0],
138 | ['hitLastTime', 'safe'],
139 | ['handledByRetour', 'integer', 'min' => 0, 'max' => 1],
140 | ['handledByRetour', 'default', 'value' => 0],
141 | ];
142 | }
143 |
144 | /**
145 | * @return array
146 | */
147 | public function behaviors(): array
148 | {
149 | return [
150 | 'typecast' => [
151 | 'class' => AttributeTypecastBehavior::class,
152 | // 'attributeTypes' will be composed automatically according to `rules()`
153 | ],
154 | ];
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/src/templates/settings/_includes/advanced.twig:
--------------------------------------------------------------------------------
1 | {# @var craft \craft\web\twig\variables\CraftVariable #}
2 | {#
3 | /**
4 | * Retour plugin for Craft CMS
5 | *
6 | * Retour Settings index.twig
7 | *
8 | * @author nystudio107
9 | * @copyright Copyright (c) 2018 nystudio107
10 | * @link https://nystudio107.com/
11 | * @package Retour
12 | * @since 3.0.0
13 | */
14 | #}
15 |
16 | {% import "_includes/forms" as forms %}
17 | {% from "retour/_includes/macros.twig" import configWarning %}
18 |
19 |
20 | {% namespace "settings" %}
21 | {{ forms.lightswitchField({
22 | label: "Enable API Access"|t("retour"),
23 | instructions: "Determines whether the Retour API endpoint should be enabled for anonymous frontend access."|t("retour"),
24 | id: "enableApiEndpoint",
25 | name: "enableApiEndpoint",
26 | on: settings.enableApiEndpoint,
27 | warning: configWarning("enableApiEndpoint", "retour"),
28 | errors: settings.getErrors("enableApiEndpoint"),
29 | }) }}
30 |
31 | {{ forms.lightswitchField({
32 | label: "Resolve Craft Sites"|t("retour"),
33 | instructions: "Should Craft sites factor into determining redirect destination URLs."|t("retour"),
34 | id: "resolveCraftSites",
35 | name: "resolveCraftSites",
36 | on: settings.resolveCraftSites,
37 | warning: configWarning("resolveCraftSites", "retour"),
38 | errors: settings.getErrors("resolveCraftSites"),
39 | }) }}
40 |
41 | {{ forms.editableTableField({
42 | label: "Exclude Patterns"|t("retour"),
43 | instructions: "[Regular expressions](https://regexr.com/) to match URIs that should be excluded from Retour."|t("retour"),
44 | id: 'excludePatterns',
45 | name: 'excludePatterns',
46 | required: false,
47 | allowAdd: true,
48 | allowDelete: true,
49 | allowReorder: true,
50 | defaultValues: {
51 | pattern: "",
52 | },
53 | cols: {
54 | pattern: {
55 | heading: "RegEx pattern to exclude"|t("retour"),
56 | type: "singleline",
57 | width: "100%",
58 | code: true,
59 | },
60 | },
61 | rows: settings.excludePatterns,
62 | errors: settings.getErrors("excludePatterns"),
63 | }) }}
64 |
65 | {{ forms.editableTableField({
66 | label: "Additional Headers"|t("retour"),
67 | instructions: "Additional headers to add to the redirected request"|t("retour"),
68 | id: 'additionalHeaders',
69 | name: 'additionalHeaders',
70 | allowAdd: true,
71 | allowDelete: true,
72 | allowReorder: true,
73 | required: false,
74 | defaultValues: {
75 | name: "",
76 | value: "",
77 | },
78 | cols: {
79 | name: {
80 | heading: "Header Name"|t("retour"),
81 | type: "singleline",
82 | width: "50%",
83 | code: true,
84 | },
85 | value: {
86 | heading: "Header Value"|t("retour"),
87 | type: "singleline",
88 | width: "50%",
89 | code: true,
90 | },
91 | },
92 | rows: settings.additionalHeaders,
93 | errors: settings.getErrors("additionalHeaders"),
94 | }) }}
95 |
96 | {{ forms.textField({
97 | label: "CSV Delimiter"|t("retour"),
98 | instructions: "The delimiter between data column values for importing CSV files (normally `,`)."|t("retour"),
99 | id: "csvColumnDelimiter",
100 | name: "csvColumnDelimiter",
101 | size: 1,
102 | maxlength: 1,
103 | value: settings.csvColumnDelimiter,
104 | warning: configWarning("csvColumnDelimiter", "retour"),
105 | errors: settings.getErrors("csvColumnDelimiter"),
106 | }) }}
107 |
108 | {% endnamespace %}
109 |
110 |
--------------------------------------------------------------------------------
/src/gql/resolvers/RetourResolver.php:
--------------------------------------------------------------------------------
1 | uri;
41 | $siteId = $source->siteId;
42 | } else {
43 | // Otherwise, use the passed in arguments, or defaults
44 | $uri = $arguments['uri'] ?? '/';
45 | $siteId = $arguments['siteId'] ?? null;
46 | if (isset($arguments['site'])) {
47 | $site = Craft::$app->getSites()->getSiteByHandle($arguments['site']);
48 | if ($site !== null) {
49 | $siteId = $site->id;
50 | }
51 | }
52 | }
53 | $uri = trim($uri === '/' ? '__home__' : $uri);
54 |
55 | $redirect = null;
56 |
57 | // Strip the query string if `alwaysStripQueryString` is set
58 | if (Retour::$settings->alwaysStripQueryString) {
59 | $uri = UrlHelper::stripQueryString($uri);
60 | }
61 |
62 | if (!Retour::$plugin->redirects->excludeUri($uri)) {
63 | $redirect = Retour::$plugin->redirects->findRedirectMatch($uri, $uri, $siteId);
64 |
65 | if ($redirect === null && Craft::$app->getElements()->getElementByUri(trim($uri, '/'), $siteId) === null) {
66 | // Set the `site` virtual field
67 | $redirect['site'] = null;
68 | $redirect['siteId'] = $siteId;
69 | if (isset($redirect['siteId']) && (int)$redirect['siteId'] !== 0) {
70 | $site = Craft::$app->getSites()->getSiteById((int)$redirect['siteId']);
71 | if ($site !== null) {
72 | $redirect['site'] = $site->handle;
73 | }
74 | }
75 | // Increment the stats
76 | Retour::$plugin->statistics->incrementStatistics($uri, false, $siteId);
77 | }
78 | }
79 | if ($redirect !== null && isset($redirect['redirectDestUrl']) && $redirect['redirectSrcMatch'] === 'pathonly' && Retour::$settings->resolveCraftSites) {
80 | $dest = $redirect['redirectDestUrl'];
81 | $path = $redirect['redirectDestUrl'];
82 | // Combine the URL and path together, merging them as appropriate
83 | try {
84 | if (!UrlHelper::isAbsoluteUrl($dest) && !UrlHelper::pathHasSitePrefix($path)) {
85 | $dest = UrlHelper::siteUrl('/', null, null, $siteId);
86 | $dest = parse_url($dest, PHP_URL_PATH);
87 | $dest = UrlHelper::mergeUrlWithPath($dest, $path);
88 | }
89 | } catch (Throwable $e) {
90 | // That's ok
91 | }
92 | $redirect['redirectDestUrl'] = $dest;
93 | }
94 |
95 | return $redirect;
96 | }
97 |
98 | /**
99 | * Return all static redirects for a site.
100 | *
101 | * @param $source
102 | * @param array $arguments
103 | * @param $context
104 | * @param ResolveInfo $resolveInfo
105 | * @return array
106 | * @throws SiteNotFoundException
107 | */
108 | public static function resolveAll($source, array $arguments, $context, ResolveInfo $resolveInfo)
109 | {
110 | $siteId = $arguments['siteId'] ?? Craft::$app->getSites()->getCurrentSite()->id;
111 |
112 | $redirects = Retour::$plugin->redirects->getAllStaticRedirects(null, $siteId);
113 |
114 | return $redirects;
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/src/gql/interfaces/RetourInterface.php:
--------------------------------------------------------------------------------
1 | static::getName(),
50 | 'fields' => self::class . '::getFieldDefinitions',
51 | 'description' => 'This is the interface implemented by Retour.',
52 | 'resolveType' => function(array $value) {
53 | return GqlEntityRegistry::getEntity(RetourGenerator::getName());
54 | },
55 | ]));
56 | RetourGenerator::generateTypes();
57 |
58 | return $type;
59 | }
60 |
61 | /**
62 | * @inheritdoc
63 | */
64 | public static function getName(): string
65 | {
66 | return 'RetourInterface';
67 | }
68 |
69 | /**
70 | * @inheritdoc
71 | */
72 | public static function getFieldDefinitions(): array
73 | {
74 | return array_merge(parent::getFieldDefinitions(), [
75 | 'id' => [
76 | 'name' => 'id',
77 | 'type' => Type::int(),
78 | 'description' => 'The id of the redirect.',
79 | ],
80 | 'site' => [
81 | 'name' => 'site',
82 | 'type' => Type::string(),
83 | 'description' => 'The site handle of the redirect (or null for all sites).',
84 | ],
85 | 'siteId' => [
86 | 'name' => 'siteId',
87 | 'type' => Type::int(),
88 | 'description' => 'The siteId of the redirect (0 or null for all sites).',
89 | ],
90 | 'associatedElementId' => [
91 | 'name' => 'associatedElementId',
92 | 'type' => Type::int(),
93 | 'description' => 'The id of the Element associated with this redirect (unused/vestigial).',
94 | ],
95 | 'enabled' => [
96 | 'name' => 'enabled',
97 | 'type' => Type::boolean(),
98 | 'description' => 'Whether the redirect is enabled or not.',
99 | ],
100 | 'redirectSrcUrl' => [
101 | 'name' => 'redirectSrcUrl',
102 | 'type' => Type::string(),
103 | 'description' => 'The unparsed URL pattern that Retour should match.',
104 | ],
105 | 'redirectSrcUrlParsed' => [
106 | 'name' => 'redirectSrcUrlParsed',
107 | 'type' => Type::string(),
108 | 'description' => 'The parsed URL pattern that Retour should match.',
109 | ],
110 | 'redirectSrcMatch' => [
111 | 'name' => 'redirectSrcMatch',
112 | 'type' => Type::string(),
113 | 'description' => 'Should the legacy URL be matched by path or by full URL?',
114 | ],
115 | 'redirectMatchType' => [
116 | 'name' => 'redirectMatchType',
117 | 'type' => Type::string(),
118 | 'description' => 'Whether an `exactmatch` or `regexmatch` should be used when matching the URL.',
119 | ],
120 | 'redirectDestUrl' => [
121 | 'name' => 'redirectDestUrl',
122 | 'type' => Type::string(),
123 | 'description' => 'The URL that should be redirected to.',
124 | ],
125 | 'redirectHttpCode' => [
126 | 'name' => 'redirectHttpCode',
127 | 'type' => Type::int(),
128 | 'description' => 'The http status code that should be used for the redirect.',
129 | ],
130 | 'hitCount' => [
131 | 'name' => 'hitCount',
132 | 'type' => Type::int(),
133 | 'description' => 'The number of times this redirect has been hit.',
134 | ],
135 | 'hitLastTime' => [
136 | 'name' => 'hitLastTime',
137 | 'type' => Type::string(),
138 | 'description' => 'A datetime string of when this redirect was last hit.',
139 | ],
140 | ]);
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/src/controllers/SettingsController.php:
--------------------------------------------------------------------------------
1 | pluginName;
66 | $templateTitle = Craft::t('retour', 'Settings');
67 | $view = Craft::$app->getView();
68 | // Asset bundle
69 | try {
70 | $view->registerAssetBundle(RetourAsset::class);
71 | } catch (InvalidConfigException $e) {
72 | Craft::error($e->getMessage(), __METHOD__);
73 | }
74 | $variables['baseAssetsUrl'] = Craft::$app->assetManager->getPublishedUrl(
75 | '@nystudio107/retour/web/assets/dist',
76 | true
77 | );
78 | // Basic variables
79 | $variables['fullPageForm'] = true;
80 | $variables['docsUrl'] = self::DOCUMENTATION_URL;
81 | $variables['pluginName'] = $pluginName;
82 | $variables['title'] = $templateTitle;
83 | $variables['crumbs'] = [
84 | [
85 | 'label' => $pluginName,
86 | 'url' => UrlHelper::cpUrl('retour'),
87 | ],
88 | [
89 | 'label' => $templateTitle,
90 | 'url' => UrlHelper::cpUrl('retour/settings'),
91 | ],
92 | ];
93 | $variables['docTitle'] = "{$pluginName} - {$templateTitle}";
94 | $variables['selectedSubnavItem'] = 'settings';
95 | $variables['settings'] = $settings;
96 |
97 | // Render the template
98 | return $this->renderTemplate('retour/settings', $variables);
99 | }
100 |
101 | /**
102 | * Saves a plugin’s settings.
103 | *
104 | * @return Response|null
105 | * @throws NotFoundHttpException if the requested plugin cannot be found
106 | * @throws BadRequestHttpException
107 | * @throws MissingComponentException
108 | * @throws ForbiddenHttpException
109 | */
110 | public function actionSavePluginSettings(): ?Response
111 | {
112 | PermissionHelper::controllerPermissionCheck('retour:settings');
113 | $this->requirePostRequest();
114 | $pluginHandle = Craft::$app->getRequest()->getRequiredBodyParam('pluginHandle');
115 | $settings = Craft::$app->getRequest()->getBodyParam('settings', []);
116 | $plugin = Craft::$app->getPlugins()->getPlugin($pluginHandle);
117 |
118 | if ($plugin === null) {
119 | throw new NotFoundHttpException('Plugin not found');
120 | }
121 |
122 | if (!is_array($settings['additionalHeaders'])) {
123 | $settings['additionalHeaders'] = [];
124 | }
125 | if (!is_array($settings['excludePatterns'])) {
126 | $settings['excludePatterns'] = [];
127 | }
128 | if (!Craft::$app->getPlugins()->savePluginSettings($plugin, $settings)) {
129 | Craft::$app->getSession()->setError(Craft::t('app', "Couldn't save plugin settings."));
130 |
131 | // Send the plugin back to the template
132 | /** @var UrlManager $urlManager */
133 | $urlManager = Craft::$app->getUrlManager();
134 | $urlManager->setRouteParams([
135 | 'plugin' => $plugin,
136 | ]);
137 |
138 | return null;
139 | }
140 |
141 | Retour::$plugin->clearAllCaches();
142 | Craft::$app->getSession()->setNotice(Craft::t('app', 'Plugin settings saved.'));
143 |
144 | return $this->redirectToPostedUrl();
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/src/templates/dashboard/index.twig:
--------------------------------------------------------------------------------
1 | {# @var craft \craft\web\twig\variables\CraftVariable #}
2 | {#
3 | /**
4 | * Retour plugin for Craft CMS
5 | *
6 | * Retour Dashboard index.twig
7 | *
8 | * @author nystudio107
9 | * @copyright Copyright (c) 2018 nystudio107
10 | * @link https://nystudio107.com/
11 | * @package Retour
12 | * @since 3.0.0
13 | */
14 | #}
15 |
16 | {% requirePermission "retour:dashboard" %}
17 |
18 | {% extends "retour/_layouts/retour-cp.twig" %}
19 |
20 | {% import "_includes/forms" as forms %}
21 |
22 | {% do view.registerTranslations('retour', [
23 | '404 File Not Found URL',
24 | 'Last Referrer URL',
25 | 'Remote IP',
26 | 'Hits',
27 | 'Last Hit',
28 | 'Handled',
29 | 'Search for:',
30 | 'Reset',
31 | 'Displaying',
32 | 'to',
33 | 'of',
34 | 'items',
35 | 'Per-page:',
36 | 'Delete',
37 | 'statistic',
38 | 'statistics',
39 | ]) %}
40 |
41 | {% block contextMenu %}
42 | {% include "retour/_includes/sites-menu.twig" %}
43 | {% endblock %}
44 |
45 | {% block actionButton %}
46 |
54 | {% endblock %}
55 |
56 | {% block content %}
57 |
58 |
59 | {% if showWelcome %}
60 |
61 |
62 |
63 |
65 |
Thanks for using Retour!
66 |
67 | Retour allows you to intelligently redirect legacy URLs, so that you don't lose SEO value when
68 | rebuilding & restructuring a website.
69 |
70 |
71 | Retour was entirely rewritten for Craft CMS, and was designed to be performant.
72 |
73 |
We hope you love it! For more information, please see the documentation .
75 |
76 |
77 | {% endif %}
78 |
79 |
80 |
93 |
94 |
95 |
103 |
104 |
105 |
106 |
107 |
108 |
116 |
117 |
118 |
119 |
120 |
125 |
126 |
127 |
128 | {% endblock %}
129 |
130 | {% block foot %}
131 | {# include our JavaScript modules #}
132 | {{ parent() }}
133 | {% set tagOptions = {
134 | 'depends': [
135 | 'nystudio107\\retour\\assetbundles\\retour\\RetourAsset'
136 | ],
137 | } %}
138 | {{ craft.retour.register('src/js/Dashboard.js', false, tagOptions, tagOptions) }}
139 | {% endblock %}
140 |
--------------------------------------------------------------------------------
/src/helpers/MultiSite.php:
--------------------------------------------------------------------------------
1 | Craft::t(
41 | 'retour',
42 | 'All Sites'
43 | ),
44 | ];
45 | // Enabled sites
46 | $sites = Craft::$app->getSites();
47 | if (Craft::$app->getIsMultiSite()) {
48 | $editableSites = $sites->getEditableSiteIds();
49 | foreach ($sites->getAllGroups() as $group) {
50 | $groupSites = $sites->getSitesByGroupId($group->id);
51 | $variables['sitesMenu'][$group->name]
52 | = ['optgroup' => $group->name];
53 | foreach ($groupSites as $groupSite) {
54 | if (in_array($groupSite->id, $editableSites, false)) {
55 | $variables['sitesMenu'][$groupSite->id] = $groupSite->name;
56 | }
57 | }
58 | }
59 | }
60 | }
61 |
62 | /**
63 | * @param string $siteHandle
64 | * @param $siteId
65 | * @param $variables
66 | *
67 | * @throws ForbiddenHttpException
68 | */
69 | public static function setMultiSiteVariables($siteHandle, &$siteId, array &$variables): void
70 | {
71 | // Enabled sites
72 | $sites = Craft::$app->getSites();
73 | if (Craft::$app->getIsMultiSite()) {
74 | // Set defaults based on the section settings
75 | $variables['enabledSiteIds'] = [];
76 | $variables['siteIds'] = [];
77 |
78 | foreach ($sites->getEditableSiteIds() as $editableSiteId) {
79 | $variables['enabledSiteIds'][] = $editableSiteId;
80 | $variables['siteIds'][] = $editableSiteId;
81 | }
82 |
83 | // Make sure the $siteId they are trying to edit is in our array of editable sites
84 | if (!in_array($siteId, $variables['enabledSiteIds'], false)) {
85 | if (!empty($variables['enabledSiteIds'])) {
86 | if ($siteId !== 0) {
87 | $siteId = reset($variables['enabledSiteIds']);
88 | }
89 | } else {
90 | self::requirePermission('editSite:' . $siteId);
91 | }
92 | }
93 | }
94 | // Set the currentSiteId and currentSiteHandle
95 | $variables['currentSiteId'] = empty($siteId) ? 0 : $siteId;
96 | $variables['currentSiteHandle'] = empty($siteHandle)
97 | ? Craft::$app->getSites()->currentSite->handle
98 | : $siteHandle;
99 |
100 | // Page title
101 | $variables['showSites'] = (
102 | Craft::$app->getIsMultiSite() &&
103 | count($variables['enabledSiteIds'])
104 | );
105 |
106 | if ($variables['showSites']) {
107 | if ($variables['currentSiteId'] === 0) {
108 | $variables['sitesMenuLabel'] = Craft::t(
109 | 'retour',
110 | 'All Sites'
111 | );
112 | } else {
113 | $variables['sitesMenuLabel'] = Craft::t(
114 | 'site',
115 | $sites->getSiteById((int)$variables['currentSiteId'])->name
116 | );
117 | }
118 | } else {
119 | $variables['currentSiteId'] = 0;
120 | $variables['sitesMenuLabel'] = '';
121 | }
122 | }
123 |
124 | /**
125 | * @param string $permissionName
126 | *
127 | * @throws ForbiddenHttpException
128 | */
129 | public static function requirePermission(string $permissionName): void
130 | {
131 | if (!Craft::$app->getUser()->checkPermission($permissionName)) {
132 | throw new ForbiddenHttpException('User is not permitted to perform this action');
133 | }
134 | }
135 |
136 | /**
137 | * Return a siteId from a siteHandle
138 | *
139 | * @param ?string $siteHandle
140 | *
141 | * @return int|null
142 | * @throws NotFoundHttpException
143 | */
144 | public static function getSiteIdFromHandle(?string $siteHandle): ?int
145 | {
146 | // Get the site to edit
147 | if ($siteHandle !== null) {
148 | $site = Craft::$app->getSites()->getSiteByHandle($siteHandle);
149 | if (!$site) {
150 | throw new NotFoundHttpException('Invalid site handle: ' . $siteHandle);
151 | }
152 | $siteId = $site->id;
153 | } else {
154 | $siteId = 0;
155 | }
156 |
157 | return $siteId;
158 | }
159 |
160 | // Protected Static Methods
161 | // =========================================================================
162 | }
163 |
--------------------------------------------------------------------------------
/src/helpers/UrlHelper.php:
--------------------------------------------------------------------------------
1 | getSites()->getAllSites();
89 | foreach ($sites as $site) {
90 | $sitePath = parse_url($site->baseUrl, PHP_URL_PATH);
91 | if (!empty($sitePath)) {
92 | // Normalizes a URI path by trimming leading/ trailing slashes and removing double slashes
93 | $sitePath = '/' . preg_replace('/\/\/+/', '/', trim($sitePath, '/'));
94 | }
95 | // See if the path begins with a site path prefix
96 | if ($sitePath !== '/' && str_starts_with($path, $sitePath)) {
97 | return true;
98 | }
99 | }
100 |
101 | return false;
102 | }
103 |
104 | /**
105 | * Merge the $url and $path together, combining any overlapping path segments
106 | *
107 | * @param ?string $url
108 | * @param ?string $path
109 | * @return string
110 | */
111 | public static function mergeUrlWithPath(?string $url, ?string $path): string
112 | {
113 | $overlap = 0;
114 | $url = $url ?? '';
115 | $path = $path ?? '';
116 | $url = rtrim($url, '/') . '/';
117 | $path = '/' . ltrim($path, '/');
118 | $urlOffset = strlen($url);
119 | $pathLength = strlen($path);
120 | $pathOffset = 0;
121 | while ($urlOffset > 0 && $pathOffset < $pathLength) {
122 | $urlOffset--;
123 | $pathOffset++;
124 | if (str_starts_with($path, substr($url, $urlOffset, $pathOffset))) {
125 | $overlap = $pathOffset;
126 | }
127 | }
128 |
129 | return rtrim($url, '/') . '/' . ltrim(substr($path, $overlap), '/');
130 | }
131 |
132 | /**
133 | * Return a sanitized URL
134 | *
135 | * @param string $url
136 | *
137 | * @return string
138 | */
139 | public static function sanitizeUrl(string $url): string
140 | {
141 | $originalUrl = $url;
142 | // HTML decode the entities, then strip out any tags
143 | $url = html_entity_decode($url, ENT_NOQUOTES, 'UTF-8');
144 | $url = urldecode($url);
145 | $decodedUrl = $url;
146 | $url = strip_tags($url);
147 | // Remove any Twig tags that somehow are present in the incoming URL
148 | /** @noinspection CallableParameterUseCaseInTypeContextInspection */
149 | $url = preg_replace('/{.*}/', '', $url);
150 | // Remove any linebreaks that may be errantly in the URL
151 | $url = (string)str_replace([
152 | PHP_EOL,
153 | "\r",
154 | "\n",
155 | ]
156 | , '', $url
157 | );
158 | // If the URL didn't have anything stripped from it, us the original encoded URL
159 | if ($url === $decodedUrl) {
160 | $url = $originalUrl;
161 | }
162 | return $url;
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/src/models/Redirects.php:
--------------------------------------------------------------------------------
1 | redirectSrcUrl, 'http') === 0) {
100 | $this->redirectSrcMatch = 'fullurl';
101 | }
102 | }
103 |
104 | /**
105 | * @inheritdoc
106 | */
107 | public function rules(): array
108 | {
109 | return [
110 | ['id', 'integer'],
111 | ['siteId', 'integer'],
112 | ['siteId', 'default', 'value' => null],
113 | ['associatedElementId', 'default', 'value' => 0],
114 | ['associatedElementId', 'integer'],
115 | ['enabled', 'boolean'],
116 | ['redirectSrcMatch', 'string'],
117 | ['redirectSrcMatch', 'in', 'range' => ['pathonly', 'fullurl']],
118 | ['redirectSrcMatch', 'default', 'value' => 'pathonly'],
119 | ['redirectSrcMatch', DbStringValidator::class, 'max' => 32],
120 | ['redirectMatchType', 'string'],
121 | ['redirectMatchType', 'in', 'range' => ['exactmatch', 'regexmatch']],
122 | ['redirectMatchType', 'default', 'value' => 'exactmatch'],
123 | ['redirectMatchType', DbStringValidator::class, 'max' => 32],
124 | [
125 | [
126 | 'redirectSrcUrl',
127 | 'redirectSrcUrlParsed',
128 | 'redirectDestUrl',
129 | ],
130 | 'default',
131 | 'value' => '',
132 | ],
133 | ['redirectSrcUrlParsed', ParsedUriValidator::class, 'source' => 'redirectSrcUrl'],
134 | [
135 | [
136 | 'redirectSrcUrl',
137 | 'redirectSrcUrlParsed',
138 | 'redirectDestUrl',
139 | ],
140 | UriValidator::class,
141 | ],
142 | [
143 | [
144 | 'redirectSrcUrl',
145 | 'redirectSrcUrlParsed',
146 | 'redirectMatchType',
147 | 'redirectDestUrl',
148 | ],
149 | DbStringValidator::class,
150 | 'max' => 255,
151 | ],
152 | [
153 | [
154 | 'redirectSrcUrl',
155 | 'redirectSrcUrlParsed',
156 | 'redirectDestUrl',
157 | ],
158 | 'string',
159 | ],
160 | ['redirectHttpCode', 'integer'],
161 | ['redirectHttpCode', 'default', 'value' => 301],
162 | ['redirectHttpCode', 'in', 'range' => [301, 302, 307, 308, 410]],
163 | ['hitCount', 'default', 'value' => 0],
164 | ['hitCount', 'integer'],
165 | ['hitLastTime', 'safe'],
166 | ];
167 | }
168 |
169 | /**
170 | * @return array
171 | */
172 | public function behaviors(): array
173 | {
174 | return [
175 | 'typecast' => [
176 | 'class' => AttributeTypecastBehavior::class,
177 | // 'attributeTypes' will be composed automatically according to `rules()`
178 | ],
179 | ];
180 | }
181 | }
182 |
--------------------------------------------------------------------------------
/src/controllers/ChartsController.php:
--------------------------------------------------------------------------------
1 | getDb();
72 | if ($db->getIsMysql()) {
73 | // Query the db
74 | $query = (new Query())
75 | ->from('{{%retour_stats}}')
76 | ->select([
77 | "DATE_FORMAT(hitLastTime, '%Y-%m-%d') AS date_formatted",
78 | 'COUNT(redirectSrcUrl) AS cnt',
79 | 'COUNT(handledByRetour = 1 or null) as handled_cnt',
80 | ])
81 | ->where("hitLastTime >= ( CURDATE() - INTERVAL '{$days}' DAY )");
82 | if ((int)$siteId !== 0) {
83 | $query->andWhere(['siteId' => $siteId]);
84 | }
85 | $query
86 | ->orderBy('date_formatted ASC')
87 | ->groupBy('date_formatted');
88 | $stats = $query->all();
89 | }
90 | if ($db->getIsPgsql()) {
91 | // Query the db
92 | $query = (new Query())
93 | ->from('{{%retour_stats}}')
94 | ->select([
95 | "to_char(\"hitLastTime\", 'yyyy-mm-dd') AS date_formatted",
96 | "COUNT(\"redirectSrcUrl\") AS cnt",
97 | "COUNT(CASE WHEN \"handledByRetour\" = true THEN 1 END) as handled_cnt",
98 | ])
99 | ->where("\"hitLastTime\" >= ( CURRENT_TIMESTAMP - INTERVAL '{$days} days' )");
100 | if ((int)$siteId !== 0) {
101 | $query->andWhere(['siteId' => $siteId]);
102 | }
103 | $query
104 | ->orderBy('date_formatted ASC')
105 | ->groupBy('date_formatted');
106 | $stats = $query->all();
107 | }
108 | if ($stats) {
109 | $data[] = [
110 | 'name' => '404 hits',
111 | 'data' => array_merge(['0'], ArrayHelper::getColumn($stats, 'cnt')),
112 | 'labels' => array_merge(['-'], ArrayHelper::getColumn($stats, 'date_formatted')),
113 | ];
114 | $data[] = [
115 | 'name' => 'Handled 404 hits',
116 | 'data' => array_merge(['0'], ArrayHelper::getColumn($stats, 'handled_cnt')),
117 | 'labels' => array_merge(['-'], ArrayHelper::getColumn($stats, 'date_formatted')),
118 | ];
119 | }
120 |
121 | return $this->asJson($data);
122 | }
123 |
124 | /**
125 | * The Dashboard chart
126 | *
127 | * @param int $days
128 | *
129 | * @return Response
130 | */
131 | public function actionWidget(int $days = 1): Response
132 | {
133 | $data = [];
134 | // Different dbs do it different ways
135 | $stats = null;
136 | $handledStats = null;
137 | $db = Craft::$app->getDb();
138 | if ($db->getIsMysql()) {
139 | // Query the db
140 | $stats = (new Query())
141 | ->from('{{%retour_stats}}')
142 | ->where("hitLastTime >= ( CURDATE() - INTERVAL '{$days}' DAY )")
143 | ->count();
144 | $handledStats = (new Query())
145 | ->from('{{%retour_stats}}')
146 | ->where("hitLastTime >= ( CURDATE() - INTERVAL '{$days}' DAY )")
147 | ->andWhere('handledByRetour is TRUE')
148 | ->count();
149 | }
150 | if ($db->getIsPgsql()) {
151 | // Query the db
152 | $stats = (new Query())
153 | ->from('{{%retour_stats}}')
154 | ->where("\"hitLastTime\" >= ( CURRENT_TIMESTAMP - INTERVAL '{$days} days' )")
155 | ->count();
156 | $handledStats = (new Query())
157 | ->from('{{%retour_stats}}')
158 | ->where("\"hitLastTime\" >= ( CURRENT_TIMESTAMP - INTERVAL '{$days} days' )")
159 | ->andWhere('"handledByRetour" = TRUE')
160 | ->count();
161 | }
162 | if ($stats) {
163 | $data = [
164 | (int)$stats,
165 | (int)$handledStats,
166 | ];
167 | }
168 |
169 | return $this->asJson($data);
170 | }
171 |
172 | // Protected Methods
173 | // =========================================================================
174 | }
175 |
--------------------------------------------------------------------------------
/src/models/StaticRedirects.php:
--------------------------------------------------------------------------------
1 | redirectSrcUrl, 'http') === 0) {
105 | $this->redirectSrcMatch = 'fullurl';
106 | }
107 | }
108 |
109 | /**
110 | * @inheritdoc
111 | */
112 | public function rules(): array
113 | {
114 | return [
115 | ['id', 'integer'],
116 | ['siteId', 'integer'],
117 | ['siteId', 'default', 'value' => null],
118 | ['associatedElementId', 'default', 'value' => 0],
119 | ['associatedElementId', 'integer'],
120 | ['enabled', 'boolean'],
121 | ['redirectSrcMatch', 'string'],
122 | ['redirectSrcMatch', 'in', 'range' => ['pathonly', 'fullurl']],
123 | ['redirectSrcMatch', 'default', 'value' => 'pathonly'],
124 | ['redirectSrcMatch', DbStringValidator::class, 'max' => 32],
125 | ['redirectMatchType', 'string'],
126 | ['redirectMatchType', 'in', 'range' => ['exactmatch', 'regexmatch']],
127 | ['redirectMatchType', 'default', 'value' => 'exactmatch'],
128 | ['redirectMatchType', DbStringValidator::class, 'max' => 32],
129 | [
130 | [
131 | 'redirectSrcUrl',
132 | 'redirectSrcUrlParsed',
133 | 'redirectDestUrl',
134 | ],
135 | 'default',
136 | 'value' => '',
137 | ],
138 | ['redirectSrcUrlParsed', ParsedUriValidator::class, 'source' => 'redirectSrcUrl'],
139 | [
140 | [
141 | 'redirectSrcUrl',
142 | 'redirectSrcUrlParsed',
143 | 'redirectDestUrl',
144 | ],
145 | UriValidator::class,
146 | ],
147 | [
148 | [
149 | 'redirectSrcUrl',
150 | 'redirectSrcUrlParsed',
151 | 'redirectDestUrl',
152 | ],
153 | DbStringValidator::class,
154 | 'max' => 255,
155 | ],
156 | [
157 | [
158 | 'redirectSrcUrl',
159 | 'redirectSrcUrlParsed',
160 | 'redirectDestUrl',
161 | ],
162 | 'string',
163 | ],
164 | ['redirectHttpCode', 'integer'],
165 | ['redirectHttpCode', 'in', 'range' => [301, 302, 307, 308, 410]],
166 | ['redirectHttpCode', 'default', 'value' => 301],
167 | ['priority', 'default', 'value' => 5],
168 | ['priority', 'integer'],
169 | ['priority', 'in', 'range' => [1, 2, 3, 4, 5, 6, 7, 8, 9]],
170 | ['hitCount', 'default', 'value' => 0],
171 | ['hitCount', 'integer'],
172 | ['hitLastTime', 'safe'],
173 | ];
174 | }
175 |
176 | /**
177 | * @return array
178 | */
179 | public function behaviors(): array
180 | {
181 | return [
182 | 'typecast' => [
183 | 'class' => AttributeTypecastBehavior::class,
184 | // 'attributeTypes' will be composed automatically according to `rules()`
185 | ],
186 | ];
187 | }
188 | }
189 |
--------------------------------------------------------------------------------
/src/controllers/StatisticsController.php:
--------------------------------------------------------------------------------
1 | statistics->trimStatistics();
63 | // Get the site to edit
64 | $siteId = MultiSiteHelper::getSiteIdFromHandle($siteHandle);
65 | $pluginName = Retour::$settings->pluginName;
66 | $templateTitle = Craft::t('retour', 'Dashboard');
67 | $view = Craft::$app->getView();
68 | // Asset bundle
69 | try {
70 | $view->registerAssetBundle(RetourDashboardAsset::class);
71 | } catch (InvalidConfigException $e) {
72 | Craft::error($e->getMessage(), __METHOD__);
73 | }
74 | $variables['baseAssetsUrl'] = Craft::$app->assetManager->getPublishedUrl(
75 | '@nystudio107/retour/web/assets/dist',
76 | true
77 | );
78 | // Enabled sites
79 | MultiSiteHelper::setMultiSiteVariables($siteHandle, $siteId, $variables);
80 | $variables['controllerHandle'] = 'dashboard';
81 |
82 | // Basic variables
83 | $variables['fullPageForm'] = false;
84 | $variables['docsUrl'] = self::DOCUMENTATION_URL;
85 | $variables['pluginName'] = $pluginName;
86 | $variables['title'] = $templateTitle;
87 | $siteHandleUri = Craft::$app->isMultiSite ? '/' . $siteHandle : '';
88 | $variables['crumbs'] = [
89 | [
90 | 'label' => $pluginName,
91 | 'url' => UrlHelper::cpUrl('retour'),
92 | ],
93 | [
94 | 'label' => $templateTitle,
95 | 'url' => UrlHelper::cpUrl('retour/dashboard' . $siteHandleUri),
96 | ],
97 | ];
98 | $variables['docTitle'] = "{$pluginName} - {$templateTitle}";
99 | $variables['selectedSubnavItem'] = 'dashboard';
100 | $variables['showWelcome'] = $showWelcome;
101 | $variables['settings'] = Retour::$settings;
102 |
103 | // Render the template
104 | return $this->renderTemplate('retour/dashboard/index', $variables);
105 | }
106 |
107 | /**
108 | * @return Response
109 | * @throws ForbiddenHttpException
110 | */
111 | public function actionClearStatistics(): Response
112 | {
113 | PermissionHelper::controllerPermissionCheck('retour:dashboard');
114 | $error = Retour::$plugin->statistics->clearStatistics();
115 | Craft::info(
116 | Craft::t(
117 | 'retour',
118 | 'Retour statistics cleared: {error}',
119 | ['error' => $error]
120 | ),
121 | __METHOD__
122 | );
123 | Retour::$plugin->clearAllCaches();
124 | try {
125 | Craft::$app->getSession()->setNotice(Craft::t('retour', 'Retour statistics cleared.'));
126 | } catch (MissingComponentException $e) {
127 | Craft::error($e->getMessage(), __METHOD__);
128 | }
129 |
130 | return $this->redirect('retour/dashboard');
131 | }
132 |
133 | /**
134 | * @return ?Response
135 | * @throws MissingComponentException
136 | * @throws BadRequestHttpException
137 | * @throws ForbiddenHttpException
138 | */
139 | public function actionDeleteStatistics(): ?Response
140 | {
141 | PermissionHelper::controllerPermissionCheck('retour:dashboard');
142 | $request = Craft::$app->getRequest();
143 | $statisticIds = $request->getRequiredBodyParam('statisticIds');
144 | $stickyError = false;
145 | foreach ($statisticIds as $statisticId) {
146 | if (Retour::$plugin->statistics->deleteStatisticById($statisticId) === 0) {
147 | $stickyError = true;
148 | }
149 | }
150 | Craft::info(
151 | Craft::t(
152 | 'retour',
153 | 'Retour statistics deleted: {error}',
154 | ['error' => $stickyError]
155 | ),
156 | __METHOD__
157 | );
158 |
159 | Retour::$plugin->clearAllCaches();
160 | // Handle any cumulative errors
161 | if (!$stickyError) {
162 | // Clear the caches and continue on
163 | Craft::$app->getSession()->setNotice(Craft::t('retour', 'Retour statistics deleted.'));
164 |
165 | return $this->redirect('retour/dashboard');
166 | }
167 | Craft::$app->getSession()->setError(Craft::t('retour', "Couldn't delete statistic."));
168 |
169 | return null;
170 | }
171 | }
172 |
--------------------------------------------------------------------------------
/src/fields/ShortLink.php:
--------------------------------------------------------------------------------
1 | getView()->renderTemplate(
81 | 'retour/_components/fields/ShortLink_input',
82 | [
83 | 'name' => $this->handle,
84 | 'value' => $value,
85 | 'field' => $this,
86 | ]
87 | );
88 | }
89 |
90 | /**
91 | * @inheritdoc
92 | */
93 | public function getSettingsHtml(): string
94 | {
95 | return Craft::$app->getView()->renderTemplate('retour/_components/fields/ShortLink_settings',
96 | [
97 | 'field' => $this,
98 | ]);
99 | }
100 |
101 | /**
102 | * @inheritdoc
103 | */
104 | public function getPreviewHtml($value, ElementInterface $element): string
105 | {
106 | $decoded = Json::decodeIfJson($value);
107 | if (is_array($decoded)) {
108 | $value = $decoded['legacyUrl'] ?? '';
109 | }
110 | // Render the preview template
111 | return Craft::$app->getView()->renderTemplate(
112 | 'retour/_components/fields/ShortLink_preview',
113 | [
114 | 'name' => $this->handle,
115 | 'value' => $value,
116 | 'field' => $this,
117 | ]
118 | );
119 | }
120 |
121 | /**
122 | * @inheritdoc
123 | */
124 | public function afterElementSave(ElementInterface $element, bool $isNew): void
125 | {
126 | if (!self::$allowShortLinkUpdates || $element->getIsDraft() || !$element->getSite()->hasUrls) {
127 | return;
128 | }
129 |
130 | $value = $element->getFieldValue($this->handle);
131 | // Return for propagating elements
132 | if ($this->redirectSrcMatch === 'pathonly') {
133 | $parentElement = ElementHelper::rootElement($element);
134 | if ($this->translationMethod === Field::TRANSLATION_METHOD_NONE && ($element->propagating || $parentElement->propagating)) {
135 | return;
136 | }
137 | } elseif (!empty($value) && !StringHelper::startsWith($value, 'http')) {
138 | $value = UrlHelper::siteUrl($value, null, null, $element->siteId);
139 | }
140 |
141 | $parentElement = ElementHelper::rootElement($element);
142 | RetourPlugin::$plugin->redirects->removeElementRedirect($parentElement, false);
143 |
144 | if (!empty($value)) {
145 | $redirectSrcMatch = $this->redirectSrcMatch;
146 |
147 | RetourPlugin::$plugin->redirects->enableElementRedirect($parentElement, $value, $redirectSrcMatch, $this->redirectHttpCode);
148 | }
149 |
150 | parent::afterElementSave($element, $isNew);
151 | }
152 |
153 | /**
154 | * @inheritdoc
155 | */
156 | public function afterElementDelete(ElementInterface $element): void
157 | {
158 | if (!$element->getIsCanonical()) {
159 | return;
160 | }
161 |
162 | RetourPlugin::$plugin->redirects->removeElementRedirect($element, true, true);
163 | parent::afterElementDelete($element);
164 | }
165 |
166 | /**
167 | * @inheritdoc
168 | */
169 | public function getElementValidationRules(): array
170 | {
171 | return [
172 | [
173 | function(ElementInterface $element) {
174 | $value = $element->getFieldValue($this->handle);
175 | $redirect = RetourPlugin::$plugin->getRedirects()->getRedirectByRedirectSrcUrl($value);
176 | // Handle drafts
177 | $element = $element->getCanonical();
178 | if ($redirect && isset($redirect['associatedElementId'])) {
179 | if ($redirect['associatedElementId'] == 0) {
180 | $element->addError($this->handle, Craft::t('retour', 'A Retour redirect with this Legacy URL already exists.'));
181 | } elseif ($redirect['associatedElementId'] !== $element->id) {
182 | $element->addError($this->handle, Craft::t('retour', 'A Short Link with this URL already exists.'));
183 | }
184 | }
185 | },
186 | ],
187 | ];
188 | }
189 | }
190 |
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/retour-exeLeENM.css:
--------------------------------------------------------------------------------
1 | /*! tailwindcss v4.1.12 | MIT License | https://tailwindcss.com */@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-leading:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}:root,:host{--color-gray-600:oklch(44.6% .03 256.802);--spacing:.25rem;--leading-tight:1.25}.visible{visibility:visible}.fixed{position:fixed}.relative{position:relative}.float-right{float:right}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.mx-2{margin-inline:calc(var(--spacing)*2)}.mr-2{margin-right:calc(var(--spacing)*2)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.ml-2{margin-left:calc(var(--spacing)*2)}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.table{display:table}.w-full{width:100%}.flex-shrink{flex-shrink:1}.flex-grow{flex-grow:1}.transform{transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.items-start{align-items:flex-start}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.border-solid{--tw-border-style:solid;border-style:solid}.py-3{padding-block:calc(var(--spacing)*3)}.py-4{padding-block:calc(var(--spacing)*4)}.py-5{padding-block:calc(var(--spacing)*5)}.pt-3{padding-top:calc(var(--spacing)*3)}.pl-3{padding-left:calc(var(--spacing)*3)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.align-bottom{vertical-align:bottom}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.text-gray-600{color:var(--color-gray-600)}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.filter{filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}div.retour-button-container{margin-right:10px;display:inline-block}table.vuetable{table-layout:fixed;width:100%;overflow:hidden}.retour-menubtn-asc:before{content:"upangle";font-weight:700}.retour-menubtn-desc:before{content:"downangle";font-weight:700}table.retour-dashboard th.vuetable-th-checkbox-id{width:3%!important}table.retour-dashboard th.vuetable-th-redirectSrcUrl{width:34%!important}th.vuetable-th-referrerUrl{width:20%!important}th.vuetable-th-remoteIp{width:14%!important}th.vuetable-th-hitCount{text-align:right!important;width:8%!important}th.vuetable-th-hitLastTime{width:16%!important}th.vuetable-th-handledByRetour{width:12%!important}th.vuetable-th-addLink,table.retour-redirects th.vuetable-th-checkbox-id{width:3%!important}table.retour-redirects th.vuetable-th-redirectSrcUrl{width:28%!important}th.vuetable-th-redirectDestUrl{width:22%!important}th.vuetable-th-redirectMatchType{width:10%!important}th.vuetable-th-priority{text-align:right!important;width:8%!important}th.vuetable-th-siteName{width:10%!important}th.vuetable-th-redirectHttpCode{width:7%!important}td.text-center,th.text-center{text-align:center!important}td.text-right,th.text-right{text-align:right!important}.retour-import-list-group-item{cursor:move;background-color:#fff;border:1px solid #ddd;margin-bottom:-1px;padding:10px 15px;display:block;position:relative}.retour-import-field-group-item{cursor:default;background-color:#fff;border:1px solid #ddd;margin-bottom:-1px;padding:10px 15px;display:block;position:relative}.retour-import-arrow-item{padding:11px 15px;display:block;position:relative}.retour-import-drag-area{min-height:100px}.retour-empty-item{background:repeating-linear-gradient(-55deg,#ddd,#ddd 10px,#eee 10px,#eee 20px)}.retour-inputfile{opacity:0;z-index:-1;width:.1px;height:.1px;position:absolute;overflow:hidden}.retour-reset:before{padding-bottom:4px}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-leading{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}
2 |
--------------------------------------------------------------------------------
/src/models/Settings.php:
--------------------------------------------------------------------------------
1 | 'Retour'],
157 | [
158 | [
159 | 'createUriChangeRedirects',
160 | 'alwaysStripQueryString',
161 | 'preserveQueryString',
162 | 'stripQueryStringFromStats',
163 | 'recordRemoteIp',
164 | 'enableApiEndpoint',
165 | 'resolveCraftSites',
166 | ],
167 | 'boolean',
168 | ],
169 | ['uriChangeRedirectSrcMatch', 'default', 'value' => 'pathonly'],
170 | ['uriChangeRedirectSrcMatch', 'string'],
171 | ['uriChangeRedirectSrcMatch', 'in', 'range' => [
172 | 'pathonly',
173 | 'fullurl',
174 | ]],
175 | ['staticRedirectDisplayLimit', 'integer', 'min' => 1],
176 | ['staticRedirectDisplayLimit', 'default', 'value' => 100],
177 | ['dynamicRedirectDisplayLimit', 'integer', 'min' => 1],
178 | ['dynamicRedirectDisplayLimit', 'default', 'value' => 100],
179 | ['statsStoredLimit', 'integer', 'min' => 1],
180 | ['statsStoredLimit', 'default', 'value' => 1000],
181 | ['refreshIntervalSecs', 'integer', 'min' => 0],
182 | ['refreshIntervalSecs', 'default', 'value' => 3],
183 | ['statsDisplayLimit', 'integer', 'min' => 1],
184 | ['statsDisplayLimit', 'default', 'value' => 1000],
185 | [
186 | [
187 | 'excludePatterns',
188 | 'additionalHeaders',
189 | ],
190 | ArrayValidator::class,
191 | ],
192 | ];
193 | }
194 |
195 | /**
196 | * @return array
197 | */
198 | public function behaviors(): array
199 | {
200 | return [
201 | 'parser' => [
202 | 'class' => EnvAttributeParserBehavior::class,
203 | 'attributes' => [
204 | ],
205 | ],
206 | ];
207 | }
208 | }
209 |
--------------------------------------------------------------------------------
/src/web/assets/dist/assets/shortlinks-Jev4OoWa.js:
--------------------------------------------------------------------------------
1 | import{s as c,V as d,a as u,b as f,c as h,p,d as m}from"./purify.es-DP_WP6YH.js";import{L as g}from"./LegacyUrl-CeiszSHN.js";import{n as l}from"./_plugin-vue2_normalizer-BFq-tfv7.js";const v=[{name:"__checkbox",titleClass:"center aligned",dataClass:"center aligned"},{name:"__component:legacy-url",sortField:"redirectSrcUrl",title:Craft.t("retour","Short Link"),titleClass:"center",dataClass:"center"},{name:"__component:element-url",sortField:"elementTitle",title:Craft.t("retour","Redirect To"),titleClass:"center",dataClass:"center"},{name:"redirectMatchType",sortField:"redirectMatchType",title:Craft.t("retour","Match Type"),titleClass:"text-left",dataClass:"text-left",callback:"matchFormatter"},{name:"siteName",sortField:"siteId",title:Craft.t("retour","Sites"),titleClass:"text-left",dataClass:"text-left"},{name:"redirectHttpCode",sortField:"redirectHttpCode",title:Craft.t("retour","Status"),titleClass:"text-left",dataClass:"text-left"},{name:"hitCount",sortField:"hitCount",title:Craft.t("retour","Hits"),titleClass:"text-right",dataClass:"text-right"},{name:"hitLastTime",sortField:"hitLastTime",title:Craft.t("retour","Last Hit"),titleClass:"center",dataClass:"center"}],b={props:{rowData:{type:Object,required:!0},rowIndex:{type:Number,default:0}},computed:{linkTitle:function(){let a="";return a+=this.rowData.redirectDestUrl,a}}};var C=function(){var e=this,t=e._self._c;return t("div",[t("a",{staticClass:"go",attrs:{href:e.rowData.elementCpUrl,title:e.linkTitle}},[t("span",{staticStyle:{"white-space":"nowrap"}},[t("span"),t("span",{staticStyle:{"white-space":"normal"}},[e._v(e._s(e.rowData.elementTitle))])])])])},_=[],T=l(b,C,_,!1,null,null);const x=T.exports;Vue.component("LegacyUrl",g);Vue.component("ElementUrl",x);const $={components:{vuetable:h,"vuetable-pagination":f,"vuetable-pagination-info":u,"vuetable-filter-bar":d},mixins:[c],props:{siteId:{type:Number,default:0},apiUrl:{type:String,default:""}},data:function(){return{moreParams:{siteId:this.siteId,shortLinks:!0},css:{tableClass:"data fullwidth retour-redirects",ascendingIcon:"icon retour-menubtn-asc",descendingIcon:"icon retour-menubtn-desc"},sortOrder:[{field:"hitCount",sortField:"hitCount",direction:"desc"}],fields:v,numSelected:0,selectedIds:[],filterText:"",perPage:20,stringPerPage:Craft.t("retour","Per-page:"),stringDelete:Craft.t("retour","Delete"),stringRedirect:Craft.t("retour","redirect"),stringRedirects:Craft.t("retour","redirects")}},computed:{csrfTokenName:function(){return window.Craft.csrfTokenName},csrfTokenValue:function(){return window.Craft.csrfTokenValue}},watch:{perPage:function(){this.$events.fire("refresh-table",this.$refs.vuetable)}},mounted(){this.$events.$on("filter-set",a=>this.onFilterSet(a)),this.$events.$on("filter-reset",()=>this.onFilterReset()),this.$refs.vuetable.$on("vuetable:checkbox-toggled",(a,e)=>this.onCheckboxToggled(a,e)),this.$refs.vuetable.$on("vuetable:checkbox-toggled-all",a=>this.onCheckboxToggled(a,null))},methods:{getSaveStateConfig(){return{cacheKey:"retour-shortlinks-state-v2-"+Craft.username+Craft.siteId,ignoreProperties:["numSelected","selectedIds","moreParams"]}},onFilterSet(a){this.filterText=a,this.moreParams={filter:this.filterText,siteId:this.siteId,shortLinks:!0},this.$events.fire("refresh-table",this.$refs.vuetable)},onFilterReset(){this.filterText="",this.moreParams={filter:this.filterText,siteId:this.siteId,shortLinks:!0},this.$events.fire("refresh-table",this.$refs.vuetable)},onPaginationData(a){this.$refs.paginationTop.setPaginationData(a),this.$refs.paginationInfoTop.setPaginationData(a),this.$refs.pagination.setPaginationData(a),this.$refs.paginationInfo.setPaginationData(a)},onChangePage(a){this.$refs.vuetable.changePage(a)},onCheckboxToggled(){this.numSelected=0,this.selectedIds=[],this.$refs.vuetable!==void 0&&this.$refs.vuetable.selectedTo!==void 0&&(this.numSelected=this.$refs.vuetable.selectedTo.length,this.selectedIds=this.$refs.vuetable.selectedTo)},matchFormatter(a){let e="Pluing Match";switch(a){case"exactmatch":e="Exact Match";break;case"regexmatch":e="RegEx Match";break}return e},urlFormatter(a){if(a==="")return"";a=p.sanitize(a),a=encodeURI(a);let e=a;return!new RegExp("^(?:[a-z]+:)?//","i").test(e)&&!e.includes("$")&&(e=Craft.getSiteUrl(e)),`
2 | ${a}
3 | `},deleteRedirectFormatter(a){return a===""?"":`
4 |
5 | `}}};var P=function(){var e=this,t=e._self._c;return t("div",[t("div",{directives:[{name:"show",rawName:"v-show",value:e.numSelected!==0,expression:"numSelected !== 0"}]},[t("form",{attrs:{"accept-charset":"UTF-8",method:"post"}},[t("input",{attrs:{name:e.csrfTokenName,type:"hidden"},domProps:{value:e.csrfTokenValue}}),e._l(e.selectedIds,function(r){return t("input",{key:r,attrs:{name:"redirectIds[]",type:"hidden"},domProps:{value:r}})}),t("label",{staticClass:"text-gray-600"},[e._v(e._s(e.numSelected)+" "),e.numSelected===1?t("span",[e._v(e._s(e.stringRedirect))]):e._e(),e.numSelected!==1?t("span",[e._v(e._s(e.stringRedirects))]):e._e(),e._v(":")]),t("div",{staticClass:"btngroup inline"},[t("div",{staticClass:"ml-2 btn menubtn",attrs:{"data-icon":"settings"}}),t("div",{staticClass:"menu",attrs:{"data-align":"right"}},[t("ul",[t("li",[t("a",{staticClass:"formsubmit",attrs:{"data-action":"retour/redirects/delete-shortlinks"}},[e._v(e._s(e.stringDelete))])])])])])],2)]),t("vuetable-filter-bar",{directives:[{name:"show",rawName:"v-show",value:e.numSelected===0,expression:"numSelected === 0"}],attrs:{"initial-filter-text":e.filterText}}),t("div",{staticClass:"vuetable-pagination clearafter"},[t("vuetable-pagination-info",{ref:"paginationInfoTop"}),t("div",{staticClass:"floated left vuetable-pagination-info py-3"},[t("div",{staticClass:"inline pl-3 text-gray-600"},[e._v(" "+e._s(e.stringPerPage)+" ")]),t("div",{staticClass:"inline pl-3 text-gray-600"},[t("div",{staticClass:"select"},[t("select",{directives:[{name:"model",rawName:"v-model",value:e.perPage,expression:"perPage"}],staticClass:"fieldtoggle",attrs:{"data-target-prefix":"per-page-",name:"perPage"},on:{change:function(r){var n=Array.prototype.filter.call(r.target.options,function(s){return s.selected}).map(function(s){var o="_value"in s?s._value:s.value;return o});e.perPage=r.target.multiple?n:n[0]}}},[t("option",{attrs:{selected:"",value:"20"}},[e._v(" 20 ")]),t("option",{attrs:{value:"50"}},[e._v(" 50 ")]),t("option",{attrs:{value:"100"}},[e._v(" 100 ")]),t("option",{attrs:{value:"500"}},[e._v(" 500 ")])])])])]),t("vuetable-pagination",{ref:"paginationTop",on:{"vuetable-pagination:change-page":e.onChangePage}})],1),t("vuetable",{ref:"vuetable",attrs:{"api-url":e.apiUrl,"append-params":e.moreParams,css:e.css,fields:e.fields,"per-page":e.perPage,"sort-order":e.sortOrder},on:{"vuetable:pagination-data":e.onPaginationData}}),t("div",{staticClass:"vuetable-pagination clearafter border-solid"},[t("vuetable-pagination-info",{ref:"paginationInfo"}),t("vuetable-pagination",{ref:"pagination",on:{"vuetable-pagination:change-page":e.onChangePage}})],1)],1)},k=[],w=l($,P,k,!1,null,null);const y=w.exports,i=window.Vue;i.use(m);new i({el:"#cp-nav-content",components:{"shortlinks-table":y},mounted(){this.$events.$on("refresh-table",a=>this.onTableRefresh(a))},methods:{onTableRefresh(a){i.nextTick(()=>a.refresh())}}});
6 | //# sourceMappingURL=shortlinks-Jev4OoWa.js.map
7 |
--------------------------------------------------------------------------------