├── 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 | <views jcr:primaryType="nt:unstructured"> 31 | 32 | <list 33 | jcr:primaryType="nt:unstructured" 34 | layoutId="list" 35 | sling:resourceType="granite/ui/components/coral/foundation/table" 36 | limit="50" 37 | src="/apps/ibm/aem-tenant-specific-vanity-urls/tools/report/page/jcr:content/views/list{.offset,limit}.html{?sortName,sortDir}" 38 | path="${requestPathInfo.suffix}" 39 | sortMode="remote" 40 | stateId="shell.collectionpage" 41 | modeGroup="atsvu-report-entries" 42 | granite:rel="atsvu-report-entries" 43 | > 44 | <columns jcr:primaryType="nt:unstructured"> 45 | <vanityUrl 46 | jcr:primaryType="nt:unstructured" 47 | jcr:title="Vanity URL" 48 | name="vanityUrl" 49 | sortable="{Boolean}true" 50 | sortType="alphanumeric" 51 | /> 52 | <mappedPath 53 | jcr:primaryType="nt:unstructured" 54 | jcr:title="Path" 55 | name="path" 56 | sortable="{Boolean}true" 57 | sortType="alphanumeric" 58 | /> 59 | <link 60 | jcr:primaryType="nt:unstructured" 61 | jcr:title="Link" 62 | /> 63 | </columns> 64 | <datasource 65 | jcr:primaryType="nt:unstructured" 66 | path="${requestPathInfo.suffix}" 67 | sling:resourceType="ibm/aem-tenant-specific-vanity-urls/tools/report/datasource" 68 | itemResourceType="ibm/aem-tenant-specific-vanity-urls/tools/report/dataitem" 69 | limit="${empty requestPathInfo.selectors[1] ? "50" : requestPathInfo.selectors[1]}" 70 | offset="${requestPathInfo.selectors[0]}" 71 | /> 72 | </list> 73 | 74 | </views> 75 | </jcr:content> 76 | </jcr:root> 77 | -------------------------------------------------------------------------------- /ui.apps.structure/pom.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <!-- 3 | | Copyright 2015 Adobe Systems Incorporated 4 | | 2023 IBM iX 5 | | 6 | | Licensed under the Apache License, Version 2.0 (the "License"); 7 | | you may not use this file except in compliance with the License. 8 | | You may obtain a copy of the License at 9 | | 10 | | http://www.apache.org/licenses/LICENSE-2.0 11 | | 12 | | Unless required by applicable law or agreed to in writing, software 13 | | distributed under the License is distributed on an "AS IS" BASIS, 14 | | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | | See the License for the specific language governing permissions and 16 | | limitations under the License. 17 | --> 18 | <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 19 | <modelVersion>4.0.0</modelVersion> 20 | 21 | <!-- ====================================================================== --> 22 | <!-- P A R E N T P R O J E C T D E S C R I P T I O N --> 23 | <!-- ====================================================================== --> 24 | <parent> 25 | <groupId>com.ibm.aem.aem-tenant-specific-vanity-urls</groupId> 26 | <artifactId>aem-tenant-specific-vanity-urls</artifactId> 27 | <version>1.2.2-SNAPSHOT</version> 28 | <relativePath>../pom.xml</relativePath> 29 | </parent> 30 | 31 | <!-- ====================================================================== --> 32 | <!-- P R O J E C T D E S C R I P T I O N --> 33 | <!-- ====================================================================== --> 34 | <artifactId>aem-tenant-specific-vanity-urls.ui.apps.structure</artifactId> 35 | <packaging>content-package</packaging> 36 | <name>AEM Tenant Specific Vanity URLs - Repository Structure Package</name> 37 | <description> 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 | </description> 41 | 42 | <build> 43 | <plugins> 44 | <plugin> 45 | <groupId>org.apache.jackrabbit</groupId> 46 | <artifactId>filevault-package-maven-plugin</artifactId> 47 | <configuration> 48 | <properties> 49 | <cloudManagerTarget>none</cloudManagerTarget> 50 | </properties> 51 | <filters> 52 | <!-- /apps root --> 53 | <filter><root>/apps</root></filter> 54 | <filter><root>/apps/ibm/aem-tenant-specific-vanity-urls</root></filter> 55 | <filter><root>/apps/ibm/aem-tenant-specific-vanity-urls-examples</root></filter> 56 | 57 | <!-- Common overlay roots --> 58 | <filter><root>/apps/sling</root></filter> 59 | <filter><root>/apps/cq</root></filter> 60 | <filter><root>/apps/dam</root></filter> 61 | <filter><root>/apps/wcm</root></filter> 62 | <filter><root>/apps/msm</root></filter> 63 | 64 | <!-- Immutable context-aware configurations --> 65 | <filter><root>/apps/settings</root></filter> 66 | 67 | <!-- Navigation --> 68 | <filter><root>/apps/cq/core/content/nav/tools</root></filter> 69 | 70 | </filters> 71 | </configuration> 72 | </plugin> 73 | </plugins> 74 | </build> 75 | </project> 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 `<VirtualHosts>` 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/<CUSTOMER_CHOICE>.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 | <?xml version="1.0" encoding="UTF-8"?> 2 | <!-- 3 | | Copyright 2015 Adobe Systems Incorporated 4 | | 2023 IBM iX 5 | | 6 | | Licensed under the Apache License, Version 2.0 (the "License"); 7 | | you may not use this file except in compliance with the License. 8 | | You may obtain a copy of the License at 9 | | 10 | | http://www.apache.org/licenses/LICENSE-2.0 11 | | 12 | | Unless required by applicable law or agreed to in writing, software 13 | | distributed under the License is distributed on an "AS IS" BASIS, 14 | | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | | See the License for the specific language governing permissions and 16 | | limitations under the License. 17 | --> 18 | <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 19 | <modelVersion>4.0.0</modelVersion> 20 | 21 | <!-- ====================================================================== --> 22 | <!-- P A R E N T P R O J E C T D E S C R I P T I O N --> 23 | <!-- ====================================================================== --> 24 | <parent> 25 | <groupId>com.ibm.aem.aem-tenant-specific-vanity-urls</groupId> 26 | <artifactId>aem-tenant-specific-vanity-urls</artifactId> 27 | <version>1.2.2-SNAPSHOT</version> 28 | <relativePath>../pom.xml</relativePath> 29 | </parent> 30 | 31 | <!-- ====================================================================== --> 32 | <!-- P R O J E C T D E S C R I P T I O N --> 33 | <!-- ====================================================================== --> 34 | <artifactId>aem-tenant-specific-vanity-urls.examples.content</artifactId> 35 | <packaging>content-package</packaging> 36 | <name>AEM Tenant Specific Vanity URLs - Examples content</name> 37 | <description>Examples content package for AEM Tenant Specific Vanity URLs</description> 38 | 39 | <!-- ====================================================================== --> 40 | <!-- B U I L D D E F I N I T I O N --> 41 | <!-- ====================================================================== --> 42 | <build> 43 | <plugins> 44 | <!-- ====================================================================== --> 45 | <!-- V A U L T P A C K A G E P L U G I N S --> 46 | <!-- ====================================================================== --> 47 | <plugin> 48 | <groupId>org.apache.jackrabbit</groupId> 49 | <artifactId>filevault-package-maven-plugin</artifactId> 50 | <configuration> 51 | <properties> 52 | <cloudManagerTarget>none</cloudManagerTarget> 53 | </properties> 54 | <group>IBM</group> 55 | <name>aem-tenant-specific-vanity-urls.examples.content</name> 56 | <packageType>content</packageType> 57 | <validatorsSettings> 58 | <jackrabbit-filter> 59 | <options> 60 | <validRoots>/conf,/content,/content/experience-fragments,/content/dam</validRoots> 61 | </options> 62 | </jackrabbit-filter> 63 | </validatorsSettings> 64 | <dependencies> 65 | <dependency> 66 | <groupId>com.adobe.aem.guides</groupId> 67 | <artifactId>aem-guides-wknd-shared.ui.content</artifactId> 68 | <version>2.2.2</version> 69 | </dependency> 70 | </dependencies> 71 | </configuration> 72 | </plugin> 73 | <plugin> 74 | <groupId>com.day.jcr.vault</groupId> 75 | <artifactId>content-package-maven-plugin</artifactId> 76 | <extensions>true</extensions> 77 | <configuration> 78 | <verbose>true</verbose> 79 | <failOnError>true</failOnError> 80 | </configuration> 81 | </plugin> 82 | </plugins> 83 | </build> 84 | 85 | <!-- ====================================================================== --> 86 | <!-- D E P E N D E N C I E S --> 87 | <!-- ====================================================================== --> 88 | <dependencies> 89 | <dependency> 90 | <groupId>com.adobe.aem.guides</groupId> 91 | <artifactId>aem-guides-wknd-shared.ui.content</artifactId> 92 | <version>2.2.2</version> 93 | <type>zip</type> 94 | </dependency> 95 | </dependencies> 96 | </project> 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<ReportEntry> 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<DataSource> 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<Resource> 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 | ![IBM iX](docs/images/IBM_iX_logo.png) 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 | [![AEM Tenant Specific Vanity URLs @ adaptTo() 2023](https://img.youtube.com/vi/IvclYIoIEpk/0.jpg)](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 | <dependency> 29 | <groupId>com.ibm.aem.aem-tenant-specific-vanity-urls</groupId> 30 | <artifactId>aem-tenant-specific-vanity-urls.all</artifactId> 31 | <version>LATEST</version> 32 | <type>zip</type> 33 | </dependency> 34 | <dependency> 35 | <groupId>io.wcm</groupId> 36 | <artifactId>io.wcm.caconfig.editor.package</artifactId> 37 | <version>LATEST</version> 38 | <type>zip</type> 39 | </dependency> 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 | ![Context Aware Configuration](docs/images/caconfig1.png) 57 | ![Prefix Configuration](docs/images/caconfig2.png) 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 | ![Duplicate found](docs/images/duplicate.png) 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 | ![Vanity URL Report](docs/images/report.png) 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<Resource> iterator() { 97 | List<Resource> entries = new ArrayList<>(); 98 | try { 99 | List<ReportEntry> 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<Hit> hits = Arrays.asList(hit1, hit2); 116 | when(searchResult.getHits()).thenReturn(hits); 117 | 118 | List<ReportEntry> 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<ReportEntry> getVanityEntries(int offset, int limit, ORDER_ATTR orderAttr, ORDER order, ResourceResolver resolver) throws AtsvuException { 105 | Map<String, Object> 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<ReportEntry> entries = new ArrayList<>(); 127 | try { 128 | SearchResult result = query.getResult(); 129 | List<Hit> 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<any>} 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 | <?xml version="1.0" encoding="UTF-8"?> 2 | <!-- 3 | | Copyright 2015 Adobe Systems Incorporated 4 | | 2023 IBM iX 5 | | 6 | | Licensed under the Apache License, Version 2.0 (the "License"); 7 | | you may not use this file except in compliance with the License. 8 | | You may obtain a copy of the License at 9 | | 10 | | http://www.apache.org/licenses/LICENSE-2.0 11 | | 12 | | Unless required by applicable law or agreed to in writing, software 13 | | distributed under the License is distributed on an "AS IS" BASIS, 14 | | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | | See the License for the specific language governing permissions and 16 | | limitations under the License. 17 | --> 18 | <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 19 | <modelVersion>4.0.0</modelVersion> 20 | <parent> 21 | <groupId>com.ibm.aem.aem-tenant-specific-vanity-urls</groupId> 22 | <artifactId>aem-tenant-specific-vanity-urls</artifactId> 23 | <version>1.2.2-SNAPSHOT</version> 24 | <relativePath>../pom.xml</relativePath> 25 | </parent> 26 | <artifactId>aem-tenant-specific-vanity-urls.core</artifactId> 27 | <name>AEM Tenant Specific Vanity URLs - Core</name> 28 | <description>Core bundle for AEM Tenant Specific Vanity URLs</description> 29 | <build> 30 | <plugins> 31 | <plugin> 32 | <groupId>org.apache.sling</groupId> 33 | <artifactId>sling-maven-plugin</artifactId> 34 | </plugin> 35 | <plugin> 36 | <groupId>org.apache.felix</groupId> 37 | <artifactId>maven-bundle-plugin</artifactId> 38 | </plugin> 39 | <plugin> 40 | <groupId>biz.aQute.bnd</groupId> 41 | <artifactId>bnd-baseline-maven-plugin</artifactId> 42 | <configuration> 43 | <failOnMissing>false</failOnMissing> 44 | </configuration> 45 | <executions> 46 | <execution> 47 | <id>baseline</id> 48 | <goals> 49 | <goal>baseline</goal> 50 | </goals> 51 | </execution> 52 | </executions> 53 | </plugin> 54 | <plugin> 55 | <groupId>org.apache.maven.plugins</groupId> 56 | <artifactId>maven-surefire-plugin</artifactId> 57 | </plugin> 58 | <plugin> 59 | <groupId>org.apache.maven.plugins</groupId> 60 | <artifactId>maven-jar-plugin</artifactId> 61 | <configuration> 62 | <archive> 63 | <manifestFile>${project.build.outputDirectory}/META-INF/MANIFEST.MF</manifestFile> 64 | </archive> 65 | </configuration> 66 | </plugin> 67 | <plugin> 68 | <groupId>org.jacoco</groupId> 69 | <artifactId>jacoco-maven-plugin</artifactId> 70 | <executions> 71 | <execution> 72 | <id>prepare-agent</id> 73 | <goals> 74 | <goal>prepare-agent</goal> 75 | </goals> 76 | </execution> 77 | <execution> 78 | <id>report</id> 79 | <phase>prepare-package</phase> 80 | <goals> 81 | <goal>report</goal> 82 | </goals> 83 | </execution> 84 | <execution> 85 | <id>post-unit-test</id> 86 | <phase>test</phase> 87 | <goals> 88 | <goal>report</goal> 89 | </goals> 90 | </execution> 91 | </executions> 92 | </plugin> 93 | </plugins> 94 | </build> 95 | 96 | <dependencies> 97 | <dependency> 98 | <groupId>org.osgi</groupId> 99 | <artifactId>osgi.core</artifactId> 100 | <scope>provided</scope> 101 | </dependency> 102 | <dependency> 103 | <groupId>org.osgi</groupId> 104 | <artifactId>osgi.cmpn</artifactId> 105 | <scope>provided</scope> 106 | </dependency> 107 | <dependency> 108 | <groupId>org.osgi</groupId> 109 | <artifactId>osgi.annotation</artifactId> 110 | <scope>provided</scope> 111 | </dependency> 112 | 113 | <dependency> 114 | <groupId>uk.org.lidalia</groupId> 115 | <artifactId>slf4j-test</artifactId> 116 | <scope>test</scope> 117 | </dependency> 118 | <dependency> 119 | <groupId>com.adobe.aem</groupId> 120 | <artifactId>uber-jar</artifactId> 121 | </dependency> 122 | <dependency> 123 | <groupId>com.adobe.cq</groupId> 124 | <artifactId>core.wcm.components.core</artifactId> 125 | <scope>test</scope> 126 | </dependency> 127 | 128 | 129 | <!-- Testing --> 130 | <dependency> 131 | <groupId>org.junit.jupiter</groupId> 132 | <artifactId>junit-jupiter</artifactId> 133 | <scope>test</scope> 134 | </dependency> 135 | <dependency> 136 | <groupId>org.mockito</groupId> 137 | <artifactId>mockito-core</artifactId> 138 | <scope>test</scope> 139 | </dependency> 140 | <dependency> 141 | <groupId>org.mockito</groupId> 142 | <artifactId>mockito-junit-jupiter</artifactId> 143 | <scope>test</scope> 144 | </dependency> 145 | <dependency> 146 | <groupId>junit-addons</groupId> 147 | <artifactId>junit-addons</artifactId> 148 | <scope>test</scope> 149 | </dependency> 150 | <dependency> 151 | <groupId>io.wcm</groupId> 152 | <artifactId>io.wcm.testing.aem-mock.junit5</artifactId> 153 | <scope>test</scope> 154 | </dependency> 155 | <dependency> 156 | <groupId>org.apache.sling</groupId> 157 | <artifactId>org.apache.sling.testing.caconfig-mock-plugin</artifactId> 158 | <scope>test</scope> 159 | </dependency> 160 | <dependency> 161 | <groupId>com.adobe.cq</groupId> 162 | <artifactId>core.wcm.components.testing.aem-mock-plugin</artifactId> 163 | <scope>test</scope> 164 | </dependency> 165 | <!-- Required to be able to support injection with @Self and @Via --> 166 | <dependency> 167 | <groupId>org.apache.sling</groupId> 168 | <artifactId>org.apache.sling.models.impl</artifactId> 169 | <version>1.4.4</version> 170 | <scope>test</scope> 171 | </dependency> 172 | <dependency> 173 | <groupId>com.google.code.gson</groupId> 174 | <artifactId>gson</artifactId> 175 | <scope>test</scope> 176 | </dependency> 177 | </dependencies> 178 | </project> 179 | -------------------------------------------------------------------------------- /ui.apps/pom.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <!-- 3 | | Copyright 2015 Adobe Systems Incorporated 4 | | 2023 IBM iX 5 | | 6 | | Licensed under the Apache License, Version 2.0 (the "License"); 7 | | you may not use this file except in compliance with the License. 8 | | You may obtain a copy of the License at 9 | | 10 | | http://www.apache.org/licenses/LICENSE-2.0 11 | | 12 | | Unless required by applicable law or agreed to in writing, software 13 | | distributed under the License is distributed on an "AS IS" BASIS, 14 | | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | | See the License for the specific language governing permissions and 16 | | limitations under the License. 17 | --> 18 | <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 19 | <modelVersion>4.0.0</modelVersion> 20 | 21 | <!-- ====================================================================== --> 22 | <!-- P A R E N T P R O J E C T D E S C R I P T I O N --> 23 | <!-- ====================================================================== --> 24 | <parent> 25 | <groupId>com.ibm.aem.aem-tenant-specific-vanity-urls</groupId> 26 | <artifactId>aem-tenant-specific-vanity-urls</artifactId> 27 | <version>1.2.2-SNAPSHOT</version> 28 | <relativePath>../pom.xml</relativePath> 29 | </parent> 30 | 31 | <!-- ====================================================================== --> 32 | <!-- P R O J E C T D E S C R I P T I O N --> 33 | <!-- ====================================================================== --> 34 | <artifactId>aem-tenant-specific-vanity-urls.ui.apps</artifactId> 35 | <packaging>content-package</packaging> 36 | <name>AEM Tenant Specific Vanity URLs - UI apps</name> 37 | <description>UI apps package for AEM Tenant Specific Vanity URLs</description> 38 | 39 | <!-- ====================================================================== --> 40 | <!-- B U I L D D E F I N I T I O N --> 41 | <!-- ====================================================================== --> 42 | <build> 43 | <sourceDirectory>src/main/content/jcr_root</sourceDirectory> 44 | <plugins> 45 | <!-- ====================================================================== --> 46 | <!-- V A U L T P A C K A G E P L U G I N S --> 47 | <!-- ====================================================================== --> 48 | <plugin> 49 | <groupId>org.apache.jackrabbit</groupId> 50 | <artifactId>filevault-package-maven-plugin</artifactId> 51 | <configuration> 52 | <properties> 53 | <cloudManagerTarget>none</cloudManagerTarget> 54 | </properties> 55 | <group>IBM</group> 56 | <name>aem-tenant-specific-vanity-urls.ui.apps</name> 57 | <packageType>application</packageType> 58 | <repositoryStructurePackages> 59 | <repositoryStructurePackage> 60 | <groupId>com.ibm.aem.aem-tenant-specific-vanity-urls</groupId> 61 | <artifactId>aem-tenant-specific-vanity-urls.ui.apps.structure</artifactId> 62 | </repositoryStructurePackage> 63 | </repositoryStructurePackages> 64 | <dependencies> 65 | <dependency> 66 | <groupId>io.wcm</groupId> 67 | <artifactId>io.wcm.caconfig.editor.package</artifactId> 68 | <version>1.16.6</version> 69 | </dependency> 70 | </dependencies> 71 | </configuration> 72 | </plugin> 73 | <plugin> 74 | <groupId>com.day.jcr.vault</groupId> 75 | <artifactId>content-package-maven-plugin</artifactId> 76 | <extensions>true</extensions> 77 | <configuration> 78 | <verbose>true</verbose> 79 | <failOnError>true</failOnError> 80 | </configuration> 81 | </plugin> 82 | 83 | <plugin> 84 | <groupId>org.apache.sling</groupId> 85 | <artifactId>htl-maven-plugin</artifactId> 86 | <executions> 87 | <execution> 88 | <id>validate-htl-scripts</id> 89 | <goals> 90 | <goal>validate</goal> 91 | </goals> 92 | <phase>generate-sources</phase> 93 | <configuration> 94 | <generateJavaClasses>true</generateJavaClasses> 95 | <generatedJavaClassesPrefix>org.apache.sling.scripting.sightly</generatedJavaClassesPrefix> 96 | <sourceDirectory>${project.build.sourceDirectory}</sourceDirectory> 97 | <allowedExpressionOptions> 98 | <allowedExpressionOption>cssClassName</allowedExpressionOption> 99 | <allowedExpressionOption>decoration</allowedExpressionOption> 100 | <allowedExpressionOption>decorationTagName</allowedExpressionOption> 101 | <allowedExpressionOption>wcmmode</allowedExpressionOption> 102 | </allowedExpressionOptions> 103 | </configuration> 104 | </execution> 105 | </executions> 106 | </plugin> 107 | </plugins> 108 | </build> 109 | 110 | 111 | <!-- ====================================================================== --> 112 | <!-- D E P E N D E N C I E S --> 113 | <!-- ====================================================================== --> 114 | <dependencies> 115 | <dependency> 116 | <groupId>com.ibm.aem.aem-tenant-specific-vanity-urls</groupId> 117 | <artifactId>aem-tenant-specific-vanity-urls.core</artifactId> 118 | <version>${project.version}</version> 119 | </dependency> 120 | 121 | <dependency> 122 | <groupId>com.ibm.aem.aem-tenant-specific-vanity-urls</groupId> 123 | <artifactId>aem-tenant-specific-vanity-urls.ui.apps.structure</artifactId> 124 | <version>${project.version}</version> 125 | <type>zip</type> 126 | </dependency> 127 | 128 | <!-- HTL dependencies needed for the HTL Maven Plugin source code generation --> 129 | <dependency> 130 | <groupId>org.apache.sling</groupId> 131 | <artifactId>org.apache.sling.scripting.sightly.runtime</artifactId> 132 | <version>1.2.6-1.4.0</version> 133 | <scope>provided</scope> 134 | </dependency> 135 | 136 | <dependency> 137 | <groupId>io.wcm</groupId> 138 | <artifactId>io.wcm.caconfig.editor.package</artifactId> 139 | <version>1.16.6</version> 140 | <type>zip</type> 141 | </dependency> 142 | 143 | </dependencies> 144 | </project> 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 | * <p>Examples:</p> 129 | * <pre> 130 | * prependPrefixIfMissing("wow", "/us/en/") = "/us/en/wow" 131 | * </pre> 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 `<a href="${href}" target="_blank">${conflict.title}</a>`; 196 | } 197 | } 198 | 199 | window.tsvu_prefix.init(); 200 | -------------------------------------------------------------------------------- /examples/pom.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <!-- 3 | | Copyright 2015 Adobe Systems Incorporated 4 | | 2023 IBM iX 5 | | 6 | | Licensed under the Apache License, Version 2.0 (the "License"); 7 | | you may not use this file except in compliance with the License. 8 | | You may obtain a copy of the License at 9 | | 10 | | http://www.apache.org/licenses/LICENSE-2.0 11 | | 12 | | Unless required by applicable law or agreed to in writing, software 13 | | distributed under the License is distributed on an "AS IS" BASIS, 14 | | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | | See the License for the specific language governing permissions and 16 | | limitations under the License. 17 | --> 18 | <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 19 | <modelVersion>4.0.0</modelVersion> 20 | 21 | <!-- ====================================================================== --> 22 | <!-- P A R E N T P R O J E C T D E S C R I P T I O N --> 23 | <!-- ====================================================================== --> 24 | <parent> 25 | <groupId>com.ibm.aem.aem-tenant-specific-vanity-urls</groupId> 26 | <artifactId>aem-tenant-specific-vanity-urls</artifactId> 27 | <version>1.2.2-SNAPSHOT</version> 28 | <relativePath>../pom.xml</relativePath> 29 | </parent> 30 | 31 | <!-- ====================================================================== --> 32 | <!-- P R O J E C T D E S C R I P T I O N --> 33 | <!-- ====================================================================== --> 34 | <artifactId>aem-tenant-specific-vanity-urls.examples</artifactId> 35 | <packaging>content-package</packaging> 36 | <name>AEM Tenant Specific Vanity URLs - Examples</name> 37 | <description>Example content package for AEM Tenant Specific Vanity URLs</description> 38 | 39 | <!-- ====================================================================== --> 40 | <!-- B U I L D D E F I N I T I O N --> 41 | <!-- ====================================================================== --> 42 | <build> 43 | <plugins> 44 | <!-- ====================================================================== --> 45 | <!-- V A U L T P A C K A G E P L U G I N S --> 46 | <!-- ====================================================================== --> 47 | <plugin> 48 | <groupId>org.apache.jackrabbit</groupId> 49 | <artifactId>filevault-package-maven-plugin</artifactId> 50 | <extensions>true</extensions> 51 | <configuration> 52 | <group>IBM</group> 53 | <packageType>container</packageType> 54 | <!-- skip sub package validation for now as some vendor packages like CIF apps will not pass --> 55 | <skipSubPackageValidation>true</skipSubPackageValidation> 56 | <embeddeds> 57 | <embedded> 58 | <groupId>com.ibm.aem.aem-tenant-specific-vanity-urls</groupId> 59 | <artifactId>aem-tenant-specific-vanity-urls.examples.content</artifactId> 60 | <type>zip</type> 61 | <target>/apps/ibm/aem-tenant-specific-vanity-urls-examples/install</target> 62 | </embedded> 63 | </embeddeds> 64 | </configuration> 65 | </plugin> 66 | <plugin> 67 | <groupId>com.day.jcr.vault</groupId> 68 | <artifactId>content-package-maven-plugin</artifactId> 69 | <extensions>true</extensions> 70 | <configuration> 71 | <verbose>true</verbose> 72 | <failOnError>true</failOnError> 73 | </configuration> 74 | </plugin> 75 | <plugin> 76 | <artifactId>maven-clean-plugin</artifactId> 77 | <executions> 78 | <execution> 79 | <id>auto-clean</id> 80 | <phase>initialize</phase> 81 | <goals> 82 | <goal>clean</goal> 83 | </goals> 84 | </execution> 85 | </executions> 86 | </plugin> 87 | <plugin> 88 | <groupId>com.adobe.aem</groupId> 89 | <artifactId>aemanalyser-maven-plugin</artifactId> 90 | <executions> 91 | <execution> 92 | <id>aem-analyser</id> 93 | <goals> 94 | <goal>project-analyse</goal> 95 | </goals> 96 | </execution> 97 | </executions> 98 | </plugin> 99 | </plugins> 100 | </build> 101 | 102 | <!-- ====================================================================== --> 103 | <!-- P R O F I L E S --> 104 | <!-- ====================================================================== --> 105 | <profiles> 106 | <profile> 107 | <id>autoInstallSinglePackage</id> 108 | <activation> 109 | <activeByDefault>false</activeByDefault> 110 | </activation> 111 | <build> 112 | <plugins> 113 | <plugin> 114 | <groupId>com.day.jcr.vault</groupId> 115 | <artifactId>content-package-maven-plugin</artifactId> 116 | <executions> 117 | <execution> 118 | <id>install-package</id> 119 | <goals> 120 | <goal>install</goal> 121 | </goals> 122 | <configuration> 123 | <targetURL>http://${aem.host}:${aem.port}/crx/packmgr/service.jsp</targetURL> 124 | <failOnError>true</failOnError> 125 | </configuration> 126 | </execution> 127 | </executions> 128 | </plugin> 129 | </plugins> 130 | </build> 131 | </profile> 132 | <profile> 133 | <id>autoInstallSinglePackagePublish</id> 134 | <activation> 135 | <activeByDefault>false</activeByDefault> 136 | </activation> 137 | <build> 138 | <plugins> 139 | <plugin> 140 | <groupId>com.day.jcr.vault</groupId> 141 | <artifactId>content-package-maven-plugin</artifactId> 142 | <executions> 143 | <execution> 144 | <id>install-package-publish</id> 145 | <goals> 146 | <goal>install</goal> 147 | </goals> 148 | <configuration> 149 | <targetURL>http://${aem.publish.host}:${aem.publish.port}/crx/packmgr/service.jsp</targetURL> 150 | <failOnError>true</failOnError> 151 | </configuration> 152 | </execution> 153 | </executions> 154 | </plugin> 155 | </plugins> 156 | </build> 157 | </profile> 158 | </profiles> 159 | 160 | <!-- ====================================================================== --> 161 | <!-- D E P E N D E N C I E S --> 162 | <!-- ====================================================================== --> 163 | <dependencies> 164 | <dependency> 165 | <groupId>com.ibm.aem.aem-tenant-specific-vanity-urls</groupId> 166 | <artifactId>aem-tenant-specific-vanity-urls.examples.content</artifactId> 167 | <version>${project.version}</version> 168 | <type>zip</type> 169 | </dependency> 170 | 171 | </dependencies> 172 | </project> 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 | <?xml version="1.0" encoding="UTF-8"?> 2 | <!-- 3 | | Copyright 2015 Adobe Systems Incorporated 4 | | 2023 IBM iX 5 | | 6 | | Licensed under the Apache License, Version 2.0 (the "License"); 7 | | you may not use this file except in compliance with the License. 8 | | You may obtain a copy of the License at 9 | | 10 | | http://www.apache.org/licenses/LICENSE-2.0 11 | | 12 | | Unless required by applicable law or agreed to in writing, software 13 | | distributed under the License is distributed on an "AS IS" BASIS, 14 | | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | | See the License for the specific language governing permissions and 16 | | limitations under the License. 17 | --> 18 | <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 19 | <modelVersion>4.0.0</modelVersion> 20 | 21 | <!-- ====================================================================== --> 22 | <!-- P A R E N T P R O J E C T D E S C R I P T I O N --> 23 | <!-- ====================================================================== --> 24 | <parent> 25 | <groupId>com.ibm.aem.aem-tenant-specific-vanity-urls</groupId> 26 | <artifactId>aem-tenant-specific-vanity-urls</artifactId> 27 | <version>1.2.2-SNAPSHOT</version> 28 | <relativePath>../pom.xml</relativePath> 29 | </parent> 30 | 31 | <!-- ====================================================================== --> 32 | <!-- P R O J E C T D E S C R I P T I O N --> 33 | <!-- ====================================================================== --> 34 | <artifactId>aem-tenant-specific-vanity-urls.all</artifactId> 35 | <packaging>content-package</packaging> 36 | <name>AEM Tenant Specific Vanity URLs - All</name> 37 | <description>All content package for AEM Tenant Specific Vanity URLs</description> 38 | 39 | <!-- ====================================================================== --> 40 | <!-- B U I L D D E F I N I T I O N --> 41 | <!-- ====================================================================== --> 42 | <build> 43 | <plugins> 44 | <!-- ====================================================================== --> 45 | <!-- V A U L T P A C K A G E P L U G I N S --> 46 | <!-- ====================================================================== --> 47 | <plugin> 48 | <groupId>org.apache.jackrabbit</groupId> 49 | <artifactId>filevault-package-maven-plugin</artifactId> 50 | <extensions>true</extensions> 51 | <configuration> 52 | <group>IBM</group> 53 | <packageType>container</packageType> 54 | <!-- skip sub package validation for now as some vendor packages like CIF apps will not pass --> 55 | <skipSubPackageValidation>true</skipSubPackageValidation> 56 | <embeddeds> 57 | <embedded> 58 | <groupId>com.ibm.aem.aem-tenant-specific-vanity-urls</groupId> 59 | <artifactId>aem-tenant-specific-vanity-urls.ui.apps</artifactId> 60 | <type>zip</type> 61 | <target>/apps/ibm/aem-tenant-specific-vanity-urls/install.author</target> 62 | </embedded> 63 | <embedded> 64 | <groupId>com.ibm.aem.aem-tenant-specific-vanity-urls</groupId> 65 | <artifactId>aem-tenant-specific-vanity-urls.core</artifactId> 66 | <target>/apps/ibm/aem-tenant-specific-vanity-urls/install.author</target> 67 | </embedded> 68 | </embeddeds> 69 | </configuration> 70 | </plugin> 71 | <plugin> 72 | <groupId>com.day.jcr.vault</groupId> 73 | <artifactId>content-package-maven-plugin</artifactId> 74 | <extensions>true</extensions> 75 | <configuration> 76 | <verbose>true</verbose> 77 | <failOnError>true</failOnError> 78 | </configuration> 79 | </plugin> 80 | <plugin> 81 | <artifactId>maven-clean-plugin</artifactId> 82 | <executions> 83 | <execution> 84 | <id>auto-clean</id> 85 | <phase>initialize</phase> 86 | <goals> 87 | <goal>clean</goal> 88 | </goals> 89 | </execution> 90 | </executions> 91 | </plugin> 92 | <plugin> 93 | <groupId>com.adobe.aem</groupId> 94 | <artifactId>aemanalyser-maven-plugin</artifactId> 95 | <executions> 96 | <execution> 97 | <id>aem-analyser</id> 98 | <goals> 99 | <goal>project-analyse</goal> 100 | </goals> 101 | </execution> 102 | </executions> 103 | </plugin> 104 | </plugins> 105 | </build> 106 | 107 | <!-- ====================================================================== --> 108 | <!-- P R O F I L E S --> 109 | <!-- ====================================================================== --> 110 | <profiles> 111 | <profile> 112 | <id>autoInstallSinglePackage</id> 113 | <activation> 114 | <activeByDefault>false</activeByDefault> 115 | </activation> 116 | <build> 117 | <plugins> 118 | <plugin> 119 | <groupId>com.day.jcr.vault</groupId> 120 | <artifactId>content-package-maven-plugin</artifactId> 121 | <executions> 122 | <execution> 123 | <id>install-package</id> 124 | <goals> 125 | <goal>install</goal> 126 | </goals> 127 | <configuration> 128 | <targetURL>http://${aem.host}:${aem.port}/crx/packmgr/service.jsp</targetURL> 129 | <failOnError>true</failOnError> 130 | </configuration> 131 | </execution> 132 | </executions> 133 | </plugin> 134 | </plugins> 135 | </build> 136 | </profile> 137 | <profile> 138 | <id>autoInstallSinglePackagePublish</id> 139 | <activation> 140 | <activeByDefault>false</activeByDefault> 141 | </activation> 142 | <build> 143 | <plugins> 144 | <plugin> 145 | <groupId>com.day.jcr.vault</groupId> 146 | <artifactId>content-package-maven-plugin</artifactId> 147 | <executions> 148 | <execution> 149 | <id>install-package-publish</id> 150 | <goals> 151 | <goal>install</goal> 152 | </goals> 153 | <configuration> 154 | <targetURL>http://${aem.publish.host}:${aem.publish.port}/crx/packmgr/service.jsp</targetURL> 155 | <failOnError>true</failOnError> 156 | </configuration> 157 | </execution> 158 | </executions> 159 | </plugin> 160 | </plugins> 161 | </build> 162 | </profile> 163 | </profiles> 164 | 165 | <!-- ====================================================================== --> 166 | <!-- D E P E N D E N C I E S --> 167 | <!-- ====================================================================== --> 168 | <dependencies> 169 | <dependency> 170 | <groupId>com.ibm.aem.aem-tenant-specific-vanity-urls</groupId> 171 | <artifactId>aem-tenant-specific-vanity-urls.ui.apps</artifactId> 172 | <version>${project.version}</version> 173 | <type>zip</type> 174 | </dependency> 175 | </dependencies> 176 | </project> 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<String> allContextPaths = configurationResourceResolver.getAllContextPaths(request.getResource()); 131 | List<String> orderedContextPaths = new ArrayList<>(allContextPaths); 132 | orderedContextPaths.sort(null); 133 | String searchPath = orderedContextPaths.get(0); 134 | Iterator<Page> 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<Page> findPagesByVanityPath(String contentPath, String vanityPath, ResourceResolver resolver) { 164 | Map<String, String> 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<Page> filterCurrentPage(Iterator<Page> 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 | <LocationMatch "/system/probes"> 25 | RewriteEngine Off 26 | </LocationMatch> 27 | 28 | <Directory "/etc/httpd/probes"> 29 | SetHandler default-handler 30 | AllowOverride None 31 | Require all granted 32 | </Directory> 33 | 34 | 35 | #SKYOPS-13837: Proxy static frontend code requests through dispatcher 36 | <IfDefine FRONTEND_SUPPORT> 37 | SSLProxyEngine on 38 | <LocationMatch "/libs/cq/frontend-static"> 39 | RewriteRule "^/mnt/var/www/html/libs/cq/frontend-static(/[^\.].*)$" "%{env:FRONTEND_URI_PREFIX}$1?%{env:FRONTEND_URI_SUFFIX}" [P,L] 40 | </LocationMatch> 41 | </IfDefine> 42 | 43 | # CQ-4315090: Allow the functional replication to access publish instance directly for dev and stage environments 44 | <IfDefine ENVIRONMENT_DEV> 45 | <LocationMatch "/content/test-site/"> 46 | ProxyPassMatch http://${AEM_HOST}:${AEM_PORT} 47 | RewriteEngine Off 48 | </LocationMatch> 49 | </IfDefine> 50 | <IfDefine ENVIRONMENT_STAGE> 51 | <LocationMatch "/content/test-site/"> 52 | ProxyPassMatch http://${AEM_HOST}:${AEM_PORT} 53 | RewriteEngine Off 54 | </LocationMatch> 55 | </IfDefine> 56 | 57 | # If the module loads correctly then apply base settings for the module 58 | <IfModule disp_apache2.c> 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 | </IfModule> 91 | 92 | <IfDefine !DISABLE_DEFAULT_CACHING> 93 | <IfModule mod_expires.c> 94 | # Expire text/html after this many seconds 95 | ExpiresActive On 96 | ExpiresByType text/html A${EXPIRATION_TIME} 97 | </IfModule> 98 | Header unset Age 99 | </IfDefine> 100 | 101 | # SITES-11040 Do ProxyPassMatch, if caching for GraphQL Persisted Queries is not enabled 102 | <IfDefine !CACHE_GRAPHQL_PERSISTED_QUERIES> 103 | # SITES-3659 Prevent re-encodes of URLs sent to GraphQL Persisted Queries API endpoint 104 | <LocationMatch "/graphql/execute.json/.*"> 105 | ProxyPassMatch http://${AEM_HOST}:${AEM_PORT} nocanon 106 | </LocationMatch> 107 | </IfDefine> 108 | 109 | # Legacy /systemready mapped to new Health probe URL /system/probes/health in AEM 110 | <Location "/systemready"> 111 | ProxyPass http://${AEM_HOST}:${AEM_PORT}/system/probes/health 112 | RewriteEngine Off 113 | </Location> 114 | 115 | # Allow ingressroute checks through on /system/probes/health (regardless of dispatcher filters) 116 | <Location "/system/probes/health"> 117 | ProxyPass http://${AEM_HOST}:${AEM_PORT}/system/probes/health 118 | RewriteEngine Off 119 | </Location> 120 | 121 | # Allow access to CRXDE on dev environment 122 | <IfDefine ENVIRONMENT_DEV> 123 | <LocationMatch "/crx/(de|server)/"> 124 | ProxyPassMatch http://${AEM_HOST}:${AEM_PORT} 125 | RewriteEngine Off 126 | </LocationMatch> 127 | </IfDefine> 128 | 129 | # CQ-4287185: Allow access to magento reverse-proxy endpoint 130 | <IfDefine COMMERCE> 131 | SSLProxyEngine on 132 | # CIF-2557 add ProxyRemote to tunnel reverse-proxy traffic through egress proxy if available 133 | <IfDefine HTTP_EGRESS_PROXY> 134 | ProxyRemote ${COMMERCE_ENDPOINT} "http://${AEM_HTTP_PROXY_HOST}:${AEM_HTTP_PROXY_PORT}" 135 | </IfDefine> 136 | <LocationMatch "/api/graphql(/default)?$"> 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 | </LocationMatch> 146 | </IfDefine> 147 | <IfDefine COMMERCE_ENDPOINT_2> 148 | SSLProxyEngine on 149 | <IfDefine HTTP_EGRESS_PROXY> 150 | ProxyRemote ${AEM_COMMERCE_ENDPOINT_2} "http://${AEM_HTTP_PROXY_HOST}:${AEM_HTTP_PROXY_PORT}" 151 | </IfDefine> 152 | <LocationMatch "/api/graphql/endpoint-2$"> 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 | </LocationMatch> 159 | </IfDefine> 160 | <IfDefine COMMERCE_ENDPOINT_3> 161 | SSLProxyEngine on 162 | <IfDefine HTTP_EGRESS_PROXY> 163 | ProxyRemote ${AEM_COMMERCE_ENDPOINT_3} "http://${AEM_HTTP_PROXY_HOST}:${AEM_HTTP_PROXY_PORT}" 164 | </IfDefine> 165 | <LocationMatch "/api/graphql/endpoint-3$"> 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 | </LocationMatch> 172 | </IfDefine> 173 | <IfDefine COMMERCE_ENDPOINT_4> 174 | SSLProxyEngine on 175 | <IfDefine HTTP_EGRESS_PROXY> 176 | ProxyRemote ${AEM_COMMERCE_ENDPOINT_4} "http://${AEM_HTTP_PROXY_HOST}:${AEM_HTTP_PROXY_PORT}" 177 | </IfDefine> 178 | <LocationMatch "/api/graphql/endpoint-4$"> 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 | </LocationMatch> 185 | </IfDefine> 186 | <IfDefine COMMERCE_ENDPOINT_5> 187 | SSLProxyEngine on 188 | <IfDefine HTTP_EGRESS_PROXY> 189 | ProxyRemote ${AEM_COMMERCE_ENDPOINT_5} "http://${AEM_HTTP_PROXY_HOST}:${AEM_HTTP_PROXY_PORT}" 190 | </IfDefine> 191 | <LocationMatch "/api/graphql/endpoint-5$"> 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 | </LocationMatch> 198 | </IfDefine> 199 | 200 | # ASSETS-10359 Prevent rewrites and filtering of Delivery API URLs 201 | <LocationMatch "^/adobe/dynamicmedia/deliver/.*"> 202 | ProxyPassMatch http://${AEM_HOST}:${AEM_PORT} 203 | RewriteEngine Off 204 | </LocationMatch> 205 | 206 | # Disable access to default CGI scripts 207 | <Directory "/var/www/localhost/cgi-bin"> 208 | AllowOverride None 209 | Options None 210 | Require all denied 211 | </Directory> 212 | 213 | # internal metadata endpoint 214 | Alias "/gitinit-status" metadata/gitinit-status.json 215 | 216 | <LocationMatch "/gitinit-status"> 217 | RewriteEngine Off 218 | </LocationMatch> 219 | 220 | <Directory "/etc/httpd/metadata"> 221 | SetHandler default-handler 222 | AllowOverride None 223 | Require expr "%{HTTP_HOST} == '${POD_NAME}'" 224 | </Directory> 225 | 226 | # Dedicated vhost for EaaS: 227 | # (currently disabled, but customers can expect it to be enabled in future versions - CQ-4349728) 228 | #<VirtualHost *:80> 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 | # <IfModule mod_headers.c> 237 | # Header add X-Vhost "test.eaas" 238 | # </IfModule> 239 | # <Directory "/var/www/localhost/htdocs"> 240 | # Options Indexes FollowSymLinks 241 | # AllowOverride None 242 | # Require all granted 243 | # </Directory> 244 | # 245 | # # SKYOPS-49434: Allow EaaS to access publish instance directly for dev and stage environments when test.eaas vhost is requested 246 | # <IfDefine ENVIRONMENT_DEV> 247 | # <LocationMatch "/"> 248 | # ProxyPassMatch http://${AEM_HOST}:${AEM_PORT} 249 | # RewriteEngine Off 250 | # </LocationMatch> 251 | # </IfDefine> 252 | # <IfDefine ENVIRONMENT_STAGE> 253 | # <LocationMatch "/"> 254 | # ProxyPassMatch http://${AEM_HOST}:${AEM_PORT} 255 | # RewriteEngine Off 256 | # </LocationMatch> 257 | # </IfDefine> 258 | # # 403 Forbidden on prod 259 | # <IfDefine ENVIRONMENT_PROD> 260 | # <IfModule mod_rewrite.c> 261 | # RewriteEngine on 262 | # RewriteRule ^ - [F] 263 | # </IfModule> 264 | # </IfDefine> 265 | # # possibility to make overrides after directives in this vhost 266 | # IncludeOptional conf.d/includes/first-listed-vhost.post.includes 267 | #</VirtualHost> 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 | <VirtualHost *:80> 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 | <Directory "/var/www/localhost/htdocs"> 291 | Options Indexes FollowSymLinks 292 | AllowOverride None 293 | Require all granted 294 | </Directory> 295 | <IfModule mod_headers.c> 296 | Header always add X-Vhost catch-all 297 | </IfModule> 298 | <IfModule mod_rewrite.c> 299 | RewriteEngine on 300 | RewriteRule ^/* /index.html [PT,L,NC] 301 | </IfModule> 302 | </VirtualHost> 303 | 304 | # We want to make sure the apache versions are hidden so avoid possible attack vectors 305 | ServerSignature Off 306 | ServerTokens Prod 307 | --------------------------------------------------------------------------------