├── dispatcher
├── src
│ ├── opt-in
│ │ └── USE_SOURCES_DIRECTLY
│ ├── conf.d
│ │ ├── enabled_vhosts
│ │ │ ├── ca.vhost
│ │ │ ├── us.vhost
│ │ │ ├── vhosts.conf
│ │ │ └── README
│ │ ├── variables
│ │ │ ├── custom.vars
│ │ │ └── global.vars
│ │ ├── rewrites
│ │ │ ├── rewrite.rules
│ │ │ ├── rewrite_ca.rules
│ │ │ ├── rewrite_us.rules
│ │ │ └── default_rewrite.rules
│ │ ├── available_vhosts
│ │ │ ├── ca.vhost
│ │ │ ├── us.vhost
│ │ │ └── default.vhost
│ │ └── dispatcher_vhost.conf
│ └── conf.dispatcher.d
│ │ ├── enabled_farms
│ │ ├── default.farm
│ │ ├── farms.any
│ │ └── README
│ │ ├── cache
│ │ ├── rules.any
│ │ ├── default_invalidate.any
│ │ ├── default_rules.any
│ │ └── marketing_query_parameters.any
│ │ ├── clientheaders
│ │ ├── clientheaders.any
│ │ └── default_clientheaders.any
│ │ ├── virtualhosts
│ │ ├── virtualhosts.any
│ │ └── default_virtualhosts.any
│ │ ├── renders
│ │ └── default_renders.any
│ │ ├── dispatcher.any
│ │ ├── filters
│ │ ├── filters.any
│ │ └── default_filters.any
│ │ └── available_farms
│ │ └── default.farm
├── assembly.xml
├── update_sdk.sh
└── README.md
├── .whitesource
├── docs
├── images
│ ├── report.png
│ ├── caconfig1.png
│ ├── caconfig2.png
│ ├── duplicate.png
│ └── IBM_iX_logo.png
└── developers.md
├── .gitattributes
├── ui.apps
├── src
│ └── main
│ │ └── content
│ │ ├── jcr_root
│ │ └── apps
│ │ │ ├── ibm
│ │ │ └── aem-tenant-specific-vanity-urls
│ │ │ │ ├── clientlibs
│ │ │ │ └── clientlib-author
│ │ │ │ │ ├── css.txt
│ │ │ │ │ ├── js.txt
│ │ │ │ │ ├── .content.xml
│ │ │ │ │ ├── css
│ │ │ │ │ └── backend-validation.css
│ │ │ │ │ └── js
│ │ │ │ │ ├── backend-validation.js
│ │ │ │ │ └── site.js
│ │ │ │ └── tools
│ │ │ │ └── report
│ │ │ │ ├── datasource
│ │ │ │ └── datasource.html
│ │ │ │ ├── dataitem
│ │ │ │ └── dataitem.html
│ │ │ │ └── page
│ │ │ │ └── .content.xml
│ │ │ └── cq
│ │ │ └── core
│ │ │ ├── .content.xml
│ │ │ └── content
│ │ │ ├── .content.xml
│ │ │ └── nav
│ │ │ ├── .content.xml
│ │ │ └── tools
│ │ │ ├── .content.xml
│ │ │ └── aem-tenant-specific-vanity-urls
│ │ │ ├── .content.xml
│ │ │ └── report
│ │ │ └── .content.xml
│ │ └── META-INF
│ │ └── vault
│ │ ├── definition
│ │ ├── thumbnail.png
│ │ └── .content.xml
│ │ └── filter.xml
└── pom.xml
├── renovate.json
├── .github
├── codeql
│ └── codeql-config.yml
└── workflows
│ ├── build.yml
│ └── codeql.yml
├── all
├── src
│ └── main
│ │ └── content
│ │ └── META-INF
│ │ └── vault
│ │ ├── definition
│ │ ├── thumbnail.png
│ │ └── .content.xml
│ │ └── filter.xml
└── pom.xml
├── examples
├── src
│ └── main
│ │ └── content
│ │ ├── META-INF
│ │ └── vault
│ │ │ ├── definition
│ │ │ ├── thumbnail.png
│ │ │ └── .content.xml
│ │ │ └── filter.xml
│ │ └── jcr_root
│ │ └── apps
│ │ └── ibm
│ │ └── aem-tenant-specific-vanity-urls-examples
│ │ ├── config.publish
│ │ ├── com.day.cq.rewriter.linkchecker.impl.LinkCheckerTransformerFactory.cfg.json
│ │ └── org.apache.sling.jcr.resource.internal.JcrResourceResolverFactoryImpl.cfg.json
│ │ └── config
│ │ └── org.apache.sling.jcr.repoinit.RepositoryInitializer~tenantspecificvanityurls.config
└── pom.xml
├── examples.content
├── src
│ └── main
│ │ └── content
│ │ ├── META-INF
│ │ └── vault
│ │ │ ├── definition
│ │ │ ├── thumbnail.png
│ │ │ └── .content.xml
│ │ │ └── filter.xml
│ │ └── jcr_root
│ │ └── conf
│ │ ├── TSVU-CA
│ │ ├── _sling_configs
│ │ │ ├── .content.xml
│ │ │ └── com.ibm.aem.aemtenantspecificvanityurls.core.caconfig.TenantSpecificVanityUrlConfig
│ │ │ │ └── .content.xml
│ │ ├── settings
│ │ │ ├── cloudconfigs
│ │ │ │ └── .content.xml
│ │ │ └── .content.xml
│ │ └── .content.xml
│ │ ├── TSVU-US
│ │ ├── _sling_configs
│ │ │ ├── .content.xml
│ │ │ └── com.ibm.aem.aemtenantspecificvanityurls.core.caconfig.TenantSpecificVanityUrlConfig
│ │ │ │ └── .content.xml
│ │ ├── settings
│ │ │ ├── cloudconfigs
│ │ │ │ └── .content.xml
│ │ │ └── .content.xml
│ │ └── .content.xml
│ │ └── TSVU-MAPPINGS
│ │ └── http
│ │ └── .content.xml
└── pom.xml
├── SECURITY.md
├── CHANGES.md
├── LICENSE
├── core
├── src
│ ├── main
│ │ └── java
│ │ │ └── com
│ │ │ └── ibm
│ │ │ └── aem
│ │ │ └── aemtenantspecificvanityurls
│ │ │ └── core
│ │ │ ├── util
│ │ │ ├── package-info.java
│ │ │ └── VanityUrlUtils.java
│ │ │ ├── caconfig
│ │ │ ├── package-info.java
│ │ │ └── TenantSpecificVanityUrlConfig.java
│ │ │ ├── model
│ │ │ └── report
│ │ │ │ ├── package-info.java
│ │ │ │ ├── ReportEntry.java
│ │ │ │ ├── ReportDataItem.java
│ │ │ │ ├── ReportDataSource.java
│ │ │ │ └── ReportService.java
│ │ │ ├── servlets
│ │ │ ├── package-info.java
│ │ │ └── TenantSpecificVanityUrlServlet.java
│ │ │ └── exceptions
│ │ │ └── AtsvuException.java
│ └── test
│ │ └── java
│ │ └── com
│ │ └── ibm
│ │ └── aem
│ │ └── aemtenantspecificvanityurls
│ │ └── core
│ │ ├── util
│ │ └── VanityUrlUtilsTest.java
│ │ ├── model
│ │ └── report
│ │ │ ├── ReportDataItemTest.java
│ │ │ ├── ReportDataSourceTest.java
│ │ │ └── ReportServiceTest.java
│ │ └── servlets
│ │ └── TenantSpecificVanityUrlServletTest.java
└── pom.xml
├── .gitignore
├── ui.apps.structure
└── pom.xml
└── README.md
/dispatcher/src/opt-in/USE_SOURCES_DIRECTLY:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/dispatcher/src/conf.d/enabled_vhosts/ca.vhost:
--------------------------------------------------------------------------------
1 | ../available_vhosts/ca.vhost
--------------------------------------------------------------------------------
/dispatcher/src/conf.d/enabled_vhosts/us.vhost:
--------------------------------------------------------------------------------
1 | ../available_vhosts/us.vhost
--------------------------------------------------------------------------------
/.whitesource:
--------------------------------------------------------------------------------
1 | {
2 | "settingsInheritedFrom": "ibm-mend-config/mend-config@main"
3 | }
--------------------------------------------------------------------------------
/dispatcher/src/conf.dispatcher.d/enabled_farms/default.farm:
--------------------------------------------------------------------------------
1 | ../available_farms/default.farm
--------------------------------------------------------------------------------
/docs/images/report.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IBM/aem-tenant-specific-vanity-urls/main/docs/images/report.png
--------------------------------------------------------------------------------
/docs/images/caconfig1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IBM/aem-tenant-specific-vanity-urls/main/docs/images/caconfig1.png
--------------------------------------------------------------------------------
/docs/images/caconfig2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IBM/aem-tenant-specific-vanity-urls/main/docs/images/caconfig2.png
--------------------------------------------------------------------------------
/docs/images/duplicate.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IBM/aem-tenant-specific-vanity-urls/main/docs/images/duplicate.png
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
2 |
3 | *.conf text
4 | *.vhost text
5 | *.rules text
6 | *.vars text
7 | *.any text
8 | magic text
--------------------------------------------------------------------------------
/docs/images/IBM_iX_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IBM/aem-tenant-specific-vanity-urls/main/docs/images/IBM_iX_logo.png
--------------------------------------------------------------------------------
/dispatcher/src/conf.dispatcher.d/enabled_farms/farms.any:
--------------------------------------------------------------------------------
1 | ## Include all of the customers *.farm files
2 | $include "./*.farm"
3 |
--------------------------------------------------------------------------------
/dispatcher/src/conf.d/enabled_vhosts/vhosts.conf:
--------------------------------------------------------------------------------
1 | ## Include all of the customers *.vhost files
2 | Include conf.d/enabled_vhosts/*.vhost
3 |
--------------------------------------------------------------------------------
/ui.apps/src/main/content/jcr_root/apps/ibm/aem-tenant-specific-vanity-urls/clientlibs/clientlib-author/css.txt:
--------------------------------------------------------------------------------
1 | #base=css
2 | backend-validation.css
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "config:recommended"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/dispatcher/src/conf.d/enabled_vhosts/README:
--------------------------------------------------------------------------------
1 | #
2 | # Enabled virtual hosts will be symlinked here. Their names should match the pattern '*.vhost'
3 | #
4 |
--------------------------------------------------------------------------------
/dispatcher/src/conf.dispatcher.d/enabled_farms/README:
--------------------------------------------------------------------------------
1 | #
2 | # Enabled farms will be symlinked here. Their names should match the pattern '*.farm'
3 | #
4 |
--------------------------------------------------------------------------------
/ui.apps/src/main/content/jcr_root/apps/ibm/aem-tenant-specific-vanity-urls/clientlibs/clientlib-author/js.txt:
--------------------------------------------------------------------------------
1 | #base=js
2 | backend-validation.js
3 | site.js
--------------------------------------------------------------------------------
/.github/codeql/codeql-config.yml:
--------------------------------------------------------------------------------
1 | name: "ATSVU CodeQL config"
2 |
3 | queries:
4 | - uses: security-and-quality
5 |
6 | paths-ignore:
7 | - '**/target/**/*.*'
--------------------------------------------------------------------------------
/all/src/main/content/META-INF/vault/definition/thumbnail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IBM/aem-tenant-specific-vanity-urls/main/all/src/main/content/META-INF/vault/definition/thumbnail.png
--------------------------------------------------------------------------------
/examples/src/main/content/META-INF/vault/definition/thumbnail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IBM/aem-tenant-specific-vanity-urls/main/examples/src/main/content/META-INF/vault/definition/thumbnail.png
--------------------------------------------------------------------------------
/ui.apps/src/main/content/META-INF/vault/definition/thumbnail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IBM/aem-tenant-specific-vanity-urls/main/ui.apps/src/main/content/META-INF/vault/definition/thumbnail.png
--------------------------------------------------------------------------------
/examples.content/src/main/content/META-INF/vault/definition/thumbnail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IBM/aem-tenant-specific-vanity-urls/main/examples.content/src/main/content/META-INF/vault/definition/thumbnail.png
--------------------------------------------------------------------------------
/dispatcher/src/conf.dispatcher.d/cache/rules.any:
--------------------------------------------------------------------------------
1 | #
2 | # This file contains the cache rules, and can be customized.
3 | #
4 | # By default, it includes the default rules.
5 | #
6 |
7 | $include "./default_rules.any"
8 |
--------------------------------------------------------------------------------
/ui.apps/src/main/content/jcr_root/apps/cq/core/.content.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/ui.apps/src/main/content/jcr_root/apps/ibm/aem-tenant-specific-vanity-urls/tools/report/datasource/datasource.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/all/src/main/content/META-INF/vault/filter.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/dispatcher/src/conf.d/variables/custom.vars:
--------------------------------------------------------------------------------
1 | #
2 | # This file contains the variables defined within a virtual host definition
3 | #
4 | # By default, it is empty and does not define any variable
5 | #
6 | Define CONTENT_FOLDER_NAME wknd
--------------------------------------------------------------------------------
/ui.apps/src/main/content/jcr_root/apps/cq/core/content/.content.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/ui.apps/src/main/content/jcr_root/apps/cq/core/content/nav/.content.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/examples/src/main/content/jcr_root/apps/ibm/aem-tenant-specific-vanity-urls-examples/config.publish/com.day.cq.rewriter.linkchecker.impl.LinkCheckerTransformerFactory.cfg.json:
--------------------------------------------------------------------------------
1 | {
2 | "linkcheckertransformer.stripHtmltExtension": true
3 | }
--------------------------------------------------------------------------------
/examples/src/main/content/jcr_root/apps/ibm/aem-tenant-specific-vanity-urls-examples/config.publish/org.apache.sling.jcr.resource.internal.JcrResourceResolverFactoryImpl.cfg.json:
--------------------------------------------------------------------------------
1 | {
2 | "resource.resolver.map.location": "/conf/TSVU-MAPPINGS"
3 | }
--------------------------------------------------------------------------------
/ui.apps/src/main/content/jcr_root/apps/cq/core/content/nav/tools/.content.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/dispatcher/src/conf.dispatcher.d/clientheaders/clientheaders.any:
--------------------------------------------------------------------------------
1 | #
2 | # This file contains the request headers, and can be customized.
3 | #
4 | # By default, it includes the default client headers.
5 | #
6 |
7 | $include "./default_clientheaders.any"
8 |
--------------------------------------------------------------------------------
/examples.content/src/main/content/jcr_root/conf/TSVU-CA/_sling_configs/.content.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
--------------------------------------------------------------------------------
/examples.content/src/main/content/jcr_root/conf/TSVU-US/_sling_configs/.content.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
--------------------------------------------------------------------------------
/examples.content/src/main/content/jcr_root/conf/TSVU-CA/settings/cloudconfigs/.content.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
--------------------------------------------------------------------------------
/examples.content/src/main/content/jcr_root/conf/TSVU-US/settings/cloudconfigs/.content.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
--------------------------------------------------------------------------------
/all/src/main/content/META-INF/vault/definition/.content.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/examples.content/src/main/content/META-INF/vault/filter.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/examples/src/main/content/META-INF/vault/definition/.content.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/ui.apps/src/main/content/META-INF/vault/definition/.content.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/examples.content/src/main/content/META-INF/vault/definition/.content.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/dispatcher/src/conf.dispatcher.d/virtualhosts/virtualhosts.any:
--------------------------------------------------------------------------------
1 | #
2 | # This file contains the list of virtual hosts (or domain names) that the dispatcher
3 | # will handle.
4 | #
5 | # By default, it includes the default list of virtual hosts.
6 | #
7 |
8 | $include "./default_virtualhosts.any"
9 |
--------------------------------------------------------------------------------
/examples.content/src/main/content/jcr_root/conf/TSVU-CA/settings/.content.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/examples.content/src/main/content/jcr_root/conf/TSVU-US/settings/.content.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/examples.content/src/main/content/jcr_root/conf/TSVU-CA/.content.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/examples.content/src/main/content/jcr_root/conf/TSVU-US/.content.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/dispatcher/src/conf.dispatcher.d/renders/default_renders.any:
--------------------------------------------------------------------------------
1 | #
2 | # This is the default list of backends that the dispatcher contacts.
3 | #
4 | # DO NOT EDIT this file, your changes will have no impact on your deployment.
5 | #
6 |
7 | /0 {
8 | /hostname "${AEM_HOST}"
9 | /port "${AEM_PORT}"
10 | /timeout "10000"
11 | }
12 |
--------------------------------------------------------------------------------
/dispatcher/src/conf.dispatcher.d/virtualhosts/default_virtualhosts.any:
--------------------------------------------------------------------------------
1 | #
2 | # This is the default list of virtual hosts (or domain names) that the dispatcher
3 | # will handle.
4 | #
5 | # DO NOT EDIT this file, your changes will have no impact on your deployment.
6 | #
7 | # Instead modify virtualhosts.any.
8 | #
9 |
10 | "*"
11 |
--------------------------------------------------------------------------------
/ui.apps/src/main/content/jcr_root/apps/cq/core/content/nav/tools/aem-tenant-specific-vanity-urls/.content.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 | | Version | Supported |
6 | | ------- | ------------------ |
7 | | 1.x | :white_check_mark: |
8 | | < 1.0 | :x: |
9 |
10 | ## Reporting a Vulnerability
11 |
12 | Please create an issue and provide details like the attack vector or examples for exploitation.
13 |
--------------------------------------------------------------------------------
/dispatcher/src/conf.dispatcher.d/dispatcher.any:
--------------------------------------------------------------------------------
1 | #
2 | # This is a file provided by the runtime environment and only included for
3 | # illustration purposes.
4 | #
5 | # DO NOT EDIT this file, your changes will have no impact on your deployment.
6 | #
7 |
8 | /farms {
9 | # Include all *.farm files in enabled_farms
10 | $include "enabled_farms/*.farm"
11 | }
12 |
--------------------------------------------------------------------------------
/ui.apps/src/main/content/META-INF/vault/filter.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/examples/src/main/content/META-INF/vault/filter.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/examples.content/src/main/content/jcr_root/conf/TSVU-CA/_sling_configs/com.ibm.aem.aemtenantspecificvanityurls.core.caconfig.TenantSpecificVanityUrlConfig/.content.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/examples.content/src/main/content/jcr_root/conf/TSVU-US/_sling_configs/com.ibm.aem.aemtenantspecificvanityurls.core.caconfig.TenantSpecificVanityUrlConfig/.content.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/ui.apps/src/main/content/jcr_root/apps/ibm/aem-tenant-specific-vanity-urls/clientlibs/clientlib-author/.content.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
--------------------------------------------------------------------------------
/CHANGES.md:
--------------------------------------------------------------------------------
1 | # Release History
2 | * 1.2.1
3 | * Fixed report for pages that have multiple vanity URLs set
4 |
5 | * 1.2.0
6 | * Check if vanity path is already existing
7 |
8 | * 1.1.0
9 | * Added possibility to auto-convert vanity values to lower-case
10 | * More example vanity URLs
11 |
12 | * 1.0.1
13 | * Fixed visibility of CA config
14 |
15 | * 1.0.0
16 | * Added vanity URL report tool
17 |
18 | * 0.9.1
19 | * Initial release
20 |
--------------------------------------------------------------------------------
/dispatcher/src/conf.dispatcher.d/cache/default_invalidate.any:
--------------------------------------------------------------------------------
1 | #
2 | # This is the default list of hosts that are allowed to invalidate (flush) the cache
3 | # on the dispatcher.
4 | #
5 | # DO NOT EDIT this file, your changes will have no impact on your deployment.
6 | #
7 |
8 | # Contains the AEM backend that is allowed to invalidate the cache on the dispatcher
9 | /0001 {
10 | /type "deny"
11 | /glob "*.*.*.*"
12 | }
13 | /0002 {
14 | /type "allow"
15 | /glob "${AEM_IP}"
16 | }
17 |
--------------------------------------------------------------------------------
/ui.apps/src/main/content/jcr_root/apps/cq/core/content/nav/tools/aem-tenant-specific-vanity-urls/report/.content.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/dispatcher/src/conf.dispatcher.d/filters/filters.any:
--------------------------------------------------------------------------------
1 | #
2 | # This file contains the filter ACL, and can be customized.
3 | #
4 | # By default, it includes the default filter ACL.
5 | #
6 |
7 | $include "./default_filters.any"
8 |
9 | # Allow components JSON model
10 | /0101 { /type "allow" /extension "json" /selectors "model" /path "/content/*" }
11 |
12 | # Allow manifest.webmanifest files located in the content
13 | /0102 { /type "allow" /extension "webmanifest" /path "/content/*/manifest" }
14 |
15 | # allow vanity URLs
16 | /0200 { /type "allow" /method '(GET)' /url '^/[a-zA-Z0-9_-]+$' }
17 |
--------------------------------------------------------------------------------
/ui.apps/src/main/content/jcr_root/apps/ibm/aem-tenant-specific-vanity-urls/tools/report/dataitem/dataitem.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | ${dataItem.vanityUrl}
5 |
6 |
7 | ${dataItem.mappedPath}
8 |
9 |
10 | Edit
11 |
12 |
--------------------------------------------------------------------------------
/examples.content/src/main/content/jcr_root/conf/TSVU-MAPPINGS/http/.content.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/dispatcher/assembly.xml:
--------------------------------------------------------------------------------
1 |
4 | distribution
5 |
6 | zip
7 |
8 | false
9 |
10 |
11 | ${basedir}/src
12 |
13 | **/*
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/dispatcher/src/conf.d/rewrites/rewrite.rules:
--------------------------------------------------------------------------------
1 | #
2 | # This file contains the rewrite rules, and can be customized.
3 | #
4 | # By default, it includes just the rewrite rules. You can
5 | # add rewrite rules to this file but you should still include
6 | # the default rewrite rules.
7 |
8 | Include conf.d/rewrites/default_rewrite.rules
9 |
10 | # rewrite for root redirect
11 | RewriteRule ^/?$ /content/${CONTENT_FOLDER_NAME}/us/en.html [PT,L]
12 |
13 | RewriteCond %{REQUEST_URI} !^/apps
14 | RewriteCond %{REQUEST_URI} !^/bin
15 | RewriteCond %{REQUEST_URI} !^/content
16 | RewriteCond %{REQUEST_URI} !^/etc
17 | RewriteCond %{REQUEST_URI} !^/home
18 | RewriteCond %{REQUEST_URI} !^/libs
19 | RewriteCond %{REQUEST_URI} !^/saml_login
20 | RewriteCond %{REQUEST_URI} !^/system
21 | RewriteCond %{REQUEST_URI} !^/tmp
22 | RewriteCond %{REQUEST_URI} !^/var
23 | RewriteCond %{REQUEST_URI} (.html|.jpe?g|.png|.svg)$
24 | RewriteRule ^/(.*)$ /content/${CONTENT_FOLDER_NAME}/$1 [PT,L]
25 |
--------------------------------------------------------------------------------
/dispatcher/src/conf.dispatcher.d/clientheaders/default_clientheaders.any:
--------------------------------------------------------------------------------
1 | #
2 | # This is the default list of request headers to forward to AEM.
3 | #
4 | # DO NOT EDIT this file, your changes will have no impact on your deployment.
5 | #
6 | # Instead modify clientheaders.any.
7 | #
8 |
9 | "X-Forwarded-Proto"
10 | "X-Forwarded-SSL-Certificate"
11 | "X-Forwarded-SSL-Client-Cert"
12 | "X-Forwarded-SSL"
13 | "X-Forwarded-Protocol"
14 | "CSRF-Token"
15 | "referer"
16 | "user-agent"
17 | "from"
18 | "content-type"
19 | "content-length"
20 | "accept-charset"
21 | "accept-encoding"
22 | "accept-language"
23 | "accept"
24 | "host"
25 | "if-match"
26 | "if-none-match"
27 | "if-range"
28 | "if-unmodified-since"
29 | "max-forwards"
30 | "range"
31 | "cookie"
32 | "depth"
33 | "translate"
34 | "expires"
35 | "date"
36 | "if"
37 | "lock-token"
38 | "x-expected-entity-length"
39 | "destination"
40 | "Sling-uploadmode"
41 | "x-requested-with"
42 | "If-Modified-Since"
43 | "Authorization"
44 | "x-request-id"
45 |
--------------------------------------------------------------------------------
/dispatcher/src/conf.d/rewrites/rewrite_ca.rules:
--------------------------------------------------------------------------------
1 | #
2 | # Rewrites for http://ca.vanity.local:8080/
3 | #
4 |
5 | Include conf.d/rewrites/default_rewrite.rules
6 |
7 | # rewrite for root redirect
8 | RewriteRule ^/?$ /content/${CONTENT_FOLDER_NAME}/ca/en.html [PT,L]
9 |
10 | # readd .html
11 | RewriteCond %{REQUEST_URI} !(.html|.jpe?g|.png|.svg|.json)$
12 | RewriteCond %{REQUEST_URI} !^/etc
13 | RewriteCond %{REQUEST_URI} !^/conf/
14 | RewriteRule ^/(.*)$ /content/${CONTENT_FOLDER_NAME}/ca/en/$1.html [PT,L]
15 |
16 | RewriteCond %{REQUEST_URI} !^/apps
17 | RewriteCond %{REQUEST_URI} !^/bin
18 | RewriteCond %{REQUEST_URI} !^/content
19 | RewriteCond %{REQUEST_URI} !^/etc
20 | RewriteCond %{REQUEST_URI} !^/home
21 | RewriteCond %{REQUEST_URI} !^/libs
22 | RewriteCond %{REQUEST_URI} !^/saml_login
23 | RewriteCond %{REQUEST_URI} !^/system
24 | RewriteCond %{REQUEST_URI} !^/tmp
25 | RewriteCond %{REQUEST_URI} !^/var
26 | RewriteCond %{REQUEST_URI} (.html|.jpe?g|.png|.svg)$
27 | RewriteRule ^/(.*)$ /content/${CONTENT_FOLDER_NAME}/ca/en/$1 [PT,L]
28 |
29 |
--------------------------------------------------------------------------------
/dispatcher/src/conf.d/rewrites/rewrite_us.rules:
--------------------------------------------------------------------------------
1 | #
2 | # Rewrites for http://us.vanity.local:8080/
3 | #
4 |
5 | Include conf.d/rewrites/default_rewrite.rules
6 |
7 | # rewrite for root redirect
8 | RewriteRule ^/?$ /content/${CONTENT_FOLDER_NAME}/us/en.html [PT,L]
9 |
10 | # readd .html
11 | RewriteCond %{REQUEST_URI} !(.html|.jpe?g|.png|.svg|.json)$
12 | RewriteCond %{REQUEST_URI} !^/etc
13 | RewriteCond %{REQUEST_URI} !^/conf/
14 | RewriteRule ^/(.*)$ /content/${CONTENT_FOLDER_NAME}/us/en/$1.html [PT,L]
15 |
16 | RewriteCond %{REQUEST_URI} !^/apps
17 | RewriteCond %{REQUEST_URI} !^/bin
18 | RewriteCond %{REQUEST_URI} !^/content
19 | RewriteCond %{REQUEST_URI} !^/etc
20 | RewriteCond %{REQUEST_URI} !^/home
21 | RewriteCond %{REQUEST_URI} !^/libs
22 | RewriteCond %{REQUEST_URI} !^/saml_login
23 | RewriteCond %{REQUEST_URI} !^/system
24 | RewriteCond %{REQUEST_URI} !^/tmp
25 | RewriteCond %{REQUEST_URI} !^/var
26 | RewriteCond %{REQUEST_URI} (.html|.jpe?g|.png|.svg)$
27 | RewriteRule ^/(.*)$ /content/${CONTENT_FOLDER_NAME}/us/en/$1 [PT,L]
28 |
29 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 International Business Machines
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/dispatcher/src/conf.dispatcher.d/cache/default_rules.any:
--------------------------------------------------------------------------------
1 | #
2 | # These are the default cache rules.
3 | #
4 | # DO NOT EDIT this file, your changes will have no impact on your deployment.
5 | #
6 | # Instead modify rules.any.
7 | #
8 |
9 | # Put entries of items you do or don't want to cache in apaches doc root
10 | # the globbing pattern to be compared against the url
11 | # example: * -> everything
12 | # : /foo/bar.* -> only the /foo/bar documents
13 | # : /foo/bar/* -> all pages below /foo/bar
14 | # : /foo/bar[./]* -> all pages below and /foo/bar itself
15 | # : *.html -> all .html files
16 | # Default allow all items to cache
17 | /0000 {
18 | /glob "*"
19 | /type "allow"
20 | }
21 | # Don't cache csrf login tokens
22 | /0001 {
23 | /glob "/libs/granite/csrf/token.json"
24 | /type "deny"
25 | }
26 |
27 | # AEM Screens cache rules
28 | # Do not cache Screens channels json
29 | /0010 {
30 | /glob "/content/screens/svc.channels.json"
31 | /type "deny"
32 | }
33 | /0011 {
34 | /glob "/content/screens/svc/channels.channels.json"
35 | /type "deny"
36 | }
37 | /0012 {
38 | /glob "/screens/channels.json"
39 | /type "deny"
40 | }
41 |
--------------------------------------------------------------------------------
/core/src/main/java/com/ibm/aem/aemtenantspecificvanityurls/core/util/package-info.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2025 IBM iX
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
5 | * associated documentation files (the "Software"), to deal in the Software without restriction,
6 | * including without limitation the rights to use, copy, modify, merge, publish, distribute,
7 | * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
8 | * furnished to do so, subject to the following conditions:
9 | *
10 | * The above copyright notice and this permission notice shall be included in all copies or
11 | * substantial portions of the Software.
12 | *
13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
14 | * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
15 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
16 | * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
17 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
18 | */
19 | @Version("1.2.0")
20 | package com.ibm.aem.aemtenantspecificvanityurls.core.util;
21 |
22 | import org.osgi.annotation.versioning.Version;
23 |
--------------------------------------------------------------------------------
/core/src/main/java/com/ibm/aem/aemtenantspecificvanityurls/core/caconfig/package-info.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2023 IBM iX
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
5 | * associated documentation files (the "Software"), to deal in the Software without restriction,
6 | * including without limitation the rights to use, copy, modify, merge, publish, distribute,
7 | * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
8 | * furnished to do so, subject to the following conditions:
9 | *
10 | * The above copyright notice and this permission notice shall be included in all copies or
11 | * substantial portions of the Software.
12 | *
13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
14 | * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
15 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
16 | * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
17 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
18 | */
19 | @Version("1.1.0")
20 | package com.ibm.aem.aemtenantspecificvanityurls.core.caconfig;
21 |
22 | import org.osgi.annotation.versioning.Version;
23 |
--------------------------------------------------------------------------------
/core/src/main/java/com/ibm/aem/aemtenantspecificvanityurls/core/model/report/package-info.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2023 IBM iX
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
5 | * associated documentation files (the "Software"), to deal in the Software without restriction,
6 | * including without limitation the rights to use, copy, modify, merge, publish, distribute,
7 | * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
8 | * furnished to do so, subject to the following conditions:
9 | *
10 | * The above copyright notice and this permission notice shall be included in all copies or
11 | * substantial portions of the Software.
12 | *
13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
14 | * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
15 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
16 | * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
17 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
18 | */
19 | @Version("1.0")
20 | package com.ibm.aem.aemtenantspecificvanityurls.core.model.report;
21 |
22 | import org.osgi.annotation.versioning.Version;
--------------------------------------------------------------------------------
/core/src/main/java/com/ibm/aem/aemtenantspecificvanityurls/core/servlets/package-info.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2025 IBM iX
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
5 | * associated documentation files (the "Software"), to deal in the Software without restriction,
6 | * including without limitation the rights to use, copy, modify, merge, publish, distribute,
7 | * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
8 | * furnished to do so, subject to the following conditions:
9 | *
10 | * The above copyright notice and this permission notice shall be included in all copies or
11 | * substantial portions of the Software.
12 | *
13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
14 | * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
15 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
16 | * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
17 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
18 | */
19 | @Version("1.2.0")
20 | package com.ibm.aem.aemtenantspecificvanityurls.core.servlets;
21 |
22 | import org.osgi.annotation.versioning.Version;
23 |
--------------------------------------------------------------------------------
/dispatcher/src/conf.d/variables/global.vars:
--------------------------------------------------------------------------------
1 | #
2 | # This file contains the variables defined for all virtual hosts
3 | #
4 |
5 | # Log level for the dispatcher
6 | #
7 | # Possible values are: Error, Warn, Info, Debug and Trace1
8 | # Default value: Warn
9 | #
10 | # Define DISP_LOG_LEVEL Warn
11 |
12 | # Log level for mod_rewrite
13 | #
14 | # Possible values are: Error, Warn, Info, Debug and Trace1 - Trace8
15 | # Default value: Warn
16 | #
17 | # To debug your RewriteRules, it is recommended to raise your log
18 | # level to Trace2.
19 | #
20 | # More information can be found at:
21 | # https://httpd.apache.org/docs/current/mod/mod_rewrite.html#logging
22 | #
23 | # Define REWRITE_LOG_LEVEL Warn
24 |
25 | # Disable default caching headers
26 | #
27 | # The following headers are set by default dispatcher configuration Expires, Cache-Control, Age.
28 | # If you uncomment and define DISABLE_DEFAULT_CACHING variable these headers are not set any more
29 | # and you can fully customize the caching behavior.
30 | #
31 | # Define DISABLE_DEFAULT_CACHING
32 |
33 | # Enable caching for GraphQL persisted queries
34 | #
35 | # By default, GraphQL persisted query responses are not cached in dispatcher.
36 | # If you uncomment and define CACHE_GRAPHQL_PERSISTED_QUERIES variable, then persisted query results
37 | # will be cached in dispatcher. Using CORS, in that case, will require additional dispatcher configuration.
38 | #
39 | # Define CACHE_GRAPHQL_PERSISTED_QUERIES
40 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Java CI
2 | on:
3 | push:
4 | branches:
5 | - main
6 | pull_request:
7 | types: [opened, synchronize, reopened]
8 | jobs:
9 | build:
10 | name: Build and analyze
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v6
14 | with:
15 | fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
16 | - name: Set up JDK 11
17 | uses: actions/setup-java@v5
18 | with:
19 | java-version: 11
20 | distribution: 'zulu' # Alternative distribution options are available.
21 | - name: Cache SonarCloud packages
22 | uses: actions/cache@v4
23 | with:
24 | path: ~/.sonar/cache
25 | key: ${{ runner.os }}-sonar
26 | restore-keys: ${{ runner.os }}-sonar
27 | - name: Cache Maven packages
28 | uses: actions/cache@v4
29 | with:
30 | path: ~/.m2
31 | key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
32 | restore-keys: ${{ runner.os }}-m2
33 | - name: Build with Maven
34 | run: mvn clean install javadoc:javadoc
35 | - name: Build and analyze
36 | env:
37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
38 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
39 | run: mvn -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=aem-tenant-specific-vanity-urls_aem-tenant-specific-vanity-urls
40 |
--------------------------------------------------------------------------------
/dispatcher/src/conf.dispatcher.d/cache/marketing_query_parameters.any:
--------------------------------------------------------------------------------
1 | #
2 | # These are the marketing parameters ignored for the dispatcher.
3 | #
4 | # Marketing parameters rarely have impact on what content is loaded
5 | # If your website is using marketing campaigns that do not influence the content
6 | # of your website enable the parameters that you expect or add others to enable
7 | # caching of in the dispatcher.
8 |
9 | # Commonly used
10 | # /1001 { /glob "cid" /type "allow" }
11 | # /1002 { /glob "partnerId" /type "allow" }
12 |
13 | # Urchin Tracking Module base parameters.
14 | /1010 { /glob "utm_content" /type "allow" }
15 | /1011 { /glob "utm_source" /type "allow" }
16 | /1012 { /glob "utm_medium" /type "allow" }
17 | /1013 { /glob "utm_campaign" /type "allow" }
18 | /1014 { /glob "utm_term" /type "allow" }
19 |
20 | # /1015 { /glob "utm_audience" /type "allow" }
21 | # /1016 { /glob "utm_creative" /type "allow" }
22 |
23 | # /1017 { /glob "utm_source_platform" /type "allow" } # Google Analytics 4
24 | # /1018 { /glob "utm_creative_format" /type "allow" } # Google Analytics 4
25 | # /1019 { /glob "utm_marketing_tactic" /type "allow" } # Google Analytics 4
26 |
27 | /1030 { /glob "gclid" /type "allow" } # Google Ads
28 | /1031 { /glob "gclsrc" /type "allow" } # Google Ads
29 |
30 | /1040 { /glob "wbraid" /type "allow" } # Parameter for iOS14+
31 | /1041 { /glob "gbraid" /type "allow" } # Parameter for iOS14+
32 |
33 | /1050 { /glob "dcli" /type "allow" } # DoubleClick click identifier
34 | /1051 { /glob "ga" /type "allow" }
35 | # /1052 { /glob "_ga" /type "allow" }
36 |
37 | /1060 { /glob "fbclid" /type "allow" } # Facebook click identifier
38 |
39 | /1070 { /glob "twclid" /type "allow" } # Twitter click identifier
40 |
--------------------------------------------------------------------------------
/core/src/test/java/com/ibm/aem/aemtenantspecificvanityurls/core/util/VanityUrlUtilsTest.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2025 IBM iX
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
5 | * associated documentation files (the "Software"), to deal in the Software without restriction,
6 | * including without limitation the rights to use, copy, modify, merge, publish, distribute,
7 | * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
8 | * furnished to do so, subject to the following conditions:
9 | *
10 | * The above copyright notice and this permission notice shall be included in all copies or
11 | * substantial portions of the Software.
12 | *
13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
14 | * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
15 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
16 | * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
17 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
18 | */
19 | package com.ibm.aem.aemtenantspecificvanityurls.core.util;
20 |
21 | import org.junit.jupiter.api.Test;
22 | import org.junit.jupiter.api.extension.ExtendWith;
23 | import org.mockito.junit.jupiter.MockitoExtension;
24 |
25 | import static org.junit.jupiter.api.Assertions.assertEquals;
26 |
27 | @ExtendWith(MockitoExtension.class)
28 | class VanityUrlUtilsTest {
29 |
30 | @Test
31 | void testPrependPrefixIfMissing() {
32 | assertEquals("/us/en_wow", VanityUrlUtils.prependPrefixIfMissing("wow", "/us/en_"));
33 | assertEquals("/us/en/wow", VanityUrlUtils.prependPrefixIfMissing("wow", "/us/en/"));
34 |
35 | assertEquals("/us/en/wow", VanityUrlUtils.prependPrefixIfMissing("/us/en/wow", "/us/en/"));
36 | }
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.gitignore.io/api/eclipse,java,maven
2 |
3 | ### Eclipse ###
4 | *.pydevproject
5 | .metadata
6 | .gradle
7 | bin/
8 | tmp/
9 | *.tmp
10 | *.bak
11 | *.swp
12 | *~.nib
13 | local.properties
14 | .settings/
15 | .loadpath
16 |
17 | # Eclipse Core
18 | .project
19 |
20 | # External tool builders
21 | .externalToolBuilders/
22 |
23 | # Escape dummy_sdk bin folder
24 | !dispatcher.cloud/test/dummy_sdk/bin
25 |
26 | # Locally stored "Eclipse launch configurations"
27 | *.launch
28 |
29 | # CDT-specific
30 | .cproject
31 |
32 | # JDT-specific (Eclipse Java Development Tools)
33 | .classpath
34 |
35 | # Java annotation processor (APT)
36 | .factorypath
37 |
38 | # PDT-specific
39 | .buildpath
40 |
41 | # sbteclipse plugin
42 | .target
43 |
44 | # TeXlipse plugin
45 | .texlipse
46 |
47 |
48 | ### Java ###
49 | *.class
50 |
51 | # Mobile Tools for Java (J2ME)
52 | .mtj.tmp/
53 |
54 | # Package Files #
55 | *.jar
56 | *.war
57 | *.ear
58 |
59 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
60 | hs_err_pid*
61 |
62 |
63 | ### Maven ###
64 | target/
65 | pom.xml.tag
66 | pom.xml.releaseBackup
67 | pom.xml.versionsBackup
68 | pom.xml.next
69 | release.properties
70 | dependency-reduced-pom.xml
71 | buildNumber.properties
72 | .mvn/timing.properties
73 |
74 |
75 | ### Vault ###
76 | .vlt
77 |
78 |
79 | ### IntelliJ ###
80 | .idea/
81 | *.iml
82 |
83 |
84 | ### Node.js ###
85 |
86 | # Log files
87 | *.log
88 |
89 | # NPM
90 | node_modules/
91 | yarn.lock
92 |
93 | # Webpack
94 | build/
95 | dist/
96 | actions/common/
97 |
98 | # Frontend Maven Plugin
99 | node/
100 |
101 | # Tests
102 | coverage/
103 | reports/
104 |
105 | # Others
106 | dispatcher/src/conf.d/variables/default.vars
107 | ui.tests/test-module/assets/form/themes/**/*diff.png
108 | ui.tests/test-module/assets/form/themes/**/*current.png
109 |
110 | .DS_Store
111 |
--------------------------------------------------------------------------------
/core/src/main/java/com/ibm/aem/aemtenantspecificvanityurls/core/exceptions/AtsvuException.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2023 IBM iX
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
5 | * associated documentation files (the "Software"), to deal in the Software without restriction,
6 | * including without limitation the rights to use, copy, modify, merge, publish, distribute,
7 | * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
8 | * furnished to do so, subject to the following conditions:
9 | *
10 | * The above copyright notice and this permission notice shall be included in all copies or
11 | * substantial portions of the Software.
12 | *
13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
14 | * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
15 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
16 | * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
17 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
18 | */
19 | package com.ibm.aem.aemtenantspecificvanityurls.core.exceptions;
20 |
21 | /**
22 | * Exception type for AEM Tenant Specific Vanity URLs.
23 | *
24 | * @author Roland Gruber
25 | */
26 | public class AtsvuException extends Exception {
27 |
28 | private static final long serialVersionUID = 1L;
29 |
30 | /**
31 | * Constructor
32 | *
33 | * @param message error message
34 | * @param e original exception
35 | */
36 | public AtsvuException(String message, Throwable e) {
37 | super(message, e);
38 | }
39 |
40 | /**
41 | * Constructor
42 | *
43 | * @param message error message
44 | */
45 | public AtsvuException(String message) {
46 | super(message);
47 | }
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/dispatcher/update_sdk.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Copyright 2022 Adobe Systems Incorporated
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 |
17 | function usage() {
18 | echo "Usage: ./update_sdk.sh [dispatcher config directory]"
19 | echo "Example 1: ./update_sdk.sh /opt/dispatcher-sdks/dispatcher-sdk-2.0.116"
20 | echo "Example 2: ./update_sdk.sh /opt/dispatcher-sdks/dispatcher-sdk-2.0.116 src"
21 | }
22 |
23 | if [[ -z "$1" ]]; then
24 | echo "You have to specify a path to an SDK as a parameter!"
25 | usage
26 | exit -1
27 | fi
28 |
29 | sdkPath="$1"
30 |
31 | if [[ ! -e "${sdkPath}/bin/docker_run.sh" ]]; then
32 | echo "Cannot find docker_run.sh file in '${sdkPath}/bin/docker_run.sh'."
33 | usage
34 | exit -1
35 | fi
36 |
37 | dispatcherVersion=$(cat ${sdkPath}/bin/docker_run.sh | grep version= | cut -f2 -d '=')
38 |
39 | if [[ -z "$sdkPath" ]]; then
40 | echo "Cannot evaluate SDK. Is it a valid path to a dispatcher SDK?"
41 | usage
42 | exit -1
43 | fi
44 |
45 | echo "Attempting to upgrade to dispatcher SDK version $dispatcherVersion..."
46 |
47 | if [[ ! -e "${sdkPath}/bin/update_maven.sh" ]]; then
48 | echo "The dispatcher SDK version that you have chosen does not yet support updates."
49 | exit -1
50 | fi
51 |
52 | if [[ -z "$2" ]]; then
53 | scriptDir=$(dirname "$0")
54 | configPath=$scriptDir/src
55 | else
56 | configPath="$2"
57 | fi
58 |
59 |
60 | $sdkPath/bin/update_maven.sh "$configPath"
61 |
--------------------------------------------------------------------------------
/docs/developers.md:
--------------------------------------------------------------------------------
1 | # Developer Area
2 |
3 | ## Local Testing
4 |
5 | For local testing please run an author and a publish instance on default ports (4502/4503).
6 | The dispatcher can be started with the dispatcher SDK from Adobe (part of cloud SDK).
7 |
8 | * Link its "src" directory to this repo "dispatcher/src"
9 | * Run `./bin/docker_run.sh src host.docker.internal:4503 8080`
10 |
11 | If you need to clean the cache delete the content of the "cache" directory.
12 |
13 | Required entries in "/etc/hosts":
14 |
15 | ```
16 | 127.0.0.1 ca.vanity.local
17 | 127.0.0.1 us.vanity.local
18 | ```
19 |
20 | Test URLs:
21 |
22 | * http://us.vanity.local:8080/wow
23 | * http://ca.vanity.local:8080/wow
24 |
25 | ## Publish a Release
26 |
27 | * Update CHANGES.md if needed
28 | * `mvn release:clean release:prepare`
29 | * `mvn release:perform`
30 | * Add GitHub release from tag https://github.com/IBM/aem-tenant-specific-vanity-urls/tags
31 | * attach "all" and "examples" packages
32 |
33 | ## Requirements for Release Publishing
34 |
35 | Follow these steps to get authorized to perform a release.
36 |
37 | * Create a PGP key: `gpg --gen-key`
38 | * Upload your public key to e.g. http://keyserver.ubuntu.com:11371/
39 | * `brew install pinentry-mac`
40 | * Update your "~/.zshrc" and add `export GPG_TTY=$(tty)`, then close all open terminals and exit the terminal app
41 | * Create/update "~/.gnupg/gpg-agent.conf" and add `pinentry-program /opt/homebrew/bin/pinentry-mac`
42 | * `gpgconf --kill gpg-agent`
43 | * Request publish rights for Maven Central at "com.ibm.aem" (https://central.sonatype.org/register/central-portal/).
44 | The request needs to be approved by someone who already has this right.
45 | * Follow https://central.sonatype.org/publish/generate-portal-token/ to get your user token and add it to your .m2/settings.xml file:
46 |
47 | ```
48 |
49 |
50 |
51 | central
52 | token-id
53 | token-value
54 |
55 |
56 |
57 | ```
58 |
--------------------------------------------------------------------------------
/core/src/main/java/com/ibm/aem/aemtenantspecificvanityurls/core/util/VanityUrlUtils.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2025 IBM iX
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
5 | * associated documentation files (the "Software"), to deal in the Software without restriction,
6 | * including without limitation the rights to use, copy, modify, merge, publish, distribute,
7 | * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
8 | * furnished to do so, subject to the following conditions:
9 | *
10 | * The above copyright notice and this permission notice shall be included in all copies or
11 | * substantial portions of the Software.
12 | *
13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
14 | * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
15 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
16 | * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
17 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
18 | */
19 | package com.ibm.aem.aemtenantspecificvanityurls.core.util;
20 |
21 | public final class VanityUrlUtils {
22 |
23 | private VanityUrlUtils() {
24 | // empty
25 | }
26 |
27 | /**
28 | * Prepends the specified prefix to the given vanity path if it is not already a descendant of the prefix.
29 | * Example:
30 | *
31 | * prependPrefixIfMissing("wow", "/us/en/") = "/us/en/wow"
32 | *
33 | *
34 | * @param vanityPath the vanity path to check and potentially prepend the prefix to
35 | * @param prefix the prefix to prepend if the vanity path is not already under it
36 | * @return the resulting path with the prefix prepended if necessary
37 | */
38 | public static String prependPrefixIfMissing(final String vanityPath, final String prefix) {
39 | if (vanityPath.startsWith(prefix)) {
40 | return vanityPath;
41 | }
42 | return prefix + vanityPath;
43 | }
44 |
45 | }
46 |
--------------------------------------------------------------------------------
/dispatcher/src/conf.d/available_vhosts/ca.vhost:
--------------------------------------------------------------------------------
1 | #
2 | # Vhost for http://ca.vanity.local:8080/
3 | #
4 |
5 | # Include customer defined variables
6 | Include conf.d/variables/custom.vars
7 |
8 |
9 | ServerName "ca.vanity.local"
10 | # Put names of which domains are used for your published site/content here
11 | # ServerAlias "*"
12 | # Use a document root that matches the one in conf.dispatcher.d/default.farm
13 | DocumentRoot "${DOCROOT}"
14 | # URI dereferencing algorithm is applied at Sling's level, do not decode parameters here
15 | AllowEncodedSlashes NoDecode
16 | # Add header breadcrumbs for help in troubleshooting
17 |
18 | Header add X-Vhost "publish"
19 |
20 |
21 |
22 | # Some items cache with the wrong mime type
23 | # Use this option to use the name to auto-detect mime types when cached improperly
24 | ModMimeUsePathInfo On
25 | # Use this option to avoid cache poisioning
26 | # Sling will return /content/image.jpg as well as /content/image.jpg/ but apache can't search /content/image.jpg/ as a file
27 | # Apache will treat that like a directory. This assures the last slash is never stored in cache
28 | DirectorySlash Off
29 | # Enable the dispatcher file handler for apache to fetch files from AEM
30 | SetHandler dispatcher-handler
31 |
32 | Options FollowSymLinks
33 | AllowOverride None
34 | # Insert filter
35 | SetOutputFilter DEFLATE
36 | # Don't compress images
37 | SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png)$ no-gzip dont-vary
38 | # Prevent clickjacking
39 | Header always append X-Frame-Options SAMEORIGIN
40 |
41 |
42 | AllowOverride None
43 | Require all granted
44 |
45 |
46 | # Enabled to allow rewrites to take affect and not be ignored by the dispatcher module
47 | DispatcherUseProcessedURL On
48 | # Default setting to allow all errors to come from the aem instance
49 | DispatcherPassError 0
50 |
51 |
52 | RewriteEngine on
53 | Include conf.d/rewrites/rewrite_ca.rules
54 |
55 | # Rewrite index page internally, pass through (PT)
56 | RewriteRule "^(/?)$" "/index.html" [PT]
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/dispatcher/src/conf.d/available_vhosts/us.vhost:
--------------------------------------------------------------------------------
1 | #
2 | # Vhost for http://us.vanity.local:8080/
3 | #
4 |
5 | # Include customer defined variables
6 | Include conf.d/variables/custom.vars
7 |
8 |
9 | ServerName "us.vanity.local"
10 | # Put names of which domains are used for your published site/content here
11 | # ServerAlias "*"
12 | # Use a document root that matches the one in conf.dispatcher.d/default.farm
13 | DocumentRoot "${DOCROOT}"
14 | # URI dereferencing algorithm is applied at Sling's level, do not decode parameters here
15 | AllowEncodedSlashes NoDecode
16 | # Add header breadcrumbs for help in troubleshooting
17 |
18 | Header add X-Vhost "publish"
19 |
20 |
21 |
22 | # Some items cache with the wrong mime type
23 | # Use this option to use the name to auto-detect mime types when cached improperly
24 | ModMimeUsePathInfo On
25 | # Use this option to avoid cache poisioning
26 | # Sling will return /content/image.jpg as well as /content/image.jpg/ but apache can't search /content/image.jpg/ as a file
27 | # Apache will treat that like a directory. This assures the last slash is never stored in cache
28 | DirectorySlash Off
29 | # Enable the dispatcher file handler for apache to fetch files from AEM
30 | SetHandler dispatcher-handler
31 |
32 | Options FollowSymLinks
33 | AllowOverride None
34 | # Insert filter
35 | SetOutputFilter DEFLATE
36 | # Don't compress images
37 | SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png)$ no-gzip dont-vary
38 | # Prevent clickjacking
39 | Header always append X-Frame-Options SAMEORIGIN
40 |
41 |
42 | AllowOverride None
43 | Require all granted
44 |
45 |
46 | # Enabled to allow rewrites to take affect and not be ignored by the dispatcher module
47 | DispatcherUseProcessedURL On
48 | # Default setting to allow all errors to come from the aem instance
49 | DispatcherPassError 0
50 |
51 |
52 | RewriteEngine on
53 | Include conf.d/rewrites/rewrite_us.rules
54 |
55 | # Rewrite index page internally, pass through (PT)
56 | RewriteRule "^(/?)$" "/index.html" [PT]
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/core/src/main/java/com/ibm/aem/aemtenantspecificvanityurls/core/caconfig/TenantSpecificVanityUrlConfig.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2023 IBM iX
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
5 | * associated documentation files (the "Software"), to deal in the Software without restriction,
6 | * including without limitation the rights to use, copy, modify, merge, publish, distribute,
7 | * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
8 | * furnished to do so, subject to the following conditions:
9 | *
10 | * The above copyright notice and this permission notice shall be included in all copies or
11 | * substantial portions of the Software.
12 | *
13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
14 | * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
15 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
16 | * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
17 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
18 | */
19 | package com.ibm.aem.aemtenantspecificvanityurls.core.caconfig;
20 |
21 | import org.apache.sling.caconfig.annotation.Configuration;
22 | import org.apache.sling.caconfig.annotation.Property;
23 | import org.apache.sling.models.annotations.Default;
24 |
25 | /**
26 | * CA Config to manage the prefix for a content tree.
27 | *
28 | * @author Roland Gruber
29 | */
30 | @Configuration(label = "Tenant Specific Vanity URL Configuration", description = "Manage vanity URL prefixes")
31 | public @interface TenantSpecificVanityUrlConfig {
32 |
33 | @Property(
34 | label = "Prefix",
35 | description = "Specify the prefix for vanity URLs in the linked content tree. Must match your Apache configuration.")
36 | String prefix();
37 |
38 | @Property(
39 | label = "Convert to lower-case",
40 | description = "Enforces a conversion of the vanity entry to lower-case. This is helpful if you want to have case-insensitive URLs (requires also conversion on dispatcher).")
41 | @Default(booleanValues = false)
42 | boolean toLowerCase();
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/core/src/main/java/com/ibm/aem/aemtenantspecificvanityurls/core/model/report/ReportEntry.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2023 IBM iX
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
5 | * associated documentation files (the "Software"), to deal in the Software without restriction,
6 | * including without limitation the rights to use, copy, modify, merge, publish, distribute,
7 | * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
8 | * furnished to do so, subject to the following conditions:
9 | *
10 | * The above copyright notice and this permission notice shall be included in all copies or
11 | * substantial portions of the Software.
12 | *
13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
14 | * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
15 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
16 | * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
17 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
18 | */
19 | package com.ibm.aem.aemtenantspecificvanityurls.core.model.report;
20 |
21 | /**
22 | * Represents a single report entry.
23 | *
24 | * @author Roland Gruber
25 | */
26 | public class ReportEntry {
27 |
28 | private String vanityUrl;
29 |
30 | private String pagePath;
31 |
32 | /**
33 | * Returns the effective vanity URL (with prefix).
34 | *
35 | * @return vanity URL
36 | */
37 | public String getVanityUrl() {
38 | return vanityUrl;
39 | }
40 |
41 | /**
42 | * Sets the effective vanity URL (with prefix).
43 | *
44 | * @param vanityUrl vanity URL
45 | */
46 | public void setVanityUrl(String vanityUrl) {
47 | this.vanityUrl = vanityUrl;
48 | }
49 |
50 | /**
51 | * Returns the page path.
52 | *
53 | * @return path
54 | */
55 | public String getPagePath() {
56 | return pagePath;
57 | }
58 |
59 | /**
60 | * Sets the page path.
61 | *
62 | * @param pagePath path
63 | */
64 | public void setPagePath(String pagePath) {
65 | this.pagePath = pagePath;
66 | }
67 |
68 | }
69 |
--------------------------------------------------------------------------------
/dispatcher/src/conf.d/available_vhosts/default.vhost:
--------------------------------------------------------------------------------
1 | #
2 | # This is the default publish virtualhost definition for Apache.
3 | #
4 | # DO NOT EDIT this file, your changes will have no impact on your deployment.
5 | #
6 | # Instead create a copy in the folder conf.d/available_vhosts and edit the copy.
7 | # Finally, change to the directory conf.d/enabled_vhosts, remove the symbolic
8 | # link for default.vhost and create a symbolic link to your copy.
9 | #
10 |
11 | # Include customer defined variables
12 | Include conf.d/variables/custom.vars
13 |
14 |
15 | ServerName "publish"
16 | # Put names of which domains are used for your published site/content here
17 | ServerAlias "*"
18 | # Use a document root that matches the one in conf.dispatcher.d/default.farm
19 | DocumentRoot "${DOCROOT}"
20 | # URI dereferencing algorithm is applied at Sling's level, do not decode parameters here
21 | AllowEncodedSlashes NoDecode
22 | # Add header breadcrumbs for help in troubleshooting
23 |
24 | Header add X-Vhost "publish"
25 |
26 |
27 |
28 | # Some items cache with the wrong mime type
29 | # Use this option to use the name to auto-detect mime types when cached improperly
30 | ModMimeUsePathInfo On
31 | # Use this option to avoid cache poisioning
32 | # Sling will return /content/image.jpg as well as /content/image.jpg/ but apache can't search /content/image.jpg/ as a file
33 | # Apache will treat that like a directory. This assures the last slash is never stored in cache
34 | DirectorySlash Off
35 | # Enable the dispatcher file handler for apache to fetch files from AEM
36 | SetHandler dispatcher-handler
37 |
38 | Options FollowSymLinks
39 | AllowOverride None
40 | # Insert filter
41 | SetOutputFilter DEFLATE
42 | # Don't compress images
43 | SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png)$ no-gzip dont-vary
44 | # Prevent clickjacking
45 | Header always append X-Frame-Options SAMEORIGIN
46 |
47 |
48 | AllowOverride None
49 | Require all granted
50 |
51 |
52 | # Enabled to allow rewrites to take affect and not be ignored by the dispatcher module
53 | DispatcherUseProcessedURL On
54 | # Default setting to allow all errors to come from the aem instance
55 | DispatcherPassError 0
56 |
57 |
58 | RewriteEngine on
59 | Include conf.d/rewrites/rewrite.rules
60 |
61 | # Rewrite index page internally, pass through (PT)
62 | RewriteRule "^(/?)$" "/index.html" [PT]
63 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/dispatcher/src/conf.d/rewrites/default_rewrite.rules:
--------------------------------------------------------------------------------
1 | #
2 | # These are the default rewrite rules.
3 | #
4 | # DO NOT EDIT this file, your changes will have no impact on your deployment.
5 | #
6 | # Instead modify your rewrite.rules file
7 | #
8 |
9 | # Examples:
10 | # This ruleset would look for robots.txt and fetch it from the dam only if the domain is exampleco-dev.adobecqms.net
11 | # RewriteCond %{SERVER_NAME} exampleco-dev.adobecqms.net [NC]
12 | # RewriteRule ^/robots.txt$ /content/dam/exampleco/robots.txt [NC,PT]
13 | # This ruleset would look for favicon.ico in exampleco's base dam folder if the domain is exampleco-brand1-dev.adobecqms.net
14 | # RewriteCond %{SERVER_NAME} exampleco-brand1-dev.adobecqms.net [NC]
15 | # RewriteRule ^/favicon.ico$ /content/dam/exampleco/favicon.ico [NC,PT]
16 | # This ruleset would look for sitemap.xml and point it at the re-usable file in exampleco's general folder of their site pages
17 | # RewriteCond %{SERVER_NAME} exampleco-brand2-dev.adobecqms.net [NC]
18 | # RewriteRule ^/sitemap.xml$ /content/exampleco/general/sitemap.xml [NC,PT]
19 | # This ruleset would look for logo.jpg on all sites and source it from exampleco's general folder
20 | # RewriteRule ^/logo.jpg$ /content/dam/exampleco/general/logo.jpg [NC,PT]
21 | # This ruleset is a vanity url that exampleco's contactus site that doesn't exist on our environment
22 | # RewriteRule ^/contactus https://corp.exampleco.com/contactus.html [NC,R=301]
23 |
24 | # Prevent X-FORWARDED-FOR spoofing
25 | RewriteCond %{HTTP:X-Forwarded-For} !^$
26 | RewriteCond %{HTTP:X-Forwarded-For} !^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}
27 | RewriteCond %{HTTP:X-Forwarded-For} !^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))
28 | RewriteRule .* - [F]
29 |
30 | # Uncomment to force HSTS protection
31 | # Header always set Strict-Transport-Security "max-age=63072000; includeSubdomains;"
32 |
33 | # Block wordpress DDOS Attempts
34 | RewriteRule ^.*xmlrpc.php - [F]
35 | RewriteCond %{HTTP_USER_AGENT} ^.*wordpress [NC]
36 | RewriteRule .* - [F]
37 |
38 | # Block wp-login
39 | RewriteRule ^.*wp-login - [F,NC,L]
40 |
41 | # Allow the dispatcher to be able to cache persisted queries - they need an extension for the cache file
42 | RewriteCond %{REQUEST_URI} ^/graphql/execute.json
43 | RewriteRule ^/(.*)$ /$1;.json [PT]
--------------------------------------------------------------------------------
/examples/src/main/content/jcr_root/apps/ibm/aem-tenant-specific-vanity-urls-examples/config/org.apache.sling.jcr.repoinit.RepositoryInitializer~tenantspecificvanityurls.config:
--------------------------------------------------------------------------------
1 | scripts=["
2 | set properties on /content/wknd/jcr:content
3 | set cq:allowedTemplates to /conf/wknd/settings/wcm/templates/landing-page-template, /conf/wknd/settings/wcm/templates/article-page-template, /conf/wknd/settings/wcm/templates/content-page-template, /conf/wknd/settings/wcm/templates/adventure-page-template, /apps/wcm-io/caconfig/editor-package/templates/editor
4 | end
5 |
6 | set properties on /content/wknd/us/en/adventures/climbing-new-zealand/jcr:content
7 | set sling:vanityPath to /content/wknd/us/en/wow
8 | set cq:propertyInheritanceCancelled{String} to sling:vanityPath
9 | end
10 |
11 | set properties on /content/wknd/ca/en/adventures/yosemite-backpacking/jcr:content
12 | set sling:vanityPath to /content/wknd/ca/en/wow
13 | set cq:propertyInheritanceCancelled{String} to sling:vanityPath
14 | end
15 |
16 | set properties on /content/wknd/us/en/magazine/san-diego-surf/jcr:content
17 | set sling:vanityPath to /content/wknd/us/en/surf
18 | set cq:propertyInheritanceCancelled{String} to sling:vanityPath
19 | end
20 |
21 | set properties on /content/wknd/ca/en/magazine/arctic-surfing/jcr:content
22 | set sling:vanityPath to /content/wknd/ca/en/surf
23 | set cq:propertyInheritanceCancelled{String} to sling:vanityPath
24 | end
25 |
26 | set properties on /content/wknd/us/en/adventures/ski-touring-mont-blanc/jcr:content
27 | set sling:vanityPath to /content/wknd/us/en/ski
28 | set cq:propertyInheritanceCancelled{String} to sling:vanityPath
29 | end
30 |
31 | set properties on /content/wknd/ca/en/adventures/tahoe-skiing/jcr:content
32 | set sling:vanityPath to /content/wknd/ca/en/ski
33 | set cq:propertyInheritanceCancelled{String} to sling:vanityPath
34 | end
35 |
36 | set properties on /content/wknd/us/jcr:content
37 | set cq:conf to /conf/TSVU-US
38 | end
39 |
40 | set properties on /content/wknd/ca/jcr:content
41 | set cq:conf to /conf/TSVU-CA
42 | end
43 |
44 | create path /content/wknd/us/configuration(cq:Page)
45 | create path /content/wknd/us/configuration/jcr:content(cq:PageContent)
46 | set properties on /content/wknd/us/configuration/jcr:content
47 | set cq:template to /apps/wcm-io/caconfig/editor-package/templates/editor
48 | set hideInNav{Boolean} to true
49 | set jcr:title to Configuration
50 | set sling:resourceType to wcm-io/caconfig/editor/components/page/editor
51 | end
52 |
53 | create path /content/wknd/ca/configuration(cq:Page)
54 | create path /content/wknd/ca/configuration/jcr:content(cq:PageContent)
55 | set properties on /content/wknd/ca/configuration/jcr:content
56 | set cq:template to /apps/wcm-io/caconfig/editor-package/templates/editor
57 | set hideInNav{Boolean} to true
58 | set jcr:title to Configuration
59 | set sling:resourceType to wcm-io/caconfig/editor/components/page/editor
60 | end
61 |
62 | "]
--------------------------------------------------------------------------------
/core/src/main/java/com/ibm/aem/aemtenantspecificvanityurls/core/model/report/ReportDataItem.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2023 IBM iX
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
5 | * associated documentation files (the "Software"), to deal in the Software without restriction,
6 | * including without limitation the rights to use, copy, modify, merge, publish, distribute,
7 | * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
8 | * furnished to do so, subject to the following conditions:
9 | *
10 | * The above copyright notice and this permission notice shall be included in all copies or
11 | * substantial portions of the Software.
12 | *
13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
14 | * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
15 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
16 | * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
17 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
18 | */
19 | package com.ibm.aem.aemtenantspecificvanityurls.core.model.report;
20 |
21 | import com.ibm.aem.aemtenantspecificvanityurls.core.caconfig.TenantSpecificVanityUrlConfig;
22 | import org.apache.commons.lang3.StringUtils;
23 | import org.apache.sling.api.resource.Resource;
24 | import org.apache.sling.caconfig.ConfigurationBuilder;
25 | import org.apache.sling.models.annotations.Model;
26 | import org.apache.sling.models.annotations.injectorspecific.SlingObject;
27 |
28 | import javax.annotation.PostConstruct;
29 |
30 | /**
31 | * Model class for a single report item.
32 | *
33 | * @author Roland Gruber
34 | */
35 | @Model(adaptables = Resource.class)
36 | public class ReportDataItem {
37 |
38 | @SlingObject
39 | private Resource resource;
40 |
41 | protected ReportEntry reportEntry = null;
42 |
43 | @PostConstruct
44 | public void setup() {
45 | reportEntry = resource.getValueMap().get(ReportDataSource.ATTR_REPORT, ReportEntry.class);
46 | }
47 |
48 | /**
49 | * Returns the vanity URL (without prefix).
50 | *
51 | * @return date
52 | */
53 | public String getVanityUrl() {
54 | ConfigurationBuilder configBuilder = resource.adaptTo(ConfigurationBuilder.class);
55 | TenantSpecificVanityUrlConfig config = configBuilder.as(TenantSpecificVanityUrlConfig.class);
56 | String prefix = config.prefix();
57 | String url = reportEntry.getVanityUrl();
58 | if (!StringUtils.isEmpty(prefix) && url.startsWith(prefix)) {
59 | return url.substring(prefix.length());
60 | }
61 | return url;
62 | }
63 |
64 | /**
65 | * Returns mapped path.
66 | *
67 | * @return date
68 | */
69 | public String getMappedPath() {
70 | return resource.getResourceResolver().map(reportEntry.getPagePath());
71 | }
72 |
73 | /**
74 | * Returns the page path.
75 | *
76 | * @return path
77 | */
78 | public String getPagePath() {
79 | return reportEntry.getPagePath();
80 | }
81 |
82 | }
83 |
--------------------------------------------------------------------------------
/.github/workflows/codeql.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: [ "main" ]
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: [ "main" ]
20 | schedule:
21 | - cron: '22 17 * * 3'
22 |
23 | jobs:
24 | analyze:
25 | name: Analyze
26 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
27 | permissions:
28 | actions: read
29 | contents: read
30 | security-events: write
31 |
32 | strategy:
33 | fail-fast: false
34 | matrix:
35 | language: [ 'java', 'javascript' ]
36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
37 | # Use only 'java' to analyze code written in Java, Kotlin or both
38 | # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both
39 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
40 |
41 | steps:
42 | - name: Checkout repository
43 | uses: actions/checkout@v6
44 |
45 | - name: Cache Maven packages
46 | uses: actions/cache@v4
47 | with:
48 | path: ~/.m2
49 | key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
50 | restore-keys: ${{ runner.os }}-m2
51 |
52 | # Initializes the CodeQL tools for scanning.
53 | - name: Initialize CodeQL
54 | uses: github/codeql-action/init@v4
55 | with:
56 | languages: ${{ matrix.language }}
57 | # If you wish to specify custom queries, you can do so here or in a config file.
58 | # By default, queries listed here will override any specified in a config file.
59 | # Prefix the list here with "+" to use these queries and those in the config file.
60 |
61 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
62 | # queries: security-extended,security-and-quality
63 |
64 |
65 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
66 | # If this step fails, then you should remove it and run the build manually (see below)
67 | # - name: Autobuild
68 | # uses: github/codeql-action/autobuild@v2
69 |
70 | # ℹ️ Command-line programs to run using the OS shell.
71 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
72 |
73 | # If the Autobuild fails above, remove it and uncomment the following three lines.
74 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
75 |
76 | - run: |
77 | mvn clean install
78 |
79 | - name: Perform CodeQL Analysis
80 | uses: github/codeql-action/analyze@v4
81 | with:
82 | category: "/language:${{matrix.language}}"
83 |
--------------------------------------------------------------------------------
/ui.apps/src/main/content/jcr_root/apps/ibm/aem-tenant-specific-vanity-urls/clientlibs/clientlib-author/css/backend-validation.css:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2023 - 2025 IBM iX
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
5 | * associated documentation files (the "Software"), to deal in the Software without restriction,
6 | * including without limitation the rights to use, copy, modify, merge, publish, distribute,
7 | * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
8 | * furnished to do so, subject to the following conditions:
9 | *
10 | * The above copyright notice and this permission notice shall be included in all copies or
11 | * substantial portions of the Software.
12 | *
13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
14 | * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
15 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
16 | * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
17 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
18 | */
19 |
20 | .coral3-Textfield.atsv-is-loading {
21 | background-repeat: no-repeat;
22 | background-size: 18px 18px;
23 | background-position: calc(100% - 11px) 9px;
24 | padding-right: 41px
25 | }
26 |
27 | ._coral-Textfield.atsv-is-loading {
28 | background-repeat: no-repeat;
29 | background-size: 18px 18px;
30 | background-position: calc(100% - 11px) 6px;
31 | padding-right: 41px
32 | }
33 |
34 | .coral--large ._coral-Textfield.atsv-is-loading {
35 | background-size: 22px 22px;
36 | background-position: calc(100% - 14px) 8px;
37 | padding-right: 51px
38 | }
39 |
40 | .coral--light .coral3-Textfield.atsv-is-loading,
41 | .coral--light ._coral-Textfield.atsv-is-loading {
42 | background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' height='18' viewBox='0 0 18 18' width='18'%3E%3Ccircle cx='3' cy='9' r='2' fill='%23323232'%3E%3Canimate attributeName='opacity' dur='2s' calcMode='spline' repeatCount='indefinite' values='0; 1; 0' keySplines='0.5 0 0.5 1; 0.5 0 0.5 1' begin='-0.4s'/%3E%3C/circle%3E%3Ccircle cx='9' cy='9' r='2' fill='%23323232'%3E%3Canimate attributeName='opacity' dur='2s' calcMode='spline' repeatCount='indefinite' values='0; 1; 0' keySplines='0.5 0 0.5 1; 0.5 0 0.5 1' begin='-0.2s'/%3E%3C/circle%3E%3Ccircle cx='15' cy='9' r='2' fill='%23323232'%3E%3Canimate attributeName='opacity' dur='2s' calcMode='spline' repeatCount='indefinite' values='0; 1; 0' keySplines='0.5 0 0.5 1; 0.5 0 0.5 1' begin='0s'/%3E%3C/circle%3E%3C/svg%3E");
43 | }
44 |
45 | .coral--dark .coral3-Textfield.atsv-is-loading,
46 | .coral--dark ._coral-Textfield.atsv-is-loading {
47 | background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' height='18' viewBox='0 0 18 18' width='18'%3E%3Ccircle cx='3' cy='9' r='2' fill='%23fff'%3E%3Canimate attributeName='opacity' dur='2s' calcMode='spline' repeatCount='indefinite' values='0; 1; 0' keySplines='0.5 0 0.5 1; 0.5 0 0.5 1' begin='-0.4s'/%3E%3C/circle%3E%3Ccircle cx='9' cy='9' r='2' fill='%23fff'%3E%3Canimate attributeName='opacity' dur='2s' calcMode='spline' repeatCount='indefinite' values='0; 1; 0' keySplines='0.5 0 0.5 1; 0.5 0 0.5 1' begin='-0.2s'/%3E%3C/circle%3E%3Ccircle cx='15' cy='9' r='2' fill='%23fff'%3E%3Canimate attributeName='opacity' dur='2s' calcMode='spline' repeatCount='indefinite' values='0; 1; 0' keySplines='0.5 0 0.5 1; 0.5 0 0.5 1' begin='0s'/%3E%3C/circle%3E%3C/svg%3E");
48 | }
49 |
--------------------------------------------------------------------------------
/ui.apps/src/main/content/jcr_root/apps/ibm/aem-tenant-specific-vanity-urls/tools/report/page/.content.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
18 |
19 |
24 |
25 |
30 |
31 |
32 |
44 |
45 |
52 |
59 |
63 |
64 |
72 |
73 |
74 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/ui.apps.structure/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
18 |
19 | 4.0.0
20 |
21 |
22 |
23 |
24 |
25 | com.ibm.aem.aem-tenant-specific-vanity-urls
26 | aem-tenant-specific-vanity-urls
27 | 1.2.2-SNAPSHOT
28 | ../pom.xml
29 |
30 |
31 |
32 |
33 |
34 | aem-tenant-specific-vanity-urls.ui.apps.structure
35 | content-package
36 | AEM Tenant Specific Vanity URLs - Repository Structure Package
37 |
38 | Empty package that defines the structure of the Adobe Experience Manager repository the Code packages in this project deploy into.
39 | Any roots in the Code packages of this project should have their parent enumerated in the Filters list below.
40 |
41 |
42 |
43 |
44 |
45 | org.apache.jackrabbit
46 | filevault-package-maven-plugin
47 |
48 |
49 | none
50 |
51 |
52 |
53 | /apps
54 | /apps/ibm/aem-tenant-specific-vanity-urls
55 | /apps/ibm/aem-tenant-specific-vanity-urls-examples
56 |
57 |
58 | /apps/sling
59 | /apps/cq
60 | /apps/dam
61 | /apps/wcm
62 | /apps/msm
63 |
64 |
65 | /apps/settings
66 |
67 |
68 | /apps/cq/core/content/nav/tools
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/core/src/test/java/com/ibm/aem/aemtenantspecificvanityurls/core/model/report/ReportDataItemTest.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2023 IBM iX
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
5 | * associated documentation files (the "Software"), to deal in the Software without restriction,
6 | * including without limitation the rights to use, copy, modify, merge, publish, distribute,
7 | * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
8 | * furnished to do so, subject to the following conditions:
9 | *
10 | * The above copyright notice and this permission notice shall be included in all copies or
11 | * substantial portions of the Software.
12 | *
13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
14 | * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
15 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
16 | * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
17 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
18 | */
19 | package com.ibm.aem.aemtenantspecificvanityurls.core.model.report;
20 |
21 | import com.ibm.aem.aemtenantspecificvanityurls.core.caconfig.TenantSpecificVanityUrlConfig;
22 | import org.apache.sling.api.resource.Resource;
23 | import org.apache.sling.api.resource.ResourceResolver;
24 | import org.apache.sling.api.resource.ValueMap;
25 | import org.apache.sling.caconfig.ConfigurationBuilder;
26 | import org.junit.jupiter.api.BeforeEach;
27 | import org.junit.jupiter.api.Test;
28 | import org.junit.jupiter.api.extension.ExtendWith;
29 | import org.mockito.InjectMocks;
30 | import org.mockito.Mock;
31 | import org.mockito.junit.jupiter.MockitoExtension;
32 | import org.mockito.junit.jupiter.MockitoSettings;
33 | import org.mockito.quality.Strictness;
34 |
35 | import static org.junit.jupiter.api.Assertions.assertEquals;
36 | import static org.mockito.Mockito.when;
37 |
38 | /**
39 | * Tests ReportDataItem
40 | *
41 | * @author Roland Gruber
42 | */
43 | @ExtendWith(MockitoExtension.class)
44 | @MockitoSettings(strictness = Strictness.LENIENT)
45 | class ReportDataItemTest {
46 |
47 | private static final String PREFIX = "/content/site/";
48 |
49 | private static final String PAGE_PATH = "/content/site/x/y/z/page";
50 |
51 | private static final String MAPPED_PATH = "/x/y/z/page";
52 |
53 | private static final String VANITY_PATH = "/content/site/wow";
54 |
55 | private static final String VANITY_SHORT = "wow";
56 |
57 | @Mock
58 | private Resource resource;
59 |
60 | @Mock
61 | private ValueMap vm;
62 |
63 | @Mock
64 | private ResourceResolver resolver;
65 |
66 | @Mock
67 | private ConfigurationBuilder configurationBuilder;
68 |
69 | @Mock
70 | TenantSpecificVanityUrlConfig config;
71 |
72 | @InjectMocks
73 | private ReportDataItem item;
74 |
75 | @BeforeEach
76 | void setup() {
77 | when(resource.getValueMap()).thenReturn(vm);
78 | ReportEntry reportEntry = new ReportEntry();
79 | reportEntry.setVanityUrl(VANITY_PATH);
80 | reportEntry.setPagePath(PAGE_PATH);
81 | when(vm.get(ReportDataSource.ATTR_REPORT, ReportEntry.class)).thenReturn(reportEntry);
82 | when(resource.adaptTo(ConfigurationBuilder.class)).thenReturn(configurationBuilder);
83 | when(configurationBuilder.as(TenantSpecificVanityUrlConfig.class)).thenReturn(config);
84 | when(config.prefix()).thenReturn(PREFIX);
85 | when(resource.getResourceResolver()).thenReturn(resolver);
86 | when(resolver.map(PAGE_PATH)).thenReturn(MAPPED_PATH);
87 | }
88 |
89 | @Test
90 | void getVanityUrl() {
91 | item.setup();
92 |
93 | assertEquals(VANITY_SHORT, item.getVanityUrl());
94 | }
95 |
96 | @Test
97 | void getPagePath() {
98 | item.setup();
99 |
100 | assertEquals(PAGE_PATH, item.getPagePath());
101 | }
102 |
103 | @Test
104 | void getMappedPath() {
105 | item.setup();
106 |
107 | assertEquals(MAPPED_PATH, item.getMappedPath());
108 | }
109 |
110 | }
111 |
--------------------------------------------------------------------------------
/dispatcher/README.md:
--------------------------------------------------------------------------------
1 | # Dispatcher configuration
2 |
3 | This module contains the basic dispatcher configurations. The configuration gets bundled in a ZIP file,
4 | and can be downloaded and unzipped to a local folder for development.
5 |
6 | ## File Structure
7 |
8 | ```
9 | ./
10 | ├── conf.d
11 | │ ├── available_vhosts
12 | │ │ └── default.vhost
13 | │ ├── dispatcher_vhost.conf
14 | │ ├── enabled_vhosts
15 | │ │ ├── README
16 | │ │ └── default.vhost -> ../available_vhosts/default.vhost
17 | │ └── rewrites
18 | │ │ ├── default_rewrite.rules
19 | │ │ └── rewrite.rules
20 | │ └── variables
21 | │ └── custom.vars
22 | └── conf.dispatcher.d
23 | ├── available_farms
24 | │ └── default.farm
25 | ├── cache
26 | │ ├── default_invalidate.any
27 | │ ├── default_rules.any
28 | │ └── rules.any
29 | ├── clientheaders
30 | │ ├── clientheaders.any
31 | │ └── default_clientheaders.any
32 | ├── dispatcher.any
33 | ├── enabled_farms
34 | │ ├── README
35 | │ └── default.farm -> ../available_farms/default.farm
36 | ├── filters
37 | │ ├── default_filters.any
38 | │ └── filters.any
39 | ├── renders
40 | │ └── default_renders.any
41 | └── virtualhosts
42 | ├── default_virtualhosts.any
43 | └── virtualhosts.any
44 | ```
45 |
46 | ## Files Explained
47 |
48 | - `conf.d/available_vhosts/default.vhost`
49 | - `*.vhost` (Virtual Host) files are included from inside the `dispatcher_vhost.conf`. These are `` entries to match host names and allow Apache to handle each domain traffic with different rules. From the `*.vhost` file, other files like rewrites, white listing, etc. will be included. The `available_vhosts` directory is where the `*.vhost` files are stored and `enabled_vhosts` directory is where you enable Virtual Hosts by using a symbolic link from a file in the `available_vhosts` to the `enabled_vhosts` directory.
50 |
51 | - `conf.d/rewrites/rewrite.rules`
52 | - `rewrite.rules` file is included from inside the `conf.d/enabled_vhosts/*.vhost` files. It has a set of rewrite rules for `mod_rewrite`.
53 |
54 | - `conf.d/variables/custom.vars`
55 | - `custom.vars` file is included from inside the `conf.d/enabled_vhosts/*.vhost` files. You can put your Apache variables in there.
56 |
57 | - `conf.dispatcher.d/available_farms/.farm`
58 | - `*.farm` files are included inside the `conf.dispatcher.d/dispatcher.any` file. These parent farm files exist to control module behavior for each render or website type. Files are created in the `available_farms` directory and enabled with a symbolic link into the `enabled_farms` directory.
59 |
60 | - `conf.dispatcher.d/filters/filters.any`
61 | - `filters.any` file is included from inside the `conf.dispatcher.d/enabled_farms/*.farm` files. It has a set of rules change what traffic should be filtered out and not make it to the backend.
62 |
63 | - `conf.dispatcher.d/virtualhosts/virtualhosts.any`
64 | - `virtualhosts.any` file is included from inside the `conf.dispatcher.d/enabled_farms/*.farm` files. It has a list of host names or URI paths to be matched by blob matching to determine which backend to use to serve that request.
65 |
66 | - `conf.dispatcher.d/cache/rules.any`
67 | - `rules.any` file is included from inside the `conf.dispatcher.d/enabled_farms/*.farm` files. It specifies caching preferences.
68 |
69 | - `conf.dispatcher.d/clientheaders.any`
70 | - `clientheaders.any` file is included inside the `conf.dispatcher.d/enabled_farms/*.farm` files. It specifies which client headers should be passed through to each renderer.
71 |
72 | ## Environment Variables
73 |
74 | - `CONTENT_FOLDER_NAME`
75 | - This is the customer's content folder in the repository. This is used in the `customer_rewrite.rules` to map shortened URLs to their correct repository path.
76 |
77 | ## Immutable Configuration Files
78 |
79 | Some files are immutable, meaning they cannot be altered or deleted. These are part of the base framework and enforce standards and best practices. When customization is needed, copies of immutable files (i.e. `default.vhost` -> `publish.vhost`) can be used to modify the behavior. Where possible, be sure to retain includes of immutable files unless customization of included files is also needed.
80 |
81 | ### Immutable Files
82 |
83 | ```
84 | conf.d/available_vhosts/default.vhost
85 | conf.d/dispatcher_vhost.conf
86 | conf.d/rewrites/default_rewrite.rules
87 | conf.dispatcher.d/available_farms/default.farm
88 | conf.dispatcher.d/cache/default_invalidate.any
89 | conf.dispatcher.d/cache/default_rules.any
90 | conf.dispatcher.d/clientheaders/default_clientheaders.any
91 | conf.dispatcher.d/dispatcher.any
92 | conf.dispatcher.d/enabled_farms/default.farm
93 | conf.dispatcher.d/filters/default_filters.any
94 | conf.dispatcher.d/renders/default_renders.any
95 | conf.dispatcher.d/virtualhosts/default_virtualhosts.any
96 | ```
--------------------------------------------------------------------------------
/examples.content/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
18 |
19 | 4.0.0
20 |
21 |
22 |
23 |
24 |
25 | com.ibm.aem.aem-tenant-specific-vanity-urls
26 | aem-tenant-specific-vanity-urls
27 | 1.2.2-SNAPSHOT
28 | ../pom.xml
29 |
30 |
31 |
32 |
33 |
34 | aem-tenant-specific-vanity-urls.examples.content
35 | content-package
36 | AEM Tenant Specific Vanity URLs - Examples content
37 | Examples content package for AEM Tenant Specific Vanity URLs
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | org.apache.jackrabbit
49 | filevault-package-maven-plugin
50 |
51 |
52 | none
53 |
54 | IBM
55 | aem-tenant-specific-vanity-urls.examples.content
56 | content
57 |
58 |
59 |
60 | /conf,/content,/content/experience-fragments,/content/dam
61 |
62 |
63 |
64 |
65 |
66 | com.adobe.aem.guides
67 | aem-guides-wknd-shared.ui.content
68 | 2.2.2
69 |
70 |
71 |
72 |
73 |
74 | com.day.jcr.vault
75 | content-package-maven-plugin
76 | true
77 |
78 | true
79 | true
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 | com.adobe.aem.guides
91 | aem-guides-wknd-shared.ui.content
92 | 2.2.2
93 | zip
94 |
95 |
96 |
97 |
--------------------------------------------------------------------------------
/core/src/test/java/com/ibm/aem/aemtenantspecificvanityurls/core/model/report/ReportDataSourceTest.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2023 IBM iX
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
5 | * associated documentation files (the "Software"), to deal in the Software without restriction,
6 | * including without limitation the rights to use, copy, modify, merge, publish, distribute,
7 | * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
8 | * furnished to do so, subject to the following conditions:
9 | *
10 | * The above copyright notice and this permission notice shall be included in all copies or
11 | * substantial portions of the Software.
12 | *
13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
14 | * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
15 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
16 | * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
17 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
18 | */
19 | package com.ibm.aem.aemtenantspecificvanityurls.core.model.report;
20 |
21 | import com.adobe.granite.ui.components.ds.DataSource;
22 | import com.ibm.aem.aemtenantspecificvanityurls.core.exceptions.AtsvuException;
23 | import org.apache.http.client.fluent.Request;
24 | import org.apache.sling.api.SlingHttpServletRequest;
25 | import org.apache.sling.api.request.RequestPathInfo;
26 | import org.apache.sling.api.resource.Resource;
27 | import org.apache.sling.api.resource.ResourceResolver;
28 | import org.apache.sling.models.annotations.injectorspecific.OSGiService;
29 | import org.apache.sling.models.annotations.injectorspecific.SlingObject;
30 | import org.junit.jupiter.api.BeforeEach;
31 | import org.junit.jupiter.api.Test;
32 | import org.junit.jupiter.api.extension.ExtendWith;
33 | import org.mockito.ArgumentCaptor;
34 | import org.mockito.InjectMocks;
35 | import org.mockito.Mock;
36 | import org.mockito.Mockito;
37 | import org.mockito.junit.jupiter.MockitoExtension;
38 |
39 | import java.util.ArrayList;
40 | import java.util.Iterator;
41 | import java.util.List;
42 |
43 | import static com.ibm.aem.aemtenantspecificvanityurls.core.model.report.ReportDataSource.ATTR_REPORT;
44 | import static org.junit.jupiter.api.Assertions.*;
45 | import static org.mockito.Mockito.verify;
46 | import static org.mockito.Mockito.when;
47 |
48 | /**
49 | * Tests ReportDataSource
50 | *
51 | * @author Roland Gruber
52 | */
53 | @ExtendWith(MockitoExtension.class)
54 | class ReportDataSourceTest {
55 |
56 | private static final String PAGE1 = "/content/site/x/y/z/p1";
57 | private static final String PAGE2 = "/content/site/x/y/z/p2";
58 |
59 | private static final String VANITY1 = "/content/site/v1";
60 | private static final String VANITY2 = "/content/site/v2";
61 |
62 | @Mock
63 | private ReportService reportService;
64 |
65 | @Mock
66 | private SlingHttpServletRequest request;
67 |
68 | @Mock
69 | private RequestPathInfo requestPathInfo;
70 |
71 | @Mock
72 | private ResourceResolver resolver;
73 |
74 | @InjectMocks
75 | private ReportDataSource reportDataSource;
76 |
77 | private List reportList = new ArrayList<>();
78 |
79 | @BeforeEach
80 | void setup() throws AtsvuException {
81 | when(request.getRequestPathInfo()).thenReturn(requestPathInfo);
82 | when(request.getResourceResolver()).thenReturn(resolver);
83 | when(requestPathInfo.getSelectors()).thenReturn(new String[] {"200", "100"});
84 | when(reportService.getVanityEntries(200, 101, ReportService.ORDER_ATTR.PATH, ReportService.ORDER.ASC, resolver)).thenReturn(reportList);
85 | }
86 |
87 | @Test
88 | void requestAttribute() {
89 | ReportEntry e1 = new ReportEntry();
90 | e1.setPagePath(PAGE1);
91 | e1.setVanityUrl(VANITY1);
92 | reportList.add(e1);
93 | ReportEntry e2 = new ReportEntry();
94 | e2.setPagePath(PAGE2);
95 | e2.setVanityUrl(VANITY2);
96 | reportList.add(e2);
97 |
98 | reportDataSource.setup();
99 |
100 | ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(DataSource.class);
101 | verify(request).getRequestPathInfo();
102 | verify(request).getRequestParameter("sortDir");
103 | verify(request).getRequestParameter("sortName");
104 | verify(request).setAttribute(Mockito.eq(DataSource.class.getName()), argumentCaptor.capture());
105 | DataSource dataSource = argumentCaptor.getValue();
106 | Iterator it = dataSource.iterator();
107 | assertTrue(it.hasNext());
108 | Resource res1 = it.next();
109 | assertEquals(e1, res1.getValueMap().get(ATTR_REPORT));
110 | assertTrue(it.hasNext());
111 | Resource res2 = it.next();
112 | assertEquals(e2, res2.getValueMap().get(ATTR_REPORT));
113 | assertFalse(it.hasNext());
114 | }
115 |
116 | }
117 |
--------------------------------------------------------------------------------
/dispatcher/src/conf.dispatcher.d/filters/default_filters.any:
--------------------------------------------------------------------------------
1 | #
2 | # This is the default filter ACL specifying what requests are handled by the dispatcher.
3 | #
4 | # DO NOT EDIT this file, your changes will have no impact on your deployment.
5 | #
6 | # Instead modify filters.any.
7 | #
8 |
9 | # deny everything and allow specific entries
10 | # Start with everything blocked as a safeguard and open things customers need and what's safe OOTB
11 | /0001 { /type "deny" /url "*" }
12 |
13 | # Open consoles if this isn't a production environment by uncommenting the next few lines
14 | # /002 { /type "allow" /url "/crx/*" } # allow content repository
15 | # /003 { /type "allow" /url "/system/*" } # allow OSGi console
16 |
17 | # allow non-public content directories if this isn't a production environment by uncommenting the next few lines
18 | # /004 { /type "allow" /url "/apps/*" } # allow apps access
19 | # /005 { /type "allow" /url "/bin/*" } # allow bin path access
20 |
21 | # This rule allows content to be access
22 | /0010 { /type "allow" /extension '(css|eot|gif|ico|jpeg|jpg|js|gif|pdf|png|svg|swf|ttf|woff|woff2|html|mp4|mov|m4v)' /path "/content/*" } # disable this rule to allow mapped content only
23 |
24 | # Enable specific mime types in non-public content directories
25 | /0011 { /type "allow" /method "GET" /extension '(css|eot|gif|ico|jpeg|jpg|js|gif|png|svg|swf|ttf|woff|woff2)' }
26 |
27 | # Enable clientlibs proxy servlet
28 | /0012 { /type "allow" /method "GET" /url "/etc.clientlibs/*" }
29 |
30 | # Enable basic features
31 | /0013 { /type "allow" /method "GET" /url '/libs/granite/csrf/token.json' /extension 'json' } # AEM provides a framework aimed at preventing Cross-Site Request Forgery attacks
32 | /0014 { /type "allow" /method "POST" /url "/content/*.form.html" } # allow POSTs to form selectors under content
33 |
34 | /0015 { /type "allow" /method "GET" /path "/libs/cq/personalization" } # enable personalization
35 | /0016 { /type "allow" /method "POST" /path "/content/*.commerce.cart.json" } # allow POSTs to update the shopping cart
36 |
37 | # Deny content grabbing for greedy queries and prevent un-intended self DOS attacks
38 | /0017 { /type "deny" /selectors '(feed|rss|pages|languages|blueprint|infinity|tidy|sysview|docview|query|[0-9-]+|jcr:content)' /extension '(json|xml|html|feed)' }
39 |
40 | # Deny authoring query params
41 | /0018 { /type "deny" /method "GET" /query "debug=*" }
42 | /0019 { /type "deny" /method "GET" /query "wcmmode=*" }
43 |
44 | # Allow current user
45 | /0020 { /type "allow" /url "/libs/granite/security/currentuser.json" }
46 |
47 | # Allow index page
48 | /0030 { /type "allow" /url "/index.html" }
49 |
50 | # Allow IMS Authentication
51 | /0031 { /type "allow" /method "GET" /url "/callback/j_security_check" }
52 |
53 | # AEM Forms specific filters
54 | # to allow AF specific endpoints for prefill, submit and sign
55 | /0032 { /type "allow" /path "/content/forms/af/*" /method "POST" /selectors '(submit|internalsubmit|agreement|signSubmit|prefilldata|save|analyticsconfigparser)' /extension '(jsp|json)' }
56 |
57 | # to allow AF specific endpoints for thank you page
58 | /0033 { /type "allow" /path "/content/forms/af/*" /method "GET" /selectors '(guideThankYouPage|guideAsyncThankYouPage)' /extension '(html)'}
59 |
60 | # to allow AF specific endpoints for lazy loading
61 | /0034 { /type "allow" /path "/content/forms/af/*" /method "GET" /extension '(jsonhtmlemitter)'}
62 |
63 | # to allow fp related functionalities
64 | /0035 { /type "allow" /path "/content/forms/*" /selectors '(fp|attach|draft|dor|api)' /extension '(html|jsp|json|pdf)' }
65 |
66 | # to allow forms access via dam path
67 | /0036 { /type "allow" /path "/content/dam/formsanddocuments/**/jcr:content" /method "GET"}
68 |
69 | # to allow invoke service functionality (FDM)
70 | /0037 { /type "allow" /path "/content/forms/*" /selectors '(af)' /extension '(dermis)' }
71 |
72 | # to allow forms portal draft and submissions component operation servlet
73 | /0038 { /type "allow" /path "/content/*" /method "GET" /selectors '(fp)' /extension '(operation)' }
74 |
75 | # AEM Screens Filters
76 | # to allow AEM Screens channels selectors
77 | /0050 { /type "allow" /method "GET" /url "/screens/channels.json" }
78 |
79 | # to allow AEM Screens Content and selectors
80 | /0051 { /type "allow" /method '(GET|HEAD)' /url "/content/screens/*" }
81 |
82 | # AEM Sites Filters
83 | # to allow site30 theme servlet
84 | /0052 { /type "allow" /extension "theme" /path "/content/*" }
85 |
86 | # Allow manifest.webmanifest files located in the content
87 | /0053 { /type "allow" /extension "webmanifest" /path "/content/*/manifest" }
88 |
89 | # Allow Apache Sling Sitemap selectors: sitemap, sitemap-index, sitemap.any-nested-or-named-sitemap
90 | /0054 { /type "allow" /method "GET" /path "/content/*" /selectors 'sitemap(-index)?' /extension "xml" }
91 |
92 | # Allow GraphQL & preflight requests
93 | # GraphQL also supports "GET" requests, if you intend to use "GET" add a rule in filters.any
94 | /0060 { /type "allow" /method '(POST|OPTIONS)' /url "/content/_cq_graphql/*/endpoint.json" }
95 |
96 | # GraphQL Persisted Queries & preflight requests
97 | /0061 { /type "allow" /method '(GET|POST|OPTIONS)' /url "/graphql/execute.json*" }
98 |
99 | # Allow Forms Document Services requests
100 | /0062 { /type "allow" /method '(GET|POST)' /url "/adobe/forms/*" }
101 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # AEM Tenant Specific Vanity URLs
4 |
5 | This solution allows you to manage the same vanity URL for different content trees.
6 | It circumvents the limitation of Sling where vanity URLs are global.
7 |
8 | The tool was presented at the [adaptTo() conference 2023](https://adapt.to/2023/schedule/tenant-specific-vanity-urls-with-aem). You can download the slides there and here is the video:
9 |
10 | [](https://www.youtube.com/watch?v=IvclYIoIEpk "AEM Tenant Specific Vanity URLs @ adaptTo() 2023")
11 |
12 |
13 | ## Requirements
14 |
15 | The following AEM versions are supported:
16 | * AEM 6.5.15 and later
17 | * AEM Cloud
18 |
19 | ## Installation
20 |
21 | Please install the aem-tenant-specific-vanity-urls.all package from
22 | [Maven Central](https://repo1.maven.org/maven2/com/ibm/aem/aem-tenant-specific-vanity-urls/aem-tenant-specific-vanity-urls.all/)
23 | or our [releases section](https://github.com/IBM/aem-tenant-specific-vanity-urls/releases) on your AEM instance.
24 | This will install the bundle and clientlib to manage tenant specific vanity URLs.
25 | As the configuration is done via context aware configuration, please also install the wcm.io configuration editor from https://wcm.io/caconfig/editor/usage.html.
26 |
27 | ```
28 |
29 | com.ibm.aem.aem-tenant-specific-vanity-urls
30 | aem-tenant-specific-vanity-urls.all
31 | LATEST
32 | zip
33 |
34 |
35 | io.wcm
36 | io.wcm.caconfig.editor.package
37 | LATEST
38 | zip
39 |
40 | ```
41 |
42 | Now, you can setup the domains.
43 |
44 | ### AEM
45 |
46 | Please put a context aware configuration in each content root folder of a domain.
47 | E.g. in our example "/content/wknd/ca/en" serves "http://ca.vanity.local:8080". Then put it in "/content/wknd/ca/en".
48 | Add the configuration type "Tenant Specific Vanity URL Configuration" and set a prefix.
49 |
50 | The prefix can be any value (e.g. "/content/wknd/ca/en") but must be unique for all domains. If you use sling mappings please use the root path (serving e.g. "http://ca.vanity.local:8080/") of your content tree (e.g. "/content/wknd/ca/en"). This makes sure that the sling mapping maps the vanity URL with the right prefix.
51 |
52 | Now, as soon as an author sets a vanity URL, this prefix will be added. This makes sure all vanity URLs have the correct prefix.
53 |
54 | You can also convert all editor input for vanity URLs to lower-case. This is helpful if you want to have case-insensitive vanity URLs. Please note that you need to convert incoming vanity requests to lower-case on dispatcher then.
55 |
56 | 
57 | 
58 |
59 | ### Dispatcher
60 |
61 | Please check that vanity URLs (e.g. "http://ca.vanity.local:8080/wow") are handled correctly by your rewrites and filter rules.
62 | E.g. check if they are not blocked or transformed. The result needs to be "/content/wknd/ca/en/wow.html" in our example.
63 |
64 | Vanity URLs should not have any extension. If your normal pages have an ".html" extension then make sure that
65 | a call of e.g. "http://ca.vanity.local:8080/wow" is correctly handled by your rewrites and results in e.g. "/content/wknd/ca/en/wow.html".
66 |
67 | ## Duplicate Check
68 |
69 | The tool automatically checks for duplicates of vanity URLs. This prevents editors to assign the same vanity URL on multiple pages.
70 |
71 | 
72 |
73 | ## Vanity Report Tool
74 |
75 | You can see a list of all vanity URLs in the system. Open Tools ->
76 | AEM Tenant Specific Vanity URLs -> Vanity URL Report. This provides a quick overview which vanity URLs are setup and where.
77 | You can edit the pages directly from the report.
78 |
79 | 
80 |
81 | ## Examples
82 |
83 | You can install our example package. This includes pre-defined configuration for AEM's [WKND](https://github.com/adobe/aem-guides-wknd) pages.
84 | Please make sure that the [WKND](https://github.com/adobe/aem-guides-wknd) package is installed before as our example package will configure it during package installation.
85 |
86 | There are two domains setup in the dispatcher example configuration to demonstrate the solution.
87 | This will allow you to see different content for the same vanity URL ("wow"):
88 |
89 | * http://us.vanity.local:8080/wow
90 | * http://ca.vanity.local:8080/wow
91 |
92 | Required changes to /etc/hosts:
93 |
94 | 127.0.0.1 ca.vanity.local
95 | 127.0.0.1 us.vanity.local
96 |
97 | See the page properties for the vanity URL configuration:
98 |
99 | * http://localhost:4502/editor.html/content/wknd/us/en/adventures/climbing-new-zealand.html
100 | * http://localhost:4502/editor.html/content/wknd/ca/en/adventures/yosemite-backpacking.html
101 |
102 | The vanity URL configuration can be found here:
103 |
104 | * http://localhost:4502/content/wknd/us/configuration.html
105 | * http://localhost:4502/content/wknd/ca/configuration.html
106 |
107 | ## License
108 |
109 | The software is licensed under the [MIT license](LICENSE).
110 |
111 | ## Developers
112 |
113 | [Developers area](docs/developers.md)
114 |
--------------------------------------------------------------------------------
/core/src/main/java/com/ibm/aem/aemtenantspecificvanityurls/core/model/report/ReportDataSource.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2023 IBM iX
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
5 | * associated documentation files (the "Software"), to deal in the Software without restriction,
6 | * including without limitation the rights to use, copy, modify, merge, publish, distribute,
7 | * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
8 | * furnished to do so, subject to the following conditions:
9 | *
10 | * The above copyright notice and this permission notice shall be included in all copies or
11 | * substantial portions of the Software.
12 | *
13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
14 | * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
15 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
16 | * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
17 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
18 | */
19 | package com.ibm.aem.aemtenantspecificvanityurls.core.model.report;
20 |
21 | import com.adobe.granite.ui.components.ds.AbstractDataSource;
22 | import com.adobe.granite.ui.components.ds.DataSource;
23 | import com.adobe.granite.ui.components.ds.ValueMapResource;
24 | import com.ibm.aem.aemtenantspecificvanityurls.core.exceptions.AtsvuException;
25 | import org.apache.sling.api.SlingHttpServletRequest;
26 | import org.apache.sling.api.request.RequestParameter;
27 | import org.apache.sling.api.resource.Resource;
28 | import org.apache.sling.api.resource.ValueMap;
29 | import org.apache.sling.api.wrappers.ValueMapDecorator;
30 | import org.apache.sling.models.annotations.Model;
31 | import org.apache.sling.models.annotations.injectorspecific.OSGiService;
32 | import org.apache.sling.models.annotations.injectorspecific.SlingObject;
33 | import org.slf4j.Logger;
34 | import org.slf4j.LoggerFactory;
35 |
36 | import javax.annotation.PostConstruct;
37 | import java.util.ArrayList;
38 | import java.util.HashMap;
39 | import java.util.Iterator;
40 | import java.util.List;
41 |
42 | /**
43 | * Datasource model for report page.
44 | *
45 | * @author Roland Gruber
46 | */
47 | @Model(adaptables = SlingHttpServletRequest.class)
48 | public class ReportDataSource {
49 |
50 | private static final String ITEM_TYPE = "ibm/aem-tenant-specific-vanity-urls/tools/report/dataitem";
51 | public static final String ATTR_REPORT = "report";
52 |
53 | private static final Logger LOG = LoggerFactory.getLogger(ReportDataSource.class);
54 |
55 | @OSGiService
56 | private ReportService reportService;
57 |
58 | @SlingObject
59 | private SlingHttpServletRequest request;
60 |
61 | @PostConstruct
62 | public void setup() {
63 | String[] selectors = request.getRequestPathInfo().getSelectors();
64 | int offset = 0;
65 | int limit = 50;
66 | if (selectors.length > 1) {
67 | offset = Integer.parseInt(selectors[0]);
68 | limit = Integer.parseInt(selectors[1]);
69 | }
70 | ReportService.ORDER sortOrder = ReportService.ORDER.ASC;
71 | RequestParameter sortOrderParam = request.getRequestParameter("sortDir");
72 | if (sortOrderParam != null) {
73 | sortOrder = ReportService.ORDER.parse(sortOrderParam.getString());
74 | }
75 | ReportService.ORDER_ATTR sortAttr = ReportService.ORDER_ATTR.PATH;
76 | RequestParameter sortAttrParam = request.getRequestParameter("sortName");
77 | if (sortAttrParam != null) {
78 | sortAttr = ReportService.ORDER_ATTR.parse(sortAttrParam.getString());
79 | }
80 | request.setAttribute(DataSource.class.getName(), getResourceIterator(offset, limit, sortAttr, sortOrder));
81 | }
82 |
83 | /**
84 | * Returns the history entries.
85 | *
86 | * @param offset offset where to start reading
87 | * @param limit maximum number of entries to return
88 | * @param sortAttr sort attribute
89 | * @param sortOrder sort direction
90 | * @return entries
91 | */
92 | private DataSource getResourceIterator(int offset, int limit, ReportService.ORDER_ATTR sortAttr, ReportService.ORDER sortOrder) {
93 | return new AbstractDataSource() {
94 |
95 | @Override
96 | public Iterator iterator() {
97 | List entries = new ArrayList<>();
98 | try {
99 | List reportEntries = reportService.getVanityEntries(offset, limit + 1, sortAttr, sortOrder, request.getResourceResolver());
100 | for (ReportEntry reportEntry : reportEntries) {
101 | ValueMap vm = new ValueMapDecorator(new HashMap<>());
102 | vm.put(ATTR_REPORT, reportEntry);
103 | entries.add(new ValueMapResource(request.getResourceResolver(), reportEntry.getPagePath(),
104 | ITEM_TYPE, vm));
105 | }
106 | } catch (AtsvuException e) {
107 | LOG.error("Unable to read vanity URLs", e);
108 | }
109 | return entries.iterator();
110 | }
111 |
112 | };
113 | }
114 |
115 | }
116 |
--------------------------------------------------------------------------------
/dispatcher/src/conf.dispatcher.d/available_farms/default.farm:
--------------------------------------------------------------------------------
1 | #
2 | # This is the default publish farm definition for the dispatcher module.
3 | #
4 | # DO NOT EDIT this file, your changes will have no impact on your deployment.
5 | #
6 | # Instead create a copy in the folder conf.dispatcher.d/available_farms and edit the copy.
7 | # Finally, change to the directory conf.dispatcher.d/enabled_farms, remove the symbolic
8 | # link for default_farm.any and create a symbolic link to your copy.
9 | #
10 |
11 | /publishfarm {
12 | # client headers which should be passed through to the render instances
13 | # (feature supported since dispatcher build 2.6.3.5222)
14 | /clientheaders {
15 | $include "../clientheaders/clientheaders.any"
16 | }
17 | # hostname globbing for farm selection (virtual domain addressing)
18 | /virtualhosts {
19 | $include "../virtualhosts/virtualhosts.any"
20 | }
21 | # the load will be balanced among these render instances
22 | /renders {
23 | $include "../renders/default_renders.any"
24 | }
25 | # only handle the requests in the following acl. default is 'none'
26 | # the glob pattern is matched against the first request line
27 | /filter {
28 | $include "../filters/filters.any"
29 | }
30 | # if the package is installed on publishers to generate a list of all content with a vanityurl attached
31 | # this section will auto-allow the items to bypass the normal dispatcher filters
32 | # Reference: https://docs.adobe.com/docs/en/dispatcher/disp-config.html#Enabling%20Access%20to%20Vanity%20URLs%20-%20/vanity_urls
33 | # /vanity_urls {
34 | # /url "/libs/granite/dispatcher/content/vanityUrls.html"
35 | # /file "/tmp/vanity_urls"
36 | # /delay 300
37 | # }
38 | # allow propagation of replication posts (should seldomly be used)
39 | /propagateSyndPost "0"
40 | # the cache is used to store requests from the renders for faster delivery
41 | # for a second time.
42 | /cache {
43 | # The cacheroot must be equal to the document root of the webserver
44 | /docroot "${DOCROOT}"
45 | # sets the level upto which files named ".stat" will be created in the
46 | # document root of the webserver. when an activation request for some
47 | # handle is received, only files within the same subtree are affected
48 | # by the invalidation.
49 | /statfileslevel "2"
50 | # caches also authorized data
51 | /allowAuthorized "0"
52 | # Flag indicating whether the dispatcher should serve stale content if
53 | # no remote server is available.
54 | /serveStaleOnError "1"
55 | # the rules define, which pages should be cached. please note that
56 | # - only GET requests are cached
57 | # - only requests with an extension are cached
58 | # - only requests without query parameters ( ? ) are cached
59 | # - only unauthorized pages are cached unless allowUnauthorized is set to 1
60 | /rules {
61 | $include "../cache/rules.any"
62 | }
63 | # the invalidate section defines those pages which are 'invalidated' after
64 | # any activation. please note that, the activated page itself and all
65 | # related documents are flushed on an modification. for example: if the
66 | # page /foo/bar is activated, all /foo/bar.* files are removed from the
67 | # cache.
68 | /invalidate {
69 | /0000 {
70 | /glob "*"
71 | /type "deny"
72 | }
73 | /0001 {
74 | /glob "*.html"
75 | /type "allow"
76 | }
77 | # to ensure that AEM forms HTMLs are not auto-invalidated due to invalidation of any other resource. It is supposed to be deleted only after its own activation.
78 | /0002
79 | {
80 | /glob "/content/forms/**/*.html"
81 | /type "deny"
82 | }
83 | }
84 | /allowedClients {
85 | $include "../cache/default_invalidate.any"
86 | }
87 | # The ignoreUrlParams section contains query string parameter names that
88 | # should be ignored when determining whether some request's output can be
89 | # cached or delivered from cache.
90 | # In this example configuration, the "q" parameter will be ignored as
91 | # well as general marketing related parameters such as e.g. utm_campaign.
92 | # Marketing parameters can normally be ignored on most websites as they are tracked
93 | # through different means.
94 | # /ignoreUrlParams {
95 | # /0001 { /glob "*" /type "deny" }
96 | # /0002 { /glob "q" /type "allow" }
97 | # $include "../cache/marketing_query_parameters.any"
98 | # }
99 |
100 | # Cache response headers next to a cached file. On the first request to
101 | # an uncached resource, all headers matching one of the values found here
102 | # are stored in a separate file, next to the cache file. On subsequent
103 | # requests to the cached resource, the stored headers are added to the
104 | # response.
105 | # Note, that file globbing characters are not allowed here.
106 | /headers {
107 | "Cache-Control"
108 | "Content-Disposition"
109 | "Content-Type"
110 | "Expires"
111 | "Last-Modified"
112 | "X-Content-Type-Options"
113 | }
114 | # A grace period defines the number of seconds a stale, auto-invalidated
115 | # resource may still be served from the cache after the last activation
116 | # occurring. Auto-invalidated resources are invalidated by any activation,
117 | # when their path matches the /invalidate section above. This setting
118 | # can be used in a setup, where a batch of activations would otherwise
119 | # repeatedly invalidate the entire cache.
120 | /gracePeriod "2"
121 |
122 | # Enable TTL evaluates the response headers from the backend, and if they
123 | # contain a Cache-Control max-age or Expires date, an auxiliary, empty file
124 | # next to the cache file is created, with modification time equal to the
125 | # expiry date. When the cache file is requested past the modification time
126 | # it is automatically re-requested from the backend.
127 | /enableTTL "1"
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/core/src/test/java/com/ibm/aem/aemtenantspecificvanityurls/core/model/report/ReportServiceTest.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2023 IBM iX
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
5 | * associated documentation files (the "Software"), to deal in the Software without restriction,
6 | * including without limitation the rights to use, copy, modify, merge, publish, distribute,
7 | * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
8 | * furnished to do so, subject to the following conditions:
9 | *
10 | * The above copyright notice and this permission notice shall be included in all copies or
11 | * substantial portions of the Software.
12 | *
13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
14 | * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
15 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
16 | * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
17 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
18 | */
19 | package com.ibm.aem.aemtenantspecificvanityurls.core.model.report;
20 |
21 | import com.day.cq.search.Query;
22 | import com.day.cq.search.QueryBuilder;
23 | import com.day.cq.search.result.Hit;
24 | import com.day.cq.search.result.SearchResult;
25 | import com.day.cq.wcm.api.NameConstants;
26 | import com.ibm.aem.aemtenantspecificvanityurls.core.exceptions.AtsvuException;
27 | import org.apache.sling.api.resource.Resource;
28 | import org.apache.sling.api.resource.ResourceResolver;
29 | import org.apache.sling.api.resource.ValueMap;
30 | import org.junit.jupiter.api.BeforeEach;
31 | import org.junit.jupiter.api.Test;
32 | import org.junit.jupiter.api.extension.ExtendWith;
33 | import org.mockito.InjectMocks;
34 | import org.mockito.Mock;
35 | import org.mockito.Mockito;
36 | import org.mockito.junit.jupiter.MockitoExtension;
37 | import org.mockito.junit.jupiter.MockitoSettings;
38 | import org.mockito.quality.Strictness;
39 |
40 | import javax.jcr.RepositoryException;
41 | import javax.jcr.Session;
42 | import java.util.Arrays;
43 | import java.util.List;
44 |
45 | import static org.junit.jupiter.api.Assertions.assertEquals;
46 | import static org.junit.jupiter.api.Assertions.assertNull;
47 | import static org.mockito.Mockito.when;
48 |
49 | /**
50 | * Tests ReportService.
51 | *
52 | * @author Roland Gruber
53 | */
54 | @ExtendWith(MockitoExtension.class)
55 | @MockitoSettings(strictness = Strictness.LENIENT)
56 | class ReportServiceTest {
57 |
58 | private static final String PATH1 = "/content/site/x/y/z/page1";
59 | private static final String PATH2 = "/content/site/x/y/z/page2";
60 | private static final String VANITY1 = "/content/site/v1";
61 | private static final String VANITY2 = "/content/site/v2";
62 |
63 | @Mock
64 | private QueryBuilder queryBuilder;
65 |
66 | @Mock
67 | private ResourceResolver resolver;
68 |
69 | @Mock
70 | private Session session;
71 |
72 | @Mock
73 | private Query query;
74 |
75 | @Mock
76 | private SearchResult searchResult;
77 |
78 | @Mock
79 | private Hit hit1;
80 | @Mock
81 | private Hit hit2;
82 |
83 | @Mock
84 | private Resource resource1;
85 | @Mock
86 | private Resource resource2;
87 |
88 | @Mock
89 | private ValueMap vm1;
90 | @Mock
91 | private ValueMap vm2;
92 |
93 | @InjectMocks
94 | private ReportService service;
95 |
96 | @BeforeEach
97 | void setup() throws RepositoryException {
98 | when(resolver.adaptTo(Session.class)).thenReturn(session);
99 | when(queryBuilder.createQuery(Mockito.any(), Mockito.eq(session))).thenReturn(query);
100 | when(query.getResult()).thenReturn(searchResult);
101 | when(hit1.getResource()).thenReturn(resource1);
102 | when(hit2.getResource()).thenReturn(resource2);
103 | when(resource1.getValueMap()).thenReturn(vm1);
104 | when(resource2.getValueMap()).thenReturn(vm2);
105 | when(resource1.getParent()).thenReturn(resource1);
106 | when(resource2.getParent()).thenReturn(resource2);
107 | when(resource1.getPath()).thenReturn(PATH1);
108 | when(resource2.getPath()).thenReturn(PATH2);
109 | when(vm1.get(NameConstants.PN_SLING_VANITY_PATH, String[].class)).thenReturn(new String[] {VANITY1});
110 | when(vm2.get(NameConstants.PN_SLING_VANITY_PATH, String[].class)).thenReturn(new String[] {VANITY2});
111 | }
112 |
113 | @Test
114 | void getVanityEntries() throws AtsvuException {
115 | List hits = Arrays.asList(hit1, hit2);
116 | when(searchResult.getHits()).thenReturn(hits);
117 |
118 | List entries = service.getVanityEntries(0, 100, ReportService.ORDER_ATTR.PATH, ReportService.ORDER.ASC, resolver);
119 |
120 | assertEquals(2, entries.size());
121 | assertEquals(PATH1, entries.get(0).getPagePath());
122 | assertEquals(PATH2, entries.get(1).getPagePath());
123 | assertEquals(VANITY1, entries.get(0).getVanityUrl());
124 | assertEquals(VANITY2, entries.get(1).getVanityUrl());
125 | }
126 |
127 | @Test
128 | void checkEnums() {
129 | assertEquals(ReportService.ORDER.ASC, ReportService.ORDER.parse(ReportService.ORDER_ASC));
130 | assertEquals(ReportService.ORDER.DESC, ReportService.ORDER.parse(ReportService.ORDER_DESC));
131 | assertNull(ReportService.ORDER.parse("invalid"));
132 | assertEquals(ReportService.ORDER_ATTR.PATH, ReportService.ORDER_ATTR.parse(ReportService.ORDER_BY_PATH));
133 | assertEquals(ReportService.ORDER_ATTR.VANITY_PATH, ReportService.ORDER_ATTR.parse(ReportService.ORDER_BY_VANITY_PATH));
134 | assertNull(ReportService.ORDER_ATTR.parse("invalid"));
135 | }
136 |
137 | }
138 |
--------------------------------------------------------------------------------
/core/src/main/java/com/ibm/aem/aemtenantspecificvanityurls/core/model/report/ReportService.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2023 IBM iX
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
5 | * associated documentation files (the "Software"), to deal in the Software without restriction,
6 | * including without limitation the rights to use, copy, modify, merge, publish, distribute,
7 | * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
8 | * furnished to do so, subject to the following conditions:
9 | *
10 | * The above copyright notice and this permission notice shall be included in all copies or
11 | * substantial portions of the Software.
12 | *
13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
14 | * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
15 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
16 | * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
17 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
18 | */
19 | package com.ibm.aem.aemtenantspecificvanityurls.core.model.report;
20 |
21 | import com.day.cq.search.PredicateGroup;
22 | import com.day.cq.search.Query;
23 | import com.day.cq.search.QueryBuilder;
24 | import com.day.cq.search.result.Hit;
25 | import com.day.cq.search.result.SearchResult;
26 | import com.day.cq.wcm.api.NameConstants;
27 | import com.ibm.aem.aemtenantspecificvanityurls.core.exceptions.AtsvuException;
28 | import org.apache.sling.api.resource.Resource;
29 | import org.apache.sling.api.resource.ResourceResolver;
30 | import org.osgi.service.component.annotations.Component;
31 | import org.osgi.service.component.annotations.Reference;
32 |
33 | import javax.jcr.RepositoryException;
34 | import javax.jcr.Session;
35 | import java.util.ArrayList;
36 | import java.util.HashMap;
37 | import java.util.List;
38 | import java.util.Map;
39 |
40 | /**
41 | * Searches for vanity entries for the report.
42 | *
43 | * @author Roland Gruber
44 | */
45 | @Component(service = ReportService.class)
46 | public class ReportService {
47 |
48 | public static final String ORDER_ASC = "asc";
49 | public static final String ORDER_DESC = "desc";
50 | public static final String ORDER_BY_PATH = "path";
51 | public static final String ORDER_BY_VANITY_PATH = "vanityUrl";
52 |
53 | /**
54 | * Order types
55 | */
56 | public enum ORDER {
57 | ASC,
58 | DESC;
59 |
60 | public static ORDER parse(String value) {
61 | if (ORDER_ASC.equals(value)) {
62 | return ASC;
63 | }
64 | if (ORDER_DESC.equals(value)) {
65 | return DESC;
66 | }
67 | return null;
68 | }
69 |
70 | }
71 |
72 | /**
73 | * Order attributes
74 | */
75 | public enum ORDER_ATTR {
76 | PATH,
77 | VANITY_PATH;
78 |
79 | public static ORDER_ATTR parse(String value) {
80 | if (ORDER_BY_PATH.equals(value)) {
81 | return PATH;
82 | }
83 | if (ORDER_BY_VANITY_PATH.equals(value)) {
84 | return VANITY_PATH;
85 | }
86 | return null;
87 | }
88 | }
89 |
90 | @Reference
91 | private QueryBuilder queryBuilder;
92 |
93 | /**
94 | * Returns a list of vanity entries.
95 | *
96 | * @param offset offset
97 | * @param limit search limit
98 | * @param orderAttr order attribute
99 | * @param order order direction
100 | * @param resolver resource resolver
101 | * @return entries
102 | * @throws AtsvuException error during search
103 | */
104 | public List getVanityEntries(int offset, int limit, ORDER_ATTR orderAttr, ORDER order, ResourceResolver resolver) throws AtsvuException {
105 | Map predicates = new HashMap<>();
106 | predicates.put("path", "/content");
107 | predicates.put("property", NameConstants.PN_SLING_VANITY_PATH);
108 | predicates.put("property.operation", "exists");
109 | if (orderAttr == ORDER_ATTR.PATH) {
110 | predicates.put("orderby", "@jcr:path");
111 | }
112 | else {
113 | predicates.put("orderby", "@sling:vanityPath");
114 | }
115 | if (ORDER.DESC == order) {
116 | predicates.put("orderby.sort", "desc");
117 | }
118 | PredicateGroup predicateGroup = PredicateGroup.create(predicates);
119 | Query query = queryBuilder.createQuery(predicateGroup, resolver.adaptTo(Session.class));
120 | if (limit != 0) {
121 | query.setHitsPerPage(limit);
122 | }
123 | if (offset != 0) {
124 | query.setStart(offset);
125 | }
126 | List entries = new ArrayList<>();
127 | try {
128 | SearchResult result = query.getResult();
129 | List hits = result.getHits();
130 | for (Hit hit : hits) {
131 | Resource resource = hit.getResource();
132 | Resource pageResource = resource.getParent();
133 | String[] vanities = resource.getValueMap().get(NameConstants.PN_SLING_VANITY_PATH, String[].class);
134 | for (String vanity : vanities) {
135 | ReportEntry entry = new ReportEntry();
136 | entry.setVanityUrl(vanity);
137 | entry.setPagePath(pageResource.getPath());
138 | entries.add(entry);
139 | }
140 | }
141 | }
142 | catch (RepositoryException e) {
143 | throw new AtsvuException("Vanity search failed", e);
144 | }
145 | return entries;
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/ui.apps/src/main/content/jcr_root/apps/ibm/aem-tenant-specific-vanity-urls/clientlibs/clientlib-author/js/backend-validation.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2023 - 2025 IBM iX
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
5 | * associated documentation files (the "Software"), to deal in the Software without restriction,
6 | * including without limitation the rights to use, copy, modify, merge, publish, distribute,
7 | * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
8 | * furnished to do so, subject to the following conditions:
9 | *
10 | * The above copyright notice and this permission notice shall be included in all copies or
11 | * substantial portions of the Software.
12 | *
13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
14 | * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
15 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
16 | * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
17 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
18 | */
19 |
20 | (function (window, Granite, $) {
21 | "use strict";
22 |
23 | const MSG_IS_LOADING = "loading";
24 |
25 | const registry = $(window).adaptTo("foundation-registry")
26 | const defaultValidator = registry.get("foundation.validation.validator")
27 | .filter(v => v.selector === "*")[0];
28 | const defaultShow = defaultValidator.show;
29 | const defaultClear = defaultValidator.clear;
30 |
31 | /**
32 | * Registers a custom asynchronous form validator using the Granite UI foundation validation
33 | * framework.
34 | *
35 | * Note: It's recommended to trigger an initial validation on page load, to avoid issues when
36 | * submitting the form.
37 | *
38 | * @param {Object} options - Configuration options for the validator.
39 | * @param {string} options.selector - CSS selector string to target input elements for validation.
40 | * @param {boolean} [options.ignoreEmpty=false] - If true, validation is skipped when the input
41 | * value is blank.
42 | * @param {function(HTMLElement, AbortController): Promise} options.checkValidity - Asynchronous
43 | * function that validates the input. It should return a promise that resolves with the
44 | * validation result.
45 | * @param {function(HTMLElement, any): string} options.validationMessage - Function that returns a
46 | * validation message string based on the validation result.
47 | * @param {function(HTMLElement, string, Object)} [options.show] - Optional function to show a
48 | * validation error. Defaults to a standard error handler.
49 | * @param {function(HTMLElement, Object)} [options.clear] - Optional function to clear a validation
50 | * state. Defaults to a standard clear handler.
51 | *
52 | * @example
53 | * registerValidator({
54 | * selector: 'input[name="./myInput"]',
55 | * checkValidity: async function(el, controller) {
56 | * const response = await fetch(validationUrl, {signal: controller.signal});
57 | * return await response.json();
58 | * },
59 | * validationMessage: function(el, result) {
60 | * return result.valid ? "" : result.message;
61 | * }
62 | * });
63 | *
64 | * $(document).one("foundation-contentloaded", function(e) {
65 | * inputFields.forEach(el => {
66 | * BackendValidation.validateField(el)
67 | * });
68 | * });
69 | */
70 | function registerValidator(options) {
71 | registry.register("foundation.validation.validator", {
72 | selector: options.selector,
73 | validate: function (el) {
74 | const context = el.validationContext = el.validationContext || {};
75 |
76 | if (options.ignoreEmpty && el.value.trim().length === 0) {
77 | return;
78 | }
79 |
80 | if (context.result && context.value === el.value) {
81 | return options.validationMessage(el, context.result);
82 | }
83 |
84 | if (context.controller && context.value !== el.value) {
85 | context.controller.abort();
86 | delete context.controller;
87 | }
88 |
89 | if (!context.controller) {
90 | context.controller = new AbortController();
91 | context.value = el.value;
92 | delete context.result;
93 | options.checkValidity(el, context.controller).then(result => {
94 | context.result = result;
95 | delete context.controller;
96 | validateField(el);
97 | });
98 | }
99 |
100 | return MSG_IS_LOADING;
101 | },
102 | show: function (el, message, ctx) {
103 | if (message === MSG_IS_LOADING) {
104 | el.classList.add("atsv-is-loading");
105 | options.clear ? options.clear(el, ctx) : defaultClear(el, ctx);
106 | } else {
107 | options.show ? options.show(el, message, ctx) : defaultShow(el, message, ctx);
108 | el.classList.remove("atsv-is-loading");
109 | }
110 | },
111 | clear: function (el, ctx) {
112 | el.classList.remove("atsv-is-loading");
113 | options.clear ? options.clear(el, ctx) : defaultClear(el, ctx);
114 | }
115 | });
116 | }
117 |
118 | /**
119 | * Trigger validation for the given element.
120 | */
121 | function validateField(el) {
122 | const elValidation = $(el).adaptTo("foundation-validation");
123 | elValidation.checkValidity();
124 | elValidation.updateUI();
125 | }
126 |
127 | window.tsvu_prefix = window.tsvu_prefix || {};
128 | window.tsvu_prefix.BackendValidation = window.tsvu_prefix.BackendValidation || {
129 | registerValidator: registerValidator,
130 | validateField: validateField
131 | };
132 |
133 | }(window, Granite, Granite.$));
--------------------------------------------------------------------------------
/core/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
18 |
19 | 4.0.0
20 |
21 | com.ibm.aem.aem-tenant-specific-vanity-urls
22 | aem-tenant-specific-vanity-urls
23 | 1.2.2-SNAPSHOT
24 | ../pom.xml
25 |
26 | aem-tenant-specific-vanity-urls.core
27 | AEM Tenant Specific Vanity URLs - Core
28 | Core bundle for AEM Tenant Specific Vanity URLs
29 |
30 |
31 |
32 | org.apache.sling
33 | sling-maven-plugin
34 |
35 |
36 | org.apache.felix
37 | maven-bundle-plugin
38 |
39 |
40 | biz.aQute.bnd
41 | bnd-baseline-maven-plugin
42 |
43 | false
44 |
45 |
46 |
47 | baseline
48 |
49 | baseline
50 |
51 |
52 |
53 |
54 |
55 | org.apache.maven.plugins
56 | maven-surefire-plugin
57 |
58 |
59 | org.apache.maven.plugins
60 | maven-jar-plugin
61 |
62 |
63 | ${project.build.outputDirectory}/META-INF/MANIFEST.MF
64 |
65 |
66 |
67 |
68 | org.jacoco
69 | jacoco-maven-plugin
70 |
71 |
72 | prepare-agent
73 |
74 | prepare-agent
75 |
76 |
77 |
78 | report
79 | prepare-package
80 |
81 | report
82 |
83 |
84 |
85 | post-unit-test
86 | test
87 |
88 | report
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 | org.osgi
99 | osgi.core
100 | provided
101 |
102 |
103 | org.osgi
104 | osgi.cmpn
105 | provided
106 |
107 |
108 | org.osgi
109 | osgi.annotation
110 | provided
111 |
112 |
113 |
114 | uk.org.lidalia
115 | slf4j-test
116 | test
117 |
118 |
119 | com.adobe.aem
120 | uber-jar
121 |
122 |
123 | com.adobe.cq
124 | core.wcm.components.core
125 | test
126 |
127 |
128 |
129 |
130 |
131 | org.junit.jupiter
132 | junit-jupiter
133 | test
134 |
135 |
136 | org.mockito
137 | mockito-core
138 | test
139 |
140 |
141 | org.mockito
142 | mockito-junit-jupiter
143 | test
144 |
145 |
146 | junit-addons
147 | junit-addons
148 | test
149 |
150 |
151 | io.wcm
152 | io.wcm.testing.aem-mock.junit5
153 | test
154 |
155 |
156 | org.apache.sling
157 | org.apache.sling.testing.caconfig-mock-plugin
158 | test
159 |
160 |
161 | com.adobe.cq
162 | core.wcm.components.testing.aem-mock-plugin
163 | test
164 |
165 |
166 |
167 | org.apache.sling
168 | org.apache.sling.models.impl
169 | 1.4.4
170 | test
171 |
172 |
173 | com.google.code.gson
174 | gson
175 | test
176 |
177 |
178 |
179 |
--------------------------------------------------------------------------------
/ui.apps/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
18 |
19 | 4.0.0
20 |
21 |
22 |
23 |
24 |
25 | com.ibm.aem.aem-tenant-specific-vanity-urls
26 | aem-tenant-specific-vanity-urls
27 | 1.2.2-SNAPSHOT
28 | ../pom.xml
29 |
30 |
31 |
32 |
33 |
34 | aem-tenant-specific-vanity-urls.ui.apps
35 | content-package
36 | AEM Tenant Specific Vanity URLs - UI apps
37 | UI apps package for AEM Tenant Specific Vanity URLs
38 |
39 |
40 |
41 |
42 |
43 | src/main/content/jcr_root
44 |
45 |
46 |
47 |
48 |
49 | org.apache.jackrabbit
50 | filevault-package-maven-plugin
51 |
52 |
53 | none
54 |
55 | IBM
56 | aem-tenant-specific-vanity-urls.ui.apps
57 | application
58 |
59 |
60 | com.ibm.aem.aem-tenant-specific-vanity-urls
61 | aem-tenant-specific-vanity-urls.ui.apps.structure
62 |
63 |
64 |
65 |
66 | io.wcm
67 | io.wcm.caconfig.editor.package
68 | 1.16.6
69 |
70 |
71 |
72 |
73 |
74 | com.day.jcr.vault
75 | content-package-maven-plugin
76 | true
77 |
78 | true
79 | true
80 |
81 |
82 |
83 |
84 | org.apache.sling
85 | htl-maven-plugin
86 |
87 |
88 | validate-htl-scripts
89 |
90 | validate
91 |
92 | generate-sources
93 |
94 | true
95 | org.apache.sling.scripting.sightly
96 | ${project.build.sourceDirectory}
97 |
98 | cssClassName
99 | decoration
100 | decorationTagName
101 | wcmmode
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 | com.ibm.aem.aem-tenant-specific-vanity-urls
117 | aem-tenant-specific-vanity-urls.core
118 | ${project.version}
119 |
120 |
121 |
122 | com.ibm.aem.aem-tenant-specific-vanity-urls
123 | aem-tenant-specific-vanity-urls.ui.apps.structure
124 | ${project.version}
125 | zip
126 |
127 |
128 |
129 |
130 | org.apache.sling
131 | org.apache.sling.scripting.sightly.runtime
132 | 1.2.6-1.4.0
133 | provided
134 |
135 |
136 |
137 | io.wcm
138 | io.wcm.caconfig.editor.package
139 | 1.16.6
140 | zip
141 |
142 |
143 |
144 |
145 |
--------------------------------------------------------------------------------
/ui.apps/src/main/content/jcr_root/apps/ibm/aem-tenant-specific-vanity-urls/clientlibs/clientlib-author/js/site.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2023 - 2025 IBM iX
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
5 | * associated documentation files (the "Software"), to deal in the Software without restriction,
6 | * including without limitation the rights to use, copy, modify, merge, publish, distribute,
7 | * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
8 | * furnished to do so, subject to the following conditions:
9 | *
10 | * The above copyright notice and this permission notice shall be included in all copies or
11 | * substantial portions of the Software.
12 | *
13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
14 | * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
15 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
16 | * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
17 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
18 | */
19 |
20 | window.tsvu_prefix = window.tsvu_prefix || {};
21 |
22 | /**
23 | * Setup of form and field handlers.
24 | */
25 | window.tsvu_prefix.init = function () {
26 | if (window.location.pathname === '/mnt/overlay/wcm/core/content/sites/properties.html') {
27 | const prefixUrl = window.tsvu_prefix.getTsvuServletUrl();
28 | if (prefixUrl) {
29 | fetch(prefixUrl)
30 | .then((response) => response.json())
31 | .then((data) => {
32 | if (data.prefix && (data.prefix.length > 0)) {
33 | window.tsvu_prefix.clearPrefixWhenLoaded(data.prefix);
34 | window.tsvu_prefix.addSaveHandler(data.prefix, data.toLowerCase);
35 | window.tsvu_prefix.addFieldValidator();
36 | }
37 | });
38 | }
39 | }
40 | }
41 |
42 | /**
43 | * Constructs a URL for the `TenantSpecificVanityUrlServlet` with optional parameters.
44 | *
45 | * @param params An optional object representing query parameters to append to the URL.
46 | */
47 | window.tsvu_prefix.getTsvuServletUrl = function (params) {
48 | const urlParams = new URLSearchParams(window.location.search);
49 | const item = urlParams.get('item');
50 | if (item && item.startsWith("/content")) {
51 | let url = item + ".tsvu.json";
52 | if (params) {
53 | url += "?" + new URLSearchParams(params).toString();
54 | }
55 | return url;
56 | }
57 | }
58 |
59 | /**
60 | * Clears the prefix from existing vanity URL fields once the content is loaded.
61 | *
62 | * @param prefix vanity prefix
63 | */
64 | window.tsvu_prefix.clearPrefixWhenLoaded = function(prefix) {
65 | const fields = window.tsvu_prefix.findInputFields();
66 | if (fields.length > 0) {
67 | window.tsvu_prefix.clearPrefix(prefix);
68 | }
69 | else {
70 | // probably, content is not yet loaded, wait
71 | $(document).on("foundation-contentloaded", function(e) {
72 | window.tsvu_prefix.clearPrefix(prefix);
73 | });
74 | }
75 | }
76 |
77 | /**
78 | * Returns a list of vanity URL input fields.
79 | *
80 | * @return fields
81 | */
82 | window.tsvu_prefix.findInputFields = function() {
83 | return document.querySelectorAll('input[name="./sling:vanityPath"]');
84 | }
85 |
86 | /**
87 | * Clears the prefix from existing vanity URL fields.
88 | *
89 | * @param prefix vanity prefix
90 | */
91 | window.tsvu_prefix.clearPrefix = function(prefix) {
92 | const fields = window.tsvu_prefix.findInputFields();
93 | fields.forEach(function (currentValue) {
94 | if (currentValue.value && currentValue.value.startsWith(prefix)) {
95 | currentValue.value = currentValue.value.substring(prefix.length);
96 | }
97 | });
98 | }
99 |
100 | /**
101 | * Registers the form save handler and readds the prefixes before form is saved.
102 | *
103 | * @param prefix vanity prefix
104 | * @param toLowerCase convert the value to lower-case
105 | */
106 | window.tsvu_prefix.addSaveHandler = function (prefix, toLowerCase) {
107 | $(window).adaptTo("foundation-registry").register("foundation.form.submit", {
108 | selector: '*',
109 | handler: function() {
110 | const fields = window.tsvu_prefix.findInputFields();
111 | fields.forEach(function (currentValue) {
112 | if (currentValue.value && !currentValue.value.startsWith(prefix)) {
113 | if (toLowerCase) {
114 | currentValue.value = currentValue.value.toLowerCase();
115 | }
116 | currentValue.value = window.tsvu_prefix.prependPrefixIfMissing(
117 | currentValue.value, prefix);
118 | }
119 | });
120 | return {};
121 | }
122 | });
123 | }
124 |
125 | /**
126 | * Prepends the specified prefix to the given vanity path if it is not already a descendant of the
127 | * prefix.
128 | * Examples:
129 | *
130 | * prependPrefixIfMissing("wow", "/us/en/") = "/us/en/wow"
131 | *
132 | *
133 | * @param vanityPath the vanity path to check and potentially prepend the prefix to
134 | * @param prefix the prefix to prepend if the vanity path is not already under it
135 | * @return the resulting path with the prefix prepended if necessary
136 | */
137 | window.tsvu_prefix.prependPrefixIfMissing = function (vanityPath, prefix) {
138 | if (vanityPath.startsWith(prefix)) {
139 | return vanityPath;
140 | }
141 | return prefix + vanityPath;
142 | }
143 |
144 | /**
145 | * Registers backend validation for the vanity URL input fields.
146 | */
147 | window.tsvu_prefix.addFieldValidator = function () {
148 | window.tsvu_prefix.BackendValidation.registerValidator({
149 | selector: 'input[name="./sling:vanityPath"]',
150 | ignoreEmpty: true,
151 | checkValidity: async function (el, controller) {
152 | try {
153 | const validationUrl = window.tsvu_prefix.getTsvuServletUrl({
154 | cmd: "unique",
155 | path: el.value
156 | });
157 | if (!validationUrl) {
158 | throw new Error("Can't build validation url");
159 | }
160 | const response = await fetch(validationUrl, {
161 | signal: controller.signal
162 | });
163 | if (!response.ok) {
164 | throw new Error(`Response status: ${response.status}`);
165 | }
166 | return await response.json();
167 | } catch (e) {
168 | return {
169 | error: true,
170 | message: e.message
171 | };
172 | }
173 | },
174 | validationMessage(el, result) {
175 | if (result.error) {
176 | return Granite.I18n.get("Error checking for unique value");
177 | }
178 |
179 | if (!result.valid) {
180 | const conflictLink = createConflictLink(result.conflicts[0]);
181 | return Granite.I18n.get("The value is already used: ") + conflictLink;
182 | }
183 | }
184 | });
185 |
186 | $(document).one("foundation-contentloaded", function(e) {
187 | window.tsvu_prefix.findInputFields().forEach(el => {
188 | window.tsvu_prefix.BackendValidation.validateField(el)
189 | });
190 | });
191 |
192 | function createConflictLink(conflict) {
193 | const editorUrl = Granite.HTTP.externalize("/mnt/overlay/wcm/core/content/sites/properties.html");
194 | const href = `${editorUrl}?${new URLSearchParams({item: conflict.path})}`;
195 | return `${conflict.title} `;
196 | }
197 | }
198 |
199 | window.tsvu_prefix.init();
200 |
--------------------------------------------------------------------------------
/examples/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
18 |
19 | 4.0.0
20 |
21 |
22 |
23 |
24 |
25 | com.ibm.aem.aem-tenant-specific-vanity-urls
26 | aem-tenant-specific-vanity-urls
27 | 1.2.2-SNAPSHOT
28 | ../pom.xml
29 |
30 |
31 |
32 |
33 |
34 | aem-tenant-specific-vanity-urls.examples
35 | content-package
36 | AEM Tenant Specific Vanity URLs - Examples
37 | Example content package for AEM Tenant Specific Vanity URLs
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | org.apache.jackrabbit
49 | filevault-package-maven-plugin
50 | true
51 |
52 | IBM
53 | container
54 |
55 | true
56 |
57 |
58 | com.ibm.aem.aem-tenant-specific-vanity-urls
59 | aem-tenant-specific-vanity-urls.examples.content
60 | zip
61 | /apps/ibm/aem-tenant-specific-vanity-urls-examples/install
62 |
63 |
64 |
65 |
66 |
67 | com.day.jcr.vault
68 | content-package-maven-plugin
69 | true
70 |
71 | true
72 | true
73 |
74 |
75 |
76 | maven-clean-plugin
77 |
78 |
79 | auto-clean
80 | initialize
81 |
82 | clean
83 |
84 |
85 |
86 |
87 |
88 | com.adobe.aem
89 | aemanalyser-maven-plugin
90 |
91 |
92 | aem-analyser
93 |
94 | project-analyse
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 | autoInstallSinglePackage
108 |
109 | false
110 |
111 |
112 |
113 |
114 | com.day.jcr.vault
115 | content-package-maven-plugin
116 |
117 |
118 | install-package
119 |
120 | install
121 |
122 |
123 | http://${aem.host}:${aem.port}/crx/packmgr/service.jsp
124 | true
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 | autoInstallSinglePackagePublish
134 |
135 | false
136 |
137 |
138 |
139 |
140 | com.day.jcr.vault
141 | content-package-maven-plugin
142 |
143 |
144 | install-package-publish
145 |
146 | install
147 |
148 |
149 | http://${aem.publish.host}:${aem.publish.port}/crx/packmgr/service.jsp
150 | true
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 | com.ibm.aem.aem-tenant-specific-vanity-urls
166 | aem-tenant-specific-vanity-urls.examples.content
167 | ${project.version}
168 | zip
169 |
170 |
171 |
172 |
173 |
--------------------------------------------------------------------------------
/core/src/test/java/com/ibm/aem/aemtenantspecificvanityurls/core/servlets/TenantSpecificVanityUrlServletTest.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2023 - 2025 IBM iX
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
5 | * associated documentation files (the "Software"), to deal in the Software without restriction,
6 | * including without limitation the rights to use, copy, modify, merge, publish, distribute,
7 | * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
8 | * furnished to do so, subject to the following conditions:
9 | *
10 | * The above copyright notice and this permission notice shall be included in all copies or
11 | * substantial portions of the Software.
12 | *
13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
14 | * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
15 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
16 | * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
17 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
18 | */
19 | package com.ibm.aem.aemtenantspecificvanityurls.core.servlets;
20 |
21 | import com.day.cq.search.Query;
22 | import com.day.cq.search.QueryBuilder;
23 | import com.day.cq.search.result.SearchResult;
24 | import com.day.cq.wcm.api.Page;
25 | import com.ibm.aem.aemtenantspecificvanityurls.core.caconfig.TenantSpecificVanityUrlConfig;
26 | import org.apache.commons.collections4.IteratorUtils;
27 | import org.apache.sling.api.SlingHttpServletRequest;
28 | import org.apache.sling.api.SlingHttpServletResponse;
29 | import org.apache.sling.api.resource.Resource;
30 | import org.apache.sling.api.resource.ResourceResolver;
31 | import org.apache.sling.caconfig.ConfigurationBuilder;
32 | import org.apache.sling.caconfig.resource.ConfigurationResourceResolver;
33 | import org.junit.jupiter.api.BeforeEach;
34 | import org.junit.jupiter.api.Test;
35 | import org.junit.jupiter.api.extension.ExtendWith;
36 | import org.mockito.InjectMocks;
37 | import org.mockito.Mock;
38 | import org.mockito.Mockito;
39 | import org.mockito.junit.jupiter.MockitoExtension;
40 | import org.mockito.junit.jupiter.MockitoSettings;
41 | import org.mockito.quality.Strictness;
42 |
43 | import javax.jcr.Session;
44 | import javax.servlet.ServletException;
45 | import java.io.IOException;
46 | import java.io.PrintWriter;
47 | import java.util.Arrays;
48 |
49 | import static com.ibm.aem.aemtenantspecificvanityurls.core.servlets.TenantSpecificVanityUrlServlet.*;
50 | import static org.mockito.Mockito.*;
51 |
52 | /**
53 | * Tests TenantSpecificVanityUrlServlet
54 | */
55 | @ExtendWith(MockitoExtension.class)
56 | @MockitoSettings(strictness = Strictness.LENIENT)
57 | public class TenantSpecificVanityUrlServletTest {
58 |
59 | public static final String MYPREFIX = "/myprefix";
60 | public static final String CURRENT_PAGE_PATH = "/content/wknd/us/en/adventures/yosemite-backpacking";
61 | public static final String CONFLICTING_PAGE_PATH = "/content/wknd/us/en/adventures/ski-touring-mont-blanc";
62 | public static final String CONFLICTING_PAGE_TITLE = "Ski Touring Mont Blanc";
63 |
64 | @Mock
65 | private SlingHttpServletRequest request;
66 |
67 | @Mock
68 | private SlingHttpServletResponse response;
69 |
70 | @Mock
71 | private Resource resource;
72 |
73 | @Mock
74 | private ConfigurationBuilder builder;
75 |
76 | @Mock
77 | private TenantSpecificVanityUrlConfig config;
78 |
79 | @Mock
80 | private PrintWriter writer;
81 |
82 | @Mock
83 | private QueryBuilder queryBuilder;
84 |
85 | @Mock
86 | private ConfigurationResourceResolver configurationResourceResolver;
87 |
88 | @Mock
89 | private ResourceResolver resolver;
90 |
91 | @Mock
92 | private Session session;
93 |
94 | @Mock
95 | private Query query;
96 |
97 | @Mock
98 | private SearchResult searchResult;
99 |
100 | @Mock
101 | private Page currentPage;
102 |
103 | @InjectMocks
104 | private TenantSpecificVanityUrlServlet servlet;
105 |
106 | @BeforeEach
107 | void setup() throws IOException {
108 | when(request.getResource()).thenReturn(resource);
109 | when(resource.adaptTo(ConfigurationBuilder.class)).thenReturn(builder);
110 | when(builder.as(TenantSpecificVanityUrlConfig.class)).thenReturn(config);
111 | when(config.prefix()).thenReturn(MYPREFIX);
112 | when(response.getWriter()).thenReturn(writer);
113 | when(configurationResourceResolver.getAllContextPaths(resource)).thenReturn(Arrays.asList(MYPREFIX));
114 | }
115 |
116 | @Test
117 | void doGet() throws ServletException, IOException {
118 | servlet.doGet(request, response);
119 |
120 | verify(writer).write("{\"prefix\":\"" + MYPREFIX + "\",\"toLowerCase\":false}");
121 | }
122 |
123 | @Test
124 | void testUniqueVanityUrl() throws ServletException, IOException {
125 | when(resource.adaptTo(Page.class)).thenReturn(currentPage);
126 | when(request.getParameter(RP_COMMAND)).thenReturn(CMD_CHECK_UNIQUENESS);
127 | when(request.getParameter(RP_VANITY_PATH)).thenReturn("wow");
128 |
129 | when(request.getResourceResolver()).thenReturn(resolver);
130 | when(resolver.adaptTo(Session.class)).thenReturn(session);
131 | when(queryBuilder.createQuery(Mockito.any(), Mockito.eq(session))).thenReturn(query);
132 | when(query.getResult()).thenReturn(searchResult);
133 | when(searchResult.getResources()).thenReturn(IteratorUtils.arrayIterator());
134 |
135 | servlet.doGet(request, response);
136 |
137 | verify(writer).write("{" +
138 | "\"valid\":true," +
139 | "\"prefix\":\"" + MYPREFIX + "\"," +
140 | "\"vanityPath\":\"" + MYPREFIX + "wow\"" +
141 | "}");
142 | }
143 |
144 | @Test
145 | void testUniqueVanityUrlWithLowerCase() throws ServletException, IOException {
146 | when(config.toLowerCase()).thenReturn(true);
147 |
148 | when(resource.adaptTo(Page.class)).thenReturn(currentPage);
149 | when(request.getParameter(RP_COMMAND)).thenReturn(CMD_CHECK_UNIQUENESS);
150 | when(request.getParameter(RP_VANITY_PATH)).thenReturn("WOW");
151 |
152 | when(request.getResourceResolver()).thenReturn(resolver);
153 | when(resolver.adaptTo(Session.class)).thenReturn(session);
154 | when(queryBuilder.createQuery(Mockito.any(), Mockito.eq(session))).thenReturn(query);
155 | when(query.getResult()).thenReturn(searchResult);
156 | when(searchResult.getResources()).thenReturn(IteratorUtils.arrayIterator());
157 |
158 | servlet.doGet(request, response);
159 |
160 | verify(writer).write("{" +
161 | "\"valid\":true," +
162 | "\"prefix\":\"" + MYPREFIX + "\"," +
163 | "\"vanityPath\":\"" + MYPREFIX + "wow\"" +
164 | "}");
165 | }
166 |
167 | @Test
168 | void testDuplicateVanityUrl() throws ServletException, IOException {
169 | when(resource.adaptTo(Page.class)).thenReturn(currentPage);
170 | when(currentPage.getPath()).thenReturn(CURRENT_PAGE_PATH);
171 | when(request.getParameter(RP_COMMAND)).thenReturn(CMD_CHECK_UNIQUENESS);
172 | when(request.getParameter(RP_VANITY_PATH)).thenReturn("wow");
173 |
174 | when(request.getResourceResolver()).thenReturn(resolver);
175 | when(resolver.adaptTo(Session.class)).thenReturn(session);
176 | when(queryBuilder.createQuery(Mockito.any(), Mockito.eq(session))).thenReturn(query);
177 | when(query.getResult()).thenReturn(searchResult);
178 | Page conflictingPage = mock(Page.class);
179 | when(conflictingPage.getPath()).thenReturn(CONFLICTING_PAGE_PATH);
180 | when(conflictingPage.getTitle()).thenReturn(CONFLICTING_PAGE_TITLE);
181 | Resource conflictingPageResource = mock(Resource.class);
182 | when(conflictingPageResource.adaptTo(Page.class)).thenReturn(conflictingPage);
183 | when(searchResult.getResources()).thenReturn(IteratorUtils.arrayIterator(new Resource[]{conflictingPageResource}));
184 |
185 | servlet.doGet(request, response);
186 |
187 | verify(writer).write("{" +
188 | "\"valid\":false," +
189 | "\"prefix\":\"" + MYPREFIX + "\"," +
190 | "\"vanityPath\":\"" + MYPREFIX + "wow\"," +
191 | "\"conflicts\":[" +
192 | "{\"path\":\"" + CONFLICTING_PAGE_PATH + "\",\"title\":\"" + CONFLICTING_PAGE_TITLE + "\"}" +
193 | "]}");
194 | }
195 |
196 | }
197 |
--------------------------------------------------------------------------------
/all/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
18 |
19 | 4.0.0
20 |
21 |
22 |
23 |
24 |
25 | com.ibm.aem.aem-tenant-specific-vanity-urls
26 | aem-tenant-specific-vanity-urls
27 | 1.2.2-SNAPSHOT
28 | ../pom.xml
29 |
30 |
31 |
32 |
33 |
34 | aem-tenant-specific-vanity-urls.all
35 | content-package
36 | AEM Tenant Specific Vanity URLs - All
37 | All content package for AEM Tenant Specific Vanity URLs
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | org.apache.jackrabbit
49 | filevault-package-maven-plugin
50 | true
51 |
52 | IBM
53 | container
54 |
55 | true
56 |
57 |
58 | com.ibm.aem.aem-tenant-specific-vanity-urls
59 | aem-tenant-specific-vanity-urls.ui.apps
60 | zip
61 | /apps/ibm/aem-tenant-specific-vanity-urls/install.author
62 |
63 |
64 | com.ibm.aem.aem-tenant-specific-vanity-urls
65 | aem-tenant-specific-vanity-urls.core
66 | /apps/ibm/aem-tenant-specific-vanity-urls/install.author
67 |
68 |
69 |
70 |
71 |
72 | com.day.jcr.vault
73 | content-package-maven-plugin
74 | true
75 |
76 | true
77 | true
78 |
79 |
80 |
81 | maven-clean-plugin
82 |
83 |
84 | auto-clean
85 | initialize
86 |
87 | clean
88 |
89 |
90 |
91 |
92 |
93 | com.adobe.aem
94 | aemanalyser-maven-plugin
95 |
96 |
97 | aem-analyser
98 |
99 | project-analyse
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 | autoInstallSinglePackage
113 |
114 | false
115 |
116 |
117 |
118 |
119 | com.day.jcr.vault
120 | content-package-maven-plugin
121 |
122 |
123 | install-package
124 |
125 | install
126 |
127 |
128 | http://${aem.host}:${aem.port}/crx/packmgr/service.jsp
129 | true
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 | autoInstallSinglePackagePublish
139 |
140 | false
141 |
142 |
143 |
144 |
145 | com.day.jcr.vault
146 | content-package-maven-plugin
147 |
148 |
149 | install-package-publish
150 |
151 | install
152 |
153 |
154 | http://${aem.publish.host}:${aem.publish.port}/crx/packmgr/service.jsp
155 | true
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 | com.ibm.aem.aem-tenant-specific-vanity-urls
171 | aem-tenant-specific-vanity-urls.ui.apps
172 | ${project.version}
173 | zip
174 |
175 |
176 |
177 |
--------------------------------------------------------------------------------
/core/src/main/java/com/ibm/aem/aemtenantspecificvanityurls/core/servlets/TenantSpecificVanityUrlServlet.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2023 - 2025 IBM iX
3 | *
4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
5 | * associated documentation files (the "Software"), to deal in the Software without restriction,
6 | * including without limitation the rights to use, copy, modify, merge, publish, distribute,
7 | * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
8 | * furnished to do so, subject to the following conditions:
9 | *
10 | * The above copyright notice and this permission notice shall be included in all copies or
11 | * substantial portions of the Software.
12 | *
13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
14 | * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
15 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
16 | * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
17 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
18 | */
19 | package com.ibm.aem.aemtenantspecificvanityurls.core.servlets;
20 |
21 | import java.io.IOException;
22 | import java.nio.charset.StandardCharsets;
23 | import java.util.ArrayList;
24 | import java.util.Collection;
25 | import java.util.HashMap;
26 | import java.util.Iterator;
27 | import java.util.List;
28 | import java.util.Map;
29 |
30 | import javax.jcr.Session;
31 | import javax.servlet.Servlet;
32 | import javax.servlet.ServletException;
33 | import javax.servlet.http.HttpServletResponse;
34 |
35 | import org.apache.commons.collections4.iterators.FilterIterator;
36 | import org.apache.commons.collections4.iterators.TransformIterator;
37 | import org.apache.commons.lang3.StringUtils;
38 | import org.apache.http.entity.ContentType;
39 | import org.apache.sling.api.SlingHttpServletRequest;
40 | import org.apache.sling.api.SlingHttpServletResponse;
41 | import org.apache.sling.api.resource.Resource;
42 | import org.apache.sling.api.resource.ResourceResolver;
43 | import org.apache.sling.api.servlets.SlingSafeMethodsServlet;
44 | import org.apache.sling.caconfig.ConfigurationBuilder;
45 | import org.apache.sling.caconfig.resource.ConfigurationResourceResolver;
46 | import org.osgi.service.component.annotations.Component;
47 | import org.osgi.service.component.annotations.Reference;
48 |
49 | import com.day.cq.search.PredicateGroup;
50 | import com.day.cq.search.Query;
51 | import com.day.cq.search.QueryBuilder;
52 | import com.day.cq.search.result.SearchResult;
53 | import com.day.cq.wcm.api.Page;
54 | import com.google.gson.JsonArray;
55 | import com.google.gson.JsonObject;
56 | import com.ibm.aem.aemtenantspecificvanityurls.core.caconfig.TenantSpecificVanityUrlConfig;
57 | import com.ibm.aem.aemtenantspecificvanityurls.core.util.VanityUrlUtils;
58 |
59 | /**
60 | * Servlet to provide prefix for vanity URLs.
61 | *
62 | * @author Roland Gruber
63 | */
64 | @Component(service = Servlet.class,
65 | immediate = true,
66 | property = {
67 | "sling.servlet.methods=GET",
68 | "sling.servlet.extensions=json",
69 | "sling.servlet.selectors=tsvu",
70 | "sling.servlet.resourceTypes=sling/servlet/default"})
71 | public class TenantSpecificVanityUrlServlet extends SlingSafeMethodsServlet {
72 |
73 | public static final String RP_COMMAND = "cmd";
74 | public static final String CMD_LOAD_CONFIG = "cfg";
75 | public static final String CMD_CHECK_UNIQUENESS = "unique";
76 |
77 | public static final String RP_VANITY_PATH = "path";
78 |
79 | @Reference
80 | private QueryBuilder queryBuilder;
81 |
82 | @Reference
83 | private ConfigurationResourceResolver configurationResourceResolver;
84 |
85 | @Override
86 | protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response)
87 | throws ServletException, IOException {
88 | String cmd = StringUtils.defaultIfBlank(request.getParameter(RP_COMMAND), CMD_LOAD_CONFIG);
89 |
90 | if (StringUtils.equals(cmd, CMD_LOAD_CONFIG)) {
91 | doLoadConfigCommand(request, response);
92 | } else if (StringUtils.equals(cmd, CMD_CHECK_UNIQUENESS)) {
93 | doCheckUniquenessCommand(request, response);
94 | } else {
95 | response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Unsupported command: " + cmd);
96 | }
97 | }
98 |
99 | private void doLoadConfigCommand(SlingHttpServletRequest request, SlingHttpServletResponse response) throws IOException {
100 | TenantSpecificVanityUrlConfig config = resolveVanityUrlConfig(request);
101 |
102 | JsonObject json = new JsonObject();
103 | json.addProperty("prefix", config.prefix());
104 | json.addProperty("toLowerCase", config.toLowerCase());
105 |
106 | sendResponse(response, json);
107 | }
108 |
109 | private void doCheckUniquenessCommand(SlingHttpServletRequest request, SlingHttpServletResponse response) throws IOException {
110 | JsonObject result = new JsonObject();
111 | result.addProperty("valid", true);
112 | TenantSpecificVanityUrlConfig config = resolveVanityUrlConfig(request);
113 | if (config.prefix() == null) {
114 | sendResponse(response, result);
115 | return;
116 | }
117 |
118 | String vanityPath = request.getParameter(RP_VANITY_PATH);
119 | if (StringUtils.isBlank(vanityPath)) {
120 | response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Vanity path required");
121 | return;
122 | }
123 | vanityPath = VanityUrlUtils.prependPrefixIfMissing(vanityPath, config.prefix());
124 | if (config.toLowerCase()) {
125 | vanityPath = StringUtils.lowerCase(vanityPath);
126 | }
127 |
128 | ResourceResolver resolver = request.getResourceResolver();
129 | // the search path for duplicates is the highest level that contains a CA config
130 | Collection allContextPaths = configurationResourceResolver.getAllContextPaths(request.getResource());
131 | List orderedContextPaths = new ArrayList<>(allContextPaths);
132 | orderedContextPaths.sort(null);
133 | String searchPath = orderedContextPaths.get(0);
134 | Iterator conflictingPages = filterCurrentPage(
135 | findPagesByVanityPath(searchPath, vanityPath, resolver),
136 | request.getResource().adaptTo(Page.class)
137 | );
138 |
139 | result.addProperty("prefix", config.prefix());
140 | result.addProperty("vanityPath", vanityPath);
141 | result.addProperty("valid", !conflictingPages.hasNext());
142 | if (conflictingPages.hasNext()) {
143 | JsonArray conflicts = new JsonArray();
144 | while (conflictingPages.hasNext()) {
145 | Page page = conflictingPages.next();
146 | JsonObject conflict = new JsonObject();
147 | conflict.addProperty("path", page.getPath());
148 | conflict.addProperty("title", StringUtils.defaultIfBlank(page.getTitle(), page.getName()));
149 | conflicts.add(conflict);
150 | }
151 | result.add("conflicts", conflicts);
152 | }
153 |
154 | sendResponse(response, result);
155 | }
156 |
157 | private TenantSpecificVanityUrlConfig resolveVanityUrlConfig(SlingHttpServletRequest request) {
158 | Resource resource = request.getResource();
159 | ConfigurationBuilder configurationBuilder = resource.adaptTo(ConfigurationBuilder.class);
160 | return configurationBuilder.as(TenantSpecificVanityUrlConfig.class);
161 | }
162 |
163 | private Iterator findPagesByVanityPath(String contentPath, String vanityPath, ResourceResolver resolver) {
164 | Map map = new HashMap<>();
165 | map.put("path", contentPath);
166 | map.put("type", "cq:Page");
167 | map.put("1_property", "jcr:content/sling:vanityPath");
168 | map.put("1_property.value", vanityPath);
169 | map.put("p.limit", "2");
170 |
171 | Session session = resolver.adaptTo(Session.class);
172 | Query query = queryBuilder.createQuery(PredicateGroup.create(map), session);
173 |
174 | SearchResult result = query.getResult();
175 | return new TransformIterator<>(result.getResources(), resource -> resource.adaptTo(Page.class));
176 | }
177 |
178 | private Iterator filterCurrentPage(Iterator pages, Page currentPage) {
179 | if (currentPage != null) {
180 | return new FilterIterator<>(pages, page -> !StringUtils.equals(page.getPath(), currentPage.getPath()));
181 | }
182 | return pages;
183 | }
184 |
185 | private void sendResponse(SlingHttpServletResponse response, JsonObject result) throws IOException {
186 | response.setContentType(ContentType.APPLICATION_JSON.getMimeType());
187 | response.setCharacterEncoding(StandardCharsets.UTF_8.name());
188 | response.getWriter().write(result.toString());
189 | }
190 |
191 | }
192 |
--------------------------------------------------------------------------------
/dispatcher/src/conf.d/dispatcher_vhost.conf:
--------------------------------------------------------------------------------
1 | #
2 | # This is a file provided by the runtime environment and only included for
3 | # illustration purposes.
4 | #
5 | # DO NOT EDIT this file, your changes will have no impact on your deployment.
6 | #
7 |
8 | ServerName dispatcher
9 |
10 | Include conf.d/variables/default.vars
11 | Include conf.d/variables/global.vars
12 |
13 |
14 | # WARNING!!! The probe paths below are INTERNAL and RESERVED - please DO NOT USE them in your virtual host configurations!
15 |
16 | # Liveness probe URL
17 | Alias "/system/probes/live" probes/live-status.json
18 | # Readiness probe URL
19 | Alias "/system/probes/ready" probes/ready-status.json
20 | # Startup probe URL
21 | Alias "/system/probes/start" probes/startup-status.json
22 |
23 | # internal probes endpoint
24 |
25 | RewriteEngine Off
26 |
27 |
28 |
29 | SetHandler default-handler
30 | AllowOverride None
31 | Require all granted
32 |
33 |
34 |
35 | #SKYOPS-13837: Proxy static frontend code requests through dispatcher
36 |
37 | SSLProxyEngine on
38 |
39 | RewriteRule "^/mnt/var/www/html/libs/cq/frontend-static(/[^\.].*)$" "%{env:FRONTEND_URI_PREFIX}$1?%{env:FRONTEND_URI_SUFFIX}" [P,L]
40 |
41 |
42 |
43 | # CQ-4315090: Allow the functional replication to access publish instance directly for dev and stage environments
44 |
45 |
46 | ProxyPassMatch http://${AEM_HOST}:${AEM_PORT}
47 | RewriteEngine Off
48 |
49 |
50 |
51 |
52 | ProxyPassMatch http://${AEM_HOST}:${AEM_PORT}
53 | RewriteEngine Off
54 |
55 |
56 |
57 | # If the module loads correctly then apply base settings for the module
58 |
59 | # location of the configuration file. eg: 'conf/dispatcher.any'
60 | DispatcherConfig conf.dispatcher.d/dispatcher.any
61 |
62 | # Format for the dispatcher log file
63 | LogFormat "%t \"%m %{dispatcher:uri}e%q %H\" %{dispatcher:status}e %{dispatcher:cache}e [%{dispatcher:backend}e] %{ms}Tms \"%{Host}i\"" dispatcher
64 | CustomLog "| /usr/sbin/rotatelogs -e -f -t logs/dispatcher.log 86400" dispatcher "expr=%{HANDLER} == 'dispatcher-handler'"
65 |
66 | # Log level for the dispatcher module
67 | LogLevel dispatcher_module:${DISP_LOG_LEVEL} rewrite_module:${REWRITE_LOG_LEVEL}
68 |
69 | # if turned to 1, request to / are not handled by the dispatcher
70 | # use the mod_alias then for the correct mapping
71 | DispatcherDeclineRoot Off
72 |
73 | # if turned to 1, the dispatcher uses the URL already processed
74 | # by handlers preceeding the dispatcher (i.e. mod_rewrite)
75 | # instead of the original one passed to the web server.
76 | DispatcherUseProcessedURL On
77 | # Default value of 0 but if its set to 1 then the dispatcher will have apache handle all errors
78 | # If set to a string of error numbers it will only hand off those errors to apache to handle
79 | # DispatcherPassError 403,404
80 | # DispatcherPassError 1
81 |
82 | # Setting to replace the Host header with the value of X-Forwarded-Host
83 | #
84 | # Possible values are: Off, On or a file name, containing the edge key to expect
85 | # Default: Off
86 | DispatcherUseForwardedHost ${FORWARDED_HOST_SETTING}
87 |
88 | # When enabled it removes Cache-Control headers set by mod_expires to unchacheable content
89 | DispatcherRestrictUncacheableContent On
90 |
91 |
92 |
93 |
94 | # Expire text/html after this many seconds
95 | ExpiresActive On
96 | ExpiresByType text/html A${EXPIRATION_TIME}
97 |
98 | Header unset Age
99 |
100 |
101 | # SITES-11040 Do ProxyPassMatch, if caching for GraphQL Persisted Queries is not enabled
102 |
103 | # SITES-3659 Prevent re-encodes of URLs sent to GraphQL Persisted Queries API endpoint
104 |
105 | ProxyPassMatch http://${AEM_HOST}:${AEM_PORT} nocanon
106 |
107 |
108 |
109 | # Legacy /systemready mapped to new Health probe URL /system/probes/health in AEM
110 |
111 | ProxyPass http://${AEM_HOST}:${AEM_PORT}/system/probes/health
112 | RewriteEngine Off
113 |
114 |
115 | # Allow ingressroute checks through on /system/probes/health (regardless of dispatcher filters)
116 |
117 | ProxyPass http://${AEM_HOST}:${AEM_PORT}/system/probes/health
118 | RewriteEngine Off
119 |
120 |
121 | # Allow access to CRXDE on dev environment
122 |
123 |
124 | ProxyPassMatch http://${AEM_HOST}:${AEM_PORT}
125 | RewriteEngine Off
126 |
127 |
128 |
129 | # CQ-4287185: Allow access to magento reverse-proxy endpoint
130 |
131 | SSLProxyEngine on
132 | # CIF-2557 add ProxyRemote to tunnel reverse-proxy traffic through egress proxy if available
133 |
134 | ProxyRemote ${COMMERCE_ENDPOINT} "http://${AEM_HTTP_PROXY_HOST}:${AEM_HTTP_PROXY_PORT}"
135 |
136 |
137 | # Use an empty back reference from ProxyPassMatch to the LocationMatch regex to prevent the
138 | # original URL being appended to the proxy request
139 | ProxyPassMatch ${COMMERCE_ENDPOINT}$2
140 | ProxyPassReverse ${COMMERCE_ENDPOINT}
141 | RewriteEngine Off
142 | # CIF-2971: Experience Platform Connector cookie to header forwarding
143 | SetEnvIfNoCase Cookie "(^| )aep-segments-membership=([^;]*)" AEP_SEGMENTS_MEMBERSHIP=$2
144 | RequestHeader set aep-segments-membership "%{AEP_SEGMENTS_MEMBERSHIP}e" env=AEP_SEGMENTS_MEMBERSHIP
145 |
146 |
147 |
148 | SSLProxyEngine on
149 |
150 | ProxyRemote ${AEM_COMMERCE_ENDPOINT_2} "http://${AEM_HTTP_PROXY_HOST}:${AEM_HTTP_PROXY_PORT}"
151 |
152 |
153 | ProxyPassMatch ${AEM_COMMERCE_ENDPOINT_2}$2
154 | ProxyPassReverse ${AEM_COMMERCE_ENDPOINT_2}
155 | RewriteEngine Off
156 | SetEnvIfNoCase Cookie "(^| )aep-segments-membership=([^;]*)" AEP_SEGMENTS_MEMBERSHIP=$2
157 | RequestHeader set aep-segments-membership "%{AEP_SEGMENTS_MEMBERSHIP}e" env=AEP_SEGMENTS_MEMBERSHIP
158 |
159 |
160 |
161 | SSLProxyEngine on
162 |
163 | ProxyRemote ${AEM_COMMERCE_ENDPOINT_3} "http://${AEM_HTTP_PROXY_HOST}:${AEM_HTTP_PROXY_PORT}"
164 |
165 |
166 | ProxyPassMatch ${AEM_COMMERCE_ENDPOINT_3}$2
167 | ProxyPassReverse ${AEM_COMMERCE_ENDPOINT_3}
168 | RewriteEngine Off
169 | SetEnvIfNoCase Cookie "(^| )aep-segments-membership=([^;]*)" AEP_SEGMENTS_MEMBERSHIP=$2
170 | RequestHeader set aep-segments-membership "%{AEP_SEGMENTS_MEMBERSHIP}e" env=AEP_SEGMENTS_MEMBERSHIP
171 |
172 |
173 |
174 | SSLProxyEngine on
175 |
176 | ProxyRemote ${AEM_COMMERCE_ENDPOINT_4} "http://${AEM_HTTP_PROXY_HOST}:${AEM_HTTP_PROXY_PORT}"
177 |
178 |
179 | ProxyPassMatch ${AEM_COMMERCE_ENDPOINT_4}$2
180 | ProxyPassReverse ${AEM_COMMERCE_ENDPOINT_4}
181 | RewriteEngine Off
182 | SetEnvIfNoCase Cookie "(^| )aep-segments-membership=([^;]*)" AEP_SEGMENTS_MEMBERSHIP=$2
183 | RequestHeader set aep-segments-membership "%{AEP_SEGMENTS_MEMBERSHIP}e" env=AEP_SEGMENTS_MEMBERSHIP
184 |
185 |
186 |
187 | SSLProxyEngine on
188 |
189 | ProxyRemote ${AEM_COMMERCE_ENDPOINT_5} "http://${AEM_HTTP_PROXY_HOST}:${AEM_HTTP_PROXY_PORT}"
190 |
191 |
192 | ProxyPassMatch ${AEM_COMMERCE_ENDPOINT_5}$2
193 | ProxyPassReverse ${AEM_COMMERCE_ENDPOINT_5}
194 | RewriteEngine Off
195 | SetEnvIfNoCase Cookie "(^| )aep-segments-membership=([^;]*)" AEP_SEGMENTS_MEMBERSHIP=$2
196 | RequestHeader set aep-segments-membership "%{AEP_SEGMENTS_MEMBERSHIP}e" env=AEP_SEGMENTS_MEMBERSHIP
197 |
198 |
199 |
200 | # ASSETS-10359 Prevent rewrites and filtering of Delivery API URLs
201 |
202 | ProxyPassMatch http://${AEM_HOST}:${AEM_PORT}
203 | RewriteEngine Off
204 |
205 |
206 | # Disable access to default CGI scripts
207 |
208 | AllowOverride None
209 | Options None
210 | Require all denied
211 |
212 |
213 | # internal metadata endpoint
214 | Alias "/gitinit-status" metadata/gitinit-status.json
215 |
216 |
217 | RewriteEngine Off
218 |
219 |
220 |
221 | SetHandler default-handler
222 | AllowOverride None
223 | Require expr "%{HTTP_HOST} == '${POD_NAME}'"
224 |
225 |
226 | # Dedicated vhost for EaaS:
227 | # (currently disabled, but customers can expect it to be enabled in future versions - CQ-4349728)
228 | #
229 | # ServerName "test.eaas"
230 | # # possibility to make overrides before directives in this vhost
231 | # IncludeOptional conf.d/includes/first-listed-vhost.pre.includes
232 | # # since this vhost is first-listed one, this setting influences other vhosts - see https://httpd.apache.org/docs/2.4/mod/core.html#limitrequestfieldsize
233 | # LimitRequestFieldSize 32768
234 | # DocumentRoot /var/www/localhost/htdocs
235 | # AllowEncodedSlashes NoDecode
236 | #
237 | # Header add X-Vhost "test.eaas"
238 | #
239 | #
240 | # Options Indexes FollowSymLinks
241 | # AllowOverride None
242 | # Require all granted
243 | #
244 | #
245 | # # SKYOPS-49434: Allow EaaS to access publish instance directly for dev and stage environments when test.eaas vhost is requested
246 | #
247 | #
248 | # ProxyPassMatch http://${AEM_HOST}:${AEM_PORT}
249 | # RewriteEngine Off
250 | #
251 | #
252 | #
253 | #
254 | # ProxyPassMatch http://${AEM_HOST}:${AEM_PORT}
255 | # RewriteEngine Off
256 | #
257 | #
258 | # # 403 Forbidden on prod
259 | #
260 | #
261 | # RewriteEngine on
262 | # RewriteRule ^ - [F]
263 | #
264 | #
265 | # # possibility to make overrides after directives in this vhost
266 | # IncludeOptional conf.d/includes/first-listed-vhost.post.includes
267 | #
268 |
269 | # Customer's vhosts:
270 | Include conf.d/enabled_vhosts/*.vhost
271 |
272 | # Create a catch-all vhost
273 | # A catch-all is a safe place for un-matched hostnames to land.
274 | # This prevents someone pointing an-unwanted DNS record at your site and loading your pages.
275 | # Example: yoursitesucks.com (CNAME) -> yourelbaddressQKAWZM8H-208090978.us-east-1.elb.amazonaws.com
276 | # This host will accept any hostname and with a rewrite rule load the same index page giving away no details as to what they are hitting
277 | # That way bots and hackers won't know what purpose a random IP listening on webports is really doing.
278 | # Hitting the catch all doesn't let them know the customer is ExampleCo.com etc..
279 |
280 | ServerName unmatched-host-catch-all
281 | ServerAlias "*"
282 | # Azure traffic manager will hit here so lets have a custom log for that
283 | SetEnvIf User-agent .*Azure\sTraffic\sManager.* trafficmanager
284 | CustomLog logs/healthcheck_access_log combined env=trafficmanager
285 | CustomLog logs/httpd_access.log combined env=!trafficmanager
286 |
287 | # Specify where the catch all html files live
288 | DocumentRoot /var/www/localhost/htdocs
289 | # Add some visible targets AKA breadcrumbs that you can see in your browser dev tools or curl -I command
290 |
291 | Options Indexes FollowSymLinks
292 | AllowOverride None
293 | Require all granted
294 |
295 |
296 | Header always add X-Vhost catch-all
297 |
298 |
299 | RewriteEngine on
300 | RewriteRule ^/* /index.html [PT,L,NC]
301 |
302 |
303 |
304 | # We want to make sure the apache versions are hidden so avoid possible attack vectors
305 | ServerSignature Off
306 | ServerTokens Prod
307 |
--------------------------------------------------------------------------------