Issues (2811)

public/htdocs/api/index.php (1 issue)

Labels
Severity
1
<?php
2
3
/* Copyright (C) 2015       Jean-François Ferry         <[email protected]>
4
 * Copyright (C) 2016	    Laurent Destailleur		    <[email protected]>
5
 * Copyright (C) 2017	    Regis Houssin			    <[email protected]>
6
 * Copyright (C) 2021	    Alexis LAURIER			    <[email protected]>
7
 * Copyright (C) 2024		MDW							<[email protected]>
8
 * Copyright (C) 2024       Rafael San José             <[email protected]>
9
 *
10
 * This program is free software; you can redistribute it and/or modify
11
 * it under the terms of the GNU General Public License as published by
12
 * the Free Software Foundation; either version 3 of the License, or
13
 * (at your option) any later version.
14
 *
15
 * This program is distributed in the hope that it will be useful,
16
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18
 * GNU General Public License for more details.
19
 *
20
 * You should have received a copy of the GNU General Public License
21
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
22
 */
23
24
use Dolibarr\Code\Api\Classes\DolibarrApiAccess;
25
use Dolibarr\Core\Base\DolibarrApi;
26
use Luracast\Restler\Format\UploadFormat;
27
28
$api_route = $_GET['api_route'] ?? '';
29
unset($_GET['api_route']);
30
$_SERVER['SCRIPT_NAME'] = '/api/index.php';
31
32
if (!defined('NOCSRFCHECK')) {
33
    define('NOCSRFCHECK', '1'); // Do not check anti CSRF attack test
34
}
35
if (!defined('NOTOKENRENEWAL')) {
36
    define('NOTOKENRENEWAL', '1'); // Do not check anti POST attack test
37
}
38
if (!defined('NOREQUIREMENU')) {
39
    define('NOREQUIREMENU', '1'); // If there is no need to load and show top and left menu
40
}
41
if (!defined('NOREQUIREHTML')) {
42
    define('NOREQUIREHTML', '1'); // If we don't need to load the html.form.class.php
43
}
44
if (!defined('NOREQUIREAJAX')) {
45
    define('NOREQUIREAJAX', '1'); // Do not load ajax.lib.php library
46
}
47
if (!defined("NOLOGIN")) {
48
    define("NOLOGIN", '1'); // If this page is public (can be called outside logged session)
49
}
50
if (!defined("NOSESSION")) {
51
    define("NOSESSION", '1');
52
}
53
if (!defined("NODEFAULTVALUES")) {
54
    define("NODEFAULTVALUES", '1');
55
}
56
57
// Force entity if a value is provided into HTTP header. Otherwise, will use the entity of user of token used.
58
if (!empty($_SERVER['HTTP_DOLAPIENTITY'])) {
59
    define("DOLENTITY", (int)$_SERVER['HTTP_DOLAPIENTITY']);
60
}
61
62
// Response for preflight requests (used by browser when into a CORS context)
63
if (!empty($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] == 'OPTIONS' && !empty($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'])) {
64
    header('Access-Control-Allow-Origin: *');
65
    header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE');
66
    header('Access-Control-Allow-Headers: Content-Type, Authorization, api_key, DOLAPIKEY');
67
    http_response_code(204);
68
    exit;
69
}
70
71
// When we request url to get the json file, we accept Cross site so we can include the descriptor into an external tool.
72
if (str_contains($_SERVER["PHP_SELF"], '/explorer/swagger.json')) {
73
    header('Access-Control-Allow-Origin: *');
74
    header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE');
75
    header('Access-Control-Allow-Headers: Content-Type, Authorization, api_key, DOLAPIKEY');
76
}
77
78
// When we request url to get an API, we accept Cross site so we can make js API call inside another website
79
if (str_contains($_SERVER["PHP_SELF"], '/api/index.php')) {
80
    header('Access-Control-Allow-Origin: *');
81
    header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE');
82
    header('Access-Control-Allow-Headers: Content-Type, Authorization, api_key, DOLAPIKEY');
83
}
84
header('X-Frame-Options: SAMEORIGIN');
85
86
$res = 0;
87
$res = include constant('DOL_DOCUMENT_ROOT') . "/main.inc.php";
88
if (!$res) {
89
    die("Include of main fails");
90
}
91
92
require_once constant('DOL_DOCUMENT_ROOT') . '/core/lib/functions2.lib.php';
93
94
$url = $_SERVER['PHP_SELF'];
95
if (str_ends_with($_SERVER['PHP_SELF'], 'api/index.php')) {
96
    $additionalPath = !empty($_SERVER['PATH_INFO']) ? $_SERVER['PATH_INFO'] : ($_SERVER['ORIG_PATH_INFO'] ?? '');
97
    $url = $_SERVER['PHP_SELF'] . $additionalPath;
98
}
99
100
// Fix for some NGINX setups (this should not be required even with NGINX, however setup of NGINX are often mysterious and this may help is such cases)
101
if (getDolGlobalString('MAIN_NGINX_FIX')) {
102
    $url = (isset($_SERVER['SCRIPT_URI']) && $_SERVER["SCRIPT_URI"] !== null) ? $_SERVER["SCRIPT_URI"] : $_SERVER['PHP_SELF'];
103
}
104
105
// Enable and test if module Api is enabled
106
if (!isModEnabled('api')) {
107
    $langs->load("admin");
108
    dol_syslog("Call of Dolibarr API interfaces with module API REST are disabled");
109
    print $langs->trans("WarningModuleNotActive", 'Api') . '.<br><br>';
110
    print $langs->trans("ToActivateModule");
111
    //session_destroy();
112
    exit(0);
113
}
114
115
// Test if explorer is not disabled
116
if (str_contains($url, 'api/index.php/explorer') && getDolGlobalString('API_EXPLORER_DISABLED')) {
117
    $langs->load("admin");
118
    dol_syslog("Call Dolibarr API interfaces with module API REST disabled");
119
    print $langs->trans("WarningAPIExplorerDisabled") . '.<br><br>';
120
    //session_destroy();
121
    exit(0);
122
}
123
124
// This 2 lines are useful only if we want to exclude some Urls from the explorer
125
//use Luracast\Restler\Explorer;
126
//Explorer::$excludedPaths = array('/categories');
127
128
129
// Analyze URLs
130
// index.php/explorer                           do a redirect to index.php/explorer/
131
// index.php/explorer/                          called by swagger to build explorer page index.php/explorer/index.html
132
// index.php/explorer/.../....png|.css|.js      called by swagger for resources to build explorer page
133
// index.php/explorer/resources.json            called by swagger to get list of all services
134
// index.php/explorer/resources.json/xxx        called by swagger to get detail of services xxx
135
// index.php/xxx                                called by any REST client to run API
136
137
$reg = array();
138
preg_match('/index\.php\/([^\/]+)(.*)$/', $url, $reg);
139
// .../index.php/categories?sortfield=t.rowid&sortorder=ASC
140
141
$hookmanager->initHooks(array('api'));
142
143
// When in production mode, a file api/temp/routes.php is created with the API available of current call.
144
// But, if we set $refreshcache to false, so it may have only one API in the routes.php file if we make a call for one API without
145
// using the explorer. And when we make another call for another API, the API is not into the api/temp/routes.php and a 404 is returned.
146
// So we force refresh to each call.
147
$refreshcache = (getDolGlobalString('API_PRODUCTION_DO_NOT_ALWAYS_REFRESH_CACHE') ? false : true);
148
if (!empty($reg[1]) && $reg[1] == 'explorer' && ($reg[2] == '/swagger.json' || $reg[2] == '/swagger.json/root' || $reg[2] == '/resources.json' || $reg[2] == '/resources.json/root')) {
149
    $refreshcache = true;
150
    if (!is_dir($conf->api->dir_temp)) {
151
        mkdir($conf->api->dir_temp);
152
    }
153
    if (!is_writable($conf->api->dir_temp)) {
154
        print 'Erreur temp dir api/temp (' . $conf->api->dir_temp . ') not writable';
155
        header('HTTP/1.1 500 temp dir api/temp not writable');
156
        exit(0);
157
    }
158
}
159
160
$api = new DolibarrApi($db, '', $refreshcache);
161
// var_dump($api->r->apiVersionMap);
162
163
// If MAIN_API_DEBUG is set to 1, we save logs into file "dolibarr_api.log"
164
if (getDolGlobalString('MAIN_API_DEBUG')) {
165
    $r = $api->r;
166
    $r->onCall(function () use ($r) {
167
        // Don't log Luracast Restler Explorer resources calls
168
        //if (!preg_match('/^explorer/', $r->url)) {
169
        //  'method'  => $api->r->requestMethod,
170
        //  'url'     => $api->r->url,
171
        //  'route'   => $api->r->apiMethodInfo->className.'::'.$api->r->apiMethodInfo->methodName,
172
        //  'version' => $api->r->getRequestedApiVersion(),
173
        //  'data'    => $api->r->getRequestData(),
174
        //dol_syslog("Debug API input ".var_export($r, true), LOG_DEBUG, 0, '_api');
175
        dol_syslog("Debug API url " . var_export($r->url, true), LOG_DEBUG, 0, '_api');
176
        dol_syslog("Debug API input " . var_export($r->getRequestData(), true), LOG_DEBUG, 0, '_api');
177
        //}
178
    });
179
}
180
181
// Enable the Restler API Explorer.
182
// See https://github.com/Luracast/Restler-API-Explorer for more info.
183
$api->r->addAPIClass('Luracast\\Restler\\Explorer');
184
$api->r->addAPIClass('Luracast\\Restler\\Explorer\\v1\\Explorer');
185
$api->r->addAPIClass('Luracast\\Restler\\Explorer\\v2\\Explorer');
186
187
$api->r->setSupportedFormats('JsonFormat', 'XmlFormat', 'UploadFormat'); // 'YamlFormat'
188
$api->r->addAuthenticationClass('Dolibarr\\Code\\Api\\Classes\\DolibarrApiAccess', '');
189
190
// Define accepted mime types
191
UploadFormat::$allowedMimeTypes = array('image/jpeg', 'image/png', 'text/plain', 'application/octet-stream');
192
193
// Restrict API to some IPs
194
if (getDolGlobalString('API_RESTRICT_ON_IP')) {
195
    $allowedip = explode(' ', getDolGlobalString('API_RESTRICT_ON_IP'));
196
    $ipremote = getUserRemoteIP();
197
    if (!in_array($ipremote, $allowedip)) {
198
        dol_syslog('Remote ip is ' . $ipremote . ', not into list ' . getDolGlobalString('API_RESTRICT_ON_IP'));
199
        print 'APIs are not allowed from the IP ' . $ipremote;
200
        header('HTTP/1.1 503 API not allowed from your IP ' . $ipremote);
201
        //session_destroy();
202
        exit(0);
203
    }
204
}
205
206
// Call Explorer file for all APIs definitions (this part is slow)
207
if (!empty($reg[1]) && $reg[1] == 'explorer' && ($reg[2] == '/swagger.json' || $reg[2] == '/swagger.json/' || $reg[2] == '/swagger.json/root' || $reg[2] == '/resources.json/' || $reg[2] == '/resources.json' || $reg[2] == '/resources.json/root')) {
208
    // Scan all API files to load them
209
    $listofapis = DolibarrApi::getModules();
210
    foreach ($listofapis as $apiname => $classname) {
211
        new $classname();
212
        $api->r->addAPIClass($classname);
213
    }
214
}
215
216
217
// Call one APIs or one definition of an API
218
$regbis = array();
219
if (!empty($reg[1]) && ($reg[1] != 'explorer' || ($reg[2] != '/swagger.json' && $reg[2] != '/resources.json' && preg_match('/^\/(swagger|resources)\.json\/(.+)$/', $reg[2], $regbis) && $regbis[2] != 'root'))) {
220
    $moduleobject = $reg[1];
221
    if ($moduleobject == 'explorer') {  // If we call page to explore details of a service
222
        $moduleobject = $regbis[2];
223
    }
224
225
    $moduleobject = strtolower($moduleobject);
226
    $moduledirforclass = getModuleDirForApiClass($moduleobject);
227
228
    // Load a dedicated API file
229
    dol_syslog("Load a dedicated API file moduleobject=" . $moduleobject . " moduledirforclass=" . $moduledirforclass);
230
231
    $tmpmodule = $moduleobject;
232
    if ($tmpmodule != 'api') {
233
        $tmpmodule = preg_replace('/api$/i', '', $tmpmodule);
234
    }
235
    $classfile = str_replace('_', '', $tmpmodule);
236
237
    // Special cases that does not match name rules conventions
238
    if ($moduleobject == 'supplierproposals') {
239
        $classfile = 'supplier_proposals';
240
    }
241
    if ($moduleobject == 'supplierorders') {
242
        $classfile = 'supplier_orders';
243
    }
244
    if ($moduleobject == 'supplierinvoices') {
245
        $classfile = 'supplier_invoices';
246
    }
247
    if ($moduleobject == 'ficheinter') {
248
        $classfile = 'interventions';
249
    }
250
    if ($moduleobject == 'interventions') {
251
        $classfile = 'interventions';
252
    }
253
254
    $filename = '/' . $moduledirforclass . '/class/api_' . $classfile . '.class.php';
255
    $dir_part_file = dol_buildpath($filename, 0, 2);
256
257
    $classname = ucwords($moduleobject);
258
    $modulename = ucwords($moduledirforclass);
259
260
    // Test rules on endpoints. For example:
261
    // $conf->global->API_ENDPOINT_RULES = 'endpoint1:1,endpoint2:1,...'
262
    if (getDolGlobalString('API_ENDPOINT_RULES')) {
263
        $listofendpoints = explode(',', getDolGlobalString('API_ENDPOINT_RULES'));
264
        $endpointisallowed = false;
265
266
        foreach ($listofendpoints as $endpointrule) {
267
            $tmparray = explode(':', $endpointrule);
268
            if (($classfile == $tmparray[0] || $classfile . 'api' == $tmparray[0]) && $tmparray[1] == 1) {
269
                $endpointisallowed = true;
270
                break;
271
            }
272
        }
273
274
        if (!$endpointisallowed) {
275
            dol_syslog('The API with endpoint /' . $classfile . ' is forbidden by config API_ENDPOINT_RULES', LOG_WARNING);
276
            print 'The API with endpoint /' . $classfile . ' is forbidden by config API_ENDPOINT_RULES';
277
            header('HTTP/1.1 501 API is forbidden by API_ENDPOINT_RULES');
278
            //session_destroy();
279
            exit(0);
280
        }
281
    }
282
283
    dol_syslog('Search api file /' . $moduledirforclass . '/class/api_' . $classfile . '.class.php => dir_part_file=' . $dir_part_file . ', classname=' . $classname);
284
285
    $namespace = DolibarrApi::getModuleNamespace($modulename, $classname);
286
    $class = new $namespace();
287
    if (!isset($class)) {
288
        dol_syslog('Failed to make include_once ' . $dir_part_file, LOG_WARNING);
289
        print 'API not found (failed to include API file)';
290
        header('HTTP/1.1 501 API not found (failed to include API file)');
291
        //session_destroy();
292
        exit(0);
293
    }
294
295
    $api->r->addAPIClass($namespace);
296
}
297
298
// We do not want that restler outputs data if we use native compression (default behaviour) but we want to have it returned into a string.
299
// If API_DISABLE_COMPRESSION is set, returnResponse is false => It use default handling so output result directly.
300
$usecompression = (!getDolGlobalString('API_DISABLE_COMPRESSION') && !empty($_SERVER['HTTP_ACCEPT_ENCODING']));
301
$foundonealgorithm = 0;
302
if ($usecompression) {
303
    if (strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'br') !== false && function_exists('brotli_compress')) {
304
        $foundonealgorithm++;
305
    }
306
    if (strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'bz') !== false && function_exists('bzcompress')) {
307
        $foundonealgorithm++;
308
    }
309
    if (strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== false && function_exists('gzencode')) {
310
        $foundonealgorithm++;
311
    }
312
    if (!$foundonealgorithm) {
313
        $usecompression = false;
314
    }
315
}
316
317
//dol_syslog('We found some compression algorithm: '.$foundonealgorithm.' -> usecompression='.$usecompression, LOG_DEBUG);
318
319
Luracast\Restler\Defaults::$returnResponse = $usecompression;
320
321
// Call API (we suppose we found it).
322
// The handle will use the file api/temp/routes.php to get data to run the API. If the file exists and the entry for API is not found, it will return 404.
323
$responsedata = $api->r->handle();
0 ignored issues
show
Are you sure the assignment to $responsedata is correct as $api->r->handle() targeting Luracast\Restler\Restler::handle() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
324
325
if (Luracast\Restler\Defaults::$returnResponse) {
326
    // We try to compress the data received data
327
    if (strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'br') !== false && function_exists('brotli_compress') && defined('BROTLI_TEXT')) {
328
        header('Content-Encoding: br');
329
        $result = brotli_compress($responsedata, 11, constant('BROTLI_TEXT'));
330
    } elseif (strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'bz') !== false && function_exists('bzcompress')) {
331
        header('Content-Encoding: bz');
332
        $result = bzcompress($responsedata, 9);
333
    } elseif (strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== false && function_exists('gzencode')) {
334
        header('Content-Encoding: gzip');
335
        $result = gzencode($responsedata, 9);
336
    } else {
337
        header('Content-Encoding: text/html');
338
        print "No compression method found. Try to disable compression by adding API_DISABLE_COMPRESSION=1";
339
        exit(0);
340
    }
341
342
    // Restler did not output data yet, we return it now
343
    echo $result;
344
}
345
346
if (getDolGlobalInt("API_ENABLE_COUNT_CALLS") && $api->r->responseCode == 200) {
347
    $error = 0;
348
    $db->begin();
349
    $userid = DolibarrApiAccess::$user->id;
350
351
    $sql = "SELECT up.value";
352
    $sql .= " FROM " . MAIN_DB_PREFIX . "user_param as up";
353
    $sql .= " WHERE up.param = 'API_COUNT_CALL'";
354
    $sql .= " AND up.fk_user = " . ((int)$userid);
355
    $sql .= " AND up.entity = " . ((int)$conf->entity);
356
357
    $result = $db->query($sql);
358
    if ($result) {
359
        $updateapi = false;
360
        $nbrows = $db->num_rows($result);
361
        if ($nbrows == 0) {
362
            $sql2 = "INSERT INTO " . MAIN_DB_PREFIX . "user_param";
363
            $sql2 .= " (fk_user, entity, param, value)";
364
            $sql2 .= " VALUES (" . ((int)$userid) . ", " . ((int)$conf->entity) . ", 'API_COUNT_CALL', 1)";
365
        } else {
366
            $updateapi = true;
367
            $sql2 = "UPDATE " . MAIN_DB_PREFIX . "user_param as up";
368
            $sql2 .= " SET up.value = up.value + 1";
369
            $sql2 .= " WHERE up.param = 'API_COUNT_CALL'";
370
            $sql2 .= " AND up.fk_user = " . ((int)$userid);
371
            $sql2 .= " AND up.entity = " . ((int)$conf->entity);
372
        }
373
374
        $result2 = $db->query($sql2);
375
        if (!$result2) {
376
            $modeapicall = $updateapi ? 'updating' : 'inserting';
377
            dol_syslog('Error while ' . $modeapicall . ' API_COUNT_CALL for user ' . $userid, LOG_ERR);
378
            $error++;
379
        }
380
    } else {
381
        dol_syslog('Error on select API_COUNT_CALL for user ' . $userid, LOG_ERR);
382
        $error++;
383
    }
384
385
    if ($error) {
386
        $db->rollback();
387
    } else {
388
        $db->commit();
389
    }
390
}
391
392
// Call API termination method
393
$apiMethodInfo = &$api->r->apiMethodInfo;
394
$terminateCall = '_terminate_' . $apiMethodInfo->methodName . '_' . $api->r->responseFormat->getExtension();
395
if (method_exists($apiMethodInfo->className, $terminateCall)) {
396
    // Now flush output buffers so that response data is sent to the client even if we still have action to do in a termination method.
397
    ob_end_flush();
398
399
    // If you're using PHP-FPM, this function will allow you to send the response and then continue processing
400
    if (function_exists('fastcgi_finish_request')) {
401
        fastcgi_finish_request();
402
    }
403
404
    // Call a termination method. Warning: This method can do I/O, sync but must not make output.
405
    call_user_func(array(Luracast\Restler\Scope::get($apiMethodInfo->className), $terminateCall), $responsedata);
406
}
407
408
//session_destroy();
409