Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// SPDX-License-Identifier: MPL-2.0
// SPDX-FileCopyrightText: 2026 Chris Gibson <cgibson@outlook.com>

package com.mirth.connect.client.core.api.servlets;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;

import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import com.mirth.connect.client.core.ClientException;
import com.mirth.connect.client.core.api.BaseServletInterface;
import com.mirth.connect.client.core.api.MirthOperation;
import com.mirth.connect.client.core.api.Param;

/**
* Serializes a message through a data type's own serializer — the engine's exact toXML()/toJSON()
* output — so a browser client can build message trees that match the runtime {@code msg}/{@code tmp}
* without shipping the engine's datatype libraries. Uses the installed datatype plugins, so every
* data type (and strict/non-strict via serialization-property overrides) is covered. Session-authed,
* not audited.
*/
@Path("/datatypes")
@Tag(name = "Data Types")
@Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
public interface DataTypeServletInterface extends BaseServletInterface {

@POST
@Path("/_serialize")
@Consumes(MediaType.TEXT_PLAIN)
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Serializes a message via the given data type's serializer. Returns { format, data, meta: { root, descriptions } }.")
@MirthOperation(name = "serializeMessage", display = "Serialize message for data type", auditable = false)
public Response serializeMessage(// @formatter:off
// dataType is a query param (not a path segment) so values containing a slash — e.g. "EDI/X12" — pass cleanly.
@Param("dataType") @Parameter(description = "The data type name (HL7V2, XML, JSON, EDI/X12, NCPDP, DELIMITED, RAW, DICOM, HL7V3).", required = true) @QueryParam("dataType") String dataType,
@Param("props") @Parameter(description = "Optional serialization-property overrides as newline-separated key=value pairs (e.g. useStrictParser=true).", required = false) @QueryParam("props") String props,
@Param("message") String message) throws ClientException;
// @formatter:on
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// SPDX-License-Identifier: MPL-2.0
// SPDX-FileCopyrightText: 2026 Chris Gibson <cgibson@outlook.com>

package com.mirth.connect.client.core.api.servlets;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;

import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import com.mirth.connect.client.core.ClientException;
import com.mirth.connect.client.core.api.BaseServletInterface;
import com.mirth.connect.client.core.api.MirthOperation;
import com.mirth.connect.client.core.api.Param;

/**
* JavaScript utilities the script editors need — script validation — exposed over REST so a
* browser client can use the engine's own Rhino compiler (the same one the Swing client uses
* in-process) instead of shipping its own. Takes a raw script body, requires only a valid
* session, and is not audited (editor-support calls, hit frequently). (Formatting is done
* client-side by the web admin with js-beautify, so there is no pretty-print endpoint.)
*/
@Path("/javascript")
@Tag(name = "JavaScript")
@Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
public interface JavaScriptServletInterface extends BaseServletInterface {

@POST
@Path("/_validate")
@Consumes(MediaType.TEXT_PLAIN)
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Validates a JavaScript script with the engine's Rhino compiler. Returns { \"error\": <message|null> }.")
@MirthOperation(name = "validateScript", display = "Validate JavaScript", auditable = false)
public Response validateScript(@Param("script") String script) throws ClientException;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// SPDX-License-Identifier: MPL-2.0
// SPDX-FileCopyrightText: 2026 Chris Gibson <cgibson@outlook.com>

package com.mirth.connect.client.core.api.servlets;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;

import java.util.List;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import com.mirth.connect.client.core.ClientException;
import com.mirth.connect.client.core.api.BaseServletInterface;
import com.mirth.connect.client.core.api.MirthOperation;
import com.mirth.connect.client.core.api.Param;

/**
* Serves the browser (web administrator) half of installed extensions.
*
* An extension may ship a web UI alongside its engine code as a {@code webadmin/} folder
* (containing a {@code plugin.json} manifest and its compiled ES-module assets). This servlet
* lets the web administrator discover and fetch those web halves directly from the engine that
* has them installed — so a plugin's UI follows the engine, not the web-admin install. Both
* endpoints require only a valid session (any authenticated user) and are not audited: they
* serve non-sensitive static UI code and are hit once per page load.
*/
@Path("/webplugins")
@Tag(name = "Web Plugins")
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
public interface WebPluginServletInterface extends BaseServletInterface {

@GET
@Path("/")
@Operation(summary = "Returns the install-directory paths of all enabled extensions that ship a web administrator UI (i.e. contain webadmin/plugin.json).")
@MirthOperation(name = "getWebPluginPaths", display = "Get web plugin paths", auditable = false)
public List<String> getWebPluginPaths() throws ClientException;

@GET
@Path("/{extensionPath}/{resourcePath:.*}")
@Produces(MediaType.WILDCARD)
@Operation(summary = "Serves a static file from an extension's webadmin/ folder (the browser half of the plugin).")
@MirthOperation(name = "getWebPluginResource", display = "Get web plugin resource", auditable = false)
public Response getWebPluginResource(// @formatter:off
@Param("extensionPath") @Parameter(description = "The extension's install-directory name.", required = true) @PathParam("extensionPath") String extensionPath,
@Param("resourcePath") @Parameter(description = "The file path within the extension's webadmin/ folder.", required = true) @PathParam("resourcePath") String resourcePath) throws ClientException;
// @formatter:on
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import com.mirth.connect.model.datatype.SerializationProperties;
import com.mirth.connect.model.datatype.SerializerProperties;
import com.mirth.connect.model.transmission.TransmissionModeProperties;
import com.mirth.connect.model.util.MessageVocabulary;
import com.mirth.connect.server.message.DefaultAutoResponder;
import com.mirth.connect.server.message.DefaultResponseValidator;

Expand Down Expand Up @@ -90,4 +91,14 @@ public AutoResponder getAutoResponder(SerializationProperties serializationPrope
public ResponseValidator getResponseValidator(SerializationProperties serializationProperties, ResponseValidationProperties responseValidationProperties) {
return new DefaultResponseValidator();
}

/**
* Get the message vocabulary for this data type — element descriptions used to annotate a
* message tree (the same text the Swing client shows). {@code version} and {@code type} come
* from the serializer's message metadata (mirth_version / mirth_type). Returns null for data
* types with no vocabulary (the default); types that have one override this.
*/
public MessageVocabulary getVocabulary(String version, String type) {
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
package com.mirth.connect.plugins.datatypes.dicom;

import com.mirth.connect.model.datatype.DataTypeDelegate;
import com.mirth.connect.model.util.MessageVocabulary;
import com.mirth.connect.plugins.DataTypeServerPlugin;

public class DICOMDataTypeServerPlugin extends DataTypeServerPlugin {
Expand All @@ -31,4 +32,9 @@ protected DataTypeDelegate getDataTypeDelegate() {
return dataTypeDelegate;
}

@Override
public MessageVocabulary getVocabulary(String version, String type) {
return new DICOMVocabulary(version, type);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import com.mirth.connect.donkey.server.message.batch.BatchAdaptorFactory;
import com.mirth.connect.model.datatype.DataTypeDelegate;
import com.mirth.connect.model.datatype.SerializerProperties;
import com.mirth.connect.model.util.MessageVocabulary;
import com.mirth.connect.plugins.DataTypeServerPlugin;

public class EDIDataTypeServerPlugin extends DataTypeServerPlugin {
Expand All @@ -38,4 +39,9 @@ public BatchAdaptorFactory getBatchAdaptorFactory(SourceConnector sourceConnecto
protected DataTypeDelegate getDataTypeDelegate() {
return dataTypeDelegate;
}

@Override
public MessageVocabulary getVocabulary(String version, String type) {
return new X12Vocabulary(version, type);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import com.mirth.connect.model.datatype.SerializerProperties;
import com.mirth.connect.model.transmission.TransmissionModeProperties;
import com.mirth.connect.model.transmission.framemode.FrameModeProperties;
import com.mirth.connect.model.util.MessageVocabulary;
import com.mirth.connect.plugins.DataTypeServerPlugin;
import com.mirth.connect.util.TcpUtil;

Expand Down Expand Up @@ -71,4 +72,9 @@ protected DataTypeDelegate getDataTypeDelegate() {
return dataTypeDelegate;
}

@Override
public MessageVocabulary getVocabulary(String version, String type) {
return new HL7v2Vocabulary(version, type);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import com.mirth.connect.donkey.server.message.batch.BatchAdaptorFactory;
import com.mirth.connect.model.datatype.DataTypeDelegate;
import com.mirth.connect.model.datatype.SerializerProperties;
import com.mirth.connect.model.util.MessageVocabulary;
import com.mirth.connect.plugins.DataTypeServerPlugin;

public class NCPDPDataTypeServerPlugin extends DataTypeServerPlugin {
Expand All @@ -38,4 +39,9 @@ public BatchAdaptorFactory getBatchAdaptorFactory(SourceConnector sourceConnecto
protected DataTypeDelegate getDataTypeDelegate() {
return dataTypeDelegate;
}

@Override
public MessageVocabulary getVocabulary(String version, String type) {
return new NCPDPVocabulary(version, type);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,34 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha

HttpServletRequest servletRequest = (HttpServletRequest)request;
String requestedWithHeader = (String) servletRequest.getHeader("X-Requested-With");

//if header is required and not present, send an error
if(isRequestedWithHeaderRequired && StringUtils.isBlank(requestedWithHeader)) {
if(isRequestedWithHeaderRequired && StringUtils.isBlank(requestedWithHeader) && !isWebPluginAssetRequest(servletRequest)) {
res.sendError(400, "All requests must have 'X-Requested-With' header");
}
else {
chain.doFilter(request, response);
}


}

/**
* A web administrator loads a plugin's browser assets from /api/webplugins/... using
* &lt;script&gt;/import(), which cannot set request headers. Those GETs serve only static
* UI code (no state change, nothing sensitive), so they are exempt from the CSRF header
* requirement. State-changing requests and all other endpoints still require the header.
*/
private static boolean isWebPluginAssetRequest(HttpServletRequest request) {
if (!"GET".equalsIgnoreCase(request.getMethod())) {
return false;
}
// getPathInfo() is context-relative (e.g. "/webplugins/..."); fall back to the full
// URI so a configured http.contextpath doesn't defeat the check.
String path = request.getPathInfo();
if (StringUtils.isBlank(path)) {
path = request.getRequestURI();
}
return path != null && (path.startsWith("/webplugins/") || path.contains("/api/webplugins/"));
}

public boolean isRequestedWithHeaderRequired() {
Expand Down
Loading
Loading