Cyclos web services and WEB-RPC
This documentation is kept for historic purposes, to document WEB-RPC and its Java / PHP libraries. It is not recommented to directly use Cyclos WEB-RPC web services, but the REST API instead. For this reason, starting with Cyclos 4.16, the PHP library will no longer be published.
Web services
Here you will find infomation on how to call Cyclos services from 3rd party applications.
Cyclos 4 provides distinct web service interfaces: the REST API, custom web services and WEB-RPC. In all cases, security layer is exactly the same (hence, both grant exactly the same permissions), and users are authenticated in the same way, as webservices-auth,described below.
WARNING Do not call the Cyclos web services from within a Cyclos script. This will cause the main database transaction which is running the script to open another request, which will open another database transaction. Besides being sub-optimal in terms of architecture and performance, it can lead to dead locks, in which the database connection pool is exhausted and no new request is accepted.
3.1. REST API
This API is implemented with REST concepts in mind, such as using proper HTTP verbs (GET, PUT, POST, DELETE), using JSON data for input and output. It should be relatively easy for developers to leverage existing knowledge when using it. It is documented using Open API.
A detailed reference documentation is available online on each Cyclos installation, at <cyclos-root-url>[/network]/api. You can also refer to the Cyclos Demo API. It is possible to disable the API reference documentation page by setting cyclos.rest.reference = false in cyclos.properties.
The REST API contains a subset of the Cyclos functionality. Most notably, system management and content management are not part of the API. Most user-facing operations, however, are available via the REST interface. New functionality will be added on demand, in a cautious manner, as each path, parameter and data model needs to be planned to fit the target architecture.
The REST interface is the preferred API for 3rd party clients to connect to Cyclos, as it should be relatively stable between Cyclos releases, and provides better documentation and tooling support. For example, there are OpenAPI generators that can generate clients for distinct languages / frameworks.
3.2. Custom web services
It is also possible to create custom web services, which run a script on every invocation. Custom web services can be configured to be executed as guest, with a fixed HTTP user / password or as a Cyclos user (using the same authentication as other web services).
After creating a custom web service script, you have to create a custom web service entity in System > Tools > Custom web services. There you will find the Url mappings field, which contains the path which the script should be available. The endpoint for the custom web service will be <cyclos-root>/run/<mapping>.
Also, take into account that custom web services executed as a logged user must also be granted to users through a product (System > User configuration > Products (permissions)).
3.3. WEB-RPC
The WEB-RPC provides access to the entire service layer in Cyclos. It is used by the classic Cyclos web application. The WEB-RPC also uses JSON objects for input / output, and provides, besides plain HTTPS calls. Java and PHP client libraries are provided for backwards compatibility. It is not recommended to use the WEB-RPC directly, unless some very specific service is needed, which is not available via the REST API. The service layer can change frequently between milestone releases (example, 4.10 to 4.11).
The available services the public service interfaces, are listed JavaDocs, under each org.cyclos.services subpackage.
3.3.1. Using WEB-RPC services
For other clients, WEB-RPC behaves like a "REST level 0", or RPC-like interface is available, using JSON encoded strings for passing parameters and receiving results from services.
Each service responds to POST requests to the following URL <cyclos.root>/[network/]web-rpc/<short-service-name>, where the short-service-name is the service with the first letter as lowercase. Also, the Service suffix can be omitted, so, for example both /web-rpc/accountService and /web-rpc/account are mapped to AccountService. For more on this, refer to URL mapping.
For authentication, the same methods described in authentication are used for WEB-RPC as well. Also, to specify a channel, pass the HTTP header Channel: <channel internal name>. If no channel is passed, the Web services channel is assumed.
When the URL is specified up to the service, as stated above, the request body must be a JSON object with the operation and params properties. operation indicates the service method name, and params is either an array with parameters, or optionally the parameter if the method has a single parameter (without the array) or even omitted if the method have no parameters.
For objects, the parameters are expected to be the same as the Java counterparts (see the JavaDocs for a reference on the available properties for each object).
As result, if the request was successful (http status code is 200), an object with a single property called result will be returned. The object has the same structure as the object returned by the service method, or is a string, boolean or number for simple types.
Requests which resulted in error (status code distinct than 200) will have the following structure:
-
errorCode: A string generated from the exception java class name. The unqualified class name has the Exception suffix removed, and is transformed to all uppercase, separated by underlines. So, for example, for org.cyclos.model.ValidationException, the error code isVALIDATION; for org.cyclos.model.banking.InsufficientBalanceException, the error code isINSUFFICIENT_BALANCE, and so on. -
Any other properties (public getters in the exception class) the thrown exception has will also be mapped as a property here, for example, org.cyclos.model.ValidationException holds a property called
validationwhich contains a org.cyclos.utils.ValidationResult.
URL mapping
Besides using the URL pointing to the service, and have the POST body as a JSON, selecting the operation and the parameters, it is also possible to choose the operation in the URL itself, as a subpath in the URL.
For example, <cyclos-root>/web-rpc/userService/search already maps to the search operation. In this case, the POST body must contain only the JSON for the parameters, with the same rules as explained above: an array with parameter values, or, if is a single parameter, the body can be the JSON value directly, and if no parameters, the POST body can be empty.
Additionally, the service methods that are readonly can be invoked by GET requests. In this case, the parameter can be passed using 2 forms:
-
When the parameters are simple (just identifiers or internal names), they can be passed in as URL parts. For example,
<cyclos-root>/web-rpc/accountService/load/836144284089; -
When there is a single parameter of type object, it can be passed using URL parameters. For example:
<cyclos-root>/userService/search?keywords=shop&groups=business;
Finally, services are mapped to other 2 URLs besides <name>Service: one without the Service suffix, and another one, pluralized. Also, if an operation doesn’t match and the request method is GET it will be attempted by prepending 'get' with the first letter capitalized. This will allow shorter urls on calls, like:
-
GET <cyclos-root>/web-rpc/users/search?keywords=shop&groups=businessis equivalent toGET <cyclos-root>/web-rpc/userService/search?keywords=shop&groups=business; -
GET <cyclos-root>/web-rpc/user/data/4534657457is equivalent toGET <cyclos-root>/web-rpc/userService/getData/4534657457.
Details on JSON handling
In WEB-RPC, all output objects, when converted to JSON, will have a property called class, which represents the fully-qualified Java class name of the source object. Most clients can just ignore the result.
However, when sending requests to services that expect a polymorphic object, the server needs to know which subclass the passed object represents. In those cases, passing the class property, with the fully qualified Java class name is required.
An example is the AdService. When saving an advertisement, it could either be a simple advertisement (AdvertisementDTO) or a webshop advertisement (AdWebShopDTO). In this case, a class property with the fully qualified class name is required. Note, however, that in most cases, the class information is not needed. And many times, the expected flow is to first call a method (such as getData) which returns data containing an object that should be modified and sent back. In such scenarios, clients also don’t have to worry about the class property.
Whenever a subclass of EntityVO is used, numbers or strings are also accepted (besides objects). Numbers always represent the VO identifier (id property). Strings can either be the id when they are numeric, or can represent one of the following cases:
-
When the type is a BasicUserVO or a subclass, an UserLocatorVO is created, and the string is used as the value of the
principalproperty. If the string isselfit will resolve to the logged user; -
When the type is AccountVO, the string represents the account number;
-
When the destination VO has the
internalNameproperty, the string is used for the internal name; -
In other cases, the VO will be null
If the value is supposed to be a number handled as string principal (for example, a mobile phone) or account number, it must be prefixed with a single quote. For example, to represent a phone number as string, the following is accepted: '5187653456. If not prefixed, it would be interpreted as user id instead. The single quote prefix is the same as Excel / LibreOffice use to represent a number as string.
Other points to note with JSON handling:
-
Whenever a collection is expected, a single value can be passed, resulting in a collection with a single element;
-
Java long values (mostly identifiers) are always returned as string, because of the identifier ciphering, the whole 64-bit space is used. In JavaScript, however, integer numbers cannot use 64 bit, resulting in different numbers when reading from JSON.
-
Whenever dates are used (represented by the DateTime class) they are returned / expected to be strings in the ISO 8601 format, with or without timezone. On input, if no time zone is set, it is assumed the one from the current user configuration. For example, "2015-01-31T22:29:00" represent 31 January 2015, at 10:29 pm in the user time zone, whereas "2015-01-31T22:29:00+03:00" represents the same date in the GMT+3 offset. Also, for input, the text "now" is accepted (without quotes) to represent the current time.
Examples
Assuming that the authentication header is correctly passed, the following request can be performed to search for users: POST https://my.cyclos.instance.com/network/web-rpc/userService with the following body:
{
"operation": "search",
"params": {
"keywords": "user",
"groups": "consumers",
"pageSize": 5
}
}
The resulting JSON will be something like:
{
"result": {
"currentPage": "0",
"pageSize": "5",
"totalCount": "2",
"pageItems": [
{
"class": "org.cyclos.model.users.users.UserVO",
"id": "-2717327251475675143",
"display": "Consumer 1"
},
{
"class": "org.cyclos.model.users.users.UserVO",
"id": "-2717467988964030471",
"display": "Consumer 3"
}
]
}
}
Note the params "groups" property of the input query is a collection of BasicGroupVO. It is being passed the string "consumers", which is matched to the group internal name.
The above request is equivalent to a POST to https://my.cyclos.instance.com/network/web-rpc/users/search (using the plural name) with the following body:
{
"keywords": "user",
"groups": "consumers",
"pageSize": 5
}
Also, the above request is equivalent to GET to https://my.cyclos.instance.com/network/web-rpc/user/search?keywords=user&groups=consumers&pageSize=5(singular name). Only methods which take a single parameter object can use query parameters.
3.3.2. Java WEB-RPC client
Cyclos provides a Java utility to access all services using WEB-RPC, which can be used on 3rd party Java applications. Starting with Cyclos version 4.16, Java 17 is required for clients to use Cyclos. Keep in mind that the REST API is recommended. Only use the Java client if the needed operation isn’t available in the REST API.
Dependencies
In order to use the client, you will need some JAR files which are available in the download bundle, on the cyclos-4.x.x/web/WEB-INF/lib directory. Not all jars are required, only the following:
-
cyclos-api-*.jar -
jackson-core-*.jar -
jackson-databind-*.jar -
jackson-annotations-*.jar -
httpclient-*.jar -
httpcore-*.jar -
commons-collections-*.jar -
commons-logging-*.jar -
commons-codec-*.jar -
guava-*.jar
Those jars (except cyclos-*.jar) are provided by the following projects, all distributed under the Apache 2.0 license:
-
http://hc.apache.org/[Apache HttpComponents
Usage
The Java client for Cyclos 4 uses JSON as the data interchange format, Jackson for serializing and deserializing JSON to Java objects and vice-versa and Apache HttpComponents to communicate with the server and invoke the web services.
A dynamic proxy for the service interface is obtained, and methods can be invoked on it as if it were a local object. The proxy, however, passes the parameters to the server and returns the result back to the client.
The Cyclos 4 API library provides the org.cyclos.server.utils.HttpServiceFactory class, which is used to obtain the service proxies. Here is an example:
HttpServiceFactory factory = new HttpServiceFactory();
factory.setRootUrl("https://www.my-cyclos.com/network");
factory.setInvocationData(HttpServiceInvocationData.stateless("username", "password"));
// OR factory.setInvocationData(HttpServiceInvocationData.stateful("session token"));
// OR factory.setInvocationData(HttpServiceInvocationData.accessClient("access client token"));
AccountService accountService = factory.getProxy(AccountService.class);
In the above example, the AccountService can be used to query account information. The permissions are the same as in the main Cyclos application. The user may be either a regular user or an administrator. When an administrator, will allow performing operations over regular users (managed by that administrator). Otherwise, the web services will only affect the own user.
To specify a channel other than Web Services, call HttpServiceInvocationData.setChannel(internalName) before passing it to the factory.
Examples
Configure Cyclos
All following examples use the following class to configure the web services:.
import org.cyclos.server.utils.HttpServiceFactory;
import org.cyclos.server.utils.HttpServiceInvocationData;
/**
* This class will provide the Cyclos server configuration for the web service
* samples
*/
public class Cyclos {
private static final String ROOT_URL = "http://localhost:8888/england";
private static HttpServiceFactory factory;
static {
factory = new HttpServiceFactory();
factory.setRootUrl(ROOT_URL);
factory.setInvocationData(HttpServiceInvocationData.stateless("admin", "1234"));
}
public static HttpServiceFactory getServiceFactory() {
return factory;
}
public static HttpServiceFactory getServiceFactory(
HttpServiceInvocationData invocationData) {
var factory = new HttpServiceFactory();
factory.setRootUrl(ROOT_URL);
factory.setInvocationData(invocationData);
return factory;
}
}
Search users
import org.cyclos.model.users.users.UserQuery;
import org.cyclos.model.users.users.UserVO;
import org.cyclos.services.users.UserService;
/**
* Provides a sample on searching for users
*/
public class SearchUsers {
public static void main(String[] args) throws Exception {
var userService = Cyclos.getServiceFactory().getProxy(UserService.class);
// Search for the top 5 users by keywords
var query = new UserQuery();
query.setKeywords("consumer");
query.setIgnoreProfileFieldsInList(true);
query.setPageSize(5);
var users = userService.search(query);
System.out.printf("Found a total of %d users\n", users.getTotalCount());
for (UserVO user : users) {
System.out.printf("* %s\n", user.getDisplay());
}
}
}
Search advertisements
import org.cyclos.model.marketplace.advertisements.BasicAdQuery;
import org.cyclos.model.marketplace.advertisements.BasicAdVO;
import org.cyclos.services.marketplace.AdService;
/**
* Provides a sample on searching for advertisements
*/
public class SearchAds {
public static void main(String[] args) throws Exception {
var adService = Cyclos.getServiceFactory().getProxy(AdService.class);
var query = new BasicAdQuery();
query.setKeywords("Gear");
query.setHasImages(true);
var ads = adService.search(query);
System.out.printf("Found a total of %d advertisements\n", ads.getTotalCount());
for (BasicAdVO ad : ads) {
System.out.printf("%s\nBy: %s\n%s\n-------\n",
ad.getName(), ad.getOwner().getDisplay(),
ad.getDescription());
}
}
}
Register user
import java.util.ArrayList;
import java.util.Arrays;
import org.cyclos.model.system.fields.CustomFieldDetailedVO;
import org.cyclos.model.system.fields.CustomFieldPossibleValueVO;
import org.cyclos.model.users.addresses.UserAddressDTO;
import org.cyclos.model.users.fields.UserCustomFieldValueDTO;
import org.cyclos.model.users.groups.BasicGroupVO;
import org.cyclos.model.users.groups.GroupVO;
import org.cyclos.model.users.phones.LandLinePhoneDTO;
import org.cyclos.model.users.phones.MobilePhoneDTO;
import org.cyclos.model.users.users.PasswordRegistrationDTO;
import org.cyclos.model.users.users.PasswordRegistrationData;
import org.cyclos.model.users.users.UserDataParams;
import org.cyclos.model.users.users.UserRegistrationDTO;
import org.cyclos.model.users.users.UserRegistrationData;
import org.cyclos.model.users.users.UserSearchContext;
import org.cyclos.services.users.UserService;
import org.cyclos.utils.CustomFieldHelper;
/**
* Provides a sample on registering a user with all custom fields, addresses
* and phones
*/
public class RegisterUser {
public static void main(String[] args) {
// Get the services
var userService = Cyclos.getServiceFactory().getProxy(UserService.class);
// The available groups for new users are obtained in the search data
var searchData = userService.getSearchData(UserSearchContext.MENU, null);
var possibleGroups = searchData.getInitialGroups();
// Find the consumers group
GroupVO group = null;
for (BasicGroupVO current : possibleGroups) {
if (current instanceof GroupVO && current.getName().equals("Consumers")) {
group = (GroupVO) current;
break;
}
}
// Get data for a new user
var params = new UserDataParams();
params.setGroup(group);
var data = (UserRegistrationData) userService.getDataForNew(params);
// Basic fields
var user = (UserRegistrationDTO) data.getDto();
user.setPasswords(new ArrayList<PasswordRegistrationDTO>());
var passwords = data.getPasswordsData();
for (PasswordRegistrationData passData : passwords) {
var passDTO = new PasswordRegistrationDTO();
passDTO.setType(passData.getType());
passDTO.setValue("1234");
passDTO.setConfirmationValue("1234");
passDTO.setAssign(true);
passDTO.setForceChange(true);
user.getPasswords().add(passDTO);
}
user.setGroup(group);
user.setName("John Smith");
user.setUsername("johnsmith");
user.setEmail("john.smith@mail.com");
user.setSkipActivationEmail(true);
// Custom fields
var customFields =
CustomFieldHelper.getCustomFields(data.getProfileFieldActions());
CustomFieldDetailedVO gender = null;
CustomFieldDetailedVO idNumber = null;
for (CustomFieldDetailedVO customField : customFields) {
if (customField.getInternalName().equals("gender")) {
gender = customField;
}
if (customField.getInternalName().equals("idNumber")) {
idNumber = customField;
}
}
user.setCustomValues(new ArrayList<UserCustomFieldValueDTO>());
// Value for the gender custom field
var genderValue = new UserCustomFieldValueDTO();
genderValue.setField(gender);
for (CustomFieldPossibleValueVO possibleValue : gender.getPossibleValues()) {
if (possibleValue.getValue().equals("Male")) {
// Found the value for 'Male'
genderValue.setEnumeratedValue(possibleValue);
break;
}
}
user.getCustomValues().add(genderValue);
// Value for id number custom field
var idNumberValue = new UserCustomFieldValueDTO();
idNumberValue.setField(idNumber);
idNumberValue.setStringValue("123.456.789-10");
user.getCustomValues().add(idNumberValue);
// Address
var address = new UserAddressDTO();
address.setName("Home");
address.setAddressLine1("John's Street, 500");
address.setCity("John's City");
address.setRegion("John's Region");
address.setCountry("BR"); // Country is given in 2-letter ISO code
user.setAddresses(Arrays.asList(address));
// Landline phone
var landLinePhone = new LandLinePhoneDTO();
landLinePhone.setName("Home");
landLinePhone.setRawNumber("+551133333333");
user.setLandLinePhones(Arrays.asList(landLinePhone));
// Mobile phone
var mobilePhone = new MobilePhoneDTO();
mobilePhone.setName("Mobile phone 1");
mobilePhone.setRawNumber("+5511999999999");
user.setMobilePhones(Arrays.asList(mobilePhone));
// Effectively register the user
var result = userService.register(user);
var status = result.getStatus();
switch (status) {
case ACTIVE:
System.out.println("The user is now active");
break;
case INACTIVE:
System.out.println("The user is in an inactive group, "
+ "and needs activation by administrators");
break;
case EMAIL_VALIDATION:
System.out
.println("The user needs to validate the e-mail "
+ "address in order to confirm the registration");
break;
}
}
}
Edit user profile
import org.cyclos.model.users.fields.UserCustomFieldValueDTO;
import org.cyclos.model.users.users.EditProfileData;
import org.cyclos.model.users.users.UserLocatorVO;
import org.cyclos.services.users.UserService;
public class EditUser {
public static void main(String[] args) {
// Get the services
var userService = Cyclos.getServiceFactory().getProxy(UserService.class);
// Locate the user by username, so we get the id
var locate = new UserLocatorVO(UserLocatorVO.USERNAME, "some-user");
var userVO = userService.locate(locate);
// Get the profile data
var data = (EditProfileData) userService.getData(userVO.getId());
var user = data.getDto();
user.setName("Some modified name");
var customValues = user.getCustomValues();
for (UserCustomFieldValueDTO fieldValue : customValues) {
if (fieldValue.getField().getInternalName().equals("website")) {
fieldValue.setStringValue("http://new.url.com");
}
}
// Update the user
userService.save(user);
System.out.println("The user was updated.");
}
}
Login user
import org.cyclos.model.access.LoggedOutException;
import org.cyclos.model.access.channels.BuiltInChannel;
import org.cyclos.model.banking.accounts.AccountWithStatusVO;
import org.cyclos.model.users.users.UserLocatorVO;
import org.cyclos.model.users.users.UserLoginDTO;
import org.cyclos.model.users.users.UserLoginResult;
import org.cyclos.server.utils.HttpServiceInvocationData;
import org.cyclos.services.access.LoginService;
import org.cyclos.services.banking.AccountService;
/**
* Cyclos web service example: logs-in a user via web services.
* This is useful when creating an alternative front-end for Cyclos.
*/
public class LoginUser {
public static void main(String[] args) throws Exception {
// This LoginService has the administrator credentials
var loginService = Cyclos.getServiceFactory().getProxy(LoginService.class);
// Another option is to use an access client to connect with the
// server (for the admin)
// To make it works you must:
// 1- create an access client
// 2- assign it to the admin (to obtain the activation code)
// 3- activate it making a HTTP POST to the server using this url:
// ROOT_URL/activate-access-client containing only the activation code
// as the body
// 4- put the token returned from the servlet as the parameter of the
// HttpServiceInvocationData.accessClient(...) method
// 5- comment the first line (that using user and password and
// uncomment the following two sentences
// HttpServiceInvocationData adminSessionInvocationData =
// HttpServiceInvocationData
// .accessClient("put_the_token_here");
// LoginService loginService = Cyclos.getServiceFactory(
// adminSessionInvocationData).getProxy(LoginService.class);
var remoteAddress = "192.168.1.200";
// Set the login parameters
var params = new UserLoginDTO();
params.setUser(new UserLocatorVO(UserLocatorVO.USERNAME, "some-user"));
params.setPassword("1234");
params.setRemoteAddress(remoteAddress);
params.setChannel(BuiltInChannel.MAIN.getInternalName());
// Login the user
UserLoginResult result = loginService.loginUser(params);
var userAuth = result.getUser();
var sessionToken = result.getSessionToken();
System.out.println("Logged-in '" + userAuth.getUser().getDisplay()
+ "' with session token = " + sessionToken);
// Do something as user. As the session token is only valid per ip
// address, we need to pass-in the client ip address again
var sessionInvocationData =
HttpServiceInvocationData.stateful(sessionToken, remoteAddress);
// The services acquired by the following factory will carry on the
// user session data
var userFactory = Cyclos.getServiceFactory(sessionInvocationData);
var accountService = userFactory.getProxy(AccountService.class);
var accounts =
accountService.getAccountsSummary(userAuth.getUser(), null);
for (AccountWithStatusVO account : accounts) {
System.out.println(account.getType()
+ ", balance: " + account.getStatus().getBalance());
}
// Logout. There are 2 possibilities:
// - Logout as administrator:
loginService.logoutUser(sessionToken);
// - OR logout as own user:
try {
userFactory.getProxy(LoginService.class).logout();
} catch (LoggedOutException e) {
// already logged out
}
}
}
Get account information
import java.math.BigDecimal;
import org.cyclos.model.banking.accounts.AccountHistoryEntryVO;
import org.cyclos.model.banking.accounts.AccountHistoryQuery;
import org.cyclos.model.banking.accounts.AccountWithStatusVO;
import org.cyclos.model.banking.accounttypes.AccountTypeNature;
import org.cyclos.model.users.users.UserLocatorVO;
import org.cyclos.model.users.users.UserVO;
import org.cyclos.services.banking.AccountService;
/**
* Provides a sample on getting the account information for a given user.
*/
public class GetAccountInformation {
public static void main(String[] args) throws Exception {
var accountService =
Cyclos.getServiceFactory().getProxy(AccountService.class);
// Get the accounts summary
var user = new UserLocatorVO(UserLocatorVO.USERNAME, "some-user");
var accounts = accountService.getAccountsSummary(user, null);
// For each account, we'll show the balances
for (AccountWithStatusVO account : accounts) {
var status = account.getStatus();
if (status != null) {
var balance = status.getBalance();
System.out.printf("%s has balance of %.2f %s\n",
account.getType().getName(),
balance,
account.getCurrency());
}
// Also, search for the last 5 payments on each account
var query = new AccountHistoryQuery();
query.setAccount(account);
query.setPageSize(5);
var entries = accountService.searchAccountHistory(query);
for (AccountHistoryEntryVO entry : entries) {
var relatedAccount = entry.getRelatedAccount();
var relatedType = relatedAccount.getType();
var relatedNature = relatedType.getNature();
// The from or to...
String fromOrTo;
if (relatedNature == AccountTypeNature.SYSTEM) {
// ... might be the account type name if a system account
fromOrTo = relatedType.getName();
} else {
// ... or just the user display
var relatedUser = (UserVO) relatedAccount.getOwner();
fromOrTo = relatedUser.getDisplay();
}
// Display the amount, which can be negative or positive
var amount = entry.getAmount();
var debit = amount.compareTo(BigDecimal.ZERO) < 0;
System.out.printf("Date: %s\n", entry.getDate());
System.out.printf("%s: %s\n", debit ? "To" : "From", fromOrTo);
System.out.printf("Amount: %.2f\n", amount);
System.out.println();
}
System.out.println("**********");
}
}
}
Perform payment
import java.math.BigDecimal;
import org.cyclos.model.EntityNotFoundException;
import org.cyclos.model.banking.InsufficientBalanceException;
import org.cyclos.model.banking.MaxAmountPerDayExceededException;
import org.cyclos.model.banking.MaxAmountPerMonthExceededException;
import org.cyclos.model.banking.MaxAmountPerWeekExceededException;
import org.cyclos.model.banking.MaxAmountPerYearExceededException;
import org.cyclos.model.banking.MaxPaymentAmountExceededException;
import org.cyclos.model.banking.MaxPaymentsPerDayExceededException;
import org.cyclos.model.banking.MaxPaymentsPerMonthExceededException;
import org.cyclos.model.banking.MaxPaymentsPerWeekExceededException;
import org.cyclos.model.banking.MinTimeBetweenPaymentsException;
import org.cyclos.model.banking.accounts.InternalAccountOwner;
import org.cyclos.model.banking.accounts.SystemAccountOwner;
import org.cyclos.model.banking.transactions.PerformPaymentDTO;
import org.cyclos.model.banking.transactions.PerformPaymentData;
import org.cyclos.model.banking.transactions.TransactionAuthorizationStatus;
import org.cyclos.model.users.users.UserLocatorVO;
import org.cyclos.services.banking.PaymentService;
import org.cyclos.services.banking.TransactionService;
import org.cyclos.utils.CollectionHelper;
/**
* Provides a sample on performing a payment between a user and a system
* account
*/
public class PerformPayment {
public static void main(String[] args) {
// Get the services
var factory = Cyclos.getServiceFactory();
var transactionService = factory.getProxy(TransactionService.class);
var paymentService = factory.getProxy(PaymentService.class);
// The payer and payee
InternalAccountOwner payer = new UserLocatorVO(UserLocatorVO.USERNAME, "some-user");
InternalAccountOwner payee = SystemAccountOwner.instance();
// Get data regarding the payment
PerformPaymentData data;
try {
data = transactionService.getPaymentData(payer, payee, null);
} catch (EntityNotFoundException e) {
System.out.println("Some of the users were not found");
return;
}
// Get the first available payment type
var types = data.getPaymentTypes();
var paymentType = CollectionHelper.first(types);
if (paymentType == null) {
System.out.println("There is no possible payment type");
}
// The payment amount
var amount = new BigDecimal(10.5);
// Perform the payment itself
var payment = new PerformPaymentDTO();
payment.setType(paymentType);
payment.setOwner(data.getFrom());
payment.setSubject(data.getTo());
payment.setAmount(amount);
try {
var result = paymentService.perform(payment);
// Check whether the payment is pending authorization
var auth = result.getAuthorizationStatus();
if (auth == TransactionAuthorizationStatus.PENDING_AUTHORIZATION) {
System.out.println("The payment is pending authorization");
} else {
System.out.println("The payment has been processed");
}
} catch (InsufficientBalanceException e) {
System.out.println("Insufficient balance");
} catch (MaxPaymentsPerDayExceededException e) {
System.out.println("Maximum daily amount of transfers "
+ e.getMaxPayments() + " has been reached");
} catch (MaxPaymentsPerWeekExceededException e) {
System.out.println("Maximum weekly amount of transfers "
+ e.getMaxPayments() + " has been reached");
} catch (MaxPaymentsPerMonthExceededException e) {
System.out.println("Maximum monthly amount of transfers "
+ e.getMaxPayments() + " has been reached");
} catch (MinTimeBetweenPaymentsException e) {
System.out.println("A minimum period of time should be awaited to make "
+ "a payment of this type");
} catch (MaxAmountPerDayExceededException e) {
System.out.println("Maximum daily amount of "
+ e.getMaxAmount() + " has been reached");
} catch (MaxAmountPerWeekExceededException e) {
System.out.println("Maximum weekly amount of "
+ e.getMaxAmount() + " has been reached");
} catch (MaxAmountPerMonthExceededException e) {
System.out.println("Maximum monthly amount of "
+ e.getMaxAmount() + " has been reached");
} catch (MaxAmountPerYearExceededException e) {
System.out.println("Maximum yearly amount of "
+ e.getMaxAmount() + " has been reached");
} catch (MaxPaymentAmountExceededException e) {
System.out.println("Maximum payment amount of "
+ e.getMaxAmount() + " has been reached");
} catch (Exception e) {
System.out.println("The payment couldn't be performed");
}
}
}
3.3.3. PHP WEB-RPC client
Cyclos provides a PHP client library for invoking WEB-RPC operations more easily. Keep in mind that the REST API is recommended. Only use the PHP client if the needed operation isn’t available in the REST API.
Here are the download links for the PHP client library:
-
Latest Cyclos version: https://documentation.cyclos.org/current/cyclos-php-library.zip;
-
Cyclos 4.16: https://documentation.cyclos.org/4.16/cyclos-php-library.zip.
A PHP class is generated for each Cyclos service interface, and all methods are generated on them. The parameters and result types, however, are not generated, and are either handled as strings, numbers, booleans or generic objects (stdclass). Please, read the considerations in Details on JSON handling.
You can download the PHP client for the corresponding Cyclos version on https://documentation.cyclos.org/4.16/cyclos-php-library.zip.
Dependencies
-
PHP 5.3 or newer
-
PHP CURL extension (Ubuntu package
php5-curl) -
PHP JSON extension (Ubuntu package
php5-json)
Usage
In order to use the Cyclos classes, we first register an autoload function to load the required classes automatically, like this:
function load($c) {
if (strpos($c, "Cyclos\\") >= 0) {
include str_replace("\\", "/", $c) . ".php";
}
}
spl_autoload_register("load");
Then, the Cyclos class must be configured with the server root URL and authentication details:
Cyclos\Configuration::setRootUrl("http://192.168.1.27:8888/network");
Cyclos\Configuration::setAuthentication("admin", "1234");
// OR Cyclos\Configuration::setSessionToken("sessionToken");
// OR Cyclos\Configuration::setAccessClientToken("accessClientToken");
To specify a channel other than Web Services, call Cyclos\Configuration::setChannel("channel");
Afterwards, services can be instantiated using the new operator, and the corresponding methods will be available:
$userService = new Cyclos\UserService();
$page = $userService->search(new stdclass());
Examples
Configuration
All the following examples include the configureCyclos.php file, which contains the following:
<?php
function load($c) {
if (strpos($c, "Cyclos\\") >= 0) {
include str_replace("\\", "/", $c) . ".php";
}
}
spl_autoload_register('load');
Cyclos\Configuration::setRootUrl("http://localhost:8888/england");
Cyclos\Configuration::setAuthentication("admin", "1234");
?>
Search users
<?php
require_once 'configureCyclos.php';
$userService = new Cyclos\UserService();
$query = new stdclass();
$query->keywords = 'Consumer*';
$query->pageSize = 5;
$query->ignoreProfileFieldsInList = true;
$page = $userService->search($query);
echo("Found a total of $page->totalCount users\n");
if (!empty($page->pageItems)) {
foreach ($page->pageItems as $user) {
echo("* $user->display\n");
}
}
?>
Search advertisements
<?php
require_once 'configureCyclos.php';
$adService = new Cyclos\AdService();
$query = new stdclass();
$query->keywords = 'Computer*';
$query->pageSize = 10;
$query->orderBy = 'PRICE_LOWEST';
$page = $adService->search($query);
echo("Found a total of $page->totalCount advertisements\n");
if (!empty($page->pageItems)) {
foreach ($page->pageItems as $ad) {
echo("* $ad->name\n");
}
}
?>
Login user
<?php
// Configure Cyclos and obtain an instance of LoginService
require_once 'configureCyclos.php';
$loginService = new Cyclos\LoginService();
// Set the parameters
$params = new stdclass();
$params->user = array("principal" => $_POST['username']);
$params->password = $_POST['password'];
$params->remoteAddress = $_SERVER['REMOTE_ADDR'];
// Perform the login
try {
$result = $loginService->loginUser($params);
} catch (Cyclos\ConnectionException $e) {
echo("Cyclos server couldn't be contacted");
die();
} catch (Cyclos\ServiceException $e) {
switch ($e->errorCode) {
case 'VALIDATION':
echo("Missing username / password");
break;
case 'LOGIN':
echo("Invalid username / password");
break;
case 'REMOTE_ADDRESS_BLOCKED':
echo("Your access is blocked by exceeding invalid login attempts");
break;
default:
echo("Error while performing login: {$e->errorCode}");
break;
}
die();
}
// Redirect the user to Cyclos with the returned session token
header("Location: "
. Cyclos\Configuration::getRootUrl()
. "?sessionToken="
. $result->sessionToken);
?>
Perform payment from system to user
<?php
require_once 'configureCyclos.php';
$transactionService = new Cyclos\TransactionService();
$paymentService = new Cyclos\PaymentService();
try {
$data = $transactionService->getPaymentData('SYSTEM', array('username' => 'c1'), null);
$parameters = new stdclass();
$parameters->from = $data->from;
$parameters->to = $data->to;
$parameters->type = $data->paymentTypes[0];
$parameters->amount = 5;
$parameters->description = "Test from system to user";
$paymentResult = $paymentService->perform($parameters);
if (isset($paymentResult->authorizationStatus) && $paymentResult->authorizationStatus == 'PENDING_AUTHORIZATION') {
echo("Not yet authorized\n");
} else {
echo("Payment done with id $paymentResult->id\n");
}
} catch (Cyclos\ServiceException $e) {
echo("Error while calling $e->service.$e->operation: $e->errorCode");
}
?>
Perform payment from user to user
<?php
require_once 'configureCyclos.php';
//Perform the payment from user c1 to c2
Cyclos\Configuration::setAuthentication("c1", "1234");
$transactionService = new Cyclos\TransactionService();
$paymentService = new Cyclos\PaymentService();
try {
$data = $transactionService->getPaymentData(
array('username' => 'c1'),
array('username' => 'c2'),
null);
$parameters = new stdclass();
$parameters->from = $data->from;
$parameters->to = $data->to;
$parameters->type = $data->paymentTypes[0];
$parameters->amount = 5;
$parameters->description = "Test payment to user";
$paymentResult = $paymentService->perform($parameters);
if (isset($paymentResult->authorizationStatus) && $paymentResult->authorizationStatus == 'PENDING_AUTHORIZATION') {
echo("Not yet authorized\n");
} else {
echo("Payment done with id $paymentResult->id\n");
}
} catch (Cyclos\ServiceException $e) {
switch ($e->errorCode) {
case "VALIDATION":
echo("Some of the parameters are invalid\n");
var_dump($e->error);
break;
case "INSUFFICIENT_BALANCE":
echo("Insufficient balance to perform the payment\n");
break;
case "MAX_AMOUNT_PER_DAY_EXCEEDED":
echo("Maximum amount exeeded today\n");
break;
default:
echo("Error with code $e->errorCode while performing the payment\n");
break;
}
}
?>
Error handling
All errors thrown by the server are translated into PHP by throwing Cyclos\ServiceException. This class has the following properties:
-
service: The service path which generated the error. For example, paymentService, accountService and so on; -
operation: The name of the operation which generated the error. Is the same name as the method invoked on the service; -
errorCode: Is the simple Java exception class name, uppercased, with the word 'Exception' removed. Check the API (as described above) to see which exceptions can be thrown by each service method. Keep in mind that many times the declared exception is a superclass, of many possible concrete exceptions. All methods declare to throw FrameworkException, but it is abstract, and is implemented by several concrete exception types, like PermissionException. In this example, the errorCode will be PERMISSION. Another example is the InsufficientBalanceException class, which has as errorCode the string INSUFFICIENT_BALANCE; -
error: Contains details about the error. Only some specific exceptions have this field. For example, if theerrorCodeisVALIDATION, and the exception variable name$e,$e→error→validationwill provide information on errors by property, custom field or general errors.
3.4. Authentication in web services
Regardless the web service interface (REST, WEB-RPC or custom), users are authenticated either as user / password (stateless), logging-in with a session (stateful) or using access clients (stateless). The way authentication data is passed from client to server depends on whether the clients are using a web service or a client API (Java or PHP).
3.4.1. User and password
In this mode, a principal (user identification method), which can be the login name, e-mail, mobile phone, custom field, account number or token value (card number), depending on the channel configuration, is sent on each request together with the password (live systems must always be over HTTPS, so should be secure). The drawbacks are that the username and password need to be stored in the client application, and changing the password on the web (if the same password type is used) will make the application stop working.
3.4.2. Login with a session
In this mode, first a request authenticated with user and password is made to POST /api/auth/session (REST) or LoginService.login() (WEB-RPC). It returns a session token. Subsequent requests should pass this session token instead in the subsequent requests, via the Session-Token HTTP header.
To end a session (logout), a request authenticated with the session token must be performed to DELETE /api/auth/session, or to LoginService.logout() (WEB-RPC).
3.4.3. Access clients
Access clients can be configured to prevent the login name and password to be passed on every request by clients, decoupling them from the actual password, which can then be changed without affecting the client access. It also improves security, as each client application has its own authorization token, which can be individually blocked or revoked.
To configure access clients, first a new identification method of this type must be created by administrators in System > System configuration > User identification methods. Then, in a member product of users which can use this kind of access, permissions over that type should be granted. Finally, the user (or an admin) should create a new access client in Cyclos main access, and get the activation code for it.
The activation code is a short (4 digits) code which uniquely identifies an access client pending activation for a given user. To use the access client, on the client application side (probably a server-side application or an interactive application), an request must be performed: POST /api/clients/activate, passing the username / password in a BASIC authentication and sending the activation code as the code query parameter.
The result will be a token which should be passed in subsequent requests using the HTTP header Access-Client-Token. The activation process should be done only once, and the token will be valid until the access client in Cyclos is blocked or disabled.
Here is an example which can be called by the command-line program curl:
curl https://<cyclos-root>[/network]/api/clients/activate?code=<4-digit code> \
-u "<username>:<password>"
The generated token will be printed on the console, and should be stored on the client application to be used on requests.
Additionally, clients can improve security if they can have some unique identifier which can be relied on, and don’t need to be stored. For example, Android devices always provide an unique device identifier. In that case, this identification string can be passed on the moment of activation, and will be stored on the server as a prefix to the generated token. The server will return only the generated token part, and this prefix should be passed on requests together with the generated token. The prefix is passed in the activation request as a query parameter prefix. So, for example:
curl https://<cyclos-root>[/network]/api/clients/activate?code=<4-digit code>&prefix=XYZW \
-u "<username>:<password>"
Imagining the server returns the fictional token ABCDEFG (the actual token is 64 characters long), the actual token that then should be passed to requests is XYZWABCDFG.
3.5. Channels
Channels can be seen as a set of configurations for an access in Cyclos. There are some built-in channels, and additional ones can be created. The built-in channels that can use used on web services are:
-
Main web: The main web application. The internal name is
main. -
Mobile: The Cyclos (or another 3rd party) mobile application. The internal name is
mobile. -
Web services: Is the default channel for clients using any web service client. The internal name is
webServices.
By default, the channel used on any web service (regardless the interface or user authentication mode) is Web services. It is possible to specify another channel, for example, with third party web applications (handled as Main web) or third party mobile applications.
In such cases, the channel internal name must be passed on each request using the HTTP header Channel.
3.6. Configuring web services
For clients to invoke web services in Cyclos, the following configuration needs to be done on the server (as global or network administrator):
-
On
System > System configuration > Configurationsmenu, select the configuration used by users to go to the configuration details page; -
On the
Channelstab, click on theWeb serviceschannel row, to go to the channel configuration details page; -
Make sure the channel is enabled. It can be allowed or disallowed by default. Click the edit icon on the right if the channel is not defined on this configuration. Then mark the channel as enabled, choose the way users will be able to access this channel (by default or manually) and the password type used to access the web services channel. You can also set a confirmation password, so sensitive operations, like performing a payment, will require that additional password.
-
For specific users, in the user profile page (as administrator), under the
User managementbox, click theChannels accesslink; -
On that page, make sure the
Web serviceschannel is enabled for that user. Also, only active users may access any channel - on the profile page, on the sameUser managementbox, there should be a link with actions likeEnable / Block / Disable / Remove. On that page, make sure the user status is Active; -
A side note: If performing payments via Web services, make sure the desired
Transfer typeis enabled for theWeb serviceschannel. To check that, go toSystem > Account configuration > Account typesmenu item. Then click the row of the desired account type, select theTransfer typestab and click on the desired payment type (generated types cannot be used for direct payment). There, make sure theChannelsfield has theWeb serviceschannel.
3.7. External login
With the right configuration, it is possible to add a Cyclos login form to an external website, such as the company main web site.
The user types in their Cyclos username and password in that form and, after a successful login, is redirected to Cyclos, where the session will be already valid, and the user can perform the operations as usual.
After the user clicks logout, or the session expires, the user is redirected back to the external website.
The following aspects should be considered:
-
It is needed to have an administrator whose group is granted the permission
Login users via web services. This is needed because the website will relay logins from users their clients to Cyclos, authenticated as that administrator; -
The website needs to have that administrator’s username and password configured in order to make the web services call. Even better, you can configure and access client which will allow using a separated key instead of the username / password.
-
It is a good practice to create a separated configuration for that administrator. That configuration should have an IP address whitelist for the
Web serviceschannel. Doing that, no other server, even if the adminitrator username / password is known by someone else, will be able to perform such operations. -
The Cyclos configuration for users needs the following settings:
-
Redirect login to URL: This is the URL of the external website which contains the login form. This is used to redirect the user when his session expires and a new login is needed, or when the user navigates directly to some URL in Cyclos (as guest). In that case the external web site receives a parameter namedreturnTothat must be sent back to Cyclos without any modification after a sucessful login; -
URL to redirect after logout: This is the URL where the user will be redirected after clickingLogoutin Cyclos. It might be the same URL as the one for redirect login, but not necessarily.
-
-
Finally, the web service code needs to be created, and deployed to the website. Here is an example, which receives the username and password parameters, calls the web service to create a session for the user (passing his remote address), redirecting the user to Cyclos.
<?php
// Configure Cyclos and obtain an instance of LoginService
require_once 'configureCyclos.php';
$loginService = new Cyclos\LoginService();
// Set the parameters
$params = new stdclass();
$params->user = array("principal" => $_POST['username']);
$params->password = $_POST['password'];
$params->remoteAddress = $_SERVER['REMOTE_ADDR'];
// Perform the login
try {
$result = $loginService->loginUser($params);
} catch (Cyclos\ConnectionException $e) {
echo("Cyclos server couldn't be contacted");
die();
} catch (Cyclos\ServiceException $e) {
switch ($e->errorCode) {
case 'VALIDATION':
echo("Missing username / password");
break;
case 'LOGIN':
echo("Invalid username / password");
break;
case 'REMOTE_ADDRESS_BLOCKED':
echo("Your access is blocked by exceeding invalid login attempts");
break;
default:
echo("Error while performing login: {$e->errorCode}");
break;
}
die();
}
// Redirect the user to Cyclos with the returned session token
header("Location: "
. Cyclos\Configuration::getRootUrl()
. "?sessionToken="
. $result->sessionToken);
?>
Cyclos plugin for WordPress
Cyclos provides a plugin for the well-known WordPress CMS system. For details, refer to https://wordpress.org/plugins/cyclos/.
Important notes
-
In case there is a wrong configuration for the
Redirect login to URLsetting, it won’t be possible anymore to login to Cyclos. In that case, if the configuration problem is within a network, it is possible to use a global administrator to login in global mode (using the<cyclos-root>/globalURL), then switch to the network and fix the configuration. If the configuration error is in global mode, you can use a special URL to prevent redirect:<server-root>/global/login!noRedirect=true. However, this flag only works in global mode, to prevent end-users from using it to bypass the redirect. -
Users should never have username / password requested in a plain HTTP connection. Always use a secure (HTTPS) connection. Also, just having an iframe with the form on a secure page, where the iframe itself is displayed in a plain page would encrypt the traffic, but browsers won’t show the page as secure. Users won’t notice that page as secure, could refuse to provide credentials in such situation.
Creating an alternate frontend to Cyclos
It is possible to not only place a login form in an external website, but to create an entire fronted for users to interact with Cyclos. At first glimpse, this can be great, but consider the following:
-
It is a very big effort to create a frontend, as there are several Cyclos services involved, and it might not be clear without a deep analysis on the API which service / operation / parameters should be used on each case;
-
You will always have a limited subset of the functionality Cyclos offers. You may think that only the very basic features are needed, there will inevitably be the need for more features, and the custom frontend will need to grow. By using Cyclos standard web, all this comes automatically.
Instead, a better approach could be to extend the new frontend (cyclos4-ui), which provides a modern interface and can be easily customized.
Neverthless, some (large) organizations might find it is better to provide their users with a single, integrated interface. Also, some organizations develop all their services with mobile applications.