Author Archives: Ian Harris

  • 0

FileMaker 17: Data API Multi-File Upload

Tags : 

The FileMaker 17 Data API, also known as fmrest, includes a new capability to upload data to a container field. Prior to 17 the Data API only allowed container data to be downloaded. Uploading data from a website to FileMaker had to be done using plugins, Base64 encoding, or by saving a temporary copy of the file in a spot where a FileMaker script could find it. Now we can upload to FileMaker container fields using only the Data API. This blog post describes how to use the new upload capability with HTML and JavaScript.

Before uploading to FileMaker, we need to obtain an access token. We’ll use this token to authenticate our requests to FileMaker. The 17 Data API allows you to obtain an access token using either a basic authentication header or an OAuth identity provider like Google. In this blog I’ll use basic authentication.

To simplify the code I’m using jQuery. Google makes jQuery easily available through their content delivery network. Load the library by including the following script tag at the top of your HTML:

<html>
   <head>
       <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
   </head>

 

To configure basic authentication we prompt the web user for a username and password. The user needs to enter credentials for an account that has been assigned the “fmrest” extended privilege. Once the user submits this form, we concatenate their username and password, separated by a colon. Then we Base64 encode this concatenated string, which can be done in a web browser using the JavaScript method btoa. Here’s what this looks like:

   <form id="login">
       <label for="username">Username</label>
       <input name="username" type="text" />
       <label for="password">Password</label>
       <input name="password" type="password" />

       <input type="submit" value="Login" />
   </form>
  
   <script>
   let request;

   $("#login").submit(function(event){
       event.preventDefault();
       if(request) {
           request.abort();
       }

       let $form = $(this);

       let $inputs = $form.find("input");

       const auth = btoa($form.find('input[name="username"]').val() + ':' +  
$form.find('input[name="password"]').val());

       $inputs.prop('disabled', true);

 

With our newly encoded string, we’re ready to make our login request. If you used the Data API in FileMaker 16, the format of 17 API calls will be familiar to you. Note however that many of the URIs have changed. The URI for logging in has changed to “/fmi/data/v1/databases/filename/sessions”, for example.

Our login request looks like this:

       request = $.ajax({
           url: 'https://17.app.works/fmi/data/v1/databases/MultiUpload/sessions',
           type: 'post',
           headers: {
               'Content-Type': 'application/json',
               'Authorization': 'Basic ' + auth
           },
           data: {}
       });

 

If our login request is successful we store the access token in localStorage so we can use it in subsequent requests, then load the upload page. Otherwise we log an error message.

       request.done((response) => {
           localStorage.setItem('fmrest_token', response.response.token)
           $inputs.prop('disabled', false);
           location = 'blog-demo-upload.html'
       });

       request.fail((jqXHR, textStatus, errorThrown) => {
           console.log(errorThrown)
           $inputs.prop('disabled', false);
       })
   });
   </script>
</html>

 

Now that we have an access token saved in localStorage, we can begin making other requests to FileMaker, including uploading files to container fields.

My FileMaker demo file contains a simple table and layout called “Image”. The Image table contains 17’s default fields and two fields of my own: a container field named File and a text field named Name. We’ll upload a file by creating a new record in the Image table, obtaining the recordId in the response, then using the recordId in a call to insert the file into the container field.

To get started, let’s load jQuery and set up our form:

<html>
   <head>
       <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
   </head>

   <form id="uploadForm">
       <label for="upload">Upload</label>
       <input name="upload" type="file" />

       <label for="name">Name</label>
       <input name="name" type="text" />

       <input type="submit" value="Upload" />
   </form>

 

Next let’s get our access token from localStorage. If we find that the token does not exist, we redirect the user back to the login page.

   <script>
       const token = localStorage.getItem('fmrest_token');
       if (token === null) {
           location = 'blog-demo-login.html'
       }

 

When the user submits the form, we need to create a new record in the Image table and obtain it’s recordId from FileMaker’s response. First let’s grab what the user typed in the “Name” field:

       let request;
       $("#uploadForm").submit(function(event) {
           event.preventDefault();
           if(request) {
               request.abort();
           }

           let $form = $(this);

           let $inputs = $form.find("input");

           $inputs.prop('disabled', true);

           let name = $('#name').prop('value');

 

To create a record using the 17 Data API, we need to send a JSON object containing the fields and values we want to set. Let’s create our JSON object:

            const fieldData = {"fieldData": {"Name": name } }
            const data = JSON.stringify(fieldData)

 

With our JSON object created, we’re ready to make our request. The URI for creating records is in the format “/fmi/data/v1/databases/database-name/layouts/layout-name/records”. Notice that in our request, we include an Authorization header set to our token, with the prefix Bearer:

           request = $.ajax({
               url: 'https://17.app.works/fmi/data/v1/databases/MultiUpload/layouts/Image/records',
               type: 'post',
               headers: {
                   'Content-Type': 'application/json',
                   'Authorization': 'Bearer ' + token
               },
               data: data
           });

 

If our request succeeded, we obtain the recordId from FileMaker’s response and pass it to our insertFile function (see below). Otherwise, we display an error.

           request.done((response) => {
               insertFile(response.response.recordId)
               $inputs.prop('disabled', false);
           });

           request.fail((jqXHR, textStatus, errorThrown) => {
               console.log(errorThrown)
               $inputs.prop('disabled', false);
           })
       })

 

Our insertFile function builds a FormData object from the file upload input box:

        const insertFile = (recordId) => {
           let formData = new FormData();
           formData.append('upload', ($("#upload"))[0].files[0]);
           console.log(formData)
           event.preventDefault();
           if(request) {
               request.abort();
           }

 

Then it sends the object to FileMaker and uses recordId to identify which record it’s trying to modify:

           request = $.ajax({
               url: 'https://17.app.works/fmi/data/v1/databases/MultiUpload/layouts/Image/records/9/containers/File/1',
               type: 'post',
               headers: {
                   'Authorization': 'Bearer ' + token
               },
               data: formData,
               cache : false,
               contentType: false,
               processData: false
           });

       }

 

Knowing what we know now about uploading a single file to FileMaker, how can we change our code to allow us to upload multiple files at once? The answer lies in the HTML Drag and Drop API. This API is built into modern browsers and allows us to assign drag and drop event listeners to our DOM elements. A property of these events is dataTransfer, which is an object containing whatever files the user dropped on our DOM element.

The following chunk of code is borrowed from Mozilla’s documentation.

First let’s set up a div on the page where users can drop their files:

<div id="dropbox" style="height:200px; width:200px; background:rgba(0,0,0,0.2)">Drop files here.</div>

 

When “dropbox” hears a “drop” event, it assigns a dataTransfer object to dt. Then we select the files from dt and pass them to a handler function, handleFiles. Our handler function simply needs to iterate through our list of files and pass them off one by one to functions that call the Data API:

       const handleFiles = (files) => {
           for(i = 0; i < files.length; i++){
               createRecord(files[i], multiInsert)
           }
       }

 

Like our process when uploading a single file, we first create a record in the Image table using createRecord then call multiInsert to upload the file:

       const createRecord = (file, callback) => {

           const fieldData = {"fieldData": {"Name": file.name } }
           const data = JSON.stringify(fieldData)

           request = $.ajax({
               url: 'https://17.app.works/fmi/data/v1/databases/MultiUpload/layouts/Image/records',
               type: 'post',
               headers: {
                   'Content-Type': 'application/json',
                   'Authorization': 'Bearer ' + token
               },
               data: data
           });

           request.done((response) => {
               callback(file, response.response.recordId)
           });

           request.fail((jqXHR, textStatus, errorThrown) => {
               console.log(errorThrown)
           });

       }


       const multiInsert = (file, recordId) => {
           const formData = new FormData();
           formData.append('upload', file);
           event.preventDefault();

           request = $.ajax({
               url: 'https://17.app.works/fmi/data/v1/databases/MultiUpload/layouts/Image/records/' + recordId + '/containers/File/1',
               type: 'post',
               headers: {
                   'Authorization': 'Bearer ' + token
               },
               data: formData,
               cache : false,
               contentType: false,
               processData: false
           });
       }

 

These code snippets should hopefully get you started working with the Data API’s container features. They can be refined and built upon to do things like validation (only accept certain file types), relate an uploaded file to a record in another table, and even traverse a folder structure. If you’re interested in learning more about 17’s features, check out our other recent blog posts.


  • 0

FileMaker 17: Admin API

Tags : 

If you manage multiple instances of FileMaker Server, maintenance would typically be performed by logging in to each via a web browser or running fmsadmin CLI commands. These methods become unwieldy when you need to make the same change or gather the same bit of information across several servers. A seemingly simple task now takes 30 minutes or more with multiple servers. For example, ensuring that servers have enabled the security setting “Host password-protected databases only” is a quick task for the first few servers, but surprisingly time-consuming when checking twenty.

The time-saving solution to administering a fleet of Filemaker Server instances is the Admin API. Prior to 17, the Admin API was only available in FileMaker Cloud. Now, FileMaker Server 17 running on Windows or Mac OS can be administered from any tool capable of generating HTTP requests, including curl, PHP, and Filemaker Pro Advanced. Given a list of server addresses, we can programmatically log in to each server to gather configuration information, close database files, run schedules, and more.

To start off, here is the sequence of curl commands necessary to enable the “Host password-protected databases only” setting on a single server:

Obtain a JWT token:

curl -X POST \
 https://example.com/fmi/admin/api/v1/user/login \
 -H ‘Content-Type: application/json’ \
 -d ‘{
“username”: “admin”,
“password”: “admin-password”
}’

Use the JWT token (“a-very-long-string”) to authenticate subsequent requests.

Change the security setting:

curl -X PATCH \
 https://example.com/fmi/admin/api/v1/server/config/security \
 -H ‘Authorization: Bearer a-very-long-string’ \
 -H ‘Content-Type: application/json’ \
 -d ‘{
        “requireSecureDB”: true
}’

Confirm the new security setting (optional):

curl -X GET \
 https://example.com/fmi/admin/api/v1/server/config/security \
 -H ‘Authorization: Bearer a-very-long-string’

Logout (also optional, but generally a good idea as only a few admin connections can be open at once):

curl -X POST \
 https://example.com/fmi/admin/api/v1/user/logout \
 -H ‘Authorization: Bearer a-very-long-string’ \
 -H ‘Content-Type: application/json’

To make these requests against a list of servers, you just need to programmatically set the host address (e.g. https://example.com), username, and password within each curl command. The screencap below illustrates how to achieve this using a FileMaker script. For the purposes of this blog post, I’ve hard-coded and stored these in plain text. In production you’ll want to store and retrieve credentials in a secure manner using a service like AWS Secrets Manager.

The Admin API will be a trial feature until September 2019. You can provide feedback to FileMaker here.


  • 2

Render WebDirect in an Iframe

Tags : 

Is there a feature you want to add to your website that you could quickly implement using FileMaker but not using the language your site’s programmed in? You could send users to a WebDirect app, but replicating all of your website’s design elements in FileMaker can be time consuming. Plus, if your site’s style is not carefully carried over to WebDirect, you risk weakening your brand’s cohesiveness in the eyes of users. What if you could create the feature in FileMaker and drop it into a page of your website as a component, letting your website’s theme take care of unrelated components like headers, navigation bars, and footers?

The HTML Iframe element loads another website or web resource as a child element of the surrounding page. They’re useful in cases when there’s an external resource you’d like to serve to visitors of your website, but you do not want to send visitors to a whole new site. Most if not all modern browsers support Iframes, and you can even include multiple Iframe elements on a single page. This post describes how to embed a FileMaker WebDirect solution within a website using an Iframe element.

Before we get started it may be helpful to see an example of a framed WebDirect app in production. If you’ve visited our Downloads page recently, you already have. That page uses a framed WebDirect app to collect contact details before providing access to file downloads. The parent elements (header, navigation bar, footer, etc.) are the exact same as other sections of our website. Changes we make to the WebDirect app via FileMaker Pro get pushed to the framed element, and changes we make to our website’s template get pushed to the parent elements.

To accomplish this we had to make a few server-side configuration changes. By default, FileMaker Server’s HTTP response headers only allow pages to load WebDirect content within an Iframe if those pages have the same origin. According to Mozilla, “pages have the same origin if the protocol, port (if one is specified), and host are the same for both pages.” For example https://app.works/downloads/ and https://app.works/project-give2017/ have the same origin, but https://app.works/blog and https://differenthost.app.works do not. This is the default configuration because it prevents other sites from surreptitiously framing your FileMaker WebDirect solution. Without the right response headers in place, your app would be vulnerable to attacks like Cross-Frame Scripting.

Luckily, you can maintain your site’s security and take advantage of Iframes by using Content-Security-Policy (CSP) and X-Frame-Options response headers. These headers tell web browsers whether or not to load a page within an Iframe. CSP, the newer and more robust of the two headers, lets you specify which sources to trust for specific resource types using what are called directives. For example, you can use the “font-src” directive to define https://fonts.google.com/ as the only valid source of fonts for your page. In the case of framing WebDirect, the CSP directive we care about is “frame-ancestors”, which lets us define who can frame our app.

X-Frame-Options has been deprecated in favor of CSP, however it still needs to be included in order to support certain browsers including Internet Explorer. If your web server’s response headers include CSP but not X-Frame-Options, browsers that do not support CSP will assume you have not set any restrictions on who can frame your site. Unfortunately, X-Frame-Options only provides three options: do not allow framing at all, allow same origin framing, or allow from one specific origin (single frame ancestor).

Now that the details are out the way, here’s how you implement these response headers. In the following steps I’ll assume you’re running FileMaker Server on Windows.

  1. Open File Explorer and go to this folder: C:\Program Files\FileMaker\FileMaker Server\HTTPServer\conf 
  2. Copy a backup of the file C:\Program Files\FileMaker\FileMaker Server\HTTPServer\conf\web.config to another folder like the Desktop. If you make an irreversible edit to the web.config file, you can simply restore your backup copy.
  3. Run your preferred text editor as administrator and open the web.config file.
  4. Scroll to the bottom of the file. Add or edit the <customHeaders> element to include the Content-Security-Policy and and X-Frame-Options headers.
  5. Save and exit the web.config file.

One last change is needed to ensure your WebDirect app renders within an Iframe. In Safari 11, Apple enables the “Prevent cross-site tracking” privacy preference by default. This preference prevents cookies from being set across domains if the user has not already visited the domain setting the cookie. In other words, if https://your-parent-site.com frames https://your-webdirect-app.com and a user at https://your-parent-site.com has never visited https://your-webdirect-app.com, Safari will not allow https://your-webdirect-app.com to set cookies as a frame element.

To workaround this setting, a few lines of JavaScript code (courtesy of Github user vitr) at both the parent site and the framed site are necessary. First, add this code to the parent site’s html within a script tag:

// Safari 11.0 cross-site tracking support.
// If the user is browsing in a Safari Private Window,
// browse to the framed site. The framed site should then
// set a cookie and navigate back to the original site.
// Reference: https://github.com/vitr/safari-cookie-in-iframe
var is_safari = navigator.userAgent.indexOf("Safari") > -1;
var is_chrome = navigator.userAgent.indexOf('Chrome') > -1;
if ((is_chrome) && (is_safari)) {is_safari = false;}
if (is_safari) {
if (!document.cookie.match(/^(.*;)?\s*fixed\s*=\s*[^;]+(.*)?$/)) {
// JavaScript at https://your-webdirect-app.com/cookie.html sets a cookie and
// navigates back to https://your-parent-site.com.
window.location.replace("https://your-webdirect-app.com/cookie.html");
}
}

Second, create a file called “cookie.html” and save it within the C:\Program Files\FileMaker\FileMaker Server\HTTPServer directory. Then add these lines to cookie.html within a script tag:

// Reference: https://github.com/vitr/safari-cookie-in-iframe
var is_safari = navigator.userAgent.indexOf("Safari") > -1;
// Chrome has Safari in the user agent so we need to filter (https://stackoverflow.com/a/7768006/1502448)
var is_chrome = navigator.userAgent.indexOf('Chrome') > -1;
if ((is_chrome) && (is_safari)) {is_safari = false;}
if (is_safari) {
// See if cookie exists (https://stackoverflow.com/a/25617724/1502448)
if (!document.cookie.match(/^(.*;)?\s*fixed\s*=\s*[^;]+(.*)?$/)) {
// Set cookie to maximum (https://stackoverflow.com/a/33106316/1502448)
document.cookie = 'fixed=fixed; expires=Tue, 19 Jan 2038 03:14:07 UTC; path=/';
window.location.replace("https://your-parent-site.com");
}
}

With these changes applied, your framed WebDirect app should render correctly in all modern browsers while still being secured against unwanted framing.


  • 0

DevCon 2018 Details Revealed

Tags : 

FileMaker Developer Conference 2018 (DevCon 2018) has been officially announced! See below for a snippet of the press release from FileMaker. More information is available on FileMaker’s website, including a schedule of events, session topics, and recommended tracks. We’ll see you in Texas!

August 6-9, 2018 | Gaylord Texan Resort | Dallas, Texas

Join 1500 developers, just like you, at the FileMaker Developer Conference 2018.

DevCon 2018 is the must-attend event to learn everything and anything about FileMaker custom app development:

Expand
Broaden your development skill set with more than 80 sessions to choose from. Attend beginner, intermediate, advanced, business, and special focus sessions.

Innovate
Discover how to build more innovative FileMaker apps using technologies such as REST, JSON, cURL, SSL, JavaScript, SQL,OAuth, AI, iOS, and more.

Explore
See how to plan and develop FileMaker projects that will differentiate your business from the competition and maximize ROI.


  • 0

FileMaker Portland – Integrating Web Services

Tags : 

Thank you to everyone who attended the January FileMaker Portland meetup. The evening was packed with presentations about how people are leveraging FileMaker 16’s JSON support to connect to web services.

Some of the integrations we discussed were:

  • Magento – This popular e-commerce platform has REST and SOAP APIs. Use them to connect data about products, customers, and orders to FileMaker.
  • SmartyStreets – Supercharge your address entry fields with five different APIs.
  • Google Charts – A Javascript library for creating interactive HTML5/SVG charts.
  • FileMaker – Why not the other way around? Make POST, GET, PUT, and DELETE calls to FileMaker using the Data API.

Have something you’d like to share or discuss with other FileMaker developers? Consider attending the next meetup.


  • 0

Generating Google Charts in Web Viewer

Tags : 

Google deprecated Google Image Charts in 2012. While that API can still be used to create charts with a URL, Google currently recommends that people use the similarly named Google Charts. This blog post will go over how to format data for the Google Charts library and generate a chart within a Web Viewer object.

Classic vs Material

Google Charts offers two versions: Classic and Material. The two versions differ in how charts look and feel, and how customization options are defined. Material charts are redesigns of core charts that follow Material Design guidelines. They offer marked style improvements over Classic charts, and will appear familiar to users of Google products. However, they are currently in beta and do not support all of the features of Classic charts. If you are having trouble implementing a Material chart, make sure that you are not trying to include an unsupported customization option by referencing the open Feature Parity issue.

 

When you need to use an option that is only available for Classic charts, you can still style your chart like a Material chart by including the “theme : material” option.

 

 

DataTables

Information to be charted must be sent as a DataTable. The simplest approaches to generating DataTables within Web Viewer are by combining addColumn() and addRows(), or by using arrayToDataTable().

The addColumn() method requires either a type, or an object describing the column’s type and role. You can optionally pass labels as parameters as well.

In the example below, the x and y axes of a scatter plot are defined by calling data.addColumn(‘number’, ‘Revenue’) and data.addColumn(‘number’, ‘Gross Margin’). The third column is a tooltip, which displays when a user hovers over a datapoint.

Material-Themed Scatter Chart

 

The addRows() method accepts a nested array as a parameter. In the above example, a FileMaker script was used to assign a list of arrays to global variable $$A. When inspected using Data Viewer, $$A looks like this:

 

While these two methods do the job, if you are adding more than a few columns it will likely be more helpful to use arrayToDataTable(). This method converts a nested array to a DataTable and can infer the data types of each column, meaning you do not need to manually define them using addColumn().

Material Bar Chart

 

The global variable $$B referenced in the above code was generated using a FileMaker script and looks like this:

Trying It Out

  1. Add a Web Viewer object to a layout and open Web Viewer Setup. Set the website to Custom Web Address and select Specify…
  2. Begin your calculation with the following line: “data:text/html;charset=UTF-8,” & “
  3. Prepare your own html code or sample code from Google’s Chart Gallery in a text editor. Specifically, replace any double quotes within the html with single quotation marks. This makes inserting FileMaker variables and fields into html code easier, since you can define a block of html between double quotes, concatenate a variable or field, then concatenate another block of html between another pair of double quotes.
  4. Following the quotation mark, paste in the prepared html code.
  5. End your calculation with a double quotation mark.

Questions?

If you’re interested in integrating Google Charts within a FileMaker solution but could use some more guidance, please feel free to contact us. We have experience with various web services, and have even created a free downloadable FileMaker module, fmMapping, using the Google Maps API.


  • 0

FileMaker Portland – Holiday Potluck

Tags : 

FileMaker Portland would like to thank everyone who feasted at our Holiday Potluck. Plates were stacked high with homemade chili and cornbread, smoked salmon, and three types of sandwiches from Lardo. In between bites, revelers discussed all things FileMaker and toured our new office.

Interested in joining us at our next event? Check out the FileMaker Portland meetup page for more information.


  • 0

Have You Been Wingdinged?

Tags : 

 

Have your FileMaker layouts miraculously transformed into a collection of nonsensical symbols? Does your data resemble a prehistoric cave painting? If so, you’ve likely been wingdinged. Due to a known FileMaker bug affecting Windows computers running Adobe products, fonts can be replaced with a dingbat-style temporary font. Before translating your application character by character, try following these steps for a quick fix.

The Simple Solution

First close FileMaker completely, then close any open Adobe programs. Next, reopen FileMaker before reopening your Adobe programs. Your FileMaker fonts should now have returned to normal.

Other Things to Try

Keeping Adobe products up to date can potentially help mitigate font-related issues. To make sure you’re running the latest available version, open your Adobe program, choose the Help menu, then select Check for Updates.

There are a few advanced troubleshooting steps available if neither restarting your programs nor updating Adobe restored your FileMaker layouts to normal. Take care while attempting these fixes, as deleting the wrong file can lead to more headaches.

Delete the file “fntcache.dat” located within the C:\Windows\System32 folder, then restart your computer. Your operating system temporarily saves font data to this file. Deleting it and rebooting forces your computer to recreate it, hopefully clearing away any corrupt data.

Close any open Adobe programs, then delete the folder “acrord32_sbx” located within the C:\Users\\AppData\Local\Temp folder. Adobe programs save temporary files to this folder. Recreating it has been known to resolve various Adobe issues.

Hopefully these troubleshooting tips have brought your FileMaker application back from wingdingland. If you’re still running into issues, it may be time to bring in one of our experienced developers to help get things working again. We’re always happy to help, so feel free to contact us!