All pages
Powered by GitBook
1 of 7

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Debugging ColdBox Apps

We have a special module just for helping you monitor and debug ColdBox applications. To install, fire up CommandBox and install it as a dependency in your application.

CbDebugger Requirements

  • Hibernate extension (on Lucee)

  • ORM package (on ACF 2021)

Remember that this module is for development so use the --saveDev flag. This will install the ColdBox Debugger module for you, which will attach a debugger to the end of the request and give you lots of flexibility. Please read the instructions here in order to spice it up as you see fit:

Application Templates

We have a Github organization called that has a great collection of application startup templates. You can collaborate with the existing ones or send us new ones to add.

CommandBox Integration

The coldbox create app command has integration to our application templates via the skeleton argument. This can be the name of each of the templates in our repositories or you can use the following alternatives:

Name of template in our organization: advanced,simple,super-simple,rest, rest-hmvc, vuejs, etc

  • A name of a ForgeBox entry: cbtemplate-advanced-script,cbtemplate-simple

  • A Github shortcut: github-username/repo

  • An HTTP/S URL to a zip file containing a template: http://myapptemplates.com/template.zip

  • A folder containing a template: /opt/shared/templates/my-template

  • A zip file containing the template: /opt/shared/templates/my-template.zip

  • coldbox-templates
    install cbdebugger --saveDev
    https://github.com/coldbox-modules/cbdebugger

    Clearing the View Cache

    Introduction

    If you use any of the view partial caching mechanisms in Coldbox either through setView() orrenderView() you might be asking yourself:

    Is there an easy, programmatic way to remove a specific element from the view cache?

    The answer is, of course! All view and event caching occurs in a cache provider called template and you can retrieve it like so from your handlers, layouts, views, plugins and interceptors:

    You can also use the WireBox injection DSL

    Clearing methods

    There are a few methods that will help you clear views:

    • clearView(viewSnippet) - Clear views with a snippet

    • clearAllViews(async) - Clear all views

    • clearViewMulti(viewSnippets) - Clear multiple view snippets with a list or array of snippets

    Very easy! Just send in what you need and it will be purged.

    var cache = cachebox.getCache("template");
    property name="cache" inject="cachebox:template"
    getCache( "template" ).clearView('home');

    ColdBox Exception Handling

    ColdBox provides you with several ways to handle different types of exceptions during a typical ColdBox request execution:

    • Global exception handler

    • Global onException interceptor

    • Global invalid event handler

    • Global onInvalidEvent interceptor

    • Global missing template handler

    • Handler onMissingAction()

    • Handler onError()

    • Handler onInvalidHTTPMethod

    Global Exception Handler

    The global exception handler will manage any runtime exception that occurs during the flow of a typical ColdBox request execution. This could be an exception at the handler, model, or view levels. This feature is activated by configuring the coldbox.exceptionhandler setting in your configuration ColdBox.cfc. The value of the setting is the event that will act as your global exception handler.

    This event can the be used for logging the exception, relocating to a fail page or aborting the request. By default once the exception handler executes, ColdBox will continue the request by presenting the user the default ColdBox error page or a customized error template of your choosing via the coldbox.customErrorTemplate setting. ColdBox will also place an object in the private request collection called exception. This object models the entire exception and request data that created the exception. You can then use it to get any information needed to handle the exception.

    Caution Please note that if you set a view for rendering or renderdata in this exception handler, nothing will be rendered. The framework is in exception mode and will not allow any custom renderings or views as that could also produce exceptions as well.

    Global onException Interceptor

    This is a standard ColdBox interception point that can be used to intercept whenever a global exception has occurred in the system. The interceptor call is actually made before the global exception handler, any automatic logging or presentation of the error page to the user. This is the first line of defense towards exceptions. You might be asking yourself, what is the difference between writing an onException() interceptor and just a simple exception handler? Well, the difference is in the nature of the design.

    Interceptors are designed to be decoupled classes that can react to announced events, thus an event-driven approach. You can have as many CFCs listening to the onException event and react accordingly without them ever knowing about each other and doing one job and one job only. This is a much more flexible and decoupled approach than calling a single event handler where you will procedurally decide what happens in an exception.

    Also remember that you need to register the interceptor in your configuration file or dynamically so ColdBox knows about it:

    Caution Remember that the onException() interceptors execute before the global exception handler, any automatic error logging and presentation of the core or custom error templates.

    Global Invalid Event Handler

    The global invalid event handler allows you to configure an event to execute whenever ColdBox detects that the requested event does not exist. This is a great way to present the user with page not found exceptions and 404 error codes. The setting is called coldbox.invalidEventHandler and can be set in your configuration ColdBox.cfc. The value of the setting is the event that will handle these missing events.

    Please note the importance of setting a 404 header as this identifies to the requester that the requested event does not exist in your system. Also note that if you do not set a view for rendering or render data back, the request might most likely fail as it will try to render the implicit view according to the incoming event.

    ColdBox will also place the invalid event requested in the private request collection with the value of invalidevent.

    Global onInvalidEvent Interceptor

    This is a standard ColdBox interception point that can be used to intercept whenever a requested event in your application was invalid. You might be asking yourself, what is the difference between writing an onInvalidEvent() interceptor and just a simple invalid event handler? Well, the difference is in the nature of the design.

    Interceptors are designed to be decoupled classes that can react to announced events, thus an event-driven approach. You can have as many CFCs listening to the onInvalidEvent event and react accordingly without them ever knowing about each other and doing one job and one job only. This is a much more flexible and decoupled approach than calling a single event handler where you will procedurally decide what happens in an invalid event.

    The interceptData argument receives the following variables:

    • invalidEvent : The invalid event string

    • ehBean : The internal ColdBox event handler bean

    • override : A boolean indicator that you overwrote the event

    You must tell ColdBox that you want to override the invalid event (override = true) and you must set in the ehBean to tell ColdBox what event to execute:

    Also remember that you need to register the interceptor in your configuration file or dynamically so ColdBox knows about it:

    Global Missing Template Handler

    The global missing template handler allows you to configure an event to execute whenever ColdBox detects a request to a non-existent CFML page. This is a great way to present the user with page not found exceptions or actually use it to route the request in a dynamic matter, just like if those pages existed on disk. The setting is called coldbox.missingTemplateHandler and can be set in your configuration ColdBox.cfc. The value of the setting is the event that will handle these missing pages.

    Info Note that in order for this functionality to work the method onMissingTemplate() must exist in the Application.cfc with the default ColdBox handler code.

    ColdBox will place the path to the requested page as a request collection variable called missingTemplate, which you can parse and route or just use.

    Handler onMissingAction()

    This approach allows you to intercept at the handler level when someone requested an action (method) that does not exist in the specified handler. This is really useful when you want to respond to dynamic requests like /page/contact-us, /page/hello, where page points to a Page.cfc and the rest of the URL will try to match to an action that does not exist. You can then use that portion of the URL to lookup a dynamic record. However, you can also use it to detect when invalid actions are sent to a specific handler.

    Handler onError()

    This approach allows you to intercept at the handler level whenever a runtime exception has occurred. This is a great approach when creating a family of event handlers and you create a base handler with the onError() defined in it. We have found tremendous success with this approach when building ColdBox RESTFul services in order to provide uniformity for all RESTFul handlers.

    *Caution Please note that this only traps runtime exceptions, compiler exceptions will bubble up to a global exception handler or interceptor.

    Handler onInvalidHTTPMethod()

    This approach allows you to intercept at the handler level whenever an action execution is requested with an invalid HTTP verb. This is a great approach when creating a family of event handlers and you create a base handler with the onInvalidHTTPMethod() defined in it. We have found tremendous success with this approach when building ColdBox RESTFul services in order to provide uniformity for all RESTFul handlers.

    coldbox = {
        ...
        exceptionHandler = "main.onException"
        ...
    };
    function onException(event,rc,prc){
        // Log the exception via LogBox
        log.error( prc.exception.getMessage() & prc.exception.getDetail(), prc.exception.getMemento() );
    
        // Flash where the exception occurred
        flash.put("exceptionURL", event.getCurrentRoutedURL() );
    
        // Relocate to fail page
        relocate("main.fail");
    }
    component extends="coldbox.system.Interceptor"{
    
        function onException(event, interceptData){
            // Get the exception
            var exception = arguments.interceptData.exception;
    
            // Do some logging only for some type of error and relocate
            if( exception.type eq "myType" ){
                log.error( exception.message & exception.detail, exception );
                // relocate
                relocate( "page.invalidSave" );
            }
        }
    }
    interceptors = [
    
        // Register exception handler
        { class = "interceptors.ExceptionHandler", properties = {} }
    
    ];
    coldbox = {
        ...
        invalidEventHandler = "main.pageNotFound"
        ...
    };
    function pageNotFound(event,rc,prc){
        // Log a warning
        log.warn( "Invalid page detected: #prc.invalidEvent#");
    
        // Do a quick page not found and 404 error
        event.renderData( data="<h1>Page Not Found</h1>", statusCode=404 );
    
        // Set a page for rendering and a 404 header
        event.setView( "main/pageNotFound" ).setHTTPHeader( "404", "Page Not Found" );
    }
    component extends="coldbox.system.Interceptor"{
    
        function onInvalidEvent(event, interceptData){
            // Log a warning
            log.warn( "Invalid page detected: #arguments.interceptData.invalidEvent#");
    
            // Set the invalid event to run
            arguments.interceptData.ehBean
                .setHandler("Main")
                .setMethod("pageNotFound");
    
            // Override
            arguments.interceptData.override = true;
        }
    }
    interceptors = [
    
        // Register invalid event handler
        { class = "interceptors.InvalidHandler", properties = {} }
    
    ];
    coldbox = {
        ...
        missingTemplateHandler = "main.missingTemplate"
        ...
    };
    function missingTemplate(event,rc,prc){
        // Log a warning
        log.warn( "Missing page detected: #rc.missingTemplate#");
    
        // Do a quick page not found and 404 error
        event.renderData( data="<h1>Page Not Found</h1>", statusCode=404 );
    
        // Set a page for rendering and a 404 header
        event.setView( "main/pageNotFound" ).setHTTPHeader( "404", "Page Not Found" );
    }
    function onMissingAction(event,rc,prc,missingAction,eventArguments){
    
        // lookup the missing action in our mini cms
        prc.page = pageService.findBySlug( arguments.missingAction );
    
        if( !prc.page.isPersisted() ){
            prc.missingPage = arguments.missingAction;
            event.setView( "page/notFound" );
        }
    
        // Else present the page in a dynamic layout
        event.setView( view="page/display", layout=prc.page.getLayout() );
    
    }
    // error uniformity for resources
    function onError(event,rc,prc,faultaction,exception){
        prc.response = getInstance("ResponseObject");
    
        // setup error response
        prc.response.setError(true);
        prc.response.addMessage("Error executing resource #arguments.exception.message#");
    
        // log exception
        log.error( "The action: #arguments.faultaction# failed when requesting resource: #arguments.event.getCurrentRoutedURL()#", getHTTPRequestData() );
    
        // display
        arguments.event.setHTTPHeader(statusCode="500",statusText="Error executing resource #arguments.exception.message#")
            .renderData( data=prc.response.getDataPacket(), type="json" );
    }
    /**
    * on invalid http verbs
    */
    function onInvalidHTTPMethod( event, rc, prc, faultAction, eventArguments ){
        // Log Locally
        log.warn( "InvalidHTTPMethod Execution of (#arguments.faultAction#): #event.getHTTPMethod()#", getHTTPRequestData() );
        // Setup Response
        prc.response = getInstance( "Response" )
            .setError( true )
            .setErrorCode( 405 )
            .addMessage( "InvalidHTTPMethod Execution of (#arguments.faultAction#): #event.getHTTPMethod()#" )
            .setStatusCode( 405 )
            .setStatusText( "Invalid HTTP Method" );
        // Render Error Out
        event.renderData( 
            type        = prc.response.getFormat(),
            data         = prc.response.getDataPacket(),
            contentType = prc.response.getContentType(),
            statusCode     = prc.response.getStatusCode(),
            statusText     = prc.response.getStatusText(),
            location     = prc.response.getLocation(),
            isBinary     = prc.response.getBinary()
        );
    }

    Building REST APIs

    Overview

    REST APIs are a popular and easy way to add HTTP endpoints to your web applications to act as web services for third parties or even other internal systems. REST is simpler and requires less verbosity and overhead than other protocols such as SOAP or XML-RPC.

    Creating a fully-featured REST API is easy with the ColdBox Platform. Everything you need for creating routes, working with headers, basic auth, massaging data, and enforcing security comes out of the box. We even have several application templates just for REST, so you can use CommandBox to create your first RESTFul app:

    You will then see the following JSON output:

    The rest template is a basic REST template that does not rely on modules or versioning. If you would like to add versioning and HMVC modularity use the rest-hmvc template. You can also find a full demo here:

    Quick Reference Card

    Below you can download our quick reference card on RESTFul APIs

    Introduction To REST

    REST stands for Representational State Transfer and builds upon the basic idea that data is represented as resources and accessed via a URI, or unique address. An HTTP client (such as a browser, or the CFHTTP tag) can send requests to a URI to interact with it. The HTTP verb (GET, POST, etc) sent in the header of the request tells the server how the client wants to interact with that resource.

    As far as how your data is formatted or how you implement security is left up to you. REST is less prescriptive than other standards such as SOAP (which uses tons of heavy XML and strictly-typed parameters). This makes it more natural to understand and easier to test and debug.

    Defining Resources

    A REST API can define its resources on its own domain (https://api.example.com), or after a static placeholder that differentiates it from the rest of the app (https://www.example.com/api/). We'll use the latter for these examples.

    Please note that we have an extensive . Please check out our of the docs.

    Let's consider a resource we need to represent called user. Resources should usually be nouns. If you have a verb in your URL, you're probably doing it wrong.

    Hint It is also important to note that REST is a style of URL architecture not a mandate, so it is an open avenue of sorts. However, you must stay true to its concepts of resources and usage of the HTTP verbs.

    Here are a few pointers when using the HTTP verbs:

    • GET /api/user will return a representation of all the users. It is permissible to use a query string to control pagination or filtering.

    • POST /api/user/ will create a new user

    • GET /api/user/53 will return a representation of user 53

    GET, PUT, and DELETE methods should be idempotent which means repeated requests to the same URI don't do anything. Repeated POST calls however, would create multiple users.

    In ColdBox, the easiest way to represent our /api/user resource is to create a handler called user.cfc in the /handlers/api/ directory. In this instance, ColdBox will consider the api to be a handler package. You can leverage CommandBox for this:

    Hint This command will create all the necessary files for you and even the integration tests for you.

    Here in my handler, I have stubbed out actions for each of the operations I need to perform against my user resource.

    /handlers/api/user.cfc

    Defining URL Routes

    Now that we have this skeleton in place to represent our user resource, let's move on to show how you can have full control of the URL as well as mapping HTTP verbs to specific handler actions.

    The default route for our user.cfc handler is /api/user, but what if we want the resource in the URL to be completely different than the handler name convention? To do this, use the /config/Router.cfc. file to declare URL routes we want the application to capture and define how to process them. This is your and it is your best friend!

    Install the route-visualizer module to visualize the router graphically. This is a huuuuge help when building APIs or anything with routes.

    install route-visualizer

    Let's add our new routes BEFORE the default route. We add them BEFORE because you must declare routes from the most specific to the most generic. Remember, routes fire in declared order.

    You can see if that if action is a string, all HTTP verbs will be mapped there, however a struct can also be provided that maps different verbs to different actions. This gives you exact control over how the requests are routed. We recommend you check out our as you can build very expressive and detailed URL patterns.

    Route Placeholders

    The :userID part of the . It matches whatever text is in the URL in that position. The value of the text that is matched will be available to you in the request collection as rc.userID. You can get even more specific about what kind of text you want to match in your route pattern.

    Numeric Pattern Matcher

    Append -numeric to the end of the placeholder to only match numbers.

    addRoute( pattern = 'user/:userID-numeric' );

    This route will match user/123 but not user/bob.

    Alphabetic Pattern Matcher

    Append -alpha to the end of the placeholder to only match upper and lowercase letters.

    addRoute( pattern = 'page/:slug-alpha' );

    This route will match page/contactus but not page/contact-us3.

    Regex Pattern Matcher

    For full control, you can specify your own regex pattern to match parts of the route

    addRoute( pattern = 'api/:resource-regex(user|person)' );

    This route will match api/user and api/person, but not /api/contact

    Placeholder Quantifiers

    You can also add the common regex {} quantifier to restrict how many digits a placeholder should have or be between:

    If a route is not matched it will be skipped and the next route will be inspected. If you want to validate parameters and return custom error messages inside your handler, then don't put the validations on the route.

    As you can see, you have many options to craft the URL routes your API will use. Routes can be as long as you need. You can even nest levels for URLs like /api/users/contact/address/27 which would refer to the address resource inside the contact belonging to a user.

    Returning Representations (Data)

    REST does not dictate the format you use to represent your data. It can be JSON, XML, WDDX, plain text, a binary file or something else of your choosing.

    Handler Return Data

    The most common way to return data from your handlers is to simply return it. This leverages the capabilities of ColdBox, which will detect the return variables and marshal accordingly:

    • String => HTML

    • Complex => JSON

    This approach allows the user to render back any string representation and be able to output any content type they like.

    renderData()

    The next most common way to return data from your handler's action is to use the Request Context renderData() method. It takes complex data and turns it into a . Here are some of the most common formats supported by event.renderData():

    • XML

    • JSON/JSONP

    • TEXT

    • WDDX

    Format Detection

    Many APIs allow the user to choose the format they want back from the endpoint. ColdBox will inspect the Accepts header to determine the right format to use by default or the URI for an extension.

    Another way to do this is by appending a file extension to the end of the URL:

    ColdBox has built-in support for detecting an extension in the URL and will save it into the request collection in a variable called format. What's even better is that renderData() can find the format variable and automatically render your data in the appropriate way. All you need to do is pass in a list of valid rendering formats and renderData() will do the rest.

    Status Codes

    Status codes are a core concept in HTTP and REST APIs use them to send messages back to the client. Here are a few sample REST status codes and messages.

    • 200 - OK - Everything is hunky-dory

    • 201 - Created - The resource was created successfully

    • 202 - Accepted - A 202 response is typically used for actions that take a long while to process. It indicates that the request has been accepted for processing, but the processing has not been completed

    You can easily set status codes as well as the status message with renderData(). HTTP status codes and messages are not part of the response body. They live in the HTTP header.

    Status codes can also be set manually by using the event.setHTTPHeader()method in your handler.

    Caching

    One of the great benefits of building your REST API on the ColdBox platform is tapping into awesome features such as event caching. Event caching allows you to cache the entire response for a resource using the incoming FORM and URL variables as the cache key. To enable event caching, set the following flag to true in your ColdBox config: Coldbox.cfc:

    Next, simply add the cache=true annotation to any action you want to be cached. That's it! You can also get fancy, and specify an optional cacheTimeout and cacheLastAccesstimeout (in minutes) to control how long to cache the data.

    Data is stored in CacheBox's template cache. You can configure this cache to store its contents anywhere including a Couchbase cluster!

    Auto-Deserialization of JSON Payloads

    If you are working with any modern JavaScript framework, this feature is for you. ColdBox on any incoming request will inspect the HTTP Body content and if the payload is JSON, it can deserialize it for you and if it is a structure/JS object, it will append itself to the request collection for you. So if we have the following incoming payload:

    The request collection will have 3 keys for name, type and data according to their native CFML type.

    To disable this feature go to your Coldbox config: coldbox.jsonPayloadToRC = false

    Security

    Adding authentication to an API is a common task and while there is no standard for REST, ColdBox supports just about anything you might need.

    Requiring SSL

    To prevent man-in-the-middle attacks or HTTP sniffing, we recommend your API require SSL. (This assumes you have purchased an SSL Cert and installed it on your server). When you define your routes, you can add withSSL() and ColdBox will only allow those routes to be accessed securely

    If your client is capable of handling cookies (like a web browser), you can use the session or client scopes to store login details. Generally speaking, your REST API should be stateless, meaning nothing is stored on the server after the request completes. In this scenario, authentication information is passed along with every request. It can be passed in HTTP headers or as part of the request body. How you do this is up to you.

    Another approach to force SSL for all routes is to create an that listens to the request and inspects if ssl is enabled.

    Basic HTTP Auth

    One of the simplest and easiest forms of authentication is Basic HTTP Auth. Note, this is not the most robust or secure method of authentication and most major APIs such as Twitter and FaceBook have all moved away from it. In Basic HTTP Auth, the client sends a header called Authorization that contains a base 64 encoded concatenation of the username and password.

    You can easily get the username and password using event.getHTTPBasicCredentials().

    Custom

    The previous example put the security check in a preHandler() method which will get automatically . You can implement a broader solution by tapping into any of the points such as preProcess which is announced at the start of every request.

    Remember interceptors can include an eventPattern annotation to limit what ColdBox events they apply to.

    In addition to having access to the entire request collection, the event object also has handy methods such as event.getHTTPHeader() to pull specific headers from the HTTP request.

    /interceptors/APISecurity.cfc

    Register the interceptor with ColdBox in your ColdBox.cfc:

    As you can see, there are many points to apply security to your API. One not covered here would be to tap into and place your security checks into an advice that can be bound to whatever API method you need to be secured.

    Restricting HTTP Verbs

    In our route configuration we mapped HTTP verbs to handlers and actions, but what if users try to access resources directly with an invalid HTTP verb? You can easily enforce valid verbs (methods) by adding this.allowedMethods at the top of your handler. In this handler the list() method can only be accessed via a GET, and the remove() method can only be accessed via POST and DELETE.

    The key is the name of the action and the value is a list of allowed HTTP methods. If the action is not listed in the structure, then it means allow all. If the request action HTTP method is not found in the list then it throws a 405 exception. You can catch this scenario and still return a properly-formatted response to your clients by using the onError() or the onInvalidHTTPMethod() convention in your handler or an exception handler which applies to the entire app.

    Error Handling

    ColdBox REST APIs can use all the same error faculties that an ordinary ColdBox application has. We'll cover three of the most common ways here.

    Handler onError()

    If you create a method called onError() in a handler, ColdBox will automatically call that method for runtime errors that occur while executing any of the actions in that handler. This allows for localized error handling that is customized to that resource.

    Handler onInvalidHTTPMethod()

    If you create a method called onInvalidHTTPMethod() in a handler, ColdBox will automatically call that method whenever an action is trying to be executed with an invalid HTTP verb. This allows for localized error handling that is customized to that resource.

    Global Exception Handler

    The global exception handler will get called for any runtime errors that happen anywhere in the typical flow of your application. This is like the onError() convention but covers the entire application. First, configure the event you want called in the ColdBox.cfc config file. The event must have the handler plus action that you want called.

    Then create that action and put your exception handling code inside. You can choose to do error logging, notifications, or custom output here. You can even run other events.

    ColdBox Relax

    ColdBox Relax is a set of ReSTful Tools For Lazy Experts. We pride ourselves in helping you (the developer) work smarter and ColdBox Relax is a tool to help you document your projects faster. ColdBox Relax provides you with the necessary tools to automagically model, document and test your ReSTful services. One can think of ColdBox Relax as a way to describe ReSTful web services, test ReSTful web services, monitor ReSTful web services and document ReSTful web services–all while you relax!

    Please note: Installing relax via CommandBox installs without the examples, if required you will need to obtain the examples from the Relax Github repo here:

    To install the examples place them into the models directory in a subdirectory called 'resources' (as per the Github repo), then add the following relax structure to your Coldbox.cfc file:

    You can then visit http://[yourhost]/relax to view Relax.

    You can read more about Relax on the Official Relax Doc page () or at it's repository:

    Ref Card -

    ## create app
    coldbox create app name=MyRestAPP skeleton=rest
    
    ## Start up a server with rewrites
    server start
    {
        data: "Welcome to my ColdBox RESTFul Service",
        error: false,
        messages: [ ],
        errorcode: "0"
    }

    PUT /api/user/53 will update user 53

  • DELETE /api/user/53 will delete user 53

  • PDF

  • Custom

  • 400 - Bad Request - The server couldn't figure out what the client was sending it

  • 401 - Unauthorized - The client isn't authorized to access this resource

  • 404 - Not Found - The resource was not found

  • 500 - Server Error - Something bad happened on the server

  • https://github.com/lmajano/hmvc-presso-demo
    Download RefCard
    Routing mechanism
    routing sections
    URL Router
    Routing DSL guide
    route pattern is a placeholder
    auto marshalling
    string representation
    interceptor
    run prior to each action in that handler
    ColdBox interception
    WireBox's AOP
    https://github.com/ColdBox/coldbox-relax
    http://www.ortussolutions.com/products/relax
    https://github.com/coldbox/coldbox-relax
    https://github.com/ColdBox/cbox-refcards/raw/master/ColdBox REST APIs/ColdBox-REST-APIs-Refcard.pdf
    http://www.example.com/api/user
    coldbox create handler name=api.user actions=index,view,save,remove
    component {
    
      function index( event, rc, prc ) {
        // List all users
      }
    
      function view( event, rc, prc ) {
        // View a single user
      }
    
      function save( event, rc, prc ) {
        // Save a user
      }
    
      function remove( event, rc, prc ) {
        // Remove a user
      }
    
    }
    // Map route to specific user.  Different verbs call different actions!
    component{
    
        function configure(){
            setFullRewrites( true );
    
            // User Resource
            route( "/api/user/:userID" )
                .withAction( {
                    GET    = 'view',
                    POST   = 'save',
                    PUT    = 'save',
                    DELETE = 'remove'
                } )
                .toHandler( "api.user" );
    
            route( ":handler/:action?" ).end();
        }
    
    }
    // two digits
    :state{2}
    
    // 2-4 digits
    :year{2,4}
    
    // 2 or more
    :page{2,}
    function index( event, rc, prc ) {
        // List all users
        return userService.getUserList();
    }
    
    function 404( event, rc, prc ) {
        return "<h1>Page not found</h1>";
    }
    // xml marshalling
    function getUsersXML( event, rc, prc ){
        var qUsers = getUserService().getUsers();
        event.renderData( type="XML", data=qUsers );
    }
    //json marshalling
    function getUsersJSON( event, rc, prc ){
        var qUsers = getUserService().getUsers();
        event.renderData( type="json", data=qUsers );
    }
    // pdf marshalling
    function getUsersAsPDF( event, rc, prc ){
        prc.user = userService.getUser( rc.id );
        event.renderData( data=renderView( "users/pdf" ), type="pdf" );
    }
    // Multiple formats
    function listUsers( event, rc, prc ){
        prc.users = userService.getAll();
        event.renderData( data=prc.users, formats="json,xml,html,pdf" );
    }
    http://www.example.com/api/user.json
    http://www.example.com/api/user.xml
    http://www.example.com/api/user.text
    function index( event, rc, prc ) {
        var qUsers = getUserService().getUsers();
        // Correct format auto-detected from the URL
        event.renderData( data=qUsers, formats="json,xml,text" );
    }
    function view( event, rc, prc ){
        var qUser = getUserService().getUser( rc.userID );
        if( qUser.recordCount ) {
            event.renderData( type="JSON", data=qUser );
        } else {
            event.renderData( type="JSON", data={}, statusCode=404, statusMessage="User not found");
        }
    }
    
    function save( event, rc, prc ){
        // Save the user represented in the request body
        var userIDNew = userService.saveUser( event.getHTTPContent() );
        // Return back the new userID to the client
        event.renderData( type="JSON", data={ userID = userIDNew }, statusCode=201, statusMessage="We have created your user");
    }
    function worldPeace( event, rc, prc ){
        event.setHTTPHeader( statusCode=501, statusText='Not Implemented' );
        return 'Try back later.';
    }
    config/ColdBox.cfc
    coldbox.eventCaching = true;
    // Cache for default timeout
    function showEntry( event, rc, prc ) cache="true" {
        prc.qEntry = getEntryService().getEntry( event.getValue( 'entryID', 0 ) );        
        event.renderData( type="JSON", data=prc.qEntry );
    }
    
    // Cache for one hour, or 20 minutes from the last time accessed.
    function showEntry( event, rc, prc ) cache="true" cacheTimeout="60" cacheLastAccessTimeout="20" {
        prc.qEntry = getEntryService().getEntry( event.getValue( 'entryID', 0 ) );        
        event.renderData( type="JSON", data=prc.qEntry );
    }
    {
        "name" : "Jon Clausen",
        "type" : "awesomeness",
        "data" : [ 1,2,3 ]
    }
    // Secure Route
    route( "/api/user/:userID" )
        .withSSL()
        .withAction( {
            GET    = 'view',
            POST   = 'save',
            PUT    = 'save',
            DELETE = 'remove'
        } )
        .toHandler( "api.user" );
    function preHandler( event, action, eventArguments ){
        var authDetails = event.getHTTPBasicCredentials();
        if( !securityService.authenticate( authDetails.username, authDetails.password ) ) {
            event.renderData( type="JSON", data={ message = 'Please check your credentials' }, statusCode=401, statusMessage="You're not authorized to do that");
        }
    }
    /**
    * This interceptor secures all API requests
    */
    component{
        // This will only run when the event starts with "api."
        function preProcess( event, interceptData, buffer ) eventPattern = '^api\.' {
            var APIUser = event.getHTTPHeader( 'APIUser', 'default' );
    
            // Only Honest Abe can access our API
            if( APIUser != 'Honest Abe' ) {
                // Every one else will get the error response from this event
                event.overrideEvent( 'api.general.authFailed' );
            }
        }
    }
    config/ColdBox.cfc
    interceptors = [
      { class="interceptors.APISecurity" }
    ];
    component{
    
        this.allowedMethods = { 
            remove = "POST,DELETE",
            list   = "GET"
        };
    
        function list( event, rc, prc ){}
    
        function remove( event, rc, prc ){}
    }
    // error uniformity for resources
    function onError( event, rc, prc, faultaction, exception ){
        prc.response = getModel("ResponseObject");
    
        // setup error response
        prc.response.setError(true);
        prc.response.addMessage("Error executing resource #arguments.exception.message#");
    
        // log exception
        log.error( "The action: #arguments.faultaction# failed when requesting resource: #arguments.event.getCurrentRoutedURL()#", getHTTPRequestData() );
    
        // display
        arguments.event.setHTTPHeader(statusCode="500",statusText="Error executing resource #arguments.exception.message#")
            .renderData( data=prc.response.getDataPacket(), type="json" );
    }
    /**
    * on invalid http verbs
    */
    function onInvalidHTTPMethod( event, rc, prc, faultAction, eventArguments ){
        // Log Locally
        log.warn( "InvalidHTTPMethod Execution of (#arguments.faultAction#): #event.getHTTPMethod()#", getHTTPRequestData() );
        // Setup Response
        prc.response = getModel( "Response" )
            .setError( true )
            .setErrorCode( 405 )
            .addMessage( "InvalidHTTPMethod Execution of (#arguments.faultAction#): #event.getHTTPMethod()#" )
            .setStatusCode( 405 )
            .setStatusText( "Invalid HTTP Method" );
        // Render Error Out
        event.renderData( 
            type        = prc.response.getFormat(),
            data         = prc.response.getDataPacket(),
            contentType = prc.response.getContentType(),
            statusCode     = prc.response.getStatusCode(),
            statusText     = prc.response.getStatusText(),
            location     = prc.response.getLocation(),
            isBinary     = prc.response.getBinary()
        );
    }
    config/ColdBox.cfc
    coldbox = {
        ...
        exceptionHandler = "main.onException"
        ...
    };
    // /handlers/main.cfc
    component {
        function onException( event, rc, prc ){
            // Log the exception via LogBox
            log.error( prc.exception.getMessage() & prc.exception.getDetail(), prc.exception.getMemento() );
    
            // Flash where the exception occurred
            flash.put("exceptionURL", event.getCurrentRoutedURL() );
    
            // Relocate to fail page
            relocate("main.fail");
        }
    }
    install relax --saveDev
    relax = {
        // The location of the relaxed APIs, defaults to models.resources
        APILocation = "models.resources",
        // Default API to load, name of the directory inside of resources
        defaultAPI = "myapi",
        // History stack size, the number of history items to track in the RelaxURL
        maxHistory = 10
    };

    Basic HTTP Authentication Interceptor

    Introduction

    In this recipe we will create a simple interceptor that will be in charge of challenging users with HTTP Basic Authentication. It features the usage of all the new RESTful methods in our Request Context that will make this interceptor really straightforward. We will start by knowing that this interceptor will need a security service to verify security, so we will also touch on this.

    You can find the code for this recipe here: https://github.com/coldbox-samples/simple-auth

    Application Setup

    Let's start building the app using CommandBox and installing all the necessary dependencies:

    Use [email protected] if using ColdBox Pre-Releases

    Security Service

    Let's build a simple security service to track users. Use CommandBox to generate the service model with two functions and let's mark it as a singleton:

    This will create the models/SecurityService and the companion unit tests. Let's fill them out:

    Security Service Test

    Security Service

    Please note that we are using a hard coded username and password, but you can connect this to any provider or db.

    The Interceptor

    Let's generate the interceptor now and listen to preProcess

    The preProcesslistener is to listen to all incoming requests are inspected for security. Please note that the unit test for this interceptor is also generated. Let's fill out the interceptor test first:

    Interceptor Test

    So to make sure this works, here is our Interceptor Test Case with all possibilities for our Security Interceptor.

    As you can see from our A,B, anc C tests that we use MockBox to mock the security service, the request context and methods so we can build our interceptor without knowledge of other parts.

    Interceptor Code

    As you can see it relies on a SecurityService model object that is being wired via:

    Then we check if a user is logged in or not and if not we either verify their incoming HTTP basic credentials or if none, we challenge them by setting up some cool headers and bypass event execution:

    The renderData() is essential in not only setting the 401 status codes but also concatenating to a noExecution() method so it bypasses any event as we want to secure them.

    Interceptor Declaration

    Open your Coldbox.cfc configuration file and add it.

    Now reinit your app coldbox reinit and you are simple auth secured!

    Why provider

    You might have noticed the injection of the security service into the interceptor used a provider: DSL prefix, why? Well, global interceptors are created first, then modules are loaded, so if we don't use a provider to delay the injection, then the storages module might not be loaded yet.

    Extra Credit

    Now that the hard part is done, we encourage you to try and build the integration test for the application now. Please note that most likely you would NOT do the unit test for the interceptor if you do the integration portion in BDD Style.

    # create our folder
    mkdir simple-auth --cd
    # generate a coldbox app
    coldbox create app "SimpleAuth"
    # Install extra dependencies
    install cbStorages
    # Startup the server
    server start
    coldbox create model SecurityService authorize,isLoggedIn singleton
    /**
    * The base model test case will use the 'model' annotation as the instantiation path
    * and then create it, prepare it for mocking and then place it in the variables scope as 'model'. It is your
    * responsibility to update the model annotation instantiation path and init your model.
    */
    component extends="coldbox.system.testing.BaseModelTest" model="models.SecurityService"{
    
        /*********************************** LIFE CYCLE Methods ***********************************/
    
        function beforeAll(){
            super.beforeAll();
    
            // setup the model
            super.setup();
    
            mockSession = createMock( "modules.cbstorages.models.SessionStorage" )
        }
    
        function afterAll(){
            super.afterAll();
        }
    
        /*********************************** BDD SUITES ***********************************/
    
        function run(){
    
            describe( "SecurityService Suite", function(){
    
                beforeEach(function( currentSpec ){
                    model.init();
                    model.setSessionStorage( mockSession );
                });
    
                it( "can be created (canary)", function(){
                    expect( model ).toBeComponent();
                });
    
                it( "should authorize valid credentials", function(){
                    mockSession.$( "set", mockSession );
                    expect( model.authorize( "luis", "coldbox" ) ).toBeTrue();
                });
    
                it( "should not authorize invalid credentials ", function(){
                    expect( model.authorize( "test", "test" ) ).toBeFalse();
                });
    
                it( "should verify if you are logged in", function(){
                    mockSession.$( "get", true );
                    expect( model.isLoggedIn() ).toBeTrue();
                });
    
                it( "should verify if you are NOT logged in", function(){
                    mockSession.$( "get", false );
                    expect( model.isLoggedIn() ).toBeFalse();
                });
    
            });
    
        }
    
    }
    component accessors="true" singleton{
    
        // Dependencies
        property name="sessionStorage" inject="SessionStorage@cbStorages";
    
        /**
         * Constructor
         */
        function init(){
                // Mock Security For Now
            variables.username = "luis";
            variables.password = "coldbox";
    
            return this;
        }
    
        /**
         * Authorize with basic auth
         */
        function authorize( username, password ){
    
            // Validate Credentials, we can do better here
            if( variables.username eq username AND variables.password eq password ){
                // Set simple validation
                sessionStorage.set( "userAuthorized", true );
                return true;
            }
    
            return false;
        }
    
        /**
         * Checks if user already logged in or not.
         */
        function isLoggedIn(){
                return sessionStorage.get( "userAuthorized", "false" );
        }
    
    }
    coldbox create interceptor name="SimpleSecurity" points="preProcess"
    component extends="coldbox.system.testing.BaseInterceptorTest" interceptor="interceptors.SimpleSecurity" {
    
        /*********************************** LIFE CYCLE Methods ***********************************/
    
        function beforeAll() {
            super.beforeAll();
    
            // mock security service
            mockSecurityService = createEmptyMock( "models.SecurityService" );
        }
    
        function afterAll() {
            super.afterAll();
        }
    
        /*********************************** BDD SUITES ***********************************/
    
        function run() {
            describe( "SimpleSecurity Interceptor Suite", function() {
                beforeEach( function( currentSpec ) {
                    // Setup the interceptor target
                    super.setup();
                    // inject mock into interceptor
                    interceptor.$property(
                        "securityService",
                        "variables",
                        mockSecurityService
                    );
                    mockEvent = getMockRequestContext();
                } );
    
                it( "can be created (canary)", function() {
                    expect( interceptor ).toBeComponent();
                } );
    
                it( "can allow already logged in users", function() {
                    // test already logged in and mock authorize so we can see if it was called
                    mockSecurityService.$( "isLoggedIn", true ).$( "authorize", false );
                    // call interceptor
                    interceptor.preProcess( mockEvent, {} );
                    // verify nothing called
                    expect( mockSecurityService.$never( "authorize" ) ).toBeTrue();
                } );
    
                it( "will challenge if you are not logged in and you don't have the right credentials", function() {
                    // test NOT logged in and NO credentials, so just challenge
                    mockSecurityService.$( "isLoggedIn", false ).$( "authorize", false );
                    // mock incoming headers and no auth credentials
                    mockEvent
                        .$( "getHTTPHeader" )
                        .$args( "Authorization" )
                        .$results( "" )
                        .$( "getHTTPBasicCredentials", { username : "", password : "" } )
                        .$( "setHTTPHeader" );
                    // call interceptor
                    interceptor.preProcess( mockEvent, {} );
                    // verify authorize called once
                    expect( mockSecurityService.$once( "authorize" ) ).toBeTrue();
                    // Assert Set Header
                    expect( mockEvent.$once( "setHTTPHeader" ) ).toBeTrue();
                    // assert renderdata
                    expect( mockEvent.getRenderData().statusCode ).toBe( 401 );
                } );
    
                it( "should authorize if you are not logged in but have valid credentials", function() {
                    // Test NOT logged in With basic credentials that are valid
                    mockSecurityService.$( "isLoggedIn", false ).$( "authorize", true );
                    // reset mocks for testing
                    mockEvent
                        .$( "getHTTPBasicCredentials", { username : "luis", password : "luis" } )
                        .$( "setHTTPHeader" );
                    // call interceptor
                    interceptor.preProcess( mockEvent, {} );
                    // Assert header never called.
                    expect( mockEvent.$never( "setHTTPHeader" ) ).toBeTrue();
                } );
            } );
        }
    
    }
    interceptors/SimpleSecurity.cfc
    /**
     * Intercepts with HTTP Basic Authentication
     */
    component {
    
        // Security Service
        property name="securityService" inject="provider:SecurityService";
    
        void function configure(){}
    
        void function preProcess( event, interceptData, rc, prc ){
    
            // Verify Incoming Headers to see if we are authorizing already or we are already Authorized
            if( !securityService.isLoggedIn() OR len( event.getHTTPHeader( "Authorization", "" ) ) ){
    
                // Verify incoming authorization
                var credentials = event.getHTTPBasicCredentials();
                if( securityService.authorize(credentials.username, credentials.password) ){
                    // we are secured woot woot!
                    return;
                };
    
                // Not secure!
                event.setHTTPHeader(
                    name     = "WWW-Authenticate",
                    value     = "basic realm=""Please enter your username and password for our Cool App!"""
                );
    
                // secured content data and skip event execution
                event
                    .renderData(
                        data         = "<h1>Unathorized Access<p>Content Requires Authentication</p>",
                        statusCode     = "401",
                        statusText     = "Unauthorized"
                    )
                    .noExecution();
            }
    
        }
    
    }
    // Security Service
    property name="securityService" inject="provider:SecurityService";
    // Verify Incoming Headers to see if we are authorizing already or we are already Authorized
    if( !securityService.isLoggedIn() OR len( event.getHTTPHeader("Authorization","") ) ){
    
        // Verify incoming authorization
        var credentials = event.getHTTPBasicCredentials();
        if( securityService.authorize(credentials.username, credentials.password) ){
            // we are secured woot woot!
            return;
        };
    
        // Not secure!
        event.setHTTPHeader(name="WWW-Authenticate",value="basic realm=""Please enter your username and password for our Cool App!""");
    
        // secured content data and skip event execution
        event.renderData(data="<h1>Unathorized Access<p>Content Requires Authentication</p>",statusCode="401",statusText="Unauthorized")
            .noExecution();
    }
    //Register interceptors as an array, we need order
    interceptors = [
        // Security
        { class="interceptors.SimpleSecurity" }
    ];

    Recipes

    A collection of useful recipes for the ColdBox Framework.