├── .editorconfig
├── .gitattributes
├── .github
└── workflows
│ ├── build.yml
│ └── release.yml
├── .gitignore
├── .gitmodules
├── CHANGELOG.md
├── Geta.NotFoundHandler.sln
├── LICENSE
├── NuGet.config
├── README.md
├── images
├── icon.png
└── redirects.png
├── pack.ps1
├── src
├── Geta.NotFoundHandler.Admin
│ ├── Areas
│ │ └── GetaNotFoundHandlerAdmin
│ │ │ └── Pages
│ │ │ ├── Administer.cshtml
│ │ │ ├── Administer.cshtml.cs
│ │ │ ├── Base
│ │ │ └── AbstractSortablePageModel.cs
│ │ │ ├── Components
│ │ │ ├── Card
│ │ │ │ ├── CardType.cs
│ │ │ │ ├── CardTypeExtensions.cs
│ │ │ │ ├── CardViewComponent.cs
│ │ │ │ ├── CardViewModel.cs
│ │ │ │ └── Default.cshtml
│ │ │ ├── CheckboxReadonly
│ │ │ │ ├── CheckboxReadonlyViewComponent.cs
│ │ │ │ ├── CheckboxReadonlyViewModel.cs
│ │ │ │ └── Default.cshtml
│ │ │ ├── Pager
│ │ │ │ ├── Default.cshtml
│ │ │ │ ├── PagerViewComponent.cs
│ │ │ │ └── PagerViewModel.cs
│ │ │ └── SortableHeaderCell
│ │ │ │ ├── Default.cshtml
│ │ │ │ ├── SortableHeaderCellViewComponent.cs
│ │ │ │ └── SortableHeaderCellViewModel.cs
│ │ │ ├── Deleted.cshtml
│ │ │ ├── Deleted.cshtml.cs
│ │ │ ├── Extensions
│ │ │ ├── EnumerableExtensions.cs
│ │ │ └── ModelStateExtensions.cs
│ │ │ ├── Ignored.cshtml
│ │ │ ├── Ignored.cshtml.cs
│ │ │ ├── Index.cshtml
│ │ │ ├── Index.cshtml.cs
│ │ │ ├── Infrastructure
│ │ │ └── IFormFileExtensions.cs
│ │ │ ├── Models
│ │ │ ├── DeletedRedirectModel.cs
│ │ │ ├── Paging.cs
│ │ │ ├── RedirectModel.cs
│ │ │ ├── RedirectsRequest.cs
│ │ │ ├── RegexRedirectModel.cs
│ │ │ ├── SortDirection.cs
│ │ │ └── SuggestionRedirectModel.cs
│ │ │ ├── Regex.cshtml
│ │ │ ├── Regex.cshtml.cs
│ │ │ ├── Shared
│ │ │ ├── _Layout.cshtml
│ │ │ └── _Logo.cshtml
│ │ │ ├── Suggestions.cshtml
│ │ │ ├── Suggestions.cshtml.cs
│ │ │ ├── _ViewImports.cshtml
│ │ │ └── _ViewStart.cshtml
│ ├── Geta.NotFoundHandler.Admin.csproj
│ └── wwwroot
│ │ └── GetaNotFoundHandlerAdmin
│ │ ├── css
│ │ └── dashboard.css
│ │ └── js
│ │ └── dashboard.js
├── Geta.NotFoundHandler.Optimizely.Commerce
│ ├── AutomaticRedirects
│ │ ├── CommerceContentKeyProvider.cs
│ │ ├── CommerceContentLinkProvider.cs
│ │ ├── EntryContentUrlProvider.cs
│ │ └── NodeContentUrlProvider.cs
│ ├── Geta.NotFoundHandler.Optimizely.Commerce.csproj
│ └── Infrastructure
│ │ └── Configuration
│ │ └── OptimizelyNotFoundHandlerOptionsExtensions.cs
├── Geta.NotFoundHandler.Optimizely
│ ├── Constants.cs
│ ├── ContainerController.cs
│ ├── Core
│ │ ├── AutomaticRedirects
│ │ │ ├── ChannelMovedContentRegistratorQueue.cs
│ │ │ ├── CmsContentKeyProvider.cs
│ │ │ ├── CmsContentLinkProvider.cs
│ │ │ ├── CmsContentUrlProvider.cs
│ │ │ ├── ContentKeyGenerator.cs
│ │ │ ├── ContentKeyResult.cs
│ │ │ ├── ContentLinkLoader.cs
│ │ │ ├── ContentUrlHistory.cs
│ │ │ ├── ContentUrlHistoryEvents.cs
│ │ │ ├── ContentUrlIndexer.cs
│ │ │ ├── ContentUrlLoader.cs
│ │ │ ├── DefaultAutomaticRedirectsService.cs
│ │ │ ├── IAutomaticRedirectsService.cs
│ │ │ ├── IContentKeyProvider.cs
│ │ │ ├── IContentLinkProvider.cs
│ │ │ ├── IContentUrlHistoryLoader.cs
│ │ │ ├── IContentUrlProvider.cs
│ │ │ ├── IMovedContentRegistratorQueue.cs
│ │ │ ├── IndexContentUrlsJob.cs
│ │ │ ├── MovedContentRegistratorBackgroundService.cs
│ │ │ ├── RedirectBuilder.cs
│ │ │ ├── RegisterMovedContentRedirectsJob.cs
│ │ │ ├── TypedUrl.cs
│ │ │ └── UrlType.cs
│ │ ├── Events
│ │ │ └── OptimizelySyncEvents.cs
│ │ └── Suggestions
│ │ │ └── Jobs
│ │ │ └── SuggestionsCleanupJob.cs
│ ├── Data
│ │ └── SqlContentUrlHistoryRepository.cs
│ ├── Geta.NotFoundHandler.Optimizely.Views
│ │ └── Views
│ │ │ ├── Container
│ │ │ └── Index.cshtml
│ │ │ ├── Shared
│ │ │ └── _ShellLayout.cshtml
│ │ │ └── _ViewStart.cshtml
│ ├── Geta.NotFoundHandler.Optimizely.csproj
│ ├── Infrastructure
│ │ ├── Configuration
│ │ │ ├── OptimizelyNotFoundHandlerOptions.cs
│ │ │ └── ServiceCollectionExtensions.cs
│ │ ├── Initialization
│ │ │ ├── ApplicationBuilderExtensions.cs
│ │ │ └── Upgrader.cs
│ │ └── JobStatusLogger.cs
│ ├── MenuProvider.cs
│ ├── module.config
│ └── msbuild
│ │ └── CopyModule.targets
├── Geta.NotFoundHandler.Web
│ ├── .gitignore
│ ├── Geta.NotFoundHandler.Web.csproj
│ ├── Program.cs
│ ├── Properties
│ │ └── launchSettings.json
│ ├── Startup.cs
│ └── appsettings.json
└── Geta.NotFoundHandler
│ ├── Core
│ ├── INotFoundHandler.cs
│ ├── Providers
│ │ └── RegexRedirects
│ │ │ ├── DefaultRegexRedirectsService.cs
│ │ │ ├── IRegexRedirectCache.cs
│ │ │ ├── IRegexRedirectsService.cs
│ │ │ ├── MemoryCacheRegexRedirectRepository.cs
│ │ │ ├── RegexRedirect.cs
│ │ │ ├── RegexRedirectFactory.cs
│ │ │ └── RegexRedirectNotFoundHandler.cs
│ ├── Redirects
│ │ ├── CustomRedirect.cs
│ │ ├── CustomRedirectCollection.cs
│ │ ├── CustomRedirectEqualityComparer.cs
│ │ ├── CustomRedirectHandler.cs
│ │ ├── DefaultRedirectsService.cs
│ │ ├── IRedirectHandler.cs
│ │ ├── IRedirectsParser.cs
│ │ ├── IRedirectsService.cs
│ │ ├── RedirectState.cs
│ │ ├── RedirectType.cs
│ │ ├── RedirectsCsvParser.cs
│ │ ├── RedirectsEvents.cs
│ │ ├── RedirectsInitializer.cs
│ │ ├── RedirectsTxtParser.cs
│ │ └── RedirectsXmlParser.cs
│ ├── RequestHandler.cs
│ ├── RewriteResult.cs
│ ├── ScheduledJobs
│ │ ├── ApplicationBuilderExtensions.cs
│ │ ├── ServiceCollectionExtensions.cs
│ │ └── Suggestions
│ │ │ └── SuggestionsCleanupJob.cs
│ └── Suggestions
│ │ ├── DefaultSuggestionService.cs
│ │ ├── IRequestLogger.cs
│ │ ├── ISuggestionService.cs
│ │ ├── ISuggestionsCleanupService.cs
│ │ ├── LogEvent.cs
│ │ ├── RefererSummary.cs
│ │ ├── RequestLogger.cs
│ │ ├── SuggestionRedirect.cs
│ │ ├── SuggestionSummary.cs
│ │ ├── SuggestionsCleanupOptions.cs
│ │ └── SuggestionsCleanupService.cs
│ ├── Data
│ ├── IDataExecutor.cs
│ ├── IRedirectLoader.cs
│ ├── IRegexRedirectLoader.cs
│ ├── IRegexRedirectOrderUpdater.cs
│ ├── IRepository.cs
│ ├── ISuggestionLoader.cs
│ ├── ISuggestionRepository.cs
│ ├── SqlDataExecutor.cs
│ ├── SqlRedirectRepository.cs
│ ├── SqlRegexRedirectRepository.cs
│ └── SqlSuggestionRepository.cs
│ ├── Geta.NotFoundHandler.csproj
│ ├── Infrastructure
│ ├── Configuration
│ │ ├── FileNotFoundMode.cs
│ │ ├── LoggerMode.cs
│ │ ├── NotFoundHandlerOptions.cs
│ │ └── ServiceCollectionExtensions.cs
│ ├── Constants.cs
│ ├── Initialization
│ │ ├── ApplicationBuilderExtensions.cs
│ │ ├── NotFoundHandlerMiddleware.cs
│ │ └── Upgrader.cs
│ ├── Processing
│ │ └── SpanExtensions.cs
│ └── Web
│ │ └── HttpContextExtensions.cs
│ └── Models
│ └── CsvImportModel.cs
└── tests
├── Geta.NotFoundHandler.Optimizely.Tests
├── AutomaticRedirects
│ ├── CmsContentKeyProviderTests.cs
│ ├── CmsContentLinkProviderTests.cs
│ ├── CmsContentUrlProviderTests.cs
│ ├── CommerceContentKeyProviderTests.cs
│ ├── CommerceContentLinkProviderTests.cs
│ ├── ContentKeyGeneratorTests.cs
│ ├── ContentLinkLoaderTests.cs
│ ├── ContentUrlIndexerTests.cs
│ ├── ContentUrlLoaderTests.cs
│ ├── EntryContentUrlProviderTests.cs
│ ├── NodeContentUrlProviderTests.cs
│ └── RedirectBuilderTests.cs
└── Geta.NotFoundHandler.Optimizely.Tests.csproj
└── Geta.NotFoundHandler.Tests
├── Base
└── Uris.cs
├── CustomRedirectCollectionTests.cs
├── ExternalHandlerTests.cs
├── Geta.NotFoundHandler.Tests.csproj
├── Hosting
├── RedirectServerBuilder.cs
└── TestServerBuilder.cs
├── Providers
└── RegexNotFoundHandlerTests.cs
└── RequestHandlerTests.cs
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 | on:
3 | push:
4 | branches:
5 | - master
6 | pull_request:
7 | types: [opened, synchronize, reopened]
8 | concurrency:
9 | group: "build-${{ github.ref_name }}"
10 | cancel-in-progress: false
11 |
12 | jobs:
13 | build:
14 | name: Build
15 | runs-on: windows-latest
16 | steps:
17 | - name: Set up JDK 11
18 | uses: actions/setup-java@v4
19 | with:
20 | java-version: 11
21 | distribution: 'temurin'
22 |
23 | - name: Checkout repository with submodules
24 | uses: actions/checkout@v4
25 | with:
26 | fetch-depth: 0
27 | submodules: recursive
28 | - name: Cache NuGet packages
29 | uses: actions/cache@v3
30 | with:
31 | path: ~/.nuget/packages
32 | key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
33 | restore-keys: |
34 | ${{ runner.os }}-nuget-
35 | - name: Cache SonarCloud packages
36 | uses: actions/cache@v4
37 | with:
38 | path: ~\sonar\cache
39 | key: ${{ runner.os }}-sonar
40 | restore-keys: ${{ runner.os }}-sonar
41 |
42 | - name: Cache SonarCloud scanner
43 | id: cache-sonar-scanner
44 | uses: actions/cache@v4
45 | with:
46 | path: .\.sonar\scanner
47 | key: ${{ runner.os }}-sonar-scanner
48 | restore-keys: ${{ runner.os }}-sonar-scanner
49 |
50 | - name: Install SonarCloud scanner
51 | if: steps.cache-sonar-scanner.outputs.cache-hit != 'true'
52 | shell: powershell
53 | run: |
54 | New-Item -Path .\.sonar\scanner -ItemType Directory
55 | dotnet tool update dotnet-sonarscanner --tool-path .\.sonar\scanner
56 |
57 | - name: Restore dependencies
58 | run: dotnet restore
59 |
60 | - name: Build and analyze
61 | env:
62 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
63 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
64 | shell: powershell
65 | run: |
66 | .\.sonar\scanner\dotnet-sonarscanner begin /k:"Geta_${{ github.event.repository.name }}" /o:"geta" /d:sonar.login="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.opencover.reportsPaths=**/**/coverage.opencover.xml
67 | dotnet build
68 | dotnet test --filter Category!=Integration /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:CoverletOutput=coverage
69 | .\.sonar\scanner\dotnet-sonarscanner end /d:sonar.login="${{ secrets.SONAR_TOKEN }}"
70 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 | on:
3 | push:
4 | tags:
5 | - "v[0-9]+.[0-9]+.[0-9]+"
6 | concurrency:
7 | group: "release-${{ github.ref_name }}"
8 | cancel-in-progress: false
9 | permissions:
10 | contents: write
11 | packages: write
12 |
13 | jobs:
14 | build:
15 | name: Release
16 | runs-on: windows-latest
17 | steps:
18 | - name: Checkout
19 | uses: actions/checkout@v4
20 | with:
21 | submodules: recursive
22 | - name: Cache NuGet packages
23 | uses: actions/cache@v3
24 | with:
25 | path: ~/.nuget/packages
26 | key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
27 | restore-keys: |
28 | ${{ runner.os }}-nuget-
29 | - name: Verify commit exists in origin/master
30 | run: |
31 | git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/*
32 | git branch --remote --contains | grep origin/master
33 | - name: Set VERSION variable from tag
34 | run: |
35 | $version = ("${{github.ref_name}}").Remove(0, 1)
36 | echo "VERSION=$version" >> $env:GITHUB_ENV
37 | - name: Release
38 | run: |
39 | echo "${env:VERSION}"
40 | dotnet build --configuration Release /p:Version=${{env.VERSION}}
41 | - name: Test
42 | run: dotnet test --configuration Release /p:Version=${{env.VERSION}} --no-build
43 | - name: Pack
44 | run: dotnet pack --configuration Release /p:Version=${{env.VERSION}} --no-build --output .
45 | - name: Push
46 | run: |
47 | dotnet nuget push Geta.NotFoundHandler.${{env.VERSION}}.nupkg --source https://nuget.pkg.github.com/Geta/index.json --api-key ${{env.GITHUB_TOKEN}}
48 | dotnet nuget push Geta.NotFoundHandler.Admin.${{env.VERSION}}.nupkg --source https://nuget.pkg.github.com/Geta/index.json --api-key ${{env.GITHUB_TOKEN}}
49 | dotnet nuget push Geta.NotFoundHandler.Optimizely.${{env.VERSION}}.nupkg --source https://nuget.pkg.github.com/Geta/index.json --api-key ${{env.GITHUB_TOKEN}}
50 | dotnet nuget push Geta.NotFoundHandler.Optimizely.Commerce.${{env.VERSION}}.nupkg --source https://nuget.pkg.github.com/Geta/index.json --api-key ${{env.GITHUB_TOKEN}}
51 | env:
52 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
53 | - name: Create GitHub Release with Auto-Generated Notes
54 | run: |
55 | gh release create ${{ github.ref_name }} --generate-notes
56 | env:
57 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
58 | - name: Loop through all .nupkg files in the current directory and upload them to the release
59 | run: |
60 | Get-ChildItem -Filter *.nupkg -Recurse | ForEach-Object {
61 | Write-Host "Uploading file: $($_.Name)"
62 | gh release upload ${{ github.ref_name }} $_.FullName --clobber
63 | }
64 | env:
65 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
66 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 |
4 | # User-specific files
5 | *.suo
6 | *.user
7 | *.userosscache
8 | *.sln.docstates
9 |
10 | # User-specific files (MonoDevelop/Xamarin Studio)
11 | *.userprefs
12 |
13 | # Build results
14 | [Dd]ebug/
15 | [Dd]ebugPublic/
16 | [Rr]elease/
17 | [Rr]eleases/
18 | x64/
19 | x86/
20 | build/
21 | bld/
22 | [Bb]in/
23 | [Oo]bj/
24 | tmp
25 |
26 | # Visual Studo 2015 cache/options directory
27 | .vs/
28 |
29 | # MSTest test Results
30 | [Tt]est[Rr]esult*/
31 | [Bb]uild[Ll]og.*
32 |
33 | # NUNIT
34 | *.VisualState.xml
35 | TestResult.xml
36 |
37 | # Build Results of an ATL Project
38 | [Dd]ebugPS/
39 | [Rr]eleasePS/
40 | dlldata.c
41 |
42 | *_i.c
43 | *_p.c
44 | *_i.h
45 | *.ilk
46 | *.meta
47 | *.obj
48 | *.pch
49 | *.pdb
50 | *.pgc
51 | *.pgd
52 | *.rsp
53 | *.sbr
54 | *.tlb
55 | *.tli
56 | *.tlh
57 | *.tmp
58 | *.tmp_proj
59 | *.log
60 | *.vspscc
61 | *.vssscc
62 | .builds
63 | *.pidb
64 | *.svclog
65 | *.scc
66 |
67 | # Chutzpah Test files
68 | _Chutzpah*
69 |
70 | # Visual C++ cache files
71 | ipch/
72 | *.aps
73 | *.ncb
74 | *.opensdf
75 | *.sdf
76 | *.cachefile
77 |
78 | # Visual Studio profiler
79 | *.psess
80 | *.vsp
81 | *.vspx
82 |
83 | # TFS 2012 Local Workspace
84 | $tf/
85 |
86 | # Guidance Automation Toolkit
87 | *.gpState
88 |
89 | # ReSharper is a .NET coding add-in
90 | _ReSharper*/
91 | *.[Rr]e[Ss]harper
92 | *.DotSettings.user
93 |
94 | # JustCode is a .NET coding addin-in
95 | .JustCode
96 |
97 | # TeamCity is a build add-in
98 | _TeamCity*
99 |
100 | # DotCover is a Code Coverage Tool
101 | *.dotCover
102 |
103 | # NCrunch
104 | _NCrunch_*
105 | .*crunch*.local.xml
106 |
107 | # MightyMoose
108 | *.mm.*
109 | AutoTest.Net/
110 |
111 | # Web workbench (sass)
112 | .sass-cache/
113 |
114 | # Installshield output folder
115 | [Ee]xpress/
116 |
117 | # DocProject is a documentation generator add-in
118 | DocProject/buildhelp/
119 | DocProject/Help/*.HxT
120 | DocProject/Help/*.HxC
121 | DocProject/Help/*.hhc
122 | DocProject/Help/*.hhk
123 | DocProject/Help/*.hhp
124 | DocProject/Help/Html2
125 | DocProject/Help/html
126 |
127 | # Click-Once directory
128 | publish/
129 |
130 | # Publish Web Output
131 | *.[Pp]ublish.xml
132 | *.azurePubxml
133 | # TODO: Comment the next line if you want to checkin your web deploy settings
134 | # but database connection strings (with potential passwords) will be unencrypted
135 | *.pubxml
136 | *.publishproj
137 |
138 | # NuGet Packages
139 | *.nupkg
140 | # The packages folder can be ignored because of Package Restore
141 | **/packages/*
142 | # except build/, which is used as an MSBuild target.
143 | !**/packages/build/
144 | # Uncomment if necessary however generally it will be regenerated when needed
145 | #!**/packages/repositories.config
146 |
147 | # Windows Azure Build Output
148 | csx/
149 | *.build.csdef
150 |
151 | # Windows Store app package directory
152 | AppPackages/
153 |
154 | # Others
155 | *.[Cc]ache
156 | ClientBin/
157 | [Ss]tyle[Cc]op.*
158 | ~$*
159 | *~
160 | *.dbmdl
161 | *.dbproj.schemaview
162 | *.pfx
163 | *.publishsettings
164 | node_modules/
165 | bower_components/
166 |
167 | # RIA/Silverlight projects
168 | Generated_Code/
169 |
170 | # Backup & report files from converting an old project file
171 | # to a newer Visual Studio version. Backup files are not needed,
172 | # because we have git ;-)
173 | _UpgradeReport_Files/
174 | Backup*/
175 | UpgradeLog*.XML
176 | UpgradeLog*.htm
177 |
178 | # SQL Server files
179 | *.mdf
180 | *.ldf
181 |
182 | # Business Intelligence projects
183 | *.rdl.data
184 | *.bim.layout
185 | *.bim_*.settings
186 |
187 | # Microsoft Fakes
188 | FakesAssemblies/
189 |
190 | # Node.js Tools for Visual Studio
191 | .ntvs_analysis.dat
192 |
193 | # Visual Studio 6 build log
194 | *.plg
195 |
196 | # Visual Studio 6 workspace options file
197 | *.opt
198 |
199 | # Rider
200 | */.idea/*
201 | .idea/*
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "sub/geta-foundation-core"]
2 | path = sub/geta-foundation-core
3 | url = https://github.com/Geta/geta-foundation-core
4 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | ## [1.1.0]
6 |
7 | - Refctoring
8 |
9 | ## [1.0.0]
10 |
11 | - Initial release
12 |
--------------------------------------------------------------------------------
/NuGet.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Geta/geta-notfoundhandler/7883e132fec21ad3ac629fd5d79377059175c4dc/images/icon.png
--------------------------------------------------------------------------------
/images/redirects.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Geta/geta-notfoundhandler/7883e132fec21ad3ac629fd5d79377059175c4dc/images/redirects.png
--------------------------------------------------------------------------------
/pack.ps1:
--------------------------------------------------------------------------------
1 | $outputDir = ".\package\"
2 |
3 | dotnet pack --output $outputDir
4 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Base/AbstractSortablePageModel.cs:
--------------------------------------------------------------------------------
1 | using Geta.NotFoundHandler.Admin.Areas.GetaNotFoundHandlerAdmin.Pages.Models;
2 | using Microsoft.AspNetCore.Mvc.RazorPages;
3 |
4 | namespace Geta.NotFoundHandler.Admin.Areas.GetaNotFoundHandlerAdmin.Pages.Base;
5 |
6 | public abstract class AbstractSortablePageModel : PageModel
7 | {
8 | public string SortColumn { get; set; }
9 | public SortDirection? SortDirection { get; set; }
10 |
11 | public void ApplySort(string sortColumn, SortDirection? sortDirection)
12 | {
13 | SortColumn = sortColumn;
14 | SortDirection = sortDirection;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Components/Card/CardType.cs:
--------------------------------------------------------------------------------
1 | namespace Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin.Components.Card
2 | {
3 | public enum CardType
4 | {
5 | Default,
6 | Success,
7 | Warning
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Components/Card/CardTypeExtensions.cs:
--------------------------------------------------------------------------------
1 | namespace Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin.Components.Card
2 | {
3 | public static class CardTypeExtensions
4 | {
5 | public static string GetCssClass(this CardType cardType)
6 | {
7 | switch (cardType)
8 | {
9 | case CardType.Success:
10 | return "text-white bg-success";
11 | case CardType.Warning:
12 | return "text-dark bg-warning";
13 | default:
14 | return string.Empty;
15 | }
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Components/Card/CardViewComponent.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 |
3 | namespace Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin.Components.Card
4 | {
5 | public class CardViewComponent : ViewComponent
6 | {
7 | public IViewComponentResult Invoke(string message, CardType cardType)
8 | {
9 | return View(new CardViewModel { Message = message, CardType = cardType });
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Components/Card/CardViewModel.cs:
--------------------------------------------------------------------------------
1 | namespace Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin.Components.Card
2 | {
3 | public class CardViewModel
4 | {
5 | public string Message { get; set; }
6 | public bool HasMessage => !string.IsNullOrEmpty(Message);
7 | public CardType CardType { get; set; } = CardType.Default;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Components/Card/Default.cshtml:
--------------------------------------------------------------------------------
1 | @using Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin.Components.Card
2 | @model Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin.Components.Card.CardViewModel
3 |
4 |
5 | @if (Model.HasMessage)
6 | {
7 |
8 |
9 | @Model.Message
10 |
11 |
12 | }
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Components/CheckboxReadonly/CheckboxReadonlyViewComponent.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 |
3 | namespace Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin.Components.CheckboxReadonly
4 | {
5 | public class CheckboxReadonlyViewComponent : ViewComponent
6 | {
7 | public IViewComponentResult Invoke(bool isChecked)
8 | {
9 | return View(new CheckboxReadonlyViewModel { IsChecked = isChecked });
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Components/CheckboxReadonly/CheckboxReadonlyViewModel.cs:
--------------------------------------------------------------------------------
1 | namespace Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin.Components.CheckboxReadonly
2 | {
3 | public class CheckboxReadonlyViewModel
4 | {
5 | public bool IsChecked { get; set; }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Components/CheckboxReadonly/Default.cshtml:
--------------------------------------------------------------------------------
1 | @model Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin.Components.CheckboxReadonly.CheckboxReadonlyViewModel
2 |
3 | @if (Model.IsChecked)
4 | {
5 |
6 | }
7 | else
8 | {
9 |
10 | }
11 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Components/Pager/Default.cshtml:
--------------------------------------------------------------------------------
1 | @model Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin.Components.Pager.PagerViewModel
2 |
3 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Components/Pager/PagerViewComponent.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Http;
2 | using Microsoft.AspNetCore.Mvc;
3 | using X.PagedList;
4 |
5 | namespace Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin.Components.Pager
6 | {
7 | public class PagerViewComponent : ViewComponent
8 | {
9 | private readonly IHttpContextAccessor _contextAccessor;
10 |
11 | public PagerViewComponent(IHttpContextAccessor contextAccessor)
12 | {
13 | _contextAccessor = contextAccessor;
14 | }
15 |
16 | public IViewComponentResult Invoke(IPagedList items)
17 | {
18 | var context = _contextAccessor.HttpContext;
19 | return View(new PagerViewModel
20 | {
21 | HasPreviousPage = items.HasPreviousPage,
22 | HasNextPage = items.HasNextPage,
23 | PageNumber = items.PageNumber,
24 | PageCount = items.PageCount,
25 | QueryString = context.Request.QueryString.ToString()
26 | });
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Components/Pager/PagerViewModel.cs:
--------------------------------------------------------------------------------
1 | using System.Web;
2 |
3 | namespace Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin.Components.Pager
4 | {
5 | public class PagerViewModel
6 | {
7 | public bool HasPreviousPage { get; set; }
8 | public bool HasNextPage { get; set; }
9 | public int PageNumber { get; set; }
10 | public int PageCount { get; set; }
11 | public string QueryString { get; set; }
12 |
13 | public string PageUrl(int page)
14 | {
15 | var qs = HttpUtility.ParseQueryString(QueryString);
16 | qs["page"] = page.ToString();
17 | return $"?{qs}";
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Components/SortableHeaderCell/Default.cshtml:
--------------------------------------------------------------------------------
1 | @using Geta.NotFoundHandler.Admin.Areas.GetaNotFoundHandlerAdmin.Pages.Models
2 | @model Geta.NotFoundHandler.Admin.Areas.GetaNotFoundHandlerAdmin.Pages.Components.SortableHeaderCell.SortableHeaderCellViewModel
3 |
4 |
6 | @Model.DisplayName
7 |
8 | @if (Model.IsActive() && Model.GetSortDirection() != null)
9 | {
10 |
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Components/SortableHeaderCell/SortableHeaderCellViewComponent.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Http;
2 | using Microsoft.AspNetCore.Mvc;
3 |
4 | namespace Geta.NotFoundHandler.Admin.Areas.GetaNotFoundHandlerAdmin.Pages.Components.SortableHeaderCell;
5 |
6 | public class SortableHeaderCellViewComponent : ViewComponent
7 | {
8 | private readonly IHttpContextAccessor _contextAccessor;
9 |
10 | public SortableHeaderCellViewComponent(IHttpContextAccessor contextAccessor)
11 | {
12 | _contextAccessor = contextAccessor;
13 | }
14 |
15 | public IViewComponentResult Invoke(string key, string displayName)
16 | {
17 | var context = _contextAccessor.HttpContext;
18 |
19 | return View(new SortableHeaderCellViewModel
20 | {
21 | QueryString = context?.Request.QueryString.ToString(),
22 | Key = key,
23 | DisplayName = displayName
24 | });
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Components/SortableHeaderCell/SortableHeaderCellViewModel.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Web;
3 | using Geta.NotFoundHandler.Admin.Areas.GetaNotFoundHandlerAdmin.Pages.Base;
4 | using Geta.NotFoundHandler.Admin.Areas.GetaNotFoundHandlerAdmin.Pages.Models;
5 |
6 | namespace Geta.NotFoundHandler.Admin.Areas.GetaNotFoundHandlerAdmin.Pages.Components.SortableHeaderCell;
7 |
8 | public class SortableHeaderCellViewModel
9 | {
10 | public string DisplayName { get; set; }
11 | public string Key { get; set; }
12 | public string QueryString { get; set; }
13 |
14 | public SortDirection? GetNextSortDirection()
15 | {
16 | return GetSortDirection() switch
17 | {
18 | null => SortDirection.Ascending,
19 | SortDirection.Ascending => SortDirection.Descending,
20 | SortDirection.Descending => null,
21 | _ => throw new ArgumentOutOfRangeException()
22 | };
23 | }
24 |
25 | public string GetSortColumn()
26 | {
27 | if (GetNextSortDirection() == null)
28 | {
29 | return null;
30 | }
31 |
32 | return Key;
33 | }
34 |
35 | public string GetSortUrl()
36 | {
37 | var qs = HttpUtility
38 | .ParseQueryString(QueryString);
39 |
40 | qs[nameof(AbstractSortablePageModel.SortColumn)] = GetSortColumn();
41 | qs[nameof(AbstractSortablePageModel.SortDirection)] = GetNextSortDirection().ToString();
42 |
43 | return $"?{qs}";
44 | }
45 |
46 | public bool IsActive()
47 | {
48 | var sortColumn = HttpUtility
49 | .ParseQueryString(QueryString)
50 | .Get(nameof(AbstractSortablePageModel.SortColumn));
51 |
52 | return !string.IsNullOrEmpty(sortColumn) && sortColumn == Key;
53 | }
54 |
55 | public SortDirection? GetSortDirection()
56 | {
57 | var sortDirection = HttpUtility
58 | .ParseQueryString(QueryString)
59 | .Get(nameof(AbstractSortablePageModel.SortDirection));
60 |
61 | return Enum.TryParse(sortDirection, out SortDirection sort) ? sort : null;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Deleted.cshtml:
--------------------------------------------------------------------------------
1 | @page "{handler?}"
2 | @using Geta.NotFoundHandler.Admin.Areas.GetaNotFoundHandlerAdmin.Pages.Components.SortableHeaderCell
3 | @using Geta.NotFoundHandler.Core.Providers.RegexRedirects
4 | @using Microsoft.AspNetCore.Mvc.TagHelpers
5 | @model Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin.DeletedModel
6 |
7 | @await Component.InvokeAsync("Card", new { message = Model.Message })
8 |
9 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Deleted.cshtml.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using Geta.NotFoundHandler.Admin.Areas.GetaNotFoundHandlerAdmin.Pages.Base;
4 | using Geta.NotFoundHandler.Admin.Areas.GetaNotFoundHandlerAdmin.Pages.Extensions;
5 | using Geta.NotFoundHandler.Admin.Areas.GetaNotFoundHandlerAdmin.Pages.Models;
6 | using Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin.Models;
7 | using Geta.NotFoundHandler.Core.Redirects;
8 | using Geta.NotFoundHandler.Infrastructure;
9 | using Microsoft.AspNetCore.Authorization;
10 | using Microsoft.AspNetCore.Mvc;
11 | using X.PagedList;
12 |
13 | namespace Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin;
14 |
15 | [Authorize(Constants.PolicyName)]
16 | public class DeletedModel : AbstractSortablePageModel
17 | {
18 | private readonly IRedirectsService _redirectsService;
19 |
20 | public DeletedModel(IRedirectsService redirectsService)
21 | {
22 | _redirectsService = redirectsService;
23 | }
24 |
25 | public string Message { get; set; }
26 |
27 | public IPagedList Items { get; set; } = Enumerable.Empty().ToPagedList();
28 |
29 | [BindProperty]
30 | public DeletedRedirectModel DeletedRedirect { get; set; }
31 |
32 | [BindProperty(SupportsGet = true)]
33 | public Paging Paging { get; set; }
34 |
35 | public void OnGet(string sortColumn, SortDirection? sortDirection)
36 | {
37 | ApplySort(sortColumn, sortDirection);
38 |
39 | Load();
40 | }
41 |
42 | public IActionResult OnPostCreate()
43 | {
44 | if (!ModelState.IsValid)
45 | {
46 | Load();
47 | return Page();
48 | }
49 |
50 | _redirectsService.AddDeletedRedirect(DeletedRedirect.OldUrl);
51 |
52 | return RedirectToPage();
53 | }
54 |
55 | public IActionResult OnPostDelete(string oldUrl)
56 | {
57 | _redirectsService.DeleteByOldUrl(oldUrl);
58 | return RedirectToPage();
59 | }
60 |
61 | private void Load()
62 | {
63 | var items = FindRedirects().ToPagedList(Paging.PageNumber, Paging.PageSize);
64 | Message =
65 | $"There are currently {items.TotalItemCount} URLs that return a Deleted response. This tells crawlers to remove these URLs from their index.";
66 | Items = items;
67 | }
68 |
69 | private IEnumerable FindRedirects()
70 | {
71 | return _redirectsService
72 | .GetDeleted()
73 | .Sort(SortColumn, SortDirection);
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Extensions/EnumerableExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using System.Reflection;
4 | using System.Text.RegularExpressions;
5 | using Geta.NotFoundHandler.Admin.Areas.GetaNotFoundHandlerAdmin.Pages.Models;
6 |
7 | namespace Geta.NotFoundHandler.Admin.Areas.GetaNotFoundHandlerAdmin.Pages.Extensions;
8 |
9 | public static class EnumerableExtensions
10 | {
11 | public static IEnumerable Sort(this IEnumerable list, string propertyName, SortDirection? sortDirection)
12 | {
13 | if (!string.IsNullOrEmpty(propertyName))
14 | {
15 | var prop = typeof(T).GetProperty(propertyName);
16 |
17 | if (prop != null && sortDirection != null)
18 | {
19 | if (sortDirection == SortDirection.Ascending)
20 | {
21 | list = list.OrderBy(x => GetValue(prop, x));
22 | }
23 | else
24 | {
25 | list = list.OrderByDescending(x => GetValue(prop, x));
26 | }
27 | }
28 | }
29 |
30 | return list;
31 | }
32 |
33 | private static object GetValue(PropertyInfo prop, T element)
34 | {
35 | var value = prop.GetValue(element);
36 |
37 | // Value should be IComparable
38 | if (value is Regex regex)
39 | {
40 | return regex.ToString();
41 | }
42 |
43 | return value;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Extensions/ModelStateExtensions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc.ModelBinding;
2 |
3 | namespace Geta.NotFoundHandler.Admin.Areas.GetaNotFoundHandlerAdmin.Pages.Extensions;
4 |
5 | public static class ModelStateExtensions
6 | {
7 | public static ModelStateDictionary RemoveNestedKeys(this ModelStateDictionary source, string prefix)
8 | {
9 | foreach (var key in source.Keys)
10 | {
11 | if (key.StartsWith($"{prefix}."))
12 | {
13 | source.Remove(key);
14 | }
15 | }
16 |
17 | return source;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Ignored.cshtml:
--------------------------------------------------------------------------------
1 | @page "{handler?}"
2 | @using Geta.NotFoundHandler.Core.Redirects
3 | @using Microsoft.AspNetCore.Mvc.TagHelpers
4 | @model Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin.IgnoredModel
5 |
6 | @await Component.InvokeAsync("Card", new { message = Model.Message })
7 |
8 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Ignored.cshtml.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using Geta.NotFoundHandler.Admin.Areas.GetaNotFoundHandlerAdmin.Pages.Base;
4 | using Geta.NotFoundHandler.Admin.Areas.GetaNotFoundHandlerAdmin.Pages.Extensions;
5 | using Geta.NotFoundHandler.Admin.Areas.GetaNotFoundHandlerAdmin.Pages.Models;
6 | using Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin.Models;
7 | using Geta.NotFoundHandler.Core.Redirects;
8 | using Geta.NotFoundHandler.Infrastructure;
9 | using Microsoft.AspNetCore.Authorization;
10 | using Microsoft.AspNetCore.Mvc;
11 | using X.PagedList;
12 |
13 | namespace Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin;
14 |
15 | [Authorize(Constants.PolicyName)]
16 | public class IgnoredModel : AbstractSortablePageModel
17 | {
18 | private readonly IRedirectsService _redirectsService;
19 |
20 | public IgnoredModel(IRedirectsService redirectsService)
21 | {
22 | _redirectsService = redirectsService;
23 | }
24 |
25 | public string Message { get; set; }
26 |
27 | public IPagedList Items { get; set; } = Enumerable.Empty().ToPagedList();
28 |
29 | [BindProperty(SupportsGet = true)]
30 | public Paging Paging { get; set; }
31 |
32 | public void OnGet(string sortColumn, SortDirection? sortDirection)
33 | {
34 | ApplySort(sortColumn, sortDirection);
35 |
36 | Load();
37 | }
38 |
39 | public IActionResult OnPostUnignore(string oldUrl)
40 | {
41 | _redirectsService.DeleteByOldUrl(oldUrl);
42 |
43 | return RedirectToPage();
44 | }
45 |
46 | private void Load()
47 | {
48 | var items = FindRedirects().ToPagedList(Paging.PageNumber, Paging.PageSize);
49 | Message = $"There are currently {items.TotalItemCount} ignored suggestions stored.";
50 | Items = items;
51 | }
52 |
53 | private IEnumerable FindRedirects()
54 | {
55 | return _redirectsService
56 | .GetIgnored()
57 | .Sort(SortColumn, SortDirection);
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Infrastructure/IFormFileExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 | using Microsoft.AspNetCore.Http;
4 |
5 | namespace Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin.Infrastructure
6 | {
7 | public static class IFormFileExtensions
8 | {
9 | public static bool IsXml(this IFormFile file)
10 | {
11 | return FileIsOfType(file, new[] { "text/xml", "application/xml" }, new[] { "xml" });
12 | }
13 |
14 | public static bool IsCsv(this IFormFile file)
15 | {
16 | return FileIsOfType(file, new[] { "text/csv" }, new[] { "csv" });
17 | }
18 |
19 | public static bool IsTxt(this IFormFile file)
20 | {
21 | return FileIsOfType(file, new[] { "text/plain" }, new[] { "txt" });
22 | }
23 |
24 | public static bool FileIsOfType(this IFormFile file, string[] allowedContentTypes, string[] allowedExtensions)
25 | {
26 | var isAllowedContentType = allowedContentTypes.Any(
27 | x => file.ContentType.Equals(x, StringComparison.InvariantCultureIgnoreCase));
28 | if (isAllowedContentType)
29 | {
30 | return true;
31 | }
32 |
33 | return allowedExtensions.Any(x => file.FileName.EndsWith(x, StringComparison.OrdinalIgnoreCase));
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Models/DeletedRedirectModel.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 |
3 | namespace Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin.Models
4 | {
5 | public class DeletedRedirectModel
6 | {
7 | [Required]
8 | public string OldUrl { get; set; }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Models/Paging.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 |
3 | namespace Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin.Models
4 | {
5 | public class Paging
6 | {
7 | public const int DefaultPageSize = 50;
8 |
9 | [FromQuery(Name = "page")]
10 | public int PageNumber { get; set; } = 1;
11 |
12 | [FromQuery(Name = "page-size")]
13 | public int PageSize { get; set; } = DefaultPageSize;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Models/RedirectModel.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.ComponentModel.DataAnnotations;
3 | using Geta.NotFoundHandler.Core.Redirects;
4 |
5 | namespace Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin.Models
6 | {
7 | public class RedirectModel
8 | {
9 | public Guid? Id { get; set; }
10 | [Required]
11 | public string OldUrl { get; set; }
12 | [Required]
13 | public string NewUrl { get; set; }
14 | public bool WildCardSkipAppend { get; set; }
15 | public RedirectType RedirectType { get; set; }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Models/RedirectsRequest.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Geta.NotFoundHandler.Admin.Areas.GetaNotFoundHandlerAdmin.Pages;
4 |
5 | public class RedirectsRequest
6 | {
7 | public string Query { get; set; }
8 | public int? PageNumber { get; set; }
9 | public Guid? Id { get; set; }
10 | }
11 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Models/RegexRedirectModel.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Geta Digital. All rights reserved.
2 | // Licensed under Apache-2.0. See the LICENSE file in the project root for more information
3 |
4 | using System;
5 | using System.ComponentModel.DataAnnotations;
6 |
7 | namespace Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin.Models;
8 |
9 | public class RegexRedirectModel
10 | {
11 | public Guid? Id { get; set; }
12 | [Required]
13 | [Display(Name = "Old URL Regex")]
14 | public string OldUrlRegex { get; set; }
15 | [Required]
16 | [Display(Name = "New URL Format")]
17 | public string NewUrlFormat { get; set; }
18 | [Required]
19 | [Display(Name = "Order Number")]
20 | public int OrderNumber { get; set; }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Models/SortDirection.cs:
--------------------------------------------------------------------------------
1 | namespace Geta.NotFoundHandler.Admin.Areas.GetaNotFoundHandlerAdmin.Pages.Models;
2 |
3 | public enum SortDirection
4 | {
5 | Ascending,
6 | Descending
7 | }
8 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Models/SuggestionRedirectModel.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.ComponentModel.DataAnnotations;
3 | using Geta.NotFoundHandler.Core.Suggestions;
4 |
5 | namespace Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin.Models
6 | {
7 | public class SuggestionRedirectModel
8 | {
9 | [Required]
10 | public string OldUrl { get; set; }
11 | [Required]
12 | public string NewUrl { get; set; }
13 | public int Count { get; set; }
14 | public ICollection Referers { get; set; } = new List();
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Regex.cshtml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using Geta.NotFoundHandler.Admin.Areas.GetaNotFoundHandlerAdmin.Pages.Base;
5 | using Geta.NotFoundHandler.Admin.Areas.GetaNotFoundHandlerAdmin.Pages.Extensions;
6 | using Geta.NotFoundHandler.Admin.Areas.GetaNotFoundHandlerAdmin.Pages.Models;
7 | using Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin.Models;
8 | using Geta.NotFoundHandler.Core.Providers.RegexRedirects;
9 | using Geta.NotFoundHandler.Data;
10 | using Geta.NotFoundHandler.Infrastructure;
11 | using Microsoft.AspNetCore.Authorization;
12 | using Microsoft.AspNetCore.Mvc;
13 |
14 | namespace Geta.NotFoundHandler.Admin.Areas.Geta.NotFoundHandler.Admin;
15 |
16 | [Authorize(Constants.PolicyName)]
17 | public class RegexModel : AbstractSortablePageModel
18 | {
19 | private readonly IRegexRedirectLoader _redirectLoader;
20 | private readonly IRegexRedirectsService _regexRedirectsService;
21 |
22 | public RegexModel(
23 | IRegexRedirectsService regexRedirectsService,
24 | IRegexRedirectLoader redirectLoader)
25 | {
26 | _regexRedirectsService = regexRedirectsService;
27 | _redirectLoader = redirectLoader;
28 | }
29 |
30 | public string Message { get; set; }
31 |
32 | public IEnumerable Items { get; set; } = Enumerable.Empty();
33 |
34 | [BindProperty(Name = nameof(RegexRedirect))]
35 | public RegexRedirectModel RegexRedirect { get; set; }
36 |
37 | [BindProperty(Name = nameof(EditRedirect))]
38 | public RegexRedirectModel EditRedirect { get; set; }
39 |
40 | public void OnGet(string sortColumn, SortDirection? sortDirection)
41 | {
42 | ApplySort(sortColumn, sortDirection);
43 |
44 | Load();
45 | }
46 |
47 | public IActionResult OnPostCreate()
48 | {
49 | ModelState.RemoveNestedKeys(nameof(EditRedirect));
50 |
51 | if (ModelState.IsValid)
52 | {
53 | _regexRedirectsService.Create(RegexRedirect.OldUrlRegex, RegexRedirect.NewUrlFormat, RegexRedirect.OrderNumber);
54 |
55 | return RedirectToPage();
56 | }
57 |
58 | Load();
59 | return Page();
60 | }
61 |
62 | public IActionResult OnPostDelete(Guid id)
63 | {
64 | _regexRedirectsService.Delete(id);
65 |
66 | return RedirectToPage();
67 | }
68 |
69 | public IActionResult OnPostUpdate()
70 | {
71 | ModelState.RemoveNestedKeys(nameof(RegexRedirect));
72 |
73 | if (ModelState.IsValid &&
74 | EditRedirect.Id != null)
75 | {
76 | _regexRedirectsService.Update(EditRedirect.Id.Value,
77 | EditRedirect.OldUrlRegex,
78 | EditRedirect.NewUrlFormat,
79 | EditRedirect.OrderNumber);
80 | return RedirectToPage();
81 | }
82 |
83 | Load();
84 |
85 | return Page();
86 | }
87 |
88 | private void Load()
89 | {
90 | var items = FindRedirects();
91 | Message = $"There are currently stored {items.Count()} Regex redirects.";
92 | Items = items;
93 | RegexRedirect = new RegexRedirectModel { OrderNumber = items.Select(x => x.OrderNumber).DefaultIfEmpty().Max() + 1 };
94 | }
95 |
96 | private IEnumerable FindRedirects()
97 | {
98 | return _redirectLoader
99 | .GetAll()
100 | .Sort(SortColumn, SortDirection);
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Shared/_Logo.cshtml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
24 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/Suggestions.cshtml.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin.Models;
4 | using Geta.NotFoundHandler.Core.Suggestions;
5 | using Geta.NotFoundHandler.Infrastructure;
6 | using Microsoft.AspNetCore.Authorization;
7 | using Microsoft.AspNetCore.Mvc;
8 | using Microsoft.AspNetCore.Mvc.RazorPages;
9 | using X.PagedList;
10 |
11 | namespace Geta.NotFoundHandler.Admin.Pages.Geta.NotFoundHandler.Admin;
12 |
13 | [Authorize(Constants.PolicyName)]
14 | public class SuggestionsModel : PageModel
15 | {
16 | private readonly ISuggestionService _suggestionService;
17 |
18 | public SuggestionsModel(ISuggestionService suggestionService)
19 | {
20 | _suggestionService = suggestionService;
21 | }
22 |
23 | public string Message { get; set; }
24 |
25 | public IPagedList Items { get; set; } = Enumerable.Empty().ToPagedList();
26 |
27 | [BindProperty(SupportsGet = true)]
28 | public Paging Paging { get; set; }
29 |
30 | public void OnGet()
31 | {
32 | Load();
33 | }
34 |
35 | public IActionResult OnPostCreate(Dictionary items)
36 | {
37 | if (!ModelState.IsValid)
38 | {
39 | Load();
40 | return Page();
41 | }
42 |
43 | var item = items.First().Value;
44 |
45 | _suggestionService.AddRedirect(new SuggestionRedirect(item.OldUrl, item.NewUrl));
46 |
47 | return RedirectToPage();
48 | }
49 |
50 | public IActionResult OnPostIgnore(string oldUrl)
51 | {
52 | _suggestionService.IgnoreSuggestion(oldUrl);
53 |
54 | return RedirectToPage();
55 | }
56 |
57 | private void Load()
58 | {
59 | var summaries = _suggestionService.GetSummaries(Paging.PageNumber, Paging.PageSize);
60 | var redirectModels = summaries.Select(x => new SuggestionRedirectModel
61 | {
62 | OldUrl = x.OldUrl,
63 | Count = x.Count,
64 | Referers = x.Referers
65 | });
66 |
67 | Message = $"Based on the logged 404 errors, there are {summaries.TotalItemCount} custom redirect suggestions.";
68 | Items = new StaticPagedList(redirectModels, summaries);
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/_ViewImports.cshtml:
--------------------------------------------------------------------------------
1 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
2 | @addTagHelper *, Geta.NotFoundHandler.Admin
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Admin/Areas/GetaNotFoundHandlerAdmin/Pages/_ViewStart.cshtml:
--------------------------------------------------------------------------------
1 | @{
2 | Layout = "Shared/_Layout";
3 | }
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Admin/Geta.NotFoundHandler.Admin.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | true
6 | Geta.NotFoundHandler.Admin
7 | Admin UI for NotFound Handler for ASP.NET Core and EPiServer
8 | Geta Digital
9 | Geta Digital
10 | Apache-2.0
11 | https://github.com/Geta/geta-notfoundhandler
12 | icon.png
13 | https://cdn.geta.no/opensource/icons/Geta-logo-3.png
14 | false
15 | This library contains an Admin user interface for a NotFound handler for your ASP.NET Core or EPiServer project.
16 | https://github.com/Geta/geta-notfoundhandler/blob/master/CHANGELOG.md
17 | 404 NotFound 404Error Handler Geta Redirect
18 | https://github.com/Geta/geta-notfoundhandler.git
19 | /_content/
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Admin/wwwroot/GetaNotFoundHandlerAdmin/css/dashboard.css:
--------------------------------------------------------------------------------
1 | /*
2 | * Icons
3 | */
4 |
5 | .feather {
6 | width: 16px;
7 | height: 16px;
8 | margin-bottom: 3px;
9 | }
10 |
11 | /*
12 | * Sidebar
13 | */
14 |
15 | .sidebar {
16 | position: fixed;
17 | top: 0;
18 | /* rtl:raw:
19 | right: 0;
20 | */
21 | bottom: 0;
22 | /* rtl:remove */
23 | left: 0;
24 | z-index: 100; /* Behind the navbar */
25 | padding: 48px 0 0; /* Height of navbar */
26 | box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
27 | }
28 |
29 | @media (max-width: 767.98px) {
30 | .sidebar {
31 | top: 5rem;
32 | }
33 | }
34 |
35 | .sidebar-sticky {
36 | position: relative;
37 | top: 0;
38 | height: calc(100vh - 48px);
39 | padding-top: .5rem;
40 | overflow-x: hidden;
41 | overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
42 | }
43 |
44 | .sidebar .nav-link {
45 | font-weight: 500;
46 | color: #333;
47 | }
48 |
49 | .sidebar .nav-link .feather {
50 | margin-right: 4px;
51 | color: #727272;
52 | }
53 |
54 | .sidebar .nav-link.active {
55 | color: #007bff;
56 | }
57 |
58 | .sidebar .nav-link:hover .feather,
59 | .sidebar .nav-link.active .feather {
60 | color: inherit;
61 | }
62 |
63 | .sidebar-heading {
64 | font-size: .75rem;
65 | text-transform: uppercase;
66 | }
67 |
68 | /*
69 | * Navbar
70 | */
71 |
72 | .geta-logo svg {
73 | width: 60px;
74 | }
75 |
76 | .geta-logo-prefix {
77 | color: white;
78 | font-size: 12px;
79 | }
80 |
81 | .version {
82 | color: white;
83 | font-size: 0.6em;
84 | }
85 |
86 | /*
87 | * Search
88 | */
89 |
90 | .search-container {
91 | margin-top: 15px;
92 | }
93 |
94 | .search-container .search-button {
95 | width: 120px;
96 | }
97 |
98 | /*
99 | * Form
100 | */
101 |
102 | .input-number {
103 | width: 50px;
104 | }
105 |
106 | .input-inline {
107 | margin: 0 5px 0 5px;
108 | }
109 |
110 | .administer-button {
111 | width: 120px;
112 | }
113 |
114 | .input-file {
115 | width: 450px;
116 | }
117 |
118 | /*
119 | * Table
120 | */
121 |
122 | td {
123 | word-break: break-all;
124 | }
125 |
126 | .edit-modal {
127 | max-height: 600px !important;
128 | }
129 |
130 | .referrers-modal {
131 | max-height: 350px !important;
132 | }
133 |
134 | .pagination-spacing {
135 | margin-top: 5rem !important;
136 | }
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Admin/wwwroot/GetaNotFoundHandlerAdmin/js/dashboard.js:
--------------------------------------------------------------------------------
1 | /* globals feather:false */
2 |
3 | (function () {
4 | 'use strict';
5 |
6 | feather.replace();
7 |
8 | function clearInput() {
9 | var initiators = document.querySelectorAll('[data-clear]');
10 | initiators.forEach(function (initiator) {
11 | initiator.addEventListener('click',
12 | function (e) {
13 | var target = e.currentTarget;
14 | var selector = target.getAttribute('data-clear');
15 | var input = document.querySelector(selector);
16 | input.value = '';
17 | });
18 | });
19 | }
20 |
21 | function confirmSubmit() {
22 | var initiators = document.querySelectorAll('[data-confirm]');
23 | initiators.forEach(function (initiator) {
24 | var form = initiator.form;
25 | form.addEventListener('submit',
26 | function (e) {
27 | e.preventDefault();
28 |
29 | var message = initiator.getAttribute('data-confirm');
30 | if (confirm(message)) {
31 | form.action = initiator.formAction;
32 | form.submit();
33 | }
34 | });
35 | });
36 | }
37 |
38 | function adjustModalPosition() {
39 | var modalTriggers = document.querySelectorAll('.modal-trigger[data-bs-target]');
40 | modalTriggers.forEach(function (modalTrigger) {
41 | modalTrigger.addEventListener('click', function () {
42 | var dialogSelector = modalTrigger.dataset.bsTarget + " .modal-dialog";
43 |
44 | var modalDialog = document.querySelector(dialogSelector);
45 |
46 | if (!modalDialog) { return; }
47 |
48 | modalDialog.style = "position: fixed;" +
49 | "top: " + modalTrigger.getBoundingClientRect().top + "px;" +
50 | "left: 50%;" +
51 | "min-width: 500px;" +
52 | "transform: translate(-50%, -50%);";
53 | });
54 | });
55 | }
56 |
57 | clearInput();
58 | confirmSubmit();
59 | adjustModalPosition();
60 | })()
61 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Optimizely.Commerce/AutomaticRedirects/CommerceContentKeyProvider.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Geta Digital. All rights reserved.
2 | // Licensed under Apache-2.0. See the LICENSE file in the project root for more information
3 |
4 | using EPiServer.Core;
5 | using Geta.NotFoundHandler.Optimizely.Core.AutomaticRedirects;
6 | using Mediachase.Commerce.Catalog;
7 |
8 | namespace Geta.NotFoundHandler.Optimizely.Commerce.AutomaticRedirects
9 | {
10 | public class CommerceContentKeyProvider : IContentKeyProvider
11 | {
12 | private readonly ReferenceConverter _referenceConverter;
13 |
14 | public CommerceContentKeyProvider(ReferenceConverter referenceConverter)
15 | {
16 | _referenceConverter = referenceConverter;
17 | }
18 |
19 | public ContentKeyResult GetContentKey(ContentReference contentLink)
20 | {
21 | if (ContentReference.IsNullOrEmpty(contentLink) || contentLink.ProviderName != "CatalogContent")
22 | {
23 | return ContentKeyResult.Empty;
24 | }
25 |
26 | var contentType = _referenceConverter.GetContentType(contentLink);
27 | if (contentType == CatalogContentType.Root || contentType == CatalogContentType.Catalog)
28 | {
29 | return ContentKeyResult.Empty;
30 | }
31 |
32 | var code = _referenceConverter.GetCode(contentLink);
33 | if (string.IsNullOrWhiteSpace(code))
34 | {
35 | return ContentKeyResult.Empty;
36 | }
37 |
38 | return new ContentKeyResult($"{code}--{contentLink.ProviderName}");
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Optimizely.Commerce/AutomaticRedirects/CommerceContentLinkProvider.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Geta Digital. All rights reserved.
2 | // Licensed under Apache-2.0. See the LICENSE file in the project root for more information
3 |
4 | using System.Collections.Generic;
5 | using System.Linq;
6 | using EPiServer;
7 | using EPiServer.Core;
8 | using Geta.NotFoundHandler.Optimizely.Core.AutomaticRedirects;
9 | using Mediachase.Commerce.Catalog;
10 |
11 | namespace Geta.NotFoundHandler.Optimizely.Commerce.AutomaticRedirects
12 | {
13 | public class CommerceContentLinkProvider : IContentLinkProvider
14 | {
15 | private readonly ReferenceConverter _referenceConverter;
16 | private readonly IContentLoader _contentLoader;
17 |
18 | public CommerceContentLinkProvider(ReferenceConverter referenceConverter, IContentLoader contentLoader)
19 | {
20 | _referenceConverter = referenceConverter;
21 | _contentLoader = contentLoader;
22 | }
23 |
24 | public IEnumerable GetAllLinks()
25 | {
26 | return _contentLoader.GetDescendents(_referenceConverter.GetRootLink()).ToList();
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Optimizely.Commerce/AutomaticRedirects/EntryContentUrlProvider.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Geta Digital. All rights reserved.
2 | // Licensed under Apache-2.0. See the LICENSE file in the project root for more information
3 |
4 | using System.Collections.Generic;
5 | using System.Linq;
6 | using EPiServer.Cms.Shell;
7 | using EPiServer.Commerce.Catalog.ContentTypes;
8 | using EPiServer.Commerce.Catalog.Linking;
9 | using EPiServer.Core;
10 | using EPiServer.Web.Routing;
11 | using Geta.NotFoundHandler.Optimizely.Core.AutomaticRedirects;
12 |
13 | namespace Geta.NotFoundHandler.Optimizely.Commerce.AutomaticRedirects
14 | {
15 | public class EntryContentUrlProvider : IContentUrlProvider
16 | {
17 | private readonly IUrlResolver _urlResolver;
18 | private readonly IRelationRepository _relationRepository;
19 |
20 | public EntryContentUrlProvider(IUrlResolver urlResolver, IRelationRepository relationRepository)
21 | {
22 | _urlResolver = urlResolver;
23 | _relationRepository = relationRepository;
24 | }
25 |
26 | public IEnumerable GetUrls(IContent content)
27 | {
28 | if (!CanHandle(content))
29 | {
30 | return Enumerable.Empty();
31 | }
32 |
33 | var entry = (EntryContentBase)content;
34 |
35 | return GetNodeContentUrls(entry);
36 | }
37 |
38 | public bool CanHandle(IContent content)
39 | {
40 | return content is EntryContentBase;
41 | }
42 |
43 | private IEnumerable GetNodeContentUrls(EntryContentBase entry)
44 | {
45 | var language = entry.LanguageBranch();
46 |
47 | var parentsLinks = _relationRepository
48 | .GetParents(entry.ContentLink)
49 | .ToList();
50 |
51 | foreach (var nodeParent in parentsLinks)
52 | {
53 | yield return new TypedUrl
54 | {
55 | Url = $"{_urlResolver.GetUrl(nodeParent.Parent, language)}/{entry.RouteSegment}",
56 | Type = nodeParent.IsPrimary ? UrlType.Primary : UrlType.Secondary,
57 | Language = language
58 | };
59 | }
60 |
61 | yield return new TypedUrl { Url = $"/{entry.SeoUri}", Type = UrlType.Seo, Language = language};
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Optimizely.Commerce/AutomaticRedirects/NodeContentUrlProvider.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Geta Digital. All rights reserved.
2 | // Licensed under Apache-2.0. See the LICENSE file in the project root for more information
3 |
4 | using System.Collections.Generic;
5 | using System.Linq;
6 | using EPiServer.Cms.Shell;
7 | using EPiServer.Commerce.Catalog.ContentTypes;
8 | using EPiServer.Commerce.Catalog.Linking;
9 | using EPiServer.Core;
10 | using EPiServer.Web.Routing;
11 | using Geta.NotFoundHandler.Optimizely.Core.AutomaticRedirects;
12 |
13 | namespace Geta.NotFoundHandler.Optimizely.Commerce.AutomaticRedirects
14 | {
15 | public class NodeContentUrlProvider : IContentUrlProvider
16 | {
17 | private readonly IUrlResolver _urlResolver;
18 | private readonly IRelationRepository _relationRepository;
19 |
20 | public NodeContentUrlProvider(IUrlResolver urlResolver, IRelationRepository relationRepository)
21 | {
22 | _urlResolver = urlResolver;
23 | _relationRepository = relationRepository;
24 | }
25 |
26 | public IEnumerable GetUrls(IContent content)
27 | {
28 | if (!CanHandle(content))
29 | {
30 | return Enumerable.Empty();
31 | }
32 |
33 | var node = (NodeContent)content;
34 |
35 | return GetNodeContentUrls(node);
36 | }
37 |
38 | public bool CanHandle(IContent content)
39 | {
40 | return content is NodeContent;
41 | }
42 |
43 | private IEnumerable GetNodeContentUrls(NodeContent node)
44 | {
45 | var language = node.LanguageBranch();
46 |
47 | var parentsLinks = _relationRepository
48 | .GetParents(node.ContentLink)
49 | .Select(x => x.Parent)
50 | .ToList();
51 |
52 | // primary parent node is not returned from _relationRepository.GetParents for nodes while it is for entries
53 | parentsLinks.Insert(0, node.ParentLink);
54 | parentsLinks = parentsLinks.Distinct().ToList();
55 |
56 | foreach (var parentLink in parentsLinks)
57 | {
58 | yield return new TypedUrl
59 | {
60 | Url = $"{_urlResolver.GetUrl(parentLink, language)}/{node.RouteSegment}",
61 | Type = parentLink == node.ParentLink ? UrlType.Primary : UrlType.Secondary,
62 | Language = language
63 | };
64 | }
65 |
66 | yield return new TypedUrl { Url = $"/{node.SeoUri}", Type = UrlType.Seo, Language = language};
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Optimizely.Commerce/Geta.NotFoundHandler.Optimizely.Commerce.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | Geta.NotFoundHandler.Optimizely.Commerce
6 | NotFound handler Admin UI integration Optimizely Commerce
7 | Geta Digital
8 | Geta Digital
9 | Apache-2.0
10 | https://github.com/Geta/geta-notfoundhandler
11 | icon.png
12 | https://cdn.geta.no/opensource/icons/Geta-logo-3.png
13 | false
14 | This library contains a NotFound handler Admin user interface integration in an Optimizely Commerce project.
15 | https://github.com/Geta/geta-notfoundhandler/blob/master/CHANGELOG.md
16 | 404 NotFound 404Error Handler Geta Redirect
17 | https://github.com/Geta/geta-notfoundhandler.git
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Optimizely.Commerce/Infrastructure/Configuration/OptimizelyNotFoundHandlerOptionsExtensions.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Geta Digital. All rights reserved.
2 | // Licensed under Apache-2.0. See the LICENSE file in the project root for more information
3 |
4 | using Geta.NotFoundHandler.Optimizely.Commerce.AutomaticRedirects;
5 | using Geta.NotFoundHandler.Optimizely.Infrastructure.Configuration;
6 |
7 | namespace Geta.NotFoundHandler.Optimizely.Commerce.Infrastructure.Configuration
8 | {
9 | public static class OptimizelyNotFoundHandlerOptionsExtensions
10 | {
11 | public static OptimizelyNotFoundHandlerOptions AddOptimizelyCommerceProviders(
12 | this OptimizelyNotFoundHandlerOptions options)
13 | {
14 | options.AddContentKeyProviders();
15 | options.AddContentLinkProviders();
16 | options.AddContentUrlProviders();
17 | options.AddContentUrlProviders();
18 |
19 | return options;
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Optimizely/Constants.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Geta Digital. All rights reserved.
2 | // Licensed under Apache-2.0. See the LICENSE file in the project root for more information
3 |
4 | namespace Geta.NotFoundHandler.Optimizely
5 | {
6 | public static class Constants
7 | {
8 | public const string ModuleName = "Geta.NotFoundHandler.Optimizely";
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Optimizely/ContainerController.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Geta Digital. All rights reserved.
2 | // Licensed under Apache-2.0. See the LICENSE file in the project root for more information
3 |
4 | using Microsoft.AspNetCore.Authorization;
5 | using Microsoft.AspNetCore.Mvc;
6 |
7 | namespace Geta.NotFoundHandler.Optimizely
8 | {
9 | public class ContainerController : Controller
10 | {
11 | [Authorize(Policy = NotFoundHandler.Infrastructure.Constants.PolicyName)]
12 | [HttpGet]
13 | public IActionResult Index()
14 | {
15 | return View();
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Optimizely/Core/AutomaticRedirects/ChannelMovedContentRegistratorQueue.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Geta Digital. All rights reserved.
2 | // Licensed under Apache-2.0. See the LICENSE file in the project root for more information
3 |
4 | using System.Collections.Generic;
5 | using System.Threading;
6 | using System.Threading.Channels;
7 | using EPiServer.Core;
8 |
9 | namespace Geta.NotFoundHandler.Optimizely.Core.AutomaticRedirects
10 | {
11 | public class ChannelMovedContentRegistratorQueue : IMovedContentRegistratorQueue
12 | {
13 | private static readonly Channel Buffer = Channel.CreateUnbounded(
14 | new UnboundedChannelOptions
15 | {
16 | AllowSynchronousContinuations = false,
17 | SingleWriter = false,
18 | SingleReader = true
19 | });
20 |
21 | public virtual IAsyncEnumerable ReadAllAsync(CancellationToken cancellationToken = default)
22 | {
23 | return Buffer.Reader.ReadAllAsync(cancellationToken);
24 | }
25 |
26 | public virtual void Enqueue(ContentReference contentLink)
27 | {
28 | Buffer.Writer.TryWrite(contentLink);
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Optimizely/Core/AutomaticRedirects/CmsContentKeyProvider.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Geta Digital. All rights reserved.
2 | // Licensed under Apache-2.0. See the LICENSE file in the project root for more information
3 |
4 | using EPiServer.Core;
5 |
6 | namespace Geta.NotFoundHandler.Optimizely.Core.AutomaticRedirects
7 | {
8 | public class CmsContentKeyProvider : IContentKeyProvider
9 | {
10 | public ContentKeyResult GetContentKey(ContentReference contentLink)
11 | {
12 | if (ContentReference.IsNullOrEmpty(contentLink) || contentLink.ProviderName != null || contentLink.IsExternalProvider)
13 | {
14 | return ContentKeyResult.Empty;
15 | }
16 |
17 | var key = contentLink.ToReferenceWithoutVersion().ToString();
18 | return new ContentKeyResult(key);
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Optimizely/Core/AutomaticRedirects/CmsContentLinkProvider.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Geta Digital. All rights reserved.
2 | // Licensed under Apache-2.0. See the LICENSE file in the project root for more information
3 |
4 | using System.Collections.Generic;
5 | using System.Linq;
6 | using EPiServer;
7 | using EPiServer.Core;
8 | using EPiServer.Web;
9 |
10 | namespace Geta.NotFoundHandler.Optimizely.Core.AutomaticRedirects
11 | {
12 | public class CmsContentLinkProvider : IContentLinkProvider
13 | {
14 | private readonly ISiteDefinitionRepository _siteDefinitionRepository;
15 | private readonly IContentLoader _contentLoader;
16 |
17 | public CmsContentLinkProvider(ISiteDefinitionRepository siteDefinitionRepository, IContentLoader contentLoader)
18 | {
19 | _siteDefinitionRepository = siteDefinitionRepository;
20 | _contentLoader = contentLoader;
21 | }
22 |
23 | public IEnumerable GetAllLinks()
24 | {
25 | var allSites = _siteDefinitionRepository.List();
26 | return allSites.SelectMany(site => _contentLoader.GetDescendents(site.StartPage));
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Optimizely/Core/AutomaticRedirects/CmsContentUrlProvider.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Geta Digital. All rights reserved.
2 | // Licensed under Apache-2.0. See the LICENSE file in the project root for more information
3 |
4 | using System;
5 | using System.Collections.Generic;
6 | using System.Globalization;
7 | using System.Linq;
8 | using EPiServer;
9 | using EPiServer.Cms.Shell;
10 | using EPiServer.Core;
11 | using EPiServer.Web.Routing;
12 |
13 | namespace Geta.NotFoundHandler.Optimizely.Core.AutomaticRedirects
14 | {
15 | public class CmsContentUrlProvider : IContentUrlProvider
16 | {
17 | private readonly IContentLoader _contentLoader;
18 | private readonly IContentVersionRepository _contentVersionRepository;
19 | private readonly IUrlResolver _urlResolver;
20 |
21 | public CmsContentUrlProvider(
22 | IContentLoader contentLoader,
23 | IContentVersionRepository contentVersionRepository,
24 | IUrlResolver urlResolver)
25 | {
26 | _contentLoader = contentLoader;
27 | _contentVersionRepository = contentVersionRepository;
28 | _urlResolver = urlResolver;
29 | }
30 |
31 | public IEnumerable GetUrls(IContent content)
32 | {
33 | if (!CanHandle(content))
34 | {
35 | return Enumerable.Empty();
36 | }
37 |
38 | var page = (PageData)content;
39 | if (page.StopPublish <= DateTime.UtcNow)
40 | {
41 | return Enumerable.Empty();
42 | }
43 |
44 | return GetPageUrls(page);
45 | }
46 |
47 | public bool CanHandle(IContent content)
48 | {
49 | return content is PageData;
50 | }
51 |
52 | private IEnumerable GetPageUrls(PageData page)
53 | {
54 | var language = page.LanguageBranch();
55 |
56 | return new List { new() { Url = GetPageUrl(page, language), Type = UrlType.Primary, Language = language} };
57 | }
58 |
59 | private string GetPageUrl(PageData page, string language)
60 | {
61 | if (page.LinkType == PageShortcutType.External || page.LinkType == PageShortcutType.Shortcut)
62 | {
63 | var lastPublishedVersion = _contentVersionRepository.LoadPublished(page.ParentLink, language);
64 |
65 | if (lastPublishedVersion != null)
66 | {
67 | var parent = _contentLoader.Get(lastPublishedVersion.ContentLink, new CultureInfo(language));
68 | if (parent is PageData parentPage)
69 | {
70 | var parentUrl = GetPageUrl(parentPage, language);
71 | return $"{parentUrl}/{page.URLSegment}/";
72 | }
73 | }
74 |
75 | return "/";
76 | }
77 |
78 | return _urlResolver.GetUrl(page.ContentLink, language);
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Optimizely/Core/AutomaticRedirects/ContentKeyGenerator.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Geta Digital. All rights reserved.
2 | // Licensed under Apache-2.0. See the LICENSE file in the project root for more information
3 |
4 | using System.Collections.Generic;
5 | using System.Linq;
6 | using EPiServer.Core;
7 |
8 | namespace Geta.NotFoundHandler.Optimizely.Core.AutomaticRedirects
9 | {
10 | public class ContentKeyGenerator
11 | {
12 | private readonly IEnumerable _contentKeyProviders;
13 |
14 | public ContentKeyGenerator(IEnumerable contentKeyProviders)
15 | {
16 | _contentKeyProviders = contentKeyProviders;
17 | }
18 |
19 | public virtual ContentKeyResult GetContentKey(ContentReference contentLink)
20 | {
21 | if (ContentReference.IsNullOrEmpty(contentLink))
22 | {
23 | return ContentKeyResult.Empty;
24 | }
25 |
26 | return _contentKeyProviders
27 | .Select(provider => provider.GetContentKey(contentLink))
28 | .FirstOrDefault(result => result.HasValue)
29 | ?? ContentKeyResult.Empty;
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Optimizely/Core/AutomaticRedirects/ContentKeyResult.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Geta Digital. All rights reserved.
2 | // Licensed under Apache-2.0. See the LICENSE file in the project root for more information
3 |
4 | namespace Geta.NotFoundHandler.Optimizely.Core.AutomaticRedirects
5 | {
6 | public class ContentKeyResult
7 | {
8 | public static ContentKeyResult Empty { get; } = new();
9 |
10 | public string Key { get; }
11 | public bool HasValue { get; }
12 |
13 | private ContentKeyResult() { }
14 |
15 | public ContentKeyResult(string key)
16 | {
17 | Key = key;
18 | HasValue = true;
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Optimizely/Core/AutomaticRedirects/ContentLinkLoader.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Geta Digital. All rights reserved.
2 | // Licensed under Apache-2.0. See the LICENSE file in the project root for more information
3 |
4 | using System.Collections.Generic;
5 | using System.Linq;
6 | using EPiServer.Core;
7 |
8 | namespace Geta.NotFoundHandler.Optimizely.Core.AutomaticRedirects
9 | {
10 | public class ContentLinkLoader
11 | {
12 | private readonly IEnumerable _contentLinkProviders;
13 |
14 | public ContentLinkLoader(IEnumerable contentLinkProviders)
15 | {
16 | _contentLinkProviders = contentLinkProviders;
17 | }
18 |
19 | public virtual IEnumerable GetAllLinks()
20 | {
21 | return _contentLinkProviders.SelectMany(provider => provider.GetAllLinks()).Distinct().ToList();
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Optimizely/Core/AutomaticRedirects/ContentUrlHistory.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Geta Digital. All rights reserved.
2 | // Licensed under Apache-2.0. See the LICENSE file in the project root for more information
3 |
4 | using System;
5 | using System.Collections.Generic;
6 |
7 | namespace Geta.NotFoundHandler.Optimizely.Core.AutomaticRedirects
8 | {
9 | public class ContentUrlHistory
10 | {
11 | public Guid Id { get; set; }
12 | public string ContentKey { get; set; }
13 | public DateTime CreatedUtc { get; set; }
14 | public ICollection Urls { get; set; }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Optimizely/Core/AutomaticRedirects/ContentUrlHistoryEvents.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Geta Digital. All rights reserved.
2 | // Licensed under Apache-2.0. See the LICENSE file in the project root for more information
3 |
4 | using System.Linq;
5 | using EPiServer;
6 | using EPiServer.Core;
7 | using Geta.NotFoundHandler.Optimizely.Infrastructure.Configuration;
8 | using Microsoft.Extensions.Options;
9 |
10 | namespace Geta.NotFoundHandler.Optimizely.Core.AutomaticRedirects
11 | {
12 | public class ContentUrlHistoryEvents
13 | {
14 | private readonly IContentEvents _contentEvents;
15 | private readonly ContentUrlIndexer _contentUrlIndexer;
16 | private readonly IMovedContentRegistratorQueue _movedContentRegistratorQueue;
17 | private readonly OptimizelyNotFoundHandlerOptions _configuration;
18 |
19 | public ContentUrlHistoryEvents(
20 | IContentEvents contentEvents,
21 | IOptions options,
22 | ContentUrlIndexer contentUrlIndexer,
23 | IMovedContentRegistratorQueue movedContentRegistratorQueue)
24 | {
25 | _contentEvents = contentEvents;
26 | _contentUrlIndexer = contentUrlIndexer;
27 | _movedContentRegistratorQueue = movedContentRegistratorQueue;
28 | _configuration = options.Value;
29 | }
30 |
31 | public void Initialize()
32 | {
33 | if (_configuration.AutomaticRedirectsEnabled)
34 | {
35 | _contentEvents.MovedContent += OnMovedContent;
36 | _contentEvents.PublishedContent += OnPublishedContent;
37 | }
38 | }
39 |
40 | private void OnMovedContent(object sender, ContentEventArgs e)
41 | {
42 | if (e is MoveContentEventArgs me
43 | && new[] { me.OriginalParent, me.TargetLink }.Any(x => x.CompareToIgnoreWorkID(ContentReference.WasteBasket)))
44 | {
45 | return;
46 | }
47 | _movedContentRegistratorQueue.Enqueue(e.ContentLink);
48 | }
49 |
50 | private void OnPublishedContent(object sender, ContentEventArgs e)
51 | {
52 | _contentUrlIndexer.IndexContentUrls(e.ContentLink);
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Optimizely/Core/AutomaticRedirects/ContentUrlIndexer.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Geta Digital. All rights reserved.
2 | // Licensed under Apache-2.0. See the LICENSE file in the project root for more information
3 |
4 | using System.Linq;
5 | using EPiServer.Core;
6 | using Geta.NotFoundHandler.Data;
7 |
8 | namespace Geta.NotFoundHandler.Optimizely.Core.AutomaticRedirects
9 | {
10 | public class ContentUrlIndexer
11 | {
12 | private readonly ContentKeyGenerator _contentKeyGenerator;
13 | private readonly ContentUrlLoader _contentUrlLoader;
14 | private readonly IRepository _contentUrlHistoryRepository;
15 | private readonly IContentUrlHistoryLoader _contentUrlHistoryLoader;
16 |
17 | public ContentUrlIndexer(
18 | ContentKeyGenerator contentKeyGenerator,
19 | ContentUrlLoader contentUrlLoader,
20 | IRepository contentUrlHistoryRepository,
21 | IContentUrlHistoryLoader contentUrlHistoryLoader)
22 | {
23 | _contentKeyGenerator = contentKeyGenerator;
24 | _contentUrlLoader = contentUrlLoader;
25 | _contentUrlHistoryRepository = contentUrlHistoryRepository;
26 | _contentUrlHistoryLoader = contentUrlHistoryLoader;
27 | }
28 |
29 | public virtual void IndexContentUrls(ContentReference contentLink)
30 | {
31 | var keyResult = _contentKeyGenerator.GetContentKey(contentLink);
32 | if (!keyResult.HasValue) return;
33 |
34 | var urls = _contentUrlLoader.GetUrls(contentLink).ToList();
35 | var history = new ContentUrlHistory { ContentKey = keyResult.Key, Urls = urls };
36 |
37 | if (!_contentUrlHistoryLoader.IsRegistered(history))
38 | {
39 | _contentUrlHistoryRepository.Save(history);
40 | }
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Optimizely/Core/AutomaticRedirects/DefaultAutomaticRedirectsService.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Geta Digital. All rights reserved.
2 | // Licensed under Apache-2.0. See the LICENSE file in the project root for more information
3 |
4 | using System.Collections.Generic;
5 | using System.Linq;
6 | using Geta.NotFoundHandler.Core.Redirects;
7 |
8 | namespace Geta.NotFoundHandler.Optimizely.Core.AutomaticRedirects
9 | {
10 | public class DefaultAutomaticRedirectsService : IAutomaticRedirectsService
11 | {
12 | private readonly IRedirectsService _redirectsService;
13 | private readonly RedirectBuilder _redirectBuilder;
14 |
15 | public DefaultAutomaticRedirectsService(
16 | IRedirectsService redirectsService,
17 | RedirectBuilder redirectBuilder)
18 | {
19 | _redirectsService = redirectsService;
20 | _redirectBuilder = redirectBuilder;
21 | }
22 |
23 | public void CreateRedirects(IReadOnlyCollection histories)
24 | {
25 | var redirects = _redirectBuilder.CreateRedirects(histories).ToList();
26 | _redirectsService.AddOrUpdate(redirects);
27 | var urlsToRemove = redirects.Where(x => x.NewUrl == x.OldUrl).Select(x => x.OldUrl);
28 | _redirectsService.DeleteByOldUrl(urlsToRemove);
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Optimizely/Core/AutomaticRedirects/IAutomaticRedirectsService.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Geta Digital. All rights reserved.
2 | // Licensed under Apache-2.0. See the LICENSE file in the project root for more information
3 |
4 | using System.Collections.Generic;
5 |
6 | namespace Geta.NotFoundHandler.Optimizely.Core.AutomaticRedirects
7 | {
8 | public interface IAutomaticRedirectsService
9 | {
10 | void CreateRedirects(IReadOnlyCollection histories);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Optimizely/Core/AutomaticRedirects/IContentKeyProvider.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Geta Digital. All rights reserved.
2 | // Licensed under Apache-2.0. See the LICENSE file in the project root for more information
3 |
4 | using EPiServer.Core;
5 |
6 | namespace Geta.NotFoundHandler.Optimizely.Core.AutomaticRedirects
7 | {
8 | public interface IContentKeyProvider
9 | {
10 | ContentKeyResult GetContentKey(ContentReference contentLink);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Optimizely/Core/AutomaticRedirects/IContentLinkProvider.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Geta Digital. All rights reserved.
2 | // Licensed under Apache-2.0. See the LICENSE file in the project root for more information
3 |
4 | using System.Collections.Generic;
5 | using EPiServer.Core;
6 |
7 | namespace Geta.NotFoundHandler.Optimizely.Core.AutomaticRedirects
8 | {
9 | public interface IContentLinkProvider
10 | {
11 | IEnumerable GetAllLinks();
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Optimizely/Core/AutomaticRedirects/IContentUrlHistoryLoader.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Geta Digital. All rights reserved.
2 | // Licensed under Apache-2.0. See the LICENSE file in the project root for more information
3 |
4 | using System.Collections.Generic;
5 |
6 | namespace Geta.NotFoundHandler.Optimizely.Core.AutomaticRedirects
7 | {
8 | public interface IContentUrlHistoryLoader
9 | {
10 | bool IsRegistered(ContentUrlHistory entity);
11 | IEnumerable<(string contentKey, IReadOnlyCollection histories)> GetAllMoved();
12 | IReadOnlyCollection GetMoved(string contentKey);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Optimizely/Core/AutomaticRedirects/IContentUrlProvider.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Geta Digital. All rights reserved.
2 | // Licensed under Apache-2.0. See the LICENSE file in the project root for more information
3 |
4 | using System.Collections.Generic;
5 | using EPiServer.Core;
6 |
7 | namespace Geta.NotFoundHandler.Optimizely.Core.AutomaticRedirects
8 | {
9 | public interface IContentUrlProvider
10 | {
11 | IEnumerable GetUrls(IContent content);
12 | bool CanHandle(IContent content);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Optimizely/Core/AutomaticRedirects/IMovedContentRegistratorQueue.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Geta Digital. All rights reserved.
2 | // Licensed under Apache-2.0. See the LICENSE file in the project root for more information
3 |
4 | using EPiServer.Core;
5 |
6 | namespace Geta.NotFoundHandler.Optimizely.Core.AutomaticRedirects
7 | {
8 | public interface IMovedContentRegistratorQueue
9 | {
10 | void Enqueue(ContentReference contentLink);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Optimizely/Core/AutomaticRedirects/IndexContentUrlsJob.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Geta Digital. All rights reserved.
2 | // Licensed under Apache-2.0. See the LICENSE file in the project root for more information
3 |
4 | using System;
5 | using System.Linq;
6 | using EPiServer.PlugIn;
7 | using EPiServer.Scheduler;
8 | using Geta.NotFoundHandler.Optimizely.Infrastructure;
9 |
10 | namespace Geta.NotFoundHandler.Optimizely.Core.AutomaticRedirects
11 | {
12 | [ScheduledPlugIn(DisplayName = "[Geta NotFoundHandler] Index content URLs",
13 | GUID = "53C743AE-E152-497A-A7E5-7E30F4B5B321",
14 | SortIndex = 5555)]
15 | public class IndexContentUrlsJob : ScheduledJobBase
16 | {
17 | private bool _stopped;
18 |
19 | private readonly ContentUrlIndexer _contentUrlIndexer;
20 | private readonly ContentLinkLoader _contentLinkLoader;
21 | private readonly JobStatusLogger _jobStatusLogger;
22 |
23 | public IndexContentUrlsJob(ContentUrlIndexer contentUrlIndexer, ContentLinkLoader contentLinkLoader)
24 | {
25 | _contentUrlIndexer = contentUrlIndexer;
26 | _contentLinkLoader = contentLinkLoader;
27 | _jobStatusLogger = new JobStatusLogger(OnStatusChanged);
28 |
29 | IsStoppable = true;
30 | }
31 |
32 | public override string Execute()
33 | {
34 | var contentLinks = _contentLinkLoader.GetAllLinks().ToList();
35 |
36 | var totalCount = contentLinks.Count;
37 | var successCount = 0;
38 | var failedCount = 0;
39 | var currentCount = 0;
40 |
41 | _jobStatusLogger.LogWithStatus($"In total will process unique references: {totalCount}");
42 |
43 | foreach (var contentLink in contentLinks)
44 | {
45 | if (_stopped)
46 | {
47 | _jobStatusLogger.Log(
48 | $"Job was stopped, successful references before stopped: {successCount} out of total {totalCount} references");
49 | return _jobStatusLogger.ToString();
50 | }
51 |
52 | currentCount++;
53 |
54 | try
55 | {
56 | _contentUrlIndexer.IndexContentUrls(contentLink);
57 |
58 | successCount++;
59 | }
60 | catch (Exception ex)
61 | {
62 | _jobStatusLogger.Log($"Processing [{contentLink}] failed, exception: {ex}");
63 | failedCount++;
64 | }
65 |
66 | if (currentCount % 500 == 0)
67 | {
68 | _jobStatusLogger.Status(
69 | $"Processed {currentCount} of whom successful {successCount} out of total {totalCount} references; failed: {failedCount}");
70 | }
71 | }
72 |
73 | _jobStatusLogger.Log(
74 | $"Processed {currentCount} of whom successful {successCount} out of total {totalCount} references; failed: {failedCount}");
75 |
76 | return _jobStatusLogger.ToString();
77 | }
78 |
79 | public override void Stop()
80 | {
81 | _stopped = true;
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Optimizely/Core/AutomaticRedirects/MovedContentRegistratorBackgroundService.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Geta Digital. All rights reserved.
2 | // Licensed under Apache-2.0. See the LICENSE file in the project root for more information
3 |
4 | using System;
5 | using System.Threading;
6 | using System.Threading.Tasks;
7 | using EPiServer.Core;
8 | using Microsoft.Extensions.Hosting;
9 | using Microsoft.Extensions.Logging;
10 |
11 | namespace Geta.NotFoundHandler.Optimizely.Core.AutomaticRedirects
12 | {
13 | public class MovedContentRegistratorBackgroundService : BackgroundService
14 | {
15 | private readonly ChannelMovedContentRegistratorQueue _registratorQueue;
16 | private readonly Func _contentKeyGeneratorFactory;
17 | private readonly IAutomaticRedirectsService _automaticRedirectsService;
18 | private readonly IContentUrlHistoryLoader _contentUrlHistoryLoader;
19 | private readonly Func _contentUrlIndexerFactory;
20 | private readonly ILogger _logger;
21 |
22 | public MovedContentRegistratorBackgroundService(
23 | ChannelMovedContentRegistratorQueue registratorQueue,
24 | Func contentKeyGeneratorFactory,
25 | IAutomaticRedirectsService automaticRedirectsService,
26 | IContentUrlHistoryLoader contentUrlHistoryLoader,
27 | Func contentUrlIndexerFactory,
28 | ILogger logger)
29 | {
30 | _registratorQueue = registratorQueue;
31 | _contentKeyGeneratorFactory = contentKeyGeneratorFactory;
32 | _automaticRedirectsService = automaticRedirectsService;
33 | _contentUrlHistoryLoader = contentUrlHistoryLoader;
34 | _contentUrlIndexerFactory = contentUrlIndexerFactory;
35 | _logger = logger;
36 | }
37 |
38 | protected override async Task ExecuteAsync(CancellationToken stoppingToken)
39 | {
40 | await foreach (var contentLink in _registratorQueue.ReadAllAsync(stoppingToken))
41 | {
42 | try
43 | {
44 | IndexContentUrls(contentLink);
45 | CreateRedirects(contentLink);
46 | }
47 | catch (Exception ex)
48 | {
49 | _logger.LogError(ex, "Failed registering redirects for content: {ContentLink}", contentLink);
50 | }
51 | }
52 | }
53 |
54 | private void CreateRedirects(ContentReference contentLink)
55 | {
56 | var contentKeyGenerator = _contentKeyGeneratorFactory();
57 | var keyResult = contentKeyGenerator.GetContentKey(contentLink);
58 | if (!keyResult.HasValue) return;
59 |
60 | var contentKey = keyResult.Key;
61 | var histories = _contentUrlHistoryLoader.GetMoved(contentKey);
62 | _automaticRedirectsService.CreateRedirects(histories);
63 | }
64 |
65 | private void IndexContentUrls(ContentReference contentLink)
66 | {
67 | var contentUrlIndexer = _contentUrlIndexerFactory();
68 | contentUrlIndexer.IndexContentUrls(contentLink);
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Optimizely/Core/AutomaticRedirects/RegisterMovedContentRedirectsJob.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Geta Digital. All rights reserved.
2 | // Licensed under Apache-2.0. See the LICENSE file in the project root for more information
3 |
4 | using System;
5 | using System.Linq;
6 | using EPiServer.PlugIn;
7 | using EPiServer.Scheduler;
8 | using Geta.NotFoundHandler.Optimizely.Infrastructure;
9 |
10 | namespace Geta.NotFoundHandler.Optimizely.Core.AutomaticRedirects
11 | {
12 | [ScheduledPlugIn(DisplayName = "[Geta NotFoundHandler] Register content move redirects",
13 | GUID = "EC96ABEE-5DA4-404F-A0C8-451C77CA4983",
14 | SortIndex = 5555)]
15 | public class RegisterMovedContentRedirectsJob : ScheduledJobBase
16 | {
17 | private readonly IContentUrlHistoryLoader _contentUrlHistoryLoader;
18 | private readonly JobStatusLogger _jobStatusLogger;
19 | private readonly IAutomaticRedirectsService _automaticRedirectsService;
20 | private bool _stopped;
21 |
22 | public RegisterMovedContentRedirectsJob(
23 | IAutomaticRedirectsService automaticRedirectsService,
24 | IContentUrlHistoryLoader contentUrlHistoryLoader)
25 | {
26 | _automaticRedirectsService = automaticRedirectsService;
27 | _contentUrlHistoryLoader = contentUrlHistoryLoader;
28 | _jobStatusLogger = new JobStatusLogger(OnStatusChanged);
29 |
30 | IsStoppable = true;
31 | }
32 |
33 | public override string Execute()
34 | {
35 | var movedContent = _contentUrlHistoryLoader.GetAllMoved().ToList();
36 | var totalCount = movedContent.Count;
37 | var successCount = 0;
38 | var failedCount = 0;
39 | var currentCount = 0;
40 |
41 | _jobStatusLogger.LogWithStatus($"In total will process moved content: {totalCount}");
42 |
43 | foreach (var content in movedContent)
44 | {
45 | if (_stopped)
46 | {
47 | _jobStatusLogger.Log(
48 | $"Job was stopped, successful content handled before stopped: {successCount} out of total {totalCount} content");
49 | return _jobStatusLogger.ToString();
50 | }
51 |
52 | currentCount++;
53 |
54 | try
55 | {
56 | _automaticRedirectsService.CreateRedirects(content.histories);
57 | successCount++;
58 | }
59 | catch (Exception ex)
60 | {
61 | _jobStatusLogger.Log($"Processing [{content.contentKey}] failed, exception: {ex}");
62 | failedCount++;
63 | }
64 |
65 | if (currentCount % 500 == 0)
66 | {
67 | _jobStatusLogger.Status(
68 | $"Processed {currentCount} of whom successful {successCount} out of total {totalCount} content; failed: {failedCount}");
69 | }
70 | }
71 |
72 | _jobStatusLogger.Log(
73 | $"Processed {currentCount} of whom successful {successCount} out of total {totalCount} content; failed: {failedCount}");
74 |
75 | return _jobStatusLogger.ToString();
76 | }
77 |
78 | public override void Stop()
79 | {
80 | _stopped = true;
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Optimizely/Core/AutomaticRedirects/TypedUrl.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Geta Digital. All rights reserved.
2 | // Licensed under Apache-2.0. See the LICENSE file in the project root for more information
3 |
4 | namespace Geta.NotFoundHandler.Optimizely.Core.AutomaticRedirects
5 | {
6 | public record TypedUrl
7 | {
8 | public string Url { get; set; }
9 | public UrlType Type { get; set; }
10 | public string Language { get; set; }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Optimizely/Core/AutomaticRedirects/UrlType.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Geta Digital. All rights reserved.
2 | // Licensed under Apache-2.0. See the LICENSE file in the project root for more information
3 |
4 | namespace Geta.NotFoundHandler.Optimizely.Core.AutomaticRedirects
5 | {
6 | public enum UrlType
7 | {
8 | Primary = 1,
9 | Secondary = 2,
10 | Seo = 3
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Optimizely/Core/Events/OptimizelySyncEvents.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Geta Digital. All rights reserved.
2 | // Licensed under Apache-2.0. See the LICENSE file in the project root for more information
3 |
4 | using System;
5 | using EPiServer.Events.Clients;
6 | using Geta.NotFoundHandler.Core.Providers.RegexRedirects;
7 | using Geta.NotFoundHandler.Core.Redirects;
8 |
9 | namespace Geta.NotFoundHandler.Optimizely.Core.Events
10 | {
11 | public class OptimizelySyncEvents
12 | {
13 | private readonly RedirectsEvents _redirectsEvents;
14 | private readonly IEventRegistry _eventRegistry;
15 | private readonly RedirectsInitializer _redirectsInitializer;
16 | private readonly IRegexRedirectCache _regexRedirectCache;
17 |
18 | private static readonly Guid UpdateEventId = new("{AC263F88-6C17-45A5-81E0-DCC28DF26AEF}");
19 | private static readonly Guid RegexUpdateEventId = new("{334DF0A0-793C-4B19-A0CC-8AD63F705FDD}");
20 | private static readonly Guid RaiserId = Guid.NewGuid();
21 |
22 | public OptimizelySyncEvents(
23 | RedirectsEvents redirectsEvents,
24 | IEventRegistry eventRegistry,
25 | RedirectsInitializer redirectsInitializer,
26 | IRegexRedirectCache regexRedirectCache)
27 | {
28 | _redirectsEvents = redirectsEvents;
29 | _eventRegistry = eventRegistry;
30 | _redirectsInitializer = redirectsInitializer;
31 | _regexRedirectCache = regexRedirectCache;
32 | }
33 |
34 | public void Initialize()
35 | {
36 | _redirectsEvents.OnRedirectsUpdated += OnRedirectsUpdated;
37 | _redirectsEvents.OnRegexRedirectsUpdated += OnRegexRedirectsUpdated;
38 | _eventRegistry.Get(UpdateEventId).Raised += SyncRedirectsUpdateEventRaised;
39 | _eventRegistry.Get(RegexUpdateEventId).Raised += SyncRegexRedirectsUpdateEventRaised;
40 | }
41 |
42 | private void SyncRedirectsUpdateEventRaised(object sender, EPiServer.Events.EventNotificationEventArgs e)
43 | {
44 | if (e.RaiserId != RaiserId)
45 | {
46 | _redirectsInitializer.Initialize();
47 | }
48 | }
49 |
50 | private void SyncRegexRedirectsUpdateEventRaised(object sender, EPiServer.Events.EventNotificationEventArgs e)
51 | {
52 | if (e.RaiserId != RaiserId)
53 | {
54 | _regexRedirectCache.Remove();
55 | }
56 | }
57 |
58 | private void OnRedirectsUpdated(EventArgs e)
59 | {
60 | _eventRegistry.Get(UpdateEventId).Raise(RaiserId, UpdateEventId);
61 | }
62 |
63 | private void OnRegexRedirectsUpdated(EventArgs e)
64 | {
65 | _eventRegistry.Get(RegexUpdateEventId).Raise(RaiserId, RegexUpdateEventId);
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Optimizely/Core/Suggestions/Jobs/SuggestionsCleanupJob.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Geta Digital. All rights reserved.
2 | // Licensed under Apache-2.0. See the LICENSE file in the project root for more information
3 |
4 | using EPiServer.PlugIn;
5 | using EPiServer.Scheduler;
6 | using Geta.NotFoundHandler.Core.Suggestions;
7 |
8 | namespace Geta.NotFoundHandler.Optimizely.Core.Suggestions.Jobs;
9 |
10 | [ScheduledPlugIn(DisplayName = "[Geta NotFoundHandler] Suggestions cleanup job",
11 | Description = "As suggestions table grow fast we should add a possibility to clean up old suggestions",
12 | GUID = "6AE19CEC-1052-4482-97DF-981076DDD6F2",
13 | SortIndex = 5555)]
14 | public class SuggestionsCleanupJob : ScheduledJobBase
15 | {
16 | private readonly ISuggestionsCleanupService _suggestionsCleanupService;
17 |
18 | public SuggestionsCleanupJob(ISuggestionsCleanupService suggestionsCleanupService)
19 | {
20 | IsStoppable = true;
21 | _suggestionsCleanupService = suggestionsCleanupService;
22 | }
23 |
24 | public override string Execute()
25 | {
26 | _suggestionsCleanupService.Cleanup();
27 |
28 | return string.Empty;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Optimizely/Geta.NotFoundHandler.Optimizely.Views/Views/Container/Index.cshtml:
--------------------------------------------------------------------------------
1 | @model dynamic
2 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Optimizely/Geta.NotFoundHandler.Optimizely.Views/Views/Shared/_ShellLayout.cshtml:
--------------------------------------------------------------------------------
1 | @using EPiServer.Framework.Web.Resources
2 | @using EPiServer.Shell.Navigation
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | @ClientResources.RenderResources("ShellCore")
11 | @ClientResources.RenderResources("ShellCoreLightTheme")
12 |
13 |
29 |
30 | NotFound handler
31 |
32 |
33 | @Html.AntiForgeryToken()
34 | @Html.CreatePlatformNavigationMenu()
35 |
36 | @RenderBody()
37 |
38 |
39 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Optimizely/Geta.NotFoundHandler.Optimizely.Views/Views/_ViewStart.cshtml:
--------------------------------------------------------------------------------
1 | @{
2 | Layout = "Shared/_ShellLayout.cshtml";
3 | }
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Optimizely/Geta.NotFoundHandler.Optimizely.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | true
6 | Geta.NotFoundHandler.Optimizely
7 | NotFound handler Admin UI integration Optimizely
8 | Geta Digital
9 | Geta Digital
10 | Apache-2.0
11 | https://github.com/Geta/geta-notfoundhandler
12 | icon.png
13 | https://cdn.geta.no/opensource/icons/Geta-logo-3.png
14 | false
15 | This library contains a NotFound handler Admin user interface integration in an Optimizely project.
16 | https://github.com/Geta/geta-notfoundhandler/blob/master/CHANGELOG.md
17 | 404 NotFound 404Error Handler Geta Redirect
18 | https://github.com/Geta/geta-notfoundhandler.git
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | true
41 | true
42 | None
43 | contentFiles\any\any\modules\_protected\Geta.NotFoundHandler.Optimizely
44 |
45 |
46 |
47 |
48 |
49 | true
50 | true
51 | None
52 | build\net6.0\$(MSBuildProjectName).targets
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Optimizely/Infrastructure/Configuration/OptimizelyNotFoundHandlerOptions.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Geta Digital. All rights reserved.
2 | // Licensed under Apache-2.0. See the LICENSE file in the project root for more information
3 |
4 | using System;
5 | using System.Collections.Generic;
6 | using Geta.NotFoundHandler.Core.Redirects;
7 | using Geta.NotFoundHandler.Optimizely.Core.AutomaticRedirects;
8 |
9 | namespace Geta.NotFoundHandler.Optimizely.Infrastructure.Configuration
10 | {
11 | public class OptimizelyNotFoundHandlerOptions
12 | {
13 | public const string Section = "Geta:NotFoundHandler:Optimizely";
14 | public const int CurrentDbVersion = 3;
15 |
16 | public bool AutomaticRedirectsEnabled { get; set; }
17 | public RedirectType AutomaticRedirectType { get; set; } = RedirectType.Temporary;
18 |
19 | private readonly List _contentKeyProviders = new();
20 | public IEnumerable ContentKeyProviders => _contentKeyProviders;
21 |
22 | private readonly List _contentLinkProviders = new();
23 | public IEnumerable ContentLinkProviders => _contentLinkProviders;
24 |
25 | private readonly List _contentUrlProviders = new();
26 | public IEnumerable ContentUrlProviders => _contentUrlProviders;
27 |
28 | public OptimizelyNotFoundHandlerOptions()
29 | {
30 | AddContentKeyProviders();
31 | AddContentLinkProviders();
32 | AddContentUrlProviders();
33 | }
34 |
35 | public OptimizelyNotFoundHandlerOptions AddContentKeyProviders()
36 | where T : IContentKeyProvider
37 | {
38 | var t = typeof(T);
39 | _contentKeyProviders.Add(t);
40 | return this;
41 | }
42 |
43 | public OptimizelyNotFoundHandlerOptions AddContentLinkProviders()
44 | where T : IContentLinkProvider
45 | {
46 | var t = typeof(T);
47 | _contentLinkProviders.Add(t);
48 | return this;
49 | }
50 |
51 | public OptimizelyNotFoundHandlerOptions AddContentUrlProviders()
52 | where T : IContentUrlProvider
53 | {
54 | var t = typeof(T);
55 | _contentUrlProviders.Add(t);
56 | return this;
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Optimizely/Infrastructure/Initialization/ApplicationBuilderExtensions.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Geta Digital. All rights reserved.
2 | // Licensed under Apache-2.0. See the LICENSE file in the project root for more information
3 |
4 | using Coravel.Scheduling.Schedule;
5 | using Coravel.Scheduling.Schedule.Interfaces;
6 | using Geta.NotFoundHandler.Core.ScheduledJobs.Suggestions;
7 | using Geta.NotFoundHandler.Optimizely.Core.AutomaticRedirects;
8 | using Geta.NotFoundHandler.Optimizely.Core.Events;
9 | using Microsoft.AspNetCore.Builder;
10 | using Microsoft.Extensions.DependencyInjection;
11 |
12 | namespace Geta.NotFoundHandler.Optimizely.Infrastructure.Initialization
13 | {
14 | public static class ApplicationBuilderExtensions
15 | {
16 | public static IApplicationBuilder UseOptimizelyNotFoundHandler(this IApplicationBuilder app)
17 | {
18 | var services = app.ApplicationServices;
19 |
20 | var upgrader = services.GetRequiredService();
21 | upgrader.Start();
22 |
23 | var syncEvents = services.GetRequiredService();
24 | syncEvents.Initialize();
25 |
26 | var historyEvents = services.GetRequiredService();
27 | historyEvents.Initialize();
28 |
29 | // For optimizely we will use built-in scheduler for this job
30 | var scheduler = services.GetService();
31 | (scheduler as Scheduler)?.TryUnschedule(nameof(SuggestionsCleanupJob));
32 |
33 | return app;
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Optimizely/Infrastructure/JobStatusLogger.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Geta Digital. All rights reserved.
2 | // Licensed under Apache-2.0. See the LICENSE file in the project root for more information
3 |
4 | using System;
5 | using System.Text;
6 |
7 | namespace Geta.NotFoundHandler.Optimizely.Infrastructure
8 | {
9 | internal class JobStatusLogger
10 | {
11 | private readonly Action _onStatusChanged;
12 |
13 | private readonly StringBuilder _stringBuilder = new();
14 |
15 | public JobStatusLogger(Action onStatusChanged)
16 | {
17 | _onStatusChanged = onStatusChanged;
18 | }
19 |
20 | public void Log(string message)
21 | {
22 | _stringBuilder.AppendLine(message);
23 | }
24 |
25 | public void LogWithStatus(string message)
26 | {
27 | message = $"{DateTime.UtcNow:yyyy-MM-dd hh:mm:ss} - {message}";
28 | Status(message);
29 | Log(message);
30 | }
31 |
32 | public void Status(string message)
33 | {
34 | _onStatusChanged?.Invoke(message);
35 | }
36 |
37 | public string ToString(string separator = "
")
38 | {
39 | return _stringBuilder?.ToString().Replace(Environment.NewLine, separator);
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/Geta.NotFoundHandler.Optimizely/MenuProvider.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Geta Digital. All rights reserved.
2 | // Licensed under Apache-2.0. See the LICENSE file in the project root for more information
3 |
4 | using System.Collections.Generic;
5 | using EPiServer.Shell;
6 | using EPiServer.Shell.Navigation;
7 |
8 | namespace Geta.NotFoundHandler.Optimizely
9 | {
10 | [MenuProvider]
11 | public class MenuProvider : IMenuProvider
12 | {
13 | public IEnumerable