dontpanic/zammad-external-data
最新稳定版本:v1.2.1
Composer 安装命令:
composer create-project dontpanic/zammad-external-data
包简介
External data API for Zammad integration
README 文档
README
A tiny PHP service that exposes simple HTTP endpoints to fetch external data (via JSON‑RPC) and return it as JSON. Requests are routed to a controller that relays query parameters to a remote API using cURL.
This project is structured for small integrations (e.g., enriching Zammad or other systems) where you want a thin proxy layer between your application and a remote API.
What changed recently
- PHP namespace was renamed from
ApptoDontPanic\ZammadExternalData(PSR‑4 updated accordingly). - Router now supports explicit paths:
GET /objects→ forwards query params as a simple filterGET /conditions→ builds an i‑doit property condition from query paramsGET /report→ fetches objects delivered by a reportGET /zammad/tickets→ fetches tickets from Zammad
- Automatic base‑path detection in Router for subdirectory deployments.
Features
- Minimal routing with two endpoints (
/objects,/conditions) - Simple pass‑through of query parameters as filter conditions
- JSON responses, suitable for easy consumption
Contents
- public/index.php – front controller, initializes autoloading and routing
- src/Router/Router.php – simple router that dispatches to QueryController
- src/Controller/QueryController.php – loads configuration and calls ApiClient
- src/Service/ApiClient.php – cURL JSON‑RPC client with helper methods
- src/Config/Config.php – reads configuration from config.json
- config.json – API credentials and endpoint configuration (do not commit real secrets)
Requirements
- PHP 8.1+
- PHP cURL extension (ext-curl)
- Composer (for autoloading)
Note: ext-curl is required at runtime but is not explicitly listed in composer.json.
Installation
- Clone the repository
- Install dependencies (autoload only):
- If composer.lock is present, regenerate autoload files:
composer dump-autoload - Otherwise (or to refresh), run:
composer install
- If composer.lock is present, regenerate autoload files:
- Copy and edit config.json with your actual credentials and endpoint.
After namespace changes, ensure autoload is rebuilt:
composer dump-autoload
Configuration
The service reads its configuration from config/config.json (relative to the project root). Example:
{
"api": {
"url": "https://example.tld/i-doit/src/jsonrpc.php",
"token": "YOUR_API_TOKEN",
"user": "apiuser",
"password": "apipassword"
},
"zammad": {
"url": "https://zammad.exaple.tld",
"token": "token_from_a_user_with_access_to_tickets"
}
}
Fields used by the app:
- api.url – JSON‑RPC endpoint URL
- api.token – API token used on requests
- api.user – Auth username passed via header
- api.password – Auth password passed via header
The Config class loads this file and makes properties available to ApiClient.
Running locally
Use PHP’s built‑in web server and point the document root to public/:
php -S localhost:8000 -t public
Base path handling: When deployed in a subdirectory, the Router auto‑detects the base path (removes /public/index.php) so routes still work under that prefix.
Usage and request format
Objects by simple filter (forwards all query params to
ApiClient->getObjects()):curl "http://localhost:8000/objects?title=MyObject&status=C__RECORD_STATUS__NORMAL"More examples:
- Find by title:
GET /objects?title=MyObject - Filter by email:
GET /objects?email=user@example.com - Explicit status:
GET /objects?status=C__RECORD_STATUS__NORMAL
- Find by title:
Objects by property condition (uses
ApiClient->getObjectsByConditions()):The current implementation expects scalar parameters (not arrays):
curl "http://localhost:8000/conditions?category=C__CATG__GLOBAL&attribute=title&value=itsm*&comparison=like"This becomes a single condition with
property = "{category}-{attribute}".
Response is JSON with the raw payload returned by the remote JSON‑RPC endpoint.
Note: ApiClient also contains helper methods (e.g., getPersonByEmail, getPersonByLogin, read/write category helpers, batch operations). You can extend QueryController and routing logic to expose additional operations as needed.
Query Zammad for tickets with linked external data
Fetching Tickets
curl "http://localhost:8000/zammad/tickets/?query=object_name.value:object_id"
Adding a custom category to i-doit
This example uses the Zammad External Data Proxy to fetch tickets from Zammad and display them in an i-doit custom category.
In i-doit, go to Administration > Data structure > Custom categories and add a new category. For the new category add a new field with the type "Javascript" and add the following code:
// Fetch tickets from zammad-external-data
// Store a unique marker in the script itself, then find it (couldn’t find a better way to place the table in the DOM)
var currentScript = document.currentScript || (function() {
var scripts = document.getElementsByTagName('script');
for (var i = 0; i < scripts.length; i++) {
if (scripts[i].textContent.indexOf('UNIQUE_MARKER_12345') > -1) {
return scripts[i];
}
}
return scripts[scripts.length - 1];
})();
var container = currentScript.parentElement;
// Configuration
var proxyUrl = 'https://itsmcloud.panic.at/i-doit/zammad-external-data'; // Replace with your actual proxy URL
var zammadBaseURL = 'https://zammad.panic.at'; // Replace with your actual zammad URL
// Get object ID from URL parameter
var objectID = new URLSearchParams(window.location.search).get('objID')
// Show loading message
container.innerHTML = '<p style="padding: 10px;">Loading tickets...</p>';
// Fetch tickets through proxy
var xhr = new XMLHttpRequest();
xhr.open('GET', proxyUrl + '/zammad/tickets?query=idoit_organisations.value:' + objectID, true);
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
var response = JSON.parse(xhr.responseText);
// Clear loading message
container.innerHTML = '';
// Check for errors
if (response.error) {
container.innerHTML = '<p style="color: red; padding: 10px;">Error: ' + response.error + '</p>';
return;
}
// Response is directly an array of tickets
var tickets = Array.isArray(response) ? response : [];
// Check if we have tickets
if (tickets.length === 0) {
container.innerHTML = '<p style="padding: 10px;">No tickets found for this organisation.</p>';
return;
}
// Create wrapper div for margin
var wrapper = document.createElement('div');
wrapper.style.margin = '10px 20px';
// Create table
var table = document.createElement('table');
table.style.width = '100%';
table.style.borderCollapse = 'collapse';
// Create header
var thead = document.createElement('thead');
var headerRow = document.createElement('tr');
var headers = ['Subject', 'State', 'Owner', 'Created at', 'Updated at', 'Priority', "Group", ""];
headers.forEach(function(headerText) {
var th = document.createElement('th');
th.textContent = headerText;
th.style.border = '1px solid #ddd';
th.style.padding = '8px';
th.style.backgroundColor = '#f2f2f2';
th.style.textAlign = 'left';
th.style.fontWeight = 'bold';
headerRow.appendChild(th);
});
thead.appendChild(headerRow);
table.appendChild(thead);
// Create body with ticket data
var tbody = document.createElement('tbody');
tickets.forEach(function(ticket, index) {
var tr = document.createElement('tr');
if (index % 2 === 0) {
tr.style.backgroundColor = '#f9f9f9';
}
// Title
var tdTitle = document.createElement('td');
tdTitle.textContent = ticket.title || '-';
tdTitle.style.border = '1px solid #ddd';
tdTitle.style.padding = '8px';
tr.appendChild(tdTitle);
// State
var tdState = document.createElement('td');
tdState.textContent = ticket.state || '-';
tdState.style.border = '1px solid #ddd';
tdState.style.padding = '8px';
tr.appendChild(tdState);
// Owner
var tdOwner = document.createElement('td');
tdOwner.textContent = ticket.owner || '-';
tdOwner.style.border = '1px solid #ddd';
tdOwner.style.padding = '8px';
tr.appendChild(tdOwner);
// created at
var tdCreatedAt = document.createElement('td');
tdCreatedAt.textContent = ticket.created_at || '-';
tdCreatedAt.style.border = '1px solid #ddd';
tdCreatedAt.style.padding = '8px';
tr.appendChild(tdCreatedAt);
// created at
var tdUpdatedAt = document.createElement('td');
tdUpdatedAt.textContent = ticket.updated_at || '-';
tdUpdatedAt.style.border = '1px solid #ddd';
tdUpdatedAt.style.padding = '8px';
tr.appendChild(tdUpdatedAt);
// Priority
var tdPriority = document.createElement('td');
tdPriority.textContent = ticket.priority || '-';
tdPriority.style.border = '1px solid #ddd';
tdPriority.style.padding = '8px';
tr.appendChild(tdPriority);
// Group
var tdGroup = document.createElement('td');
tdGroup.textContent = ticket.group || '-';
tdGroup.style.border = '1px solid #ddd';
tdGroup.style.padding = '8px';
tr.appendChild(tdGroup);
// Link to ticket (https://zammad.panic.at/#ticket/zoom/id)
var tdLink = document.createElement('td');
tdLink.style.border = '1px solid #ddd';
tdLink.style.padding = '8px';
var link = document.createElement('a');
link.href = zammadBaseURL + '/#ticket/zoom/' + ticket.id;
link.className = 'btn';
link.target = '_blank';
var img = document.createElement('img');
img.src = 'images/axialis/basic/link.svg';
img.alt = '';
var span = document.createElement('span');
span.textContent = 'Open in Zammad';
link.appendChild(img);
link.appendChild(span);
tdLink.appendChild(link);
tr.appendChild(tdLink);
tbody.appendChild(tr);
});
table.appendChild(tbody);
wrapper.appendChild(table);
container.appendChild(wrapper);
} else {
container.innerHTML = '<p style="color: red; padding: 10px;">Error loading tickets: ' + xhr.status + '</p>';
}
};
xhr.onerror = function() {
container.innerHTML = '<p style="color: red; padding: 10px;">Network error occurred while loading tickets</p>';
};
xhr.send();
Project structure (overview)
- public/index.php – bootstraps Composer and dispatches request
- src/Router/Router.php – Router::dispatch($uri, $query) and base‑path handling
- src/Controller/QueryController.php – loads ../config.json, exposes /objects and /conditions
- src/Service/ApiClient.php – cURL JSON‑RPC wrapper (post, getObjects, getObjectsByConditions, and more)
- src/Config/Config.php – parses config.json
Troubleshooting
- 400/500 errors from upstream: ApiClient throws when HTTP code >= 400; ensure credentials and endpoint are correct.
- SSL verification is disabled in the client (curl options CURLOPT_SSL_VERIFYPEER=false and CURLOPT_SSL_VERIFYHOST=0). For production, enable verification or terminate TLS at a trusted proxy.
- If autoloading fails, run
composer dump-autoloadand verify PSR‑4 mapping in composer.json:"DontPanic\\ZammadExternalData\\": "src/"
Security and deployment notes
- Do NOT commit real secrets in config.json; use environment‑specific copies.
- Restrict network access to this service if it proxies sensitive data.
- Consider moving secrets to environment variables or a secret manager for production.
- Enable TLS termination in front of this service when deploying.
License
This project is licensed under the MIT License – see the LICENSE file for details.
统计信息
- 总下载量: 8
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 0
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2025-10-23