Skip to content

Commit

Permalink
Improved: Add uri shortener function (OFBIZ-13154) (#841)
Browse files Browse the repository at this point in the history
This improvement offer the possibility to use a shortener url to call ofbiz when it render a url.

The origin requirement is to send by email a url to contact OFBiz without any information on technical or functionnal context like JWToken, userLogin, orderId, partyId and so on. OFBiz forward only a short reference that match the actual uri wanted.

Example :
  * ecommerce/myaccount/order/ORD10034 -> s/tiozerzaze
  * ecommerce/myaccount?token=JWT[more..than..100]axdr&userLoginId=Me@ofbiz.org -> s/epsserlner

When a request arrive in OFBiz with the pattern s/{shortener}, the request handler forward to matched uri.

To generate a shortener on freemarker template just use it like it :

   <@ofbizUrl pathShortener="true">${MyBigUriToSecure}</@ofbizUrl>

For email template it's ugly recommand to use webSiteId and fullPath

   <@ofbizUrl webSiteId="MyWebSite" fullPath="true" pathShortener="true">${MyBigUriToSecure}</@ofbizUrl>

With this you can have a url like this : https://mywebsite.mydomain/s/rytedqzdfd

At this time only <@ofbizUrl macro freemarker support it.
  • Loading branch information
nmalin authored Nov 5, 2024
1 parent a851cf4 commit d0a5c8e
Show file tree
Hide file tree
Showing 12 changed files with 341 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ public Writer getWriter(final Writer out, @SuppressWarnings("rawtypes") Map args
final StringBuffer buf = new StringBuffer();
final boolean fullPath = checkArg(args, "fullPath", false);
final boolean secure = checkArg(args, "secure", false);
final boolean shortener = checkArg(args, "pathShortener", false);
final boolean encode = checkArg(args, "encode", true);
final String controlPath = convertToString(args.get("controlPath"));
final String webSiteId = convertToString(args.get("webSiteId"));
Expand Down Expand Up @@ -142,7 +143,7 @@ public void close() throws IOException {

RequestHandler rh = RequestHandler.from(request);
String seoUrl = seoUrl(rh.makeLink(request, response, buf.toString(), fullPath,
secure || request.isSecure(), encode, controlPath), userLogin == null);
secure || request.isSecure(), encode, controlPath, shortener), userLogin == null);
String requestURI = buf.toString();

// add / update csrf token to link when required
Expand All @@ -159,7 +160,7 @@ public void close() throws IOException {
ComponentConfig.WebappInfo webAppInfo = WebAppUtil.getWebappInfoFromWebsiteId(webSiteId);
StringBuilder newUrlBuff = new StringBuilder(250);
OfbizUrlBuilder builder = OfbizUrlBuilder.from(webAppInfo, delegator);
builder.buildFullUrl(newUrlBuff, buf.toString(), secure);
builder.buildFullUrl(newUrlBuff, buf.toString(), secure, shortener);
String newUrl = newUrlBuff.toString();
if (encode) {
newUrl = URLEncoder.encode(newUrl, "UTF-8");
Expand Down
4 changes: 4 additions & 0 deletions framework/common/webcommon/WEB-INF/common-controller.xml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ under the License.
<security https="true" auth="false"/>
<response name="success" type="request" value="main"/>
</request-map>
<request-map uri="s/{shortener}">
<security https="true" auth="false"/>
<response name="success" type="shortener" value="main"/>
</request-map>

<!-- Common Mappings used for locales and timezones -->
<request-map uri="ListLocales"><security https="true" auth="false"/><response name="success" type="view" value="ListLocales" save-last-view="true"/></request-map>
Expand Down
3 changes: 3 additions & 0 deletions framework/security/config/security.properties
Original file line number Diff line number Diff line change
Expand Up @@ -323,3 +323,6 @@ Content-Security-Policy=Content-Security-Policy-Report-Only

#-- Define policy directives, see https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
PolicyDirectives=default-src 'self'

#-- Give the size of shortener path when the functionality to shorter the url is used
path.shortener.size=10
10 changes: 9 additions & 1 deletion framework/webapp/dtd/site-conf.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -530,7 +530,8 @@ under the License.
request-redirect-noparam,
url,
url-redirect,
cross-redirect
cross-redirect,
shortener
</xs:documentation>
</xs:annotation>
<xs:simpleType>
Expand Down Expand Up @@ -625,6 +626,13 @@ under the License.
</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="shortener">
<xs:annotation>
<xs:documentation>
Call ofbiz shortener to resolve the next uri to call
</xs:documentation>
</xs:annotation>
</xs:enumeration>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
Expand Down
14 changes: 14 additions & 0 deletions framework/webapp/entitydef/entitymodel.xml
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,24 @@ under the License.
<!-- ========================================================= -->
<!-- ======================== Data Model ===================== -->
<!-- The modules in this file are as follows: -->
<!-- - org.apache.ofbiz.webapp.path -->
<!-- - org.apache.ofbiz.webapp.visit -->
<!-- - org.apache.ofbiz.webapp.website -->
<!-- ========================================================= -->

<entity entity-name="ShortenedPath"
package-name="org.apache.ofbiz.webapp.path"
title="Shortened Path">
<field name="shortenedPath" type="id"/>
<field name="originalPathHash" type="id-vlong"/>
<field name="originalPath" type="very-long"/>
<field name="createdDate" type="date-time"/>
<field name="createdByUserLogin" type="id-vlong"/>
<prim-key field="shortenedPath"/>
<index name="PATH_SHOR_HASH">
<index-field name="originalPathHash"/>
</index>
</entity>

<!-- ========================================================= -->
<!-- org.apache.ofbiz.webapp.visit -->
Expand Down
2 changes: 1 addition & 1 deletion framework/webapp/ofbiz-component.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,5 @@ under the License.
<entity-resource type="group" reader-name="main" loader="main" location="entitydef/entitygroup.xml"/>
-->

<!--<test-suite loader="main" location="testdef/webapptests.xml"/>-->
<test-suite loader="main" location="testdef/webapptests.xml"/>
</ofbiz-component>
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*******************************************************************************
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License") you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*******************************************************************************/
package org.apache.ofbiz.webapp.test

import org.apache.ofbiz.service.testtools.OFBizTestCase
import org.apache.ofbiz.webapp.OfbizPathShortener

class OfbizPathShortenerTests extends OFBizTestCase {

OfbizPathShortenerTests(String name) {
super(name)
}
void testComputeLongUrlToShortUrl() {
String longUri = "passwordChange?USERNAME=admin&TOKEN=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxML" +
"LL.eyJ1c2VyTG9naW5JZCI6Imx1Y2lsZS5wZWxsZXRpZXJAZWRsbi5vcmciLCJpc3MiOiJBcGFjaGVPRkJpeiIsImV4cCI6MTcyNTU" +
"0MjM0OSwiaWF0IjoxNzI1NTQwNTQLLL.Rycl_L-u4ZeWkx82pWWGu7gycfsHQxIxE8zu1nQ5oueGDBeOXALL-SJzMuvSARbpxCwF9A" +
"jl4rTxgoEYuRMoHg&JavaScriptEnabled=Y&Albert=Yoda"
assert OfbizPathShortener.resolveShortenedPath(this.getDelegator(), longUri).length() < 11
}
void testResolveLongUrlComputedFromShort() {
String longUri = "passwordChange?USERNAME=admin&TOKEN=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxML" +
"LL.eyJ1c2VyTG9naW5JZCI6Imx1Y2lsZS5wZWxsZXRpZXJAZWRsbi5vcmciLCJpc3MiOiJBcGFjaGVPRkJpeiIsImV4cCI6MTcyNTU" +
"0MjM0OSwiaWF0IjoxNzI1NTQwNTQLLL.Rycl_L-u4ZeWkx82pWWGu7gycfsHQxIxE8zu1nQ5oueGDBeOXALL-SJzMuvSARbpxCwF9A" +
"jl4rTxgoEYuRMoHg&JavaScriptEnabled=Y"
String shortUri = OfbizPathShortener.resolveShortenedPath(this.getDelegator(), longUri)
assert longUri == OfbizPathShortener.resolveOriginalPathFromShortened(this.getDelegator(), shortUri)
}
void testResolveLongUrlComputedFromShortAlreadyStored() {
String longUri = "passwordChange?USERNAME=admin&TOKEN=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxML" +
"LL.eyJ1c2VyTG9naW5JZCI6Imx1Y2lsZS5wZWxsZXRpZXJAZWRsbi5vcmciLCJpc3MiOiJBcGFjaGVPRkJpeiIsImV4cCI6MTcyNTU" +
"0MjM0OSwiaWF0IjoxNzI1NTQwNTQLLL.Rycl_L-u4ZeWkx82pWWGu7gycfsHQxIxE8zu1nQ5oueGDBeOXALL-SJzMuvSARbpxCwF9A" +
"jl4rTxgoEYuRMoHg&JavaScriptEnabled=Y&And=Again"
String shortUriFirst = OfbizPathShortener.resolveShortenedPath(this.getDelegator(),longUri)
String shortUriSecond = OfbizPathShortener.resolveShortenedPath(this.getDelegator(),longUri)
assert shortUriSecond == shortUriFirst
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/*******************************************************************************
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*******************************************************************************/
package org.apache.ofbiz.webapp;

import java.util.Map;
import javax.transaction.Transaction;
import org.apache.commons.lang.RandomStringUtils;
import org.apache.ofbiz.base.crypto.HashCrypt;
import org.apache.ofbiz.base.util.UtilDateTime;
import org.apache.ofbiz.base.util.UtilProperties;
import org.apache.ofbiz.entity.Delegator;
import org.apache.ofbiz.entity.GenericEntityException;
import org.apache.ofbiz.entity.GenericValue;
import org.apache.ofbiz.entity.transaction.GenericTransactionException;
import org.apache.ofbiz.entity.transaction.TransactionUtil;
import org.apache.ofbiz.entity.util.EntityQuery;

public class OfbizPathShortener {
public static final String SHORTENED_PATH = "s/";
public static final String RESTORE_PATH = "../";

/**
* For an ofbiz path, return a shortened url that will be linked to the given path
* example : orderview?orderId=HA1023 -> s/izapnreiis
* @param delegator
* @param path to shorten
* @return a shortened key corresponding to the path
* @throws GenericEntityException
*/
public static String shortenPath(Delegator delegator, String path) throws GenericEntityException {
return SHORTENED_PATH + resolveShortenedPath(delegator, path);
}

/**
* For the given path, check if a shortened path already exists otherwise generate a new one
* @param delegator
* @param path
* @return a shortened path corresponding to the given path
* @throws GenericEntityException
*/
public static String resolveShortenedPath(Delegator delegator, String path) throws GenericEntityException {
String shortenedPath = resolveExistingShortenedPath(delegator, path);
int nbLoop = 0;
if (shortenedPath == null) {
do {
shortenedPath = generate();
nbLoop++;
} while (!recordPathMapping(delegator, path, shortenedPath) || nbLoop > 10);
}
return shortenedPath;
}

/**
* Try to resolve the original path, if failed, return to the webapp root. Use views request for that define on common-controller
* @param delegator
* @param shortenedPath
* @return the origin path corresponding to the given shortened path, webapp root otherwise
* @throws GenericEntityException
*/
public static String restoreOriginalPath(Delegator delegator, String shortenedPath) throws GenericEntityException {
String originalPath = resolveOriginalPathFromShortened(delegator, shortenedPath);
return RESTORE_PATH + (originalPath != null ? originalPath : "views");
}

/**
* From a shortened path, resolve the origin path
* @param delegator
* @param shortenedPath path
* @return the original path corresponding to the shortened path
* @throws GenericEntityException
*/
public static String resolveOriginalPathFromShortened(Delegator delegator, String shortenedPath) throws GenericEntityException {
return readPathMapping(delegator, shortenedPath);
}

/**
* For a path, function tried to resolve if it already presents in database
* For performance issue the function use the hash to resolve it
* @param delegator
* @param path
* @return the shortened path if found, null otherwise
* @throws GenericEntityException
*/
private static String resolveExistingShortenedPath(Delegator delegator, String path) throws GenericEntityException {
GenericValue existingPath = EntityQuery.use(delegator)
.from("ShortenedPath")
.where("originalPathHash", generateHash(path))
.cache()
.queryFirst();
return existingPath != null ? existingPath.getString("shortenedPath") : null;
}

/**
* generate a random shortened path, the size can be set on property : security.
* @return shortened path
*/
private static String generate() {
int shortenerSize = UtilProperties.getPropertyAsInteger("security", "path.shortener.size", 10);
return RandomStringUtils.randomAlphabetic(shortenerSize);
}

/**
* Create the mapping between an origin map and the shortened path
* This will be executed on dedicate transaction to be sure to not rollback it after.
* @param delegator
* @param path
* @param shortenedPath
* @return true if it's create with success
*/
private static boolean recordPathMapping(Delegator delegator, String path, String shortenedPath) {
Transaction trans = null;
try {
try {
trans = TransactionUtil.suspend();
TransactionUtil.begin();
delegator.create("ShortenedPath", Map.of("shortenedPath", shortenedPath,
"originalPath", path,
"originalPathHash", generateHash(path),
"createdDate", UtilDateTime.nowTimestamp(),
"createdByUserLogin", "system"));
TransactionUtil.commit();
} catch (GenericEntityException e) {
TransactionUtil.rollback();
return false;
} finally {
TransactionUtil.resume(trans);
}
} catch (GenericTransactionException e) {
return false;
}
return true;
}

/**
* @param path
* @return a hash of the given path
*/
private static String generateHash(String path) {
return HashCrypt.digestHash("SHA", path.getBytes());
}

/**
* Find the origin path corresponding to the shorter in database
* @param delegator
* @param shortenedPath
* @return the original path corresponding to shortened path given, null if not found
* @throws GenericEntityException
*/
private static String readPathMapping(Delegator delegator, String shortenedPath) throws GenericEntityException {
GenericValue existingPath = EntityQuery.use(delegator)
.from("ShortenedPath")
.where("shortenedPath", shortenedPath)
.cache()
.queryOne();
return existingPath != null
? existingPath.getString("originalPath")
: null;
}

}
Loading

0 comments on commit d0a5c8e

Please sign in to comment.