Page tree
Skip to end of metadata
Go to start of metadata

This page explains how to access Magnolia content from a client-side application. We use Magnolia's built-in REST API and a single-page AngularJS application.

We use the classic cars from the My first content app tutorial as sample content. The cars are typical structured content: each car has the same content type and same fields. Structured content is easy to enter, query, analyze and publish to multiple channels. You can publish it to a website or consume it from a client-side application like we do in this tutorial. With the REST API, you can access any Magnolia workspace.

If you want to know how to fetch content app data on the server side, for instance from a FreeMarker template script, see Accessing content on the server side.

Overview

Your tasks:

  • Install the Magnolia REST module
  • Think about the REST security
  • Test REST access
  • Write an Angular app

Architecture overview:

Install Magnolia REST

Make sure that your Magnolia installation contains the REST module. You should have at least the following submodules:

  • magnolia-rest-integration 
  • magnolia-rest-services

If you use a preconfigured Magnolia bundle or webapp you already have the required modules. If you use a custom bundle check your project dependencies and add Magnolia REST module if not already there.

If you want to use Swagger to test the REST endpoints, also install the magnolia-rest-tools module. It is not required to run the Angular app but it can be helpful during development.

(warning) When using magnolia-rest-tools, set the apiBasepath. The default value is most likely not correct for your Magnolia instance.

REST endpoints

The Magnolia REST module comes with preconfigured REST endpoints to read data from content apps. In this tutorial, we access content stored in the JCR so we use the following endpoints:

These two endpoints allow you to create, update and delete nodes and properties in any JCR workspace of your Magnolia instance.

Tip: You can also implement custom REST endpoints. You don't need them in this tutorial but custom endpoints are useful for:

  • Getting a very specific data structure which is different from what you get when using the preconfigured endpoints.
  • Exposing data from a custom content app which does not store the data in the JCR. 
  • Getting data from more than one workspace within one request.

REST security

REST endpoints may be a security risk. Set permissions correctly. Read REST API security and define security for your specific requirements.

The preconfigured REST endpoints provide not only methods to read but create, edit and update data. Configure a specific role which meets your requirements. Add the role to the user group which should have access to the required REST methods. For instance, you could configure a role which can only read the nodes and properties of a specific workspace, then add this role to the anonymous user. During development it will help to add this new role to superuser too.

Install sample content

In this tutorial we use the Products app and the classic cars as sample content. The cars are stored in the products JCR workspace.

Maven is the easiest way to install the module. Add the following dependency to your bundle:

<dependency>
  <groupId>info.magnolia.documentation</groupId>
  <artifactId>magnolia-app-tutorial</artifactId>
  <version>1.4.7</version>
</dependency>

Pre-built jars are also available for download. See Installing a module for help.

If you don't have a Magnolia instance yet, follow Installing Magnolia and a content app, then come back here. You will end up with exactly what we want to have for this tutorial as well.

Grant permissions to access content via REST

Grant permission to read content from the workspace products to a new role.

  1. Open the Security app.
  2. Add a role read-products
     
  3. In Access control lists, grant the new role:
    • Read-only access to / and its subnodes in the Products workspace
    • Read-only access to /read-products in the Userroles workspace.
  4. In Web access, grant Get access to the path /.rest/nodes/v1/products*. Deny all others.
     
  5. On the author instance, add the read-products role to the superuser system account.
  6. On the author instance, publish the  read-products role. 
  7. On the public instance, add the read-products role to the anonymous system account.
  8. Log out and log back in to apply the new permissions to the currently logged-in user. This gets you a new session.

Test the app

Open the Products app installed by the app-tutorial module and get familiar with it. 

Since the items in the app are cars let's talk about cars from now on. Try the tree, list and thumbnail views. Add a car of your own.

   

Test REST access

Request the app content with a REST call. Try the following request in your browser:

http://localhost:8080/magnoliaAuthor/.rest/nodes/v1/products/cars/007/Aston-Martin-DB5

Magnolia responds with:

 Click here to expand to show the response from the REST endpoint
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<node>
  <identifier>ce39509e-e1d5-4cdd-b254-125948e2aeec</identifier>
  <name>Aston-Martin-DB5</name>
  <path>/cars/007/Aston-Martin-DB5</path>
  <properties>
    <property>
      <multiple>false</multiple>
      <name>description</name>
      <type>String</type>
      <values>
        <value>&lt;p&gt;&lt;strong&gt;Movie:&lt;/strong&gt;&amp;nbsp;Goldfinger, GoldenEye, Casino Royale and Skyfall&lt;br
          /&gt;
          &lt;strong&gt;Year:&lt;/strong&gt;&amp;nbsp;1964, 1995, 2006 and 2012&lt;/p&gt;

          &lt;p&gt;The Aston Martin DB5 is one of the most famous cars in the world thanks to Oscar-winning special
          effects expert John Stears, who created the deadly silver-birch DB5 for use by James Bond in Goldfinger
          (1964). Although Ian Fleming had placed Bond in a DB Mark III in the novel, the DB5 was the company&amp;#39;s
          latest model when the film was being made.&lt;/p&gt;

          &lt;p&gt;A different Aston Martin DB5 (registration BMT 214A) was used in the 1995 Bond film, GoldenEye, in
          which three different DB5s were used for filming. The BMT 214A also returned in Tomorrow Never Dies (1997) and
          was set to make a cameo appearance in the Scotland-set scenes in The World Is Not Enough (1999), but these
          were cut in the final edit. Yet another DB5 appeared in Casino Royale (2006), this one with Bahamian number
          plates and left-hand drive (where the previous British versions had been right-hand drive).&lt;/p&gt;

          &lt;p&gt;Source: &lt;a href="https://en.wikipedia.org/wiki/Aston_Martin_DB5"&gt;Wikipedia&lt;/a&gt;&lt;/p&gt;
        </value>
      </values>
    </property>
    <property>
      <multiple>false</multiple>
      <name>title</name>
      <type>String</type>
      <values>
        <value>Aston Martin DB5</value>
      </values>
    </property>
    <property>
      <multiple>false</multiple>
      <name>image</name>
      <type>String</type>
      <values>
        <value>jcr:4cd02639-167d-405a-923f-607aa20d0bc0</value>
      </values>
    </property>
  </properties>
  <type>mgnl:product</type>
</node> 

We got an XML response but actually we want JSON. The nodes endpoint requires an HTTP request header to return JSON. Request the content with cURL so you can pass the desired return type.

Open a terminal and type the following command:

curl -H "Accept: application/json"  http://localhost:8080/magnoliaAuthor/.rest/nodes/v1/products/cars/007/Aston-Martin-DB5 -u superuser:superuser

Now we get JSON:

{
    "name": "Aston-Martin-DB5",
    "type": "mgnl:product",
    "path": "/cars/007/Aston-Martin-DB5",
    "identifier": "ce39509e-e1d5-4cdd-b254-125948e2aeec",
    "properties": [{
        "name": "description",
        "type": "String",
        "multiple": false,
        "values": ["<p><strong>Movie:</strong>&nbsp;Goldfinger, GoldenEye, Casino Royale and Skyfall<br />\n<strong>Year:</strong>&nbsp;1964, 1995, 2006 and 2012</p>\n\n<p>The Aston Martin DB5 is one of the most famous cars in the world thanks to Oscar-winning special effects expert John Stears, who created the deadly silver-birch DB5 for use by James Bond in Goldfinger (1964). Although Ian Fleming had placed Bond in a DB Mark III in the novel, the DB5 was the company&#39;s latest model when the film was being made.</p>\n\n<p>A different Aston Martin DB5 (registration BMT 214A) was used in the 1995 Bond film, GoldenEye, in which three different DB5s were used for filming. The BMT 214A also returned in Tomorrow Never Dies (1997) and was set to make a cameo appearance in the Scotland-set scenes in The World Is Not Enough (1999), but these were cut in the final edit. Yet another DB5 appeared in Casino Royale (2006), this one with Bahamian number plates and left-hand drive (where the previous British versions had been right-hand drive).</p>\n\n<p>Source: <a href=\"https://en.wikipedia.org/wiki/Aston_Martin_DB5\">Wikipedia</a></p>\n"]
    }, {"name": "title", "type": "String", "multiple": false, "values": ["Aston Martin DB5"]}, {
        "name": "image",
        "type": "String",
        "multiple": false,
        "values": ["jcr:4cd02639-167d-405a-923f-607aa20d0bc0"]
    }]
}

If you installed the magnolia-rest-tools module, request the same with Swagger:

  1. Open the REST Tools app.
    rest-tools-launch-tile
  2. Click GET in the nodes API endpoint.
    rest-tools-swagger-nodes-api-overview
  3. Set workspace to products and path to a car such as /cars/007/Aston-Martin-DB5 and click Try it out. You get JSON data for one content item. 
    rest-tools-swagger-nodes-params   rest-tools-swagger-response

Play around a little bit to familiarize yourself with the Swagger tool. Figure out the correct parameters to get a JSON representation of all cars.

Create an AngularJS app

AngularJS is a popular Web application framework among front-end developers. In this tutorial we use an AngularJS app to access and render content from the Products app. We assume you know enough JavaScript to follow along.

The content items we render are cars. They are organized in the app like this:

cars/
├── 007/
│   ├── Aston-Martin-DB5
│   ├── Aston-Martin-V8
│   ├── Lotus-Esprit-S1
│   └── Sunbeam-Alpine
└── classics/
    ├── 1927-Hudson
    ├── Continental-Mark-II
    ├── Fiat-Cinquecento
    ├── Pontiac-Chieftain-1952
    └── Riley-Brooklands-1930

where:

  • cars, 007 and classics are folders
  • the cars are of type mgnl:product

Let's create an Angular app that provides:

  • List of cars to choose from
  • Details of a selected car with description and image
  • Checkboxes to filter the list into classics or 007 cars. We use the parent folders as pseudo-categories.

App structure

We will create an ngApp with two controllers:

  • carsList renders a list of all cars. Each car has a clickable label. Clicking on the label shows the car in the carDetail controller. The list controller also has two checkboxes to filter the list - one for each parent folder.

  • carDetail renders the detail of the selected car: title, description and image.

App files

Create the following file structure:

lightmodules/
$lightmodules
└── content-app-clients/
    └── angular/
        ├── app.js
        ├── erics-cars.html
        └── style.css 

 $lightmodules is your light module directory. It can be anywhere on your file system but it must be a real directory such as:

  • Mac OS X /Users/johndoe/dev/lightmodules
  • Windows C:\Users\johndoe\dev\lightmodules

You may want to set the magnolia.resources.dir property to reference your light modules folder:

magnolia.resources.dir=/Users/johndoe/dev/lightmodules

To avoid getting in trouble with the same-origin policy, run the Angular app on the same server as Magnolia. See how to overcome same-origin policy problem. Instead of a Freemarker template, keep it simple and just add a three static files to a light module.

CarsContentClient app

Start creating the Angular app in erics-cars.html. Add an ng-app directive to the body element which will wrap the two controllers:

lightmodules/content-app-clients/angular/erics-cars.html (intermediate state)
<!doctype html>
<html>
<head>
    <title>Angular loves Magnolia :-)</title>
    <link rel="stylesheet" href="style.css?u=12">
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.0-rc.0/angular.min.js"></script>
    <script src="app.js?v=w"></script>
</head>

<body ng-app="CarsContentClient">

<h1>Eric's cars - shown with a lightweight angular client</h1>

</body>
</html>

Note:

  • Lines 5-7: References to JS and CSS files. We use dummy request parameters such as ?u=12 to bypass cache. We recommend that you use such dummy request parameters while developing. Alternatively, you could exclude content from the cache on the Magnolia instance but your browser may also cache the resources, particularly JS and CSS files. Once you're done with development, remove the request parameters.
  • Line 10: ng-app directive in the body element.

Request the file in the browser to make sure that Magnolia serves a static file. Use a dummy parameter ?foo=bar also in the request URL. It ensures we bypass cache on both server and client side.

http://localhost:8080/magnoliaAuthor/.resources/content-app-clients/angular/erics-cars.html?foo=bar


content-app-clients/angular/erics-cars.html
 <!doctype html>
<html>
<head>
    <title>Angular loves Magnolia :-)</title>
    <link rel="stylesheet" href="style.css?u=12">
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.0-rc.0/angular.min.js"></script>
    <script src="app.js?v=w"></script>
</head>
 
<body ng-app="CarsContentClient">
 
<h1>Eric's cars - shown with a lightweight Angular client</h1>
content-app-clients/angular/erics-cars.html
</body>
</html>

Next, configure the Angular app in app.js:

lightmodules/content-app-clients/angular/app.js (fragment)
var app = angular.module('CarsContentClient', []);
 
/**
 * Configuration of some constants
 */
app.constant("APP_CONFIG", {
    restBaseUrl: 'http://localhost:8080/magnoliaAuthor/.rest',
    requestHeaders: {headers: {'Accept': 'application/json;odata=verbose'}},
    nodesEndpointPath: '/nodes/v1',
    workspace: "products",
    carsRootFolder: "/cars",
    defaultCarPath: "/cars/007/Aston-Martin-DB5",
    imgBaseUrl : "/magnoliaAuthor/dam-static"
});
Now you have created the app and defined some constants to use later on.


CSS style sheet

To style the Eric's car page, add some CSS code to style.css

lightmodules/content-app-clients/angular/style.css
 body, li{
    font-family: "Trebuchet MS", "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", geneva, helvetica, arial, tahoma, verdana, sans-serif;
}

p.intro{
    color: #666666;
    font-size: 0.768em;
}

p.intro a{
    color: dimgray;
}

div.cars .car{
    font-family: "Courier New";
    font-weight: 800;
    font-style: italic;
    padding: 5px;
}

div.cars .car .click{
    cursor: pointer;
    margin: 5px;
    background-color: aquamarine;
    color: #666;
    padding:  2px 10px;
    border-radius: 3px;
}

div.cars .car .click.selected{
    color: red;
}


div.car{
    float: left;
}

form.filters{
    display: inline-block;
    margin: 0;
    padding: 10px 10px;
    float: right;
    font-size: 0.768em;
    font-family: "Courier New";
    font-weight: 800;
    font-style: italic;
}

form div.cat{
    display: inline-block;
}

div.clear{
    clear: both;
}
div.space{
    height: 20px;
}


div.car-detail{
    width: auto;
    margin: 10px;
}

.car-detail h2{
    font-family: "Courier New";
    background-color:lightblue;
    display: inline-block;
    margin: 10px;
    padding: 5px 10px;
    border-radius: 3px;
}

.car-detail p.description{
    font-size: 0.923em;
    padding: 0 15px;
}

table td.image{
    width: auto;
    height: inherit;
    padding: 15px;
    margin: 20px;
}


img.fancy{
    border:1px solid #ccc;
    -webkit-border-radius: 20px;
    -moz-border-radius: 20px;
    border-radius: 20px;
}


carDetail controller and AngularJS utilities

To render a car's details we want a $scope object of the carDetail controller with the following properties:

  • carTitle
  • carDescription
  • imgUrl

Add the following HTML snippet to erics-car.html:

lightmodules/content-app-clients/angular/erics-cars.html (fragment)
<div ng-controller="carDetail" class="car-detail">
    <table width="100%">
        <tr valign="top">
            <td width="60%" class="image">
                <img ng-src="{{imgUrl}}" width="640" class="fancy">
            </td>
            <td width="40%"> <h2>{{carTitle}}</h2><p ng-bind-html="carDescription | sanitize" class="description"></p></td>
        </tr>
    </table>
</div><!-- eof 'carDetail' ctrl. -->

When you reload the file in the browser you may notice JavaScript errors. To see the errors, open the JavaScript console of your browser. The name of this "console" and how it is named depends on the browser you are using. For instance, on Google Chrome browser it is named DevTools.

globalData component

Add a component named globalData. Both controllers will use it. It has a property productPath which will be set by one controller and read by the other controller.

lightmodules/content-app-clients/angular/app.js (fragment)
app.service('globalData', ['APP_CONFIG', function (APP_CONFIG) {
    var productPath = APP_CONFIG.defaultCarPath;
    var setProductPath = function (newPath) {
        productPath = newPath;
    };
    var getProductPath = function () {
        return productPath;
    };
    return {
        setProductPath: setProductPath,
        getProductPath: getProductPath
    };
 
}]);

utils component

Add a component named utils. It provides some useful methods that both controllers can use. The carDetail controller will use the function #getPropertyValue.

lightmodules/content-app-clients/angular/app.js (fragment)
app.service('utils', function () {
    var getPropertyValue = function (node, propertyName) {
        for (var p in node.properties) {
            if (propertyName == node.properties[p].name) {
                return node.properties[p].values[0];
            }
        }
        return "";
    };
    var escapeRegExp = function(str){
        return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
    }
 
    var replaceAll = function (str, find, replace){
        return str.replace(new RegExp(this.escapeRegExp(find), 'g'), replace);
    };
 
    return {
        getPropertyValue: getPropertyValue,
        replaceAll: replaceAll,
        escapeRegExp: escapeRegExp,
    }
});
Note:


  • getPropertyValue(node, propertyName) is a convenience method to fetch a property from the JSON structure is returned by the standard nodes endpoint.
  • replaceAll(str, find, replace) manipulates strings. The carDetail controller uses this function just to style things a bit.

sanitize filter

The third little helper to add is the sanitize filter. Remember that the description property of the content item contains HTML, encoded HTML even. By default, Angular refuses to render JSON content that contains (HTML) markup. The sanitize filter makes sure that the (encoded) HTML is rendered properly as HTML. 

lightmodules/content-app-clients/angular/app.js (fragment)
app.filter("sanitize", ['$sce', function ($sce) {
    return function (htmlCode) {
        return $sce.trustAsHtml(htmlCode);
    }
}]);
Internally the filter is using the angular module $sce.


carDetail controller

Finally, add the carDetail controller:

lightmodules/content-app-clients/angular/app.js (fragment)
app.controller('carDetail', ['$scope', '$http', 'globalData', 'utils', 'APP_CONFIG', function ($scope, $http, globalData, utils, APP_CONFIG) {
 
    $scope.$watch(function () {
            return globalData.getProductPath();
        },
        function (productPath) {
            console.log("Rendering detail for "+productPath);
            var detailNodeRequestPath = APP_CONFIG.restBaseUrl + APP_CONFIG.nodesEndpointPath + "/" + APP_CONFIG.workspace + productPath;
            $http.get(detailNodeRequestPath, APP_CONFIG.requestHeaders)
                .success(function (response) {
                    $scope.carTitle = utils.getPropertyValue(response, "title");
                    $scope.carDescription = utils.getPropertyValue(response, "description");
                    $scope.imgUrl = APP_CONFIG.imgBaseUrl+"/"+utils.getPropertyValue(response, "image");
                });
        }, true);
}]);
Note:


  • Line 1: Custom components APP_CONFIGglobalData and utils are injected.
  • Line 9: The controller executes an HTTP#get method on the nodes REST endpoint. The content item path is the selected item. It requests data for one content item as we did in testing REST access.
  • Line 8, 9: Computes a request URL for the REST call using configuration data from the globalData component.
  • Line 3ff: The HTTP#get call is defined as a callback function of $watch which is a value change listener for globalData.productPath. The REST call is executed initially and when globalData.productPath changes.

Reload the file in your browser again. Now you can see the detail of the Aston Martin DB5 in your browser. Cool! Isn't it?

carsList controller

The carsList controller renders a list of clickable span elements that contain car titles. The list can be filtered with checkboxes using the parent folders classics and 007 as pseudo-categories.

Add this HTML in erics-cars.html:

lightmodules/content-app-clients/angular/erics-cars.html (fragment)
<div ng-controller="carsList">
    <!-- car folders filters -->
    <form class="filters">
        <div ng-repeat="cat in carCategories" class="cat">
            <label>{{cat.name}} cars</label><input type="checkbox" name="category"  ng-click="clickCategory(cat.name)" ng-model="cat.checked" />
            <label> &nbsp; &nbsp;</label>
        </div>
    </form>
    <div class="clear"></div>
    <!-- car titles to click to change the detail -->
    <div class="cars">
        <div data-ng-repeat="car in cars | filter:isInCategory" class="car">
            <span ng-click="selectCar(car.path)" class="click">{{car.title}}</span>
        </div>
    </div>
 
</div><!-- eof 'carsList' ctrl. -->
The $scope of this controller has the following properties:


  • cars: An array of car items. Each car item has these properties:
    • path
    • category: actually a pseudo-category, the name of the parent folder such as classics or 007.
    • title: Title of the car
    • name: Node name of the item.
  • carCategories: An associative array (map) for the pseudo-categories. Each item has one property:
    • checked: Whether the pseudo-category is currently selected.

Here is the JavaScript code:

lightmodules/content-app-clients/angular/app.js (fragment)
app.controller('carsList', ['$scope', '$http', 'globalData', 'utils','APP_CONFIG' , function ($scope, $http, globalData, utils, APP_CONFIG) {
 
    $scope.carCategories = {};
 
    $scope.selectCar = function (arg) {
        globalData.setProductPath(arg);
    };
 
    $scope.clickCategory = function (catName) {
        $scope.carCategories[catName].selected = !($scope.carCategories[catName].checked);
    };
 
    // a per scope filter
    $scope.isInCategory = function(car){
        return $scope.carCategories[car.category].checked;
    };
 
    var carsNodesRequestUrl = APP_CONFIG.restBaseUrl + APP_CONFIG.nodesEndpointPath + "/" + APP_CONFIG.workspace + APP_CONFIG.carsRootFolder + "?depth=3";
    $http.get(carsNodesRequestUrl, APP_CONFIG.requestHeaders)
        .success(function (response) {
            $scope.cars = [];
 
            for (var node in response.nodes) {
                // expecting folder nodes on the root level
                if ("mgnl:folder" == response.nodes[node].type) {
 
                    var folderNode = response.nodes[node];
                    var folderName = folderNode.name;
                    $scope.carCategories[folderName] = {checked: true, name: folderName};
 
                    // expecting product nodes below the folders
                    for (var subNode in folderNode.nodes) {
                        if ("mgnl:product" == folderNode.nodes[subNode].type) {
                            var carNode = folderNode.nodes[subNode];
                            var carPath = carNode.path;
                            // replacing space (" ") with non-breaking to produce button-like labels
                            var carTitle = utils.replaceAll(utils.getPropertyValue(carNode, "title"), " ", "\u00A0");
                            $scope.cars.push({path: carPath, category: folderName, title: carTitle, name: carNode.name});
                        }
                    }
                }
            }
        });
}]);
Note:


  • Line 1: Again custom components APP_CONFIGglobalData and utils are injected.
  • Lines 5-7: selectCar is executed when you click a car of the list. The function sets globalData.productPath so it indirectly triggers the execution of the carDetail controller.
  • Lines 9-11: clickCategory maintains the $scope variable carCategories. Since this changes the state of a controller $scope variable, the UI gets repainted - the list items gets updated.
  • Line 3: The controller executes an HTTP#get method. Here the URI for the JSON call requests the cars root folder and contains the argument depth.

Final result:

Get the completed files

The three files in their final form are available in Magnolia's Git repository.

Option 1: Clone the Git repository

Clone the repository to get the complete file structure. Use a terminal, go to your light modules folder, and clone:

git clone https://git.magnolia-cms.com/scm/documentation/content-app-clients.git

Option 2: Download

What next?

Custom JSON format

The JSON format provided by the nodes endpoint is not very handy. In this tutorial we managed to get the required properties with an extra JavaScript function in the utils component of the Angular app. But at some point you may want a JSON representation that you cannot get easily with the default JCR nodes and properties endpoints.

With a custom JSON service you can also get data from different JCR workspaces in one request. This cannot be done with the default endpoints for nodes and properties. A custom service therefore saves resources on the network.

With Java: create a custom endpoint 

Expose a custom endpoint. This approach requires some Java classes which must be packaged into a Magnolia Maven module.

Without Java: neat-jsonfn

Another solution is the neat-jsonfn module. Read the blog post and check out the module in GitHub:

Combine your content app with the Categorization module

The Products app used in this tutorial is very simple. We use folders to mimic car categories. Better use the Categorization module to add multiple categories.

Where to add the AngularJS app?

You can use or host the AngularJS app anywhere:

  • As static web resource served by Magnolia as seen in this tutorial
  • As part of a page or component template, in a template script
  • As a static or dynamic resource in any other server

Same-origin policy

If you want to run the AngularJS app on a distinct host – a host which has a different address than the Magnolia server which provides the REST API – you run into the same-origin policy problem. To overcome this problem some use specific Apache settings, see StackOverflow.

An elegant solution to solve the same-origin policy without changing Apache configuration is the Magnolia CORSFilter:

 


Photo credits: