initial
This commit is contained in:
@@ -0,0 +1,514 @@
|
||||
component
|
||||
output = false
|
||||
hint = "I provide a way to serialize complex ColdFusion data values as case-sensitive JavaScript Object Notation (JSON) strings."
|
||||
{
|
||||
//2024-06-27 modified by msyu: added server timezone
|
||||
|
||||
// I return the initialized component.
|
||||
public any function init() {
|
||||
|
||||
// Every key is added to the full key list - used for one-time key serialization.
|
||||
fullKeyList = {};
|
||||
|
||||
// Every key is added to the hint list so that we don't have to switch on the lists.
|
||||
fullHintList = {};
|
||||
|
||||
// These key lists determine special data serialization.
|
||||
stringKeyList = {};
|
||||
booleanKeyList = {};
|
||||
integerKeyList = {};
|
||||
floatKeyList = {};
|
||||
dateKeyList = {};
|
||||
|
||||
// These keys will NOT be used in serialization (ie. the key/value pairs will not be
|
||||
// added to the serialized output).
|
||||
blockedKeyList = {};
|
||||
|
||||
// Return the initialized component.
|
||||
return( this );
|
||||
|
||||
}
|
||||
|
||||
|
||||
// ---
|
||||
// PUBLIC METHODS.
|
||||
// ---
|
||||
|
||||
|
||||
// I define the given key without a type. This is here to provide key-casing without caring
|
||||
// about why type of data conversion takes place. Returns serializer.
|
||||
public any function asAny( required string key ) {
|
||||
|
||||
return( defineKey( fullKeyList, key, "any" ) );
|
||||
|
||||
}
|
||||
|
||||
|
||||
// I define the given key as a boolean. Returns serializer.
|
||||
public any function asBoolean( required string key ) {
|
||||
|
||||
return( defineKey( booleanKeyList, key, "boolean" ) );
|
||||
|
||||
}
|
||||
|
||||
|
||||
// I define the given key as a date. Returns serializer.
|
||||
public any function asDate( required string key ) {
|
||||
|
||||
return( defineKey( dateKeyList, key, "date" ) );
|
||||
|
||||
}
|
||||
|
||||
|
||||
// I define the given key as a float / decimal. Returns serializer.
|
||||
public any function asFloat( required string key ) {
|
||||
|
||||
return( defineKey( floatKeyList, key, "float" ) );
|
||||
|
||||
}
|
||||
|
||||
|
||||
// I define the given key as an integer. Returns serializer.
|
||||
public any function asInteger( required string key ) {
|
||||
|
||||
return( defineKey( integerKeyList, key, "integer" ) );
|
||||
|
||||
}
|
||||
|
||||
|
||||
// I define the given key as a string. Returns serializer.
|
||||
public any function asString( required string key ) {
|
||||
|
||||
return( defineKey( stringKeyList, key, "string" ) );
|
||||
|
||||
}
|
||||
|
||||
|
||||
// I define the key as one that should not be included in the serialized response.
|
||||
public any function exclude( required string key ) {
|
||||
|
||||
blockedKeyList[ key ] = true;
|
||||
|
||||
return( this );
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* I serialize the given input as JavaScript Object Notation (JSON) using the case-sensitive
|
||||
* values defined in the key-list.
|
||||
*
|
||||
* @output false
|
||||
*/
|
||||
public string function serialize( required any input ) {
|
||||
|
||||
// Write the serialized value to the output buffer.
|
||||
savecontent variable = "local.serializedInput" {
|
||||
|
||||
serializeInput( input, "any" );
|
||||
|
||||
}
|
||||
|
||||
return( serializedInput );
|
||||
|
||||
}
|
||||
|
||||
|
||||
// ---
|
||||
// PRIVATE METHODS.
|
||||
// ---
|
||||
|
||||
|
||||
// I define the given key within the given key list.
|
||||
private any function defineKey(
|
||||
required struct keyList,
|
||||
required string key,
|
||||
required string hint
|
||||
) {
|
||||
|
||||
if ( structKeyExists( fullKeyList, key ) ) {
|
||||
|
||||
throw(
|
||||
type = "DuplicateKey",
|
||||
message = "The key [#key#] has already been defined within the serializer.",
|
||||
detail = "The current key list is: #structKeyList( fullKeyList, ', ' )#"
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
// Add to the appropriate data-type lists. This one is used for existence checking.
|
||||
keyList[ key ] = key;
|
||||
|
||||
// Add all keys to the full key list as well. This one is used to store the serialization
|
||||
// of the key so that it doesn't have to be recalculated each time the object is serialized.
|
||||
fullKeyList[ key ] = serializeString( key );
|
||||
|
||||
// If we have a specific type, then add the hint to the full hint list as well. This will
|
||||
// allow us to quickly look up the pass-through data type hint during serialization.
|
||||
// --
|
||||
// NOTE: The reason we don't want to pass through "any" is that we want parent types to be
|
||||
// able to "fall through" during the object traversal. If we added "any" to the type list,
|
||||
// then it would always overwrite the parent data type.
|
||||
if ( hint != "any" ) {
|
||||
|
||||
fullHintList[ key ] = hint;
|
||||
|
||||
}
|
||||
|
||||
// Return this reference for method chaining.
|
||||
return( this );
|
||||
|
||||
}
|
||||
|
||||
|
||||
// I walk the given object, writing the serialized value to the output (which is expected to
|
||||
// be a content buffer).
|
||||
// ---
|
||||
// NOTE: THIS METHOD IS HUGE - this is on purpose. Since serialization is a rather intense
|
||||
// process, I am trying to cut out as much overhead as possible. In this case, we're cutting
|
||||
// out extra stack space by inlining and duplicating a lot of functionality. This is being done
|
||||
// at the COST of clarity and non-repetitive code.
|
||||
private void function serializeInput(
|
||||
required any input,
|
||||
required string hint
|
||||
) {
|
||||
|
||||
// Serialize the data base on the type of input. We are organizing this in terms of the
|
||||
// most commonly-used values first. The anticipation is that the vast majority of data
|
||||
// types will be simple values.
|
||||
if ( isSimpleValue( input ) ) {
|
||||
|
||||
if ( ( hint == "string" ) || ( hint == "any" ) ) {
|
||||
|
||||
// If the string appears to be numeric, then we have to prefix it to make sure
|
||||
// ColdFusion doesn't accidentally convert it to a number.
|
||||
if ( isNumeric( input ) ) {
|
||||
|
||||
writeOutput( """" & input & """" );
|
||||
|
||||
} else {
|
||||
|
||||
serializeInputString( input );
|
||||
|
||||
}
|
||||
|
||||
} else if ( ( hint == "boolean" ) && isBoolean( input ) ) {
|
||||
|
||||
writeOutput( input ? "true" : "false" );
|
||||
|
||||
} else if ( ( ( hint == "integer" ) || ( hint == "float" ) ) && isNumeric( input ) ) {
|
||||
|
||||
writeOutput( input );
|
||||
|
||||
} else if ( ( ( hint == "integer" ) || ( hint == "float" ) ) && isBoolean( input ) ) {
|
||||
|
||||
writeOutput( input ? "1" : "0" );
|
||||
|
||||
} else if ( ( hint == "date" ) && ( isDate( input ) || isNumericDate( input ) ) ) {
|
||||
|
||||
// Write the date in ISO 8601 time string format. We're going to assume that the
|
||||
// date is already in the desired timezone.
|
||||
///writeOutput( """" & dateFormat( input, "yyyy-mm-dd" ) & "T" & timeFormat( input, "HH:mm:ss.l" ) & "Z""" );
|
||||
//2024-06-27 modified by msyu: added server timezone
|
||||
writeOutput( """" & dateFormat( input, "yyyy-mm-dd" ) & "T" & timeFormat( input, "HH:mm:ss.lXX" ) & """" );
|
||||
|
||||
} else {
|
||||
|
||||
serializeInputString( input );
|
||||
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
} // END: isSimpleValue().
|
||||
|
||||
|
||||
// I'm expecting the struct to be the next most common data type since it will likely be
|
||||
// the container for the majority of data values.
|
||||
if ( isStruct( input ) ) {
|
||||
|
||||
writeOutput( "{" );
|
||||
|
||||
var isFirst = true;
|
||||
|
||||
for ( var key in input ) {
|
||||
|
||||
// Skip any black-listed keys.
|
||||
if ( structKeyExists( blockedKeyList, key ) ) {
|
||||
|
||||
continue;
|
||||
|
||||
}
|
||||
|
||||
// Handle the item delimiter.
|
||||
if ( isFirst ) {
|
||||
|
||||
isFirst = false;
|
||||
|
||||
} else {
|
||||
|
||||
writeOutput( "," );
|
||||
|
||||
}
|
||||
|
||||
// Ensure that the given key can be referenced on the full-key list. This way,
|
||||
// the subsequent logic will be easier.
|
||||
if ( ! structKeyExists( fullKeyList, key ) ) {
|
||||
|
||||
asAny( lcase( key ) );
|
||||
|
||||
}
|
||||
|
||||
writeOutput( fullKeyList[ key ] & ":" );
|
||||
|
||||
// Pass in the most appropriate data-type hint based on the parent key.
|
||||
if ( structKeyExists( fullHintList, key ) ) {
|
||||
|
||||
serializeInput( input[ key ], fullHintList[ key ] );
|
||||
|
||||
// If the given key is unknown, just pass through the most recent hint as
|
||||
// it may be defining the type for an entire structure.
|
||||
} else {
|
||||
|
||||
serializeInput( input[ key ], hint );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
writeOutput( "}" );
|
||||
|
||||
return;
|
||||
|
||||
} // END: isStruct().
|
||||
|
||||
|
||||
if ( isArray( input ) ) {
|
||||
|
||||
writeOutput( "[" );
|
||||
|
||||
var isFirst = true;
|
||||
|
||||
// Handle the item delimiter.
|
||||
for ( var value in input ) {
|
||||
|
||||
if ( isFirst ) {
|
||||
|
||||
isFirst = false;
|
||||
|
||||
} else {
|
||||
|
||||
writeOutput( "," );
|
||||
|
||||
}
|
||||
|
||||
// Since we don't have a key to go off of, pass-through the most recent hint.
|
||||
serializeInput( value, hint );
|
||||
|
||||
}
|
||||
|
||||
writeOutput( "]" );
|
||||
|
||||
return;
|
||||
|
||||
} // END: isArray().
|
||||
|
||||
|
||||
// When we serialize a query, we're going to treat it like an array of structs.
|
||||
if ( isQuery( input ) ) {
|
||||
|
||||
var keys = listToArray( input.columnList );
|
||||
|
||||
// Make sure each column is defined as a known key - makes the subsequent logic easier.
|
||||
for ( var key in keys ) {
|
||||
|
||||
if ( ! structKeyExists( fullKeyList, key ) ) {
|
||||
|
||||
asAny( lcase( key ) );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
writeOutput( "[" );
|
||||
|
||||
// Serialize each row of the query as a struct.
|
||||
for ( var i = 1 ; i <= input.recordCount ; i++ ) {
|
||||
|
||||
// Handle the row delimiter.
|
||||
if ( i > 1 ) {
|
||||
|
||||
writeOutput( "," );
|
||||
|
||||
}
|
||||
|
||||
writeOutput( "{" );
|
||||
|
||||
var isFirst = true;
|
||||
|
||||
for ( var key in keys ) {
|
||||
|
||||
// Skip any black-listed keys.
|
||||
if ( structKeyExists( blockedKeyList, key ) ) {
|
||||
|
||||
continue;
|
||||
|
||||
}
|
||||
|
||||
// Handle the item delimiter (in the current row).
|
||||
if ( isFirst ) {
|
||||
|
||||
isFirst = false;
|
||||
|
||||
} else {
|
||||
|
||||
writeOutput( "," );
|
||||
|
||||
}
|
||||
|
||||
writeOutput( fullKeyList[ key ] & ":" );
|
||||
|
||||
// Pass in the most appropriate data-type hint based on the parent key.
|
||||
if ( structKeyExists( fullHintList, key ) ) {
|
||||
|
||||
serializeInput( input[ key ][ i ], fullHintList[ key ] );
|
||||
|
||||
// If the given key is unknown, just pass through the most recent hint as
|
||||
// it may be defining the type for an entire structure.
|
||||
} else {
|
||||
|
||||
serializeInput( input[ key ][ i ], hint );
|
||||
|
||||
}
|
||||
|
||||
} // END: Key list.
|
||||
|
||||
writeOutput( "}" );
|
||||
|
||||
} // END: Row list.
|
||||
|
||||
writeOutput( "]" );
|
||||
|
||||
return;
|
||||
|
||||
} // END: isQuery().
|
||||
|
||||
|
||||
// If we made it this far, we were given a data type that we're not actively supporting.
|
||||
// As such, we just have to hand this off to the native serializer.
|
||||
writeOutput( serializeJson( input ) );
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* I serialize and write the given string to the current output context, escaping all appropriate
|
||||
* characters for the JSON specification.
|
||||
*
|
||||
* NOTE: We are using this manual-encoding process rather than the built-in serializeJson() function
|
||||
* as there is a rather nasty bug that corrupts certain patterns in the output. Read more:
|
||||
*
|
||||
* http://www.bennadel.com/blog/2842-serializejson-and-the-input-and-output-encodings-are-not-same-errors-in-coldfusion.htm
|
||||
*
|
||||
* @input I am the string being serialized.
|
||||
*/
|
||||
private void function serializeInputString( required string input ) {
|
||||
|
||||
// While this may not be technically needed, this will ensure that we are not using any
|
||||
// "undocumented features" of the language. If we explicitly cast to a Java string, and
|
||||
// something goes wrong due to odd type-casting, it's a ColdFusion bug, at that point, not
|
||||
// a logic error ;)
|
||||
input = javaCast( "string", input );
|
||||
|
||||
var length = input.length();
|
||||
|
||||
writeOutput( """" );
|
||||
|
||||
for ( var i = 1 ; i <= length ; i++ ) {
|
||||
|
||||
var charCode = input.codePointAt( javaCast( "int", i - 1 ) );
|
||||
|
||||
// Check for the most common case first (normal characters).
|
||||
if (
|
||||
( charCode >= 32 ) &&
|
||||
( charCode != 34 ) &&
|
||||
( charCode != 47 ) &&
|
||||
( charCode != 92 ) &&
|
||||
( charCode != 8232 ) &&
|
||||
( charCode != 8233 )
|
||||
) {
|
||||
|
||||
writeOutput( chr( charCode ) );
|
||||
|
||||
// Check for the special cases next (control characters, characters that
|
||||
// need to be escaped, and characters that need to be encoded nicely).
|
||||
} else if ( charCode == 8 ) {
|
||||
|
||||
writeOutput( "\b" );
|
||||
|
||||
} else if ( charCode == 9 ) {
|
||||
|
||||
writeOutput( "\t" );
|
||||
|
||||
} else if ( charCode == 10 ) {
|
||||
|
||||
writeOutput( "\n" );
|
||||
|
||||
} else if ( charCode == 12 ) {
|
||||
|
||||
writeOutput( "\f" );
|
||||
|
||||
} else if ( charCode == 13 ) {
|
||||
|
||||
writeOutput( "\r" );
|
||||
|
||||
} else if (
|
||||
( charCode < 32 ) ||
|
||||
( charCode == 8232 ) ||
|
||||
( charCode == 8233 )
|
||||
) {
|
||||
|
||||
// For Unicode hex values, we need to enforce a 4-digit code.
|
||||
writeOutput( "\u" & right( ( "000" & formatBaseN( charCode, 16 ) ), 4 ) );
|
||||
|
||||
} else if ( charCode == 34 ) {
|
||||
|
||||
writeOutput( "\""" );
|
||||
|
||||
} else if ( charCode == 47 ) {
|
||||
|
||||
writeOutput( "\/" );
|
||||
|
||||
} else if ( charCode == 92 ) {
|
||||
|
||||
writeOutput( "\\" );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
writeOutput( """" );
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* I serialize and return the given string, escaping all appropriate characters for the
|
||||
* JSON specification.
|
||||
*
|
||||
* @input I am the string being serialized.
|
||||
* @output false
|
||||
*/
|
||||
private string function serializeString( required string input ) {
|
||||
|
||||
savecontent variable = "local.json" {
|
||||
|
||||
serializeInputString( input );
|
||||
|
||||
}
|
||||
|
||||
return( json );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
component {
|
||||
|
||||
public any function init() {
|
||||
variables.utcBaseDate = createObject( 'java', 'java.util.Date' ).init( javacast( 'int', 0 ) );
|
||||
variables.ECParameterSpecCache = { };
|
||||
}
|
||||
|
||||
function convertDateToUnixTimestamp( required date dateToConvert ) {
|
||||
return dateDiff( 's', utcBaseDate, parseDateTime( dateToConvert ) );
|
||||
}
|
||||
|
||||
function convertUnixTimestampToDate( required numeric timestamp ) {
|
||||
return dateAdd( 's', timestamp, utcBaseDate );
|
||||
}
|
||||
|
||||
function base64UrlToBinary( base64url ) {
|
||||
var base64 = base64url.replace( '-', '+', 'all' ).replace( '_', '/', 'all' );
|
||||
var padded = base64 & repeatString( '=', 4 - ( len( base64 ) % 4 ) );
|
||||
return binaryDecode( padded, 'base64' );
|
||||
}
|
||||
|
||||
function binaryToBase64Url( source ) {
|
||||
return binaryEncode( source, 'base64' )
|
||||
.replace( '+', '-', 'all' )
|
||||
.replace( '/', '_', 'all' )
|
||||
.replace( '=', '', 'all' );
|
||||
}
|
||||
|
||||
/**
|
||||
* The INTEGER encoding for DER consists of a 02 tag a length encoding of the value
|
||||
* and then a signed, minimum sized, big endian encoding of the encoded number.
|
||||
*
|
||||
* - https://stackoverflow.com/questions/54718741/how-to-der-encode-an-ecdsa-signature
|
||||
*/
|
||||
function derEncodeIntegerBytes( byteArray ) {
|
||||
// first remove any padding
|
||||
for ( var i = 1; i <= arrayLen( byteArray ); i++ ) {
|
||||
if ( byteArray[ i ] != 0 ) break;
|
||||
}
|
||||
var unpadded = arraySlice( byteArray, i );
|
||||
|
||||
// add sign if negative
|
||||
if ( unpadded[ 1 ] < 0 ) {
|
||||
unpadded.prepend( 0 );
|
||||
}
|
||||
|
||||
// if len > 127 the length encoding will be wrong, but that won't happen for the supported signature sizes
|
||||
var derEncoded = [ 2, unpadded.len() ];
|
||||
|
||||
derEncoded.append( unpadded, true );
|
||||
|
||||
return derEncoded;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The SEQUENCE encoding is simply a tag set to the byte value 30, the
|
||||
* length encoding and then the concatenation of the two INTEGER
|
||||
* structures.
|
||||
*
|
||||
* https://stackoverflow.com/questions/54718741/how-to-der-encode-an-ecdsa-signature
|
||||
*
|
||||
* Also see:
|
||||
* https://crypto.stackexchange.com/questions/57731/ecdsa-signature-rs-to-asn1-der-encoding-question
|
||||
*/
|
||||
function convertP1363ToDER( signature ) {
|
||||
var split = len( signature ) / 2;
|
||||
var r = derEncodeIntegerBytes( arraySlice( signature, 1, split ) );
|
||||
var s = derEncodeIntegerBytes( arraySlice( signature, split + 1, split ) );
|
||||
|
||||
var DERSignature = [ 48 ];
|
||||
|
||||
var length = r.len() + s.len();
|
||||
|
||||
if ( length > 255 ) {
|
||||
throw(
|
||||
type = 'jwtcfml.InvalidSignature',
|
||||
message = 'Invalid P1363 key.',
|
||||
detail = 'The P1363 signature is too long.'
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
The length is simply a single byte if it is smaller than 128 (or hex 80)
|
||||
of the size. If it is larger then it is two byte: one byte set to 81,
|
||||
which indicates that one length byte will follow, and one byte
|
||||
containing the actual value.
|
||||
|
||||
https://stackoverflow.com/questions/54718741/how-to-der-encode-an-ecdsa-signature
|
||||
*/
|
||||
if ( length > 127 ) {
|
||||
DERSignature.append( -127 );
|
||||
length -= 256;
|
||||
}
|
||||
DERSignature.append( length );
|
||||
|
||||
DERSignature.append( r, true );
|
||||
DERSignature.append( s, true );
|
||||
|
||||
return javacast( 'byte[]', DERSignature );
|
||||
}
|
||||
|
||||
function convertDERtoP1363( required any signature, required string algorithm ) {
|
||||
// extract the two integers from the DER signature
|
||||
// assuming a 02 tag byte followed by a single length byte since we should not see
|
||||
// anything larger in the supported algorithms
|
||||
var start = 3;
|
||||
while ( signature[ start ] != 2 ) start++;
|
||||
var r = arraySlice( signature, start + 2, signature[ start + 1 ] );
|
||||
var s = arraySlice( signature, start + 2 + r.len() + 2 );
|
||||
|
||||
if ( r[ 1 ] == 0 ) r = arraySlice( r, 2 );
|
||||
if ( s[ 1 ] == 0 ) s = arraySlice( s, 2 );
|
||||
|
||||
var lengthMap = {
|
||||
ES256: 32,
|
||||
ES384: 48,
|
||||
ES512: 64
|
||||
};
|
||||
|
||||
var P1363Signature = [ ];
|
||||
|
||||
for ( var i = 1; i <= lengthMap[ algorithm ] - r.len(); i++ ) P1363Signature.append( 0 );
|
||||
P1363Signature.append( r, true );
|
||||
|
||||
for ( var i = 1; i <= lengthMap[ algorithm ] - s.len(); i++ ) P1363Signature.append( 0 );
|
||||
P1363Signature.append( s, true );
|
||||
|
||||
return javacast( 'byte[]', P1363Signature );
|
||||
}
|
||||
|
||||
function parsePEMEncodedKey( required string pemKey ) {
|
||||
if ( reFind( '^-----BEGIN (RSA|EC) (PARAMETERS|PRIVATE)', pemKey ) ) {
|
||||
throw(
|
||||
type = 'jwtcfml.InvalidPrivateKey',
|
||||
message = 'Invalid private key format.',
|
||||
detail = 'Please encode your private key in PKCS8 format, e.g.: `openssl pkcs8 -topk8 -nocrypt -in privatekey.pem -out privatekey.pk8'
|
||||
)
|
||||
}
|
||||
|
||||
var binaryKey = binaryDecode(
|
||||
trim( pemKey ).reReplace( '-----[A-Z\s]+-----', '', 'all' ).reReplace( '[\r\n]', '', 'all' ),
|
||||
'base64'
|
||||
);
|
||||
|
||||
if ( find( '-----BEGIN CERTIFICATE-----', pemKey ) ) {
|
||||
var bis = createObject( 'java', 'java.io.ByteArrayInputStream' ).init( binaryKey );
|
||||
return createObject( 'java', 'java.security.cert.CertificateFactory' )
|
||||
.getInstance( 'X.509' )
|
||||
.generateCertificate( bis )
|
||||
.getPublicKey();
|
||||
}
|
||||
|
||||
if ( find( '-----BEGIN PUBLIC KEY-----', pemKey ) ) {
|
||||
var publicKeySpec = createObject( 'java', 'java.security.spec.X509EncodedKeySpec' ).init( binaryKey );
|
||||
try {
|
||||
return createObject( 'java', 'java.security.KeyFactory' )
|
||||
.getInstance( 'RSA' )
|
||||
.generatePublic( publicKeySpec );
|
||||
} catch ( any e ) {
|
||||
}
|
||||
try {
|
||||
return createObject( 'java', 'java.security.KeyFactory' )
|
||||
.getInstance( 'EC' )
|
||||
.generatePublic( publicKeySpec );
|
||||
} catch ( any e ) {
|
||||
}
|
||||
}
|
||||
|
||||
if ( find( '-----BEGIN PRIVATE KEY-----', pemKey ) ) {
|
||||
var privateKeySpec = createObject( 'java', 'java.security.spec.PKCS8EncodedKeySpec' ).init( binaryKey );
|
||||
try {
|
||||
return createObject( 'java', 'java.security.KeyFactory' )
|
||||
.getInstance( 'RSA' )
|
||||
.generatePrivate( privateKeySpec );
|
||||
} catch ( any e ) {
|
||||
}
|
||||
try {
|
||||
return createObject( 'java', 'java.security.KeyFactory' )
|
||||
.getInstance( 'EC' )
|
||||
.generatePrivate( privateKeySpec );
|
||||
} catch ( any e ) {
|
||||
}
|
||||
}
|
||||
|
||||
throw(
|
||||
type = 'jwtcfml.InvalidPEMKey',
|
||||
message = 'Invalid PEM key.',
|
||||
detail = 'Please ensure you are using an RSA or EC public or private key or certificate.'
|
||||
)
|
||||
}
|
||||
|
||||
function parseJWK( required struct jwk ) {
|
||||
if ( jwk.kty == 'RSA' ) {
|
||||
if ( jwk.keyExists( 'd' ) ) {
|
||||
try {
|
||||
var bigInts = bigIntegers( jwk, [ 'n', 'e', 'd', 'p', 'q', 'dp', 'dq', 'qi' ] );
|
||||
var keySpec = createObject( 'java', 'java.security.spec.RSAPrivateCrtKeySpec' ).init(
|
||||
bigInts.n,
|
||||
bigInts.e,
|
||||
bigInts.d,
|
||||
bigInts.p,
|
||||
bigInts.q,
|
||||
bigInts.dp,
|
||||
bigInts.dq,
|
||||
bigInts.qi
|
||||
);
|
||||
var kf = createObject( 'java', 'java.security.KeyFactory' ).getInstance( 'RSA' );
|
||||
return kf.generatePrivate( keySpec );
|
||||
} catch ( any e ) {
|
||||
}
|
||||
|
||||
try {
|
||||
var bigInts = bigIntegers( jwk, [ 'n', 'd' ] );
|
||||
var keySpec = createObject( 'java', 'java.security.spec.RSAPrivateKeySpec' ).init(
|
||||
bigInts.n,
|
||||
bigInts.d
|
||||
);
|
||||
var kf = createObject( 'java', 'java.security.KeyFactory' ).getInstance( 'RSA' );
|
||||
return kf.generatePrivate( keySpec );
|
||||
} catch ( any e ) {
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
var bigInts = bigIntegers( jwk, [ 'n', 'e' ] );
|
||||
var ks = createObject( 'java', 'java.security.spec.RSAPublicKeySpec' ).init( bigInts.n, bigInts.e );
|
||||
var kf = createObject( 'java', 'java.security.KeyFactory' ).getInstance( 'RSA' );
|
||||
return kf.generatePublic( ks );
|
||||
} catch ( any e ) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( jwk.kty == 'EC' ) {
|
||||
var kf = createObject( 'java', 'java.security.KeyFactory' ).getInstance( 'EC' );
|
||||
var ECParameterSpec = getECParameterSpec( jwk.crv );
|
||||
|
||||
if ( jwk.keyExists( 'd' ) ) {
|
||||
var bigInts = bigIntegers( jwk, [ 'd' ] );
|
||||
var ks = createObject( 'java', 'java.security.spec.ECPrivateKeySpec' ).init(
|
||||
bigInts.d,
|
||||
ECParameterSpec
|
||||
);
|
||||
return kf.generatePrivate( ks );
|
||||
} else {
|
||||
var bigInts = bigIntegers( jwk, [ 'x', 'y' ] );
|
||||
var ECPoint = createObject( 'java', 'java.security.spec.ECPoint' ).init( bigInts.x, bigInts.y );
|
||||
var ks = createObject( 'java', 'java.security.spec.ECPublicKeySpec' ).init( ECPoint, ECParameterSpec );
|
||||
return kf.generatePublic( ks );
|
||||
}
|
||||
}
|
||||
|
||||
throw(
|
||||
type = 'jwtcfml.InvalidJWK',
|
||||
message = 'Invalid JWK key.',
|
||||
detail = 'Please ensure you are using an valid JWK RSA or EC public or private key.'
|
||||
)
|
||||
}
|
||||
|
||||
private function bigIntegers( jwk, keys ) {
|
||||
var bigInts = { };
|
||||
for ( var key in keys ) {
|
||||
bigInts[ key ] = createObject( 'java', 'java.math.BigInteger' ).init( 1, base64UrlToBinary( jwk[ key ] ) );
|
||||
}
|
||||
return bigInts;
|
||||
}
|
||||
|
||||
private function getECParameterSpec( crv ) {
|
||||
if ( !variables.ECParameterSpecCache.keyExists( crv ) ) {
|
||||
var kpg = createObject( 'java', 'java.security.KeyPairGenerator' ).getInstance( 'EC' );
|
||||
var ecgp = createObject( 'java', 'java.security.spec.ECGenParameterSpec' ).init(
|
||||
'secp#crv.listLast( '-' )#r1'
|
||||
);
|
||||
kpg.initialize( ecgp );
|
||||
variables.ECParameterSpecCache[ crv ] = kpg
|
||||
.generateKeyPair()
|
||||
.getPublic()
|
||||
.getParams();
|
||||
}
|
||||
return variables.ECParameterSpecCache[ crv ];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<cfsilent>
|
||||
<!--- 20181209 v0.3 --->
|
||||
<!---v0.3 добавлен атрибут cfSqlType--->
|
||||
<!---v0.4 добавлен атрибут container for json--->
|
||||
<!---v0.5 добавлен атрибут type и трансляция в CF_SQL_--->
|
||||
<!--- 20210326 v0.8 formatter --->
|
||||
|
||||
<cffunction name="passThrough"
|
||||
returntype="any"
|
||||
output="false"
|
||||
hint="just return argument">
|
||||
<cfargument name="x" type="ANY" required="true"/>
|
||||
<cfreturn #ARGUMENTS.x#/>
|
||||
</cffunction>
|
||||
|
||||
<cfif thisTag.executionMode IS "end" OR !thisTag.hasEndTag>
|
||||
<cfassociate basetag="cf_field_set" datacollection="fieldsArray"/>
|
||||
|
||||
<cfparam name="ATTRIBUTES.expression" default=""/>
|
||||
<cfparam name="ATTRIBUTES.title" default=""/>
|
||||
<cfparam name="ATTRIBUTES.name" default=""/>
|
||||
<cfparam name="ATTRIBUTES.type" default="string"/>
|
||||
<cfparam name="ATTRIBUTES.cfSqlType" default="#getCfSqlType(ATTRIBUTES.type)#"/>
|
||||
<cfparam name="ATTRIBUTES.container" default=""/>
|
||||
<cfparam name="ATTRIBUTES.formatter" default=#passThrough#/>
|
||||
|
||||
<cfif ATTRIBUTES.expression IS "">
|
||||
<cfset ATTRIBUTES.expression=thisTag.generatedContent/>
|
||||
</cfif>
|
||||
|
||||
<cfif ATTRIBUTES.name IS "">
|
||||
<cfset regex="([[:word:]]+)[[:space:]]*$"/>
|
||||
<cfset matches=REFindNoCase(regex, ATTRIBUTES.expression, 1, true)/>
|
||||
<cfif arrayLen(matches.pos) GT 1>
|
||||
<cfset ATTRIBUTES.name=mid(ATTRIBUTES.expression, matches.pos[2], matches.len[2])/>
|
||||
</cfif>
|
||||
</cfif>
|
||||
</cfif>
|
||||
|
||||
<cffunction name="getCfSqlType">
|
||||
<!---Translate CF type understandable by isValid to CF_SQL_*--->
|
||||
<cfargument name="type"/>
|
||||
|
||||
<cfswitch expression=#ARGUMENTS.type#>
|
||||
<cfcase value="string">
|
||||
<cfreturn "CF_SQL_VARCHAR"/>
|
||||
</cfcase>
|
||||
<cfcase value="integer">
|
||||
<cfreturn "CF_SQL_INTEGER"/>
|
||||
</cfcase>
|
||||
<cfcase value="boolean">
|
||||
<cfreturn "CF_SQL_BIT"/>
|
||||
</cfcase>
|
||||
<cfcase value="numeric">
|
||||
<cfreturn "CF_SQL_NUMERIC"/>
|
||||
</cfcase>
|
||||
<cfcase value="date,time">
|
||||
<cfreturn "CF_SQL_TIMESTAMP"/>
|
||||
</cfcase>
|
||||
<cfdefaultcase>
|
||||
<cfthrow type="custom" message="Unsupported type" detail="Type #ARGUMENTS.type# not supported"/>
|
||||
</cfdefaultcase>
|
||||
</cfswitch>
|
||||
|
||||
</cffunction>
|
||||
</cfsilent>
|
||||
@@ -0,0 +1,50 @@
|
||||
<cfsilent>
|
||||
<!--- 20181209 v0.3 добавлено поле cfSqlType--->
|
||||
<!--- 20201029 v0.5 все атрибуты необязательные--->
|
||||
<!--- 20201114 v0.6 добавлен атрибут "container" for json--->
|
||||
<!--- 20201201 v0.7 исправлен listOut--->
|
||||
<!--- 20210326 v0.8 formatter --->
|
||||
<!--- Используется в селекте для структурирования списка полей. Выводит вместо себя список через запятую, экспортирует Map со структурами выражение+заголовок, с ключом, соответствующим имени колонки в селекте (ключ вычисляется регулярным выражением) --->
|
||||
|
||||
<cfif thisTag.executionMode is "end">
|
||||
|
||||
<cfparam name="ATTRIBUTES.titleMapOut" default=""/>
|
||||
<cfparam name="ATTRIBUTES.lengthOut" default=""/>
|
||||
<cfparam name="ATTRIBUTES.listOut" default=""/><!---возвращаемый список полей через запятую для селекта. Если не задан (чаще всего так), этот список возвращается в виде контента тега --->
|
||||
|
||||
<cfparam name="thisTag.fieldsArray" type="array"/><!--- Вложенные теги field отдают сюда свои данные --->
|
||||
|
||||
<cfset titleMap=structNew("linked")/><!---railo syntax--->
|
||||
<cfset list=""/>
|
||||
<cfset i=0/>
|
||||
<cfloop array=#thisTag.fieldsArray# index="field">
|
||||
<cfset i=i+1/>
|
||||
<cfset list=listAppend(#list#, #field.expression#)/>
|
||||
<cfif len(#field.name#)>
|
||||
<cfset structInsert(#titleMap#, #field.name#, structNew())/>
|
||||
<cfset "titleMap.#field.name#.ordinal"=#i#/>
|
||||
<cfset "titleMap.#field.name#.title"=#field.title#/>
|
||||
<cfset "titleMap.#field.name#.type"=#field.type#/>
|
||||
<cfset "titleMap.#field.name#.cfSqlType"=#field.cfSqlType#/>
|
||||
<cfset "titleMap.#field.name#.container"=#field.container#/>
|
||||
<cfset "titleMap.#field.name#.formatter"=#field.formatter#/>
|
||||
</cfif>
|
||||
</cfloop>
|
||||
|
||||
<cfif len(ATTRIBUTES.lengthOut)>
|
||||
<cfset "CALLER.#ATTRIBUTES.lengthOut#"=#arrayLen(thisTag.fieldsArray)#/>
|
||||
</cfif>
|
||||
|
||||
<cfif len(ATTRIBUTES.titleMapOut)>
|
||||
<cfset "CALLER.#ATTRIBUTES.titleMapOut#"=#titleMap#/>
|
||||
</cfif>
|
||||
|
||||
<cfif len(ATTRIBUTES.listOut)>
|
||||
<cfset "CALLER.#ATTRIBUTES.listOut#"=preserveSingleQuotes(list)/>
|
||||
<cfset thisTag.generatedContent=""/>
|
||||
<cfelse>
|
||||
<cfset thisTag.generatedContent=preserveSingleQuotes(list)/>
|
||||
</cfif>
|
||||
|
||||
</cfif>
|
||||
</cfsilent>
|
||||
@@ -0,0 +1,52 @@
|
||||
<!--- version 2.01---><!---15:25 31.01.2019--->
|
||||
<!--- version 3---><!---16:42 16.11.2020--->
|
||||
<!--- build query filter string --->
|
||||
|
||||
<cfparam name="ATTRIBUTES.filter" type="struct"/>
|
||||
<cfloop collection=#ATTRIBUTES.filter# item="item">
|
||||
<cfsilent>
|
||||
<cfset fltr=structFind(ATTRIBUTES.filter,item)>
|
||||
<cfparam name="fltr.ftype"/>
|
||||
<cfparam name="fltr.field"/>
|
||||
<cfparam name="fltr.compare" default="EQ"/>
|
||||
<cfparam name="fltr.val" default=""/>
|
||||
<cfparam name="fltr.prefix" default=""/>
|
||||
<cfparam name="fltr.suffix" default=""/>
|
||||
<cfparam name="fltr.list" default="No"/>
|
||||
</cfsilent><!---
|
||||
|
||||
---> AND <cfif structKeyExists(fltr,"expression") AND len(fltr.expression)><!---
|
||||
---><cfset len=listLen(#fltr.expression#,"?")><!---
|
||||
---><cfloop from=1 to=#len# index="i"><!---
|
||||
---><cfoutput>#listGetAt(fltr.expression,i,"?")#</cfoutput><!---
|
||||
---><cfif i LT #len#><cfqueryparam cfsqltype="#getCfSqLType(fltr.ftype)#" list="#fltr.list#" value="#fltr.prefix##fltr.val##fltr.suffix#"/></cfif><!---
|
||||
---></cfloop><!---
|
||||
---><cfelse><!---
|
||||
---><cfoutput>#fltr.field#</cfoutput><cfswitch expression=#uCase(fltr.compare)#><cfcase value="EQ"> = </cfcase><cfcase value="NE,NEQ"> <> </cfcase><cfcase value="LE,LTE"> <= </cfcase><cfcase value="LT"> < </cfcase><cfcase value="GE,GTE"> >= </cfcase><cfcase value="GT"> > </cfcase><cfcase value="LIKE"> LIKE </cfcase><cfcase value="LIKE%,LIKEP"> LIKE <cfqueryparam cfsqltype=#getCfSqLType(fltr.ftype)# value="%#fltr.val#%"/><cfcontinue/></cfcase><cfcase value="IN"> IN (<cfqueryparam cfsqltype=#getCfSqLType(fltr.ftype)# list="#fltr.list#" value="#fltr.val#"/>)<cfcontinue/></cfcase><cfdefaultcase> = <cfoutput> #fltr.field#</cfoutput><cfcontinue/><!---***криво---></cfdefaultcase></cfswitch><cfqueryparam cfsqltype=#getCfSqLType(fltr.ftype)# value="#fltr.val#"/><cfcontinue/>
|
||||
</cfif>
|
||||
</cfloop>
|
||||
<cfexit method="exittag"/>
|
||||
|
||||
<cffunction name="getCfSqlType" output="No">
|
||||
<cfargument name="fieldType" default=""/>
|
||||
<cfswitch expression=#ARGUMENTS.fieldType#>
|
||||
<cfcase value="numeric">
|
||||
<cfreturn "CF_SQL_NUMERIC"/>
|
||||
</cfcase>
|
||||
<cfcase value="integer">
|
||||
<cfreturn "CF_SQL_INTEGER"/>
|
||||
</cfcase>
|
||||
<cfcase value="boolean">
|
||||
<cfreturn "CF_SQL_BIT"/>
|
||||
</cfcase>
|
||||
<cfcase value="date,time,datetime">
|
||||
<cfreturn "CF_SQL_TIMESTAMP"/>
|
||||
</cfcase>
|
||||
<cfcase value="string">
|
||||
<cfreturn "CF_SQL_VARCHAR"/>
|
||||
</cfcase>
|
||||
<cfdefaultcase>
|
||||
<cfreturn "CF_SQL_VARCHAR"/>
|
||||
</cfdefaultcase>
|
||||
</cfswitch>
|
||||
</cffunction>
|
||||
@@ -0,0 +1 @@
|
||||
<cfoutput>index ***</cfoutput>
|
||||
+262
@@ -0,0 +1,262 @@
|
||||
component {
|
||||
|
||||
variables.algorithmMap = {
|
||||
HS256: 'HmacSHA256',
|
||||
HS384: 'HmacSHA384',
|
||||
HS512: 'HmacSHA512',
|
||||
RS256: 'SHA256withRSA',
|
||||
RS384: 'SHA384withRSA',
|
||||
RS512: 'SHA512withRSA',
|
||||
ES256: 'SHA256withECDSA',
|
||||
ES384: 'SHA384withECDSA',
|
||||
ES512: 'SHA512withECDSA'
|
||||
};
|
||||
|
||||
public any function init() {
|
||||
variables.encodingUtils = new encodingUtils();
|
||||
variables.jss = createObject( 'java', 'java.security.Signature' );
|
||||
variables.messageDigest = createObject( 'java', 'java.security.MessageDigest' );
|
||||
return this;
|
||||
}
|
||||
|
||||
public string function encode(
|
||||
required struct payload,
|
||||
required any key,
|
||||
required string algorithm,
|
||||
struct headers = { }
|
||||
) {
|
||||
if ( !algorithmMap.keyExists( algorithm ) ) {
|
||||
throw(
|
||||
type = 'jwtcfml.InvalidAlgorithm',
|
||||
message = 'Invalid JWT Algorithm.',
|
||||
detail = 'The passed in algorithm is not supported.'
|
||||
);
|
||||
}
|
||||
|
||||
var header = { };
|
||||
header.append( headers );
|
||||
header.append( {
|
||||
'typ': 'JWT',
|
||||
'alg': algorithm
|
||||
} );
|
||||
|
||||
var duplicatedPayload = duplicate( payload );
|
||||
for ( var claim in [ 'iat', 'exp', 'nbf' ] ) {
|
||||
if ( duplicatedPayload.keyExists( claim ) && isDate( duplicatedPayload[ claim ] ) ) {
|
||||
duplicatedPayload[ claim ] = encodingUtils.convertDateToUnixTimestamp( duplicatedPayload[ claim ] );
|
||||
}
|
||||
}
|
||||
|
||||
var stringToSignParts = [
|
||||
encodingUtils.binaryToBase64Url( charsetDecode( serializeJSON( header ), 'utf-8' ) ),
|
||||
encodingUtils.binaryToBase64Url( charsetDecode( serializeJSON( duplicatedPayload ), 'utf-8' ) )
|
||||
];
|
||||
var stringToSign = stringToSignParts.toList( '.' );
|
||||
|
||||
return stringToSign & '.' & encodingUtils.binaryToBase64Url( sign( stringToSign, key, algorithm ) );
|
||||
}
|
||||
|
||||
public struct function decode(
|
||||
required string token,
|
||||
any key,
|
||||
any algorithms = [ ],
|
||||
struct claims = { },
|
||||
boolean verify = true
|
||||
) {
|
||||
var parts = listToArray( token, '.' );
|
||||
|
||||
if ( arrayLen( parts ) != 3 ) {
|
||||
throw(
|
||||
type = 'jwtcfml.InvalidToken',
|
||||
message = 'Invalid JWT.',
|
||||
detail = 'The passed in token does not have three `.` delimited parts.'
|
||||
);
|
||||
}
|
||||
|
||||
algorithms = isArray( algorithms ) ? algorithms : [ algorithms ];
|
||||
|
||||
var decoded = {
|
||||
header: deserializeJSON( charsetEncode( encodingUtils.base64UrlToBinary( parts[ 1 ] ), 'utf-8' ) ),
|
||||
payload: deserializeJSON( charsetEncode( encodingUtils.base64UrlToBinary( parts[ 2 ] ), 'utf-8' ) )
|
||||
};
|
||||
|
||||
if ( verify ) {
|
||||
if (
|
||||
!algorithms.find( decoded.header.alg ) ||
|
||||
!algorithmMap.keyExists( decoded.header.alg )
|
||||
) {
|
||||
throw(
|
||||
type = 'jwtcfml.InvalidAlgorithm',
|
||||
message = 'Unsupported or invalid algorithm',
|
||||
detail = 'The passed in token does not have an algorithm declaration or its declared algorithm (#decoded.header.alg#) does not match the specified algorithms of #serializeJSON( algorithms )#.'
|
||||
);
|
||||
}
|
||||
|
||||
var stringToSign = parts[ 1 ] & '.' & parts[ 2 ];
|
||||
var signature = encodingUtils.base64UrlToBinary( parts[ 3 ] );
|
||||
|
||||
if (
|
||||
!verifySignature(
|
||||
stringToSign,
|
||||
key,
|
||||
signature,
|
||||
decoded.header.alg
|
||||
)
|
||||
) {
|
||||
throw(
|
||||
type = 'jwtcfml.InvalidSignature',
|
||||
message = 'Signature is Invalid',
|
||||
detail = 'The signature of the passed in token is invalid.'
|
||||
);
|
||||
}
|
||||
|
||||
var baseClaims = {
|
||||
'exp': true,
|
||||
'nbf': true
|
||||
};
|
||||
baseClaims.append( claims );
|
||||
verifyClaims( decoded.payload, baseClaims );
|
||||
}
|
||||
|
||||
for ( var claim in [ 'iat', 'exp', 'nbf' ] ) {
|
||||
if ( decoded.payload.keyExists( claim ) ) {
|
||||
decoded.payload[ claim ] = encodingUtils.convertUnixTimestampToDate( decoded.payload[ claim ] );
|
||||
}
|
||||
}
|
||||
|
||||
return decoded.payload;
|
||||
}
|
||||
|
||||
public struct function getHeader( required string token ) {
|
||||
return deserializeJSON( charsetEncode( encodingUtils.base64UrlToBinary( listFirst( token, '.' ) ), 'utf-8' ) );
|
||||
}
|
||||
|
||||
public function parsePEMEncodedKey( required string pemKey ) {
|
||||
return encodingUtils.parsePEMEncodedKey( pemKey );
|
||||
}
|
||||
|
||||
public function parseJWK( required struct jwk ) {
|
||||
return encodingUtils.parseJWK( jwk );
|
||||
}
|
||||
|
||||
private function sign( message, key, algorithm ) {
|
||||
if ( left( algorithm, 1 ) == 'H' ) {
|
||||
var sig = binaryDecode(
|
||||
hmac(
|
||||
message,
|
||||
key,
|
||||
algorithmMap[ algorithm ],
|
||||
'utf-8'
|
||||
),
|
||||
'hex'
|
||||
);
|
||||
} else {
|
||||
if ( isSimpleValue( key ) ) {
|
||||
key = encodingUtils.parsePEMEncodedKey( key );
|
||||
} else if ( isStruct( key ) ) {
|
||||
key = encodingUtils.parseJWK( key );
|
||||
}
|
||||
|
||||
var jssInstance = variables.jss.getInstance( algorithmMap[ algorithm ] );
|
||||
jssInstance.initSign( key );
|
||||
jssInstance.update( charsetDecode( message, 'utf-8' ) );
|
||||
var sig = jssInstance.sign();
|
||||
if ( left( algorithm, 1 ) == 'E' ) {
|
||||
sig = encodingUtils.convertDERtoP1363( sig, algorithm );
|
||||
}
|
||||
}
|
||||
return sig;
|
||||
}
|
||||
|
||||
private function verifySignature( message, key, signature, algorithm ) {
|
||||
if ( left( algorithm, 1 ) == 'H' ) {
|
||||
var sig = binaryDecode(
|
||||
hmac(
|
||||
message,
|
||||
key,
|
||||
algorithmMap[ algorithm ],
|
||||
'utf-8'
|
||||
),
|
||||
'hex'
|
||||
);
|
||||
return MessageDigest.isEqual( signature, sig );
|
||||
}
|
||||
|
||||
if ( left( algorithm, 1 ) == 'E' ) {
|
||||
signature = encodingUtils.convertP1363ToDER( signature );
|
||||
}
|
||||
|
||||
if ( isSimpleValue( key ) ) {
|
||||
key = encodingUtils.parsePEMEncodedKey( key );
|
||||
} else if ( isStruct( key ) ) {
|
||||
key = encodingUtils.parseJWK( key );
|
||||
}
|
||||
|
||||
var jssInstance = variables.jss.getInstance( algorithmMap[ algorithm ] );
|
||||
jssInstance.initVerify( key );
|
||||
jssInstance.update( charsetDecode( message, 'utf-8' ) );
|
||||
return jssInstance.verify( signature );
|
||||
}
|
||||
|
||||
private function verifyClaims( payload, claims ) {
|
||||
if (
|
||||
structKeyExists( payload, 'exp' )
|
||||
&& !verifyDateClaim( payload.exp, claims.exp, -1 )
|
||||
) {
|
||||
throw(
|
||||
type = 'jwtcfml.ExpiredSignature',
|
||||
message = 'Token has expired',
|
||||
detail = 'The passed in token has expired.'
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
structKeyExists( payload, 'nbf' )
|
||||
&& !verifyDateClaim( payload.nbf, claims.nbf, 1 )
|
||||
) {
|
||||
throw(
|
||||
type = 'jwtcfml.NotBeforeException',
|
||||
message = 'Token is not valid',
|
||||
detail = 'The passed in token has not yet become valid.'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
if ( structKeyExists( claims, 'iss' ) ) {
|
||||
if ( !structKeyExists( payload, 'iss' ) || compare( payload.iss, claims.iss ) != 0 ) {
|
||||
throw(
|
||||
type = 'jwtcfml.InvalidIssuer',
|
||||
message = 'Token has an invalid issuer',
|
||||
detail = 'The passed in token either does not specify an issuer or the claimed issuer is not valid.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ( structKeyExists( claims, 'aud' ) ) {
|
||||
var audArray = isArray( claims.aud ) ? claims.aud : [ claims.aud ];
|
||||
if ( !structKeyExists( payload, 'aud' ) || !audArray.find( payload.aud ) ) {
|
||||
throw(
|
||||
type = 'jwtcfml.InvalidAudience',
|
||||
message = 'Token has an invalid audience',
|
||||
detail = 'The passed in token either does not specify an audience or the claimed audience is not valid.'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function verifyDateClaim( payloadDate, claim, failState ) {
|
||||
var pd = encodingUtils.convertUnixTimestampToDate( payloadDate );
|
||||
var cd = claim;
|
||||
if ( !isBoolean( cd ) || cd ) {
|
||||
if ( isNumeric( cd ) ) {
|
||||
cd = encodingUtils.convertUnixTimestampToDate( cd );
|
||||
} else if ( !isDate( cd ) ) {
|
||||
cd = now();
|
||||
}
|
||||
return dateCompare( pd, cd ) != failState;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<cfcomponent
|
||||
displayname="Notifier"
|
||||
output="true"
|
||||
hint="Notification helper, to be refactored">
|
||||
|
||||
<!---Внимание! необходимо зарегистрировать на сервере сертификат хоста maker.ifttt.com, иначе SSL соединение не установится и будет Connection Failure--->
|
||||
<!--- Нужно реализовывать нормальную систему с подпиской на события, по типу
|
||||
https://tonytruong.net/event-emitter-pattern-in-coldfusion/--->
|
||||
|
||||
<cffunction name="notify"
|
||||
access="public"
|
||||
returntype="any"
|
||||
output="false"
|
||||
hint="send notification">
|
||||
|
||||
<cfargument name="value1" type="string" required="true" />
|
||||
<cfargument name="value2" type="string" required="true" />
|
||||
<cfargument name="value3" type="string" required="true" />
|
||||
|
||||
<cftry>
|
||||
<cfhttp name="ifttt" method="post" url="https://maker.ifttt.com/trigger/mms_task_event/with/key/drWn5g6pNjyP_XRru0tMrQ" result="resp" timeout="10">
|
||||
<cfhttpparam type="header" name="Content-Type" value="application/json" />
|
||||
<cfhttpparam type="body" value="#serializeJSON(arguments)#">
|
||||
</cfhttp>
|
||||
<cfreturn resp.filecontent/>
|
||||
<cfcatch type="ANY">
|
||||
<cfreturn "#cfcatch.message# #cfcatch.detail#"/>
|
||||
</cfcatch>
|
||||
</cftry>
|
||||
|
||||
</cffunction>
|
||||
|
||||
</cfcomponent>
|
||||
@@ -0,0 +1,43 @@
|
||||
<cfsilent>
|
||||
<!--- build query sort string --->
|
||||
<!---v2 11:21 16.11.2020--->
|
||||
<!---v3 14:15 16.11.2020 input struct instead of array--->
|
||||
<!---v4 2024-10-15 input ANY--->
|
||||
|
||||
<cfparam name="ATTRIBUTES.sortCollection" type="any">
|
||||
<cfparam name="ATTRIBUTES.fieldCount" type="integer" default=0>
|
||||
|
||||
<cfif thisTag.executionMode IS "end" OR !thisTag.hasEndTag>
|
||||
|
||||
<cfset querySort="">
|
||||
<cfset i=1 />
|
||||
<cfloop collection=#ATTRIBUTES.sortCollection# index="item"><!--- *** Не уверен, что все будет хорошо, когда sortCollection - array, не перепутаются ли поля, порядок важен --->
|
||||
<cftry>
|
||||
<cfset sort=ATTRIBUTES.sortCollection[item] />
|
||||
<cfif (i LE ATTRIBUTES.fieldCount) OR (ATTRIBUTES.fieldCount LE 0)>
|
||||
<cfif sort.asc>
|
||||
<cfset querySort=listAppend(querySort," #sort.fld# asc")/>
|
||||
<cfelse>
|
||||
<cfset querySort=listAppend(querySort," #sort.fld# desc")/>
|
||||
</cfif>
|
||||
</cfif>
|
||||
<cfcatch type="Any">
|
||||
<cfrethrow/>
|
||||
</cfcatch>
|
||||
</cftry>
|
||||
<cfset i=i+1 />
|
||||
</cfloop>
|
||||
|
||||
<cfif len(trim(querySort)) EQ 0>
|
||||
<cfset querySort="1 desc">
|
||||
</cfif>
|
||||
|
||||
<cfset querySort=trim(querySort)>
|
||||
|
||||
<cfif isDefined("ATTRIBUTES.output")>
|
||||
<cfset "CALLER.#ATTRIBUTES.output#"=querySort/>
|
||||
<cfelse>
|
||||
<cfset thisTag.generatedContent=querySort/>
|
||||
</cfif>
|
||||
</cfif>
|
||||
</cfsilent>
|
||||
@@ -0,0 +1,391 @@
|
||||
<cfcomponent
|
||||
displayname="ReST API helper"
|
||||
output="true"
|
||||
hint="Static helper methods for ReST API">
|
||||
<!--- v0.02 2024-10-15 --->
|
||||
|
||||
|
||||
<cffunction name="empty2null"
|
||||
access="public"
|
||||
returntype="any"
|
||||
output="false"
|
||||
hint="replace empty field with null">
|
||||
|
||||
<cfargument name="value" type="any" required="true" />
|
||||
|
||||
<cfif isEmpty(ARGUMENTS.value)><!---*** некорректно, но просто, пустые строки превращает в нулл--->
|
||||
<cfreturn javacast('null','')/>
|
||||
<cfelse>
|
||||
<cfreturn ARGUMENTS.value />
|
||||
</cfif>
|
||||
</cffunction>
|
||||
|
||||
|
||||
<cffunction name="passThrough"
|
||||
returntype="any"
|
||||
output="false"
|
||||
hint="just return argument">
|
||||
<cfargument name="x" type="ANY" required="true"/>
|
||||
<cfreturn #ARGUMENTS.x#/>
|
||||
</cffunction>
|
||||
|
||||
|
||||
<cffunction name="appendRecord"
|
||||
access="public"
|
||||
returntype="any"
|
||||
output="false"
|
||||
hint="put data into struct, replace empty fields with null">
|
||||
|
||||
<cfargument name="struct" type="struct" required="true" />
|
||||
<cfargument name="key" type="string" required="true" />
|
||||
<cfargument name="fieldMap" type="struct" required="true" />
|
||||
<cfargument name="query" type="query" required="true" />
|
||||
<cfargument name="fieldNameDecorator" type="function" required="false" default=#passThrough#/>
|
||||
|
||||
<cfif len(ARGUMENTS.key)>
|
||||
<cfset ARGUMENTS.struct[ARGUMENTS.key]=structNew()/>
|
||||
<cfset var container=ARGUMENTS.struct[ARGUMENTS.key]/>
|
||||
<cfelse>
|
||||
<cfset var container=ARGUMENTS.struct/>
|
||||
</cfif>
|
||||
|
||||
<cfloop list=#structKeyList(ARGUMENTS.fieldMap)# index="local.col">
|
||||
<cfset var obj=container/>
|
||||
<cfset var formatter = structFind(ARGUMENTS.fieldMap[local.col], "formatter")/>
|
||||
<cfif structKeyExists(ARGUMENTS.fieldMap[#local.col#],"container")>
|
||||
<cfset var subContainerName = structFind(ARGUMENTS.fieldMap[local.col], "container")/>
|
||||
|
||||
<cfif len(subContainerName)>
|
||||
<cfif !structKeyExists(container, subContainerName)>
|
||||
<cfset structInsert(container, subContainerName, structNew())/>
|
||||
</cfif>
|
||||
<cfset obj=container[subContainerName]/>
|
||||
</cfif>
|
||||
</cfif>
|
||||
|
||||
<cfset obj[ARGUMENTS.fieldNameDecorator(local.col)]=empty2null(formatter(ARGUMENTS.query[local.col][ARGUMENTS.query.currentRow]))/>
|
||||
<!---*** не уверен в надежности конструкции во всех реализациях CF, контекст query передастся ли--->
|
||||
</cfloop>
|
||||
|
||||
<cfreturn ARGUMENTS.struct/>
|
||||
|
||||
</cffunction>
|
||||
|
||||
|
||||
<cffunction name="parseFilterParams"
|
||||
returntype="struct"
|
||||
output="true"
|
||||
hint="parse and collect filter params">
|
||||
<!---*** add test for multiple values--->
|
||||
<cfargument name="params" type="struct" required="true" />
|
||||
|
||||
<cfset var out=structNew()/>
|
||||
<cfloop collection=#params# index="local.item">
|
||||
|
||||
<cfset var urlParamName=snake2camel(lCase(local.item))/>
|
||||
<cfif structKeyExists(URL,urlParamName)>
|
||||
<cfset var rawValue=URL[urlParamName]/>
|
||||
<cfset var operator="EQ"/>
|
||||
<cfset var value=#rawValue#/>
|
||||
<cfif listLen(rawValue,":") GT 1>
|
||||
<cfset var operator=listGetAt(rawValue,1,":")/>
|
||||
<cfset var value=listGetAt(rawValue,2,":")/>
|
||||
</cfif>
|
||||
<cfif !isValid(ARGUMENTS.params[local.item].type,value)>
|
||||
<cfthrow type="invalidParamValue" message="Filter parameter #urlParamName# value is not a valid #ARGUMENTS.params[local.item].type#"/><!---не выводим значение, чтобы исключить XSS--->
|
||||
</cfif>
|
||||
|
||||
<cfif structKeyExists(ARGUMENTS.params[local.item], "prefix") AND len(ARGUMENTS.params[local.item].prefix)>
|
||||
<cfset var fieldName="#ARGUMENTS.params[local.item].prefix#.#local.item#"/>
|
||||
<cfelse>
|
||||
<cfset var fieldName=#local.item#/>
|
||||
</cfif>
|
||||
<cfset var rec={field=#fieldName#, val=#value#, ftype=#ARGUMENTS.params[local.item].type#, compare=#operator#}/><!---*** add list--->
|
||||
<cfset structInsert(out, local.item, rec, true)/>
|
||||
</cfif>
|
||||
|
||||
</cfloop>
|
||||
<cfreturn out/>
|
||||
</cffunction>
|
||||
|
||||
|
||||
<cffunction name="parseOrderBy"
|
||||
returntype="struct"
|
||||
output="false"
|
||||
hint="parse and collect sort order param"><!--- deprecated --->
|
||||
|
||||
<cfargument name="params" type="struct" required="true" />
|
||||
<cfargument name="orderBy" type="string" required="true" />
|
||||
|
||||
<cfset var out={}/>
|
||||
|
||||
<cfloop list=#ARGUMENTS.orderBy# index="local.item">
|
||||
<cfif listLen(local.item,".") GT 1>
|
||||
<cfset var fld=camel2snake(listGetAt(local.item,1,"."))/>
|
||||
<cfswitch expression=#uCase(listGetAt(local.item,2,"."))#>
|
||||
<cfcase value="ASC"><cfset var asc=true/></cfcase>
|
||||
<cfcase value="DESC"><cfset var asc=false/></cfcase>
|
||||
<cfdefaultcase>
|
||||
<cfthrow type="InvalidParamValue" message="Invalid orderBy format" detail="orderBy suffix should be '.asc' or '.desc'" /><!---*** XSS vulnerability, so we would not show the suffix--->
|
||||
</cfdefaultcase>
|
||||
</cfswitch>
|
||||
<cfelse>
|
||||
<cfset var fld=camel2snake(local.item)/>
|
||||
<cfset var asc=true/>
|
||||
</cfif>
|
||||
<cfif structKeyExists(ARGUMENTS.params, fld)>
|
||||
<cfset structInsert(out,"#ARGUMENTS.params[fld].prefix#.#fld#", {fld="#ARGUMENTS.params[fld].prefix#.#fld#",asc=#asc#}, true)/>
|
||||
<cfelse>
|
||||
<cfthrow type="InvalidParamValue" message="Invalid orderBy field"/>
|
||||
</cfif>
|
||||
</cfloop>
|
||||
<cfreturn out/>
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="parseNumericOrder"
|
||||
returntype="array"
|
||||
output="false"
|
||||
hint="parse and collect sort order param with numeric notation">
|
||||
|
||||
<cfargument name="fieldSet" type="struct" required="true" />
|
||||
<cfargument name="orderBy" type="string" required="true" />
|
||||
|
||||
<cfset var out=[]/>
|
||||
|
||||
<cfloop list=#ARGUMENTS.orderBy# index="local.item">
|
||||
<cfif listLen(local.item,".") GT 1>
|
||||
<cfset var fld=camel2snake(listGetAt(local.item,1,"."))/>
|
||||
<cfswitch expression=#uCase(listGetAt(local.item,2,"."))#>
|
||||
<cfcase value="ASC"><cfset var asc=true/></cfcase>
|
||||
<cfcase value="DESC"><cfset var asc=false/></cfcase>
|
||||
<cfdefaultcase>
|
||||
<cfthrow type="InvalidParamValue" message="Invalid orderBy format" detail="orderBy suffix should be '.asc' or '.desc'" /><!---*** XSS vulnerability, so we would not show the suffix--->
|
||||
</cfdefaultcase>
|
||||
</cfswitch>
|
||||
<cfelse>
|
||||
<cfset var fld=camel2snake(local.item)/>
|
||||
<cfset var asc=true/>
|
||||
</cfif>
|
||||
<cfif structKeyExists(ARGUMENTS.fieldSet, fld)>
|
||||
<cfset arrayAppend(out,{fld="#ARGUMENTS.fieldSet[fld].ordinal#",asc=#asc#})/>
|
||||
<cfelse>
|
||||
<cfthrow type="InvalidParamValue" message="Invalid orderBy field"/>
|
||||
</cfif>
|
||||
</cfloop>
|
||||
<cfreturn out/>
|
||||
</cffunction>
|
||||
|
||||
|
||||
<!---<cffunction name="snake2camel"
|
||||
access="public"
|
||||
returntype="any"
|
||||
output="false"
|
||||
hint="convert snake style name to camel style name">
|
||||
|
||||
<cfargument name="snake" type="string" required="true" />
|
||||
|
||||
<cfset var snake=ARGUMENTS.snake/>
|
||||
<cfset var camel=""/>
|
||||
<cfset var pos=1/>
|
||||
<cfloop condition="true">
|
||||
<cfset next=reFind("(_[a-z])", snake, pos, false)/>
|
||||
<cfif NOT next GT 0>
|
||||
<cfbreak/>
|
||||
</cfif>
|
||||
<cfset camel="#camel##mid(snake,pos,next-pos)##uCase(mid(snake,next+1,1))#"/>
|
||||
<cfset pos=next+2/>
|
||||
</cfloop>
|
||||
<cfset camel="#camel##mid(snake,pos,len(snake))#"/>
|
||||
|
||||
<cfreturn #camel#/>
|
||||
</cffunction>--->
|
||||
|
||||
<!---not used any more--->
|
||||
<cffunction name="query4json" access="public" returntype="any" output="false"
|
||||
hint="Converts query to an array of structs. Key names are lowercase. Empty fields are treated as nulls">
|
||||
|
||||
<cfargument name="Query" type="query" required="true" />
|
||||
|
||||
<cfset var out=arrayNew(1)/>
|
||||
<cfset var i=0/>
|
||||
|
||||
<cfloop query=ARGUMENTS.Query>
|
||||
<cfset i=i+1/>
|
||||
<cfset out[i]=structNew()/>
|
||||
<cfloop list=#ARGUMENTS.Query.ColumnList# index="local.col">
|
||||
<cfset var value=ARGUMENTS.Query[local.col][currentRow]/>
|
||||
<cfif isEmpty(value)><!---*** некорректно, но просто, пустые строки превращает в нулл--->
|
||||
<cfset out[i][local.col]=javacast('null','')/>
|
||||
<cfelse>
|
||||
<cfset out[i][local.col]=ARGUMENTS.Query[local.col][currentRow]/>
|
||||
</cfif>
|
||||
</cfloop>
|
||||
</cfloop>
|
||||
|
||||
<cfreturn(out)/>
|
||||
</cffunction>
|
||||
|
||||
|
||||
<cffunction name="snake2camel"
|
||||
access="public"
|
||||
returntype="any"
|
||||
output="false"
|
||||
hint="convert snake style name to camel style name">
|
||||
<cfargument name="snake" type="string" required="true" />
|
||||
<cfreturn #reReplace(ARGUMENTS.snake,"_([a-z])","\u\1","ALL")#/>
|
||||
</cffunction>
|
||||
|
||||
|
||||
<cffunction name="camel2snake"
|
||||
access="public"
|
||||
returntype="any"
|
||||
output="false"
|
||||
hint="convert camel style name to snake style name">
|
||||
<cfargument name="snake" type="string" required="true" />
|
||||
<cfreturn #reReplace(ARGUMENTS.snake,"([A-Z])","_\l\1","ALL")#/>
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="formatMessage"
|
||||
access="public"
|
||||
returntype="any"
|
||||
output="true"
|
||||
hint="formats Exception for display">
|
||||
|
||||
<cfargument name="message" type="string" required="true" />
|
||||
<cfargument name="title" type="string" required="false" />
|
||||
|
||||
<cfset var title=structKeyExists(ARGUMENTS,"title") ? #ARGUMENTS.title# : #ARGUMENTS.message# />
|
||||
|
||||
<cfreturn {type="about:blank", title="#title#", detail="#ARGUMENTS.message#"}/>
|
||||
</cffunction>
|
||||
|
||||
|
||||
<cffunction name="formatException"
|
||||
access="public"
|
||||
returntype="any"
|
||||
output="true"
|
||||
hint="formats Exception for display">
|
||||
|
||||
<cfargument name="ex" type="struct" required="true" />
|
||||
<cfargument name="title" type="string" required="false" />
|
||||
|
||||
<cfset var detail = listAppend(#ARGUMENTS.ex.message#,#ARGUMENTS.ex.detail#,":" )/>
|
||||
<cfset var title = structKeyExists(ARGUMENTS,"title") ? #ARGUMENTS.title# : #ARGUMENTS.message# />
|
||||
|
||||
<cfreturn formatMessage(detail, title) />
|
||||
</cffunction>
|
||||
|
||||
<cffunction name="formatBadRequestError"
|
||||
access="public"
|
||||
returntype="any"
|
||||
output="true"
|
||||
hint="formats Bad Request Exception">
|
||||
|
||||
<cfargument name="ex" type="struct" required="true" />
|
||||
|
||||
<cfset var detail=listAppend(#ARGUMENTS.ex.message#,#ARGUMENTS.ex.detail#,":" )/>
|
||||
|
||||
<cfreturn {type="about:blank", title="Bad Request", detail="#detail#"}/>
|
||||
</cffunction>
|
||||
|
||||
<!---<cffunction name="wrapResultSet"
|
||||
access="public"
|
||||
returntype="any"
|
||||
output="true"
|
||||
hint="format resultset as json">
|
||||
|
||||
<cfargument name="qRead" type="query" required="true" />
|
||||
<cfargument name="titleMap" type="struct" required="true" />
|
||||
<cfargument name="startrow" type="integer" required="false" default="1" />
|
||||
|
||||
<cfset var resultSet=[]/>
|
||||
<cfloop query=#ARGUMENTS.qRead# startRow=#ARGUMENTS.startrow#>
|
||||
<cfset var rec={}/>
|
||||
|
||||
<cfset appendRecord(rec, "", ARGUMENTS.titleMap, ARGUMENTS.qRead, snake2camel)/>
|
||||
<cfset arrayAppend(resultSet, rec)/>
|
||||
</cfloop>
|
||||
|
||||
<cfset var out={
|
||||
pageSize=
|
||||
}/>
|
||||
|
||||
|
||||
<cfcontent type="application/json"/>
|
||||
<cfoutput>{
|
||||
"pageSize":#pageSize#,
|
||||
"page":#page#,
|
||||
"orderBy":"#orderBy#",
|
||||
"size":"#arrayLen(resultSet)#",
|
||||
"total":"#qTotal.cnt#",
|
||||
"results":#serializeJson(resultSet)#,
|
||||
"queryDurationMs":#queryDurationMs#, <cfset runDurationMs=getTickCount()-request.startTickCount/>
|
||||
"runDurationMs":#runDurationMs#
|
||||
}</cfoutput>--->
|
||||
<cffunction name="validateField"
|
||||
access="public"
|
||||
returntype="any"
|
||||
output="false"
|
||||
hint="validate field of a structure, if exisits">
|
||||
|
||||
<cfargument name="struct" type="struct" required=true/>
|
||||
<cfargument name="name" required=true/>
|
||||
<cfargument name="type" default="string"/>
|
||||
<cfargument name="required" type="boolean" default="false"/>
|
||||
|
||||
<cfif structKeyExists(ARGUMENTS.struct, ARGUMENTS.name)>
|
||||
<cfif NOT isValid(ARGUMENTS.type, ARGUMENTS.struct[ARGUMENTS.name])>
|
||||
<cfthrow type="invalidParamValue" message="Parameter validation failed" detail="Supplied value of '#ARGUMENTS.name#' is not a valid #ARGUMENTS.type#"/>
|
||||
</cfif>
|
||||
<cfelse>
|
||||
<cfif ARGUMENTS.required>
|
||||
<cfthrow type="invalidParamValue" message="Required field not supplied" detail="Required field #ARGUMENTS.name# not found"/>
|
||||
</cfif>
|
||||
</cfif>
|
||||
</cffunction>
|
||||
|
||||
<!--- returns false if the key does not exist and throws exception on invalid format--->
|
||||
<cffunction name="keyExistsAndValid" access="public" returntype="boolean">
|
||||
<cfargument name="struct" type="struct" required=true/>
|
||||
<cfargument name="key" type="string" required=true/>
|
||||
<cfargument name="type" type="string" required=false/>
|
||||
<cfargument name="maxLength" type="numeric" required=false hint="Checked for strings only, negative value means no limit"/>
|
||||
|
||||
<cfif structKeyExists(arguments.struct, arguments.key)>
|
||||
<cfif structKeyExists(arguments, "type")>
|
||||
<cfset var x=arguments.struct[arguments.key]/>
|
||||
<cfif isValid(arguments.type, x)>
|
||||
<cfif lCase(arguments.type) EQ "string">
|
||||
<cfif structKeyExists(arguments, "maxLength")>
|
||||
<cfif ARGUMENTS.maxLength GE 0>
|
||||
<cfif len(x) GT #arguments.maxLength#>
|
||||
<cfthrow type="invalidParamValue" message="Invalid parameter value (string too long)" detail="Parameter #arguments.key# length #len(x)# is greater than #arguments.maxLength#" errorcode=400/>
|
||||
</cfif>
|
||||
</cfif>
|
||||
</cfif>
|
||||
</cfif>
|
||||
<cfelse>
|
||||
<cfthrow type="invalidParamValue" message="Invalid parameter value" detail="Parameter #arguments.key# is not a valid #arguments.type#" errorcode=400/>
|
||||
</cfif>
|
||||
</cfif>
|
||||
<cfreturn true/>
|
||||
<cfelse>
|
||||
<cfreturn false/>
|
||||
</cfif>
|
||||
</cffunction>
|
||||
|
||||
<!--- returns false if the key does not exist or has invalid format--->
|
||||
<cffunction name="keyExistsAndValidGraceful" access="public" returntype="boolean">
|
||||
<cfargument name="struct" type="struct" required=true/>
|
||||
<cfargument name="key" type="string" required=true/>
|
||||
<cfargument name="type" type="string" required=false/>
|
||||
<cfargument name="maxLength" type="numeric" required=false default="-1" hint="Checked for strings only"/>
|
||||
|
||||
<cftry>
|
||||
<cfreturn keyExistsAndValid(ARGUMENTS.struct, ARGUMENTS.key, ARGUMENTS.type, ARGUMENTS.maxLength)/>
|
||||
<cfcatch type="ANY"><!---<cfrethrow/>---></cfcatch>
|
||||
</cftry>
|
||||
<cfreturn false/>
|
||||
</cffunction>
|
||||
|
||||
</cfcomponent>
|
||||
Reference in New Issue
Block a user