From 6f503ce24bdd51df1f34d5ee605d635a34e67b1e Mon Sep 17 00:00:00 2001 From: James Mohler Date: Sat, 16 Jun 2018 10:59:02 -0700 Subject: [PATCH] Patch for 2018 Last version for Bootstrap 3.x --- application.cfc | 55 +- .../{backupsController.cfc => backups.cfc} | 0 ...ootswatchController.cfc => bootswatch.cfc} | 0 controllers/{docsController.cfc => docs.cfc} | 0 .../{loginController.cfc => login.cfc} | 0 controllers/{mainController.cfc => main.cfc} | 0 .../{pagesController.cfc => pages.cfc} | 0 .../{settingsController.cfc => settings.cfc} | 0 .../{supportController.cfc => support.cfc} | 0 .../{themeController.cfc => theme.cfc} | 0 .../{uploadController.cfc => upload.cfc} | 0 controllers/{wikiController.cfc => wiki.cfc} | 0 framework/ioc.cfc | 382 +++-- framework/one.cfc | 1262 ++++++++++++----- layouts/docs.cfm | 26 +- vendor/tags/bootstrap/bootstrap.cfc | 266 ++-- 16 files changed, 1367 insertions(+), 624 deletions(-) rename controllers/{backupsController.cfc => backups.cfc} (100%) rename controllers/{bootswatchController.cfc => bootswatch.cfc} (100%) rename controllers/{docsController.cfc => docs.cfc} (100%) rename controllers/{loginController.cfc => login.cfc} (100%) rename controllers/{mainController.cfc => main.cfc} (100%) rename controllers/{pagesController.cfc => pages.cfc} (100%) rename controllers/{settingsController.cfc => settings.cfc} (100%) rename controllers/{supportController.cfc => support.cfc} (100%) rename controllers/{themeController.cfc => theme.cfc} (100%) rename controllers/{uploadController.cfc => upload.cfc} (100%) rename controllers/{wikiController.cfc => wiki.cfc} (100%) mode change 100644 => 100755 framework/ioc.cfc mode change 100644 => 100755 framework/one.cfc diff --git a/application.cfc b/application.cfc index cd0b930..92628bb 100644 --- a/application.cfc +++ b/application.cfc @@ -4,7 +4,7 @@ component extends="framework.one" accessors="true" { -this.name="bs-4-cf-355"; +this.name="bs-4-cf-358"; this.applicationManagement = true; this.sessionManagement = true; @@ -17,7 +17,7 @@ variables.framework = { generateSES = true, SESomitIndex = false }; - + variables.framework.routes = [ // PlumaCMS { "backups/delete/:id" = "backups/delete/slug/:id"}, @@ -30,25 +30,25 @@ variables.framework.routes = [ { "logout" = "login/logout" }, { "pages/home" = "pages/home" }, { "wiki/:id" = "wiki/home/slug/:id" }, - + // documentation { "common" = "docs/common" }, { "theme" = "theme/home" }, { "bootswatch/:id" = "bootswatch/home/bootswatch/:id" } ]; - + function setupApplication() { - + application.initialized = now(); - + local.objAppFile = fileopen(expandpath('./Application.cfc'), 'read'); - - application.GSVERSION = "Version 3.3.5.#right(year(local.objAppFile.lastmodified), 2)#.#month(local.objAppFile.lastmodified)#.#day(local.objAppFile.lastmodified)#"; + + application.GSVERSION = "Version 3.3.7.#right(year(local.objAppFile.lastmodified), 2)#.#month(local.objAppFile.lastmodified)#.#day(local.objAppFile.lastmodified)#"; fileclose(objAppFile); @@ -56,7 +56,7 @@ fileclose(objAppFile); application.GSAUTHOR = "James Mohler and Web World Inc"; application.GSSITE_FULL_NAME = "PlumaCMS"; application.GSSITE_LINK_BACK_URL = "https://github.com/jmohler1970"; - + application.GSROOTPATH = getdirectoryfrompath(getBaseTemplatePath()); application.GSBACKUPSPATH = application.GSROOTPATH & "backups/"; application.GSDATAPATH = application.GSROOTPATH & "data/"; @@ -66,50 +66,49 @@ fileclose(objAppFile); application.GSDATAUPLOADPATH = application.GSROOTPATH & "data/uploads/"; application.GSUSERSPATH = application.GSROOTPATH & "data/users/"; // End Pluma - - + + // Support for complicated variables. This used to have to be in FW/1 application.objFormUtilities = new framework.formUtilities(); application.GSConfig = new model.services.settings().getWebsite(); invoke("vendor.tags.bootstrap.bootstrap", "setupApplication"); } // end setupApplication - - + + function setupSession() { - + session.authenticated = false; - + session.bootswatch = "default"; - } + } function setupRequest() { - + invoke("vendor.tags.bootstrap.bootstrap", "setupRequest"); } // end setupRequest - + function after() { - - - + + + if ( isDefined('form') ) rc.Append(application.objFormUtilities.buildFormCollections(form)); - - + + if(rc.keyExists("lang")) { application.lang = rc.lang; } - + if(rc.keyExists("bootswatch") and rc.bootswatch != "assets") { - + session.bootswatch = rc.bootswatch; } - - + + } } - diff --git a/controllers/backupsController.cfc b/controllers/backups.cfc similarity index 100% rename from controllers/backupsController.cfc rename to controllers/backups.cfc diff --git a/controllers/bootswatchController.cfc b/controllers/bootswatch.cfc similarity index 100% rename from controllers/bootswatchController.cfc rename to controllers/bootswatch.cfc diff --git a/controllers/docsController.cfc b/controllers/docs.cfc similarity index 100% rename from controllers/docsController.cfc rename to controllers/docs.cfc diff --git a/controllers/loginController.cfc b/controllers/login.cfc similarity index 100% rename from controllers/loginController.cfc rename to controllers/login.cfc diff --git a/controllers/mainController.cfc b/controllers/main.cfc similarity index 100% rename from controllers/mainController.cfc rename to controllers/main.cfc diff --git a/controllers/pagesController.cfc b/controllers/pages.cfc similarity index 100% rename from controllers/pagesController.cfc rename to controllers/pages.cfc diff --git a/controllers/settingsController.cfc b/controllers/settings.cfc similarity index 100% rename from controllers/settingsController.cfc rename to controllers/settings.cfc diff --git a/controllers/supportController.cfc b/controllers/support.cfc similarity index 100% rename from controllers/supportController.cfc rename to controllers/support.cfc diff --git a/controllers/themeController.cfc b/controllers/theme.cfc similarity index 100% rename from controllers/themeController.cfc rename to controllers/theme.cfc diff --git a/controllers/uploadController.cfc b/controllers/upload.cfc similarity index 100% rename from controllers/uploadController.cfc rename to controllers/upload.cfc diff --git a/controllers/wikiController.cfc b/controllers/wiki.cfc similarity index 100% rename from controllers/wikiController.cfc rename to controllers/wiki.cfc diff --git a/framework/ioc.cfc b/framework/ioc.cfc old mode 100644 new mode 100755 index 7ee33b2..767559e --- a/framework/ioc.cfc +++ b/framework/ioc.cfc @@ -1,8 +1,8 @@ component { - variables._fw1_version = "3.1.1"; - variables._di1_version = "1.1.0"; + variables._fw1_version = "4.2.0"; + variables._di1_version = variables._fw1_version; /* - Copyright (c) 2010-2015, Sean Corfield + Copyright (c) 2010-2018, Sean Corfield Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -19,16 +19,38 @@ component { // CONSTRUCTOR - public any function init( string folders, struct config = { } ) { - variables.folders = folders; + public any function init( any folders, struct config = { } ) { + variables.folderList = folders; + variables.folderArray = folders; + if ( isSimpleValue( folders ) ) { + variables.folderArray = listToArray( folders ); + } else { + variables.folderList = arrayToList( folders ); + } + var n = arrayLen( variables.folderArray ); + for ( var i = 1; i <= n; ++i ) { + var folderName = trim( variables.folderArray[ i ] ); + // strip trailing slash since it can cause weirdness in path + // deduction on some engines on some platforms (guess which!) + if ( len( folderName ) > 1 && + ( right( folderName, 1 ) == '/' || + right( folderName, 1 ) == chr(92) ) ) { + folderName = left( folderName, len( folderName ) - 1 ); + } + variables.folderArray[ i ] = folderName; + } variables.config = config; variables.beanInfo = { }; variables.beanCache = { }; variables.resolutionCache = { }; + variables.getBeanCache = { }; + variables.accumulatorCache = { }; variables.initMethodCache = { }; variables.settersInfo = { }; variables.autoExclude = [ '/WEB-INF', '/Application.cfc', // never manage these! + // assume default name for intermediary: + '/MyApplication.cfc', 'framework.cfc', 'ioc.cfc', // legacy FW/1 / DI/1 // recent FW/1 + DI/1 + AOP/1 exclusions: '/framework/aop.cfc', '/framework/beanProxy.cfc', @@ -47,7 +69,7 @@ component { // programmatically register an alias public any function addAlias( string aliasName, string beanName ) { - discoverBeans( variables.folders ); + discoverBeans(); // still need this since we rely on beanName having been discovered :( variables.beanInfo[ aliasName ] = variables.beanInfo[ beanName ]; return this; } @@ -55,7 +77,6 @@ component { // programmatically register new beans with the factory (add a singleton name/value pair) public any function addBean( string beanName, any beanValue ) { - discoverBeans( variables.folders ); variables.beanInfo[ beanName ] = { name = beanName, value = beanValue, isSingleton = true }; @@ -65,22 +86,95 @@ component { // return true if the factory (or a parent factory) knows about the requested bean public boolean function containsBean( string beanName ) { - discoverBeans( variables.folders ); + discoverBeans(); return structKeyExists( variables.beanInfo, beanName ) || - ( structKeyExists( variables, 'parent' ) && variables.parent.containsBean( beanName ) ); + ( hasParent() && variables.parent.containsBean( beanName ) ); + } + + + // builder syntax for declaring new beans + public any function declare( string beanName ) { + var declaration = { beanName : beanName, built : false }; + var beanFactory = this; // to make the builder functions less confusing + structAppend( declaration, { + // builder for addAlias() + aliasFor : function( string beanName ) { + if ( declaration.built ) throw "Declaration builder already completed!"; + declaration.built = true; + beanFactory.addAlias( declaration.beanName, beanName ); + return declaration; + }, + // builder for addBean() + asValue : function( any beanValue ) { + if ( declaration.built ) throw "Declaration builder already completed!"; + declaration.built = true; + beanFactory.addBean( declaration.beanName, beanValue ); + return declaration; + }, + // builder for factoryBean() + fromFactory : function( any factory, string methodName = "" ) { + if ( declaration.built ) throw "Declaration builder already completed!"; + declaration.built = true; + // use defaults -- we can override later + beanFactory.factoryBean( declaration.beanName, factory, methodName ); + return declaration; + }, + // builder for declareBean() + instanceOf : function( string dottedPath ) { + if ( declaration.built ) throw "Declaration builder already completed!"; + declaration.built = true; + // use defaults -- we can override later + beanFactory.declareBean( declaration.beanName, dottedPath ); + return declaration; + }, + // modifiers for metadata + asSingleton : function() { + if ( !declaration.built ) throw "No declaration builder to modify!"; + variables.beanInfo[ declaration.beanName ].isSingleton = true; + return declaration; + }, + asTransient : function() { + if ( !declaration.built ) throw "No declaration builder to modify!"; + variables.beanInfo[ declaration.beanName ].isSingleton = false; + return declaration; + }, + withArguments : function( array args ) { + if ( !declaration.built ) throw "No declaration builder to modify!"; + var info = variables.beanInfo[ declaration.beanName ]; + if ( !structKeyExists( info, 'factory' ) ) throw "withArguments() requires fromFactory()!"; + info.args = args; + return declaration; + }, + withOverrides : function( struct overrides ) { + if ( !declaration.built ) throw "No declaration builder to modify!"; + var info = variables.beanInfo[ declaration.beanName ]; + if ( !structKeyExists( info, 'factory' ) && + !structKeyExists( info, 'cfc' ) ) throw "withOverrides() requires fromFactory() or instanceOf()!"; + info.overrides = overrides; + return declaration; + }, + // to allow chaining + done : function() { + return beanFactory; + } + } ); + return declaration; } // programmatically register new beans with the factory (add an actual CFC) public any function declareBean( string beanName, string dottedPath, boolean isSingleton = true, struct overrides = { } ) { - discoverBeans( variables.folders ); var singleDir = ''; if ( listLen( dottedPath, '.' ) > 1 ) { var cfc = listLast( dottedPath, '.' ); var dottedPart = left( dottedPath, len( dottedPath ) - len( cfc ) - 1 ); singleDir = singular( listLast( dottedPart, '.' ) ); } - var cfcPath = replace( expandPath( '/' & replace( dottedPath, '.', '/', 'all' ) & '.cfc' ), chr(92), '/', 'all' ); + var basePath = replace( dottedPath, '.', '/', 'all' ); + var cfcPath = expandPath( '/' & basePath & '.cfc' ); + var expPath = cfcPath; + if ( !fileExists( expPath ) ) throw "Unable to find source file for #dottedPath#: expands to #cfcPath#"; + var cfcPath = replace( expPath, chr(92), '/', 'all' ); var metadata = { name = beanName, qualifier = singleDir, isSingleton = isSingleton, path = cfcPath, cfc = dottedPath, metadata = cleanMetadata( dottedPath ), @@ -90,8 +184,7 @@ component { return this; } - public any function factoryBean( string beanName, any factory, string methodName, array args = [ ], struct overrides = { } ) { - discoverBeans( variables.folders ); + public any function factoryBean( string beanName, any factory, string methodName = "", array args = [ ], struct overrides = { } ) { var metadata = { name = beanName, isSingleton = false, // really? factory = factory, method = methodName, args = args, @@ -103,33 +196,41 @@ component { // return the requested bean, fully populated - public any function getBean( string beanName ) { - discoverBeans( variables.folders ); + public any function getBean( string beanName, struct constructorArgs = { } ) { + discoverBeans(); if ( structKeyExists( variables.beanInfo, beanName ) ) { - return resolveBean( beanName ); - } else if ( structKeyExists( variables, 'parent' ) ) { - return variables.parent.getBean( beanName ); + if ( structKeyExists( variables.getBeanCache, beanName ) ) { + return variables.getBeanCache[ beanName ]; + } + var bean = resolveBean( beanName, constructorArgs ); + if ( isSingleton( beanName ) ) variables.getBeanCache[ beanName ] = bean; + return bean; + } else if ( hasParent() ) { + // ideally throw an exception for non-DI/1 parent when args passed + // WireBox adapter can do that since we control it but we can't do + // anything for other bean factories - will revisit before release + return variables.parent.getBean( beanName, constructorArgs ); } else { - throw 'bean not found: #beanName#'; + return missingBean( beanName = beanName, dependency = false ); } } // convenience API for metaprogramming perhaps? public any function getBeanInfo( string beanName = '', boolean flatten = false, string regex = '' ) { - discoverBeans( variables.folders ); + discoverBeans(); if ( len( beanName ) ) { // ask about a specific bean: if ( structKeyExists( variables.beanInfo, beanName ) ) { return variables.beanInfo[ beanName ]; } - if ( structKeyExists( variables, 'parent' ) ) { + if ( hasParent() ) { return parentBeanInfo( beanName ); } throw 'bean not found: #beanName#'; } else { var result = { beanInfo = { } }; - if ( structKeyExists( variables, 'parent' ) ) { + if ( hasParent() ) { if ( flatten || len( regex ) ) { structAppend( result.beanInfo, parentBeanInfoList( flatten ).beanInfo ); structAppend( result.beanInfo, variables.beanInfo ); @@ -154,18 +255,31 @@ component { } + // return a copy of the DI/1 configuration + public struct function getConfig() { + // note: we only make a shallow copy + return structCopy( variables.config ); + } + + // return the DI/1 version public string function getVersion() { return variables.config.version; } + // return true if this factory has a parent + public boolean function hasParent() { + return structKeyExists( variables, 'parent' ); + } + + // return true iff bean is known to be a singleton public boolean function isSingleton( string beanName ) { - discoverBeans( variables.folders ); + discoverBeans(); if ( structKeyExists( variables.beanInfo, beanName ) ) { return variables.beanInfo[ beanName ].isSingleton; - } else if ( structKeyExists( variables, 'parent' ) ) { + } else if ( hasParent() ) { try { return variables.parent.isSingleton( beanName ); } catch ( any e ) { @@ -188,7 +302,7 @@ component { if ( !isNull( properties[ property ] ) ) { var args = { }; args[ property ] = properties[ property ]; - evaluate( 'bean.set#property#( argumentCollection = args )' ); + invoke( bean, "set#property#", args ); } } return bean; @@ -201,12 +315,18 @@ component { // if you reload the parent, you must reload *all* child factories to ensure // things stay consistent!) public any function load() { - discoverBeans( variables.folders ); + discoverBeans(); variables.beanCache = { }; variables.resolutionCache = { }; + variables.accumulatorCache = { }; + variables.getBeanCache = { }; variables.initMethodCache = { }; for ( var key in variables.beanInfo ) { - if ( variables.beanInfo[ key ].isSingleton ) getBean( key ); + if ( !structKeyExists( variables.beanInfo[ key ], "isSingleton" ) ) + throw "internal error: bean #key# has no isSingleton flag!"; + if ( variables.beanInfo[ key ].isSingleton ) { + getBean( key ); + } } return this; } @@ -284,8 +404,12 @@ component { ( !structKeyExists( property, 'setter' ) || isBoolean( property.setter ) && property.setter ) ) { if ( structKeyExists( property, 'type' ) && - property.type != 'any' ) { - iocMeta.setters[ property.name ] = 'typed'; + property.type != 'any' && + variables.config.omitTypedProperties ) { + iocMeta.setters[ property.name ] = 'ignored'; + } else if ( structKeyExists( property, 'default' ) && + variables.config.omitDefaultedProperties ) { + iocMeta.setters[ property.name ] = 'ignored'; } else { iocMeta.setters[ property.name ] = 'implicit'; } @@ -333,16 +457,30 @@ component { } catch ( any e ) { var except = "Unable to getComponentMetadata(#dottedPath#) because: " & e.message & ( len( e.detail ) ? " (#e.detail#)" : "" ); + try { + except = except & ", near line " & e.tagContext[1].line & + " in " & e.tagContext[1].template; + } catch ( any e ) { + // unable to determine template / line number so just use + // the exception message we built so far + } throw except; } } private string function deduceDottedPath( string baseMapping, string basePath ) { + if ( right( basePath, 1 ) == '/' && len( basePath ) > 1 ) { + basePath = left( basePath, len( basePath ) - 1 ); + } var cfcPath = left( baseMapping, 1 ) == '/' ? ( len( baseMapping ) > 1 ? right( baseMapping, len( baseMapping ) - 1 ) : '' ) : getFileFromPath( baseMapping ); + if ( right( cfcPath, 1 ) == '/' && len( cfcPath ) > 1 ) { + cfcPath = left( cfcPath, len( cfcPath ) - 1 ); + } var expPath = basePath; + var notFound = true; var dotted = ''; do { var mapped = cfcPath; @@ -350,6 +488,7 @@ component { var mappedPath = replace( expandpath( mapped ), chr(92), '/', 'all' ); if ( mappedPath == basePath ) { dotted = replace( cfcPath, '/', '.', 'all' ); + notFound = false; break; } var prevPath = expPath; @@ -361,23 +500,25 @@ component { var piece = listLast( expPath, '/' ); cfcPath = piece & '/' & cfcPath; } while ( progress ); - if ( dotted == '' ) { + if ( notFound ) { throw 'unable to deduce dot-relative path for: #baseMapping# (#basePath#) root #expandPath("/")#'; } return dotted; } - private void function discoverBeans( string folders ) { + private void function discoverBeans() { if ( structKeyExists( variables, 'discoveryComplete' ) ) return; - lock name="#application.applicationName#_ioc1_#folders#" type="exclusive" timeout="30" { + lock name="#application.applicationName#_ioc1_#variables.folderList#" type="exclusive" timeout="30" { if ( structKeyExists( variables, 'discoveryComplete' ) ) return; - var folderArray = listToArray( folders ); variables.pathMapCache = { }; - for ( var f in folderArray ) { - discoverBeansInFolder( replace( trim( f ), chr(92), '/', 'all' ) ); + try { + for ( var f in variables.folderArray ) { + discoverBeansInFolder( replace( f, chr(92), '/', 'all' ) ); + } + } finally { + variables.discoveryComplete = true; } - variables.discoveryComplete = true; } onLoadEvent(); } @@ -404,29 +545,40 @@ component { } if ( excludePath ) continue; var relPath = right( cfcPath, len( cfcPath ) - len( folder ) ); - relPath = left( relPath, len( relPath ) - 4 ); + var extN = 1 + len( listLast( cfcPath, "." ) ); + relPath = left( relPath, len( relPath ) - extN ); var dir = listLast( getDirectoryFromPath( cfcPath ), '/' ); var singleDir = singular( dir ); var beanName = listLast( relPath, '/' ); var dottedPath = dotted & replace( relPath, '/', '.', 'all' ); - var metadata = { - name = beanName, qualifier = singleDir, isSingleton = !beanIsTransient( singleDir, dir, beanName ), - path = cfcPath, cfc = dottedPath, metadata = cleanMetadata( dottedPath ) - }; - if ( structKeyExists( metadata.metadata, "type" ) && metadata.metadata.type == "interface" ) { - continue; - } - if ( structKeyExists( variables.beanInfo, beanName ) ) { - if ( variables.config.omitDirectoryAliases ) { - throw '#beanName# is not unique (and omitDirectoryAliases is true)'; + try { + var metadata = { + name = beanName, qualifier = singleDir, isSingleton = !beanIsTransient( singleDir, dir, beanName ), + path = cfcPath, cfc = dottedPath, metadata = cleanMetadata( dottedPath ) + }; + if ( structKeyExists( metadata.metadata, "type" ) && metadata.metadata.type == "interface" ) { + continue; } - structDelete( variables.beanInfo, beanName ); - variables.beanInfo[ beanName & singleDir ] = metadata; - } else { - variables.beanInfo[ beanName ] = metadata; - if ( !variables.config.omitDirectoryAliases ) { + if ( structKeyExists( variables.beanInfo, beanName ) ) { + if ( variables.config.omitDirectoryAliases ) { + throw '#beanName# is not unique (and omitDirectoryAliases is true)'; + } + structDelete( variables.beanInfo, beanName ); variables.beanInfo[ beanName & singleDir ] = metadata; + } else { + variables.beanInfo[ beanName ] = metadata; + if ( !variables.config.omitDirectoryAliases ) { + variables.beanInfo[ beanName & singleDir ] = metadata; + } } + } catch ( any e ) { + // wrap the exception so we can add bean name for debugging + // this trades off any stack trace information for the bean name but + // since we are only trying to get metadata, the latter should be + // more useful than the former + var except = "Problem with metadata for #beanName# (#dottedPath#) because: " & + e.message & ( len( e.detail ) ? " (#e.detail#)" : "" ); + throw except; } } } @@ -436,11 +588,15 @@ component { var liveMeta = { setters = iocMeta.setters }; if ( !iocMeta.pruned ) { // need to prune known setters of transients: + var prunable = { }; for ( var known in iocMeta.setters ) { if ( !isSingleton( known ) ) { - structDelete( iocMeta.setters, known ); + prunable[ known ] = true; } } + for ( known in prunable ) { + structDelete( iocMeta.setters, known ); + } iocMeta.pruned = true; } // gather up explicit setters: @@ -491,8 +647,12 @@ component { } - private void function missingBean( string beanName, string resolvingBeanName = '' ) { - if ( variables.config.strict ) { + /* + * override this if you want to add a convention-based bean factory hook, that returns + * beans instead of throwing an exception + */ + private any function missingBean( string beanName, string resolvingBeanName = '', boolean dependency = true ) { + if ( variables.config.strict || !dependency ) { if ( len( resolvingBeanName ) ) { throw 'bean not found: #beanName#; while resolving #resolvingBeanName#'; } else { @@ -507,9 +667,7 @@ component { private void function onLoadEvent() { var head = variables.listeners; while ( isStruct( head ) ) { - if ( isCustomFunction( head.listener ) || - ( listFirst( server.coldfusion.productVersion ) >= 10 && - isClosure( head.listener ) ) ) { + if ( isCustomFunction( head.listener ) || isClosure( head.listener ) ) { head.listener( this ); } else if ( isObject( head.listener ) ) { head.listener.onLoad( this ); @@ -554,11 +712,20 @@ component { } - private any function resolveBean( string beanName ) { + private any function resolveBean( string beanName, struct constructorArgs = { } ) { // do enough resolution to create and initialization this bean // returns a struct of the bean and a struct of beans and setters still to run // construction phase: - var partialBean = resolveBeanCreate( beanName, { injection = { }, dependencies = { } } ); + var accumulator = { injection = { } }; + // ensure only injection singletons end up cached in variables scope + if ( structKeyExists( variables.accumulatorCache, beanName ) ) { + structAppend( accumulator.injection, variables.accumulatorCache[ beanName ].injection ); + } else { + variables.accumulatorCache[ beanName ] = { injection = { }, dependencies = { } }; + } + // all dependencies can be cached in variables scope + accumulator.dependencies = variables.accumulatorCache[ beanName ].dependencies; + var partialBean = resolveBeanCreate( beanName, accumulator, constructorArgs ); if ( structKeyExists( variables.resolutionCache, beanName ) && variables.resolutionCache[ beanName ] ) { // fully resolved, no action needed this time @@ -569,14 +736,17 @@ component { // injection phase: // now perform all of the injection: for ( var name in partialBean.injection ) { + if ( structKeyExists( variables.accumulatorCache[ beanName ].injection, name ) ) { + // this singleton is in the accumulatorCache, thus it has already been fully resolved + continue; + } var injection = partialBean.injection[ name ]; if ( checkForPostInjection && !isConstant( name ) && structKeyExists( injection.bean, initMethod ) ) { postInjectables[ name ] = true; } for ( var property in injection.setters ) { - if ( injection.setters[ property ] == 'typed' && - variables.config.omitTypedProperties ) { - // we do not inject typed properties! + if ( injection.setters[ property ] == 'ignored' ) { + // do not inject defaulted/typed properties! continue; } var args = { }; @@ -584,13 +754,15 @@ component { args[ property ] = injection.overrides[ property ]; } else if ( structKeyExists( partialBean.injection, property ) ) { args[ property ] = partialBean.injection[ property ].bean; - } else if ( structKeyExists( variables, 'parent' ) && variables.parent.containsBean( property ) ) { + } else if ( hasParent() && variables.parent.containsBean( property ) ) { args[ property ] = variables.parent.getBean( property ); } else { - missingBean( property, beanName ); - continue; + // allow for possible convention-based bean factory + args[ property ] = missingBean( property, beanName ); + // isNull() does not always work on ACF10... + try { if ( isNull( args[ property ] ) ) continue; } catch ( any e ) { continue; } } - evaluate( 'injection.bean.set#property#( argumentCollection = args )' ); + invoke( injection.bean, "set#property#", args ); } } // post-injection, pre-init-method phase: @@ -602,6 +774,11 @@ component { for ( var postName in postInjectables ) { callInitMethod( postName, postInjectables, partialBean, initMethod ); } + for ( name in partialBean.injection ) { + if ( isSingleton( name ) ) { + variables.accumulatorCache[ beanName ].injection[ name ] = partialBean.injection[ name ]; + } + } variables.resolutionCache[ beanName ] = isSingleton( beanName ); } return partialBean.bean; @@ -626,21 +803,32 @@ component { } else { variables.initMethodCache[ name ] = isSingleton( name ); var bean = info.injection[ name ].bean; - evaluate( 'bean.#method#()' ); + invoke( bean, method ); } } } - private struct function resolveBeanCreate( string beanName, struct accumulator ) { + private struct function resolveBeanCreate( string beanName, struct accumulator, struct constructorArgs = { } ) { var bean = 0; if ( structKeyExists( variables.beanInfo, beanName ) ) { var info = variables.beanInfo[ beanName ]; - accumulator.dependencies[ beanName ] = { }; + if ( !structKeyExists( accumulator.dependencies, beanName ) ) accumulator.dependencies[ beanName ] = { }; if ( structKeyExists( info, 'cfc' ) ) { /*******************************************************/ var metaBean = cachable( beanName ); - var overrides = structKeyExists( info, 'overrides' ) ? info.overrides : { }; + var overrides = { }; + // be careful not to modify overrides metadata: + if ( structCount( constructorArgs ) ) { + if ( structKeyExists( info, 'overrides' ) ) { + structAppend( overrides, info.overrides ); + } + structAppend( overrides, constructorArgs ); + } else { + if ( structKeyExists( info, 'overrides' ) ) { + overrides = info.overrides; + } + } bean = metaBean.bean; if ( metaBean.newObject ) { if ( structKeyExists( info.metadata, 'constructor' ) ) { @@ -678,7 +866,7 @@ component { } } } - var __ioc_newBean = evaluate( 'bean.init( argumentCollection = args )' ); + var __ioc_newBean = bean.init( argumentCollection = args ); // if the constructor returns anything, it becomes the bean // this allows for smart constructors that return things other // than the CFC being created, such as implicit factory beans @@ -709,9 +897,8 @@ component { } } } - accumulator.bean = bean; } else if ( isConstant( beanName ) ) { - accumulator.bean = info.value; + bean = info.value; accumulator.injection[ beanName ] = { bean = info.value, setters = { } }; } else if ( structKeyExists( info, 'factory' ) ) { var fmBean = isSimpleValue( info.factory ) ? this.getBean( info.factory ) : info.factory; @@ -725,19 +912,30 @@ component { argStruct[ i ] = this.getBean( argName ); } } - accumulator.bean = evaluate( 'fmBean.#info.method#(argumentCollection=argStruct)' ); - accumulator.injection[ beanName ] = { bean = accumulator.bean, setters = { } }; + if ( isCustomFunction( fmBean ) || isClosure( fmBean ) ) { + bean = fmBean( argumentCollection = argStruct ); + } else { + bean = invoke( fmBean, "#info.method#", argStruct ); + } + accumulator.injection[ beanName ] = { bean = bean, setters = { } }; } else { throw 'internal error: invalid metadata for #beanName#'; } - } else if ( structKeyExists( variables, 'parent' ) && variables.parent.containsBean( beanName ) ) { - bean = variables.parent.getBean( beanName ); - accumulator.injection[ beanName ] = { bean = bean, setters = { } }; - accumulator.bean = bean; } else { - missingBean( beanName ); + if ( hasParent() && variables.parent.containsBean( beanName ) ) { + bean = variables.parent.getBean( beanName ); + } else { + bean = missingBean( beanName = beanName, dependency = true ); + } + if ( !isNull( bean ) ) { + accumulator.injection[ beanName ] = { bean = bean, setters = { } }; + } } - return accumulator; + return { + bean = bean, + injection = accumulator.injection, + dependencies = accumulator.dependencies + }; } @@ -772,12 +970,21 @@ component { throw 'singletonPattern and transientPattern are mutually exclusive'; } + if ( !structKeyExists( variables.config, 'omitDefaultedProperties' ) ) { + variables.config.omitDefaultedProperties = true; + } if ( !structKeyExists( variables.config, 'omitTypedProperties' ) ) { - variables.config.omitTypedProperties = false; + variables.config.omitTypedProperties = true; } if ( !structKeyExists( variables.config, 'omitDirectoryAliases' ) ) { variables.config.omitDirectoryAliases = false; } + if ( !structKeyExists( variables.config, 'singulars' ) ) { + variables.config.singulars = { }; + } + if ( !structKeyExists( variables.config, 'liberal' ) ) { + variables.config.liberal = false; + } variables.config.version = variables._di1_version; } @@ -790,15 +997,18 @@ component { private string function singular( string plural ) { - if ( structKeyExists( variables.config, 'singulars' ) && - structKeyExists( variables.config.singulars, plural ) ) { + if ( structKeyExists( variables.config.singulars, plural ) ) { return variables.config.singulars[ plural ]; } var single = plural; var n = len( plural ); var last = right( plural, 1 ); if ( last == 's' ) { - single = left( plural, n - 1 ); + if ( variables.config.liberal && n > 3 && right( plural, 3 ) == 'ies' ) { + single = left( plural, n - 3 ) & 'y'; + } else { + single = left( plural, n - 1 ); + } } return single; } diff --git a/framework/one.cfc b/framework/one.cfc old mode 100644 new mode 100755 index 47fcbc9..f2354f5 --- a/framework/one.cfc +++ b/framework/one.cfc @@ -1,34 +1,37 @@ component { - variables._fw1_version = "3.1.1"; -/* - Copyright (c) 2009-2015, Sean Corfield, Marcin Szczepanski, Ryan Cogswell + variables._fw1_version = "4.2.0"; + /* + Copyright (c) 2009-2018, Sean Corfield, Marcin Szczepanski, Ryan Cogswell - Licensed 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 + Licensed 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 + 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. -*/ + 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. + */ this.name = hash( getBaseTemplatePath() ); - request._fw1 = { - cgiScriptName = CGI.SCRIPT_NAME, - cgiPathInfo = CGI.PATH_INFO, - cgiRequestMethod = CGI.REQUEST_METHOD, - controllers = [ ], - requestDefaultsInitialized = false, - doTrace = false, - trace = [ ] - }; - if ( len( getContextRoot() ) ) { - request._fw1.cgiScriptName = replace( CGI.SCRIPT_NAME, getContextRoot(), '' ); - request._fw1.cgiPathInfo = replace( CGI.PATH_INFO, getContextRoot(), '' ); + if ( !structKeyExists( request, '_fw1' ) ) { + request._fw1 = { + cgiScriptName = CGI.SCRIPT_NAME, + cgiPathInfo = CGI.PATH_INFO, + cgiRequestMethod = CGI.REQUEST_METHOD, + controllers = [ ], + requestDefaultsInitialized = false, + routeMethodsMatched = { }, + doTrace = false, + trace = [ ] + }; + if ( len( getContextRoot() ) ) { + request._fw1.cgiScriptName = replace( CGI.SCRIPT_NAME, getContextRoot(), '' ); + request._fw1.cgiPathInfo = replace( CGI.PATH_INFO, getContextRoot(), '' ); + } } // do not rely on these, they are meant to be true magic... variables.magicApplicationSubsystem = ']['; @@ -36,6 +39,16 @@ component { variables.magicApplicationAction = '__'; variables.magicBaseURL = '-[]-'; + // constructor if not extended via Application.cfc + + public any function init( struct config = { } ) { + if ( !structKeyExists( variables, 'framework' ) ) { + variables.framework = { }; + } + structAppend( variables.framework, config ); + return this; + } + public void function abortController() { request._fw1.abortController = true; internalFrameworkTrace( 'abortController() called' ); @@ -43,12 +56,7 @@ component { } public boolean function actionSpecifiesSubsystem( string action ) { - - if ( !usingSubsystems() ) { - return false; - } - return listLen( action, variables.framework.subsystemDelimiter ) > 1 || - right( action, 1 ) == variables.framework.subsystemDelimiter; + return find( variables.framework.subsystemDelimiter, action ); } public void function addRoute( any routes, string target, any methods = [ ], string statusCode = '' ) { @@ -98,30 +106,20 @@ component { */ public string function buildURL( string action = '.', string path = variables.magicBaseURL, any queryString = '' ) { if ( action == '.' ) { - action = getFullyQualifiedAction(); + action = getSubsystemSectionAndItem(); } else if ( left( action, 2 ) == '.?' ) { - action = replace( action, '.', getFullyQualifiedAction() ); + action = replace( action, '.', getSubsystemSectionAndItem() ); } var pathData = resolveBaseURL( action, path ); path = pathData.path; var omitIndex = pathData.omitIndex; - // if queryString is a struct, massage it into a string - if ( isStruct( queryString ) && structCount( queryString ) ) { - var q = ''; - for( var key in queryString ) { - if ( isSimpleValue( queryString[key] ) ) { - q &= '#urlEncodedFormat( key )#=#urlEncodedFormat( queryString[ key ] )#&'; - } - } - queryString = q; - } - else if ( !isSimpleValue( queryString ) ) { - queryString = ''; - } + queryString = normalizeQueryString( queryString ); + var q = 0; + var a = 0; if ( queryString == '' ) { // extract query string from action section: - var q = find( '?', action ); - var a = find( '##', action ); + q = find( '?', action ); + a = find( '##', action ); if ( q > 0 ) { if ( q < len( action ) ) { queryString = right( action, len( action ) - q ); @@ -140,8 +138,8 @@ component { } } } - var cosmeticAction = getFullyQualifiedAction( action ); - var isHomeAction = cosmeticAction == getFullyQualifiedAction( variables.framework.home ); + var cosmeticAction = getSubsystemSectionAndItem( action ); + var isHomeAction = cosmeticAction == getSubsystemSectionAndItem( variables.framework.home ); var isDefaultItem = getItem( cosmeticAction ) == variables.framework.defaultItem; var initialDelim = '?'; @@ -170,7 +168,7 @@ component { } var curDelim = varDelim; - if ( usingSubsystems() && getSubsystem( cosmeticAction ) == variables.framework.defaultSubsystem ) { + if ( getSubsystem( cosmeticAction ) == variables.framework.defaultSubsystem ) { cosmeticAction = getSectionAndItem( cosmeticAction ); } @@ -255,8 +253,8 @@ component { var tuple = { }; if ( structKeyExists( request._fw1, 'controllerExecutionStarted' ) ) { - raiseException( type='FW1.controllerExecutionStarted', message="Controller '#action#' may not be added at this point.", - detail='The controller execution phase has already started. Controllers may not be added by other controller methods.' ); + throw( type='FW1.controllerExecutionStarted', message="Controller '#action#' may not be added at this point.", + detail='The controller execution phase has already started. Controllers may not be added by other controller methods.' ); } tuple.controller = getController( section = section, subsystem = subsystem ); @@ -271,6 +269,14 @@ component { } } + /* + * can be overridden to customize how views and layouts are actually + * rendered; should return null if the default rendering should apply + */ + public any function customTemplateEngine( string type, string path, struct scope ) { + return; + } + /* * can be overridden to customize how views and layouts are found - can be * used to provide skinning / common views / layouts etc @@ -311,10 +317,9 @@ component { public void function frameworkTrace( string message ) { if ( request._fw1.doTrace ) { try { - if ( isDefined( 'session._fw1_trace' ) && - structKeyExists( session, '_fw1_trace' ) ) { - request._fw1.trace = session._fw1_trace; - structDelete( session, '_fw1_trace' ); + if ( sessionHas( '_fw1_trace' ) ) { + request._fw1.trace = sessionRead( '_fw1_trace' ); + sessionDelete( '_fw1_trace' ); } } catch ( any _ ) { // ignore if session is not enabled @@ -356,9 +361,6 @@ component { } return getDefaultBeanFactory(); } - if ( !usingSubsystems() ) { - return getDefaultBeanFactory(); - } if ( structKeyExists( request, 'subsystem' ) && len( request.subsystem ) > 0 ) { return getBeanFactory( request.subsystem ); } @@ -398,8 +400,8 @@ component { } if ( variables.framework.defaultSubsystem == '' ) { - raiseException( type='FW1.subsystemNotSpecified', message='No subsystem specified and no default configured.', - detail='When using subsystems, every request should specify a subsystem or variables.framework.defaultSubsystem should be configured.' ); + throw( type='FW1.subsystemNotSpecified', message='No subsystem specified and no default configured.', + detail='When using subsystems, every request should specify a subsystem or variables.framework.defaultSubsystem should be configured.' ); } return variables.framework.defaultSubsystem; @@ -434,13 +436,30 @@ component { /* * return an action with all applicable parts (subsystem, section, and item) specified * using defaults from the configuration or request where appropriate + * if the subsystem is empty, do _not_ include the delimiter - compare this behavior + * with getSubsystemSectionAndItem() below */ public string function getFullyQualifiedAction( string action = request.action ) { - if ( usingSubsystems() ) { - return getSubsystem( action ) & variables.framework.subsystemDelimiter & getSectionAndItem( action ); + var requested = getSubsystem( action ); + if ( len( requested ) ) { + // request specifies non-empty subsystem, use it as-is: + return requested & variables.framework.subsystemDelimiter & getSectionAndItem( action ); + } else { + var current = structKeyExists( request, 'action' ) ? getSubsystem( request.action ) : ''; + if ( len( current ) ) { + // request has no subsystem but we're in one, special case + if ( actionSpecifiesSubsystem( action ) ) { + // request had explicit empty subsystem so it means top-level app + return variables.framework.subsystemDelimiter & getSectionAndItem( action ); + } else { + // request was meant to be relative to current subsystem + return current & variables.framework.subsystemDelimiter & getSectionAndItem( action ); + } + } else { + // neither appears to have subsystem, just use section and item + return getSectionAndItem( action ); + } } - - return getSectionAndItem( action ); } /* @@ -457,14 +476,28 @@ component { return listLast( getSectionAndItem( action ), '.' ); } + /* + * return this request's CGI method + */ + public string function getCGIRequestMethod() { + return request._fw1.cgiRequestMethod; + } /* * return the current route (if any) + * this is the raw, matched route that we mapped */ public string function getRoute() { return structKeyExists( request._fw1, 'route' ) ? request._fw1.route : ''; } + /* + * return the part of the pathinfo that was used as the route + * prefixed by the HTTP method + */ + public string function getRoutePath() { + return '$' & request._fw1.cgiRequestMethod & request._fw1.currentRoute; + } /* * return the configured routes @@ -493,11 +526,8 @@ component { */ public string function getSectionAndItem( string action = request.action ) { var sectionAndItem = ''; - - if ( usingSubsystems() && actionSpecifiesSubsystem( action ) ) { - if ( listLen( action, variables.framework.subsystemDelimiter ) > 1 ) { - sectionAndItem = listLast( action, variables.framework.subsystemDelimiter ); - } + if ( actionSpecifiesSubsystem( action ) ) { + sectionAndItem = segmentLast( action, variables.framework.subsystemDelimiter ); } else { sectionAndItem = action; } @@ -527,7 +557,7 @@ component { */ public string function getSubsystem( string action = request.action ) { if ( actionSpecifiesSubsystem( action ) ) { - return listFirst( action, variables.framework.subsystemDelimiter ); + return segmentFirst( action, variables.framework.subsystemDelimiter ); } return getDefaultSubsystem(); } @@ -562,6 +592,25 @@ component { return { }; } + /* + * return an action with all applicable parts (subsystem, section, and item) specified + * using defaults from the configuration or request where appropriate + * differs from getFullyQualifiedAction() in that it will _always_ contain the + * subsystem delimiter, even when the subsystem is blank + */ + public string function getSubsystemSectionAndItem( string action = request.action ) { + if ( actionSpecifiesSubsystem( action ) ) { + return getSubsystem( action ) & variables.framework.subsystemDelimiter & getSectionAndItem( action ); + } else { + var current = structKeyExists( request, 'action' ) ? getSubsystem( request.action ) : ''; + if ( len( current ) ) { + return current & variables.framework.subsystemDelimiter & getSectionAndItem( action ); + } else { + return variables.framework.subsystemDelimiter & getSectionAndItem( action ); + } + } + } + /* * returns true iff a call to getBeanFactory() will successfully return a bean factory * previously set via setBeanFactory or setSubsystemBeanFactory @@ -572,16 +621,12 @@ component { return true; } - if ( !usingSubsystems() ) { - return false; - } - if ( structKeyExists( request, 'subsystem' ) ) { - return hasSubsystemBeanFactory(request.subsystem); + return hasSubsystemBeanFactory( request.subsystem ); } - if ( len(variables.framework.defaultSubsystem) > 0 ) { - return hasSubsystemBeanFactory(variables.framework.defaultSubsystem); + if ( len( variables.framework.defaultSubsystem ) > 0 ) { + return hasSubsystemBeanFactory( variables.framework.defaultSubsystem ); } return false; @@ -600,7 +645,8 @@ component { */ public boolean function hasSubsystemBeanFactory( string subsystem ) { - ensureNewFrameworkStructsExist(); + if ( !len( subsystem ) ) return false; + setupSubsystemWrapper( subsystem ); return structKeyExists( getFw1App().subsystemFactories, subsystem ); @@ -611,8 +657,19 @@ component { * executing action (after both have been expanded) */ public boolean function isCurrentAction( string action ) { - return getFullyQualifiedAction( action ) == - getFullyQualifiedAction(); + return getSubsystemSectionAndItem( action ) == + getSubsystemSectionAndItem(); + } + + /* + * returns true if this request has a valid reload URL parameter + */ + public boolean function isFrameworkReloadRequest() { + setupRequestDefaults(); + return ( isDefined( 'URL' ) && + structKeyExists( URL, variables.framework.reload ) && + URL[ variables.framework.reload ] == variables.framework.password ) || + variables.framework.reloadApplicationOnEveryRequest; } /* @@ -644,6 +701,22 @@ component { return internalLayout( layoutPath, body ); } + /* + * Exotic utility function to create proxies for FW/1 methods that can be + * called from Java: can be used with external rendering engines, for example + * NOTE: requires Java 8 for Function<> interface! + */ + public struct function makeMethodProxies( array methodNames ) { + var proxies = { }; + for ( var method in methodNames ) { + proxies[ method ] = createDynamicProxy( + new framework.methodProxy( this, method ), + [ "java.util.function.Function" ] + ); + } + return proxies; + } + /* * it is better to set up your application configuration in * your setupApplication() method since that is called on a @@ -652,7 +725,6 @@ component { * super.onApplicationStart() first */ public any function onApplicationStart() { - setupFrameworkDefaults(); setupRequestDefaults(); setupApplicationWrapper(); } @@ -668,9 +740,9 @@ component { try { if ( !structKeyExists( variables, 'framework' ) || !structKeyExists( variables.framework, 'version' ) ) { - // error occurred before framework was initialized - failure( exception, event, false, true ); - return; + // error occurred before framework was initialized + failure( exception, event, false, true ); + return; } // record details of the exception: @@ -681,8 +753,8 @@ component { request.event = event; // reset lifecycle flags: structDelete( request, 'layout' ); - structDelete( request._fw1, 'controllerExecutionComplete' ); structDelete( request._fw1, 'controllerExecutionStarted' ); + structDelete( request._fw1, 'overrideLayoutAction' ); structDelete( request._fw1, 'overrideViewAction' ); if ( structKeyExists( request._fw1, 'renderData' ) ) { // need to reset the content type as well! @@ -695,14 +767,24 @@ component { } // setup the new controller action, based on the error action: request._fw1.controllers = [ ]; - - if ( structKeyExists( variables, 'framework' ) && structKeyExists( variables.framework, 'error' ) ) { - request.action = variables.framework.error; + var key = 'error'; + var defaultAction = 'main.error'; + try { + if ( exception.type == 'fw1.viewnotfound' && structKeyExists( variables.framework, 'missingview' ) ) { + key = 'missingview'; + // shouldn't be needed -- key will be present in framework config + defaultAction = 'main.missingview'; + } + } catch ( any e ) { + // leave it as exception + } + if ( structKeyExists( variables, 'framework' ) && structKeyExists( variables.framework, key ) ) { + request.action = variables.framework[ key ]; } else { // this is an edge case so we don't bother with subsystems etc // (because if part of the framework defaults are not present, // we'd have to do a lot of conditional logic here!) - request.action = 'main.error'; + request.action = defaultAction; } // ensure request.context is available if ( !structKeyExists( request, 'context' ) ) { @@ -738,12 +820,12 @@ component { * this can be overridden if you want to change the behavior when * FW/1 cannot find a matching view */ - public string function onMissingView( struct rc ) { + public any function onMissingView( struct rc ) { // unable to find a matching view - fail with a nice exception viewNotFound(); - // if we got here, we would return the string to be rendered + // if we got here, we would return the string or struct to be rendered // but viewNotFound() throws an exception... - // for example, return view( 'main.missing' ); + // for example, return view( 'main/missing' ); } /* @@ -758,6 +840,17 @@ component { public void function onPopulateError( any cfc, string property, struct rc ) { } + /* + * This can be overridden if you want to take some actions when the + * framework is about to be reloaded, prior to starting the next + * application cycle. This will be called when an explicit reload is + * performed, or on each request if reloadApplicationOnEveryRequest is + * set true. You could use it to perform housekeeping of services, prior + * to them all being recreated in a new bean factory, for example. + */ + public void function onReload() { + } + /* * not intended to be overridden, automatically deleted for CFC requests */ @@ -771,32 +864,45 @@ component { var n = 0; request._fw1.controllerExecutionStarted = true; - try { - n = arrayLen( request._fw1.controllers ); - for ( i = 1; i <= n; i = i + 1 ) { - tuple = request._fw1.controllers[ i ]; - // run before once per controller: - if ( !structKeyExists( once, tuple.key ) ) { - once[ tuple.key ] = i; - doController( tuple, 'before', 'before' ); + if ( variables.framework.preflightOptions && + request._fw1.cgiRequestMethod == "OPTIONS" && + structCount( request._fw1.routeMethodsMatched ) ) { + // OPTIONS support enabled and at least one possible match + // bypass all normal controllers and render headers and data: + var resp = getPageContext().getResponse(); + resp.setHeader( "Access-Control-Allow-Origin", variables.framework.optionsAccessControl.origin ); + resp.setHeader( "Access-Control-Allow-Methods", "OPTIONS," & uCase( structKeyList( request._fw1.routeMethodsMatched ) ) ); + resp.setHeader( "Access-Control-Allow-Headers", variables.framework.optionsAccessControl.headers ); + resp.setHeader( "Access-Control-Allow-Credentials", variables.framework.optionsAccessControl.credentials ? "true" : "false" ); + resp.setHeader( "Access-Control-Max-Age", "#variables.framework.optionsAccessControl.maxAge#" ); + renderData( "text", "" ); + } else { + try { + n = arrayLen( request._fw1.controllers ); + for ( i = 1; i <= n; i = i + 1 ) { + tuple = request._fw1.controllers[ i ]; + // run before once per controller: + if ( !structKeyExists( once, tuple.key ) ) { + once[ tuple.key ] = i; + doController( tuple, 'before', 'before' ); + if ( structKeyExists( request._fw1, 'abortController' ) ) abortController(); + } + doController( tuple, tuple.item, 'item' ); if ( structKeyExists( request._fw1, 'abortController' ) ) abortController(); } - doController( tuple, tuple.item, 'item' ); - if ( structKeyExists( request._fw1, 'abortController' ) ) abortController(); - } - n = arrayLen( request._fw1.controllers ); - for ( i = n; i >= 1; i = i - 1 ) { - tuple = request._fw1.controllers[ i ]; - // run after once per controller (in reverse order): - if ( once[ tuple.key ] eq i ) { - doController( tuple, 'after', 'after' ); - if ( structKeyExists( request._fw1, 'abortController' ) ) abortController(); + n = arrayLen( request._fw1.controllers ); + for ( i = n; i >= 1; i = i - 1 ) { + tuple = request._fw1.controllers[ i ]; + // run after once per controller (in reverse order): + if ( once[ tuple.key ] eq i ) { + doController( tuple, 'after', 'after' ); + if ( structKeyExists( request._fw1, 'abortController' ) ) abortController(); + } } + } catch ( FW1.AbortControllerException e ) { + // do "nothing" since this is a control flow exception } - } catch ( FW1.AbortControllerException e ) { - // do "nothing" since this is a control flow exception } - request._fw1.controllerExecutionComplete = true; if ( structKeyExists( request._fw1, 'renderData' ) ) { out = renderDataWithContentType(); @@ -826,7 +932,19 @@ component { out = internalLayout( request._fw1.layouts[i], out ); } } - writeOutput( out ); + if ( isSimpleValue( out ) ) { + writeOutput( out ); + } else { + if ( structKeyExists( out, 'contentType' ) ) { + var resp = getPageContext().getResponse(); + resp.setContentType( out.contentType ); + } + if ( structKeyExists( out, 'writer' ) ) { + out.writer( out.output ); + } else { + writeOutput( out.output ); + } + } setupResponseWrapper(); } @@ -845,13 +963,15 @@ component { * super.onRequestStart() first */ public any function onRequestStart( string targetPath ) { - setupFrameworkDefaults(); setupRequestDefaults(); - if ( !isFrameworkInitialized() || isFrameworkReloadRequest() ) { + if ( !isFrameworkInitialized() ) { + setupApplicationWrapper(); + } else if ( isFrameworkReloadRequest() ) { + onReload(); setupApplicationWrapper(); } else { - variables.fw1App = getFw1App(); + request._fw1.theApp = getFw1App(); } restoreFlashContext(); @@ -879,7 +999,6 @@ component { * super.onSessionStart() first */ public any function onSessionStart() { - setupFrameworkDefaults(); setupRequestDefaults(); setupSessionWrapper(); } @@ -895,7 +1014,6 @@ component { var args = { }; args[ property ] = props[ property ]; if ( trim && isSimpleValue( args[ property ] ) ) args[ property ] = trim( args[ property ] ); - // cfc[ 'set'&property ]( argumentCollection = args ); // ugh! no portable script version of this?!?! setProperty( cfc, property, args ); } catch ( any e ) { onPopulateError( cfc, property, props ); @@ -908,7 +1026,6 @@ component { var args = { }; args[ property ] = props[ property ]; if ( trim && isSimpleValue( args[ property ] ) ) args[ property ] = trim( args[ property ] ); - // cfc[ 'set'&property ]( argumentCollection = args ); // ugh! no portable script version of this?!?! setProperty( cfc, property, args ); } else if ( deep && structKeyExists( cfc, 'get' & property ) ) { // look for a property that starts with the property @@ -934,7 +1051,6 @@ component { var args = { }; args[ trimProperty ] = props[ trimProperty ]; if ( trim && isSimpleValue( args[ trimProperty ] ) ) args[ trimProperty ] = trim( args[ trimProperty ] ); - // cfc[ 'set'&trimproperty ]( argumentCollection = args ); // ugh! no portable script version of this?!?! setProperty( cfc, trimProperty, args ); } } else if ( deep ) { @@ -968,26 +1084,27 @@ component { // call from your controller to redirect to a clean URL based on an action, pushing data to flash scope if necessary: public void function redirect( string action, string preserve = 'none', string append = 'none', string path = variables.magicBaseURL, - string queryString = '', string statusCode = '302', string header = '' + any queryString = '', string statusCode = '302', string header = '' ) { if ( path == variables.magicBaseURL ) path = getBaseURL(); var preserveKey = ''; if ( preserve != 'none' ) { preserveKey = saveFlashContext( preserve ); } + queryString = normalizeQueryString( queryString ); var baseQueryString = ''; if ( append != 'none' ) { if ( append == 'all' ) { for ( var key in request.context ) { if ( isSimpleValue( request.context[ key ] ) ) { - baseQueryString = listAppend( baseQueryString, key & '=' & urlEncodedFormat( request.context[ key ] ), '&' ); + baseQueryString = listAppend( baseQueryString, key & '=' & encodeForURL( request.context[ key ] ), '&' ); } } } else { var keys = listToArray( append ); for ( var key in keys ) { if ( structKeyExists( request.context, key ) && isSimpleValue( request.context[ key ] ) ) { - baseQueryString = listAppend( baseQueryString, key & '=' & urlEncodedFormat( request.context[ key ] ), '&' ); + baseQueryString = listAppend( baseQueryString, key & '=' & encodeForURL( request.context[ key ] ), '&' ); } } @@ -1023,7 +1140,7 @@ component { if ( request._fw1.doTrace ) { internalFrameworkTrace( 'redirecting to #targetURL# (#statusCode#)' ); try { - session._fw1_trace = request._fw1.trace; + sessionWrite( '_fw1_trace', request._fw1.trace ); } catch ( any _ ) { // ignore exception if session is not enabled } @@ -1062,7 +1179,7 @@ component { if ( request._fw1.doTrace ) { internalFrameworkTrace( 'redirecting to #targetURL# (#statusCode#)' ); try { - session._fw1_trace = request._fw1.trace; + sessionWrite( '_fw1_trace', request._fw1.trace ); } catch ( any _ ) { // ignore exception if session is not enabled } @@ -1078,8 +1195,86 @@ component { } // call this to render data rather than a view and layouts - public void function renderData( string type, any data, numeric statusCode = 200, string jsonpCallback = "" ) { - request._fw1.renderData = { type = type, data = data, statusCode = statusCode, jsonpCallback = jsonpCallback }; + // arguments are deprecated in favor of build syntax as of 4.0 + public any function renderData( string type = '', any data = '', numeric statusCode = 200, string jsonpCallback = "" ) { + if ( statusCode != 200 ) deprecated( false, "Use the .statusCode() builder syntax instead of the inline argument." ); + if ( len( jsonpCallback ) ) deprecated( false, "Use the .jsonpCallback() builder syntax instead of the inline argument." ); + request._fw1.renderData = { + type = type, + data = data, + statusCode = statusCode, + statusText = '', + jsonpCallback = jsonpCallback + }; + // return a builder to support nicer rendering syntax + return renderer(); + } + + public any function renderer() { + var builder = { }; + structAppend( builder, { + // allow type and data to be overridden just for completeness + type : function( v ) { + if ( !structKeyExists( request._fw1, 'renderData' ) ) request._fw1.renderData = { }; + request._fw1.renderData.type = v; + return builder; + }, + data : function( v ) { + if ( !structKeyExists( request._fw1, 'renderData' ) ) request._fw1.renderData = { }; + request._fw1.renderData.data = v; + return builder; + }, + header : function( h, v ) { + if ( !structKeyExists( request._fw1, 'renderData' ) ) request._fw1.renderData = { }; + if ( !structKeyExists( request._fw1.renderData, 'headers' ) ) { + request._fw1.renderData.headers = [ ]; + } + arrayAppend( request._fw1.renderData.headers, { name = h, value = v } ); + return builder; + }, + statusCode : function( v ) { + if ( !structKeyExists( request._fw1, 'renderData' ) ) request._fw1.renderData = { }; + request._fw1.renderData.statusCode = v; + return builder; + }, + statusText : function( v ) { + if ( !structKeyExists( request._fw1, 'renderData' ) ) request._fw1.renderData = { }; + request._fw1.renderData.statusText = v; + return builder; + }, + jsonpCallback : function( v ) { + if ( !structKeyExists( request._fw1, 'renderData' ) ) request._fw1.renderData = { }; + request._fw1.renderData.jsonpCallback = v; + return builder; + } + } ); + return builder; + } + + public void function sessionDefault( string keyname, any defaultValue ) { + param name="session['#keyname#']" default="#defaultValue#"; + } + + public void function sessionDelete( string keyname ) { + structDelete( session, keyname ); + } + + public boolean function sessionHas( string keyname ) { + return structKeyExists( session, keyname ); + } + + public void function sessionLock( required function callback ) { + lock scope="session" type="exclusive" timeout="30" { + callback(); + } + } + + public any function sessionRead( string keyname ) { + return session[ keyname ]; + } + + public void function sessionWrite( string keyname, any keyvalue ) { + session[ keyname ] = keyvalue; } /* @@ -1090,6 +1285,18 @@ component { */ public void function setBeanFactory( any beanFactory ) { if ( isObject( beanFactory ) ) { + if ( structKeyExists( getFw1App(), "factory" ) ) { + if ( variables.framework.diOverrideAllowed ) { + // we still log a warning because this is strange behavior + var out = createObject( "java", "java.lang.System" ).out; + out.println( "FW/1: WARNING: setBeanFactory() called more than once - use diEngine = 'none'?" ); + internalFrameworkTrace( message = "FW/1: WARNING: setBeanFactory() called more than once - use diEngine = 'none'?", traceType = 'WARNING' ); + } else { + throw( type = "FW1.Warning", + message = "setBeanFactory() called more than once - use diEngine = 'none'?", + detail = "Either set diEngine to 'none' or let FW/1 manage your bean factory for you." ); + } + } getFw1App().factory = beanFactory; } else { structDelete( getFw1App(), "factory" ); @@ -1103,7 +1310,7 @@ component { * use this to override the default layout */ public void function setLayout( string action, boolean suppressOtherLayouts = false ) { - request._fw1.overrideLayoutAction = validateAction( action ); + request._fw1.overrideLayoutAction = validateAction( getFullyQualifiedAction( action ) ); request._fw1.suppressOtherLayouts = suppressOtherLayouts; } @@ -1180,7 +1387,7 @@ component { * use this to override the default view */ public void function setView( string action ) { - request._fw1.overrideViewAction = validateAction( action ); + request._fw1.overrideViewAction = validateAction( getFullyQualifiedAction( action ) ); } /* @@ -1194,8 +1401,8 @@ component { * view() may be invoked inside views and layouts * returns the UI generated by the named view */ - public string function view( string path, struct args = { }, - any missingView = { } ) { + public any function view( string path, struct args = { }, + any missingView = { } ) { var viewPath = parseViewOrLayoutPath( path, 'view' ); if ( cachedFileExists( viewPath ) ) { internalFrameworkTrace( 'view( #path# ) called - rendering #viewPath#' ); @@ -1214,6 +1421,60 @@ component { } } + // EXPERIMENTAL COLDBOX MODULE SUPPORT + + /* + * in Application.cfc, call as follows: + * this.mappings = moduleMappings( "qb, supermod" ); + * this.mappings = moduleMappings( [ "mod1", "mod2"], "modules" ); + */ + public struct function moduleMappings( any modules, string modulePath = "modules" ) { + if ( isSimpleValue( modules ) ) modules = listToArray( modules ); + var cleanModules = [ ]; + var mappings = { }; + for ( var m in modules ) { + m = trim( m ); + arrayAppend( cleanModules, m ); + mappings[ "/" & m ] = expandPath( "/" & modulePath & "/" & m ); + } + variables._fw1_coldbox_modulePath = modulePath; + variables._fw1_coldbox_modules = cleanModules; + return mappings; + } + + /* + * call this in setupApplication() to load the modules for which + * you set up moduleMappings() using the function above -- the + * frameworkPath argument can override the default location for FW/1 + */ + public void function installModules( string frameworkPath = "framework" ) { + var bf = new "#frameworkPath#.WireBoxAdapter"(); + getBeanFactory().setParent( bf ); + var builder = bf.getBuilder(); + var nullObject = new "#frameworkPath#.nullObject"(); + var cbdsl = { }; + cbdsl.init = function() { return cbdsl; }; + cbdsl.process = function() { return nullObject; }; + builder.vars = __vars; + builder.vars().instance.ColdBoxDSL = cbdsl; + for ( var module in variables._fw1_coldbox_modules ) { + var cfg = new "#variables._fw1_coldbox_modulePath#.#module#.ModuleConfig"(); + cfg.vars = __vars; + cfg.vars().binder = bf.getBinder(); + cfg.vars().controller = { + getWireBox : function() { return bf; } + }; + cfg.configure(); + if ( structKeyExists( variables.framework, "modules" ) && + structKeyExists( variables.framework.modules, module ) ) { + structAppend( cfg.vars().settings, variables.framework.modules[ module ] ); + } + cfg.onLoad(); + } + } + // helper to allow mixins: + private struct function __vars() { return variables; } + // THE FOLLOWING METHODS SHOULD ALL BE CONSIDERED PRIVATE / UNCALLABLE private void function autowire( any cfc, any beanFactory ) { @@ -1222,8 +1483,7 @@ component { if ( beanFactory.containsBean( property ) ) { var args = { }; args[ property ] = beanFactory.getBean( property ); - // cfc['set'&property](argumentCollection = args) does not work on ACF9 - evaluate( 'cfc.set#property#( argumentCollection = args )' ); + invoke( cfc, "set#property#", args ); } } } @@ -1255,7 +1515,7 @@ component { internalFrameworkTrace( 'building layout queue', subsystem, section, item ); // look for item-specific layout: testLayout = parseViewOrLayoutPath( subsystem & variables.framework.subsystemDelimiter & - section & '/' & item, 'layout' ); + section & '/' & item, 'layout' ); if ( cachedFileExists( testLayout ) ) { internalFrameworkTrace( 'found item-specific layout #testLayout#', subsystem, section, item ); arrayAppend( request._fw1.layouts, testLayout ); @@ -1276,11 +1536,20 @@ component { } } // look for site-wide layout (only applicable if using subsystems) - if ( usingSubsystems() && siteWideLayoutBase != subsystembase ) { - testLayout = parseViewOrLayoutPath( variables.framework.siteWideLayoutSubsystem & - variables.framework.subsystemDelimiter & 'default', 'layout' ); + if ( usingSubsystems() ) { + if ( siteWideLayoutBase != subsystembase ) { + testLayout = parseViewOrLayoutPath( variables.framework.siteWideLayoutSubsystem & + variables.framework.subsystemDelimiter & 'default', 'layout' ); + if ( cachedFileExists( testLayout ) ) { + internalFrameworkTrace( 'found #variables.framework.siteWideLayoutSubsystem# layout #testLayout#', + subsystem, section, item ); + arrayAppend( request._fw1.layouts, testLayout ); + } + } + } else if ( len( subsystem ) ) { + testLayout = parseViewOrLayoutPath( variables.framework.subsystemDelimiter & 'default', 'layout' ); if ( cachedFileExists( testLayout ) ) { - internalFrameworkTrace( 'found #variables.framework.siteWideLayoutSubsystem# layout #testLayout#', + internalFrameworkTrace( 'found application layout #testLayout#', subsystem, section, item ); arrayAppend( request._fw1.layouts, testLayout ); } @@ -1307,7 +1576,7 @@ component { internalFrameworkTrace( 'building view queue', subsystem, section, item ); // view and layout setup - used to be in setupRequestWrapper(): request._fw1.view = parseViewOrLayoutPath( subsystem & variables.framework.subsystemDelimiter & - section & '/' & item, 'view' ); + section & '/' & item, 'view' ); if ( cachedFileExists( request._fw1.view ) ) { internalFrameworkTrace( 'found view #request._fw1.view#', subsystem, section, item ); } else { @@ -1348,6 +1617,7 @@ component { } else { var out = createObject( "java", "java.lang.System" ).out; out.println( "FW/1: DEPRECATED: " & message ); + internalFrameworkTrace( message = "FW/1: DEPRECATED: " & message, traceType = 'DEPRECATED' ); } } @@ -1356,7 +1626,7 @@ component { if ( structKeyExists( cfc, method ) ) { try { internalFrameworkTrace( 'calling #lifecycle# controller', tuple.subsystem, tuple.section, method ); - evaluate( 'cfc.#method#( rc = request.context )' ); + invoke( cfc, method, { rc : request.context, headers : request._fw1.headers } ); } catch ( any e ) { setCfcMethodFailureInfo( cfc, method ); rethrow; @@ -1364,7 +1634,7 @@ component { } else if ( structKeyExists( cfc, 'onMissingMethod' ) ) { try { internalFrameworkTrace( 'calling #lifecycle# controller (via onMissingMethod)', tuple.subsystem, tuple.section, method ); - evaluate( 'cfc.#method#( rc = request.context, method = lifecycle )' ); + invoke( cfc, method, { rc : request.context, method : lifecycle, headers : request._fw1.headers } ); } catch ( any e ) { setCfcMethodFailureInfo( cfc, method ); rethrow; @@ -1382,11 +1652,11 @@ component { var framework = getFw1App(); - if ( !structKeyExists(framework, 'subsystemFactories') ) { + if ( !structKeyExists( framework, 'subsystemFactories' ) ) { framework.subsystemFactories = { }; } - if ( !structKeyExists(framework, 'subsystems') ) { + if ( !structKeyExists( framework, 'subsystems' ) ) { framework.subsystems = { }; } @@ -1401,14 +1671,14 @@ component { if ( early ) { writeOutput( '

Exception occured before FW/1 was initialized

'); } else { - writeOutput( '' & ( indirect ? 'Original exception ' : 'Exception' ) & ' in #event#' ); + writeOutput( '' & ( indirect ? 'Original exception ' : 'Exception' ) & ' in #encodeForHTML(event)#' ); if ( structKeyExists( request, 'failedAction' ) ) { - writeOutput( '

The action #request.failedAction# failed.

' ); + writeOutput( '

The action #encodeForHtml(request.failedAction)# failed.

' ); } - writeOutput( '#exception.message#' ); + writeOutput( '#encodeForHtml(exception.message)#' ); } - writeOutput( '

#exception.detail# (#exception.type#)

' ); + writeOutput( '

#encodeForHtml(exception.detail)# (#encodeForHtml(exception.type)#)

' ); dumpException(exception); } @@ -1438,7 +1708,7 @@ component { for ( var i = 1; i <= n; ++i ) { var property = md.properties[ i ]; if ( implicitSetters || - structKeyExists( property, 'setter' ) && isBoolean( property.setter ) && property.setter ) { + structKeyExists( property, 'setter' ) && isBoolean( property.setter ) && property.setter ) { setters[ property.name ] = 'implicit'; } } @@ -1479,8 +1749,8 @@ component { writeOutput( '
' ); writeOutput( '
Framework Lifecycle Trace
' ); var table = '' & - '' & - ''; + '' & + ''; writeOutput( table ); var colors = [ '##ccd4dd', '##ccddcc' ]; var row = 0; @@ -1489,6 +1759,11 @@ component { for ( var i = 1; i <= n; ++i ) { var trace = request._fw1.trace[i]; var nextTraceTick = i + 1 <= n ? request._fw1.trace[i+1].tick : trace.tick; + var color = '##000'; + var traceType = structKeyExists( trace, 't' ) ? trace.t : 'INFO'; + if ( trace.msg.startsWith( 'no ' ) ) color = '##cc8888'; + else if ( trace.msg.startsWith( 'onError( ' ) || traceType == 'ERROR' ) color = '##cc0000'; + else if ( traceType == 'WARNING' ) color = '##d44b0f'; var action = ''; if ( trace.s == variables.magicApplicationController || trace.sub == variables.magicApplicationSubsystem ) { action = 'Application.cfc'; @@ -1515,11 +1790,13 @@ component { writeOutput('#duration - lastDuration#ms'); } lastDuration = duration; - writeOutput('' ); + writeOutput( '' ); + if ( !structKeyExists( trace, 't' ) || trace.t == 'INFO' ) { + writeOutput( '' ); + } else { + writeOutput( '' ); + } writeOutput( '' ); - var color = - trace.msg.startsWith( 'no ' ) ? '##cc8888' : - trace.msg.startsWith( 'onError( ' ) ? '##cc0000' : '##000'; writeOutput( '
timedeltaactionmessage
timedeltatypeactionmessage
 #ucase(trace.t)##action##trace.msg#' ); if ( structKeyExists( trace, 'v' ) ) { writeOutput( '
' ); @@ -1544,12 +1821,14 @@ component { var subsystemDot = replace( subsystemDir, '/', '.', 'all' ); var subsystemUnderscore = replace( subsystemDir, '/', '_', 'all' ); var componentKey = subsystemUnderscore & section; - var beanName = section & "controller"; + var beanName = section & variables.controllerFolder; + var controllersSlash = variables.framework.controllersFolder & '/'; + var controllersDot = variables.framework.controllersFolder & '.'; // per #310 we no longer cache the Application controller since it is new on each request if ( !structKeyExists( cache.controllers, componentKey ) || section == variables.magicApplicationController ) { lock name="fw1_#application.applicationName#_#variables.framework.applicationKey#_#componentKey#" type="exclusive" timeout="30" { if ( !structKeyExists( cache.controllers, componentKey ) || section == variables.magicApplicationController ) { - if ( usingSubsystems() && hasSubsystemBeanFactory( subsystem ) && getSubsystemBeanFactory( subsystem ).containsBean( beanName ) ) { + if ( hasSubsystemBeanFactory( subsystem ) && getSubsystemBeanFactory( subsystem ).containsBean( beanName ) ) { cfc = getSubsystemBeanFactory( subsystem ).getBean( beanName ); } else if ( !usingSubsystems() && hasDefaultBeanFactory() && getDefaultBeanFactory().containsBean( beanName ) ) { cfc = getDefaultBeanFactory().getBean( beanName ); @@ -1557,12 +1836,12 @@ component { if ( section == variables.magicApplicationController ) { // treat this (Application.cfc) as a controller: cfc = this; - } else if ( cachedFileExists( cfcFilePath( request.cfcbase ) & subsystemDir & 'controllers/' & section & 'Controller.cfc' ) ) { + } else if ( cachedFileExists( cfcFilePath( request.cfcbase ) & subsystemDir & controllersSlash & section & '.cfc' ) ) { // we call createObject() rather than new so we can control initialization: if ( request.cfcbase == '' ) { - cfc = createObject( 'component', subsystemDot & 'controllers.' & section & 'Controller' ); + cfc = createObject( 'component', subsystemDot & controllersDot & section ); } else { - cfc = createObject( 'component', request.cfcbase & '.' & subsystemDot & 'controllers.' & section & 'Controller' ); + cfc = createObject( 'component', request.cfcbase & '.' & subsystemDot & controllersDot & section ); } if ( structKeyExists( cfc, 'init' ) ) { cfc.init( this ); @@ -1594,8 +1873,8 @@ component { } private struct function getFw1App() { - if ( structKeyExists( variables, "fw1App" ) ) { - return variables.fw1App; + if ( structKeyExists( request._fw1, 'theApp' ) ) { + return request._fw1.theApp; } else { return application[variables.framework.applicationKey]; } @@ -1605,27 +1884,25 @@ component { var nextPreserveKey = ''; var oldKeyToPurge = ''; try { - if ( variables.framework.maxNumContextsPreserved > 1 ) { - lock scope="session" type="exclusive" timeout="30" { - param name="session.__fw1NextPreserveKey" default="1"; - nextPreserveKey = session.__fw1NextPreserveKey; - session.__fw1NextPreserveKey = session.__fw1NextPreserveKey + 1; - } - oldKeyToPurge = nextPreserveKey - variables.framework.maxNumContextsPreserved; - } else { - lock scope="session" type="exclusive" timeout="30" { - session.__fw1PreserveKey = ''; - nextPreserveKey = session.__fw1PreserveKey; + sessionLock(function(){ + if ( variables.framework.maxNumContextsPreserved > 1 ) { + sessionDefault( '__fw1NextPreserveKey', 1 ); + nextPreserveKey = sessionRead( '__fw1NextPreserveKey' ); + sessionWrite( '__fw1NextPreserveKey', nextPreserveKey + 1 ); + oldKeyToPurge = nextPreserveKey - variables.framework.maxNumContextsPreserved; + } else { + nextPreserveKey = ''; + sessionWrite( '__fw1PreserveKey', nextPreserveKey ); + oldKeyToPurge = ''; } - oldKeyToPurge = ''; + }); + var key = getPreserveKeySessionKey( oldKeyToPurge ); + if ( sessionHas( key ) ) { + sessionDelete( key ); } } catch ( any e ) { // ignore - assume session scope is disabled } - var key = getPreserveKeySessionKey( oldKeyToPurge ); - if ( structKeyExists( session, key ) ) { - structDelete( session, key ); - } return nextPreserveKey; } @@ -1634,7 +1911,7 @@ component { } private any function getProperty( struct cfc, string property ) { - if ( structKeyExists( cfc, 'get#property#' ) ) return evaluate( 'cfc.get#property#()' ); + if ( structKeyExists( cfc, 'get#property#' ) ) return invoke( cfc, "get#property#" ); } private string function getSubsystemDirPrefix( string subsystem ) { @@ -1642,8 +1919,11 @@ component { if ( subsystem eq '' ) { return ''; } - - return subsystem & '/'; + if ( usingSubsystems() ) { + return subsystem & '/'; + } else { + return variables.framework.subsystemsFolder & '/' & subsystem & '/'; + } } private void function injectFramework( any cfc ) { @@ -1653,22 +1933,21 @@ component { // allow alternative spellings args.fw = this; args.fw1 = this; - evaluate( 'cfc.setFramework( argumentCollection = args )' ); + cfc.setFramework( argumentCollection = args ); } } - private void function internalFrameworkTrace( string message, string subsystem = '', string section = '', string item = '' ) { + private void function internalFrameworkTrace( string message, string subsystem = '', string section = '', string item = '', string traceType = 'INFO' ) { if ( request._fw1.doTrace ) { try { - if ( isDefined( 'session._fw1_trace' ) && - structKeyExists( session, '_fw1_trace' ) ) { - request._fw1.trace = session._fw1_trace; - structDelete( session, '_fw1_trace' ); + if ( sessionHas( '_fw1_trace' ) ) { + request._fw1.trace = sessionRead( '_fw1_trace' ); + sessionDelete( '_fw1_trace' ); } } catch ( any _ ) { // ignore if session is not enabled } - arrayAppend( request._fw1.trace, { tick = getTickCount(), msg = message, sub = subsystem, s = section, i = item } ); + arrayAppend( request._fw1.trace, { tick = getTickCount(), msg = message, sub = subsystem, s = section, i = item, t = traceType } ); } } @@ -1679,13 +1958,13 @@ component { if ( structKeyExists( rc, '$' ) ) { $ = rc.$; } - if ( !structKeyExists( request._fw1, 'controllerExecutionComplete' ) ) { - raiseException( type='FW1.layoutExecutionFromController', message='Invalid to call the layout method at this point.', - detail='The layout method should not be called prior to the completion of the controller execution phase.' ); - } - var response = ''; - savecontent variable="response" { - include '#layoutPath#'; + local.body = body; + var response = customTemplateEngine( 'layout', layoutPath, local ); + if ( isNull( response ) ) { + response = ''; + savecontent variable="response" { + include '#layoutPath#'; + } } return response; } @@ -1698,23 +1977,20 @@ component { $ = rc.$; } structAppend( local, args ); - var response = ''; - savecontent variable="response" { - include '#viewPath#'; + var response = customTemplateEngine( 'view', viewPath, local ); + if ( isNull( response ) ) { + response = ''; + savecontent variable="response" { + include '#viewPath#'; + } } return response; } private boolean function isFrameworkInitialized() { return structKeyExists( variables, 'framework' ) && - structKeyExists( application, variables.framework.applicationKey ); - } - - private boolean function isFrameworkReloadRequest() { - return ( isDefined( 'URL' ) && - structKeyExists( URL, variables.framework.reload ) && - URL[ variables.framework.reload ] == variables.framework.password ) || - variables.framework.reloadApplicationOnEveryRequest; + ( structKeyExists( request._fw1, 'theApp' ) || + structKeyExists( application, variables.framework.applicationKey ) ); } private boolean function isSubsystemInitialized( string subsystem ) { @@ -1725,20 +2001,71 @@ component { } - private string function parseViewOrLayoutPath( string path, string type ) { + // like listFirst() and listLast() but they actually work with empty segments + private string function segmentFirst( string segments, string delimiter ) { + var where = find( delimiter, segments ); + if ( where ) { + if ( where == 1 ) { + return ''; + } else { + return left( segments, where - 1 ); + } + } + return ''; + } + + private string function normalizeQueryString( any queryString ) { + // if queryString is a struct, massage it into a string + if ( isStruct( queryString ) && structCount( queryString ) ) { + var q = ''; + for( var key in queryString ) { + if ( isSimpleValue( queryString[key] ) ) { + q = listAppend(q, encodeForURL( key ) & '=' & encodeForURL( queryString[ key ] ), '&'); + } + } + queryString = q; + } + else if ( !isSimpleValue( queryString ) ) { + queryString = ''; + } + return queryString; + } + private string function segmentLast( string segments, string delimiter ) { + var where = find( delimiter, segments ); + if ( where ) { + if ( where == len( segments ) ) { + return ''; + } else { + return right( segments, len( segments ) - where ); + } + } + return segments; + } + + private string function parseViewOrLayoutPath( string path, string type ) { + var folder = type; + switch ( folder ) { + case 'layout': + folder = variables.layoutFolder; + break; + case 'view': + folder = variables.viewFolder; + break; + // else leave it alone? + } var pathInfo = { }; - var subsystem = getSubsystem( path ); + var subsystem = getSubsystem( getSubsystemSectionAndItem( path ) ); // allow for :section/action to simplify logic in setupRequestWrapper(): - pathInfo.path = listLast( path, variables.framework.subsystemDelimiter ); + pathInfo.path = segmentLast( path, variables.framework.subsystemDelimiter ); pathInfo.base = request.base; pathInfo.subsystem = subsystem; - if ( usingSubsystems() ) { + if ( usingSubsystems() || len( subsystem ) ) { pathInfo.base = pathInfo.base & getSubsystemDirPrefix( subsystem ); } - - return customizeViewOrLayoutPath( pathInfo, type, '#pathInfo.base##type#s/#pathInfo.path#.cfm' ); + var defaultPath = pathInfo.base & folder & 's/' & pathInfo.path & '.cfm'; + return customizeViewOrLayoutPath( pathInfo, type, defaultPath ); } @@ -1759,8 +2086,14 @@ component { if ( routeLen ) { if ( left( routeRegEx.pattern, 1 ) == '$' ) { // check HTTP method - routeRegEx.method = listFirst( routeRegEx.pattern, '*/^' ); - var methodLen = len( routeRegEx.method ); + var methodLen = 0; + if ( routeLen >= 2 && left( routeRegEx.pattern, 2 ) == '$*' ) { + // accept all methods so don't set method but... + methodLen = 2; // ...consume 2 characters + } else { + routeRegEx.method = listFirst( routeRegEx.pattern, '*/^' ); + methodLen = len( routeRegEx.method ); + } if ( routeLen == methodLen ) { routeRegEx.pattern = '*'; } else { @@ -1808,11 +2141,25 @@ component { var routeMatch = { matched = false }; structAppend( routeMatch, regExCache[ cacheKey ] ); if ( !len( path ) || right( path, 1) != '/' ) path &= '/'; - var matched = len( routeMatch.method ) ? ( '$' & httpMethod == routeMatch.method ) : true; - if ( matched && routeRegexFind( routeMatch.pattern, path ) ) { - routeMatch.matched = true; - routeMatch.route = route; - routeMatch.path = path; + if ( routeRegexFind( routeMatch.pattern, path ) ) { + if ( len( routeMatch.method ) > 1 ) { + if ( '$' & httpMethod == routeMatch.method ) { + routeMatch.matched = true; + } else if ( variables.framework.preflightOptions ) { + // it matched apart from the method so record this + request._fw1.routeMethodsMatched[ right( routeMatch.method, len( routeMatch.method ) - 1 ) ] = true; + } + } else if ( variables.framework.preflightOptions && httpMethod == "OPTIONS" ) { + // it would have matched but we should special case OPTIONS + request._fw1.routeMethodsMatched.get = true; + request._fw1.routeMethodsMatched.post = true; + } else { + routeMatch.matched = true; + } + if ( routeMatch.matched ) { + routeMatch.route = route; + routeMatch.path = path; + } } return routeMatch; } @@ -1827,7 +2174,7 @@ component { private array function getResourceRoutes( any resourcesToRoute, string subsystem = '', string pathRoot = '', string targetAppend = '' ) { var resourceCache = isFrameworkInitialized() ? getFw1App().cache.routes.resources : { }; - var cacheKey = hash( serializeJSON( resourcesToRoute ) ); + var cacheKey = hash( serializeJSON( { rtr = resourcesToRoute, ss = subsystem, pr = pathRoot, ta = targetAppend } ) ); if ( !structKeyExists( resourceCache, cacheKey ) ) { // get passed in resourcesToRoute (string,array,struct) to match following struct var resources = { resources = [ ], subsystem = subsystem, pathRoot = pathRoot, methods = [ ], nested = [ ] }; @@ -1882,67 +2229,106 @@ component { return resourceCache[ cacheKey ]; } - private void function raiseException( string type, string message, string detail ) { - throw( type = type, message = message, detail = detail ); + private any function read_json( string json ) { + return deserializeJSON( json ); + } + + private struct function render_json( struct renderData ) { + return { + contentType = 'application/json; charset=utf-8', + output = serializeJSON( renderData.data ) + }; + } + + private struct function render_jsonp( struct renderData ) { + if ( !structKeyExists( renderData, 'jsonpCallback' ) || !len( renderData.jsonpCallback ) ){ + throw( type = 'FW1.jsonpCallbackRequired', + message = 'Callback was not defined', + detail = 'renderData() called with jsonp type requires a jsonpCallback' ); + } + return { + contentType = 'application/javascript; charset=utf-8', + output = renderData.jsonpCallback & "(" & serializeJSON( renderData.data ) & ");" + }; + } + + private struct function render_rawjson( struct renderData ) { + return { + contentType = 'application/json; charset=utf-8', + output = renderData.data + }; + } + + private struct function render_html( struct renderData ) { + structDelete( request._fw1, 'renderData' ); + return { + contentType = 'text/html; charset=utf-8', + output = renderData.data + }; + } + + private struct function render_xml( struct renderData ) { + var output = ''; + if ( isXML( renderData.data ) ) { + if ( isSimpleValue( renderData.data ) ) { + // XML as string already + output = renderData.data; + } else { + // XML object + output = toString( renderData.data ); + } + } else { + throw( type = 'FW1.UnsupportXMLRender', + message = 'Data is not XML', + detail = 'renderData() called with XML type but unrecognized data format' ); + } + return { + contentType = 'text/xml; charset=utf-8', + output = output + }; + } + + private struct function render_text( struct renderData ) { + return { + contentType = 'text/plain; charset=utf-8', + output = renderData.data + }; } - private string function renderDataWithContentType() { - var out = ''; - var contentType = ''; - var type = request._fw1.renderData.type; - var data = request._fw1.renderData.data; + private struct function renderDataWithContentType() { + var out = { }; + var renderType = request._fw1.renderData.type; var statusCode = request._fw1.renderData.statusCode; - switch ( type ) { - case 'json': - contentType = 'application/json; charset=utf-8'; - out = serializeJSON( data ); - break; - case 'jsonp': - contentType = 'application/javascript; charset=utf-8'; - if ( !len(request._fw1.renderData.jsonpCallback) ){ - throw( type = 'FW1.jsonpCallbackRequired', - message = 'Callback was not defined', - detail = 'renderData() called with jsonp type requires a jsonpCallback' ); - } - out = request._fw1.renderData.jsonpCallback & "(" & serializeJSON( data ) & ");"; - break; - case 'rawjson': - contentType = 'application/json; charset=utf-8'; - out = data; - break; - case 'html': - contentType = 'text/html; charset=utf-8'; - out = data; - structDelete( request._fw1, 'renderData' ); - break; - case 'xml': - contentType = 'text/xml; charset=utf-8'; - if ( isXML( data ) ) { - if ( isSimpleValue( data ) ) { - // XML as string already - out = data; - } else { - // XML object - out = toString( data ); - } + var statusText = request._fw1.renderData.statusText; + var headers = structKeyExists( request._fw1.renderData, 'headers' ) ? + request._fw1.renderData.headers : [ ]; + if ( isSimpleValue( renderType ) ) { + var fn_type = 'render_' & renderType; + if ( structKeyExists( variables, fn_type ) ) { + renderType = variables[ fn_type ]; + // evaluate with no FW/1 context! + out = renderType( request._fw1.renderData ); } else { - throw( type = 'FW1.UnsupportXMLRender', - message = 'Data is not XML', - detail = 'renderData() called with XML type but unrecognized data format' ); + throw( type = 'FW1.UnsupportedRenderType', + message = 'Only HTML, JSON, JSONP, RAWJSON, XML, and TEXT are supported', + detail = 'renderData() called with unknown type: ' & renderType ); } - break; - case 'text': - contentType = 'text/plain; charset=utf-8'; - out = data; - break; - default: - throw( type = 'FW1.UnsupportedRenderType', - message = 'Only HTML, JSON, JSONP, RAWJSON, XML, and TEXT are supported', - detail = 'renderData() called with unknown type: ' & type ); - break; + } else { + // assume it is a function + out = renderType( request._fw1.renderData ); + } + var resp = getPageContext().getResponse(); + for ( var h in headers ) { + resp.setHeader( h.name, h.value ); + } + // in theory, we should use sendError() instead of setStatus() but some + // Servlet containers interpret that to mean "Send my error page" instead + // of just sending the response you actually want! + if ( len( statusText ) ) { + resp.setStatus( statusCode, statusText ); + } else { + resp.setStatus( statusCode ); } - getPageContext().getResponse().setStatus( statusCode ); - getPageContext().getResponse().setContentType( contentType ); return out; } @@ -1985,16 +2371,16 @@ component { var preserveKeySessionKey = getPreserveKeySessionKey( '' ); } try { - if ( structKeyExists( session, preserveKeySessionKey ) ) { - structAppend( request.context, session[ preserveKeySessionKey ], false ); + if ( sessionHas( preserveKeySessionKey ) ) { + structAppend( request.context, sessionRead( preserveKeySessionKey ), false ); if ( variables.framework.maxNumContextsPreserved == 1 ) { /* - When multiple contexts are preserved, the oldest context is purged - within getNextPreserveKeyAndPurgeOld once the maximum is reached. - This allows for a browser refresh after the redirect to still receive - the same context. + When multiple contexts are preserved, the oldest context is purged + within getNextPreserveKeyAndPurgeOld once the maximum is reached. + This allows for a browser refresh after the redirect to still receive + the same context. */ - structDelete( session, preserveKeySessionKey ); + sessionDelete( preserveKeySessionKey ); } } } catch ( any e ) { @@ -2005,22 +2391,30 @@ component { private string function saveFlashContext( string keys ) { var curPreserveKey = getNextPreserveKeyAndPurgeOld(); var preserveKeySessionKey = getPreserveKeySessionKey( curPreserveKey ); + var tmpSession = ''; try { - param name="session.#preserveKeySessionKey#" default="#{ }#"; + sessionDefault( preserveKeySessionKey, {} ); if ( keys == 'all' ) { - structAppend( session[ preserveKeySessionKey ], request.context ); + tmpSession = sessionRead( preserveKeySessionKey ); + structAppend( tmpSession, request.context ); + sessionWrite( preserveKeySessionKey, tmpSession ); } else { var key = 0; var keyNames = listToArray( keys ); for ( key in keyNames ) { key = trim( key ); if ( structKeyExists( request.context, key ) ) { - session[ preserveKeySessionKey ][ key ] = request.context[ key ]; + tmpSession = sessionRead( preserveKeySessionKey ); + tmpSession[ key ] = request.context[ key ]; + sessionWrite( preserveKeySessionKey, tmpSession); + } else { + internalFrameworkTrace( message = 'key "#key#" does not exist in RC, cannot preserve.', traceType = 'WARNING' ); } } } } catch ( any ex ) { // session scope not enabled, do nothing + internalFrameworkTrace( message = 'sessionManagement not enabled, cannot preserve RC keys.', traceType = 'WARNING' ); } return curPreserveKey; } @@ -2048,20 +2442,14 @@ component { if ( !isNull( obj ) ) setProperty( obj, newProperty, args ); } } else { - evaluate( 'cfc.set#property#( argumentCollection = args )' ); + invoke( cfc, "set#property#", args ); } } private void function setupApplicationWrapper() { - /* - since this can be called on a reload, we need to lock it to prevent other threads - trying to reload the app at the same time since we're messing with the main application - data struct... if the application is already running, we don't blow away the factories - because we don't want to affect other threads that may be running at this time - */ if ( structKeyExists( request._fw1, "appWrapped" ) ) return; request._fw1.appWrapped = true; - variables.fw1App = { + request._fw1.theApp = { cache = { lastReload = now(), fileExists = { }, @@ -2083,12 +2471,23 @@ component { setBeanFactory( ioc ); break; case "wirebox": - var wb = new "#variables.framework.diComponent#"( - properties = variables.framework.diConfig - ); - wb.getBinder().scanLocations( variables.framework.diLocations ); - // we do not provide fw alias for controller constructor here! - setBeanFactory( wb ); + if ( isSimpleValue( variables.framework.diConfig ) ) { + // per #363 assume name of binder CFC + var wb1 = new "#variables.framework.diComponent#"( + variables.framework.diConfig, // binder path + variables.framework // properties struct + ); + // we do not provide fw alias for controller constructor here! + setBeanFactory( wb1 ); + } else { + // legacy configuration + var wb2 = new "#variables.framework.diComponent#"( + properties = variables.framework.diConfig + ); + wb2.getBinder().scanLocations( variables.framework.diLocations ); + // we do not provide fw alias for controller constructor here! + setBeanFactory( wb2 ); + } break; case "custom": var ioc = new "#variables.framework.diComponent#"( @@ -2102,12 +2501,12 @@ component { // this will recreate the main bean factory on a reload: internalFrameworkTrace( 'setupApplication() called' ); setupApplication(); - application[variables.framework.applicationKey] = variables.fw1App; + application[variables.framework.applicationKey] = request._fw1.theApp; - } + } private void function setupFrameworkDefaults() { - + if ( structKeyExists( variables, "_fw1_defaults_initialized" ) ) return; // default values for Application::variables.framework structure: if ( !structKeyExists(variables, 'framework') ) { variables.framework = { }; @@ -2132,14 +2531,11 @@ component { } } if ( !structKeyExists(variables.framework, 'usingSubsystems') ) { - variables.framework.usingSubsystems = - structKeyExists(variables.framework,'defaultSubsystem') || - structKeyExists(variables.framework,'sitewideLayoutSubsystem') || - structKeyExists(variables.framework,'subsystemDelimiter') || - structKeyExists(variables.framework,'subsystems'); + variables.framework.usingSubsystems = structKeyExists( variables.framework, 'defaultSubsystem' ) || + structKeyExists( variables.framework, 'siteWideLayoutSubsystem' ); } if ( !structKeyExists(variables.framework, 'defaultSubsystem') ) { - variables.framework.defaultSubsystem = 'home'; + variables.framework.defaultSubsystem = variables.framework.usingSubsystems ? 'home' : ''; } if ( !structKeyExists(variables.framework, 'defaultSection') ) { variables.framework.defaultSection = 'main'; @@ -2157,23 +2553,21 @@ component { variables.framework.subsystems = { }; } if ( structKeyExists(variables.framework, 'home') ) { - if (usingSubsystems()) { + if ( usingSubsystems() ) { if ( !find( variables.framework.subsystemDelimiter, variables.framework.home ) ) { - raiseException( type = "FW1.configuration.home", message = "You are using subsystems but framework.home does not specify a subsystem.", detail = "You should set framework.home to #variables.framework.defaultSubsystem##variables.framework.subsystemDelimiter##variables.framework.home#" ); + throw( type = "FW1.configuration.home", message = "You are using subsystems but framework.home does not specify a subsystem.", detail = "You should set framework.home to #variables.framework.defaultSubsystem##variables.framework.subsystemDelimiter##variables.framework.home#" ); } } } else { - if (usingSubsystems()) { - variables.framework.home = variables.framework.defaultSubsystem & variables.framework.subsystemDelimiter & variables.framework.defaultSection & '.' & variables.framework.defaultItem; - } else { - variables.framework.home = variables.framework.defaultSection & '.' & variables.framework.defaultItem; + variables.framework.home = variables.framework.subsystemDelimiter & variables.framework.defaultSection & '.' & variables.framework.defaultItem; + if ( usingSubsystems() ) { + variables.framework.home = variables.framework.defaultSubsystem & variables.framework.home; } } if ( !structKeyExists(variables.framework, 'error') ) { - if (usingSubsystems()) { - variables.framework.error = variables.framework.defaultSubsystem & variables.framework.subsystemDelimiter & variables.framework.defaultSection & '.error'; - } else { - variables.framework.error = variables.framework.defaultSection & '.error'; + variables.framework.error = variables.framework.subsystemDelimiter & variables.framework.defaultSection & '.error'; + if ( usingSubsystems() ) { + variables.framework.error = variables.framework.defaultSubsystem & variables.framework.error; } } if ( !structKeyExists(variables.framework, 'reload') ) { @@ -2194,6 +2588,10 @@ component { if ( !structKeyExists(variables.framework, 'baseURL') ) { variables.framework.baseURL = 'useCgiScriptName'; } + // remove trailing "/" from baseURL + if ( len( variables.framework.baseURL ) > 1 && right( variables.framework.baseURL, 1 ) == '/' ) { + variables.framework.baseURL = left( variables.framework.baseURL, len( variables.framework.baseURL ) - 1 ); + } if ( !structKeyExists(variables.framework, 'generateSES') ) { variables.framework.generateSES = false; } @@ -2230,6 +2628,9 @@ component { if ( !structKeyExists( variables.framework, 'routes' ) ) { variables.framework.routes = [ ]; } + if ( !structKeyExists( variables.framework, 'perResourceError' ) ) { + variables.framework.perResourceError = true; + } if ( !structKeyExists( variables.framework, 'resourceRouteTemplates' ) ) { variables.framework.resourceRouteTemplates = [ { method = 'default', httpMethods = [ '$GET' ] }, @@ -2239,6 +2640,9 @@ component { { method = 'update', httpMethods = [ '$PUT','$PATCH' ], includeId = true }, { method = 'destroy', httpMethods = [ '$DELETE' ], includeId = true } ]; + if ( variables.framework.perResourceError ) { + arrayAppend( variables.framework.resourceRouteTemplates, { method = 'error', httpMethods = [ '$*' ] } ); + } } if ( !structKeyExists( variables.framework, 'routesCaseSensitive' ) ) { variables.framework.routesCaseSensitive = true; @@ -2249,11 +2653,46 @@ component { if ( !structKeyExists( variables.framework, 'trace' ) ) { variables.framework.trace = false; } + if ( !structKeyExists( variables.framework, 'controllersFolder' ) ) { + variables.framework.controllersFolder = 'controllers'; + } + if ( right( variables.framework.controllersFolder, 1 ) != 's' ) { + throw( type = "FW1.IllegalConfiguration", + message = "ControllersFolder must be a plural word (ends in 's')." ); + } + variables.controllerFolder = left( variables.framework.controllersFolder, len( variables.framework.controllersFolder ) - 1 ); + if ( !structKeyExists( variables.framework, 'layoutsFolder' ) ) { + variables.framework.layoutsFolder = 'layouts'; + } + if ( right( variables.framework.layoutsFolder, 1 ) != 's' ) { + throw( type = "FW1.IllegalConfiguration", + message = "LayoutsFolder must be a plural word (ends in 's')." ); + } + variables.layoutFolder = left( variables.framework.layoutsFolder, len( variables.framework.layoutsFolder ) - 1 ); + if ( !structKeyExists( variables.framework, 'subsystemsFolder' ) ) { + variables.framework.subsystemsFolder = 'subsystems'; + } + if ( right( variables.framework.subsystemsFolder, 1 ) != 's' ) { + throw( type = "FW1.IllegalConfiguration", + message = "SubsystemsFolder must be a plural word (ends in 's')." ); + } + variables.subsystemFolder = left( variables.framework.subsystemsFolder, len( variables.framework.subsystemsFolder ) - 1 ); + if ( !structKeyExists( variables.framework, 'viewsFolder' ) ) { + variables.framework.viewsFolder = 'views'; + } + if ( right( variables.framework.viewsFolder, 1 ) != 's' ) { + throw( type = "FW1.IllegalConfiguration", + message = "ViewsFolder must be a plural word (ends in 's')." ); + } + variables.viewFolder = left( variables.framework.viewsFolder, len( variables.framework.viewsFolder ) - 1 ); + if ( !structKeyExists( variables.framework, 'diOverrideAllowed' ) ) { + variables.framework.diOverrideAllowed = false; + } if ( !structKeyExists( variables.framework, 'diEngine' ) ) { variables.framework.diEngine = 'di1'; } if ( !structKeyExists( variables.framework, 'diLocations' ) ) { - variables.framework.diLocations = 'model,controllers'; + variables.framework.diLocations = 'model,' & variables.framework.controllersFolder; } if ( !structKeyExists( variables.framework, 'diConfig' ) ) { variables.framework.diConfig = { }; @@ -2261,26 +2700,49 @@ component { if ( !structKeyExists( variables.framework, 'diComponent' ) ) { var diComponent = 'framework.ioc'; switch ( variables.framework.diEngine ) { - case 'aop1': - diComponent = 'framework.aop'; - break; - case 'wirebox': - diComponent = 'framework.WireBoxAdapter'; - break; - case 'custom': - throw( type="FW1.IllegalConfiguration", - message="If you specify diEngine='custom' you must specify a component path for diComponent." ); - break; - default: - // assume DI/1 - break; + case 'aop1': + diComponent = 'framework.aop'; + break; + case 'wirebox': + diComponent = 'framework.WireBoxAdapter'; + break; + case 'custom': + throw( type="FW1.IllegalConfiguration", + message="If you specify diEngine='custom' you must specify a component path for diComponent." ); + break; + default: + // assume DI/1 + break; } variables.framework.diComponent = diComponent; } + if ( structKeyExists( variables.framework, 'enableJSONPOST' ) ) { + throw( type="FW1.IllegalConfiguration", + message="The enableJSONPOST setting has been renamed to decodeRequestBody." ); + } + if ( !structKeyExists( variables.framework, 'decodeRequestBody' ) ) { + variables.framework.decodeRequestBody = false; + } + if ( !structKeyExists( variables.framework, 'preflightOptions' ) ) { + variables.framework.preflightOptions = false; + } + if ( !structKeyExists( variables.framework, 'optionsAccessControl' ) ) { + variables.framework.optionsAccessControl = { }; + } setupEnvironment( env ); + if ( variables.framework.preflightOptions ) { + var defaultAccessControl = { + origin = "*", + headers = "Accept,Authorization,Content-Type", + credentials = true, + maxAge = 1728000 + }; + structAppend( variables.framework.optionsAccessControl, defaultAccessControl, false ); + } request._fw1.doTrace = variables.framework.trace; // add this as a fingerprint so autowire can detect FW/1 CFC: this.__fw1_version = variables.framework.version; + variables._fw1_defaults_initialized = true; } private string function setupFrameworkEnvironments() { @@ -2342,6 +2804,7 @@ component { } private void function setupRequestDefaults() { + setupFrameworkDefaults(); if ( !request._fw1.requestDefaultsInitialized ) { var pathInfo = request._fw1.cgiPathInfo; request.base = variables.framework.base; @@ -2358,19 +2821,32 @@ component { // pathInfo is bogus so ignore it: pathInfo = ''; } + request._fw1.currentRoute = ''; var routes = getRoutes(); if ( arrayLen( routes ) ) { internalFrameworkTrace( 'processRoutes() called' ); var routeMatch = processRoutes( pathInfo, routes ); if ( routeMatch.matched ) { internalFrameworkTrace( 'route matched - #routeMatch.route# - #pathInfo#' ); - pathInfo = rereplace( routeMatch.path, routeMatch.pattern, routeMatch.target ); + var routeTail = ''; + if ( variables.framework.routesCaseSensitive ) { + pathInfo = rereplace( routeMatch.path, routeMatch.pattern, routeMatch.target ); + routeTail = rereplace( routeMatch.path, routeMatch.pattern, '' ); + } else { + pathInfo = rereplacenocase( routeMatch.path, routeMatch.pattern, routeMatch.target ); + routeTail = rereplacenocase( routeMatch.path, routeMatch.pattern, '' ); + } + request._fw1.currentRoute = left( routeMatch.path, len( routeMatch.path ) - len( routeTail ) ); if ( routeMatch.redirect ) { location( pathInfo, false, routeMatch.statusCode ); } else { request._fw1.route = routeMatch.route; } } + } else if ( variables.framework.preflightOptions && request._fw1.cgiRequestMethod == "OPTIONS" ) { + // non-route matching but we have OPTIONS support enabled + request._fw1.routeMethodsMatched.get = true; + request._fw1.routeMethodsMatched.post = true; } try { // we use .split() to handle empty items in pathInfo - we fallback to listToArray() on @@ -2388,6 +2864,13 @@ component { pathInfo = listToArray( pathInfo, '/' ); } var sesN = arrayLen( pathInfo ); + if ( !len( request._fw1.currentRoute ) ) { + switch ( sesN ) { + case 0 : request._fw1.currentRoute = '/'; break; + case 1 : request._fw1.currentRoute = '/' & pathInfo[1] & '/'; break; + default: request._fw1.currentRoute = '/' & pathInfo[1] & '/' & pathInfo[2] & '/'; break; + } + } if ( ( sesN > 0 || variables.framework.generateSES ) && getBaseURL() != 'useRequestURI' ) { request._fw1.generateSES = true; } @@ -2403,18 +2886,63 @@ component { } } // certain remote calls do not have URL or form scope: - if ( isDefined('URL') ) structAppend(request.context,URL); - if ( isDefined('form') ) structAppend(request.context,form); + if ( isDefined( 'URL' ) ) structAppend( request.context, URL ); + var httpData = getHttpRequestData(); + if ( variables.framework.decodeRequestBody ) { + // thanks to Adam Tuttle and by proxy Jason Dean and Ray Camden for the + // seed of this code, inspired by Taffy's basic deserialization + // also thanks to John Whish for the URL-encoded form support + // which adds support for PUT etc + var body = httpData.content; + if ( isBinary( body ) ) body = charSetEncode( body, "utf-8" ); + if ( len( body ) ) { + switch ( listFirst( CGI.CONTENT_TYPE, ';' ) ) { + case "application/json": + case "text/json": + try { + var bodyStruct = read_json( body ); + structAppend( request.context, bodyStruct ); + } catch ( any e ) { + throw( type = "FW1.JSONPOST", + message = "Content-Type implies JSON but could not deserialize body: " & e.message ); + } + break; + case "application/x-www-form-urlencoded": + try { + var paramPairs = listToArray( body, "&" ); + for ( var pair in paramPairs ) { + var parts = listToArray( pair, "=", true ); // handle blank values + var keyName = parts[ 1 ]; + var keyValue = urlDecode( parts[ 2 ] ); + if ( !structKeyExists( request.context, keyName ) ) { + request.context[ keyName ] = keyValue; + } else { + request.context[ keyName ] = listAppend( request.context[ keyName ], keyValue ); + } + } + } catch ( any e ) { + throw( type = "FW1.JSONPOST", + message = "Content-Type implies form encoded but could not deserialize body: " & e.message ); + } + break; + default: + // ignore -- either built-in (form handling) or unsupported + break; + } + } + } + if ( isDefined( 'form' ) ) structAppend( request.context, form ); + request._fw1.headers = httpData.headers; // figure out the request action before restoring flash context: - if ( !structKeyExists(request.context, variables.framework.action) ) { - request.context[variables.framework.action] = variables.framework.home; + if ( !structKeyExists( request.context, variables.framework.action ) ) { + request.context[ variables.framework.action ] = getFullyQualifiedAction( variables.framework.home ); } else { - request.context[variables.framework.action] = getFullyQualifiedAction( request.context[variables.framework.action] ); + request.context[ variables.framework.action ] = getFullyQualifiedAction( request.context[ variables.framework.action ] ); } if ( variables.framework.noLowerCase ) { - request.action = validateAction( request.context[variables.framework.action] ); + request.action = validateAction( request.context[ variables.framework.action ] ); } else { - request.action = validateAction( lCase(request.context[variables.framework.action]) ); + request.action = validateAction( lCase(request.context[ variables.framework.action ]) ); } request._fw1.requestDefaultsInitialized = true; } @@ -2426,14 +2954,11 @@ component { request.subsystembase = request.base & getSubsystemDirPrefix( request.subsystem ); request.section = getSection( request.action ); request.item = getItem( request.action ); + request._fw1.theFramework = this; // for use in the facade (only!) if ( runSetup ) { - if ( usingSubsystems() ) { - controller( variables.magicApplicationSubsystem & variables.framework.subsystemDelimiter & - variables.magicApplicationController & '.' & variables.magicApplicationAction ); - } else { - controller( variables.magicApplicationController & '.' & variables.magicApplicationAction ); - } + controller( variables.magicApplicationSubsystem & variables.framework.subsystemDelimiter & + variables.magicApplicationController & '.' & variables.magicApplicationAction ); setupSubsystemWrapper( request.subsystem ); internalFrameworkTrace( 'setupRequest() called' ); setupRequest(); @@ -2453,7 +2978,7 @@ component { } private void function setupSubsystemWrapper( string subsystem ) { - if ( !usingSubsystems() ) return; + if ( !len( subsystem ) ) return; lock name="fw1_#application.applicationName#_#variables.framework.applicationKey#_subsysteminit_#subsystem#" type="exclusive" timeout="30" { if ( !isSubsystemInitialized( subsystem ) ) { getFw1App().subsystems[ subsystem ] = now(); @@ -2464,7 +2989,7 @@ component { if ( diEngine == "di1" || diEngine == "aop1" ) { // we can only reliably automate D/I engine setup for DI/1 / AOP/1 var diLocations = structKeyExists( subsystemConfig, 'diLocations' ) ? subsystemConfig.diLocations : variables.framework.diLocations; - var locations = listToArray( diLocations ); + var locations = isSimpleValue( diLocations ) ? listToArray( diLocations ) : diLocations; var subLocations = ""; for ( var loc in locations ) { var relLoc = trim( loc ); @@ -2474,17 +2999,26 @@ component { } else if ( len( relLoc ) > 1 && left( relLoc, 1 ) == "/" ) { relLoc = right( relLoc, len( relLoc ) - 1 ); } - subLocations = listAppend( subLocations, variables.framework.base & subsystem & "/" & relLoc ); + if ( usingSubsystems() ) { + subLocations = listAppend( subLocations, variables.framework.base & subsystem & "/" & relLoc ); + } else { + subLocations = listAppend( subLocations, variables.framework.base & variables.framework.subsystemsFolder & "/" & subsystem & "/" & relLoc ); + } + } + if ( len( sublocations ) ) { + var diComponent = structKeyExists( subsystemConfig, 'diComponent' ) ? subsystemConfig : variables.framework.diComponent; + var cfg = { }; + if ( structKeyExists( subsystemConfig, 'diConfig' ) ) { + cfg = subsystemConfig.diConfig; + } else { + cfg = structCopy( variables.framework.diConfig ); + structDelete( cfg, 'loadListener' ); + } + cfg.noClojure = true; + var ioc = new "#diComponent#"( subLocations, cfg ); + ioc.setParent( getDefaultBeanFactory() ); + setSubsystemBeanFactory( subsystem, ioc ); } - var diComponent = structKeyExists( subsystemConfig, 'diComponent' ) ? subsystemConfig : variables.framework.diComponent; - var ioc = new "#diComponent#"( - subLocations, - ( structKeyExists( subsystemConfig, 'diConfig' ) ? - subsystemConfig.diConfig : - variables.framework.diConfig ) - ); - ioc.setParent( getDefaultBeanFactory() ); - setSubsystemBeanFactory( subsystem, ioc ); } } @@ -2497,8 +3031,8 @@ component { private string function validateAction( string action ) { // check for forward and backward slash in the action - using chr() to avoid confusing TextMate (Hi Nathan!) if ( findOneOf( chr(47) & chr(92), action ) > 0 ) { - raiseException( type='FW1.actionContainsSlash', message="Found a slash in the action: '#action#'.", - detail='Actions are not allowed to embed sub-directory paths.'); + throw( type='FW1.actionContainsSlash', message="Found a slash in the action: '#action#'.", + detail='Actions are not allowed to embed sub-directory paths.'); } return action; } @@ -2508,8 +3042,8 @@ component { // but this will prevent an exception while attempting to throw // the exception we actually want to throw! param name="request.missingView" default=""; - raiseException( type='FW1.viewNotFound', message="Unable to find a view for '#request.action#' action.", - detail="'#request.missingView#' does not exist." ); + throw( type='FW1.viewNotFound', message="Unable to find a view for '#request.action#' action.", + detail="'#request.missingView#' does not exist." ); } } diff --git a/layouts/docs.cfm b/layouts/docs.cfm index 366bd90..ef2aa90 100644 --- a/layouts/docs.cfm +++ b/layouts/docs.cfm @@ -5,7 +5,7 @@ - + @@ -14,15 +14,15 @@ - + - - + + @@ -33,7 +33,7 @@ - + @@ -47,32 +47,32 @@ - + - - + + - + - - + + - + @@ -82,5 +82,5 @@

 

-#body# +#body# diff --git a/vendor/tags/bootstrap/bootstrap.cfc b/vendor/tags/bootstrap/bootstrap.cfc index 9ced63d..dc1205c 100644 --- a/vendor/tags/bootstrap/bootstrap.cfc +++ b/vendor/tags/bootstrap/bootstrap.cfc @@ -2,32 +2,32 @@ component output="false" { - - - + + + void function setupApplication() { - - + + application.Bootstrap = { - + // Antisamy options isSafeHTML = [ "cf_buttongroup", "cf_buttontoolbar", "cf_column", "cf_container", "cf_dropmenu", "cf_fieldset", "cf_formgroup", "cf_head", "cf_include", "cf_jumbotron", "cf_modal", "cf_navbar", "cf_navbarlinks", "cf_navlink", - "cf_panel", "cf_row", "cf_table", "cf_tabview", "cf_tr", "cf_well"], // these tags to not run through getSafeHTML + "cf_panel", "cf_row", "cf_table", "cf_tabview", "cf_tr", "cf_well"], // these tags to not run through getSafeHTML profile = "", // blank means use system default throwOnError = false, // Default behavior for getSafeHTML() - + // Cacheing cache = {content = "Bootstrap", language = "i18n" }, // Only Bootstrap should use this - + // i18n options langRoot = expandPath("vendor/lang") & "/", arLang = [], - + actionRoot = "http://" & cgi.server_name & (cgi.server_port == 80 ? "" : ":" & cgi.server_port) & cgi.script_name & "/", - validLook = ["", "link", "default", "primary", "success", "info", "warning", "danger"], // There does not guarantee they are valid - + validLook = ["", "link", "default", "primary", "success", "info", "warning", "danger"], // There does not guarantee they are valid + // be sure to include ending dashes iconLibrary = { "default" = "glyphicon glyphicon-", // make default however you like "glyphicon" = "glyphicon glyphicon-", // Glyphicon: http://glyphicons.com/ @@ -37,166 +37,166 @@ void function setupApplication() { "octicon" = "octicon octicon-", // Github Icons: https://octicons.github.com/ "mega-octicon" = "mega-octicon octicon-", // Octicons resize in their own special way "jquery-ui" = "ui-icon ui-icon-" // jQuery UI: https://jqueryui.com/ - }, - - - + }, + + + imageLibrary = {"default" = replace(cgi.script_name, "/index.cfm", "") & "/assets/"}, // used by b:graphicImage - + // used by b:outputStyleSheet - styleSheetLibrary = {"default" = "https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css", + styleSheetLibrary = {"default" = "https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css", local = replace(cgi.script_name, "/index.cfm", "") & "/assets/", innovation = replace(cgi.script_name, "/index.cfm", "") & "/layouts/innovation/", vendor = replace(cgi.script_name, "/index.cfm", "") & "/vendor/" }, - + // used by b:outputScript - scriptLibrary = {"default" = "https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js", + scriptLibrary = {"default" = "https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js", local = replace(cgi.script_name, "/index.cfm", "") & "/assets/", vendor = replace(cgi.script_name, "/index.cfm", "") & "/vendor/" } }; - - - // Clear out bootstrap caches + + + // Clear out bootstrap caches if(cacheRegionExists(application.Bootstrap.cache.content)) CacheRemoveAll(application.Bootstrap.cache.content); if(cacheRegionExists(application.Bootstrap.cache.language)) CacheRemoveAll(application.Bootstrap.cache.language); - - + + application.geti18n = this.geti18n; application.filterAttributes = this.filterAttributes; application.generateContent = this.generateContent; - - + + // Language control - application.lang = "en_US"; // needs to be smarter + application.lang = "en_US"; // needs to be smarter } - - + + void function setupRequest() { - - + + if(application.Bootstrap.arLang.isEmpty()) { for(local.langFile in DirectoryList(application.Bootstrap.langRoot, false, "path", "*.php")) { application.Bootstrap.arLang.append(local.langFile.listLast("/").listLast("\").listFirst(".")); } - } - - - + } + + + if(!cacheRegionExists(application.Bootstrap.cache.content)) CacheRegionNew(application.Bootstrap.cache.content); if(!cacheRegionExists(application.Bootstrap.cache.language)) CacheRegionNew(application.Bootstrap.cache.language); - - + + // note: renaming application will not load new languages, you have to restart the cf service - if(cacheGetAllIds(application.Bootstrap.cache.language).isEmpty()) { - - + if(cacheGetAllIds(application.Bootstrap.cache.language).isEmpty()) { + + + + - - local.i18n = {}; local.i18n.append( this.readPHP(application.Bootstrap.langroot) ); // traditional language file // simple append won't work local.stTitle = this.readProperties( expandPath(".") & "/title.properties" ); - + for (local.languageKey in local.stTitle) { - + local.i18n[local.languageKey].append( local.stTitle[local.languageKey] ); // titles are here } - + for (local.languageKey in local.i18n) { CachePut(local.languageKey, local.i18n[local.languageKey], 1, 1, application.Bootstrap.cache.language); } - + } - + } // Functions for tags string function geti18n(required string key, any placeholder = []) output="false" { - + if (!isArray(arguments.placeholder)) arguments.placeholder = [arguments.placeholder]; - - + + if (CacheIdExists(application.lang, application.Bootstrap.cache.language)) { - + local.stLang = cacheGet(application.lang, application.Bootstrap.cache.language); - - - + + + if (local.stLang.keyExists(arguments.key)) { - - local.myString = local.stLang[arguments.key]; - + + local.myString = local.stLang[arguments.key]; + for (var i in arguments.placeholder) { - + local.myString = local.myString.replace('%s', i); // only does first match } - + return getSafeHTML(local.myString); } // end keyExists } // end cacheIdExists - - + + return "{#arguments.key#}"; } - + // See: http://codereview.stackexchange.com/questions/121963/filtering-the-attributes-for-a-custom-tag string function filterAttributes(required struct attr, string attributeFilter = "id|name|lang|style|role|rel|target|disabled|readonly|required", boolean tooltip = true) output="false" { - - + + // Patch this if(arguments.attr?.disabled == true) arguments.attr.disabled = "disabled"; if(arguments.attr?.readonly == true) arguments.attr.readonly = "readonly"; if(arguments.attr?.required == true) arguments.attr.required = "required"; - - var prev = ""; - - local.result = arguments.attr.filter(function(key, value) { + + var prev = ""; + + local.result = arguments.attr.filter(function(key, value) { return (attributeFilter.ListFind(key) || key.reFindNoCase("data\-|ng\-|on" && key != "throwonError") ); })?.reduce(function(string prev, string key, string value) { return prev & ' #key.lcase()# = "#value.encodeForHTMLAttribute()#"'; } ); - + param local.result = ""; - + if(arguments.attr?.tooltip != "" && arguments.tooltip) { param arguments.attr.tooltipPosition = "bottom"; - + local.result &= ' title = "#arguments.attr.tooltip.encodeForHTMLAttribute()#"'; local.result &= ' data-placement="#arguments.attr.tooltipPosition.encodeForHTMLAttribute()#"'; - local.result &= ' data-toggle="tooltip"'; + local.result &= ' data-toggle="tooltip"'; } - - + + return local?.result; - } + } string function generateContent(required string Content, required array tagstack, required struct attr) output="false" { - - + + if(arguments.attr?.key != "" ) { arguments.Content = application.geti18n(arguments.attr.key, arguments.attr?.placeholder); arguments.attr.isSafeHTML = true; - } - + } + param arguments.attr.isSafeHTML = application.Bootstrap.isSafeHTML.contains(arguments.tagStack[1].lcase()); if(arguments.attr.isSafeHTML) return arguments.Content.trim(); // warning content must already be clean - - + + param arguments.attr.profile = application.Bootstrap.profile; param arguments.attr.throwOnError = application.Bootstrap.throwOnError; @@ -208,27 +208,27 @@ string function generateContent(required string Content, required array tagstack struct function readProperties(required string propertyfile) { - + local.stResult = {}; - + local.stSection = getProfileSections(arguments.propertyfile.replace("\", "/", "all")); - - + + for(local.section in local.stSection) { - - local.CurrentSection = local.stSection[local.section].listToArray(); - + + local.CurrentSection = local.stSection[local.section].listToArray(); + local.stData = {}; - + for(local.key in CurrentSection) { - + local.stData[local.key] = getProfileString(arguments.propertyfile, local.section, local.key); } - + local.stResult[local.section] = local.stData; } - - + + return stResult; } @@ -238,80 +238,80 @@ struct function readPHP(required string phpPath) { local.stProperties = {}; - - + + local.arDirectory = DirectoryList(arguments.phpPath, false, "path", "*.php"); - - + + for (local.phpFile in local.arDirectory) { - + local.phpFile = local.phpFile.replace("\", "/", "all"); - + local.languageKey = local.phpFile.listLast("/").listFirst("."); local.stProperties[languageKey] = {}; - + local.stProperties[languageKey]._reading = local.phpFile; - - + + local.phpText = fileRead(local.phpFile).trim(); - + // remove comments and blank lines local.phpText = phpText.ReReplace("(?m)\##.*?$", "", "all"); local.phpText = phpText.ReReplace("[#Chr(10)#]{2,}", chr(10), "all"); local.phpText = phpText.ReplaceList('",', '"'); - - // loop over each line, ignore comments (#...) and insert keys/values into return struct + + // loop over each line, ignore comments (#...) and insert keys/values into return struct for(local.line in phpText.ListToArray(chr(10))) { - + var splitAt = local.line.find("=>"); - var commentAt = local.line.find("//"); - - + var commentAt = local.line.find("//"); + + if (local.splitAt != 0) { - + var key = local.line.left(local.splitAt - 1).trim().replacelist('"', ""); - key = key.replace(',', '', 'all'); - key = key.replace("'", "", 'all').trim(); - + key = key.replace(',', '', 'all'); + key = key.replace("'", "", 'all').trim(); + value = local.line.find("//") ? local.line.mid(splitAt + 2, CommentAt - (splitAt + 2) ).trim().replacelist('"', '') - : + : local.line.mid(splitAt + 2, 1000 ).trim().replacelist('"', '') ; - - - - - // Remove trailing , + + + + + // Remove trailing , if (value.right(1) == ",") value = value.mid(1, value.len() - 1); - - + + if (value.right(1) == "'") value = value.mid(1, value.len() - 1); - + if (value.left(1) == "'") value = value.mid(2, 1000); - + stProperties[languageKey][key] = value; - + } // valid split at - + } // end line - + } // end file - - - - + + + + return stProperties; } // end funciton - - + + } // end component