Developers

Coding style
A list of coding style rules to be used when developing in vtenext.
File encoding:
UTF-8. All files should be encoded in UTF-8
Newlines:
Unix style, so \n, not \r\n or \r
Indentation size:
With TABS, set to 4 character. Don't mix spaces and tabs:
 
 
Indentation style:
C Style, so:
Opening brace ( 
{ ) on the same line as its control clause (an exception can be made if the line is very long, so in this case the brace can go to a new line, for better readability)
Closing brace ( 
} ) aligned with the opening statement
Single space before the opening brace
Single space before the parenthesis of a control structure (
if, 
for, 
foreach, ...)
Examples
Good
 
if ($var === '234') {
    $out = 'ok';
} elseif ($var === '567') {
    $out = 'also ok';
} else {
    $out = 'maybe ok?';
}
Bad: no space between 
foreach and 
(
 
foreach($array as $var) {
    // do something
}
 
Bad: opening brace on new line
 
if ($var == 8)
{
    // do something
}
Please note that these style rules are not strictly observed throughout the existing code, so you may find exceptions. Try to follow them, to improve the code homogeneity, but don't be too rigid in following them when evaluating existing code.
 
Line length:
We don't have a strict maximum line length. Usually up to 200 characters is ok, more than that can be hard to read on smaller screens, so better to split the line, but this is left to the developer's choice.
Also, splitting too much, for example a 300 chars line divided in 30 lines of 10 chars is no more readable than the original line. Balance readability and conciseness of code.
PHP tags:
Use standard opening tag: 
<?php , not the short one: 
<?
Avoid the closing tag ( 
?> ) if there's no output.
Examples
Good: standard opening tag, no closing tag
 
<?php
$var = 'value';
// php code
 
// no closing tag here
Bad: short opening tag, closing tag
 
<? // short tag
 
$var = 'value';
// php code
 
// unnecessary
// closing tag
?>
 
Good: standard opening tag, closing tag with output
 
<?php
$var = 'value';
// php code
// output after php code
?>
<html>
<!-- html code -->
</html>
Auto formatting:
Core files: Do not enable any "auto formatting on save" functionality in your IDE/editor, to avoid too many changes in existing files, which will be hard to diff. 
Custom files: you can reformat existing files to make them compliant with our standards.
License:
When creating a new file (php, js, tpl, html, scss) to be included in the standard vtenext, use the following license at the top of the file:
 
<?php
/*************************************
* SPDX-FileCopyrightText: 2009-present Vtenext S.r.l. Società Benefit
* SPDX-License-Identifier: LicenseRef-vtenext-business-license
************************************/

Weak comparisons
Backward Incompatible Changes
View https://www.php.net/manual/en/migration80.incompatible.php
Key principles
Follow these fundamental rules when writing compatible code:
Analyze variable types: Always understand what types and values a variable can hold.
Use strict comparisons: Prefer 
=== and 
!== over 
== and 
!= to avoid implicit type coercion.
Declare types explicitly: Always use type declarations for function/method parameters and return types.
Validate with strict checks: Use strict type checks in conditional statements to prevent unexpected behavior (e.g., 
in_array with 
true as third parameter).
Example: https://www.php.net/manual/en/function.in-array.php#example-2
Use PHP constants: Always use predefined PHP constants instead of numbers (e.g., 
UPLOAD_ERR_OK instead of 
0).
Enable strict types: Add 
declare(strict_types=1); at the top of your PHP files when possible.
Strings
Use 
empty() instead of comparing with empty string.
❌ DON'T
✅ DO
if ($a == '') {
    // ...
}
if (empty($a)) {
    // ...
}
if ($a != '') {
    // ...
}
if (!empty($a)) {
    // ...
}
Checkbox values
In PHP 8, comparing numeric strings with numbers produces different results.
❌ DON'T
✅ DO
if ($a == 0) {
    // ...
} elseif ($a == 1) {
    // ...
} else {
    // ...
}
if (strval($a) === '0') {
    // ...
} elseif (strval($a) === '1') {
    // ...
} else {
    // ...
}

RequestHandler
What is RequestHandler?
RH (alias for 
RequestHandler) provides secure, centralized access to HTTP request data. It automatically sanitizes input using 
vtlib_purify() and applies type-safe filters from 
F:: enum. This replaces direct access to superglobals like 
$_REQUEST, 
$_GET, 
$_POST, etc.
Our goal with this system is to prevent vulnerabilities ex. SQL Injection, XSS, File Inclusion, ecc.
How filtering works
RequestHandler applies a two-level filtering process:
Whitelisted keys (defined in 
config/request.config.php): These keys are automatically filtered using the filter specified in the configuration during initialization. The configured filter is applied first, and if you specify an additional filter when accessing the value, that filter is applied to the already-sanitized value.
Non-whitelisted keys: All other keys pass through 
vtlib_purify() for HTML sanitization by default. If you specify an explicit filter (e.g., 
F::int, 
F::email), it is applied to the purified value.
// ex. index.php?module=Accounts&action=CustomAction&custom=123&another=TEST
// In config: 'module' => F::mod
$module = RH::r('module');               // Uses F::mod filter from config
$customParam = RH::r('custom', F::int);  // Not in config: vtlib_purify() + F::int
$anotherParam = RH::r('another');        // Not in config: only vtlib_purify()
This approach ensures:
Unknown parameters are still sanitized to prevent XSS attacks
Explicit filters always provide additional type safety
Explicitly mapping more parameters enhances input security in the application.
✅ DO
// ✅ Always access superglobals through RH
$module = RH::r('module', F::mod);
$id = RH::r('id', F::int);
// ✅ Always specify appropriate filters
$email = RH::r('email', F::email);
$age = RH::r('age', F::int);
$status = RH::r('status', F::enum(['active', 'inactive']));
// ✅ Check for null values when data is required
$userId = RH::r('user_id', F::int);
if ($userId === null) {
    throw new InvalidArgumentException('User ID is required');
}
// ✅ Use POST for operations that are sensitive or that modify the CRM state (e.g.: sending an email, saving a record, ...)
RH::ensureMethod('POST');
❌ DON'Ts
Avoid these practices: They bypass security, are redundant and can introduce vulnerabilities.
Never access superglobals directly: 
$_REQUEST, 
$_GET, 
$_POST, 
$_COOKIE, 
$_FILES, 
$_SERVER 
Don't use manual sanitization: Avoid 
vtlib_purify() calls, RH handles this centrally
Don't skip filters: Calling 
RH::r('age') without a filter may return unvalidated strings
Don't use raw methods without reason: Prefer 
RH::r(..., F::...) over 
RH::r_raw(...)
Don't ignore security: Always validate HTTP methods for sensitive operations or action that modify the CRM state
Don't assume data types: Always use appropriate filters and null checks
// ❌ Direct superglobal access
$userId = $_REQUEST['user_id'];
$email = $_POST['email'];
$page = intval($_GET['page']);
// ❌ Manual sanitization
$name = vtlib_purify($_REQUEST['name']);
$description = vtlib_purify($_POST['description']);
// ❌ No filter applied
$age = RH::r('age');  // Returns string "25" instead of int 25
if ($age > 18) {      // String comparison may not work as expected
    // ...
}
// ❌ Using raw without justification
$data = RH::r_raw('data');  // Bypasses sanitization
echo $data;                 // Potential XSS vulnerability
// ❌ No null check for required data
$recordId = RH::r('record', F::int);
$record = getRecord($recordId);  // May fail if $recordId is null
// ❌ Accepting any HTTP method for sensitive operation or action that modify the CRM state
// No method check here - vulnerable to GET-based attacks
$settings = RH::r('settings', F::json);
saveSettings($settings);
// ❌ Type assumption without validation
$ids = RH::r('ids');     // Assuming it's an array
foreach ($ids as $id) {  // Fatal error if $ids is not an array
    // ...
}
Main methods
Method
Description
RH::r($key, $filter)
Access $_REQUEST with filter (optional)
RH::g($key, $filter)
Access $_GET with filter (optional)
RH::p($key, $filter)
Access $_POST with filter (optional)
RH::c($key, $filter)
Access $_COOKIE with filter (optional)
RH::s($key, $filter)
Access $_SERVER with filter (optional)
RH::f($key)
Access uploaded files (PSR-7)
RH::r_mod()
Module name from $_REQUEST
RH::r_action()
Action name from $_REQUEST
RH::r_record()
Record ID from $_REQUEST
RH::r_has($key)
Verify key existence (sanitized)
RH::r_has_raw($key)
Verify key existence (raw)
RH::r_all()
All parameters sanitized
RH::r_all_raw()
All parameters raw (unsanitized)
RH::ensureMethod($method)
Force HTTP method (405 if different)
RH::isFromMobile()
Check if request is from mobile app
RH::isFromPortal()
Check if request is from portal
Basic filters
Null input always returns null - All filters preserve null values
Filters for primitive types (int, float, bool) - Convert invalid input to default values (0, 0.0, false)
Validation filters (email, url, ip, enum, regex, datetime) - Return null for invalid input
Sanitization filters (str, html, alpha, alphanum, mod, action, etc.) - Remove invalid characters and return cleaned string
Always check for null - Use null coalescing operator 
?? for required values
Filter
Use case
Valid input → Output
Invalid input → Output
F::int
IDs, numbers
"42" → 
42
"abc" → 
0
null → 
null
F::float
Decimal numbers
"3.14" → 
3.14
"abc" → 
0.0
null → 
null
F::str
General strings (no HTML; default max length: 1000 characters)
"Hello" → 
"Hello"
"<b>abc</b>" → 
"abc" (tags stripped)
"Very long text..." → Truncated to 1000 chars by default
null → 
null
F::str(max: 50)
Limited strings (no HTML)
"Short text" → 
"Short text"
"Very long text..." → Truncated to 50 chars
null → 
null
F::substr(max: 50)
Substring (keeps HTML, may result in invalid HTML)
"<b>text</b>" → 
"<b>text</b>"
Long string → Truncated (HTML preserved)
null → 
null
F::html
HTML strings (purified)
"<b>bold</b>" → 
"<b>bold</b>"
"<script>alert(0)</script>" → 
"" (XSS removed)
null → 
null
F::htmlurl
HTML strings (purified, allow URLs) [only from vtenext >= 26.07]
"<img src='...'>" → 
"<img src='...'>"
F::bool
Boolean flags
"1", 
"true", 
"yes" → 
true
"0", 
"false", 
"no" → 
false
null → 
null
F::email
Email addresses
"user@example.com" → 
"user@example.com"
"invalid-email" → 
null
"user@" → 
null
null → 
null
F::url
URLs
"https://example.com" → 
"https://example.com"
"invalid-url" → 
null
"htp://exa" → 
null
null → 
null
F::ip
IP addresses
"192.168.1.1" → 
"192.168.1.1"
"999.999.999.999" → 
null
"invalid-ip" → 
null
null → 
null
F::json
JSON data
'{"key":"value"}' → 
["key" => "value"]
'invalid json' → 
null
'{broken' → 
null
null → 
null
Specific filters
Filter
Use case
Valid input → Output
Invalid input → Output
F::mod
Module names
"Accounts" → unchanged
"Acc<script>" → 
"Accscript" (special chars removed)
null → 
null
F::action
Action names
"Save", 
"Detail.View" → unchanged
"Act!on@" → 
"Acton" (special chars removed)
null → 
null
F::field
Field names
"firstname" → unchanged
"field-name!" → 
"fieldname" (special chars removed)
null → 
null
F::recfields
Record/field IDs
"123@-456", 
"1x2,3|4" → unchanged
"123abc!@#" → 
"123@" (invalid chars removed)
null → 
null
F::path
File paths (dots allowed; use checkFileAccess to ensure secure file access)
"modules/Accounts" → unchanged
"path@with#special$chars" → 
"pathwithspecialchars" (keeps valid chars)
null → 
null
F::subpath
Sub paths (no dots; use checkFileAccess to ensure secure file access)
"modules/Accounts" → unchanged
"subpath.with.dots" → 
"subpathwithdots" (dots removed)
null → 
null
F::alpha
Alphabetic only
"abcABC_" → unchanged
"abc123!@#" → 
"abc" (numbers/symbols removed)
null → 
null
F::alphanum
Alphanumeric only
"abcABC123_" → unchanged
"abc123!@#" → 
"abc123" (symbols removed)
null → 
null
F::enum([...])
Whitelist values
"active" (if in list) → unchanged
"invalid" (not in list) → 
null
null → 
null
F::regex('/pattern/')
Pattern matching
"ABC123" (matches) → unchanged
"invalid" (no match) → 
null
null → 
null
F::datetime('Y-m-d')
Date/time parsing
"2026-01-08" → unchanged
"invalid-date" → 
null
"2026-99-99" → 
null
null → 
null
F::sep(',', 'intval')
Split and transform
"1,2,3" → 
[1, 2, 3]
Non-string → 
null
null → 
null
F::jsfunc
Safe JS functions
"closePopup" (if whitelisted) → unchanged
"evilFunction" (not whitelisted) → 
null
null → 
null
Advanced filters
// Enum filter - restrict to specific values
$status = RH::r('status', F::enum(['active', 'inactive', 'pending']));
// Regex filter - custom pattern validation
$code = RH::r('code', F::regex('/^[A-Z]{3}\d{3}$/'));
// DateTime filter - parse date strings
$date = RH::r('date', F::datetime('Y-m-d'));
// Separator filter - split strings into arrays
$ids = RH::r('ids', F::sep(',', 'intval')); // "1,2,3" -> [1,2,3]
// Array filter - validate nested arrays
$user = RH::r('user', F::array([
    'name' => F::str(max: 100),
    'email' => F::email,
    'age' => F::int,
]));
// Custom callable filter
$customCode = RH::r('code', function($value) {
    if (!is_string($value)) return null;
    $value = strtoupper(trim($value));
    return preg_match('/^[A-Z0-9]{6}$/', $value) ? $value : null;
});
Examples
Simple form data
$name = RH::p('name', F::str(max: 100));
$email = RH::p('email', F::email);
$age = RH::p('age', F::int);
if ($name === null || $email === null) {
    throw new InvalidArgumentException('Name and email are required');
}
List page with pagination
$page = RH::g('page', F::int) ?? 1;
$limit = RH::g('limit', F::int) ?? 20;
$sort = RH::g('sort', F::enum(['name', 'date', 'id'])) ?? 'name';
$direction = RH::g('dir', F::enum(['asc', 'desc'])) ?? 'asc';
$query = RH::g('q', F::str(max: 255));
Complex form with nested arrays
$userData = RH::p('user', F::array([
    'personal' => F::array([
        'firstname' => F::str(max: 50),
        'lastname' => F::str(max: 50),
        'email' => F::email,
        'phone' => F::str(max: 20),
    ]),
    'preferences' => F::array([
        'language' => F::enum(['en', 'it', 'es']),
        'timezone' => F::str(max: 50),
    ]),
    'addresses' => F::array([
        'shipping' => F::array([
            'street' => F::str(max: 255),
            'city' => F::str(max: 100),
            'country' => F::str(max: 2),
        ]),
    ]),
]));
File upload
$uploadedFile = RH::f('document');
if ($uploadedFile && $uploadedFile->getError() === UPLOAD_ERR_OK) {
    $filename = $uploadedFile->getClientFilename();
    $mimeType = $uploadedFile->getClientMediaType();
    $size = $uploadedFile->getSize();
    
    // ...
} else {
    // Handle upload error
    $error = $uploadedFile ? $uploadedFile->getError() : 'No file uploaded';
}
Advanced Usage
Temporary request context
Push/pop operations should be used only for testing or very specific scenarios.
// Save current state and temporarily override
RH::push_r(['module' => 'TestModule', 'action' => 'TestAction']);
// Do something with temporary data
$module = RH::r_mod(); // Returns 'TestModule'
// Restore original state
RH::pop_r();
Configuration
The RequestHandler uses a configuration file to control request sanitization behavior and define safe parameters. Configuration is defined in 
config/request.config.php and can be extended via 
config/request.config.override.php.
preserve_original_request
'preserve_original_request' => false,  // default
Controls whether all original request parameters are preserved in the 
$_REQUEST superglobal:
false (default, recommended): Only keys listed in 
safe_keys are preserved in 
$_REQUEST. This provides maximum security by removing any unexpected parameters that could be malicious.
true: All original parameters remain in 
$_REQUEST. Use only if you need backward compatibility with legacy code that accesses parameters not in the whitelist.
Security note: When 
preserve_original_request is 
false, the 
$_REQUEST array is cleaned to contain only whitelisted keys. This prevents injection of unexpected parameters but does NOT bypass RH's access control - you should still use RH methods for all access.
safe_keys
'safe_keys' => [
    'module' => F::mod,
    'action' => F::action,
    'record' => F::int,
    'id' => F::int,
    'page' => F::int,
    'limit' => F::int,
    // ... add commonly used keys here
],
Defines a whitelist of parameter keys that are:
Preserved during request cleanup: When 
preserve_original_request is 
false, only these keys remain in 
$_REQUEST
Pre-sanitized automatically: Each key is associated with a filter (e.g., 
F::mod, 
F::int) that is applied during initialization
safe_js_functions
'safe_js_functions' => [
    'closePopup',
    'LPOP.create',
    'parent.ActionTaskScript.addStaticRelatedRecord',
    // ... other trusted JavaScript function names
],
Lists JavaScript function names that are allowed to be passed as request parameters (e.g., for callbacks). This prevents XSS attacks by restricting which functions can be executed client-side.
Used by the 
F::jsfunc filter
Only listed functions are considered safe
Reject any function not in the whitelist
Creating custom configuration
To add your own safe keys without modifying the core configuration, create 
config/request.config.override.php:
// config/request.config.override.php
return [
    'safe_keys' => [
        // Your custom keys
        'custom_param' => F::str(max: 100),
        'my_record_id' => F::int,
    ],
    'safe_js_functions' => [
        // Your custom callback functions
        'MyApp.customCallback',
    ],
];
The override configuration is automatically merged with the default configuration.
Migration to version 26.04
During the upgrade to version 26.04, the system automatically scans for direct 
$_REQUEST superglobal usage. Any detected parameters are automatically added to 
config/request.config.override.php with the 
F::html filter to maintain backward compatibility and prevent breaking existing customizations. 
All core code has been refactored to use RH methods exclusively instead of direct superglobal access.

Routing system
Previously, 
index.php directly included the module file to process the action, based on the 
action and 
file request parameters.
With the new architecture, the resolution logic has been moved to the 
IndexRouter class, which handles action path resolution and processing.
Action resolution
Searched paths (in order)
When an action is requested, 
IndexRouter searches for the file in the following paths:
modules/{MODULE}/{ACTION}.php
modules/{MODULE}/{ACTION}/{ACTION}.php
modules/{MODULE}/controllers/{ACTION}.php (NEW)
modules/VteCore/{ACTION}.php
modules/VteCore/controllers/{ACTION}.php (NEW)
Removal of 
{MODULE}Ajax.php
It is no longer necessary to create a 
{MODULE}Ajax.php file in the module folder. AJAX requests can be handled directly by BaseAction classes, which automatically detect the request type and desired output format.
Execution process
After resolving and including the action file:
IndexRouter checks if the file contains a class that extends 
BaseAction
If found, it calls the 
fromRequest() method to create an instance of the class
During 
fromRequest(), class properties are automatically populated from request parameters
processFromIndex() is called, which performs validation and processing
The result is formatted according to the requested content-type and sent to the client
If the file included in 
index.php does not contain a 
BaseAction class, then the included code is executed and entirely managed by the file (previous behavior).
Creating a new BaseAction
Basic structure
To create a new action, create a file in:
modules/{MODULE}/controllers/{ACTION}.php
or 
modules/VteCore/controllers/{ACTION}.php (for global actions)
The class must:
Extend 
BaseAction
Implement the 
process() method
Declare properties with appropriate attributes
<?php
class MyAction extends BaseAction {
    
    protected function process() {
        // Your logic here
        return ['success' => true, 'data' => []];
    }
}
Declaring properties
Class properties can be automatically populated from the request using PHP 8 attributes:
Attribute
Source
Description
RequestParam
$_REQUEST
Parameter from request (GET or POST)
RequestRawParam
$_REQUEST
Raw parameter from request
GetParam
$_GET
Parameter from GET
GetRawParam
$_GET
Raw parameter from GET
PostParam
$_POST
Parameter from POST
PostRawParam
$_POST
Raw parameter from POST
CookieParam
$_COOKIE
Parameter from cookie
Attribute parameters
name: Parameter name in the request (default: property name)
filter: Filter to apply (e.g. 
F::int, 
F::str, 
F::bool). For more details, see Basic filters.
required: Whether the parameter is required (default: based on nullable type)
default: Default value if the parameter is not present
validation: Regex or callable to validate the value
description: Parameter description (for documentation)
<?php
class MyAction extends BaseAction {
    
    // Required parameter (not nullable)
    #[RequestParam()]
    protected int $record;
    
    // Optional parameter (nullable)
    #[RequestParam()]
    protected ?string $searchText;
    
    // Parameter with different name
    #[RequestParam(name: 'return_module', filter: F::mod)]
    protected ?string $returnModule;
    
    // Parameter with regex validation
    #[RequestParam(validation: '/^(true|false)$/')]
    protected ?string $isDuplicate;
    
    // Parameter with callable validation
    #[RequestParam(validation: 'is_numeric')]
    protected ?string $amount;
    
    // Parameter with default value
    #[RequestParam(default: 10)]
    protected int $limit;
    
    // Parameter from POST
    #[PostParam()]
    protected ?string $comment;
    
    // Parameter from GET
    #[GetParam()]
    protected ?int $page;
    
    protected function process() {
        // Properties are already populated here
        return [
            'record' => $this->record,
            'search' => $this->searchText,
        ];
    }
}
Main methods
process()
Main method that contains the action logic. Must return data that will be formatted and sent to the client.
protected abstract function process();
validate(?string &$error): bool
Called before 
process() to validate the request. If it returns 
false, execution stops and an error is sent.
protected function validate(?string &$error): bool {
    if ($this->record <= 0) {
        $error = "Invalid record ID";
        return false;
    }
    return true;
}
beforeProcess()
Called before 
process(), after validation.
protected function beforeProcess(): void {
    // Initialization, logging, etc.
}
afterProcess(&$result)
Called after 
process(), allows modifying the result before sending.
protected function afterProcess(&$result): void {
    // Modify result, logging, etc.
    $result['timestamp'] = time();
}
Output
The output format is automatically determined based on:
The class's 
$outputFormat property (if set)
The 
format or 
_format parameter in the request
The 
Accept header of the request
Whether the request is AJAX (default: 
json) or not (default: 
html)
Supported formats
json - JSON response (automatic for AJAX)
html - HTML response (automatic for standard requests)
You can force a specific format by setting the 
$outputFormat property:
class MyAction extends BaseAction {
    
    protected ?string $outputFormat = 'json';
    
    protected function process() {
        return ['data' => 'Always JSON'];
    }
}
Or by using the 
setOutputFormat() method:
class MyAction extends BaseAction {
    
    protected function beforeProcess(): void {
        $this->setOutputFormat('html');
    }
    
    protected function process() {
        return '<h1>HTML Output</h1>';
    }
}
JSON response format
On success:
{
    "success": true,
    "result": { /* data returned by process() */ }
}
On error:
{
    "success": false,
    "error": {
        "message": "Error message",
        "code": 400
    }
}
Requesting JSON from client
To request a JSON response, the client can:
Add the parameter 
?format=json or 
?_format=json to the URL
Set the header 
Accept: application/json
Make an AJAX request (automatically detected)
Example
<?php
use RequestParam;
use PostParam;
class SaveData extends BaseAction {
    
    // We don't specify outputFormat, it will be determined automatically
    
    #[RequestParam()]
    protected ?int $record;
    
    #[PostParam(required: true)]
    protected string $name;
    
    #[PostParam(filter: F::int, default: 1)]
    protected int $status;
    
    #[PostParam()]
    protected ?string $description;
    
    protected function validate(?string &$error): bool {
        if (strlen($this->name) < 3) {
            $error = "Name must be at least 3 characters long";
            return false;
        }
        return true;
    }
    
    protected function beforeProcess(): void {
        // Log the operation
        global $log;
        $log->info("Saving record: " . $this->name);
    }
    
    protected function process() {
        global $adb;
        
        // Save logic
        if ($this->record) {
            // Update logic
        } else {
            // Insert logic
        }
        
        // Return data
        // If the request is AJAX, it will be automatically formatted as JSON
        // Otherwise as HTML
        return [
            'record_id' => $id,
            'message' => 'Save completed'
        ];
    }
    
    protected function afterProcess(&$result): void {
        // Add timestamp to result
        $result['timestamp'] = time();
    }
}
Differences from the previous version
Aspect
Before
Now
Action resolution
In index.php
In IndexRouter.php
Ajax files
{MODULE}Ajax.php required
No longer necessary
Controllers paths
Did not exist
modules/{MODULE}/controllers/
modules/VteCore/controllers/
Parameter population
Manual with $_REQUEST
Automatic with PHP attributes
Validation
Manual
validate() method + attribute validation
Output format
Manually handled
Automatic
Error handling
Manual
Automatic with try/catch in processFromIndex()
Best Practices
Create new actions as BaseAction classes in 
modules/{MODULE}/controllers/
Use attributes to declare parameters explicitly
Always specify the appropriate filter (
F::int, 
F::str, etc.)
Implement the 
validate() method for business logic validations
Use attribute validation for simple validations (regex, callable)
Let the system automatically determine the output format (JSON/HTML)
Force 
$outputFormat only when necessary
Use 
beforeProcess() for initialization and 
afterProcess() for post-processing

Database best practices
Best practices (all versions)
Avoid methods that do automatic html conversion, like 
query_result and 
fetchByAssoc(...)
Never concatenate variables (especially coming from request) into the sql string
Always use the no_html versions (
query_result_no_html or 
fetchByAssoc(..., -1, false)) or the new shortcut methods (see below)
Always use prepared statement (
pquery), to avoid SQL injections
In 
SELECT statements, select only the necessary columns, avoid 
* if you don't need all the columns
In 
INSERT statements, always specify the column names (to avoid breaking the query in case of new columns added later)
Shortcut methods (version ≥ 26.01)
New utility methods have been added to easy query execution and retrieval of rows. None of these functions will do any html conversion of the result.
rows(): Iterate over rows:
$res = $adb->pquery("SELECT * FROM table WHERE column = ?", [$param]);
foreach ($res->rows() as $row) {
  // ... use $row
}
This is equivalent of:
$res = $adb->pquery("SELECT * FROM table WHERE column = ?", [$param]);
while ($row = $adb->fetchByAssoc($res, -1, false)) {
  // ... use $row
}
row($index = 0): Get a single row:
$res = $adb->pquery("SELECT * FROM table WHERE column = ?", [$param]);
$row1 = $res->row(1); // get the second row (index starts at 0)
allRows(): Read all rows (note: may use a lot of memory if there are many rows):
$res = $adb->pquery("SELECT * FROM table WHERE column = ?", [$param]);
$allrows = $res->allRows();
 
col($column): Return a specific column (by index or by name) from the result set (note: may use a lot of memory if there are many rows):
$res = $adb->pquery("SELECT * FROM table WHERE column = ?", [$param]);
$col = $res->col();     // first column
$col = $res->col(2);    // 3rd column (0-based index)
$col = $res->col('id'); // "id" column
 
allCols(): Return all the columns from the result set (note: may use a lot of memory if there are many rows). The return value is an array, where each key is the name of the column and the value, an array of the values for that column:
$res = $adb->pquery("SELECT id, name FROM table WHERE column = ?", [$param]);
$all = $res->allCols();
/* 
$all looks like:
Array (
    [id] => Array
        (
            [0] => 1
            [1] => 6
        )
    [name] => Array
        (
            [0] => first value
            [1] => something else
        )
)
*/
 
queryGetAll($sql, $params = [], $offset = 0, $limit = -1): Combines 
pquery and 
allRows:
// get all rows from query (no parameters)
$rows = $adb->queryGetAll("SELECT column1, column2 FROM table");
// get all rows from query (single parameter)
$rows = $adb->queryGetAll("SELECT * FROM table WHERE column = ?", $param);
// with limit, select first 5 rows
$rows = $adb->queryGetAll("SELECT * FROM table WHERE column = ? AND id < ?", [$param, 6], 0, 5);
queryGetFirst($sql, $params = []): Get the first row returned by the query:
// no need to specify LIMIT
$row = $adb->queryGetFirst("SELECT column1, column2 FROM table");
queryGetFirstValue($sql, $params = [], $column = 0): Get a single value from the first row returned:
// first column, if nothing specified
$column1 = $adb->queryGetFirstValue("SELECT column1, column2 FROM table");
// by name
$column2 = $adb->queryGetFirstValue("SELECT column1, column2 FROM table", [], 'column2');
queryHasRows($sql, $params = []): Return true if the query returns at least one row:
$hasRows = $adb->queryHasRows("SELECT column1 FROM table WHERE id > ?", [10]);
queryGetAllColumns($sql, $params = [], $offset = 0, $limit = -1): Combines 
pquery and 
allCols:
// get all columns from query (no parameters)
$cols = $adb->queryGetAllColumns("SELECT column1, column2 FROM table");
// get all columns from query (single parameter)
$cols = $adb->queryGetAllColumns("SELECT * FROM table WHERE column = ?", $param);
// with limit, select first 5 rows and return them as columns
$cols = $adb->queryGetAllColumns("SELECT * FROM table WHERE column = ? AND id < ?", [$param, 6], 0, 5);
queryGetFirstColumn($sql, $params = []): Get the first column returned by the query:
// no need to specify LIMIT
$col1 = $adb->queryGetFirstColumn("SELECT column1, column2 FROM table");
In version 26.01, there is still a main source of queries transforming data to html: reading records. So all queries reading records to be displayed in ListView, DetailView, EditView, Reports... are still converting to html. 
This behaviour was kept to avoid too many breaks with existing custom code (uitypes, views, presave, ...)
Aliased methods (vtenext ≥ 26.01)
Some methods in PearDatabase have now new aliases, to better remember that a html conversion can take place:
query_result_html -> alias of 
query_result
fetch_array_html -> alias of 
fetch_array
fetchByAssocHtml -> alias of 
fetchByAssoc, with 3rd parameter set to true
fetchByAssocNoHtml -> alias of 
fetchByAssoc, with 3rd parameter set to false
It is recommended not to use the html methods, but if really necessary, try to use the "html" aliases.
New methods (version ≥ 26.04)
pqueryTimeout(string $sql, ?array $params, int $timeout, bool $dieOnError=false): Executes a SELECT statement with a timeout in ms (only for MySql connections). Returns the string 
TIMEOUT if a timeout occured.
$res = $adb->pqueryTimeout("SELECT * FROM table ORDER BY date_field", null, 3000);
if ($res === 'TIMEOUT') {
  // took longer than 3s
  // alternative code to execute the query in background
  return;
else {
  // use $res
}

Escaping
How to properly escape html/js code in Smarty templates and PHP files

Introduction
Starting with vtenext version 26.01, all variables outputted by Smarty templates (for example 
{$VARIABLE} ) by default have all applicable characters are converted to html 
&...; notation. For instance, the string 
"nice & 'smooth' à > è" is converted to 
"nice &amp; &039;smooth&039; &agrave; &gt; &egrave;"
The conversion is done with the 
htmlentities function, without double encoding existing entities.
The reason of this change is to reduce as much as possible exposure to XSS attacks, since it was extremely difficult to track every possible variable in templates and ensure it was properly escaped.
There are of course some exceptions on the escaping and some edge cases that should be understood. The next chapter presents the rules to follow to write simple and secure code.

Escaping rules
1. Avoid html code in php
Avoid generating html code directly in php, or outputting it directly. If possible use Smarty templates. Exceptions can be made for very short snippets, see paragraph 3 for how to handle them.
Examples
Good
 
 
$name = "John & friends";
$smarty->assign("NAME", $name);
And in smarty:
 
{* & will be converted to &amp; *}
<div>{$NAME}</div>
Will output: 
 
<div>John &amp; friends</div>
Bad: html generated in php
 
 
$name = "John & friends";
// the "div" will be escaped
$string = "<div>{$name}</div>";
$smarty->assign("TEXT", $string);
 And in smarty:
 
{$TEXT}
Will output: 
 
&lt;div&gt;John &amp; friends&lt;/div&gt;
 
Bad: output via echo
 
$name = "John & friends";
$string = "<div>{$name}</div>";
// by using echo on the raw string,
// we introduce a XSS if $name comes from user input
echo $string;
 
2. Reading from database
Avoid using 
query_result and 
fetchByAssoc without the 
-1, false 2nd and 3rd parameters.
This is because the those functions already do html conversion, which is the wrong place to do it, since the result may have to be processed more before being displayed by the browser (or not displayed at all in a browser if the result is for a REST API).
Examples
Good
 
 
$res = $adb->query("SELECT helpinfo FROM vte_field");
$first = $adb->query_result_no_html($res, 0, 'helpinfo');
Bad: helpinfo is converted!
 
 
$res = $adb->query("SELECT helpinfo FROM vte_field");
$first = $adb->query_result($res, 0, 'helpinfo');
Good
 
 
$res = $adb->query("SELECT helpinfo FROM vte_field");
while ($row = $adb->fetchByAssoc($res, -1, false)) {
  // ...
}
Bad: helpinfo is converted!
 
 
$res = $adb->query("SELECT helpinfo FROM vte_field");
while ($row = $adb->fetchByAssoc($res)) {
  // ...
}
3. Generating html in php
Sometimes it's really inevitable to generate html code in php (legacy code, or existing developments...), so in this case, to avoid a double conversion when Smarty processes the template, we should use the 
\Vtenext\Types\HtmlString class to wrap the html string. This class is a simple wrapper around a string, that doesn't get automatically converted by Smarty. Remember, that in this case, all the html conversion must be done in php using the 
VStr::toHtml or 
VStr::toHtmlAttr methods.
Example:
// for brevity:
use \Vtenext\Types\HtmlString;
$name = "John Connor";
$url = "index.php?module=aaa&action=".urlencode($_REQUEST['param']); // danger here: user input!
// building the link with manual conversion
$link = "<a href='".VStr::toHtmlAttr($url)."'>".VStr::toHtml($name)."</a>";
// wrapping it in HtmlString
$link = new HtmlString($link);
$smarty->assign("LINK", $link); // this will NOT be escaped!! Be sure all parameters are properly escaped!!
There are also 2 utility methods to build html string safely, 
HtmlString::build and 
HtmlString::buildSmarty. Let's see some examples:
// with build(), every instance of ### is replaced with one of the parameters,
// and escaped for html. The resulting string is safe for raw inclusion in templates
$icon = HtmlString::build('<i class="vteicon md-sm" title="###" style="color: white;">settings</i>', [trans('LBL_AREAS_SETTINGS')]);
# even when reading from $_REQUEST, we don't have XSS here
$errStr = HtmlString::build("<font class='error'>Error: ###</font>", RH::r('error_string'));
// buildSmarty takes a smarty template in string form and does the standard conversion:
$tpl = '
  <input class="crmbutton" onclick="myFunction(\'{$module}\', {$recordid}, \'{$entityName}\')" type="button"
  value="{"LBL_LINK_ACTION"|trans}">';
$params = [
	'module' => $currentModule,
	'recordid' => intval($recordid),
	'entityName' => VStr::toJs($entityName, ''), // js stuff must be explicitely escaped
];
$html =  HtmlString::buildSmarty($tpl, $params);
4. Assigning to Smarty
Assign the variables normally, without using decode_html, htmlspecialchars, htmlentities or other conversion functions.
Examples
Good
 
 
$string = "my nice text with < and >";
$smarty->assign("VAR", $string);
And in smarty:
 
{* < and > will be converted *}
<div>{$VAR}</div>
Bad: no need to convert 2 times
 
 
$string = "my nice text with < and >";
$smarty->assign("VAR", htmlentities($string));
 And in smarty:
 
{* still work the same, 
since double_encoding is false
but not a good idea anyway *}
<div>{$VAR}</div>
 
5. Inside Smarty templates (html)
Output variables as they are, no additional escaping needed in html. For variables inside javascript code (ex: in onclick attributes) see the next paragraph.
6. Inside Smarty templates (javascript)
Javacript code, inside Smarty templates, delimited by <script> tags, has a special handling. Inside these blocks, the default html conversion is not done, instead, the following happens:
If the variable to replace begins with 
[ or 
{, no escaping is done
Otherwise, the variable is escaped with 
VStr::toJs (which does a addslashes)
For examples, if in PHP we have:
$arr = [1,2,3];
$obj = ['name' => 'test'];
// no need of js escaping here
$smarty->assign("STR", "test <tag> 'quote' end");
// this is the preferred way to pass complex types, as they are!
$smarty->assign("ARR", $arr);
$smarty->assign("OBJ", $obj);
// for legacy code it's acceptable to have this
$smarty->assign("JSARR", json_encode($arr));
$smarty->assign("JSOBJ", json_encode($obj));
// AVOID to build js structures with string manipulation in PHP!!
then in the template we can use:
<script>
  // both double quotes and single quotes can be used, they are both escaped!
  var str1 = '{$STR}'; // will become test <tag> \'quote\' end
  var str2 = "{$STR}";
  // the preferred way is to use json_encode for complex types
  var arr = {$ARR|json_encode};
  var obj = {$OBJ|json_encode};
  // but in case something is already in json form:
  var arr = {$JSARR}; // this WILL NOT be escaped!
  var obj = {$JSOBJ}; // also this one
  
</script>
Outside of 
<script> tags, the standard html conversion is done, so in case of javascript code in attributes (ex: onclick handlers), manual escaping is still necessary, for example:
{* using escape in "javascript" mode *}
<span onclick="myFunction('{$PARAM|escape:"javascript"}')">Link</span>
{* using out VStr::toJs method *}
<span onclick="myFunction2('{VStr::toJs($PARAM)}')">Link2 </span>
{* for urls inside js, use "url" mode before the js encoding *}
<span onclick="location.href='index.php?mode={$MODPARAM|escape:"url"|escape:"javascript"}';">Link 3</span>
7. Skipping the conversion
Sometimes it's necessary to override the default conversion and output the raw variable as is. This can be achieved in two ways:
By using 
nofilter:
<span>{$RAW_VAR nofilter}</span>
By using the modifier 
rawhtml (will convert the string to HtmlString, thus avoiding the conversion)
<span>{$RAW_VAR|rawhtml}</span>
8. Special cases: labels
When using labels in templates (ex: 
"LBL_SOMETHING"|trans or 
$APP.LBL_SOMETHING), html chars are converted. Some labels, though, contains html code, and in this case they should be used with the 
rawhtml or 
nofilter modifiers.
9. Special cases: {capture}
When using 
{capture} in templates, the content is saved in a variable. If that variable is then outputted, the content is escaped, which is usually unwanted since the content is html. In this case, the 
rawhtml or 
nofilter modifier must be used.
For example, if the capture block is:
{capture assign="content"}
<div> Hello {$FRIEND}</div> {* $FRIEND will be converted to html entities *}
{/capture}
It should be used with:
<h2>{$content|rawhtml}</h2>
or 
<h2>{$content nofilter}</h2>
or
{include file="NoLoginMsg.tpl" BODY=$content|rawhtml}

Cheat sheet
Quick reference if you just need to know how to escape stuff!
Do's and Don'ts
Use only 
query_result_no_html and 
fetchByAssoc(..., -1, false) or 
fetchByAssocNoHtml, we don't want that pesky 
to_html function to be called
Do not generate html strings in php, use templates or 
HtmlString::build
Do not echo html code in PHP, use Smarty
Do not use 
VStr::toJsAttr method
Do not try to build js code from PHP, use .js files or 
<script> tags in templates
Do not use 
html_entity_decode, 
htmlentities, 
htmlspecialchars, 
addslashes, it's probably not needed (unless you are working on legacy code)
Do not use 
to_html, 
from_html, 
decode_html , these were always a bad idea
How to's:
In
How do I handle... ?
... like this:
Smarty, html code
standard variable
{$VARIABLE}
variable, but it's a HtmlString
{$VARIABLE}
variable, but it's a string and already html
{$VARIABLE nofilter}
or
{$VARIABLE|rawhtml}
{capture} blocks
 
{capture assign="capname"}
  <div>.... html code {$VARIABLE} </div>
{capture}
{$capname nofilter}
Smarty,
inside 
<script>
string variable
var myvar = '{$VARIABLE}';
object or array variable
var mylist = {$VARIABLE|json_encode};
string inside url
var url = "index.php?module={$VARIABLE|escape:"url"}";
Smarty,
js in attributes
string variable
 
{* using escape in "javascript" mode *}
<span onclick="myFunction('{$PARAM|escape:"javascript"}')">Link</span>
{* using out VStr::toJs method *}
<span onclick="myFunction2('{VStr::toJs($PARAM)}')">Link2 </span>
string in url
<span onclick="location.href='index.php?mode={$MODPARAM|escape:"url"|escape:"javascript"};>Link 3</span>

SDK 2
This manual describes the SDK methods that allow customization of vtenext.

Include custom php/js/css files
In order to include your custom code (that will be included in every page) you need to register the new file with this call:
SDK::setUtil($src);
$src : the php file path and the file name to be included
To remove the customization:
SDK::unsetUtil($src);
$src : the php file path and the file name to be removed (the file will NOT be removed from disk)
In case of css/js file you can call:
Vtiger_Link::addLink($id, $type, 'SDKScript', $file);
$id : module id that register the customization (in this case, SDK, 31)
$type : can be “HEADERCSS” or “HEADERSCRIPT”
$file : the php file path and the file name to be included
Hooks:
include/Webservices/Utils.php
include/squirrelmail/src/redirect.php
install/PopulateSeedData.php
index.php

Javascript overrides and extensions
Some commonly used Javascript functions can be replaced or extended to change their behavior. To do this, simply create a function that has the same name as the function to be modified with the addition of "_override" or "_extension" and the same parameters. The behavior of the two extensions is as follows:
FUNCTION_override()
If present, this function is called instead of the original one. The return value of this function is the return value.
FUNCTION_extension()
If present, this function is called and if it returns false or a value equivalent to false, the original function ends by returning false, while if it returns true or an equivalent value, the execution continues in the original function.
The difference is that, in the first case the original function is completely ignored, while in the second, it is possible to decide whether to continue the standard execution or not. This is very convenient in the case of validation functions, usually very long, in which you simply want to add a control, without copying the entire function for small changes.
The functions that support these extensions are as follows:
File
Functions
include/js/general.js
doformValidation
startCall
getFormValidate
include/js/Inventory.js
settotalnoofrows
deleteRow
calcTotal
calcProductTotal
calcGrandTotal
validateInventory
FindDuplicate
validateNewTaxType
validateTaxes
setDiscount
callTaxCalc
calcCurrentTax
calcGroupTax
calcSHTax
validateProductDiscounts
updatePrices
updatePriceValues
resetSHandAdjValues
moveUpDown
InventorySelectAll
fnAddProductOrServiceRowNew

Standard PHP replacement
You can replace the standard php files of the modules, such as DetailView.php, EditView.php and so on through the method:
SDK::setFile($module, $file, $newfile);
$module : the name of the module
$file : the value of the "action" parameter to be compared
$newfile: the new php source, without extension and without path
The $newfile must be in the same folder of the module and must be specified without an extension and without a path.
If you want to replace the ListView, you must call setFile twice, once with $file = "ListView" and once with $file = "index".
To remove the customization:
SDK::unsetFile($module, $file);
$module : the name of the module
$file : the value of the "action"
Hooks:
include/Ajax/CommonAjax.php
index.php

Inclusion of other files
To associate files or folders to a module, so that they are imported automatically, the following methods are available:
SDK::setExtraSrc($module, $src);
$module : the name of the module
$src : the path of the file or folder to be associated
To delete the association (but not the files themselves) use:
SDK::unsetExtraSrc($module, $src);
$module : the name of the module
$src : the path of the file or folder associated

Custom Uitypes
You can add new types to the existing ones and manage them completely without changing other code. The procedure for creating a new one is:
Create a new custom field with the new type (nnn)
Create the files:
a. nnn.php in modules/SDK/examples
b. nnn.js in modules/SDK/examples
c. nnn.tpl in Smarty/templates/modules/SDK/examples
These files manage the behavior of the new field depending on the context (list, detail, and so on)
Register the new type with the class method SDK::setUitype.
In modules/SDK/examples/VTE-SDK-2.php there are various examples of field creation and uitype registration.
In modules/SDK/doc/VTE-SDK-2.pdf under Uitypes List you can find the list of the main standard uitypes.
SDK::setUitype($uitype, $src_php, $src_tpl, $src_js, $type='', $params='');
$uitype : the number of the new type; it must be nnn (the name of the files)
$src_php: the path of nnn.php
$src_tpl: the path of nnn.tpl (without Smarty/templates/ at the beginning)
$src_js : the path of nnn.js
$type : the type in webservice format (‘text’, ‘boolean’, and so on)
$params : not used yet
To remove the uitype:
SDK::unsetUitype($uitype);
$uitype : it is the number of the uitype to remove (the files associated with it will not be deleted)
We recommend using uitype with a value greater than 2000, to avoid conflicts with the future releases of vtenext.
The php script has several variables available, the first one is:
$sdk_mode : the views that can be customized for the new uitype (“insert”, “detail”, “edit”, “relatedlist”, “list”, “pdfmaker”, “report”, and so on)
Depending on the type of $sdk_mode, various variables can be read and modified.
detail
to manage the display of the field in DetailView
INPUT
$module : the current module name
$fieldlabel: the label of the field
$fieldname : the name of the field
$col_fields: (array) the values of the fields
OUTPUT
$label_fld[] : the label translated
$label_fld[] : the value to be displayed
edit
to manage the display of the field in EditView
INPUT
$module_name : the current module name
$fieldlabel : the label of the field
$value : the value of the field
OUTPUT
$editview_label[] : the label translated
$fieldvalue[] : the value to be displayed
relatedlist, list, pdfmaker
to manage the display of the field in ListView, RelatedList and PDFMaker
INPUT
$sdk_value : the value of the field
OUTPUT
$value : the value to be displayed
report
to manage the display of the field in Report
INPUT
$sdk_value : the value of the field
OUTPUT
$fieldvalue : the value to be displayed
If the value to be displayed from the interface is formatted differently than the value saved in the database (e.g. number 1.000,25 which must be saved as 1000.25) then the following methods must also be managed to save the value in the correct format and search for it.
insert
to convert the value to the format to be saved in the database
INPUT
$this->column_fields : (array) the values of the fields
$fieldname : the name of the field
OUTPUT
$fldvalue : the value to save in the database
formatvalue
to convert the value coming from the $_REQUEST into the format saved in the database (used in the new management of Conditional Fields)
INPUT
$value : the value of the field
OUTPUT
$value : the value converted to database format
querygeneratorsearch
to convert the value searched in the lists and filters by the user
INPUT AND OUTPUT
$fieldname : the name of the field
$operator : the comparison operator
$value : the value searched
customviewsearch
to manage the conversion of the value in the popup filters for the reference fields
INPUT AND OUTPUT
$tablename : the table of the field
$fieldname : the name of the field
$comparator : the comparison operator
$value : the value searched
popupbasicsearch
to manage the conversion of the value in the popup search for the reference fields
INPUT
$table_name : the table name
$column_name : the column name
$search_string : the value searched
OUTPUT
$where : the condition of the query
e.g. $where = "$table_name.$column_name = '".convertToDBFunction($search_string)."'";
popupadvancedsearch
to manage the conversion of the value in the advanced popup search for the reference fields
INPUT AND OUTPUT
$tab_col : the table and the column of the field
$srch_cond : the comparison operator
$srch_val : the value searched
reportsearch
to convert the value searched in the reports by the user
INPUT AND OUTPUT
$table : the table of the field
$column : the column of the field
$fieldname : the field name
$comparator : the comparison operator
$value : the value searched
Hooks
data/CRMEntity.php
include/ListView/ListViewController.php
include/utils/crmv_utils.php
include/utils/EditViewUtils.php
include/utils/DetailViewUtils.php
include/utils/ListViewUtils.php
include/utils/SearchUtils.php
include/QueryGenerator/QueryGenerator.php
modules/PDFMaker/InventoryPDF.php
modules/Reports/ReportRun.php
modules/Users/Users.php
modules/CustomView/Save.php
modules/CustomView/CustomView.php
Smarty/templates/DisplayFieldsReadonly.tpl
Smarty/templates/DisplayFieldsHidden.tpl
Smarty/templates/DetailViewFields.tpl
Smarty/templates/EditViewUI.tpl
Smarty/templates/DetailViewUI.tpl

Smarty Custom Templates
You can create your own templates, which replace the standard ones (such as EditView.tpl and so on). The new template is used if $_REQUEST values of the page meet the requirements. The registration of a new template is done through the method:
SDK::setSmartyTemplate($params, $src);
$params : associative array with requirements (see below)
$src : path of the new template
In the $params variable you can specify a special value ("$NOTNULL$") to indicate that this parameter must exist, with any value. The unspecified parameters are ignored. If the rule to be inserted already exists or is not compatible with the existing ones (it could cause ambiguity for some $_REQUEST), the insertion fails (and an explanatory message is saved in the log).
To remove the customization:
SDK::unsetSmartyTemplate($params, $src = NULL);
$params : associative array with requirements
$src : path of the template (if NULL, it includes all file)
To completely replace all types of views of a module you need at least 7 rules:
$params
Notes
array(‘module’=>’Leads’, ‘action’=>’ListView’)
ListView
array(‘module’=>’Leads’, ‘action’=>’index’)
ListView
array(‘module’=>’Leads’, ‘action’=>’DetailView’ , ‘record’=>’$NOTNULL$’)
DetailView
array(‘module’=>’Leads’, ‘action’=>’EditView’ , ‘record’=>’$NOTNULL$’)
EditView (with record in $_REQUEST)
array(‘module’=>’Leads’, ‘action’=>’EditView’)
New record
array(‘module’=>’Leads’, ‘action’=>’EditView’, ‘record’=>’$NOTNULL$’, "isDuplicate"=>"true")
Duplicate record
array(‘module’=>’Leads’, ‘action’=>’LeadsAjax’, ‘record’=>’$NOTNULL$’, 'ajxaction'=>'LOADRELATEDLIST', 'header'=>'Products')
The related list of leads showing the products
If multiple rules match, the most specific will be used. For example, if there are 2 rules, one with "$NOTNULL$" and one with the value "Leads" and the request is "Leads", the second rule will be used.
Hooks
Smarty_setup.php

Popup
Two actions are available for managing popup. You can insert a php script before the query is made to load the data. In addition, it is possible to insert another php script before the data is shown, so you can edit this data or the result when the popup is closed. In the first case, the are two methods available:
SDK::setPopupQuery($type, $module, $param, $src, $hidden_rel_fields = '');
$type : “field” or “related” to indicate a standard field or popup opened from a related list
$module: the module in which the popup opens
$param : the name of the field that opens the popup (must be uitype 10) in the case type = "field", otherwise the name of the connected module
$src : the path of php file
$hidden_rel_fields : associative array of fields to pass in the popup request like array($urlvalue => $jscode)
To remove the customization:
SDK::unsetPopupQuery($type, $module, $param, $src);
Same parameters as before
The following variables are available within the php script:
$query : the query that takes the values to show
$sdk_show_all_button : if true it shows the button to cancel the SDK restrictions and show all records
In the second case there are the following methods:
SDK::setPopupReturnFunction($module, $fieldname, $src);
$module : the module containing the field that opens the popup
$fieldname : the name of the field that opens the popup (only uitype 10)
$src : the php file
To remove the customization:
SDK::unsetPopupReturnFunction($module, $fieldname = NULL, $src = NULL);
For now the only fields supported are those with uitype 10.
Hooks
Popup.php
include/utils/ListViewUtils.php
include/ListView/SimpleListView.php
Esempio
<?php
SDK::setPopupQuery('field','Contacts','account_name','modules/SDK/examples/PopupQuery1.php');
SDK::setPopupQuery('related', 'Contacts', 'Products', 'modules/SDK/examples/contacts/PopupRelQuery.php');
SDK::setPopupReturnFunction('Contacts','vendor_id','modules/SDK/examples/ReturnVendorToContact.php');
// you can set a PopupQquery and a PopupReturnFunction to a field in a table field (VTENEXT 24.08)
SDK::setPopupQuery('field', 'Accounts', 'ml1_f4', 'modules/SDK/examples/Contacts/AccountQuery.php');
// here we have a table field (vcf_3) with an account field (vcf_4) and a contact field (vcf_5), I can view only accounts of rating Market Failed, Project Cancelled and Shutdown and only contacts of these accounts. 
$rel_fields = array('processmaker'=>'jQuery("#processmaker").val()','running_process'=>'jQuery("#running_process").val()');
SDK::setPopupQuery('field', 'Processes', 'vcf_3_vcf_4', 'modules/SDK/examples/Contacts/AccountQuery.php', $rel_fields);
$rel_fields['accountid'] = 'jQuery("#vcf_3_vcf_4_"+VTE.EditView.getTableFieldCurrentRow(this)).val()';
SDK::setPopupQuery('field', 'Processes', 'vcf_3_vcf_5', 'modules/SDK/examples/Contacts/ContactsQuery.php', $rel_fields);
// you can set a PopupReturnFunction to the product or other custom fields in the inventory product block (VTENEXT 26.01)
// view examples in modules/SDK/examples/VTE-SDK-2.php
?>

Presave
You can enter your own script when you press the "Save" button in EditView mode. To register a script use the method:
SDK::setPreSave($module, $src);
$module : the name of the module
$src : the path of php file
To remove the customization:
SDK::unsetPreSave($module, $src = NULL);
$module : the name of the module
$src : the path of php file (if NULL, includes all scripts registered for that module)
The following variables are available within the script:
$type : the form name (“MassEditSave”, “DetailView”, “EditView”, “createTODO”, “QcEditView”, “ConvertLead”, “createQuickTODO”, “Kanban”)
$values : (array) new values
For MassEditSave, you can know record that are going to be managed calling getListViewCheck($currentModule);
The following variables can be set:
$status : (bool) whether or not the submit should be saved
$message: (string) if not empty a popup with the message is shown
$confirm: (bool) if true, a Javascript popup is shown asking for confirmation to continue, showing $message. In this case, $status must not be set.
$focus : (string) in case of error, the element that takes the focus (only if $status = false)
$changes: (array) values to assign to the fields (only if $status = false)
The $focus and $changes variables are only available when $status is false and $type is one of 'EditView', 'createTodo', 'QcEditView', 'ConvertLead'.
Hooks
include/js/general.js
include/js/KanbanView.js
modules/Calendar/script.js
modules/Calendar/wdCalendar/sample.php
modules/Leads/Leads.js
modules/Users/Forms.php
modules/VteCore/KanbanAjax.php
Smarty/templates/Header.tpl
Smarty/templates/ComposeEmail.tpl
Smarty/templates/Popup.tpl

Advanced query
You can modify the query executed to load the data in ListView, RelatedList and Popup mode in order to limit or extend the visibility of the data. This does not affect Administrator users, who have access to all data. In addition, the module must be set as Private.
Editing the query is done through a custom php function (see below). Only one function of this type can be used in each module. To register the function use:
SDK::setAdvancedQuery($module, $func, $src);
$module : the module in which to apply the function (if a function is already registered for the module, the new one is not inserted)
$func : the name of the php function
$src : the php file that contains the function
To remove the customization:
SDK::unsetAdvancedQuery($module);
$module : the name of the module
The $func function must be defined as follows:
<?php
function myFunction($module) {
	// Your code ... 
}
$module : the module that calls the function
It returns a string:
“” : (empty string) the query is unchanged
? : (not empty string) this string is added to the query
Hooks
data/CRMEntity.php

Page Header
You can customize the user icon, the settings icon or the blue bars at the top of the pages of VTE to incorporate new features. To do this, simply extend the method setCustomVars of class VTEPageHeader as follows:
SDK::setClass('VTEPageHeader', 'NewPageHeader', 'modules/SDK/src/NewPageHeader.php');
The file NewPageHeader.php will have this content:
<?php
require_once('include/utils/PageHeader.php');
class NewPageHeader extends VTEPageHeader {
	
	protected function setCustomVars(&$smarty, $options = array()) {
		$overrides = array(
			// HTML code to be put right after the menu bar
			'post_menu_bar' => null,
			// HTML code right after the second bar
			'post_primary_bar' => null,
			// HTML code after the third bar
			'post_secondary_bar' => null,
			// HTML code that replace the standard user icon
			'user_icon' => null,
			// HTML code that replace the standard settings icon
			'settings_icon' => null,
		);
		// assign these values to a smarty variable
		$smarty->assign("HEADER_OVERRIDE", $overrides);
	}
}

Translations
Translations can be customized for each language and module installed. To modify or insert a new translation use the method:
SDK::setLanguageEntry($module, $langid, $label, $newlabel);
$module : the module name that contains the string
$langid : the code of the language (e.g. “en_us”, “it_it”)
$label : the label (e.g. LBL_TASK_TITLE)
$newlabel : the translation of the label
If the label already exists for the chosen module and language, it will be replaced. As a module you can specify "APP_STRINGS" to insert a global translation or "ALERT_ARR" to make the translation available in JavaScript files. To load a string simultaneously in multiple languages, the method is:
SDK::setLanguageEntries($module, $label, $strings);
$module : the name of the module
$label : the label
$strings : associative array with the translations (e.g. array(“it_it”=>str1, …))
To remove a translation:
SDK::deleteLanguageEntry($module, $langid, $label = NULL);
$module : the name of the module
$langid : the code of the language
$label : the label (if NULL, all the strings that match)

Fields Visibility
You can change the visibility of the various fields (value of $readonly) and other variables in the different modes (ListView, EditView, and so on) via SDK. To register a new "view" use the method:
SDK::addView($module, $src, $mode, $success);
$module : the name of the module to apply the view
$src : the path of php file
$mode : how the rule is applied (see below)
$success: what to do after applying the rule (see below)
The views defined for each module are applied in the order in which they are registered. When they register, they are added to the queue of views for that module. The $mode variable only makes sense if you change the $readonly variable and it admits the following values:
“constrain” : forces the value of $readonly to take the new value given in the script
“restrict” : changes the value of $readonly only for a more restrictive value (from 1 to 99 or 100, from 99 to 100, not vice versa)
The $success variable instead can be:
“continue” : after applying the view, continue with the next
“stop” : if the view returns $success = true, no other rules are executed
The following variables are available within the scripts:
$sdk_mode : one of “” (create), “edit”, “detail”, “popup_query”, “list_related_query”, “popup”, “related”, “list”, “mass_edit”
$readonly : the readonly value for the current field (1, 99, 100)
$col_fields: values of fields, only for $sdk_mode = “edit”, “detail” e “”
$fieldname or $fieldName: the name of the current field
$current_user: the current user
And you can set the $success variable with true or false.
Depending on the mode (ListView, EditView, and so on) there are different ways to edit the queries and different variables available.
Mode
Value of $sdk_mode
Available variables
Notes
CreateView
""
$col_fields
$current_user
$fieldname
$readonly
$success
1
EditView
"edit"
 
DetailView
"detail"
 
MassEdit
"mass_edit"
 
PopupQuery
"popup_query"
$sdk_columns
$success
2
List/RelatedQuery
"list_related_query"
$sdk_columns
$success
 
Popup
"popup"
$current_user
$fieldname
$sdk_columnnames
$sdk_columnvalues
$readonly
$success
3
Related
"related"
4
List
"list"
 
Notes:
In these modes the values of the fields are in $col_fields[nameOfTheField]
These modes are used to modify the query so that additional fields can be picked up. In $sdk_columns variable there are the database columns to add to the query. Include then the php file “modules/SDK/AddColumnsToQueryView.php”
To get values from other fields, specified in the PopupQuery and ListRelatedQuery modes, write them in the $sdk_columnnames variable, include the php file “modules/SDK/GetFieldsFromQueryView.php” and then take them from $sdk_columnvalues
In the case of the related "Activity history", only the variables $recordId and $readonly are available and apply to the entire row, not to the single field.
To remove the view:
SDK::deleteView($module, $src);
$module : the name of the module
$src : the path of php file
Hooks
include/ListView/ListViewController.php
include/utils/DetailViewUtils.php
include/utils/EditViewUtils.php
include/utils/ListViewUtils.php
include/QueryGenerator/QueryGenerator.php
Popup.php

Home Blocks
New blocks can be added to the home of VTE via SDK. The blocks cannot be deleted from the interface. The method for creating a new block is:
SDK::setHomeIframe($size, $url, $title, $userid = null, $useframe = true);
$size : the horizontal size of the block (from 1 to 4)
$url : the address to be shown within the block. It can also have a protocol at the beginning (e.g. http://www.mysite.com/file)
$title : the label of the block (it can be translated via API)
$userid : array containing the ids of the users who can see the block. If you leave null, the block is visible to all users
$useframe: if true the content will be inside an <iframe> otherwise the file is included directly
Users created later will see all previously registered blocks.
Block cancellation is possible via 2 methods:
SDK::unsetHomeIframe($stuffid);
$stuffid : the id of the block
SDK::unsetHomeIframeByUrl($url);
$url : the url of the block
Blocks are removed for all users.
Hooks
modules/Home/HomestuffAjax.php
modules/Home/HomeWidgetBlockList.php
modules/Home/HomeBlock.php
modules/Home/Homestuff.js
modules/Users/Save.php
Smarty/templates/Home/MainHomeBlock.tpl

Custom Buttons
Buttons can be added under the main menu. To insert a new button use the following method:
SDK::setMenuButton($type, $title, $onclick, $image='', $module='', $action='', $condition = '');
$type : the type of button, it can be 'fixed' or 'contextual'; in the first case the button appears on the left and is always visible, in the second case the button is inserted on the right and will be visible only in the chosen module and for the chosen action.
$title : the label of the button
$onclick : the javascript code to execute. It is NOT possible to use double quotes! (“)
$image : the button image. It must be specified without path and reside in themes/softed/images folder even in the smallest version (e.g. img.png e img_min.png)
$module : if type = ‘contestual’, the module in which the button is visible
$action : if type = ‘contestual’, the action (request action) in which the button is visible
$condition : string like FunctionName:PathPhp representing a function (in the PathPhp file) to be called before showing the button. If it returns false, the button is not shown. The function has only one parameter of type reference to an array with the information of the button.
To remove the button use:
SDK::unsetMenuButton($type, $id);
$type : the type of the button
$id : the id of the button
Hooks
Smarty/templates/Buttons_List.tpl

Transitions Manager
You can change the selection options for the picklists managed by the transitions manager, as well as add messages to the "State manager" block to the right of the record detail. To register this functionality use the method:
SDK::setTransition($module, $fieldname, $file, $function);
$module : the name of the module to handle
$fieldname : the name of the field managed by transition
$file : the path of the php file that contains the function to call
$function : the function to call
To remove the customization:
SDK::unsetTransition($module, $fieldname);
The function called by the transitions manager has the following format:
<?php
function myFunction($module, $fieldname, $record, $status, $values) {
	// Your code ...
}
$module : the current module
$fieldname : the name of the field managed by transition
$record : the current record
$status : the value of the field managed by transition of the current record
$values : array of admissible values for the status
It must return null if you do not want to change the transitions manager behavior or an array with the following format:
array(
'values' => array(..) // array of admissible values for the status
'message' => '' // html code to be included under the transitions manager block
);
Hooks
modules/Transitions/Transitions.php
modules/Transitions/Statusblock.php
Smarty/templates/modules/Transitions/StatusBlock.tpl

PdfMaker Custom Functions
Custom functions can be added in the PDFMaker module. To insert one use:
SDK::setPDFCustomFunction($label, $name, $params);
$label: the label of the function (it is translated into the PDFMaker module)
$name: the name of the function
$params: array with the names of the function parameters
Registered functions must be saved in php files in modules/PDFMaker/functions/ to be used by the PDFMaker module
To remove the function:
SDK::unsetPDFCustomFunction($name);
$name: the name of the function

Custom Folders and Reports
Custom folders can be created using the following API.
SDK::setReportFolder($name, $description);
$name : the name of the folder
$description : the description of the folder
The created folder will be on the reports page. It will be visible to all users and cannot be changed. The name and description can be translated with the translation API.
Folders created via API can be deleted with:
SDK::unsetReportFolder($name, $delreports = true);
$name : the name of the folder to be deleted
$delreports : if true it also deletes all reports (created via API) in that folder (files are not deleted)
Inside the folders you can insert customized reports with the following statement:
SDK::setReport($name, $description, $foldername, $reportrun, $class, $jsfunction = '');
$name : the name of the report (it can be translated via API)
$description : the description of the report (it can be translated via API)
$foldername : the name of the folder created via API where to insert the report
$reportrun : the path of the php file that contains the class that generates the report
$class : the name of the class that handles the report
$jsfunction : the name of the javascript function to run when the "Generate Report" button is pressed
The report created via API can be deleted with:
SDK::unsetReport($name);
$name : the name of the report to delete (files are not deleted)
The class specified in $class must follow the following structure:
<?php
require_once('modules/Reports/ReportRun.php');
class ReportRunAccounts extends ReportRun {
	
	var $enableExportPdf = true;
	var $enableExportXls = true;
	var $enablePrint = true;
	var $hideParamsBlock = true;
	
	function __construct($reportid) {
		$this->reports = Reports::getInstance($reportid); // crmv@172034
		$this->reportid = $reportid;
		$this->primarymodule = 'Accounts';
		$this->reporttype = '';
		$this->reportname = 'Account con sito';
		$this->reportlabel = getTranslatedString($this->reportname, 'Reports');
	}
	
	function getSDKBlock() {
		global $mod_strings;
		$sdkblock = '<p><h2>Questo report mostra le aziende con sito</h2></p>';
		// here I can also add custom inputs to filter the report or any html I need
		return $sdkblock;
	}
	
	// overridden, always hide the summary tab
	function hasSummary() {
		return false;
	}
	
	// overridden, always show the total tab
	function hasTotals() {
		return true;
	}
	
	// generate the report
	function GenerateReport($outputformat = "", $filterlist = null, $directOutput=false) {
		global $adb;
		
		// compatibility, please use set them with the proper methods
		if (!empty($outputformat)) {
			$format = "HTML";
			$tab = "MAIN";
		
			if (strpos($outputformat, 'HTML') !== false) $format = "HTML";
			if (strpos($outputformat, 'PRINT') !== false) $format = "PRINT";
			if (strpos($outputformat, 'PDF') !== false) $format = "PDF";
			if (strpos($outputformat, 'XLS') !== false) $format = "XLS";
			if (strpos($outputformat, 'JSON') !== false) $format = "JSON";
			if (strpos($outputformat, 'CV') !== false) $format = "NULL";
			
			if (strpos($outputformat, 'COUNT') !== false) $tab = "COUNT";
			if (strpos($outputformat, 'TOTAL') !== false) $tab = "TOTAL";
			if (strpos($outputformat, 'CV') !== false) $tab = "CV";
			
			$this->setOutputFormat($format, $directOutput);
			$this->setReportTab($tab);
		} else {
			$format = $this->outputFormat;
			$tab = $this->reportTab;
		}
		
		$format = $this->outputFormat;
		$direct = $this->directOutput;
		$tab = $this->reportTab;
		// prepare the output class
		$output = $this->getOutputClass();
		$output->clearAll();
		
		$return_data = array();
	
		
		if ($tab == 'COUNT' && $this->hasSummary()) {
			// no summary for this custom report
			
		} elseif ($tab == 'CV') {
		
			// no customview for this report
		
		} elseif ($tab == 'MAIN') {
			$sSQL = $this->getReportQuery($outputformat, $filterlist);
			$result = $adb->query($sSQL);
			$this->total_count = $adb->num_rows($result);
			
			$error_msg = $adb->database->ErrorMsg();
			if(!$result && $error_msg!=''){
				// Performance Optimization: If direct output is requried
				if($direct) {
					echo getTranslatedString('LBL_REPORT_GENERATION_FAILED', 'Reports') . "<br>" . $error_msg;
					$error_msg = false;
				}
				// END
				return $error_msg;
			}
			
			if($result) {
			
				$this->generateHeader($result, $output);
			
				while ($row = $adb->fetchByAssoc($result)) {
					$colcount = count($row);
					foreach ($row as $column => $value) {
						$cell = array(
							'value' => $value,
							'column' => $column,
							'class' => 'rptData',
						);
						$output->addCell($cell);
					}
					$output->endCurrentRow();
					
				}
				
				$output->countTotal = $this->total_count;
				$output->countFiltered = $this->total_count;
				
				if ($format == 'XLS') {
					$head = $output->getSimpleHeaderArray();
					$data = $output->getSimpleDataArray();
					foreach ($data as $row) {
						$return_data[] = array_combine($head, $row);
					}
				} else {
					$return_data[] = $output->output(!$direct);
					$return_data[] = $this->total_count;
					$return_data[] = $sSQL;
					$return_data[] = $colcount;
				}
				
			}
		} elseif ($tab == "TOTAL" && $this->hasTotals()) {
			
			$output->addHeader(array('column' => 'fieldname', 'label' => getTranslatedString('Totals')));
			$output->addHeader(array('column' => 'sum', 'label' => getTranslatedString('SUM')));
			$output->addHeader(array('column' => 'avg', 'label' => getTranslatedString('AVG')));
			$output->addHeader(array('column' => 'min', 'label' => getTranslatedString('MIN')));
			$output->addHeader(array('column' => 'max', 'label' => getTranslatedString('MAX')));
			
			// fixed totals
			$rows = array(
				array(
					array('column'=> 'fieldname', 'value' => 'Fatturato totale', 'class' => 'rptData'),
					array('column'=> 'sum', 'value' => 2000, 'class' => 'rptTotal'),
					array('column'=> 'avg', 'value' => null, 'class' => 'rptTotal'),	// not used
					array('column'=> 'min', 'value' => 850, 'class' => 'rptTotal'),
					array('column'=> 'max', 'value' => null, 'class' => 'rptTotal'),	// not used
				),
			);
			
			// add them to the output class
			foreach ($rows as $row) {
				foreach ($row as $cell) {
					$output->addCell($cell);
				}
				$output->endCurrentRow();
			}
			
			// format for xls or html
			if ($format == "XLS") {
				
				// change the output array to match the expected format for XLS export
				$return_data = array();
				$data = $output->getSimpleDataArray();
				$fieldName = '';
				foreach ($data as $row) {
					$nrow = array();
					foreach ($row as $key => $value) {
						if ($key == 'fieldname') {
							$fieldName = $value;
							continue;
						}
						$klabel = $fieldName.'_'.strtoupper($key);
						$nrow[$klabel] = $value;
					}
					$return_data[] = $nrow;
				}
			
			} else {
				$return_data = $output->output(!$direct);
			}
		}
		
		return $return_data;
	}
	
	// generate a fixed header for the report
	function generateHeader($result, $output, $options = array()) {
		global $adb, $table_prefix;
		
		$module = 'Accounts';
		$tabid = getTabid($module);
		$count = $adb->num_fields($result);
		for ($x=0; $x<$count; ++$x) {
			$fld = $adb->field_name($result, $x);
			
			// get the field label from the column (if possible)
			$res = $adb->pquery("SELECT fieldlabel FROM {$table_prefix}_field WHERE columnname = ? and tabid = ?", array($fld->name, $tabid));
			if ($res && $adb->num_rows($res) > 0) {
				$fieldlabel = $adb->query_result_no_html($res, 0, 'fieldlabel');
				$headerLabel =  getTranslatedString($fieldlabel, $module);
			} else {
				$headerLabel = $fld->name;
			}
			$hcell = array(
				'column' => $fld->name,
				'label' => $headerLabel,
				'orderable' => false,
				'searchable' => false,
			);
			$output->addHeader($hcell);
		}
		
	}
	
	// generate the report query
	function getReportQuery($outputformat, $filterlist) {
		global $table_prefix;
		$query = 'SELECT accountid, accountname, website, phone FROM '.$table_prefix.'_account WHERE website <> "" ';
		return $query;
	}
}
The javascript function specified in $jsfunction must already be declared and return a string to be added to the request.
function preRunReport(id) {
	var params = "";
	var select = getObj('picklist1’);
	if (select) {
		params += "&picklist1="+select.options[select.selectedIndex].value;
	}
	var selectuser = getObj('picklist2’);
	if (selectuser) {
		params += "&picklist2="+selectuser.options[selectuser.selectedIndex].value;
	}
	return params;
}
When updating a VTE to version 16.09 (or later), some features of the customized reports must be verified.
One of the changed parts is the management of time filters, which previously could be modified by extending the getPrimaryStdFilterHTML and getSecondaryStdFilterHTML methods. To obtain the same result, you need to extend the getStdFilterFields function which returns an array of available fields, for example:
<?php
function getStdFilterFields() {
	// See the method Reports::getStdFilterFields for the standard implementation
	
	// this example just loads standard fields for the Potential module
	$list = $this->reports->getStdFiltersFieldsListForChain(0, array('Potentials'));
	
	return $list;
}
When the report is generated the variable $this->stdfilters contains the time filter to be used. If the report query is completely customized, the generation of the time filter must also be managed manually, for example using a method like this:
<?php
function addStdFilters() {
	global $current_user;
	$sql = '';
	
	if (is_array($this->stdfilters)) {
		foreach ($this->stdfilters as $flt) {
			if ($flt['fieldid'] > 0) {
				// get field informations
				$finfo = $this->getFieldInfoById($flt['fieldid']);
				$table = $finfo['tablename'];
				$qgen = QueryGenerator::getInstance($finfo['module'], $current_user);
				$operator = 'BETWEEN';
				if ($flt['value'] == 'custom') {
					$value = array($flt['startdate'], $flt['enddate']);
				} else {
					$cv = CRMEntity::getInstance('CustomView');
					$value = $cv->getDateforStdFilterBytype($flt['value']);
				}
				// adjust for timezone
				$value[0] = $this->fixDateTimeValue(
					$qgen, $finfo['fieldname'], $value[0]
				);
				$value[1] = $this→fixDateTimeValue(
					$qgen, $finfo['fieldname'], $value[1], false
				);
				// add the condition
				$sql .= ' AND '.$table.'.'.$finfo['columnname'].' '.
					$operator.' '.$value[0].' AND '.$value[1];
			}
		}
	}
	
	return $sql;
}
Hooks
modules/Reports/Listview.php
modules/SaveAndRun.php
modules/Reports/Reports.php
modules/Reports/CreatePDF.php
modules/Reports/CreateXL.php
modules/Reports/PrintReport.php
Smarty/templates/ReportContents.tpl
Smarty/templates/ReportRunContents.tpl
Smarty/templates/ReportRun.tpl

Turbolift Counter
When changing the standard extraction criteria of a related list, for example using another method or redefining it by extending the class that contains it, the counter of the number of the linked records visible in the Turbolift (the right column with the list of modules in record detail) may no longer be valid. In this case, therefore, a method that returns the correct count must be defined through the following API.
SDK::setTurboliftCount($relation_id, $method);
$relation_id : relation id (table vte_relatedlists)
$method : the method name that returns the correct count
The method must be implemented in the class that contains the relation (e.g. in the Contact relationship connected to a Company we mean the Accounts class or any extensions to it)
To remove the customization:
SDK::unsetTurboliftCount($relation_id);
The defined method can be the related list method (column name in vte_relatedlists) or a custom method that returns an integer.

SDK Processes

Process log
The way of viewing the logs has changed in the various vtenext releases and is summarized as follows.
vtenext 16.09: Logs can be consulted by accessing the filesystem in the folder logs/ProcessEngine
vtenext 18.X: In Settings → Business Process Manager → ProcessManager the button 'LOG' will be available once the following script has been executed:
<?php
require_once('include/utils/VTEProperties.php');
$VP = VTEProperties::getInstance();
$VP->set('settings.process_manager.show_logs_button', 1);
The file 01.log is initially created. When the file exceeds the 5MB, the file 02.log will be created and so on.
vtenext 19.10: Logs are available in Settings → Other Settings → System Logs

Import Processes with a script
From version 18.05 (rev. 1696) it is possible to import processes previously exported in the format vtebpmn (diagram + configuration) or bpmn (diagram only) with php scripts using the importFile method of the ProcessMakerUtils class.
The importFile method takes in the first parameter the path of the file to be installed (.vtebpmn / .bpmn) and in the second one a Boolean value (true/false) depending on whether you want to automatically activate the process or not.
This method is useful for installing processes at the end of the installation of a new module: just include the process file in the installation zip and execute the code in the case 'postinstall' of the method vtlib_handler of the module class.
Example
require_once('modules/Settings/ProcessMaker/ProcessMakerUtils.php');
$PMUtils = ProcessMakerUtils::getInstance();
$PMUtils->importFile('PATH_FILE/Process1.vtebpmn',true);
$PMUtils->importFile('PATH_FILE/DiagramProcess2.bpmn',false);
 

SDK
You can add custom functions to processes, click here for more details.

Webservice REST
These Web services allow you to do HTTP requests to the specified endpoints (VTE_URL/restapi/v1/vtews/METHOD_NAME).
The requests need POST method, basic authentication (you must set ‘Authentication basic’ into the header which is calculated through a function based on username* and password*) and their relative parameters.
Each one method will give a JSON response with a status and the data or in case of error an error code and its relative message.
Notes:
• The “id” parameter is always specified as ‘moduleid* x recordid’ (ex: 2x313) 
   *: You can get the the moduleid from webservice ‘describe’ method (idPrefix)
Headers: 
• Authorization: Basic base64_encode("username*:password*")
• Content-Type : application/json
username* (VTE) password* (User preferences’ Webservice Access Key)
Authorization Example:
‘Basic ZmVkZXJpY28ucGVybGluOjB5cm5MRjNhS2RhMDZ2c3E=‘
Requirements for Apache2:
• Activate apache rewrite module
• Set AllowOverride All into the virtualhost file to the vte path folder. 
Example:
<Directory /var/www/html/VTE_FOLDER>
Options -Indexes +FollowSymLinks +MultiViews
AllowOverride All
Require all granted
</Directory>
Requirements for Nginx + PHP FPM
As Nginx does not handle htaccess file, some special rewrites are needed in order to handle WS requests.
Below an example of nginx conf file:
server {
	listen 80;
	root /var/www/vtenext/;
	
	autoindex off;
	
	index index.php index.html index.htm;
	# Make site accessible from http://localhost/
	
	server_name localhost;
	location / {
		# First attempt to serve request as file, then
		# as directory, then fall back to index.html
		try_files $uri $uri/ index.html;
	}
	# deny access to .htaccess files, if Apache's document root
	# concurs with nginx's one
	location ~ /\.ht {
		deny all;
	}
	
	# special folder handling, since .htaccess files are not supported
	
	# REST API
	
	location /restapi/v1 {
		rewrite ^(.+)/vtews/(.*)$ /restapi/v1/index.php/vtews/$2;
	}
    
    # PORTAL
	location ~ /portal/v2/(?!public) {
		rewrite ^/portal/v2/(.*)$ /portal/v2/public/$1;
	}
	
	# STORAGE
	
	location /storage {
		deny all;
		
		location /storage/uploads_emails_ {
			allow all;
		}
		
		location /storage/images_uploaded/ {
			allow all;
		}
		
		location ~* /storage/logo/.+\.(jpg|jpeg|png|gif)$ {
			allow all;
		}
		
		rewrite ^/storage/(.*)$ /getStorage.php?file=$1;
	}
	
	# PROTECTED FOLDERS
	
	location /logs {
		deny all;
	}
	
	location /plugins/dataimporter {
		deny all;
	}
	
	location /cache/sys {
		deny all;
	}
	
	location /cache/pdfmaker {
		deny all;
	}
	
	location /cache/import {
		deny all;
	}
	
	location /cache/session {
		deny all;
	}
	
	location /cache/pdf {
		deny all;
	}
	
	location /dataimport {
		deny all;
	}
	
	location /modules/VteSync/VteSyncLib/storage {
		deny all;
	}
	
	location /modules/Messages/src/attachment_tnef/plugins/attachment_tnef/class/ {
		deny all;
	}
	
	# SMARTOPTIMIZER
	
	# disabled by default, enable it if needed
	#location ~* \.(gif|jpe?g|png|swf|css|js|html?|xml|txt|ico)$ {
	#	rewrite ^(.*)$ /smartoptimizer/?$1;
	#}
    
    # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
	location ~ [^/]\.php(/|$) {
		include snippets/fastcgi-php.conf;
		
		# With php cgi alone:
		#fastcgi_pass 127.0.0.1:9000;
		
		# With php fpm:
		fastcgi_pass unix:/var/run/php/php7.0-fpm.sock;
		fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
	}
}
Web Service Methods:
 
 
 
 
 
Name
Description
Parameters
Response
create
Create a record of the specified module
elementType (String)element (Encoded)
Return all fields and values of the created record
Ex: Url: VTE_URL/restapi/v1/vtews/create
Parameters: 
{"elementType":"Accounts", "element":"{\"accountname\":\"account1\", \"assigned_user_id\":\"19x1\"}"}
update
Update the specified record
id (String)columns (Encoded)
Return all fields and values of the updated record
Ex: Url: VTE_URL/restapi/v1/vtews/update
Parameters: 
{"id":"3x310", "columns":"{\"accountname\":\"test fede postman\"}"}
revise
Update the specified record. It’s different only for parameters, the result is the same
element (Encoded)
Return all fields and updated values
Ex: Url: VTE_URL/restapi/v1/vtews/revise
Parameters:
{"element":"{\"id\":\"3x27\",\"employees\":\"5\",\"industry\":\"Banking\"}"}
retrieve
Illustrate the fields of the specified record and their relative values
id (String)
Return all fields and values of the specified record
Ex: Url: VTE_URL/restapi/v1/vtews/retrieve
Parameters: 
{"id":"3x310"}
retrieveinventory
Illustrate the fields of the specified inventory record, their relative values and the product block’s information
id (String)
Return all fields and values of the specified record and product’s block information
Ex:
Url: VTE_URL/restapi/v1/vtews/retrieveinventory
Parameters:
{"id":"16x104"}
delete
Delete the specified record
id (String)
Return the request status (successful or not)
Ex: Url: VTE_URL/restapi/v1/vtews/delete
Parameters: 
{"id":"3x306"}
query
Execute a query and return the result’s rows
query (String)
Return all rows of the executed query
Ex: Url: VTE_URL/restapi/v1/vtews/query
Parameters:
{"query":"SELECT * FROM Accounts WHERE accountname like '%vte%';"}
listtypes
Describe each one module which contains uitype of the specified format
fieldTypeList (Encoded)
Return module information of the specified fieldtypes
Ex: Url: VTE_URL/restapi/v1/vtews/listtypes
Parameters:
{"fieldTypeList":"[\"integer\",\"file\"]"}
describe
Describe the specified module and their relative fields
elementType (String)
Return all module information and its fields properties(no hidden fields)
Ex: Url: VTE_URL/restapi/v1/vtews/describe
Parameters:
{"elementType":"Accounts"}
describeall
The describeall method is different from describe one because it shows hidden fields too
ElementType (Encoded)
Return all module information and its fields properties(hidden fields too)
Ex: Url: VTE_URL/restapi/v1/vtews/describeall
Parameters:
{"elementType":"Accounts"}
getlabels
Return all labels and translated ones of the specified module,language
username (String)language (String)module (String)
Return labels and translated ones of the specified module, language
Ex:Url: VTE_URL/restapi/v1/vtews/getlabels
Parameters:
{"username":"admin", "language":"it_it", "module":"Accounts"}
getlangs
Return all languages installed into the CRM
 
Return all CRM languages
loginpwd
Return user webservice access key if username and password are valid
username (String)password (String)
Return user webservice access key
Ex:Url: VTE_URL/restapi/v1/vtews/loginpwd
Parameters:
{"username":"admin","password":"123456789"}
getmenulist
Return all modules information (visibility, tabid, name, sequence, ...)
 
Return modules information and properties
oldoquery
Return records that contain the searched value into specified module’s fields
module (String)search_fields (Encoded)search_value (String)
Return some record basic information where searched value is contained into the specified module’s fields
Ex: Url: VTE_URL/restapi/v1/vtews/oldoquery
Parameters:
{"module":"Accounts", "search_fields":"[\"accountname\", \"website\"]", "search_value":"vtenext"}
Technical documentation for SDK is available here.

Portal v2
Theme documentation
https://adminlte.io/docs/3.2/
https://github.com/ColorlibHQ/AdminLTE
https://adminlte.io/themes/v3/
Webservice REST
Handler: include/Webservices/CustomerPortal.php
Headers:
• Authorization: Basic base64_encode("username:accesskey")
• Portal-Session-Id: the parameter is optional and is returned by the "portal.login". Use in subsequent calls to optimize portal performance.
• Content-Type: application/json
Name
Parameters
Description
portal.info
 
Allows you to obtain information on the license and logos used by the connected vtenext installation.
portal.login
Body
. username: string (*)
. password: string (*)
. language: string
Allows you to log in to the customer portal by providing the email (username) and password of the contact. The service also returns the accesskey and session id values to be used in subsequent calls as headers (Authorization and Portal-Session-Id).
portal.logout
Headers
. Authorization
Allows you to logout the contact from the portal.
portal.send_mail_for_password
Body
. email: string (*)
. language: string
Allows you to recover the password of a contact.
portal.modules_list
Headers
. Authorization
Body
. language: string (*)
Allows you to obtain a list of modules enabled for the contact's profile.
portal.get_list
Headers
. Authorization
Body
. module: string (*)
. language: string
. folderid: int
. search: array
Allows you to obtain the list of records of a module filtered by the contact profile. 
The "folderid" parameter is used by the Documents module to get the documents of a specific folder.
The "search" parameter allows the paging of the list on the server-side.
Ex.
[
   'length' => 50, // Number of records to return
   'start' => 0, // Paging offset
   'search' => '', // Global search
   'search_columns' => [ // Search by columns
     [
       'index' => 0, // Index of the field
       'column' => '', // Name of the field
       'search' => '', // Value of the field
     ]
   ],
   'ordering' => [ // Ordering
    [
       'index' => 0, // Index of the field
       'column' => '', // Name of the field
       'dir' => '', // asc/desc
     ]
   ],
]
Note:
1. All searches are performed ONLY on the columns of the list.
2. Column search is performed in "LIKE" mode.
3. Sorting can only be done on one column.
portal.get_blocks
Headers
. Authorization
Body
. module: string (*)
. language: string
. mode: string (edit, create, detail, list) (*)
. app_data: array
Allows you to obtain the list of the blocks and the related fields of a module filtered by the contact profile.
The "app_data" parameter represents the record being edited (array with field name and value) and is used by the SDK views to establish the visibility of the fields.
This parameter will also be used in the future for managing conditional fields.
portal.get_record
Headers
. Authorization
Body
. module: string (*)
. id: int (*)
If the contact has visibility permissions for the indicated record, it allows you to obtain its data.
portal.save_record
Headers
. Authorization
Body
. module: string (*)
. id: int (*)
. values: encoded (*)
If the contact has write permissions for the indicated record, it allows its saving.
portal.delete_record
Headers
. Authorization
Body
. module: string (*)
. id: int (*)
If the contact has delete permissions for the indicated record, it allows its deletion.
portal.write_ticket_comment
Headers
. Authorization
Body
. id: int (*)
. comment: string (*)
Allows you to write a comment within the indicated ticket.
portal.get_attachments
Headers
. Authorization
Body
. id: int (*)
Allows you to obtain the list of documents of the specified record filtered by the visibility of the contact profile.
portal.download_attachment
Headers
. Authorization
Body
. relid: int (*)
. docid: int
If the contact has permission to view the attachment (relid), it allows its download.
portal.upload_attachment
Headers
. Authorization
Body
. relid: int (*)
. title: string (*)
File
Allows the uploading of an attachment related to the specified record (relid). You can indicate the name (title) of the document that will be generated.
portal.provide_confidential_info
Headers
. Authorization
Body
. id: int (*)
. comments: string
. data: string (*)
. request_commentid: int (*)
Allows you to respond to a request for confidential information. The "id" parameter indicates the ticket id, the "comments" parameter indicates the unencrypted comment, the "data" parameter indicates the confidential response, and the "request_commentid" parameter indicates the id of the comment to which the confidential response should be provided .
portal.get_home_widgets
Headers
. Authorization
Body
. language: string
Allows you to obtain the widgets configured in the profile associated with the contact.
portal.save_authenticate_cookie
Headers
. Authorization
Body
. contactid: int (*)
Provides an hash to be stored in a cookie to remember the contact's login.
portal.check_authenticate_cookie
Headers
. Authorization
Body
. contactid: int (*)
. hash: string (*)
Allows you to verify the hash used to remember the contact's login.
portal.change_password
Headers
. Authorization
Body
. username: string (*)
. old_password: string (*)
. password: string (*)
. language: string
Allows you to change the contact's password.
Register a new REST webservice
Create a new file and execute it (e.g. plugins/script/script.php).
<?php
require('../../config.inc.php');
chdir($root_directory);
require_once('include/utils/utils.php');
require_once('vtlib/Vtecrm/Module.php');
$Vtiger_Utils_Log = true;
global $adb, $table_prefix;
VteSession::start();
SDK::setClass('CustomerPortalRestApi', 'CustomerPortalRestApi2', 'modules/SDK/src/CustomerPortalRestApi2.php');
$parameters = ['param1' => 'string', 'param2' => 'encoded', 'param3' => 'encoded'];
$perm = 'read'; // read, write, readwrite
SDK::setRestOperation('portal.foo', 'modules/SDK/src/CustomerPortalRestApi2.php', 'CustomerPortalRestApi2.foo', $parameters, $perm);
Create a new file that contains the CustomerPortalRestApi2 class (e.g. modules/SDK/src/CustomerPortalRestApi2.php).
<?php
require_once('include/Webservices/CustomerPortal.php');
class CustomerPortalRestApi2 extends CustomerPortalRestApi {
	public function foo($param1, $param2, $param3) {
		$data = [1, 2, 3, 4, 5];
        // ...
		return $data;
	}
}
Extend an existing REST webservice
Create a new file and execute it (e.g. plugins/script/script.php).
<?php
require('../../config.inc.php');
chdir($root_directory);
require_once('include/utils/utils.php');
require_once('vtlib/Vtecrm/Module.php');
$Vtiger_Utils_Log = true;
global $adb, $table_prefix;
VteSession::start();
SDK::setClass('CustomerPortalRestApi', 'CustomerPortalRestApi2', 'modules/SDK/src/CustomerPortalRestApi2.php');
Create a new file that contains the CustomerPortalRestApi2 class​ (e.g. modules/SDK/src/CustomerPortalRestApi2.php).
<?php
require_once('include/Webservices/CustomerPortal.php');
class CustomerPortalRestApi2 extends CustomerPortalRestApi {
	public function get_list($module, $language, $folderid = 0, $search = []) {
		$ret = parent::get_list($module, $language, $folderid, $search);
		// your code here ...
		return $ret;
	}
}
Structure of main folders/files
The portal folder is located at: VTE_ROOT/portal/v2
The main folders/files of the new portal are:
Folder/file name
Description
app
Contains the files and the logic of the portal.
app/controllers
Contains the files that manage the portal's default actions (e.g. Login, Logout, Edit, Detail, etc.).
app/fields
Contains the files that manage the portal module fields.
app/modules
Contains custom logic of some vtenext modules (e.g. Documents, Processes, HelpDesk, etc.).
app/PortalModule.php
Class that manages the actions of the portal modules (e.g. Create, Detail, Edit, etc.). The class can be extended to change the default behaviors of a module.
config
Contains the portal configuration files.
public
Contains the portal's public files (e.g. index.php, css, javascript, images, etc.).
resources
Contains the resources used by the portal such as translation files (lang) and templates (templates).
sdk
The folder is used to insert new customizations for the customer.
storage
Contains temporary files (e.g. cache, logs, etc.).
vendor
Contains the external libraries used by the portal.
Request lifecycle
Portal configuration
The portal configuration file is located in "config/portal.config.php".
To overwrite the parameters the "config/sdk.config.php" file must be used otherwise, in case of updating the version of vtenext, the changes could be lost.
How to move the Business Portal to another host/folder
Update the "portal_url", "vte_url" and "csrf_secret" parameters in the "config/sdk.config.php" file.
Also change the "default_timezone" parameter if the host has a different timezone from the one in which the vte is located. Change the "portal.url" prop to the link pointing to the v2 folder. e.g. https://ticket.vtenext.com/v2​​
Here is the list of parameters supported by the new portal:
Parameter
Type
Default
Description
portal_url
String
$PORTAL_URL
(config.inc.php)
Indicates the URL of the customer portal. If the portal folder is inside the vtenext root directory, the variable will be set to the value of the $PORTAL_URL variable set in the config.inc.php file.
vte_url
String
$site_URL (config.inc.php)
Indicates the URL of vtenext and is used to obtain its data via the rest API. If the portal folder is inside the vtenext root directory, the variable will be set to the value of the $site_URL variable set in the config.inc.php file.
default_language
String
it_it
Indicates the default language used in the customer portal. The value can be replaced with a supported language (see "languages" parameter).
languages
Array
['en_us' => 'US English', 'it_it' => 'IT Italiano']
Indicates the languages ​​supported in the customer portal. 
To add a new language you need to create a new file in the resources/lang folder.
production
Bool
false
This configuration indicates whether errors should be displayed or not. If the environment is production, the value will be set to true to disable error display. If the environment is development, the value will be set to false to allow errors to appear.
default_module
String
 
Indicates the default module to load after logging into the portal. The value can be replaced with the name of the desired module (must be enabled from profile). If the parameter is empty, the portal home will be loaded.
favicon
String
assets/img/VTENEXT_favicon.ico
Indicates the path of the favicon. The value is relative to the public folder.
login_logo
String
assets/img/VTENEXT_login.png
Indicates the path of the logo to load on the login page. The value is relative to the public folder.
[UPDATE] The logo must be loaded in vtenext settings > Logos.
login_background
String
 
Indicates the path of the background to load on the login page. The value is relative to the public folder.
header_logo_sm
String
assets/img/VTENEXT_toggle.png
Indicates the path of the icon to load in the minimized sidebar. The value is relative to the public folder. 
[UPDATE] The icon must be loaded in vtenext settings > Logos.
header_logo_lg
String
assets/img/VTENEXT_header.png
Indicates the path of the icon to load in the expanded sidebar. The value is relative to the public folder. 
[UPDATE] The icon must be loaded in vtenext settings > Logos.
helpdesk_logo
String
assets/img/helpdesk.png
Indicates the logo used to display the response given by customer support. The value is relative to the public folder.
sidebar_theme
String
sidebar-dark-primary
Indicates the class of the main sidebar. 
It can have a "dark" or "light" brightness. 
It can also have a color variant, such as "primary", "success", "warning", "info", "danger".
enable_sidebar_search
Bool
false
Enable/disable the search bar in the main sidebar.
csrf_secret
String
$csrf_secret (config.inc.php)
Indicates the secret key used to generate a csrf token. If the portal folder is inside the vtenext root directory, the variable will be set to the value of the $csrf_secret variable set in the config.inc.php file.
upload_dir
String
 
Indicates the name of the folder used for uploading files.
browser_title_prefix
String
 
Indicates the prefix label to use for the browser title. The value can be replaced with the desired label.
browser_title_suffix
String
customer_portal
Indicates the suffix label to use for the browser title. The value can be replaced with the desired label.
remember_cookie_name
String
portal_login_hash
Indicates the name of the cookie used to remember user authentication.
login_expire_time
Int
2592000 (one month)
Indicates the expiration of the cookie used to remember user authentication. The value can be replaced with the desired number of seconds.
default_timezone
String
Europe/Rome
Indicates the default time zone used in the customer portal. This configuration must be the same as the $default_timezone variable set in the vtenext config.inc.php file.
module_icons
Array
[]
With this configuration you can override the default icons used for modules enabled in the customer portal. The default form icons are found in app/layouts/PortalLayout.php. The names of the icons can be found here https://fonts.google.com/icons.
sdk_languages
Array
[]
With this configuration you can add new labels or modify existing ones. 
You need to create a new file in the sdk folder.
sdk_global_php
Array
[]
With this configuration you can add php files to load on each page (they should contain classes/functions). 
You need to create a new file in the sdk folder.
sdk_global_js
Array
[]
With this configuration you can add js to load globally. 
You need to create a new file in the public/assets/sdk folder.
sdk_module_js
Array
[]
With this configuration you can add js to load for a specific module. 
You need to create a new file in the public/assets/sdk folder.
sdk_global_css
Array
[]
With this configuration you can add css to load globally. 
You need to create a new file in the public/assets/sdk folder.
sdk_controllers
Array
[]
With this configuration you can add custom actions ("action" field in the URL). 
The array represents an association between the action name and the file that contains the controller class to handle the request. 
You need to create a new file in the sdk folder.
sdk_module
Array
[]
With this configuration you can add customizations on a specific module. 
The array represents an association between the module name and the file that contains the module's extended class. 
You need to create a new file in the sdk folder.
sdk_menu
Array
[]
With this configuration you can add custom menu items in the sidebar. 
You need to create a new file in the sdk folder.
API Reference - Main classes, methods, functions and variables
\app\Request
Method
Arguments
Description
get()
$keys = null, $purify = false
Allows you to get one or more parameters from the global variable $_GET. If the second argument is set to "true", the parameters are purified through the HTML Purifier library. Example:
$request->get('foo');
$request->get(['foo', 'bar']);
$request->get('foo', true);
post()
$keys = null, $purify = false
Allows you to get one or more parameters from the global variable $_POST. If the second argument is set to "true", the parameters are purified through the HTML Purifier library. Example:
$request->post('foo');
$request->post(['foo', 'bar']);
$request->post('foo', true);
files()
$key
Allows you to get files uploaded via the HTTP POST method and organized via the global variable $_FILES.
$request->files('attachments');
cookie()
$keys = null, $purify = false
Allows you to obtain one or more parameters from the global variable $_COOKIE. If the second argument is set to "true", the parameters are purified through the HTML Purifier library. Example:
$request->cookie('foo');
server()
$keys = null, $purify = false
Allows you to get one or more parameters from the global variable $_SERVER. If the second argument is set to "true", the parameters are purified through the HTML Purifier library. Example:
$request->server('');
isGet()
 
Returns true if the request method is GET.
isPost()
 
Returns true if the request method is POST.
isAjax()
 
Returns true if the request is AJAX.​
purify()
$input
Purify the $input variable through the HTML Purifier library.
\app\Response
Method
Arguments
Description
__construct()
$content = '', $statusCode = 200, $headers = []
Initializes a new \app\Response() object with the response content ($content), return code ($statusCode), and default headers ($headers).
setContent()
$content
Set the content of the response.
setStatusCode()
$statusCode
Set the response return code.
setHeader()
$header, $replace = true
Set a new header in the response. With the second argument it is possible to indicate whether or not the header must replace a previous header already set.
setMimeType()
$mimeType = 'text/html'
Sets the response content mime.
json()
$data
Set response content with $data converted to JSON format and 'application/json' mime.
redirect()
$page
Performs a redirect to $page.
downloadFile()
$fullpath
Allows downloading of a file located in $fullpath.
output()
 
Outputs the content, response code, and headers you set.
\app\Session
Method
Arguments
Description
set()
$key, $value = ''
Set the value $value with key $key in $_SESSION.
get()
$key
Allows you to get the value with key $key from $_SESSION.
flash()
$key
Allows you to get the value with key $key from $_SESSION. Next, the $key will be deleted from $_SESSION.​
remove()
$key
Delete the $key from $_SESSION.
hasKey()
$key
Returns true if $key exists in $_SESSION.
setMulti()
$keys
Allows you to write multiple values ​​to $_SESSION.
removeMulti()
$keys
Delete multiple values ​​in $_SESSION.
append()
$key, $value = ''
Set the $key as an array in $_SESSION and the value $value is added to it.
\app\Config
Method
Arguments
Description
has()
$key
Returns true if $key exists in the global configuration.
get()
$key
Allows you to get the value with key $key from the global configuration.
set()
$key, $value
Set the value $value with key $key in the global configuration.
getAll()
 
It allows you to obtain a key-value list with all the global configuration.
setMulti()
$values
Allows you to write multiple values ​​into the global configuration.
clear()
$key
Delete the $key from the global configuration.
clearAll()
 
Delete all values ​​from the global configuration.
\app\PortalModule
Variable
Default
Description
$hasComments
false
Indicates whether the module supports comments.
$hasAttachments
false
Indicates whether the module supports attachments.
$enableEdit
true
Indicates whether the module can be modified (edit mode).
$formColumns
3
Indicates the number of columns to use for displaying fields in Create, Edit and Detail.
$listTemplate
List.tpl
Indicates the template used for displaying a list (action List).
$referenceListTemplate
sections/ReferenceList.tpl
Indicates the template used for displaying a related list (uitype 10).
$detailTemplate
Detail.tpl
Indicates the template used to display the detail of a record (action Detail).
$editTemplate
Edit.tpl
Indicates the template used to display the edit of a record (action Edit).
$notAuthorizedTemplate
PageNotAuthorized.tpl
Indicates the template used to display a permission error (e.g. record not found or permission errors).
Method
Arguments
Description
__construct()
$module
$module indicates the name of the module to obtain an instance of the class. If the module has been extended via SDK then an instance of the extended class will be returned.
prepareList()
$viewer, $request
Method used for displaying a list (action List).
postProcessList()
$viewer, $request
This method can be used by extended classes to insert/modify data set in prepareList().
prepareEdit()
$viewer, $request
Method used to display the edit of a record (action Edit).
postProcessEdit()
$viewer, $request
This method can be used by extended classes to insert/modify the data set in prepareEdit().
prepareDetail()
$viewer, $request
Method used to display the detail of a record (action Detail).
postProcessDetail()
$viewer, $request
This method can be used by extended classes to insert/modify data set in prepareDetail().
saveRecord()
$request
Method used to save a record (action Save).
postProcessSaveValues()
$request, &$values
This method can be used by extended classes to insert/modify data set in saveRecord().
isPermitted()
$module, $action = self::ACTION_LIST, $recordValues = []
Returns if the contact has permission to perform a certain action.
List of supported actions:
. ACTION_LIST
. ACTION_CREATE
. ACTION_EDIT
. ACTION_DETAIL
. ACTION_DELETE
. ACTION_SAVE
. ACTION_ADD_COMMENTS
. ACTION_UPLOAD_ATTACHMENTS
. ACTION_CHANGE_PWD
. ACTION_SOLVE_TICKET
\app\clients\PortalRestClient
Method
Arguments
Description
get()
$restName, $queryParameters = [], $headers = []
Allows you to perform a GET request to vtenext.
post()
$restName, $queryParameters = [], $bodyParameters = [], $headers = []
Allows you to perform a POST request to vtenext.
postMultipart()
$restName, $queryParameters = [], $bodyParameters = [], $fileParameters = [], $headers = []
Allows you to perform a multipart POST request to vtenext.
patch()
$restName, $queryParameters = [], $bodyParameters = [], $headers = []
Allows you to execute a PATCH request to vtenext.
delete()
$restName, $queryParameters = [], $headers = []
Allows you to execute a DELETE request to vtenext.
postDownload()
$restName, $queryParameters = [], $bodyParameters = [], $headers = []
Allows you to perform a POST request to download a vtenext file in stream mode.
Helpers
Function
Arguments
Description
preprint()
$var
print_r formatted with <pre> tag
predump()
$var
var_dump formatted with <pre> tags
encodeForHtml()
$value, $charset = 'UTF-8'
Encode the $value value to be inserted into an HTML page.
encodeForHtmlAttr()
$value, $enclosing = '"'
Encode the $value value to be placed inside an HTML tag attribute.
encodeForJs()
$value, $enclosing = '"'
Encode the $value value to be placed inside a javascript <script>.
htmlAttr()
$attributes
Encode a list of attributes to be placed inside an HTML tag.
config()
$key
Allows you to get the value with key $key from the global configuration.
trans()
$key, $args = []
Translate the label $key.
listUrl()
$module, $extraParams = []
Generate a link to open a list.
createUrl()
$module, $extraParams = []
Generates a link to open the creation of a record.
createDocUrl()
$module, $folderId, $extraParams = []
Generates a link to open the creation of a document.
detailUrl()
$module, $record, $extraParams = []
Generates a link to open the detail of a record.
editUrl()
$module, $record, $extraParams = []
Generates a link to open the editing of a record.
downloadUrl()
$record, $documentId, $extraParams = []
Generates a link to download a document.
docFolderUrl()
$module, $folderId, $extraParams = []
Generates a link to open a specific folder in the documents module.
returnUrl()
$request
Generate a return link (e.g. "Cancel" action).
portalLanguage()
 
Returns the language used in the portal.
setPortalLanguage()
$language
Set the $language in the portal.
getBrowserTitle()
$title
Returns the title to be set in an HTML page of the portal.
getModuleLabel()
$module
Returns the translation of the module $module.
getSingleModuleLabel()
$module
Returns the singular translation of the module $module.
getMaxUploadSize()
 
Returns the maximum upload size in the portal.
setPortalCookie()
$name, $value = "", $expires_or_options, $httponly = false
Set a cookie in the portal.
unsetPortalCookie()
$name, $httponly = false
Removes a cookie from the portal.
csrfToken()
 
Returns the csrf token.
csrfInputName()
 
Returns the name of the input for sending the csrf token.
flashPortalError()
$error
Allows you to display a timed error message.
flashPortalMessage()
$message
Allows you to display a timed message.
customerId()
 
Returns the ID of the authenticated contact.
customerEmail()
 
Returns the email of the authenticated contact.
customerUsername()
 
Returns the name and surname of the authenticated contact.
resourcever()
$filename
Allows the versioning of css and javascript files.
basePath()
$path = ''
Returns the path to the portal base folder. If $path is specified, a path is created and returned starting from the base folder.
appPath()
$path = ''
Returns the path to the portal's "app" folder. If $path is specified, a path is created and returned starting from the "app" folder.
configPath()
$path = ''
Returns the path to the portal's "config" folder. If $path is specified, a path is created and returned starting from the "config" folder.
resourcesPath()
$path = ''
Returns the path to the portal's "resources" folder. If $path is specified, a path is created and returned starting from the "resources" folder.
langPath()
only $
Returns the path to the indicated language file $lang.
storagePath()
$path = ''
Returns the path to the portal's "storage" folder. If $path is specified, a path is created and returned starting from the "storage" folder.
publicPath()
$path = ''
Returns the path to the portal's "public" folder. If $path is specified, a path is created and returned starting from the "public" folder.
vtePath()
$path = ''
Returns the path to the vtenext folder. If $path is indicated, a path is created and returned starting from the vtenext folder.
sdkPath()
$path = ''
Returns the path to the portal's "sdk" folder. If $path is indicated, a path is created and returned starting from the "sdk" folder.
sdkAssetsPath()
$path = ''
Returns the path to the portal's "public/assets/sdk" folder. If $path is specified, a path is created and returned starting from the "public/assets/sdk" folder.
publicRelPath()
$assetPath
Returns a relative path starting from the "public" folder.
Examples
SDK view
Here is an example of how to change the visibility of portal fields via view SDK.
<?php
require('../../config.inc.php');
chdir($root_directory);
require_once('include/utils/utils.php');
require_once('vtlib/Vtecrm/Module.php');
$Vtiger_Utils_Log = true;
global $adb, $table_prefix;
VteSession::start();
$module = '';
$src = '';
$mode = 'constrain';
$success = 'continue';
SDK::addView($module, $src, $mode, $success);
<?php
global $sdk_mode, $table_prefix;
switch ($sdk_mode) {
	case 'portal.create':
		$readonly = 100;
		$success = true;
		break;
	case 'portal.edit':
	case 'portal.detail':
		if ($col_fields['field3'] !== 'Open') {
			$readonly = 99;
			$success = true;
		}
		if (in_array($fieldname, ['field1', 'field2'])) {
			$readonly = 100;
			$success = true;
		}
		break;
}
Creating a new controller
Here is an example of how to create a new controller to manage a custom action.
Edit "config/sdk.config.php" to insert a new controller
return [
	'sdk_controllers' => [
		'SampleVte' => 'controllers/SampleVteController.php',
	]
];
Implement the SampleVteController class in "sdk/controllers/SampleVteController.php"
<?php
class SampleVteController extends \app\controllers\BaseController {
	
	public function index($request) {
		return $this->displaySomething($request);
	}
	protected function displaySomething($request) {
		$parameter1 = $request->get('parameter1', true);
		$parameter2 = $request->get('parameter2', true);
		$this->viewer->assign('PARAMETER1', $parameter1);
		$this->viewer->assign('PARAMETER2', $parameter2);
		$layout = \app\LayoutFactory::getPortalLayout($this->viewer, $this->client, $request);
		$output = $this->fetchWithLayout('sdk/SampleVte.tpl', $layout);
		return new \app\Response($output);
	}
	
}
Create a new template in "resources/templates/sdk/SampleVte.tpl"
{extends file='layouts/PortalLayout.tpl'}
{block name=content}
	<h1>Sample Vte</h1>
{/block}
Modify "config/sdk.config.php" indicating the sdk file for inserting the new entries in the side menu
return [
	'sdk_controllers' => [
		'SampleVte' => 'controllers/SampleVteController.php',
	],
  	'sdk_menu' => [
		'samplevte-menu.php',
	]
];
Edit the "sdk/samplevte-menu.php" file
<?php
return [
	[
		'text' => trans('SampleVte'),
		'active' => false,
		'prefix' => [
			'type' => 'icon',
			'icon_style' => 'material',
			'icon_name' => 'pie_chart',
		],
		'action' => [
			'type' => 'link',
			'link_href' => "index.php?action=SampleVte",
		],
	],
];
Extending a module
Here is an example of how to extend the functionality of a module.
Edit "config/sdk.config.php"
return [
	'sdk_module' => [
		'Contacts' => 'modules/ContactsModule.php',
	]
];
Implement the ContactsModule class in "sdk/modules/ContactsModule.php"
<?php
class ContactsModule extends \app\PortalModule {
	
	public $hasComments = true;
	public $formColumns = 2;
	public function canAddComments() {
		return true;
	}
	
}

Migration to PHP ≥ 8.3

What's new in PHP 8.3+
Minimum PHP version updated
The application now requires PHP 8.3 as the minimum version.
Database updates
Starting from version 26.01 of vtenext, some important database changes took place, such as:
Charset and Collation
Database is now using by default the charset utf8mb4 and collation utf8mb4_general_ci, which allows to store and display all possible UTF-8 characters, including emojis and special symbols.
BIGINT columns
All columns containing crmids are now BIGINT (for new installations). When creating columns that store crmid, always use BIGINT (or I8 when using adodb datadict). This is necessary to support a large number of records (more than ~2 billion).
TINYINT columns
All columns containing only boolean integers (e.g., 0/1 for checkbox fields uitype 56, deleted=0/1, or various status columns with only small integers) are now TINYINT (or I1 when using adodb datadict). This change will slightly reduce the database size and query execution time in certain cases.
During the update (step 3019 - 3020) a script to automatically convert the charset and the integer columns is executed (modules/Update/changes/bigint_utf8mb4_alter.php). Since this change can take a very long time, bigger tables (modules with more than 100k records, or tables bigger than 5GB) are excluded and should be converted manually at the end of the update (or at an appropriate time to minimize downtime, like at night). The script will output if this is necessary or if all tables have been converted. In case the manual conversion is necessary, it can be done in 2 ways:
By running the queries in file: modules/Update/changes/bigint_alter_big.sql
For extremely large tables, by executing the script: modules/Update/changes/percona_alter.sh, to use the pt-online-schema-change tool (installation to be done separately) to avoid locking the tables. It is advised to review this script and manually add the necessary command line option to the Percona command.
Licence tag
A new SPDX license tag is now mandatory in all vtenext core files (php, js and tpl) that are not external libraries
/*************************************
 * SPDX-FileCopyrightText: 2009-present Vtenext S.r.l. Società Benefit 
 * SPDX-License-Identifier: LicenseRef-vtenext-business-license 
 ************************************/
New configuration files
All new custom configuration files (e.g., custom api token) must be placed in the 
/config folder. If some configuration is different from dev/prod environments, use the corresponding config file in 
/config/.
config.$envType.php: Overrides of PHP configuration and global variables for a specific environment type
phpstan/*: Configuration to use when running phpstan
smarty/*: Configuration directory for Smarty library
logging.php: Configuration for the legacy logs (old log4php)
request.config.php: See Configuration - RequestHandler
New config variables
The following global config variables have been added to 
config.inc.php:
$enableLegacyLogs
Activate the legacy logs (the old log4php), which logs a lot of stuff in 
logs/vtenext.log. The usefulness of this log is not certain. Configuration of this log is in 
/config/logging.php.
$envType
Specify the environment type. Possible values:
"prod" (default)
"preprod"
"dev"
"debug"
Each level activates more error messages. The default configuration is in 
config.inc.php, variable 
$php_config, overridden by the specific 
/config/config.$envType.php file.
$php_config
Used to specify the default PHP configuration used by 
$envType.
$smarty_warn_superglobals
A global configuration flag that enables warnings when PHP superglobals are used in Smarty templates. When enabled, the system registers a Smarty POST filter that scans compiled template code for superglobal usage ($_GET, $_POST, $_REQUEST, $_COOKIE, $_SERVER, $_ENV). 
By default it is enabled only in development environments.
New functions and methods
RequestHandler (RH)
RequestHandler provides secure, centralized access to HTTP request data. It automatically sanitizes input using 
vtlib_purify() and applies type-safe filters from 
F:: enum. This replaces direct access to superglobals like 
$_REQUEST, 
$_GET, 
$_POST, etc.
For details, see: RequestHandler
BaseAction
We have moved the route resolution logic to the new 
IndexRouter class and introduced the 
BaseAction class to handle actions.
For details, see: Routing system.
CRMEntity methods update
The old 
retrieve_entity_info has been renamed to 
retrieve_html and 
retrieve_entity_info_no_html to 
retrieve. This is to make more explicit that there is a HTML conversion going on. The old functions are deprecated, but still working.
CRMEntity::retrieve_html(): Retrieves entity with HTML conversion
CRMEntity::retrieve(): Retrieves entity without HTML conversion
Translation aliases
New convenient aliases for translation functions:
trans(): Alias for 
getTranslatedString()
trans_app(): Alias for 
getTranslatedString($str, "APP_STRINGS")
trans_js(): Alias for 
getTranslatedString($str, "ALERT_ARR")
New database query methods
New database query methods are available. For details, see: Database Best Practices
Autoescaping of all Smarty variable
All variables outputted by Smarty templates (for example 
{$VARIABLE}) by default have all applicable characters are converted to html 
&...; notation.
The reason of this change is to reduce as much as possible exposure to XSS attacks, since it was extremely difficult to track every possible variable in templates and ensure it was properly escaped.
For details, see: Escape.
New tools
tools/change-env
Allows to easily change the 
$envType variable in 
config.inc.php and automatically execute scripts afterwards. Scripts to execute are located in 
plugins/envs/make.$env/*.
tools/activate
Prefix the absolute path of 
tools/ to 
PATH.
tools/analyze
Install and run phpstan on the vte root directory, or a custom one.
tools/tests
Install and run phpunit with vte unit and functional tests.
tools/change-color
Simple script to change color of vtenext using some standard values. The purpose is to quickly change color of an environment to differentiate it from the production.
tools/compile-themes
Compile scss sources of all (or some) known vte themes. Can monitor for file changes and trigger a compilation automatically (
inotifywait must be installed).
Important files and folders
cache_local/
This folder should contain cache files to be stored on the same host as the webserver. It is useful in case of cluster deployment, where the 
cache/ folder is normally shared, but 
cache_local/ is only local.
plugins/envs/
Contains the scripts to be executed when changing the environment type (e.g., after cloning the prod in the dev host).
GDPR portal update
The GDPR portal has been significantly updated with the following improvements:
Migration of libraries to Composer for better dependency management
API calls switched to vtenext RestClient
Smarty templating engine upgraded to the latest version (^5.4)
Libraries
All third party libraries now must be imported via composer (living in the 
/vendor/ folder), or placed in the 
/vendor-extra/ folder. Libraries in vendor-extra are automatically included in the class path.
Important note
Whenever a new library is added, either via composer or manually, the following command must be executed to refresh the classmap: 
composer optimize → alias of 
composer dump-autoload -o
Libraries updated
Library
Previous version
New version
phpmailer/phpmailer
6.6.5
6.9.3
ezyang/htmlpurifier
4.13.0
4.18.0
mpdf/mpdf
8.0.12
8.2.5
league/iso3166
2.1.5
4.3.2
league/oauth2-client
2.7.0
2.8.1
league/oauth2-google
3.0.4
4.0.1
spomky-labs/otphp
10.0.3
11.3.0
joypixels/emoji-toolkit
6.6.0
9.0.1
phpoffice/phpspreadsheet
1.8.2
4.1.0
phpseclib/phpseclib
3.0.42
3.0.43
jaybizzle/crawler-detect
1.3.0
1.3.4
hubspot/hubspot-php
2.0
5.3.0
slince/shopify-api-php
2.5
3.1.0
zendesk/zendesk_api_client_php
2.29
4.1.0
lesstif/php-jira-rest-client
1.41
5.9.0
Libraries added
web-token/jwt-library 4.0.4
php-amqplib/php-amqplib 3.7.3
econea/nusoap 0.9.18
dragonmantank/cron-expression 3.4.0
qualityunit/tnef-decoder 1.2.9
sabre/vobject 4.5.6
flipboxdigital/oauth2-hubspot 1.0.1
stevenmaguire/oauth2-salesforce 2.0.1
stevenmaguire/oauth2-zendesk 2.1.0
automattic/woocommerce 3.1.0
monolog (replaces log4php) 3.8.1
Libraries moved to vendor
The following libraries have been moved from various locations to 
/vendor/ or 
/vendor-extra/:
include/nusoap → moved to vendor
modules/Settings/ProcessMaker/thirdparty/cron-expression → moved to vendor
modules/PDFMaker/classes/simple_html_dom.php → moved to vendor-extra
include/magpierss → moved to vendor-extra
modules/Morphsuit/utils/RSA → replaced with phpseclib in vendor
modules/Newsletter/bounce_driver.class.php → moved to vendor-extra
modules/Messages/src/attachment_tnef → moved to vendor
modules/Messages/src/Squirrelmail.php → moved to vendor-extra
smartoptimizer → moved to vendor-extra (updated minifier, added brotli and zstandard support)
portal/nusoap → moved to vendor
portal/include/htmlpurifier → removed
include/pChart → patched and moved to vendor-extra
include/freetag → fixed and moved to vendor-extra
modules/VteSync/vendor → moved to main vendor
modules/VteSync/VteSyncLib/src/Connector/Jira/vendor → moved to main vendor
Removed libraries
log4php - Replaced with monolog
vtlib/SimplePie - No longer used
include/antlr - Not working, grammar unknown and never used
modules/SDK/examples/intellisense - Removed
modules/Calendar/iCal/ical-parser-class.php - Not used
modules/Calendar/iCal/iCalendar_*.php (Bennu) - Replaced with sabre/vobject
modules/Calendar/iCal/iCalcreator - Replaced with sabre/vobject
include/Zend - Simplified
gdpr/include/vtwsclib/lib/Zend - Removed
portal/include/Zend - Removed
videlalvaro/php-amqplib - Replaced with php-amqplib/php-amqplib
web-token/jwt-easy, 
web-token/jwt-key-mgmt, 
web-token/signature-pack - Replaced with web-token/jwt-library
Zend framework replaced with Laminas
The Zend framework (a discontinued project) has been completely removed and replaced with Laminas, its official successor. All Zend dependencies have been migrated to the Laminas equivalents.
Removed config variables
The following config variables have been removed:
$display_empty_home_blocks
Never used.
$php_max_execution_time
Misleading, since it was applied only in a few cases, and it was always 0, setting the limit to unlimited. Replaced by the 
$php_config and specific overrides when needed (e.g., Report export, PDF generation).
Removed files and folders
modules/Dashboard
Removed this module, already deprecated and inactive.
PEAR.php
Used only by Dashboard and by obsolete libraries.
Image/*
Used only by Dashboard module.
include/db_backup
Not used anymore (and also a bad idea to make a full backup on admin logout).
Smarty/templates_c
Moved to 
cache_local/smarty to keep the number of writable folders limited.
plugins/erpconnectorDir
This folder was used in the past to perform one/two-way synchronizations with external systems. It has now been replaced by the Data Import.
Old portal (v1)
The previous version of the portal is no longer supported. You can choose one of the following options:
Switch to the new Business Portal
Go to Settings → Business Portal to start using the updated version.
Restore the portal from the upgrade backup
You may restore the previous version from the upgrade backup and update it to ensure compatibility with the new system. Please note that the old portal is not compatible with PHP 8. For more information and assistance, contact support.

New developers features in vtenext 26.04
In version 26.04 some additional changes have been made to the code to simplify the development of custom functionalities in certain areas.
View the developer release notes.

Mailscanner (Mail Converter)
Several Mailscanner classes are now exendable via 
SDK::setClass:
MailScanner
MailScannerAction
MailScannerInfo
MailScannerRule
MailScannerSpam
MailScannerMailBox
MailScannerMailBoxZend
This allows for easier extensibility using our standard SDK.
Moreover, the list of actions (for example: Create Ticket, Update Ticket, ...) for each rule is now stored in the database and not hardcoded in various files. So adding a new a new action is much easier now:
require_once 'modules/Settings/MailScanner/core/MailScannerAction.php';
// extend the class to implement custom actions
SDK::setClass('MailScannerAction', 'MailScannerActionCustom', 'modules/SDK/src/CUSTOMER/MailScannerActionCustom.php');
// add an action to create a task (custom module, or even Calendar)
MailScannerAction::addActionType('CREATE,Task,FROM', 'LBL_CREATE_MS_TASK', 'createTask');
// add the label
SDK::setLanguageEntries('Settings', 'LBL_CREATE_MS_TASK', ['it_it' => 'Crea compito', 'en_us' => 'Create task']);
And the extended class in 
MailScannerActionCustom.php:
<?php
class MailScannerActionCustom extends MailScannerAction {
	
	/**
	 * Example of a custom mailscanner action
	 */
	function createTask($mailscanner, $mailrecord, $regexMatchInfo, $compare_parentid, $match_field) {
		
		$subject = $mailrecord->_subject;
		$description = $mailrecord->getBodyText();
		
		// create a record "CustomTask"
		$inst = \CRMEntity::getInstance('CustomTask');
		$inst->mode = '';
		
		// populate some fields
		$inst->column_fields['taskname'] = $subject;
		$inst->column_fields['description'] = $description;
		$inst->column_fields['date'] = date('Y-m-d');
		// save it
		$inst->save('CustomTask');
		
		// Associate any attachement of the email to the record
		$this->__SaveAttachements($mailrecord, $inst->modulename, $inst, $inst);
		
		// create the Messages record
		$this->__CreateNewEmail($mailrecord, $this->module, $inst);
		// return the record id, to signal the correct application of the action
		return $inst->id;
		
	}
	
}
Will produce a new action in the rule:

Record conversion
In the latest version the handling of record conversion (converting a Quote to a SalesOrder, or a SalesOrder to an Invoice...) has been generalized and centralized in a single place.
The list of available conversion modes is now in the table 
vte_convertmodes, which is managed by the class 
ConvertModesUtils which gives the possibility to add new conversion modes between standard or custom modules.
For example, to add a new conversion mode from module Quote, to a custom module (with products) PreOrders:
// instantiate the utils class
$CMU = ConvertModesUtils::getInstance();
// define the mapping for the fields, key is destination field, value is the source
$mapping = [
    // dest => source
	'vcf_1_1' => 'subject',
	'description' => 'description'
];
// add the convert mode
$CMU->addConvertMode(
	'quotetopreorder', 			// unique name for the mode, can be any string
	'Quotes',					// starting module
	'PreOrders',				// second module
	'record',					// the parameter in request that will pass the record id. by default "record"
	'quoteid',					// name of the field in the destination module with a reference to the first module, can be null
	true,						// if true, show the button in Quotes
	5,							// sequence of the button, among other conversion buttons
	'handleRecordConversion',	// the method to handle the conversion. You can specify a different one in the PreOrders class if you wish to do something different
	$mapping					// the fields mapping. products block is automatically copied
);
The button will appear automatically:

CSV Import
One of the classes used by the standard CSV Import (the one available from any module's ListView) has been made extendable via 
SDK::setClass and highly refactored to split the main method into smaller ones, to ease modification of specific behaviours:
Import_Data_Controller: This class now can be extended
For example, if you need to modify a value of a specific field before being saved to the database:
// register the class
SDK::setClass('Import_Data_Controller', 'Import_Data_ControllerCustom', 'modules/SDK/src/CUSTOMER/ImportCustom.php');
And the class:
<?php
require_once('modules/Import/controllers/Import_Data_Controller.php');
class Import_Data_ControllerCustom extends Import_Data_Controller {
	
	/**
	 * Transform a single field value to a format suitable to vte
	 */
	protected function transformFieldValue($fieldName, $fieldValue, $fieldInstance, $moduleMeta) {
		$fieldValue = parent::transformFieldValue($fieldName, $fieldValue, $fieldInstance, $moduleMeta);
		
		if ('Accounts' === $this->module && 'accountname' === $fieldName) {
			// append "IMPORTED" to the accountname
			$fieldValue .= ' - IMPORTED';
		}
		
		return $fieldValue;
	}
}

VteSync (synchronizations)
VteSync connectors are now extendable via 
SDK::setClass so it's easier to add new functionalities or changing the field mapping.
For example, to extend the WooCommerce connector:
// classes are namespaced
SDK::setClass('VteSyncLib\Connector\WooCommerce', 'WooCommerceCustom', 'modules/SDK/src/CUSTOMER/WooCommerceCustom.php');
The extended connector:
<?php
// namespace the class, so it's easier to work with it
namespace VteSyncLib\Connector;
class WooCommerceCustom extends WooCommerce {
	// extend the constructor to alter the standard models
	public function __construct($config = array(), $storage = null) {
		parent::__construct($config, $storage);
		// in this case, the class is in the same folder, so no need to include it
		$this->classes['Accounts'] = array(
          'module' => 'Accounts',
          'commonClass' => 'VteSyncLib\Model\CommonRecord', 
          'class' => 'VteSyncLib\Connector\WooCommerce\Model\AccountCustom'
        );
	}
	
	// or it's possible to redefine any existing method
}
And the custom model:
<?php
// include parent class
require_once(__DIR__.'/Account.php');
namespace VteSyncLib\Connector\WooCommerce\Model;
class AccountCustom extends Account {
	// here I redefine the mapping, removing or adding fields
    protected static $fieldMap = array(
        // WooCommerce => CommonRecord
        'email' => 'email',
        'username' => 'name',
        //'phone' => 'phone', // THIS IS COMMENTED
        // billing address
        'address' => 'billingstreet',
        'city' => 'billingcity',
        'postcode' => 'billingpostalcode',
        'state' => 'billingstate',
        'country' => 'billingcountry',
        'companybill' => 'companybill',
        'firstnamebill' => 'firstnamebill',
        'lastnamebill' => 'lastnamebill',
        // shipping address
        'address_shipping' => 'shippingstreet',
        'city_shipping' => 'shippingcity',
        'postcode_shipping' => 'shippingpostalcode',
        'state_shipping' => 'shippingstate',
        'country_shipping' => 'shippingcountry',
        'companyship' => 'companyship',
        'firstnameship' => 'firstnameship',
        'lastnameship' => 'lastnameship',
		
		'otherfield' => 'otherfield', // THIS IS CUSTOM!
		// you probably also need to alter the VTE models, to connect this mapping to vte's one
    );
    
	// this function is called to prepare the array to be sent to woocommerce
    public function toRawData($mode) {
		$raw = parent::toRawData($mode);
		
		// when sendind data to woo, hardcode this additional field:
		$row['somefield'] = 'somevalue';
		
		return $raw;
	}
}

ListView extendability
The 
ListViewController class has been refactored to be more perfomant and easily extendable.
For example, now it's much easier to add new icons into the Actions column:
SDK::setClass('ListViewController', 'ListViewControllerCustom', 'modules/SDK/src/CUSTOMER/ListViewControllerCustom.php');
And the class:
<?php
require_once('include/ListView/ListViewController.php');
class ListViewControllerCustom extends ListViewController {
	
	/**
	 * Generate an array of strings to be concatenated and set as the "action" column
	 */
	public function generateActions($focus, $recordId, array $sqlrow = [], $navigationInfo = []) : array {
		$actionLinkInfo = parent::generateActions($focus, $recordId, $sqlrow, $navigationInfo);
		
		$module = $focus->modulename;
		if ($module === 'Leads') {
			// add an icon to each lead to open the record in the erp:
			$actionLinkInfo[] = "<a href=\"https://myerp.example.com/lead/$recordId\" target=\"_blank\"><i class=\"vteicon\"'>open_in_browser</i></a>";
		}
		
		return $actionLinkInfo;
	}
	
}
Resulting in:

Webforms
The class WebformCapture is now extendable via 
SDK::setClass and the main method 
captureNow has been split up in several methods to facilitate modification of specific behaviours.
For example, to force the value of a field with a dynamic value upon Lead creation:
First we have to register the extension:
SDK::setClass('WebformCapture', 'WebformCaptureCustom', 'modules/SDK/src/CUSTOMER/WebformCaptureCustom.php');
And then extend the prepareParameters method:
<?php
require_once('modules/Webforms/WebformCapture.php');
class WebformCaptureCustom extends WebformCapture {
	
	/**
	 * Read data from request and populate the necessary fields
	 */
	protected function prepareParameters(array $request, Webforms_Model $webform) : array {
        // call the parent method to fill the standard values
		$parameters = parent::prepareParameters($request, $webform);
        // populate the field with a random value (makeRandomString is just an example here)
		$parameters['my_field_random'] = makeRandomString();
		
		return $parameters;
	}
}

Code Review Skill
There is a skill available for use with AI agents built into your IDE that can help you adapt your code to the new release.
More details available in the documentation.
Once the analysis is complete, the files are checked and the original ones are overwritten, run 
tools/check-requests to check for any further references to the superglobal variable 
$_REQUEST. If none are found, the 
config/request.config.override.php file will be removed; otherwise, it will be updated, leaving only the new occurrences.
If you use a cloud agent, make sure your files do not contain sensitive data or credentials.

New developers features in vtenext 26.07

Worker
The Worker is a background daemon that runs alongside the CRM. When a task takes a long time (such as chatting with an AI, running a process, uploading documents for a vector search, etc..), the Worker handles it asynchronously so the browser does not freeze and the user can keep working. Also it allows the processing to be detached from the browser request, allowing the user to close the tab.
Architecture Overview
Default setup flows
The Worker is composed of three processes that work together:
1. Router
File: 
include/Services/Worker/Router.php
The Router listens on a Unix socket for incoming connections. When a browser tab or a PHP script connects, the Router identifies who is connecting, decides what to do with the request, and forwards it to the Zygote. It also keeps track of all connected clients and can push live updates (Server-Sent Events) back to browsers.
Process name: 
vte-worker-router
Listens on a socket (Unix 
cache_local/worker.sock with systemd, or a TCP/IP address for cluster setups). The address is configured in 
config.inc.php via 
$worker_socket_URL and must match the path in the systemd socket unit.
Manages client subscriptions for real-time push events
2. Zygote
File: 
include/Services/Worker/Zygote.php
The Zygote manages a pool of worker processes. When the Router forwards a job, the Zygote forks a Consumer process to execute it. Up to 10 Consumers can run concurrently; if all are busy, the job is queued and executed when a slot becomes available.
Process name: 
vte-worker-zygote
Maximum concurrent Consumers: 
CONSUMERS_MAX = 10 (configurable in Zygote.php)
Queues excess jobs until a Consumer is free
3. Consumer
File: 
include/Services/Worker/Consumer.php
The Consumer is a short-lived process that performs the actual work. It connects to the database, loads the user context of the person who requested the task, executes the requested method, and terminates when done. Each Consumer handles exactly one job and then exits.
Process name: 
vte-worker-consumer
Process isolation: a crash in one Consumer does not affect others
Database connection is established fresh for each job
Communication Between Processes
The three processes communicate through Unix pipes created by 
stream_socket_pair(). This is faster and lighter than running a full HTTP server inside the Worker.
Client Types
There are two kinds of clients that can connect to the Worker:
Client
How It Connects
Used By
Web
Browser → Apache → 
modules/Utilities/Worker.php (handles web auth) → socket → Router
Browser tabs (AI chat, notifications). Each web connection holds an Apache process slot.
Script
PHP code → 
ScriptConnector → Unix socket → Router
CLI scripts, cron jobs, AJAX handlers
For Web Clients (SSE)
When a browser connects, the Router upgrades the connection to Server-Sent Events (SSE). This allows the Consumer to push data back in real-time — for example, streaming an AI response word by word, or sending a notification as soon as it is created.
Relevant files: 
WebConnector.php (client side), 
WebHandler.php (server side), 
SharedWorker.js (browser — optional, reduces connections to one per browser).
For Script Clients
PHP code connects using 
ScriptConnector with a 30-second I/O timeout (long enough for background tasks). The connection supports both synchronous calls (wait for response) and asynchronous calls (fire and forget).
Relevant files: 
ScriptConnector.php (client side), 
ScriptHandler.php (server side).
Internal Protocol
Messages use a simple text-based format over the socket. Each message is a JSON object with an event type (
init, 
call, 
return, 
error, 
event) and event-specific data. The 
ProtocolTrait handles encoding and decoding on both sides.
Available Tasks
These are the methods the Worker can execute. Consumer methods are registered in 
ScriptMethodsTrait (
Methods.php) and implemented in 
Consumer.php. Worker methods execute directly in the Zygote (no Consumer fork).
Method
Where Executed
Description
Definition
llmChat
Consumer
Send a message to an AI assistant (Agent, LLM, or External WebService). Streams the response back to the browser via SSE.
Consumer.php:162
elaborateRag
Consumer
Upload selected CRM documents to the external AI orchestrator and build the vector database for RAG retrieval.
Consumer.php:601
delegateProcess
Consumer
Execute a BPMN workflow process (ProcessMaker) in the background.
Consumer.php:118
resumeProcesses
Consumer
Resume queued workflow processes. Runs at Worker startup and on demand.
Consumer.php:133
sendToAll
Consumer
Push a custom event to a specific user or to all connected browser sessions.
Router.php:249
notifyNow
Consumer
Send a CRM notification to a specific user in real-time.
Router.php:321
workerStats
Zygote
Returns uptime, number of active consumers, queued jobs.
Zygote.php:219, Router.php:222
workerRestart
Zygote
Restarts the entire Worker (Router + Zygote + all Consumers). Works independently of how the Worker was started (systemd or direct CLI), but only if the Worker is actually running.
Router.php:239
Adding a New Task
To add a new job that the Worker can execute, two files must be modified. Because Consumer processes load PHP classes after being forked from the Zygote, changes to existing methods take effect on the next consumer start without restarting the Router or Zygote. Adding a brand new method requires registering it in the trait (step 2) and may need a full restart only for the trait to be recognized.
Step 1: Implement the method in Consumer.php
Add your method inside the 
//region Methods section of 
Consumer.php:
protected function convertPdf(int $documentId) {
    global $adb;
    // ... perform work ...
    $this->client->return($result);
}
Useful tools available inside the Consumer:
$this->client->return($data) — send a successful response
$this->client->error("message") — signal an error
$this->sendToAll(Roles::web, $userId, 'eventName', $data) — push a live event to browsers
$this->later(function() { ... }) — schedule work for the next event-loop tick
Step 2: Register the method in Methods.php
Add the method signature to 
ScriptMethodsTrait in 
Protocol/Methods.php:
/** @return void */
public function convertPdf(int $document_id) {
    return $this->call(__FUNCTION__, get_defined_vars(), ['wait' => false]);
}
Calling the new method
From PHP code (the connector is reused and reconnects on failure):
$conn = ScriptConnector::reuseInstance();
if ($conn->tryConnect()) {
    $conn->convertPdf(42);
}
From the command line:
php -f include/Services/Worker/run.php call convertPdf 42
A method that belongs in 
WorkerMethodsTrait instead (executed by the Zygote without forking a Consumer) uses the same two-step process: add the signature to the trait and implement it in 
Zygote.php or 
Router.php.
Broadcasting Events to the Browser
From any Consumer method, you can push real-time data to connected browsers:
// Send to a specific user
$this->sendToAll(Roles::web, $userId, 'myEvent', ['progress' => 50]);
// Send to ALL users
$this->sendToAll(Roles::web, 0, 'myEvent', $data);
On the browser side, events are received through 
SharedWorker.js and 
EventsClient.js. The Router maintains a tree of connected clients indexed by user ID, session ID, and instance ID, and dispatches events to the matching tabs.
Operation
Startup
The Worker can be started through systemd socket activation or directly from the command line for testing and custom setups:
php -f include/Services/Worker/run.php
With systemd socket activation:
systemd creates the Unix socket (
cache_local/worker.sock)
On the first connection, systemd launches 
php run.php
run.php loads the CRM environment (config, database, etc.)
Worker::start() calls 
pairedFork(), splitting into two processes:
The parent becomes the Router (socket listener)
The child becomes the Zygote (consumer pool manager)
When a job arrives, the Zygote forks a Consumer to execute it
The Consumer runs the job and exits
Installation
sudo tools/worker install
This copies the systemd unit files (
vte-worker@.service and 
vte-worker@.socket) to 
/etc/systemd/system/, enables the socket, and starts it. Both files are systemd templates that take the relative path from 
/var/www/html as the instance parameter (e.g. 
vte-worker@vte-agentic), which allows running multiple Workers for different VTE installations on the same machine.
If your installation differs from 
/var/www/html/PATH, you must edit the templates manually or with 
systemctl edit [--full] for both 
vte-worker@.socket and 
vte-worker@.service, before installing.
Commands
Command
Effect
tools/worker install
Install systemd units, enable and start the socket
tools/worker restart
Send a restart signal to the running Worker
tools/worker status
Print uptime, number of active consumers, queued jobs
Signals
Signal
Effect
SIGTERM / 
SIGINT
Graceful shutdown: stop accepting new connections, wait for all running Consumers to finish, then exit.
SIGHUP / 
SIGUSR1
Reload.
Configuration
Setting
Location
Maximum concurrent Consumers
Zygote.php:29 — CONSUMERS_MAX
Socket path
systemd/vte-worker@.socket — ListenStream
config.inc.php — $worker_socket_URL
Log file
logs/worker.log
Troubleshooting
Enable Logging
Logging is disabled by default. To enable it, set the static flag before starting the Worker:
\Vtenext\Services\Worker\Worker::$enableLog = true;
Logs are written to 
logs/worker.log. The log format includes a timestamp, the process role, and the message.
In the browser, put the console verbosity to debug.
Common Issues
Symptom
Likely Cause
Connection refused
The Worker is not running or the socket path is incorrect. Check 
$worker_socket_URL in 
config.inc.php and verify the socket file exists.
Consumer not starting
Process limit reached (
RLIMIT_NPROC). The Worker attempts to raise it to 1000 + 
CONSUMERS_MAX at startup.
Job queued but never runs
All 10 Consumer slots are occupied by long-running tasks. Check 
tools/worker status for current usage.
SharedWorker
When the SharedWorker is active, debugging can done differently on each browser:
Firefox — visit about:debugging#workers (copy-paste) and press Debug then you can inspect everything and put breakpoints. Logs are also shown in the first tab that spawned the SW (yes 3 times, it's a browser bug as of June 2026) and in the browser console in multi-process mode (Ctrl+Shift+J).
Chrome — visit chrome://inspect/#workers (copy-paste) and press Inspect, same story. No logs are shown in individual tabs.
Technical limitations
Browsers connections cap
Web connections from browser tabs rely on Server-Sent Events (SSE), which keep a long-lived HTTP connection open to the server. Browsers enforce a hard limit of 6 concurrent connections per domain (HTTP/1.1). The 
SharedWorker.js script multiplexes all tabs through a single SSE connection per browser, bypassing the limit entirely.
When SharedWorker is not available (older or unsupported browsers), each tab opens its own SSE connection and the 6-connection limit per domain applies.
The Worker's 
CONSUMERS_MAX limit (10 concurrent Consumer processes) is a separate server-side concern — it caps how many jobs run in parallel, not how many connections can remain open.
Disconnection detection
TCP provides no built-in notification when a peer disconnects. To detect that a browser has closed the connection, PHP must attempt to write to the socket. The 
WebConnector handles this by writing a newline to the output buffer at every read tick (default every 10 seconds). This means a disconnected browser may not be detected for such time, and an inactive but still connected tab generates a small amount of periodic traffic.
Xdebug and 
set_time_limit
When the Xdebug extension is loaded, 
set_time_limit(0) (which normally removes the execution time limit) does not work reliably. Xdebug overrides PHP's internal timer and enforces its own 
xdebug.max_nesting_level and related constraints, which can cause long-running Consumer methods to be terminated prematurely on development environments where Xdebug is active. If the Worker behaves unexpectedly during development, disable Xdebug or set 
xdebug.mode=off in the PHP configuration.
File Reference
include/Services/Worker/
├── run.php                          # Entry point (called by systemd)
├── Worker.php                       # Base Worker class + WorkerTrait
├── Router.php                       # Socket listener, client registry, SSE push
├── Zygote.php                       # Consumer pool manager, job queue
├── Consumer.php                     # Task executor (llmChat, elaborateRag, etc.)
├── utils.php                        # shared utilities
├── Protocol/
│   ├── Protocol.php                 # Wire protocol encode/decode
│   ├── Roles.php                    # Role constants (router, zygote, consumer, etc.)
│   ├── Methods.php                  # Method traits (ScriptMethods, WebMethods, WorkerMethods)
│   ├── ClientHandler.php            # Server-side connection handler
│   ├── BaseConnector.php            # Client-side connector base class
│   ├── ScriptHandler.php            # Handler for script connections
│   ├── ScriptConnector.php          # Connector for PHP scripts
│   ├── WebHandler.php               # Handler for web connections (SSE)
│   └── WebConnector.php             # Connector for browser (HTTP to socket bridge)
├── systemd/
│   ├── vte-worker@.service          # systemd service template
│   └── vte-worker@.socket           # systemd socket template
├── SharedWorker.js                  # Browser SharedWorker (multiplexes connections)
├── TabClient.js                     # Client for tab-to-tab messaging
└── EventsClient.js                  # Event subscription client in the browser
modules/Utilities/Worker.php         # HTTP bridge (Apache -> Worker socket)
modules/Settings/WorkerConfig.php    # Admin settings panel
modules/Settings/WorkerConfig.tpl    # Smarty template for the admin panel
tools/worker                         # CLI management tool
logs/worker.log                      # Log file

Agent orchestrator
Python service providing AI agent chat, MCP tools integration, and RAG on CRM documents, based on FastAPI and LangChain. Called by the Worker's Consumer processes via HTTP/SSE.
System Requirements
llama-cpp-python is installed as a pre-built wheel (not compiled from source). 
requirements.txt specifies the Vulkan variant via 
--extra-index-url https://abetlen.github.io/llama-cpp-python/whl/vulkan. Other backends are available by changing the index URL:
Backend
Index URL suffix
cpu
.../whl/cpu
vulkan (default)
.../whl/vulkan
cuda
.../whl/cuda
rocm
.../whl/rocm
metal
.../whl/metal
sycl
.../whl/sycl
Docker image installs 
libvulkan1. GPU access (
/dev/dri) is commented out in 
compose.yaml by default — uncomment for hardware acceleration. Falls back to CPU without GPU.
The x86-64-v2 baseline or equivalent is required by NumPy's pre-built wheels (see NumPy SIMD build options). CPUs without these instructions can still run the orchestrator by recompiling NumPy from source with reduced SIMD flags (
NPY_DISABLE_CPU_FEATURES), or by using a distro that ships a compatible build.
Architecture Summary
Three layers:
PHP CRM (Worker Consumer), calls orchestrator via HTTP
Python orchestrator (FastAPI, Docker)
LLM / MCP servers / Chroma
Relevant Consumer methods: 
llmChat() (chat relay), 
elaborateRag() (document upload + vector build).
RAG Document Building
Indexing (Elaboration)
Triggered on agent save with the Documents feature enabled. The Worker Consumer:
Reads selected CRM Documents
POST /rag/keep — prunes stale docs from orchestrator
POST /rag/upload — uploads new/changed files (multipart), stored as 
docs/shared/<md5>.<ext>, symlinked into 
docs/<agent_id>/
POST /rag/build — indexes all docs for the agent into Chroma at 
vectors/<agent_id>/.
Querying (Runtime)
With 
rag: true in 
/agent/run, Python injects a 
query_documents tool. The LLM decides when to call it. The orchestrator decomposes the question into ≤3 sub-questions (
needs_retrieval flag), queries Chroma per sub-question, deduplicates by 
doc_id, re-ranks with FlashRank, and returns context. The LLM answer is grounded strictly in retrieved context.
Python Orchestrator Endpoints
The Python service exposes the following REST endpoints. All are mounted on the FastAPI app at port 8120.
Endpoint
Description
POST /agent/run
Agent loop: LLM + MCP tools + guardrails + optional RAG. SSE or JSON.
POST /tools/inspect
Introspect MCP server tools.
POST /rag/build
Index documents for an 
agent_id into Chroma.
POST /rag/run
Query vector store with question decomposition.
POST /rag/keep
Prune agent's doc symlinks to match 
{filename: md5}.
POST /rag/upload?agent_id=
Upload file to shared pool + symlink into agent's dir.
Installation & Configuration
Docker Setup
The Python orchestrator runs in Docker. Quick start:
cd plugins/agent
docker compose up -d --build
Verify: 
curl http://localhost:8120/docs should show the Swagger UI.
Port 
127.0.0.1:8120:8120 — bound to localhost only, MUST NOT be publicly exposed.
Here a quick snippet to install docker.
Environment Variables
Variable
Default
HOST (inside the container)
0.0.0.0
PORT
8120
EMBED_MODEL
nomic-ai/nomic-embed-text-v2-moe-GGUF:Q8_0
RERANK_MODEL
ms-marco-MiniLM-L-12-v2
HF_CACHE_DIR
/app/hf_cache
DOCUMENTS_DIR
/app/docs
VECTORS_DIR
/app/vectors
Troubleshooting
Startup slow: Embedding model downloads from HuggingFace on first container start — cached in 
HF_CACHE_DIR afterwards.
Empty docs: 
/rag/build raises 
RuntimeError if 
docs/<agent_id>/ is empty.
GPU not used: Uncomment 
/dev/dri in 
compose.yaml. Falls back to CPU otherwise.
CPU compat: Verify with 
/lib64/ld-linux-x86-64.so.2 --help
File Reference
plugins/agent/
├── compose.yaml
├── app.Dockerfile
├── requirements.txt
├── src/vte_agent/
│   ├── __main__.py
│   ├── config.py
│   ├── schemas.py
│   ├── agent.py              # /agent/run, /tools/inspect, calculator + rag tools
│   ├── rag.py                # /rag/* endpoints
│   ├── docs.py               # doc loaders
│   ├── models.py             # GGUFEmbeddings
│   ├── user_manual.py        # builtin vtenext user manual search tool
│   └── utils.py
├── docs/          
│   ├── shared/        # <md5>.<ext> — deduplicated by content hash
│   └── <agent_id>/    # symlinks → ../shared/<md5>.<ext>
└── vectors/
    └── <agent_id>/    # chroma.sqlite3, parent_docs.json, description.txt
cache_local/
└── huggingface/       # local models cache (embedding, rerank)

SDK uitypes in ProcessMaker actions
SDK uitype fields will all be shown as uitype 1 (text) in ProcessMaker actions

New properties for processes
If you don't want to attach external dynamic form emails to the record you can set to 
false this prop:
modules.processes.dfe.save_cache_link
If you assign a dynaform to email or portal, you can force the assigned user of the record Processes with these props:
modules.processes.assigned_user_id.email
modules.processes.assigned_user_id.portal
With the value 
related_to, the process will be assigned to the owner of the linked record. Alternatively, you can enter the ID (int) of a user/group.

Stronger hashing algorithm for user password
Replaced the password hashing algorithm from md5 to argon2id.

REST Webservice Methods
Below are all the SDK functions for registering REST methods and all the properties for describing them according to the OpenAPI standard.
Registering custom Webservice methods
SDK::setRestOperation($name, $handlerFilePath, $handlerMethodName, $params, $permission, $mcpSupport, $info)
$name: method name called by REST webservice;
$handlerFilePath: file path where the function is defined;
$handlerMethodName: name of the function to use from the specified handler file;
$params: if provided, the associative array of parameter names with their definition (see Parameter Definition (
$params))
$permission (since vtenext 23.08): one of “read”, “write” or “readwrite”, describing the kind of operation of this webservice. Used with additional accesskey for the users;
$mcpSupport (since vtenext 26.XX): default to 0 ("disabled"), it can be set to 1 to allow the registration of this method to MCP servers;
$info (since vtenext 26.XX): additional information used to display this method, for documentation purposes (see 
$info Parameter)
This function returns the id of the new Webservice if created successfully, and 
false otherwise.
SDK::setRestOperationInfo(string $name, array $operation = [], array $parameters = [], array $requestExamples = [], array $responses = [])
This function takes 
name as the name of the operation, and the arrays described in the 
$info Parameter section.
This function returns 
true if the webservice information was set up successfully, and 
false otherwise.
Parameter Definition (
$params)
The 
$params function parameter is an associative array that has the name of the parameter as the key, and the type of the parameter.
The types currently registered are 
string, 
encoded, 
datetime, 
double, and 
boolean.
$info Parameter
The 
$info parameter of the function 
SDK::setRestOperation is an optional associative array that has the keys 
operation, 
parameters, 
examples, and 
responses.
This parameter is passed accordingly to 
SDK::setRestOperationInfo, which is the method responsible for filling the information for the Webservice method that is needed for the OpenAPI specification and the MCP servers.
operation Key
The 
operation key is an associative array with two optional keys:
description: 
string, human-readable description of the Webservice method;
tags: 
string, comma-separated list of tags to group the Webservice method with;
tool_description: 
string, specific description for MCP tool, if not provided the MCP server will default the generic description.
parameters Key
The 
parameters key is an associative array with the name of the parameter as the key, and an associative array that contains additional information, used for documentation purposes.
This information is used to further document the parameters of the webservice method, while still adhereing to the conventions already established with the use of this SDK.
The associative array for the parameter has the following, optional, attributes:
required: 
bool, whether the parameter is mandatory;
default_value: 
mixed, the default value of the parameter;
description: 
string, informative description of the parameter;
example: 
mixed, an example value of the parameter;
extra: associative 
array, complementary information that can't be documented otherwise.
The 
extra attribute has the following, optional, attributes, that are of type 
string except when noted:
type: used, for example, when the parameter is registered as 
encoded, allowing it to "cast" it as 
object or 
array;
schema: the name of the schema definition that describes the structure of the parameter, used when 
type is 
object;
items: required when 
type is 
array, specifies the typing of the values contained in the array; it can be a simple type such as 
string, 
int, 
float, 
double, 
bool, or the name of a schema definition;
enum: 
array of values that are supported by the parameter.
examples Key
The 
examples key is a list of associative arrays that showcase a full usage of the webservice method. This key is optional.
The associative array of a single example has the following attributes:
name: 
string, the name chosen for the example;
summary: 
string, brief description of the example;
data: associative 
array, the payload passed by the request.
responses Key
The 
responses key is an associative array with the status code of the response as the key, and an associative array that contains the information of the response.
The associative array of a response has the following optional attributes:
description: 
string, brief description of the response;
fields: associative 
array, the payload of the response, see 
fields Attribute
examples: associative 
array, example representations of the response, see 
examples Key;
ref: 
string, predefined information of the response; if provided
fields Attribute
The 
fields attribute is an associative array with the name of the field as the key, and an associative array that contains the information of the field.
The associative array of a field has the following optional attributes:
type: the type of the field, can be 
string, 
int, 
float, 
double, 
bool, 
object, or 
array; if omitted, the field can be of "any" type;
schema: if 
type is 
object, the name of the schema definition that describes the structure of the field;
items: required when 
type is 
array, specifies the typing of the values contained in the array; it can be a simple type such as 
string, 
int, 
float, 
double, 
bool, or the name of a schema definition;
required: 
bool whether the field is mandatory;
default: 
mixed, the default value of the field;
description: 
string, informative description of the field;
example: 
mixed, an example value of the field;
enum: 
array of values that are supported by the field.
Deregistering custom Webservice methods
SDK::unsetRestOperation(string $name)
$name: name of the webservice method to delete.
This function returns 
true on success and 
false on failure.
SDK::unsetRestOperationInfo(string $name, string $type = 'all')
$name: name of the webservice method whose information need to be deleted;
$type: the type of information to delete, can be 
all, 
operation, 
parameters, 
requestExamples, 
responses, and 
responseExamples.
This function returns 
true on success and 
false on failure.
Registering custom Webservice schemas
SDK::setRestOperationSchema(string $name, array $fields)
$name: name of the schema;
$fields: see 
fields Attribute.
This function returns the id of the new webservice schema if created successfully, and 
false otherwise.
Deregistering custom Webservice schemas
SDK::unsetRestOperationSchema(string $name)
$name is the method name of the webservice schema to delete.
This function returns 
true on success and 
false on failure.

MCP Server
The Model Context Protocol (MCP) is a standard that enables AI applications to connect to external systems.
In VTENEXT, this standard is used to expose REST APIs, custom methods and processes to AI applications, to enable them to interact with the CRM itself.
Currently VTENEXT has set up a basic MCP server with the main webservices (referred here as tools) that ensure a wide extent of interoperability with the CRM; the user can choose to extend this server by registering other webservices to it, or even create custom tools to be used with the servers. Be mindful that, with roughly more than 30 tools registered to a MCP server, the accuracy of the server might degrade.
In VTENEXT, the MCP servers are fundamentally handled like any other webservice, but they have their own SDK methods to work with them.
Tools registered in the base MCP server (REST name: 
mcp)
create
delete
describe
listtypes
query
relate
retrieve
retrieveInventory
get_currencies
updateRecord
convert_lead
get_current_context
process_text
summarize
tools_manual
translate
SDK methods for working with MCP servers
Setting up a MCP server
SDK::setMcpServer(string $name, ?string $description = null, bool $isActive = true, ?string $operationName = null)
$name: the name of the MCP server;
$description: human-readable description of the MCP server;
$isActive: set it to 
false to prevent the Server from accepting requests;
$operationName: the name to give to the corresponding REST Webservice method; defaults to the name of the MCP server prefixed with 
mcp..
This function returns the id of the MCP server if created successfully, and 
false otherwise.
Registering a tool to a MCP server
SDK::registerMcpTool(string $mcp, string $name, string $type, ?string $tool_name = null, ?string $description = null)
$mcp: the name of the MCP server;
$name: the name of the webservice method or the custom MCP tool;
$type: the type of the tool to register, can be 
operation for the webservice method or 
custom for the custom tool;
$tool_name: the name to give to the registered tool, defaults to 
$name;
$description: MCP-specific information to give to the registered tool, will take precedence over any information provided by the webservice method/custom tool.
If successful, this function returns an array with the id of the MCP server and the id of the webservice method/custom tool; this function returns 
false on failure.
Defining a custom tool for MCP servers
SDK::setMcpTool(string $name, string $handlerFilePath, ?string $handlerMethodName = null, ?string $description = null, ?array $inputSchema = null, ?array $outputSchema = null)
$name: the name of the custom tool;
$handlerFilePath: file path where the function is defined;
$handlerMethodName: name of the function to use from the specified handler file;
$description: informative description of the custom tool;
$inputSchema: the definition of the function input;
$outputSchema: the definition of the function output.
$inputSchema and 
$outputSchema follow the JSON Schema 2020-12 Specification.
N.B.: the function should always return an array and, if possible, the array should be associative.
Removing a MCP server
SDK::unsetMcpServer(string $mcp, bool $force = false)
$mcp: the name of the MCP server to delete;
$force: if 
true, force the deletion of the MCP server, even if it has tools registered to it.
This function returns 
true on success and 
false on failure.
Deregistering a tool from a MCP server
SDK::unregisterMcpTool(string $mcp, string $name, string $type)
$mcp: the name of the MCP server;
$name: the name of the tool to deregister;
$type: the type of the tool to deregister, can be 
operation or 
custom.
This function returns 
true on success and 
false on failure.
Deregistering a custom tool
SDK::unsetMcpTool(string $name)
$name: name of the custom tool to delete.
This function returns 
true on success and 
false on failure.

Using AI
Tips on how to use LLM to help writing/refactoring vte code

Skill - Review SDK files
Skill to (try to) port all custom files to vtenext 26 and make them compatible with vtenext 26.
Install to Codex
Create the folder 
review-sdk-26 in the skills folder of your IDE example: [...]/.codex/skills/ with the structure:
review-sdk-26/
├── SKILL.md
└── agents/
    └── openai.yaml
SKILL.md
---
name: review-sdk-26
description: Revise the SDK code to be compatible with VTENEXT version 26.*
---
# review-sdk-26
Review and update PHP files (>=7.x) to make them:
* Compatible with PHP 8.3
* Aligned with the new project guidelines
* More robust, typed, and modern
You are working on existing VTENEXT application code. DO NOT rewrite everything from scratch.
Keep the original logic, improving it only where necessary.
The project's root directory is specified in the config.inc.php file in `$root_directory`. If that directory is not accessible, use the directory containing the file config.inc.php.
## When to use
This skill should be used after the update to VTENEXT 26.* to ensure that all custom PHP files are compatible with PHP 8.3 and follow the new project guidelines.
## Input
* List of PHP files, if they are not provided execute `tools/skills/review-sdk-26/get_php_customizations.php` to get the list of files to scan and review. The list of files should be provided in batches of 10 if there are too many files to review at once. Do not analyze, process or propose changes to files that are not in the list obtained.
## Instructions
### 1. Prepare the environment
* Create folder modules/SDK/src/review_sdk_26, if already exists empty it
* If there are too many files, run the skill on 10 files at a time (batch)
* If you are running a batch, do not empty the folder
* Also copies unpatched files to the review_sdk_26 folder to maintain the same structure and allow manual review of all files
* If you find any hardcoded credentials or sensitive data while analyzing the code, stop everything and let me know to move this data to a separate configuration file
### 2. PHP 8.3 Compatibility Analysis
* Fix weak comparisons as described [here](https://usermanual.vtenext.com/books/developers/page/weak-comparisons)
* Add type casts where necessary to functions that require a string as parameter, for example `trim()` `strpos()` and `stripos()`
* Do not replace functions if the existing ones work on PHP 8.3
* For empty string comparisons, replace with the `empty()` function
* For comparisons with `$mode` or `$sdk_mode`, the creation mode is usually checked and the empty string comparison is fine. `=== ''`
#### example
old code:
```php
if ($recordid == '') {
```
new one:
```php
if (empty($recordid)) {
```
### 3. Refactoring
#### RequestHandler
* As described [here](https://usermanual.vtenext.com/books/developers/page/requesthandler), access to the superglobal variables `$_REQUEST`, `$_GET`, and `$_POST` is no longer allowed.
* Replace reading their values ​​with the `RH::r`, `RH::g`, and `RH::p` methods of the `RequestHandler` class found in include/utils/RequestHandler.php.
* When writing to superglobals, use the `RH::push_*` and `RH::pop_*` methods as described in the link above.
* Remove calls to `vtlib_purify()`; RequestHandler already handles them internally.
#### Database best practices and escaping
* Apply the new best practices as described [here](https://usermanual.vtenext.com/books/developers/page/escaping-rules)
* In particular, replace `query_result` with `query_result_no_html` and `fetchByAssoc(...)` with `fetchByAssoc(..., -1, false)`
* If HTML is generated in PHP and assigned to a smarty variable, use the `\Vtenext\Types\HtmlString` class to enclose the HTML string
* In custom reports the method `getSDKBlock()` should be return a `\Vtenext\Types\HtmlString` object instead of a string
#### Other substitutions
* Remove include/require of the file `Smarty_setup.php` that no longer exists
* Replace references to the `vtigerCRM_Smarty` class with `VteSmarty`
* Replace `session_start()` con `VteSession::start()`
* For reading and writing in session use the methods of the `VteSession` class found in `include/VteSession.php`
### 4. Coding style
* Apply the guidelines described [here](https://usermanual.vtenext.com/books/developers/page/coding-style)
* Add license header to the top of the file or update it if already present as described in the link above
* Remove the closing tag `?>` at the end of PHP files
### 5. Other examples
If you are modifying a method called get_dependents_list or get_related list and towards the end there is a piece of code similar to `$return_value['CUSTOM_BUTTON'] = $button;`, ensure that the $button variable is of type `\Vtenext\Types\HtmlString`, for example in this way:
```
$return_value['CUSTOM_BUTTON'] = new \Vtenext\Types\HtmlString($button);
```
### 6. Important Rules
* Do NOT change the functional behavior
* Do NOT introduce external dependencies
* Maintain backward compatibility if possible
* Clear code > "smart" code
* Do NOT directly modify the original files
* Do NOT return complete updated code
* Perform a final check on the files with `php -l` or with `php8.3 -l` if the former is not at version 8.3
* When you have finished analyzing all the files suggest to re-check the files copied into the folder
## Output
* List modified files
* Notify the user that modified files will be found in that folder so they can manually verify them and overwrite the originals after their review
openai.yaml
interface:
  display_name: "Review SDK files"
  short_description: "Review the code of SDK customizations and make it compatible with vtenext 26"
  default_prompt: "Use $review-sdk-26 to review code of SDK customizations and make it compatible with vtenext 26."
Prompt
Select the skill by digit $review-sdk... and indicate the path to the folder.
example:
[$review-sdk-26]([...]/.codex/skills/review-sdk-26/SKILL.md) in the folder [vtenext folder].
The skill contains links to web pages with details and usage examples; read those pages.
 

Skill - Review plugin
Skill to review the code of a specific folder (plugin) and make it compatible with vtenext 26.
Install to Codex
Create the folder 
review-plugin-26 in the skills folder of your IDE example: [...]/.codex/skills/ with the structure:
review-plugin-26/
├── SKILL.md
└── agents/
    └── openai.yaml
SKILL.md
---
name: review-plugin-26
description: Revise the php code to be compatible with VTENEXT version 26.*
---
# review-plugin-26
Review and update PHP files (>=7.x) to make them:
* Compatible with PHP 8.3
* Aligned with the new project guidelines
* More robust, typed, and modern
You are working on a plugin for VTENEXT application which needs to be reviewed to the new version. DO NOT rewrite everything from scratch.
Keep the original logic, improving it only where necessary.
## Instructions
### 1. Prepare the environment
* Duplicate the folder
* If there are too many files, run the skill on 10 files at a time (batch)
* If you are running a batch, do not empty the folder
* Also copies unpatched files to the new folder to maintain the same structure and allow manual review of all files
* If you find any hardcoded credentials or sensitive data while analyzing the code, stop everything and let me know to move this data to a separate configuration file
### 2. PHP 8.3 Compatibility Analysis
* Fix weak comparisons as described [here](https://usermanual.vtenext.com/books/developers/page/weak-comparisons)
* Add type casts where necessary to functions that require a string as parameter, for example `trim()` `strpos()` and `stripos()`
* Do not replace functions if the existing ones work on PHP 8.3
* For empty string comparisons, replace with the `empty()` function
* For comparisons with `$mode` or `$sdk_mode`, the creation mode is usually checked and the empty string comparison is fine. `=== ''`
#### example
old code:
```php
if ($recordid == '') {
```
new one:
```php
if (empty($recordid)) {
```
### 3. Refactoring
#### RequestHandler
* As described [here](https://usermanual.vtenext.com/books/developers/page/requesthandler), access to the superglobal variables `$_REQUEST`, `$_GET`, and `$_POST` is no longer allowed.
* Replace reading their values ​​with the `RH::r`, `RH::g`, and `RH::p` methods of the `RequestHandler` class found in include/utils/RequestHandler.php.
* When writing to superglobals, use the `RH::push_*` and `RH::pop_*` methods as described in the link above.
* Remove calls to `vtlib_purify()`; RequestHandler already handles them internally.
#### Database best practices and escaping
* Apply the new best practices as described [here](https://usermanual.vtenext.com/books/developers/page/escaping-rules)
* In particular, replace `query_result` with `query_result_no_html` and `fetchByAssoc(...)` with `fetchByAssoc(..., -1, false)`
* If HTML is generated in PHP and assigned to a smarty variable, use the `\Vtenext\Types\HtmlString` class to enclose the HTML string
* In custom reports the method `getSDKBlock()` should be return a `\Vtenext\Types\HtmlString` object instead of a string
#### Other substitutions
* Remove include/require of the file `Smarty_setup.php` that no longer exists
* Replace references to the `vtigerCRM_Smarty` class with `VteSmarty`
* Replace `session_start()` con `VteSession::start()`
* For reading and writing in session use the methods of the `VteSession` class found in `include/VteSession.php`
### 4. Coding style
* Apply the guidelines described [here](https://usermanual.vtenext.com/books/developers/page/coding-style)
* Add license header to the top of the file or update it if already present as described in the link above
* Remove the closing tag `?>` at the end of PHP files
### 5. Other examples
If you are modifying a method called get_dependents_list or get_related list and towards the end there is a piece of code similar to `$return_value['CUSTOM_BUTTON'] = $button;`, ensure that the $button variable is of type `\Vtenext\Types\HtmlString`, for example in this way:
```
$return_value['CUSTOM_BUTTON'] = new \Vtenext\Types\HtmlString($button);
```
### 6. Important Rules
* Do NOT change the functional behavior
* Do NOT introduce external dependencies
* Maintain backward compatibility if possible
* Clear code > "smart" code
* Do NOT directly modify the original files
* Do NOT return complete updated code
* Perform a final check on the files with `php -l` or with `php8.3 -l` if the former is not at version 8.3
* When you have finished analyzing all the files suggest to re-check the files copied into the folder
## Output
* List modified files
* Notify the user that modified files will be found in that folder so they can manually verify them and overwrite the originals after their review
openai.yaml
interface:
  display_name: "Review Plugin"
  short_description: "Review the code of a specific folder (plugin) and make it compatible with vtenext 26"
  default_prompt: "Use $review-plugin-26 to review code of a specific folder (plugin) and make it compatible with vtenext 26."
Prompt
Select the skill by digit $review-plugin... and indicate the path to the folder.
example:
[$review-plugin-26]([...]/.codex/skills/review-plugin-26/SKILL.md) on the folder [your folder].
The skill contains links to web pages with details and usage examples; read those pages.

Help us improve
Developer documentation is constantly evolving!
We are constantly committed to improving the quality and completeness of technical documentation to make your work simpler and more efficient. Your feedback is essential to us.
How you can help us
There are several ways you can contribute to improving the documentation and code:
Report missing or incomplete documentation - If you can't find the information you're looking for
Request clarifications - If a section is unclear or ambiguous
Propose improvements - Do you have ideas on how to make the documentation more useful?
Suggest code optimizations - Have you found a better way to implement something?
Report errors or issues - Have you found a bug or inconsistency?
Share best practices - Have you developed interesting patterns or solutions?
Contact us via email
Send your requests, suggestions, or questions to:
Email: support@vtenext.com
In the email subject line, specify:
[DOC] for documentation requests
[BUG] for bug reports
[SUGG] for new feature suggestions
[HELP] for assistance requests
Your contribution makes a difference! Every report, suggestion, or idea helps us create better documentation for developers.
Thank you for your support and collaboration!

Quick snippets

Install docker for Kitt
Ubuntu
#!/bin/bash
set -euo pipefail
SELF="${BASH_SOURCE[0]}"
SELF="$(realpath -ms "$SELF")"
cd "$(dirname "$SELF")" || exit 2
if ((UID)) && ! [[ -v SUDO_USER ]]; then
    exec sudo "$SELF" "$@"
fi
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
arch="$(dpkg --print-architecture)"
release="$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")"
cat <<EOF >/etc/apt/sources.list.d/docker.list
deb [arch=${arch} signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $release stable
EOF
apt update
pkgs=(
    docker-ce
    docker-ce-cli
    containerd.io
    docker-buildx-plugin
    docker-compose-plugin
)
apt install -y  "${pkgs[@]}"
systemctl enable --now docker.service