diff --git a/htdocs/api/class/api.class.php b/htdocs/api/class/api.class.php index 9228c71ccbf54883b3b8696b3003b1c7e0781b16..082e6188379be59d6caf877b93c5fb2c5d7fbdb2 100644 --- a/htdocs/api/class/api.class.php +++ b/htdocs/api/class/api.class.php @@ -19,6 +19,7 @@ use Luracast\Restler\Restler; use Luracast\Restler\RestException; use Luracast\Restler\Defaults; +use Luracast\Restler\Format\UploadFormat; require_once DOL_DOCUMENT_ROOT.'/user/class/user.class.php'; @@ -47,14 +48,14 @@ class DolibarrApi function __construct($db, $cachedir='') { global $conf; - + if (empty($cachedir)) $cachedir = $conf->api->dir_temp; Defaults::$cacheDirectory = $cachedir; - + $this->db = $db; $production_mode = ( empty($conf->global->API_PRODUCTION_MODE) ? false : true ); $this->r = new Restler($production_mode); - + $this->r->setAPIVersion(1); } @@ -86,21 +87,23 @@ class DolibarrApi // Remove $db object property for object unset($object->db); - + // Remove linkedObjects. We should already have linkedObjectIds that avoid huge responses unset($object->linkedObjects); + + unset($object->lines); // should be ->lines unset($object->fields); unset($object->oldline); - + unset($object->error); unset($object->errors); - + unset($object->ref_previous); unset($object->ref_next); unset($object->ref_int); - + unset($object->projet); // Should be fk_project unset($object->project); // Should be fk_project unset($object->author); // Should be fk_user_author @@ -112,18 +115,18 @@ class DolibarrApi unset($object->timespent_withhour); unset($object->timespent_fk_user); unset($object->timespent_note); - + unset($object->statuts); unset($object->statuts_short); unset($object->statuts_logo); unset($object->statuts_long); - + unset($object->element); unset($object->fk_element); unset($object->table_element); unset($object->table_element_line); unset($object->picto); - + // Remove the $oldcopy property because it is not supported by the JSON // encoder. The following error is generated when trying to serialize // it: "Error encoding/decoding JSON: Type is not supported" @@ -153,7 +156,7 @@ class DolibarrApi } } }*/ - + return $object; } @@ -188,12 +191,12 @@ class DolibarrApi return checkUserAccessToObject(DolibarrApiAccess::$user, $featuresarray, $resource_id, $dbtablename, $feature2, $dbt_keyfield, $dbt_select); } - + /** * Return if a $sqlfilters parameter is valid - * + * * @param string $sqlfilters sqlfilter string - * @return boolean True if valid, False if not valid + * @return boolean True if valid, False if not valid */ function _checkFilters($sqlfilters) { @@ -217,22 +220,22 @@ class DolibarrApi } return true; } - + /** * Function to forge a SQL criteria - * + * * @param array $matches Array of found string by regex search * @return string Forged criteria. Example: "t.field like 'abc%'" */ static function _forge_criteria_callback($matches) { global $db; - + //dol_syslog("Convert matches ".$matches[1]); if (empty($matches[1])) return ''; $tmp=explode(':',$matches[1]); if (count($tmp) < 3) return ''; - + $tmpescaped=$tmp[2]; if (preg_match('/^\'(.*)\'$/', $tmpescaped, $regbis)) { @@ -243,5 +246,5 @@ class DolibarrApi $tmpescaped = $db->escape($tmpescaped); } return $db->escape($tmp[0]).' '.strtoupper($db->escape($tmp[1]))." ".$tmpescaped; - } + } } diff --git a/htdocs/api/class/api_documents.class.php b/htdocs/api/class/api_documents.class.php new file mode 100644 index 0000000000000000000000000000000000000000..ca10b2befce87fe5ff2d8626dd1c9b281f32f5da --- /dev/null +++ b/htdocs/api/class/api_documents.class.php @@ -0,0 +1,143 @@ +<?php +/* Copyright (C) 2016 Xebax Christy <xebax@wanadoo.fr> + * Copyright (C) 2016 Laurent Destailleur <eldy@users.sourceforge.net> + * Copyright (C) 2016 Jean-François Ferry <jfefe@aternatik.fr> + * + * This program is free software you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +use Luracast\Restler\RestException; +use Luracast\Restler\Format\UploadFormat; + + +require_once DOL_DOCUMENT_ROOT.'/main.inc.php'; + +/** + * API class for receive files + * + * @access protected + * @class Documents {@requires user,external} + */ +class Documents extends DolibarrApi +{ + + /** + * @var array $DOCUMENT_FIELDS Mandatory fields, checked when create and update object + */ + static $DOCUMENT_FIELDS = array( + 'name', + 'modulepart', + 'file' + ); + + /** + * Constructor + */ + function __construct() + { + global $db; + $this->db = $db; + } + + /** + * Return a document + * + * @param string $module_part Module part for file + * @param string $filename File name + * + * @return array + * @throws RestException + * + */ + public function get($module_part, $filename) { + + } + + + /** + * Receive file + * + * @param array $request_data Request datas + * + * @return bool State of copy + * @throws RestException + */ + public function post($request_data) { + global $conf; + + require_once DOL_DOCUMENT_ROOT . '/core/lib/files.lib.php'; + + if (!DolibarrApiAccess::$user->rights->ecm->upload) { + throw new RestException(401); + } + + // Suppression de la chaine de caractere ../ dans $original_file + $original_file = str_replace("../","/", $request_data['name']); + $refname = str_replace("../","/", $request_data['refname']); + + // find the subdirectory name as the reference + if (empty($request_data['refname'])) $refname=basename(dirname($original_file)."/"); + + // Security: + // On interdit les remontees de repertoire ainsi que les pipe dans + // les noms de fichiers. + if (preg_match('/\.\./',$original_file) || preg_match('/[<>|]/',$original_file)) + { + throw new RestException(401,'Refused to deliver file '.$original_file); + } + if (preg_match('/\.\./',$refname) || preg_match('/[<>|]/',$refname)) + { + throw new RestException(401,'Refused to deliver file '.$refname); + } + + $modulepart = $request_data['modulepart']; + + // Check mandatory fields + $result = $this->_validate_file($request_data); + + $upload_dir = DOL_DATA_ROOT . '/' .$modulepart.'/'.dol_sanitizeFileName($refname); + $destfile = $upload_dir . $original_file; + + if (!is_dir($upload_dir)) { + throw new RestException(401,'Directory not exists : '.$upload_dir); + } + + $file = $_FILES['file']; + $srcfile = $file['tmp_name']; + $res = dol_move($srcfile, $destfile, 0, 1); + + if (!$res) { + throw new RestException(500); + } + + return $res; + } + + /** + * Validate fields before create or update object + * + * @param array $data Array with data to verify + * @return array + * @throws RestException + */ + function _validate_file($data) { + $result = array(); + foreach (Documents::$DOCUMENT_FIELDS as $field) { + if (!isset($data[$field])) + throw new RestException(400, "$field field missing"); + $result[$field] = $data[$field]; + } + return $result; + } +} diff --git a/htdocs/api/index.php b/htdocs/api/index.php index 37be4d8cc26330410cd955a224d4ded133b79978..fa96da6b139346db5e8c57a46ba60fbb5a678567 100644 --- a/htdocs/api/index.php +++ b/htdocs/api/index.php @@ -66,16 +66,18 @@ if (preg_match('/api\/index\.php\/explorer/', $_SERVER["PHP_SELF"]) && ! empty($ } - $api = new DolibarrApi($db); // Enable the Restler API Explorer. // See https://github.com/Luracast/Restler-API-Explorer for more info. $api->r->addAPIClass('Luracast\\Restler\\Explorer'); -$api->r->setSupportedFormats('JsonFormat', 'XmlFormat'); +$api->r->setSupportedFormats('JsonFormat', 'XmlFormat', 'UploadFormat'); $api->r->addAuthenticationClass('DolibarrApiAccess',''); +// Define accepted mime types +UploadFormat::$allowedMimeTypes = array('image/jpeg', 'image/png', 'text/plain', 'application/octet-stream'); + $listofapis = array(); $modulesdir = dolGetModulesDirs(); @@ -96,7 +98,7 @@ foreach ($modulesdir as $dir) $module = strtolower($reg[1]); $moduledirforclass = $module; $moduleforperm = $module; - + if ($module == 'propale') { $moduledirforclass = 'comm/propal'; $moduleforperm='propal'; @@ -129,7 +131,7 @@ foreach ($modulesdir as $dir) $moduledirforclass = 'fourn'; } //dol_syslog("Found module file ".$file." - module=".$module." - moduledirforclass=".$moduledirforclass); - + // Defined if module is enabled $enabled=true; if (empty($conf->$moduleforperm->enabled)) $enabled=false; @@ -152,7 +154,7 @@ foreach ($modulesdir as $dir) while (($file_searched = readdir($handle_part))!==false) { if ($file_searched == 'api_access.class.php') continue; - + // Support of the deprecated API. if (is_readable($dir_part.$file_searched) && preg_match("/^api_deprecated_(.*)\.class\.php$/i",$file_searched,$reg)) { diff --git a/test/phpunit/RestAPIDocumentTest.php b/test/phpunit/RestAPIDocumentTest.php new file mode 100644 index 0000000000000000000000000000000000000000..ea0c218027fc35490650b1a8b23afc3043b5e4bc --- /dev/null +++ b/test/phpunit/RestAPIDocumentTest.php @@ -0,0 +1,183 @@ +<?php +/* Copyright (C) 2010 Laurent Destailleur <eldy@users.sourceforge.net> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * or see http://www.gnu.org/ + */ + +/** + * \file test/phpunit/RestAPIDocumentTest.php + * \ingroup test + * \brief PHPUnit test + * \remarks To run this script as CLI: phpunit filename.php. + */ +global $conf,$user,$langs,$db; +//define('TEST_DB_FORCE_TYPE','mysql'); // This is to force using mysql driver +//require_once 'PHPUnit/Autoload.php'; +require_once dirname(__FILE__).'/../../htdocs/master.inc.php'; +require_once dirname(__FILE__).'/../../htdocs/core/lib/date.lib.php'; +require_once dirname(__FILE__).'/../../htdocs/core/lib/geturl.lib.php'; + +if (empty($user->id)) { + echo "Load permissions for admin user nb 1\n"; + $user->fetch(1); + $user->getrights(); +} +$conf->global->MAIN_DISABLE_ALL_MAILS = 1; +$conf->global->MAIN_UMASK = '0666'; + +/** + * Class for PHPUnit tests. + * + * @backupGlobals disabled + * @backupStaticAttributes enabled + * @remarks backupGlobals must be disabled to have db,conf,user and lang not erased. + */ +class RestAPIDocumentTest extends PHPUnit_Framework_TestCase +{ + protected $savconf; + protected $savuser; + protected $savlangs; + protected $savdb; + protected $api_url; + protected $api_key; + + /** + * Constructor + * We save global variables into local variables. + * + * @return DateLibTest + */ + public function __construct() + { + //$this->sharedFixture + global $conf,$user,$langs,$db; + $this->savconf = $conf; + $this->savuser = $user; + $this->savlangs = $langs; + $this->savdb = $db; + + echo __METHOD__.' db->type='.$db->type.' user->id='.$user->id; + //print " - db ".$db->db; + echo "\n"; + } + + // Static methods + public static function setUpBeforeClass() + { + global $conf,$user,$langs,$db; + $db->begin(); // This is to have all actions inside a transaction even if test launched without suite. + + echo __METHOD__."\n"; + } + + // tear down after class + public static function tearDownAfterClass() + { + global $conf,$user,$langs,$db; + $db->rollback(); + + echo __METHOD__."\n"; + } + + /** + * Init phpunit tests. + */ + protected function setUp() + { + global $conf,$user,$langs,$db; + $conf = $this->savconf; + $user = $this->savuser; + $langs = $this->savlangs; + $db = $this->savdb; + + $this->api_url = DOL_MAIN_URL_ROOT.'/api/index.php'; + + $login = 'admin'; + $password = 'admin'; + $url = $this->api_url.'/login?login='.$login.'&password='.$password; + // Call the API login method to save api_key for this test class + $result = getURLContent($url, 'GET', '', 1, array()); + echo __METHOD__.' result = '.var_export($result, true)."\n"; + echo __METHOD__.' curl_error_no: '.$result['curl_error_no']."\n"; + $this->assertEquals($result['curl_error_no'], ''); + $object = json_decode($result['content'], true); + $this->assertNotNull($object, 'Parsing of json result must no be null'); + $this->assertEquals('200', $object['success']['code']); + + $this->api_key = $object['success']['token']; + echo __METHOD__." api_key: $this->api_key \n"; + + echo __METHOD__."\n"; + } + + /** + * End phpunit tests. + */ + protected function tearDown() + { + echo __METHOD__."\n"; + } + + /** + * testRestReceiveDocument. + * + * @return int + */ + public function testRestReceiveDocument() + { + global $conf,$user,$langs,$db; + + $url = $this->api_url.'/documents/?api_key='.$this->api_key; + + $fileName = 'img250x20.png'; + $filePath = dirname(__FILE__).'/'.$fileName; + $mimetype = mime_content_type($filePath); + // Init Curl file object + // See https://wiki.php.net/rfc/curl-file-upload + $cfile = curl_file_create($filePath, $mimetype); + + echo __METHOD__.' Request POST url='.$url."\n"; + + // Send to existant directory + $data = array( + 'modulepart' => 'facture', + 'file' => $cfile, + 'refname' => 'AV1303-0003', + 'name' => $fileName, // Name for destination + 'type' => $mimetype, ); + + $result = getURLContent($url, 'POST', $data, 1); + + echo __METHOD__.' Result for sending document: '.var_export($result, true)."\n"; + echo __METHOD__.' curl_error_no: '.$result['curl_error_no']."\n"; + $this->assertEquals($result['curl_error_no'], ''); + $this->assertEquals($result['content'], 'true'); + + // Send to unexistant directory + $data = array( + 'modulepart' => 'facture', + 'file' => $cfile, + 'name' => 'AV1303-0003STSEIUDEISRESIJLEU/'.$fileName, // Name for destination + 'type' => $mimetype, ); + + $result2 = getURLContent($url, 'POST', $data, 1); + echo __METHOD__.' Result for sending document: '.var_export($result2, true)."\n"; + echo __METHOD__.' curl_error_no: '.$result['curl_error_no']."\n"; + + $object = json_decode($result2['content'], true); + $this->assertNotNull($object, 'Parsing of json result must no be null'); + $this->assertEquals('401', $object['error']['code']); + } +}