diff --git a/composer.json b/composer.json index f0de86ce7ea..ed75d3ca61d 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,8 @@ "require": { "php": ">=5.3.0", "ext-gd": "*", - "ext-curl": "*" + "ext-curl": "*", + "restler/framework": "3.0.*" }, "suggest": { "ext-mysqli": "*", diff --git a/dev/skeletons/build_api_class.php b/dev/skeletons/build_api_class.php new file mode 100755 index 00000000000..4ed122c95d8 --- /dev/null +++ b/dev/skeletons/build_api_class.php @@ -0,0 +1,123 @@ +#!/usr/bin/php + + * + * 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 . + */ + +/** + * \file dev/skeletons/build_api_class.php + * \ingroup core + * \brief Create a complete API class file from existant class file + */ + +$sapi_type = php_sapi_name(); +$script_file = basename(__FILE__); +$path=dirname(__FILE__).'/'; + +// Test if batch mode +if (substr($sapi_type, 0, 3) == 'cgi') { + echo "Error: You are using PHP for CGI. To execute ".$script_file." from command line, you must use PHP for CLI mode.\n"; + exit; +} + +// Include Dolibarr environment +require_once($path."../../htdocs/master.inc.php"); +// After this $db is a defined handler to database. + +// Main +$version='1'; +@set_time_limit(0); +$error=0; + +$langs->load("main"); + + +print "***** $script_file ($version) *****\n"; + + +// -------------------- START OF BUILD_API_FROM_CLASS -------------------- + +// Check parameters +if (! isset($argv[1]) && ! isset($argv[2])) +{ + print "Usage: $script_file phpClassFile phpClassName\n"; + exit; +} +// Show parameters +print 'Classfile='.$argv[1]."\n"; +print 'Classname='.$argv[2]."\n"; + +$classfile=$argv[1]; +$classname=$argv[2]; +$classmin=strtolower($classname); +$classnameApi = $classname.'Api'; +$property=array(); +$targetcontent=''; + +// Load the class and read properties +require_once($classfile); + +$property=array(); +$class = new $classname($db); +$values=get_class_vars($classname); + +unset($values['db']); +unset($values['error']); +unset($values['errors']); +unset($values['element']); +unset($values['table_element']); +unset($values['table_element_line']); +unset($values['fk_element']); +unset($values['ismultientitymanaged']); + +// Read skeleton_api_class.class.php file +$skeletonfile=$path.'skeleton_api_class.class.php'; +$sourcecontent=file_get_contents($skeletonfile); +if (! $sourcecontent) +{ + print "\n"; + print "Error: Failed to read skeleton sample '".$skeletonfile."'\n"; + print "Try to run script from skeletons directory.\n"; + exit; +} + +// Define output variables +$outfile='out.api_'.$classmin.'.class.php'; +$targetcontent=$sourcecontent; + +// Substitute class name +$targetcontent=preg_replace('/skeleton_api_class\.class\.php/', 'api_'.$classmin.'.class.php', $targetcontent); +$targetcontent=preg_replace('/skeleton/', $classmin, $targetcontent); +//$targetcontent=preg_replace('/\$table_element=\'skeleton\'/', '\$table_element=\''.$tablenoprefix.'\'', $targetcontent); +$targetcontent=preg_replace('/SkeletonApi/', $classnameApi, $targetcontent); +$targetcontent=preg_replace('/Skeleton/', $classname, $targetcontent); + +// Build file +$fp=fopen($outfile,"w"); +if ($fp) +{ + fputs($fp, $targetcontent); + fclose($fp); + print "\n"; + print "File '".$outfile."' has been built in current directory.\n"; +} +else $error++; + + + +// -------------------- END OF BUILD_CLASS_FROM_TABLE SCRIPT -------------------- + +print "You can now rename generated files by removing the 'out.' prefix in their name and store them into directory /module/class.\n"; +return $error; diff --git a/dev/skeletons/skeleton_api_class.class.php b/dev/skeletons/skeleton_api_class.class.php new file mode 100644 index 00000000000..9cc8de2bc5e --- /dev/null +++ b/dev/skeletons/skeleton_api_class.class.php @@ -0,0 +1,288 @@ + + * + * 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 . + */ + + use Luracast\Restler\RestException; + + +/** + * API class for skeleton object + * + * @smart-auto-routing false + * @access protected + * @class DolibarrApiAccess {@requires user,external} + * + * + */ +class SkeletonApi extends DolibarrApi +{ + /** + * @var array $FIELDS Mandatory fields, checked when create and update object + */ + static $FIELDS = array( + 'name' + ); + + /** + * @var Skeleton $skeleton {@type Skeleton} + */ + public $skeleton; + + /** + * Constructor + * + * @url GET skeleton/ + * + */ + function __construct() + { + global $db, $conf; + $this->db = $db; + $this->skeleton = new Skeleton($this->db); + } + + /** + * Get properties of a skeleton object + * + * Return an array with skeleton informations + * + * @param int $id ID of skeleton + * @return array|mixed data without useless information + * + * @url GET skeleton/{id} + * @throws RestException + */ + function get($id) + { + if(! DolibarrApiAccess::$user->rights->skeleton->read) { + throw new RestException(401); + } + + $result = $this->skeleton->fetch($id); + if( ! $result ) { + throw new RestException(404, 'Skeleton not found'); + } + + if( ! DolibarrApi::_checkAccessToResource('skeleton',$this->skeleton->id)) { + throw new RestException(401, 'Access not allowed for login '.DolibarrApiAccess::$user->login); + } + + return $this->_cleanObjectDatas($this->skeleton); + } + + /** + * List skeletons + * + * Get a list of skeletons + * + * @param int $mode Use this param to filter list + * @param string $sortfield Sort field + * @param string $sortorder Sort order + * @param int $limit Limit for list + * @param int $page Page number + * + * @return array Array of skeleton objects + * + * @url GET /skeletons/ + */ + function getList($mode, $sortfield = "s.rowid", $sortorder = 'ASC', $limit = 0, $page = 0) { + global $db, $conf; + + $obj_ret = array(); + + $socid = DolibarrApiAccess::$user->societe_id ? DolibarrApiAccess::$user->societe_id : ''; + + // If the internal user must only see his customers, force searching by him + if (! DolibarrApiAccess::$user->rights->societe->client->voir && !$socid) $search_sale = DolibarrApiAccess::$user->id; + + $sql = "SELECT s.rowid"; + if ((!DolibarrApiAccess::$user->rights->societe->client->voir && !$socid) || $search_sale > 0) $sql .= ", sc.fk_soc, sc.fk_user"; // We need these fields in order to filter by sale (including the case where the user can only see his prospects) + $sql.= " FROM ".MAIN_DB_PREFIX."skeleton as s"; + + if ((!DolibarrApiAccess::$user->rights->societe->client->voir && !$socid) || $search_sale > 0) $sql.= ", ".MAIN_DB_PREFIX."societe_commerciaux as sc"; // We need this table joined to the select in order to filter by sale + $sql.= ", ".MAIN_DB_PREFIX."c_stcomm as st"; + $sql.= " WHERE s.fk_stcomm = st.id"; + + // Example of use $mode + //if ($mode == 1) $sql.= " AND s.client IN (1, 3)"; + //if ($mode == 2) $sql.= " AND s.client IN (2, 3)"; + + $sql.= ' AND s.entity IN ('.getEntity('skeleton', 1).')'; + if ((!DolibarrApiAccess::$user->rights->societe->client->voir && !$socid) || $search_sale > 0) $sql.= " AND s.fk_soc = sc.fk_soc"; + if ($socid) $sql.= " AND s.fk_soc = ".$socid; + if ($search_sale > 0) $sql.= " AND s.rowid = sc.fk_soc"; // Join for the needed table to filter by sale + + // Insert sale filter + if ($search_sale > 0) + { + $sql .= " AND sc.fk_user = ".$search_sale; + } + + $nbtotalofrecords = 0; + if (empty($conf->global->MAIN_DISABLE_FULL_SCANLIST)) + { + $result = $db->query($sql); + $nbtotalofrecords = $db->num_rows($result); + } + + $sql.= $db->order($sortfield, $sortorder); + if ($limit) { + if ($page < 0) + { + $page = 0; + } + $offset = $limit * $page; + + $sql.= $db->plimit($limit + 1, $offset); + } + + $result = $db->query($sql); + if ($result) + { + $num = $db->num_rows($result); + while ($i < $num) + { + $obj = $db->fetch_object($result); + $skeleton_static = new Skeleton($db); + if($skeleton_static->fetch($obj->rowid)) { + $obj_ret[] = parent::_cleanObjectDatas($skeleton_static); + } + $i++; + } + } + else { + throw new RestException(503, 'Error when retrieve skeleton list'); + } + if( ! count($obj_ret)) { + throw new RestException(404, 'No skeleton found'); + } + return $obj_ret; + } + + /** + * Create skeleton object + * + * @param array $request_data Request datas + * @return int ID of skeleton + * + * @url POST skeleton/ + */ + function post($request_data = NULL) + { + if(! DolibarrApiAccess::$user->rights->skeleton->create) { + throw new RestException(401); + } + // Check mandatory fields + $result = $this->_validate($request_data); + + foreach($request_data as $field => $value) { + $this->skeleton->$field = $value; + } + if( ! $this->skeleton->create(DolibarrApiAccess::$user)) { + throw new RestException(500); + } + return $this->skeleton->id; + } + + /** + * Update skeleton + * + * @param int $id Id of skeleton to update + * @param array $request_data Datas + * @return int + * + * @url PUT skeleton/{id} + */ + function put($id, $request_data = NULL) + { + if(! DolibarrApiAccess::$user->rights->skeleton->create) { + throw new RestException(401); + } + + $result = $this->skeleton->fetch($id); + if( ! $result ) { + throw new RestException(404, 'Skeleton not found'); + } + + if( ! DolibarrApi::_checkAccessToResource('skeleton',$this->skeleton->id)) { + throw new RestException(401, 'Access not allowed for login '.DolibarrApiAccess::$user->login); + } + + foreach($request_data as $field => $value) { + $this->skeleton->$field = $value; + } + + if($this->skeleton->update($id, DolibarrApiAccess::$user)) + return $this->get ($id); + + return false; + } + + /** + * Delete skeleton + * + * @param int $id Skeleton ID + * @return array + * + * @url DELETE skeleton/{id} + */ + function delete($id) + { + if(! DolibarrApiAccess::$user->rights->skeleton->supprimer) { + throw new RestException(401); + } + $result = $this->skeleton->fetch($id); + if( ! $result ) { + throw new RestException(404, 'Skeleton not found'); + } + + if( ! DolibarrApi::_checkAccessToResource('skeleton',$this->skeleton->id)) { + throw new RestException(401, 'Access not allowed for login '.DolibarrApiAccess::$user->login); + } + + if( !$this->skeleton->delete($id)) + { + throw new RestException(500); + } + + return array( + 'success' => array( + 'code' => 200, + 'message' => 'Skeleton deleted' + ) + ); + + } + + /** + * Validate fields before create or update object + * + * @param array $data Data to validate + * @return array + * + * @throws RestException + */ + function _validate($data) + { + $skeleton = array(); + foreach (SkeletonApi::$FIELDS as $field) { + if (!isset($data[$field])) + throw new RestException(400, "$field field missing"); + $skeleton[$field] = $data[$field]; + } + return $skeleton; + } +} diff --git a/htdocs/adherents/card_subscriptions.php b/htdocs/adherents/card_subscriptions.php index 369027b1e14..3aedb6fe50c 100644 --- a/htdocs/adherents/card_subscriptions.php +++ b/htdocs/adherents/card_subscriptions.php @@ -873,7 +873,7 @@ if ($rowid) } }); '; - if (GETPOST('paymentsave')) print '$("#'.GETPOST('paymentsave').'").attr("checked",true);'; + if (GETPOST('paymentsave')) print '$("#'.GETPOST('paymentsave').'").prop("checked",true);'; print '});'; print ''."\n"; } diff --git a/htdocs/admin/const.php b/htdocs/admin/const.php index 6b63ba74c6b..55434d4de2e 100644 --- a/htdocs/admin/const.php +++ b/htdocs/admin/const.php @@ -165,7 +165,7 @@ jQuery(document).ready(function() { var row_num = field_id.split("_"); jQuery("#updateconst").show(); jQuery("#action").val('update'); - jQuery("#check_" + row_num[1]).attr("checked",true); + jQuery("#check_" + row_num[1]).prop("checked",true); }); }); diff --git a/htdocs/admin/mails.php b/htdocs/admin/mails.php index 159c03589b1..2b50008d58a 100644 --- a/htdocs/admin/mails.php +++ b/htdocs/admin/mails.php @@ -271,12 +271,12 @@ if ($action == 'edit') { jQuery(".drag").hide(); jQuery("#MAIN_MAIL_EMAIL_TLS").val(0); - jQuery("#MAIN_MAIL_EMAIL_TLS").attr(\'disabled\', \'disabled\'); + jQuery("#MAIN_MAIL_EMAIL_TLS").prop("disabled", true); '; if ($linuxlike) { - print ' jQuery("#MAIN_MAIL_SMTP_SERVER").attr(\'disabled\', \'disabled\');'; - print ' jQuery("#MAIN_MAIL_SMTP_PORT").attr(\'disabled\', \'disabled\');'; + print ' jQuery("#MAIN_MAIL_SMTP_SERVER").prop("disabled", true);'; + print ' jQuery("#MAIN_MAIL_SMTP_PORT").prop("disabled", true);'; } print ' } @@ -284,9 +284,9 @@ if ($action == 'edit') { jQuery(".drag").show(); jQuery("#MAIN_MAIL_EMAIL_TLS").val('.$conf->global->MAIN_MAIL_EMAIL_TLS.'); - jQuery("#MAIN_MAIL_EMAIL_TLS").removeAttr(\'disabled\'); - jQuery("#MAIN_MAIL_SMTP_SERVER").removeAttr(\'disabled\'); - jQuery("#MAIN_MAIL_SMTP_PORT").removeAttr(\'disabled\'); + jQuery("#MAIN_MAIL_EMAIL_TLS").removeAttr("disabled"); + jQuery("#MAIN_MAIL_SMTP_SERVER").removeAttr("disabled"); + jQuery("#MAIN_MAIL_SMTP_PORT").removeAttr("disabled"); } } initfields(); diff --git a/htdocs/admin/menus/edit.php b/htdocs/admin/menus/edit.php index 9ded9ae1f34..4ab41c7991e 100644 --- a/htdocs/admin/menus/edit.php +++ b/htdocs/admin/menus/edit.php @@ -255,12 +255,12 @@ if ($action == 'create') { if (jQuery("#topleft").val() == \'top\') { - jQuery("#menuId").attr(\'disabled\',\'disabled\'); + jQuery("#menuId").prop("disabled", true); jQuery("#menuId").val(\'\'); } else { - jQuery("#menuId").removeAttr(\'disabled\'); + jQuery("#menuId").removeAttr("disabled"); } } init_topleft(); diff --git a/htdocs/admin/system/modules.php b/htdocs/admin/system/modules.php index 47a386c1a03..1014beba184 100644 --- a/htdocs/admin/system/modules.php +++ b/htdocs/admin/system/modules.php @@ -118,7 +118,7 @@ foreach($sortorder as $numero=>$name) $idperms=""; $var=!$var; // Module - print ""; + print "'; $alt=$name.' - '.$modules_files[$numero]; if (! empty($picto[$numero])) { diff --git a/htdocs/admin/tools/dolibarr_export.php b/htdocs/admin/tools/dolibarr_export.php index 842efb3a0ac..671a9bf9eba 100644 --- a/htdocs/admin/tools/dolibarr_export.php +++ b/htdocs/admin/tools/dolibarr_export.php @@ -96,7 +96,7 @@ jQuery(document).ready(function() { jQuery("#select_sql_compat").click(function() { if (jQuery("#select_sql_compat").val() == 'POSTGRESQL') { - jQuery("#checkbox_dump_disable-add-locks").attr('checked',true); + jQuery("#checkbox_dump_disable-add-locks").prop('checked',true); } }); diff --git a/htdocs/api/README.md b/htdocs/api/README.md new file mode 100644 index 00000000000..30f90a7e13e --- /dev/null +++ b/htdocs/api/README.md @@ -0,0 +1,50 @@ +API howto +========= + +Explore the api +--------------- + +You can explore API method by using web interface : https://**yourdolibarr.tld**/htdocs/public/api/explorer/index.html (replace **yourdolibarr.tld** by real hostname of your Dolibarr installation) + +Access to the API +--------------- + +> **Warning : access to the API should (or better : must!) be secured with SSL connection** + +To access to the API you need a token to identify. When you access the API for the first time, you need to log in with user name and password to get a token. **Only** this token will allow to access API with. + +To log in with the API, use this uri : https://**yourdolibarr.tld**/htdocs/public/api/login?login=**username**&password=**password** (replace bold strings with real values) + +The token will be saved by Dolibarr for next user accesses to the API and it **must** be put into request uri as **api_key** parameter. + +Develop the API +-------------- + +The API uses Lucarast Restler framework. Please check documentation https://www.luracast.com/products/restler and examples http://help.luracast.com/restler/examples/ +Github contains also usefull informations : https://github.com/Luracast/Restler + +To implement it into Dolibarr, we need to create a specific class for object we want to use. A skeleton file is available into /dev directory : *skeleton_api_class.class.php* +The API class file must be put into object class directory, with specific file name. By example, API class file for '*myobject*' must be put as : /htdocs/*myobject*/class/api_*myobject*.class.php. Class must be named **MyobjectApi**. + +If a module provide several object, use a different name for '*myobject*' and put the file into the same directory. + +**Define url for methods** + +It is possible to specify url for API methods by simply use the PHPDoc tag **@url**. See examples : + + /** + * List contacts + * + * Get a list of contacts + * + * @url GET /contact/list + * @url GET /contact/list/{socid} + * @url GET /thirdparty/{socid}/contacts + * [...] + +**Other Annotations** +Other annotations are used, you are encouraged to read them : https://github.com/Luracast/Restler/blob/master/ANNOTATIONS.md + +PHPDoc tags can also be used to specify variables informations for API. Again, rtfm : https://github.com/Luracast/Restler/blob/master/PARAM.md + + diff --git a/htdocs/api/admin/api.php b/htdocs/api/admin/api.php new file mode 100644 index 00000000000..0e1673715d2 --- /dev/null +++ b/htdocs/api/admin/api.php @@ -0,0 +1,138 @@ + + * Copyright (C) 2005-2010 Laurent Destailleur + * Copyright (C) 2011 Juanjo Menent + * Copyright (C) 2012 Regis Houssin + * Copyright (C) 2015 Regis Houssin + * + * 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 . + */ + +/** + * \file htdocs/api/admin/api.php + * \ingroup api + * \brief Page to setup api module + */ + +require '../../main.inc.php'; +require_once DOL_DOCUMENT_ROOT.'/core/lib/admin.lib.php'; + +$langs->load("admin"); + +if (! $user->admin) + accessforbidden(); + +$actionsave=GETPOST("save"); + +// Sauvegardes parametres +if ($actionsave) +{ + $i=0; + + $db->begin(); + + $i+=dolibarr_set_const($db,'API_KEY',trim(GETPOST("API_KEY")),'chaine',0,'',$conf->entity); + + if ($i >= 1) + { + $db->commit(); + setEventMessage($langs->trans("SetupSaved")); + } + else + { + $db->rollback(); + setEventMessage($langs->trans("Error"), 'errors'); + } +} + + +/* + * View + */ + +llxHeader(); + +$linkback=''.$langs->trans("BackToModuleList").''; +print_fiche_titre($langs->trans("ApiSetup"),$linkback,'title_setup'); + +print $langs->trans("ApiDesc")."
\n"; +print "
\n"; + +print '
'; +print ''; +print ''; + +print ''; +print ""; +print ""; +print ""; +print ""; + +print ''; +print ''; +print ''; +print ''; +print ''; + +print '
".$langs->trans("Parameter")."".$langs->trans("Value")." 
'.$langs->trans("KeyForApiAccess").''; +if (! empty($conf->use_javascript_ajax)) + print ' '.img_picto($langs->trans('Generate'), 'refresh', 'id="generate_token" class="linkobject"'); +print ' 
'; + +print '
'; +print ''; +print '
'; + +print '
'; + +print '

'; + +// API endpoint +print ''.$langs->trans("ApiEndPointIs").':
'; +$url=DOL_MAIN_URL_ROOT.'/public/api/'; +print img_picto('','object_globe.png').' '.$url."
\n"; +$url=DOL_MAIN_URL_ROOT.'/public/api/.json'; +print img_picto('','object_globe.png').' '.$url."
\n"; + +// Explorer +print ''.$langs->trans("ApiExporerIs").':
'; +$url=DOL_MAIN_URL_ROOT.'/public/api/explorer/index.html'; +print img_picto('','object_globe.png').' '.$url."
\n"; + +print '
'; + + +print '
'; +print $langs->trans("OnlyActiveElementsAreExposed", DOL_URL_ROOT.'/admin/modules.php'); + +if (! empty($conf->use_javascript_ajax)) +{ + print "\n".''; +} + + +llxFooter(); +$db->close(); diff --git a/htdocs/api/class/api.class.php b/htdocs/api/class/api.class.php new file mode 100644 index 00000000000..9f03ae84acf --- /dev/null +++ b/htdocs/api/class/api.class.php @@ -0,0 +1,215 @@ + + * + * 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 . + */ + +use Luracast\Restler\Restler; +use Luracast\Restler\RestException; + +require_once DOL_DOCUMENT_ROOT.'/user/class/user.class.php'; + +/** + * Class for API + * + */ +class DolibarrApi +{ + + /** + * @var DoliDb $db Database object + */ + static protected $db; + + /** + * @var Restler $r Restler object + */ + var $r; + + /** + * Constructor + * + * @param DoliDb $db Database handler + */ + function __construct($db) { + $this->db = $db; + $this->r = new Restler(); + } + + /** + * Executed method when API is called without parameter + * + * Display a short message an return a http code 200 + * + * @return array + */ + function index() + { + return array( + 'success' => array( + 'code' => 200, + 'message' => __class__.' is up and running!' + ) + ); + } + + + /** + * Clean sensible object datas + * + * @param object $object Object to clean + * @return array Array of cleaned object properties + * + * @todo use an array for properties to clean + * + */ + function _cleanObjectDatas($object) { + + // Remove $db object property for object + unset($object->db); + + // If object has lines, remove $db property + if(isset($object->lines) && count($object->lines) > 0) { + for($i=0; $i < count($object->lines); $i++) { + $this->_cleanObjectDatas($object->lines[$i]); + } + } + + // If object has linked objects, remove $db property + if(isset($object->linkedObjects) && count($object->linkedObjects) > 0) { + foreach($object->linkedObjects as $type_object => $linked_object) { + foreach($linked_object as $object2clean) { + $this->_cleanObjectDatas($object2clean); + } + } + } + return $object; + } + + /** + * Check user access to a resource + * + * Check access by user to a given resource + * + * @param string $resource element to check + * @param int $resource_id Object ID if we want to check a particular record (optional) is linked to a owned thirdparty (optional). + * @param type $dbtablename 'TableName&SharedElement' with Tablename is table where object is stored. SharedElement is an optional key to define where to check entity. Not used if objectid is null (optional) + * @param string $feature2 Feature to check, second level of permission (optional). Can be or check with 'level1|level2'. + * @param string $dbt_keyfield Field name for socid foreign key if not fk_soc. Not used if objectid is null (optional) + * @param string $dbt_select Field name for select if not rowid. Not used if objectid is null (optional) + * @throws RestException + */ + static function _checkAccessToResource($resource, $resource_id=0, $dbtablename='', $feature2='', $dbt_keyfield='fk_soc', $dbt_select='rowid') { + + // Features/modules to check + $featuresarray = array($resource); + if (preg_match('/&/', $resource)) { + $featuresarray = explode("&", $resource); + } + else if (preg_match('/\|/', $resource)) { + $featuresarray = explode("|", $resource); + } + + // More subfeatures to check + if (! empty($feature2)) { + $feature2 = explode("|", $feature2); + } + + return checkUserAccessToObject(DolibarrApiAccess::$user, $featuresarray,$resource_id,$dbtablename,$feature2,$dbt_keyfield,$dbt_select); + } +} + +/** + * API init + * + */ +class DolibarrApiInit extends DolibarrApi +{ + + function __construct() { + global $db; + $this->db = $db; + } + + /** + * Login + * + * Log user with username and password + * + * @param string $login Username + * @param string $password User password + * @param int $entity User entity + * @return array Response status and user token + * + * @throws RestException + */ + public function login($login, $password, $entity = 0) { + + // Authentication mode + if (empty($dolibarr_main_authentication)) + $dolibarr_main_authentication = 'http,dolibarr'; + // Authentication mode: forceuser + if ($dolibarr_main_authentication == 'forceuser' && empty($dolibarr_auto_user)) + $dolibarr_auto_user = 'auto'; + // Set authmode + $authmode = explode(',', $dolibarr_main_authentication); + + include_once DOL_DOCUMENT_ROOT . '/core/lib/security2.lib.php'; + $login = checkLoginPassEntity($login, $password, $entity, $authmode); + if (empty($login)) + { + throw new RestException(403, 'Access denied'); + } + + // Generate token for user + $token = dol_hash($login.uniqid().$conf->global->MAIN_API_KEY,1); + + // We store API token into database + $sql = "UPDATE ".MAIN_DB_PREFIX."user"; + $sql.= " SET api_key = '".$this->db->escape($token)."'"; + $sql.= " WHERE login = '".$this->db->escape($login)."'"; + + dol_syslog(get_class($this)."::login", LOG_DEBUG); // No log + $result = $this->db->query($sql); + if (!$result) + { + throw new RestException(500, 'Error when updating user :'.$this->db->error_msg); + } + + //return token + return array( + 'success' => array( + 'code' => 200, + 'token' => $token, + 'message' => 'Welcome ' . $login + ) + ); + } + + /** + * Get status (Dolibarr version) + * + * @access protected + * @class DolibarrApiAccess {@requires admin} + */ + function status() { + require_once DOL_DOCUMENT_ROOT . '/core/lib/functions.lib.php'; + return array( + 'success' => array( + 'code' => 200, + 'dolibarr_version' => DOL_VERSION + ) + ); + } +} diff --git a/htdocs/api/class/api_access.class.php b/htdocs/api/class/api_access.class.php new file mode 100644 index 00000000000..1e3d43ed9db --- /dev/null +++ b/htdocs/api/class/api_access.class.php @@ -0,0 +1,144 @@ + + * + * 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 . + */ + +use \Luracast\Restler\iAuthenticate; +use \Luracast\Restler\Resources; +use \Luracast\Restler\Defaults; +use Luracast\Restler\RestException; + + +/** + * Dolibarr API access class + * + */ +class DolibarrApiAccess implements iAuthenticate +{ + const REALM = 'Restricted Dolibarr API'; + + /** + * @var array $requires role required by API method user / external / admin + */ + public static $requires = array('user','external','admin'); + + /** + * @var string $role user role + */ + public static $role = 'user'; + + /** + * @var User $user Loggued user + */ + public static $user = ''; + + // @codingStandardsIgnoreStart + + /** + * @return string string to be used with WWW-Authenticate header + * @example Basic + * @example Digest + * @example OAuth + */ + public function __getWWWAuthenticateString(); + + /** + * Check access + * + * @return boolean + */ + public function _isAllowed() + { + // @codingStandardsIgnoreEnd + global $db; + + $stored_key = ''; + + $userClass = Defaults::$userIdentifierClass; + + if (isset($_GET['api_key'])) { + $sql = "SELECT u.login, u.datec, u.api_key, "; + $sql.= " u.tms as date_modification, u.entity"; + $sql.= " FROM ".MAIN_DB_PREFIX."user as u"; + $sql.= " WHERE u.api_key = '".$db->escape($_GET['api_key'])."'"; + + if ($db->query($sql)) + { + if ($db->num_rows($result)) + { + $obj = $db->fetch_object($result); + $login = $obj->login; + $stored_key = $obj->api_key; + } + } + else { + throw new RestException(503, 'Error when fetching user api_key :'.$db->error_msg); + } + + if ( $stored_key != $_GET['api_key']) { + $userClass::setCacheIdentifier($_GET['api_key']); + return false; + } + + $fuser = new User($db); + if(! $fuser->fetch('',$login)) { + throw new RestException(503, 'Error when fetching user :'.$fuser->error); + } + $fuser->getrights(); + static::$user = $fuser; + + if($fuser->societe_id) + static::$role = 'external'; + + if($fuser->admin) + static::$role = 'admin'; + } + else + { + return false; + } + + $userClass::setCacheIdentifier(static::$role); + Resources::$accessControlFunction = 'DolibarrApiAccess::verifyAccess'; + return in_array(static::$role, (array) static::$requires) || static::$role == 'admin'; + } + + // @codingStandardsIgnoreStart + public function __getWWWAuthenticateString() + { + return ''; + } + // @codingStandardsIgnoreEnd + + /** + * Verify access + * + * @param array $m Properties of method + * + * @access private + */ + public static function verifyAccess(array $m) + { + $requires = isset($m['class']['DolibarrApiAccess']['properties']['requires']) + ? $m['class']['DolibarrApiAccess']['properties']['requires'] + : false; + + + return $requires + ? static::$role == 'admin' || in_array(static::$role, (array) $requires) + : true; + + } +} diff --git a/htdocs/api/index.php b/htdocs/api/index.php new file mode 100644 index 00000000000..3ac00faa65f --- /dev/null +++ b/htdocs/api/index.php @@ -0,0 +1,8 @@ +clicktodial->enabled)) { @@ -64,13 +65,15 @@ if ($resql) if ($obj) { $found = $obj->name; + } else { + $found = $notfound; } $db->free($resql); } else { dol_print_error($db,'Error'); + $found = $error; } echo $found; - diff --git a/htdocs/barcode/printsheet.php b/htdocs/barcode/printsheet.php index 1a518b1859c..36d9117eefe 100644 --- a/htdocs/barcode/printsheet.php +++ b/htdocs/barcode/printsheet.php @@ -313,28 +313,28 @@ jQuery(document).ready(function() { { if (jQuery("#fillmanually:checked").val() == "fillmanually") { - jQuery("#submitproduct").attr(\'disabled\',\'disabled\'); - jQuery("#submitthirdparty").attr(\'disabled\',\'disabled\'); - jQuery("#search_productid").attr(\'disabled\',\'disabled\'); - jQuery("#socid").attr(\'disabled\',\'disabled\'); + jQuery("#submitproduct").prop("disabled", true); + jQuery("#submitthirdparty").prop("disabled", true); + jQuery("#search_productid").prop("disabled", true); + jQuery("#socid").prop("disabled", true); jQuery(".showforproductselector").hide(); jQuery(".showforthirdpartyselector").hide(); } if (jQuery("#fillfromproduct:checked").val() == "fillfromproduct") { - jQuery("#submitproduct").removeAttr(\'disabled\'); - jQuery("#submitthirdparty").attr(\'disabled\',\'disabled\'); - jQuery("#search_productid").removeAttr(\'disabled\'); - jQuery("#socid").attr(\'disabled\',\'disabled\'); + jQuery("#submitproduct").removeAttr("disabled"); + jQuery("#submitthirdparty").prop("disabled", true); + jQuery("#search_productid").removeAttr("disabled"); + jQuery("#socid").prop("disabled", true); jQuery(".showforproductselector").show(); jQuery(".showforthirdpartyselector").hide(); } if (jQuery("#fillfromthirdparty:checked").val() == "fillfromthirdparty") { - jQuery("#submitproduct").attr(\'disabled\',\'disabled\'); - jQuery("#submitthirdparty").removeAttr(\'disabled\'); - jQuery("#search_productid").attr(\'disabled\',\'disabled\'); - jQuery("#socid").removeAttr(\'disabled\'); + jQuery("#submitproduct").prop("disabled", true); + jQuery("#submitthirdparty").removeAttr("disabled"); + jQuery("#search_productid").prop("disabled", true); + jQuery("#socid").removeAttr("disabled"); jQuery(".showforproductselector").hide(); jQuery(".showforthirdpartyselector").show(); } @@ -348,11 +348,11 @@ jQuery(document).ready(function() { { if (jQuery("#select_fk_barcode_type").val() > 0 && jQuery("#forbarcode").val()) { - jQuery("#submitformbarcodegen").removeAttr(\'disabled\'); + jQuery("#submitformbarcodegen").removeAttr("disabled"); } else { - jQuery("#submitformbarcodegen").attr(\'disabled\',\'disabled\'); + jQuery("#submitformbarcodegen").prop("disabled", true); } } init_gendoc_button(); diff --git a/htdocs/categories/class/api_category.class.php b/htdocs/categories/class/api_category.class.php new file mode 100644 index 00000000000..364147d097d --- /dev/null +++ b/htdocs/categories/class/api_category.class.php @@ -0,0 +1,355 @@ + + * + * 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 . + */ + + use Luracast\Restler\RestException; + + require_once DOL_DOCUMENT_ROOT.'/categories/class/categorie.class.php'; + +/** + * API class for category object + * + * @smart-auto-routing false + * @access protected + * @class DolibarrApiAccess {@requires user,external} + * + * + */ +class CategoryApi extends DolibarrApi +{ + /** + * @var array $FIELDS Mandatory fields, checked when create and update object + */ + static $FIELDS = array( + 'label', + 'type' + ); + + static $TYPES = array( + 0 => 'product', + 1 => 'supplier', + 2 => 'customer', + 3 => 'member', + 4 => 'contact', + ); + + /** + * @var Categorie $category {@type Categorie} + */ + public $category; + + /** + * Constructor + * + * @url GET category/ + * + */ + function __construct() + { + global $db, $conf; + $this->db = $db; + $this->category = new Categorie($this->db); + + } + + /** + * Get properties of a category object + * + * Return an array with category informations + * + * @param int $id ID of category + * @return array|mixed data without useless information + * + * @url GET category/{id} + * @throws RestException + */ + function get($id) + { + if(! DolibarrApiAccess::$user->rights->categorie->lire) { + throw new RestException(401); + } + + $result = $this->category->fetch($id); + if( ! $result ) { + throw new RestException(404, 'category not found'); + } + + if( ! DolibarrApi::_checkAccessToResource('category',$this->category->id)) { + throw new RestException(401, 'Access not allowed for login '.DolibarrApiAccess::$user->login); + } + + return $this->_cleanObjectDatas($this->category); + } + + /** + * List categories + * + * Get a list of categories + * + * @param string $type Type of category ('member', 'customer', 'supplier', 'product', 'contact') + * @param string $sortfield Sort field + * @param string $sortorder Sort order + * @param int $limit Limit for list + * @param int $page Page number + * @return array Array of category objects + * + * @url GET /category/list + */ + function getList($type='product', $sortfield = "s.rowid", $sortorder = 'ASC', $limit = 0, $page = 0) { + global $db, $conf; + + $obj_ret = array(); + + if(! DolibarrApiAccess::$user->rights->categorie->lire) { + throw new RestException(401); + } + + $sql = "SELECT s.rowid"; + $sql.= " FROM ".MAIN_DB_PREFIX."categorie as s"; + $sql.= ' WHERE s.entity IN ('.getEntity('categorie', 1).')'; + $sql.= ' AND s.type='.array_search($type,CategoryApi::$TYPES); + + $nbtotalofrecords = 0; + if (empty($conf->global->MAIN_DISABLE_FULL_SCANLIST)) + { + $result = $db->query($sql); + $nbtotalofrecords = $db->num_rows($result); + } + + $sql.= $db->order($sortfield, $sortorder); + if ($limit) { + if ($page < 0) + { + $page = 0; + } + $offset = $limit * $page; + + $sql.= $db->plimit($limit + 1, $offset); + } + + $result = $db->query($sql); + if ($result) + { + $num = $db->num_rows($result); + while ($i < $num) + { + $obj = $db->fetch_object($result); + $category_static = new Categorie($db); + if($category_static->fetch($obj->rowid)) { + $obj_ret[] = parent::_cleanObjectDatas($category_static); + } + $i++; + } + } + else { + throw new RestException(503, 'Error when retrieve category list : '.$category_static->error); + } + if( ! count($obj_ret)) { + throw new RestException(404, 'No category found'); + } + return $obj_ret; + } + + /** + * Get member categories list + * + * @param string $sortfield Sort field + * @param string $sortorder Sort order + * @param int $limit Limit for list + * @param int $page Page number + * @return mixed + * + * @url GET /category/list/member + */ + function getListCategoryMember($sortfield = "s.rowid", $sortorder = 'ASC', $limit = 0, $page = 0) { + return $this->getList('member', $sortfield, $sortorder, $limit, $page); + } + + /** + * Get customer categories list + * + * @param string $sortfield Sort field + * @param string $sortorder Sort order + * @param int $limit Limit for list + * @param int $page Page number + * + * @return mixed + * + * @url GET /category/list/customer + */ + function getListCategoryCustomer($sortfield = "s.rowid", $sortorder = 'ASC', $limit = 0, $page = 0) { + return $this->getList('customer', $sortfield, $sortorder, $limit, $page); + } + + /** + * Get supplier categories list + * + * @param string $sortfield Sort field + * @param string $sortorder Sort order + * @param int $limit Limit for list + * @param int $page Page number + * + * @return mixed + * + * @url GET /category/list/supplier + */ + function getListCategorySupplier($sortfield = "s.rowid", $sortorder = 'ASC', $limit = 0, $page = 0) { + return $this->getList('supplier', $sortfield, $sortorder, $limit, $page); + } + + /** + * Get product categories list + * + * @param string $sortfield Sort field + * @param string $sortorder Sort order + * @param int $limit Limit for list + * @param int $page Page number + * + * @return mixed + * + * @url GET /category/list/product + */ + function getListCategoryProduct($sortfield = "s.rowid", $sortorder = 'ASC', $limit = 0, $page = 0) { + return $this->getList('product', $sortfield, $sortorder, $limit, $page); + } + + /** + * Get contact categories list + * + * @param string $sortfield Sort field + * @param string $sortorder Sort order + * @param int $limit Limit for list + * @param int $page Page number + * @return mixed + * + * @url GET /category/list/contact + */ + function getListCategoryContact($sortfield = "s.rowid", $sortorder = 'ASC', $limit = 0, $page = 0) { + return $this->getList('contact', $sortfield, $sortorder, $limit, $page); + } + + /** + * Create category object + * + * @param array $request_data Request data + * @return int ID of category + * + * @url POST category/ + */ + function post($request_data = NULL) + { + if(! DolibarrApiAccess::$user->rights->categorie->creer) { + throw new RestException(401); + } + // Check mandatory fields + $result = $this->_validate($request_data); + + foreach($request_data as $field => $value) { + $this->category->$field = $value; + } + if($this->category->create(DolibarrApiAccess::$user) < 0) { + throw new RestException(503, 'Error when create category : '.$this->category->error); + } + return $this->category->id; + } + + /** + * Update category + * + * @param int $id Id of category to update + * @param array $request_data Datas + * @return int + * + * @url PUT category/{id} + */ + function put($id, $request_data = NULL) + { + if(! DolibarrApiAccess::$user->rights->categorie->creer) { + throw new RestException(401); + } + + $result = $this->category->fetch($id); + if( ! $result ) { + throw new RestException(404, 'category not found'); + } + + if( ! DolibarrApi::_checkAccessToResource('category',$this->category->id)) { + throw new RestException(401, 'Access not allowed for login '.DolibarrApiAccess::$user->login); + } + + foreach($request_data as $field => $value) { + $this->category->$field = $value; + } + + if($this->category->update(DolibarrApiAccess::$user)) + return $this->get ($id); + + return false; + } + + /** + * Delete category + * + * @param int $id Category ID + * @return array + * + * @url DELETE category/{id} + */ + function delete($id) + { + if(! DolibarrApiAccess::$user->rights->categorie->supprimer) { + throw new RestException(401); + } + $result = $this->category->fetch($id); + if( ! $result ) { + throw new RestException(404, 'category not found'); + } + + if( ! DolibarrApi::_checkAccessToResource('category',$this->category->id)) { + throw new RestException(401, 'Access not allowed for login '.DolibarrApiAccess::$user->login); + } + + if (! $this->category->delete(DolibarrApiAccess::$user)) { + throw new RestException(401,'error when delete category'); + } + + return array( + 'success' => array( + 'code' => 200, + 'message' => 'Category deleted' + ) + ); + } + + /** + * Validate fields before create or update object + * + * @param array $data Data to validate + * @return array + * + * @throws RestException + */ + function _validate($data) + { + $category = array(); + foreach (CategoryApi::$FIELDS as $field) { + if (!isset($data[$field])) + throw new RestException(400, "$field field missing"); + $category[$field] = $data[$field]; + } + return $category; + } +} diff --git a/htdocs/categories/viewcat.php b/htdocs/categories/viewcat.php index a1c544f338b..927474a5e44 100644 --- a/htdocs/categories/viewcat.php +++ b/htdocs/categories/viewcat.php @@ -254,7 +254,7 @@ else { $var=!$var; print "\t\n"; - print "\t\t"; + print "\t\t".''; print "".$cat->label.""; print "\n"; print "\t\t".''.$cat->description."\n"; diff --git a/htdocs/comm/action/card.php b/htdocs/comm/action/card.php index 7e842ae3678..eb88215fa20 100644 --- a/htdocs/comm/action/card.php +++ b/htdocs/comm/action/card.php @@ -570,11 +570,11 @@ if ($action == 'create') $(".fulldayendmin").removeAttr("disabled"); $("#p2").removeAttr("disabled"); } else { - $(".fulldaystarthour").attr("disabled","disabled").val("00"); - $(".fulldaystartmin").attr("disabled","disabled").val("00"); - $(".fulldayendhour").attr("disabled","disabled").val("23"); - $(".fulldayendmin").attr("disabled","disabled").val("59"); - $("#p2").removeAttr("disabled"); + $(".fulldaystarthour").prop("disabled", true).val("00"); + $(".fulldaystartmin").prop("disabled", true).val("00"); + $(".fulldayendhour").prop("disabled", true).val("23"); + $(".fulldayendmin").prop("disabled", true).val("59"); + $("#p2").removeAttr("disabled"); } } setdatefields(); @@ -841,10 +841,10 @@ if ($id > 0) $(".fulldayendhour").removeAttr("disabled"); $(".fulldayendmin").removeAttr("disabled"); } else { - $(".fulldaystarthour").attr("disabled","disabled").val("00"); - $(".fulldaystartmin").attr("disabled","disabled").val("00"); - $(".fulldayendhour").attr("disabled","disabled").val("23"); - $(".fulldayendmin").attr("disabled","disabled").val("59"); + $(".fulldaystarthour").prop("disabled", true).val("00"); + $(".fulldaystartmin").prop("disabled", true).val("00"); + $(".fulldayendhour").prop("disabled", true).val("23"); + $(".fulldayendmin").prop("disabled", true).val("59"); } } setdatefields(); @@ -986,7 +986,7 @@ if ($id > 0) } // Priority - print ''.$langs->trans("Priority").''; + print ''.$langs->trans("Priority").''; print ''; print ''; @@ -1174,7 +1174,7 @@ if ($id > 0) } // Priority - print ''.$langs->trans("Priority").''; + print ''.$langs->trans("Priority").''; print ($object->priority?$object->priority:''); print ''; diff --git a/htdocs/commande/class/api_commande.class.php b/htdocs/commande/class/api_commande.class.php new file mode 100644 index 00000000000..e8266fe6383 --- /dev/null +++ b/htdocs/commande/class/api_commande.class.php @@ -0,0 +1,335 @@ + + * + * 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 . + */ + + use Luracast\Restler\RestException; + + require_once DOL_DOCUMENT_ROOT.'/commande/class/commande.class.php'; + +/** + * API class for commande object + * + * @smart-auto-routing false + * @access protected + * @class DolibarrApiAccess {@requires user,external} + * + * @category Api + * @package Api + * + * + */ +class CommandeApi extends DolibarrApi +{ + + /** + * @var array $FIELDS Mandatory fields, checked when create and update object + */ + static $FIELDS = array( + 'socid' + ); + + /** + * @var Commande $commande {@type Commande} + */ + public $commande; + + /** + * Constructor + * + * @url GET order/ + * + */ + function __construct() + { + global $db, $conf; + $this->db = $db; + $this->commande = new Commande($this->db); + } + + /** + * Get properties of a commande object + * + * Return an array with commande informations + * + * @param int $id ID of order + * @param string $ref Ref of object + * @param string $ref_ext External reference of object + * @param string $ref_int Internal reference of other object + * @return array|mixed data without useless information + * + * @url GET order/{id} + * @throws RestException + */ + function get($id='',$ref='', $ref_ext='', $ref_int='') + { + if(! DolibarrApiAccess::$user->rights->commande->lire) { + throw new RestException(401); + } + + $result = $this->commande->fetch($id); + if( ! $result ) { + throw new RestException(404, 'Order not found'); + } + + if( ! DolibarrApi::_checkAccessToResource('commande',$this->commande->id)) { + throw new RestException(401, 'Access not allowed for login '.DolibarrApiAccess::$user->login); + } + + $this->commande->fetchObjectLinked(); + return $this->_cleanObjectDatas($this->commande); + } + + /** + * List orders + * + * Get a list of orders + * + * @param int $mode Use this param to filter list + * @param string $sortfield Sort field + * @param string $sortorder Sort order + * @param int $limit Limit for list + * @param int $page Page number + * + * @url GET /order/list + * @return array Array of order objects + */ + function getList($mode=0, $sortfield = "s.rowid", $sortorder = 'ASC', $limit = 0, $page = 0) { + global $db, $conf; + + $obj_ret = array(); + + $socid = DolibarrApiAccess::$user->societe_id ? DolibarrApiAccess::$user->societe_id : ''; + + // If the internal user must only see his customers, force searching by him + if (! DolibarrApiAccess::$user->rights->societe->client->voir && !$socid) $search_sale = DolibarrApiAccess::$user->id; + + $sql = "SELECT s.rowid"; + if ((!DolibarrApiAccess::$user->rights->societe->client->voir && !$socid) || $search_sale > 0) $sql .= ", sc.fk_soc, sc.fk_user"; // We need these fields in order to filter by sale (including the case where the user can only see his prospects) + $sql.= " FROM ".MAIN_DB_PREFIX."commande as s"; + + if ((!DolibarrApiAccess::$user->rights->societe->client->voir && !$socid) || $search_sale > 0) $sql.= ", ".MAIN_DB_PREFIX."societe_commerciaux as sc"; // We need this table joined to the select in order to filter by sale + + // Example of use $mode + //if ($mode == 1) $sql.= " AND s.client IN (1, 3)"; + //if ($mode == 2) $sql.= " AND s.client IN (2, 3)"; + + $sql.= ' WHERE s.entity IN ('.getEntity('commande', 1).')'; + if ((!DolibarrApiAccess::$user->rights->societe->client->voir && !$socid) || $search_sale > 0) $sql.= " AND s.fk_soc = sc.fk_soc"; + if ($socid) $sql.= " AND s.fk_soc = ".$socid; + if ($search_sale > 0) $sql.= " AND s.rowid = sc.fk_soc"; // Join for the needed table to filter by sale + + // Insert sale filter + if ($search_sale > 0) + { + $sql .= " AND sc.fk_user = ".$search_sale; + } + + $nbtotalofrecords = 0; + if (empty($conf->global->MAIN_DISABLE_FULL_SCANLIST)) + { + $result = $db->query($sql); + $nbtotalofrecords = $db->num_rows($result); + } + + $sql.= $db->order($sortfield, $sortorder); + if ($limit) { + if ($page < 0) + { + $page = 0; + } + $offset = $limit * $page; + + $sql.= $db->plimit($limit + 1, $offset); + } + + $result = $db->query($sql); + + if ($result) + { + $num = $db->num_rows($result); + while ($i < $num) + { + $obj = $db->fetch_object($result); + $commande_static = new Commande($db); + if($commande_static->fetch($obj->rowid)) { + $obj_ret[] = parent::_cleanObjectDatas($commande_static); + } + $i++; + } + } + else { + throw new RestException(503, 'Error when retrieve commande list'); + } + if( ! count($obj_ret)) { + throw new RestException(404, 'No commande found'); + } + return $obj_ret; + } + + /** + * Create order object + * + * @param array $request_data Request datas + * + * @url POST order/ + * + * @return int ID of commande + */ + function post($request_data = NULL) + { + if(! DolibarrApiAccess::$user->rights->commande->creer) { + throw new RestException(401); + } + // Check mandatory fields + $result = $this->_validate($request_data); + + foreach($request_data as $field => $value) { + $this->commande->$field = $value; + } + if(! $this->commande->create(DolibarrApiAccess::$user) ) { + throw new RestException(401); + } + + return $this->commande->ref; + } + + /** + * Update order + * + * @param int $id Id of commande to update + * @param array $request_data Datas + * + * @url PUT order/{id} + * + * @return int + */ + function put($id, $request_data = NULL) + { + if(! DolibarrApiAccess::$user->rights->commande->creer) { + throw new RestException(401); + } + + $result = $this->commande->fetch($id); + if( ! $result ) { + throw new RestException(404, 'Commande not found'); + } + + if( ! DolibarrApi::_checkAccessToResource('commande',$this->commande->id)) { + throw new RestException(401, 'Access not allowed for login '.DolibarrApiAccess::$user->login); + } + + foreach($request_data as $field => $value) { + $this->commande->$field = $value; + } + + if($this->commande->update($id, DolibarrApiAccess::$user,1,'','','update')) + return $this->get ($id); + + return false; + } + + /** + * Delete order + * + * @param int $id Order ID + * + * @url DELETE order/{id} + * + * @return array + */ + function delete($id) + { + if(! DolibarrApiAccess::$user->rights->commande->supprimer) { + throw new RestException(401); + } + $result = $this->commande->fetch($id); + if( ! $result ) { + throw new RestException(404, 'Order not found'); + } + + if( ! DolibarrApi::_checkAccessToResource('commande',$this->commande->id)) { + throw new RestException(401, 'Access not allowed for login '.DolibarrApiAccess::$user->login); + } + + if( ! $this->commande->delete(DolibarrApiAccess::$user)) { + throw new RestException(500, 'Error when delete order : '.$this->commande->error); + } + + return array( + 'success' => array( + 'code' => 200, + 'message' => 'Order deleted' + ) + ); + + } + + /** + * Validate an order + * + * @param int $id Order ID + * @param int $idwarehouse Warehouse ID + * + * @url GET order/{id}/validate + * @url POST order/{id}/validate + * + * @return array + * + */ + function validOrder($id, $idwarehouse=0) + { + if(! DolibarrApiAccess::$user->rights->commande->creer) { + throw new RestException(401); + } + $result = $this->commande->fetch($id); + if( ! $result ) { + throw new RestException(404, 'Order not found'); + } + + if( ! DolibarrApi::_checkAccessToResource('commande',$this->commande->id)) { + throw new RestException(401, 'Access not allowed for login '.DolibarrApiAccess::$user->login); + } + + if( ! $this->commande->valid(DolibarrApiAccess::$user, $idwarehouse)) { + throw new RestException(500, 'Error when validate order'); + } + + return array( + 'success' => array( + 'code' => 200, + 'message' => 'Order validated' + ) + ); + } + + /** + * Validate fields before create or update object + * + * @param array $data Array with data to verify + * @return array + * @throws RestException + */ + function _validate($data) + { + $commande = array(); + foreach (CommandeApi::$FIELDS as $field) { + if (!isset($data[$field])) + throw new RestException(400, "$field field missing"); + $commande[$field] = $data[$field]; + } + return $commande; + } +} diff --git a/htdocs/commande/orderstoinvoice.php b/htdocs/commande/orderstoinvoice.php index 39253325716..55797308fb2 100644 --- a/htdocs/commande/orderstoinvoice.php +++ b/htdocs/commande/orderstoinvoice.php @@ -512,10 +512,10 @@ if (($action != 'create' && $action != 'add') || !$error) @@ -655,12 +655,12 @@ if (($action != 'create' && $action != 'add') || !$error) print ''.$objp->ref_client.''; // Order date - print ''; + print ''; print dol_print_date($db->jdate($objp->date_commande),'day'); print ''; //Delivery date - print ''; + print ''; print dol_print_date($db->jdate($objp->date_livraison),'day'); print ''; diff --git a/htdocs/compta/bank/account.php b/htdocs/compta/bank/account.php index 536bdc91662..7184f0c71f1 100644 --- a/htdocs/compta/bank/account.php +++ b/htdocs/compta/bank/account.php @@ -776,11 +776,11 @@ if ($id > 0 || ! empty($ref)) { if ($total >= 0) { - print ' '.price($total).''; + print ' '.price($total).''; } else { - print ' '.price($total).''; + print ' '.price($total).''; } } else @@ -791,7 +791,7 @@ if ($id > 0 || ! empty($ref)) // Transaction reconciliated or edit link if ($objp->rappro && $object->canBeConciliated() > 0) // If line not conciliated and account can be conciliated { - print ''; + print ''; print ''; print img_edit(); print ''; @@ -844,7 +844,7 @@ if ($id > 0 || ! empty($ref)) if ($sep > 0) print ' '; // If we had at least one line in future else print $langs->trans("CurrentBalance"); print ' '.$object->currency_code.''; - print ''.price($total, 0, $langs, 0, 0, -1, $object->currency_code).''; + print ''.price($total, 0, $langs, 0, 0, -1, $object->currency_code).''; print ' '; print ''; } diff --git a/htdocs/compta/bank/releve.php b/htdocs/compta/bank/releve.php index 1b5549b2c4e..5293f518738 100644 --- a/htdocs/compta/bank/releve.php +++ b/htdocs/compta/bank/releve.php @@ -489,15 +489,15 @@ else if ($objp->amount < 0) { $totald = $totald + abs($objp->amount); - print ''.price($objp->amount * -1)." \n"; + print ''.price($objp->amount * -1)." \n"; } else { $totalc = $totalc + abs($objp->amount); - print " ".price($objp->amount)."\n"; + print ' '.price($objp->amount)."\n"; } - print "".price($total)."\n"; + print ''.price($total)."\n"; if ($user->rights->banque->modifier || $user->rights->banque->consolidate) { diff --git a/htdocs/compta/bank/treso.php b/htdocs/compta/bank/treso.php index 292742315bb..e62c5203ea6 100644 --- a/htdocs/compta/bank/treso.php +++ b/htdocs/compta/bank/treso.php @@ -134,13 +134,13 @@ if ($_REQUEST["account"] || $_REQUEST["ref"]) $var=!$var; print ''; print ''.$langs->trans("CurrentBalance").''; - print ''.price($solde).''; + print ''.price($solde).''; print ''; $var=!$var; print ''; print ''.$langs->trans("RemainderToPay").''; - print ' '; + print ' '; print ''; @@ -330,7 +330,7 @@ if ($_REQUEST["account"] || $_REQUEST["ref"]) $var=!$var; print ''; print ''.$langs->trans("FutureBalance").' ('.$acct->currency_code.')'; - print ''.price($solde, 0, $langs, 0, 0, -1, $acct->currency_code).''; + print ''.price($solde, 0, $langs, 0, 0, -1, $acct->currency_code).''; print ''; print ""; diff --git a/htdocs/compta/facture.php b/htdocs/compta/facture.php index 1aaca07b57d..556da5db0ce 100644 --- a/htdocs/compta/facture.php +++ b/htdocs/compta/facture.php @@ -2099,7 +2099,7 @@ if ($action == 'create') print ''; @@ -2154,7 +2154,7 @@ if ($action == 'create') print ''; @@ -2200,7 +2200,7 @@ if ($action == 'create') // Show credit note options only if we checked credit note print ' diff --git a/htdocs/compta/facture/prelevement.php b/htdocs/compta/facture/prelevement.php index 9d8b9123b68..6954ad46249 100644 --- a/htdocs/compta/facture/prelevement.php +++ b/htdocs/compta/facture/prelevement.php @@ -399,27 +399,27 @@ if ($object->id > 0) // Montants print ''.$langs->trans('AmountHT').''; - print ''.price($object->total_ht).''; + print ''.price($object->total_ht).''; print ''.$langs->trans('Currency'.$conf->currency).''; - print ''.$langs->trans('AmountVAT').''.price($object->total_tva).''; + print ''.$langs->trans('AmountVAT').''.price($object->total_tva).''; print ''.$langs->trans('Currency'.$conf->currency).''; // Amount Local Taxes if ($mysoc->localtax1_assuj=="1") //Localtax1 { print ''.$langs->transcountry("AmountLT1",$mysoc->country_code).''; - print ''.price($object->total_localtax1).''; + print ''.price($object->total_localtax1).''; print ''.$langs->trans("Currency".$conf->currency).''; } if ($mysoc->localtax2_assuj=="1") //Localtax2 { print ''.$langs->transcountry("AmountLT2",$mysoc->country_code).''; - print ''.price($object->total_localtax2).''; + print ''.price($object->total_localtax2).''; print ''.$langs->trans("Currency".$conf->currency).''; } - print ''.$langs->trans('AmountTTC').''.price($object->total_ttc).''; + print ''.$langs->trans('AmountTTC').''.price($object->total_ttc).''; print ''.$langs->trans('Currency'.$conf->currency).''; // We can also use bcadd to avoid pb with floating points @@ -428,7 +428,7 @@ if ($object->id > 0) //$resteapayer=bcadd($resteapayer,$totalavoir,$conf->global->MAIN_MAX_DECIMALS_TOT); $resteapayer = price2num($object->total_ttc - $totalpaye - $totalcreditnotes - $totaldeposits,'MT'); - print ''.$langs->trans('RemainderToPay').''.price($resteapayer).''; + print ''.$langs->trans('RemainderToPay').''.price($resteapayer).''; print ''.$langs->trans('Currency'.$conf->currency).''; // Statut diff --git a/htdocs/compta/localtax/index.php b/htdocs/compta/localtax/index.php index 33ebfd3b1be..1e8570d7378 100644 --- a/htdocs/compta/localtax/index.php +++ b/htdocs/compta/localtax/index.php @@ -86,7 +86,7 @@ function pt ($db, $sql, $date) $i++; } - print ''.$langs->trans("Total")." :".price($total)." "; + print ''.$langs->trans("Total")." :".price($total)." "; print ""; $db->free($result); @@ -253,7 +253,7 @@ for ($m = 1 ; $m < 13 ; $m++ ) { $subtotalcoll=0; $subtotalpaye=0; $subtotal=0; } } -print ''.$langs->trans("TotalToPay").':'.price($total).''; +print ''.$langs->trans("TotalToPay").':'.price($total).''; print " \n"; print ''; diff --git a/htdocs/compta/paiement/cheque/card.php b/htdocs/compta/paiement/cheque/card.php index 6b218d52430..dba17a4798c 100644 --- a/htdocs/compta/paiement/cheque/card.php +++ b/htdocs/compta/paiement/cheque/card.php @@ -414,11 +414,11 @@ if ($action == 'new') { jQuery("#checkall_'.$bid.'").click(function() { - jQuery(".checkforremise_'.$bid.'").attr(\'checked\', true); + jQuery(".checkforremise_'.$bid.'").prop(\'checked\', true); }); jQuery("#checknone_'.$bid.'").click(function() { - jQuery(".checkforremise_'.$bid.'").attr(\'checked\', false); + jQuery(".checkforremise_'.$bid.'").prop(\'checked\', false); }); }); diff --git a/htdocs/compta/tva/index.php b/htdocs/compta/tva/index.php index c0dcff6d5c3..c84c7d2b633 100644 --- a/htdocs/compta/tva/index.php +++ b/htdocs/compta/tva/index.php @@ -93,7 +93,7 @@ function pt ($db, $sql, $date) $i++; } - print ''.$langs->trans("Total")." :".price($total)." "; + print ''.$langs->trans("Total")." :".price($total)." "; print ""; $db->free($result); @@ -216,7 +216,7 @@ for ($m = 1 ; $m < 13 ; $m++ ) $subtotalcoll=0; $subtotalpaye=0; $subtotal=0; } } -print ''.$langs->trans("TotalToPay").':'.price($total).''; +print ''.$langs->trans("TotalToPay").':'.price($total).''; print " \n"; print ''; diff --git a/htdocs/contact/list.php b/htdocs/contact/list.php index 21f48e58fb3..ec8004aa18a 100644 --- a/htdocs/contact/list.php +++ b/htdocs/contact/list.php @@ -437,7 +437,7 @@ if ($result) print ''; - if ($num > $limit) print_barre_liste('', $page, $_SERVER["PHP_SELF"], '&begin='.$begin.'&view='.$view.'&userid='.$userid, $sortfield, $sortorder, '', $num, $nbtotalofrecords, ''); + if ($num > $limit) print_barre_liste('', $page, $_SERVER["PHP_SELF"], $param, $sortfield, $sortorder, '', $num, $nbtotalofrecords, ''); $db->free($result); } diff --git a/htdocs/contrat/card.php b/htdocs/contrat/card.php index 254185cc63d..cc9a394d983 100644 --- a/htdocs/contrat/card.php +++ b/htdocs/contrat/card.php @@ -1021,12 +1021,12 @@ if ($action == 'create') } // Commercial suivi - print ''.$langs->trans("TypeContact_contrat_internal_SALESREPFOLL").''; + print ''.$langs->trans("TypeContact_contrat_internal_SALESREPFOLL").''; print $form->select_dolusers(GETPOST("commercial_suivi_id")?GETPOST("commercial_suivi_id"):$user->id,'commercial_suivi_id',1,''); print ''; // Commercial signature - print ''.$langs->trans("TypeContact_contrat_internal_SALESREPSIGN").''; + print ''.$langs->trans("TypeContact_contrat_internal_SALESREPSIGN").''; print $form->select_dolusers(GETPOST("commercial_signature_id")?GETPOST("commercial_signature_id"):$user->id,'commercial_signature_id',1,''); print ''; diff --git a/htdocs/core/class/extrafields.class.php b/htdocs/core/class/extrafields.class.php index e25107e24fa..1c132502b78 100644 --- a/htdocs/core/class/extrafields.class.php +++ b/htdocs/core/class/extrafields.class.php @@ -1124,7 +1124,7 @@ class ExtraFields if (!empty($value)) { $checked=' checked '; } - $value=''; + $value=''; } elseif ($type == 'mail') { diff --git a/htdocs/core/class/html.form.class.php b/htdocs/core/class/html.form.class.php index 150ca48e477..2b8912245d5 100644 --- a/htdocs/core/class/html.form.class.php +++ b/htdocs/core/class/html.form.class.php @@ -4356,7 +4356,7 @@ class Form '; } - $out.=''; if ($show_empty) { @@ -4539,7 +4539,7 @@ class Form // Try also magic suggest // Add data-role="none" to disable jmobile decoration - $out = ''."\n"; if (is_array($array) && ! empty($array)) { if ($value_as_key) $array=array_combine($array, $array); diff --git a/htdocs/core/class/html.formactions.class.php b/htdocs/core/class/html.formactions.class.php index c656cb0432d..9322680a682 100644 --- a/htdocs/core/class/html.formactions.class.php +++ b/htdocs/core/class/html.formactions.class.php @@ -95,17 +95,17 @@ class FormActions percentage.val(value); if (defaultvalue == -1) { - percentage.attr('disabled', 'disabled'); + percentage.prop('disabled', true); $('.hideifna').hide(); } else if (defaultvalue == 0) { percentage.val(0); - percentage.attr('disabled', 'disabled'); + percentage.prop('disabled', true); $('.hideifna').show(); } else if (defaultvalue == 100) { percentage.val(100); - percentage.attr('disabled', 'disabled'); + percentage.prop('disabled', true); $('.hideifna').show(); } else { diff --git a/htdocs/core/class/html.formfile.class.php b/htdocs/core/class/html.formfile.class.php index 7870371a4ab..cecbefa0dd9 100644 --- a/htdocs/core/class/html.formfile.class.php +++ b/htdocs/core/class/html.formfile.class.php @@ -5,7 +5,8 @@ * Copyright (C) 2013 Charles-Fr BENKE * Copyright (C) 2013 Cédric Salvador * Copyright (C) 2014 Marcos García - * + * Copyright (C) 2015 Bahfir Abbes + * 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 diff --git a/htdocs/core/db/Database.interface.php b/htdocs/core/db/Database.interface.php index f27c4e75189..ab38cdf378c 100644 --- a/htdocs/core/db/Database.interface.php +++ b/htdocs/core/db/Database.interface.php @@ -4,7 +4,7 @@ * Copyright (C) 2004-2011 Laurent Destailleur * Copyright (C) 2006 Andre Cianfarani * Copyright (C) 2005-2012 Regis Houssin - * Copyright (C) 2014 Raphaël Doursenaud + * Copyright (C) 2014-2015 Raphaël Doursenaud * * 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 @@ -21,7 +21,7 @@ */ /** - * Class to manage Dolibarr database access for a Mysql database + * Class to manage Dolibarr database access for an SQL database */ interface Database { @@ -111,13 +111,6 @@ interface Database */ function error(); - /** - * Return label of manager - * - * @return string Label - */ - function getLabel(); - /** * List tables into a database * @@ -141,7 +134,7 @@ interface Database * @param string $sortorder Sort order * @return string String to provide syntax of a sort sql string */ - function order($sortfield = 0, $sortorder = 0); + function order($sortfield = null, $sortorder = null); /** * Decrypt sensitive data in database @@ -216,7 +209,7 @@ interface Database * @param string $login login * @param string $passwd password * @param string $name name of database (not used for mysql, used for pgsql) - * @param string $port Port of database server + * @param int $port Port of database server * @return resource Database access handler * @see close */ @@ -293,10 +286,10 @@ interface Database * @param string $type Type de la table * @param array $unique_keys Tableau associatifs Nom de champs qui seront clef unique => valeur * @param array $fulltext_keys Tableau des Nom de champs qui seront indexes en fulltext - * @param string $keys Tableau des champs cles noms => valeur + * @param array $keys Tableau des champs cles noms => valeur * @return int <0 if KO, >=0 if OK */ - function DDLCreateTable($table, $fields, $primary_key, $type, $unique_keys = "", $fulltext_keys = "", $keys = ""); + function DDLCreateTable($table, $fields, $primary_key, $type, $unique_keys = null, $fulltext_keys = null, $keys = null); /** * Return list of available charset that can be used to store data in database @@ -382,15 +375,15 @@ interface Database ); /** - * Convert (by PHP) a PHP server TZ string date into a Timestamps date (GMT if gm=true) - * 19700101020000 -> 3600 with TZ+1 and gmt=0 - * 19700101020000 -> 7200 whaterver is TZ if gmt=1 - * + * Convert (by PHP) a PHP server TZ string date into a Timestamps date (GMT if gm=true) + * 19700101020000 -> 3600 with TZ+1 and gmt=0 + * 19700101020000 -> 7200 whaterver is TZ if gmt=1 + * * @param string $string Date in a string (YYYYMMDDHHMMSS, YYYYMMDD, YYYY-MM-DD HH:MM:SS) - * @param int $gm 1=Input informations are GMT values, otherwise local to server TZ + * @param bool $gm 1=Input informations are GMT values, otherwise local to server TZ * @return int|string Date TMS or '' - */ - function jdate($string, $gm=false); + */ + function jdate($string, $gm=false); /** * Encrypt sensitive data in database @@ -424,7 +417,7 @@ interface Database * @param resource $resultset Fre cursor * @return void */ - function free($resultset = 0); + function free($resultset = null); /** * Close database connexion diff --git a/htdocs/core/db/DoliDB.class.php b/htdocs/core/db/DoliDB.class.php index 0aae168be79..6a7c5276025 100644 --- a/htdocs/core/db/DoliDB.class.php +++ b/htdocs/core/db/DoliDB.class.php @@ -1,6 +1,6 @@ + * Copyright (C) 2013-2015 Raphaël Doursenaud * Copyright (C) 2014-2015 Laurent Destailleur * * This program is free software; you can redistribute it and/or modify @@ -29,41 +29,43 @@ require_once DOL_DOCUMENT_ROOT .'/core/db/Database.interface.php'; */ abstract class DoliDB implements Database { - //! Database handler - public $db; - //! Database type - public $type; - //! Charset used to force charset when creating database - public $forcecharset='utf8'; - //! Collate used to force collate when creating database - public $forcecollate='utf8_general_ci'; - //! Resultset of last query - private $_results; - //! 1 if connected, else 0 - public $connected; - //! 1 if database selected, else 0 - public $database_selected; - //! Selected database name - public $database_name; - //! Database username - public $database_user; - //! Database host - public $database_host; - //! Database port - public $database_port; - //! >=1 if a transaction is opened, 0 otherwise - public $transaction_opened; - //! Last successful query - public $lastquery; - //! Last failed query - public $lastqueryerror; - //! Last error message - public $lasterror; - //! Last error number - public $lasterrno; + /** @var resource Database handler */ + public $db; + /** @var string Database type */ + public $type; + /** @var string Charset used to force charset when creating database */ + public $forcecharset='utf8'; + /** @var string Collate used to force collate when creating database */ + public $forcecollate='utf8_general_ci'; + /** @var resource Resultset of last query */ + private $_results; + /** @var bool true if connected, else false */ + public $connected; + /** @var bool true if database selected, else false */ + public $database_selected; + /** @var string Selected database name */ + public $database_name; + /** @var string Database username */ + public $database_user; + /** @var string Database host */ + public $database_host; + /** @var int Database port */ + public $database_port; + /** @var int >=1 if a transaction is opened, 0 otherwise */ + public $transaction_opened; + /** @var string Last successful query */ + public $lastquery; + /** @ar string Last failed query */ + public $lastqueryerror; + /** @var string Last error message */ + public $lasterror; + /** @var int Last error number */ + public $lasterrno; - public $ok; - public $error; + /** @var bool Status */ + public $ok; + /** @var string */ + public $error; /** * Format a SQL IF @@ -205,16 +207,6 @@ abstract class DoliDB implements Database return preg_split("/[\.,-]/",$this->getVersion()); } - /** - * Return label of manager - * - * @return string Label - */ - function getLabel() - { - return $this->label; - } - /** * Return last request executed with query() * @@ -232,9 +224,9 @@ abstract class DoliDB implements Database * @param string $sortorder Sort order * @return string String to provide syntax of a sort sql string */ - function order($sortfield=0,$sortorder=0) + function order($sortfield=null,$sortorder=null) { - if ($sortfield) + if (isset($sortfield)) { $return=''; $fields=explode(',',$sortfield); @@ -244,7 +236,9 @@ abstract class DoliDB implements Database else $return.=','; $return.=preg_replace('/[^0-9a-z_\.]/i','',$val); - if ($sortorder) $return.=' '.preg_replace('/[^0-9a-z]/i','',$sortorder); + if (isset($sortorder)) { + $return.=' '.preg_replace('/[^0-9a-z]/i','',$sortorder); + } } return $return; } @@ -270,7 +264,7 @@ abstract class DoliDB implements Database * 19700101020000 -> 7200 whaterver is TZ if gmt=1 * * @param string $string Date in a string (YYYYMMDDHHMMSS, YYYYMMDD, YYYY-MM-DD HH:MM:SS) - * @param int $gm 1=Input informations are GMT values, otherwise local to server TZ + * @param bool $gm 1=Input informations are GMT values, otherwise local to server TZ * @return int|string Date TMS or '' */ function jdate($string, $gm=false) diff --git a/htdocs/core/db/mssql.class.php b/htdocs/core/db/mssql.class.php index 3523e21bdf0..0be65dfe2e6 100644 --- a/htdocs/core/db/mssql.class.php +++ b/htdocs/core/db/mssql.class.php @@ -21,7 +21,7 @@ /** * \file htdocs/core/db/mssql.class.php - * \brief Fichier de la classe permettant de gerer une base mssql + * \brief Fichier de la classe permettant de gerer une base MSSQL */ require_once DOL_DOCUMENT_ROOT .'/core/db/DoliDB.class.php'; @@ -41,7 +41,7 @@ class DoliDBMssql extends DoliDB var $forcecollate='latin1_swedish_ci'; // Can't be static as it may be forced with a dynamic value //! Version min database const VERSIONMIN='2000'; - //! Resultset of last query + /** @var resource Resultset of last query */ private $_results; /** @@ -57,7 +57,7 @@ class DoliDBMssql extends DoliDB */ function __construct($type, $host, $user, $pass, $name='', $port=0) { - global $conf,$langs; + global $langs; $this->database_user=$user; $this->database_host=$host; @@ -66,8 +66,8 @@ class DoliDBMssql extends DoliDB if (! function_exists("mssql_connect")) { - $this->connected = 0; - $this->ok = 0; + $this->connected = false; + $this->ok = false; $this->error="Mssql PHP functions for using MSSql driver are not available in this version of PHP"; dol_syslog(get_class($this)."::DoliDBMssql : MSsql PHP functions for using MSsql driver are not available in this version of PHP",LOG_ERR); return $this->ok; @@ -75,8 +75,8 @@ class DoliDBMssql extends DoliDB if (! $host) { - $this->connected = 0; - $this->ok = 0; + $this->connected = false; + $this->ok = false; $this->error=$langs->trans("ErrorWrongHostParameter"); dol_syslog(get_class($this)."::DoliDBMssql : Erreur Connect, wrong host parameters",LOG_ERR); return $this->ok; @@ -88,14 +88,14 @@ class DoliDBMssql extends DoliDB { // Si client connecte avec charset different de celui de la base Dolibarr // (La base Dolibarr a ete forcee en this->forcecharset a l'install) - $this->connected = 1; - $this->ok = 1; + $this->connected = true; + $this->ok = true; } else { // host, login ou password incorrect - $this->connected = 0; - $this->ok = 0; + $this->connected = false; + $this->ok = false; $this->error=mssql_get_last_message(); dol_syslog(get_class($this)."::DoliDBMssql : Erreur Connect mssql_get_last_message=".$this->error,LOG_ERR); } @@ -105,15 +105,15 @@ class DoliDBMssql extends DoliDB { if ($this->select_db($name)) { - $this->database_selected = 1; + $this->database_selected = true; $this->database_name = $name; - $this->ok = 1; + $this->ok = true; } else { - $this->database_selected = 0; + $this->database_selected = false; $this->database_name = ''; - $this->ok = 0; + $this->ok = false; $this->error=$this->error(); dol_syslog(get_class($this)."::DoliDBMssql : Erreur Select_db ".$this->error,LOG_ERR); } @@ -121,7 +121,7 @@ class DoliDBMssql extends DoliDB else { // Pas de selection de base demandee, ok ou ko - $this->database_selected = 0; + $this->database_selected = false; } return $this->ok; @@ -157,8 +157,8 @@ class DoliDBMssql extends DoliDB * @param string $login login * @param string $passwd password * @param string $name name of database (not used for mysql, used for pgsql) - * @param string $port Port of database server - * @return resource Database access handler + * @param int $port Port of database server + * @return false|resource|true Database access handler * @see close */ function connect($host, $login, $passwd, $name, $port=0) @@ -212,7 +212,7 @@ class DoliDBMssql extends DoliDB /** * Close database connexion * - * @return boolean True if disconnect successfull, false otherwise + * @return bool True if disconnect successfull, false otherwise * @see connect */ function close() @@ -220,7 +220,7 @@ class DoliDBMssql extends DoliDB if ($this->db) { if ($this->transaction_opened > 0) dol_syslog(get_class($this)."::close Closing a connection with an opened transaction depth=".$this->transaction_opened,LOG_ERR); - $this->connected=0; + $this->connected=false; return mssql_close($this->db); } return false; @@ -230,7 +230,7 @@ class DoliDBMssql extends DoliDB /** * Start transaction * - * @return int 1 if transaction successfuly opened or already opened, 0 if error + * @return bool true if transaction successfuly opened or already opened, false if error */ function begin() { @@ -250,7 +250,7 @@ class DoliDBMssql extends DoliDB } else { - return 1; + return true; } } @@ -258,7 +258,7 @@ class DoliDBMssql extends DoliDB * Validate a database transaction * * @param string $log Add more log to default log line - * @return int 1 if validation is OK or transaction level no started, 0 if ERROR + * @return bool true if validation is OK or transaction level no started, false if ERROR */ function commit($log='') { @@ -272,25 +272,26 @@ class DoliDBMssql extends DoliDB if ($ret) { dol_syslog("COMMIT Transaction",LOG_DEBUG); - return 1; + return true; } else { - return 0; + return false; } } elseif ($this->transaction_opened > 1) { - return 1; - } else - trigger_error("Commit requested but no transaction remain"); + return true; + } + trigger_error("Commit requested but no transaction remain"); + return false; } /** * Annulation d'une transaction et retour aux anciennes valeurs * * @param string $log Add more log to default log line - * @return int 1 si annulation ok ou transaction non ouverte, 0 en cas d'erreur + * @return bool true si annulation ok ou transaction non ouverte, false en cas d'erreur */ function rollback($log='') { @@ -305,9 +306,10 @@ class DoliDBMssql extends DoliDB } elseif ($this->transaction_opened > 1) { - return 1; - } else - trigger_error("Rollback requested but no transaction remain"); + return true; + } + trigger_error("Rollback requested but no transaction remain"); + return false; } /** @@ -317,7 +319,7 @@ class DoliDBMssql extends DoliDB * @param int $usesavepoint 0=Default mode, 1=Run a savepoint before and a rollbock to savepoint if error (this allow to have some request with errors inside global transactions). * Note that with Mysql, this parameter is not used as Myssql can already commit a transaction even if one request is in error, without using savepoints. * @param string $type Type of SQL order ('ddl' for insert, update, select, delete or 'dml' for create, alter...) - * @return resource Resultset of answer + * @return false|resource|true Resultset of answer */ function query($query,$usesavepoint=0,$type='auto') { @@ -459,8 +461,8 @@ class DoliDBMssql extends DoliDB /** * Renvoie la ligne courante (comme un objet) pour le curseur resultset * - * @param Resultset $resultset Curseur de la requete voulue - * @return Object Object result line or false if KO or end of cursor + * @param resource $resultset Curseur de la requete voulue + * @return object|false Object result line or false if KO or end of cursor */ function fetch_object($resultset) { @@ -472,8 +474,8 @@ class DoliDBMssql extends DoliDB /** * Return datas as an array * - * @param Resultset $resultset Resultset of request - * @return array Array + * @param resource $resultset Resultset of request + * @return array|false Array or false if KO or end of cursor */ function fetch_array($resultset) { @@ -486,8 +488,8 @@ class DoliDBMssql extends DoliDB /** * Return datas as an array * - * @param Resultset $resultset Resultset of request - * @return array Array + * @param resource $resultset Resultset of request + * @return array|false Array or false if KO or end of cursor */ function fetch_row($resultset) { @@ -499,7 +501,7 @@ class DoliDBMssql extends DoliDB /** * Return number of lines for result of a SELECT * - * @param Resultset $resultset Resulset of requests + * @param resource $resultset Resulset of requests * @return int Nb of lines * @see affected_rows */ @@ -513,7 +515,7 @@ class DoliDBMssql extends DoliDB /** * Renvoie le nombre de lignes dans le resultat d'une requete INSERT, DELETE ou UPDATE * - * @param resultset $resultset Curseur de la requete voulue + * @param resource $resultset Curseur de la requete voulue * @return int Nombre de lignes * @see num_rows */ @@ -532,10 +534,10 @@ class DoliDBMssql extends DoliDB /** * Free last resultset used. * - * @param resultset $resultset Curseur de la requete voulue - * @return void + * @param resource $resultset Curseur de la requete voulue + * @return bool */ - function free($resultset=0) + function free($resultset=null) { // Si le resultset n'est pas fourni, on prend le dernier utilise sur cette connexion if (! is_resource($resultset)) { $resultset=$this->_results; } @@ -641,7 +643,7 @@ class DoliDBMssql extends DoliDB * * @param string $tab Table name concerned by insert. Ne sert pas sous MySql mais requis pour compatibilite avec Postgresql * @param string $fieldid Field name - * @return int Id of row + * @return int Id of row or -1 on error */ function last_insert_id($tab,$fieldid='rowid') { @@ -662,7 +664,7 @@ class DoliDBMssql extends DoliDB * * @param string $fieldorvalue Field name or value to encrypt * @param int $withQuotes Return string with quotes - * @return return XXX(field) or XXX('value') or field or 'value' + * @return string XXX(field) or XXX('value') or field or 'value' */ function encrypt($fieldorvalue, $withQuotes=0) { @@ -720,7 +722,7 @@ class DoliDBMssql extends DoliDB * @param string $charset Charset used to store data * @param string $collation Charset used to sort data * @param string $owner Username of database owner - * @return resource resource defined if OK, null if KO + * @return false|resource|true resource defined if OK, false if KO */ function DDLCreateDb($database,$charset='',$collation='',$owner='') { @@ -786,11 +788,13 @@ class DoliDBMssql extends DoliDB * @param string $type Type de la table * @param array $unique_keys Tableau associatifs Nom de champs qui seront clef unique => valeur * @param array $fulltext_keys Tableau des Nom de champs qui seront indexes en fulltext - * @param string $keys Tableau des champs cles noms => valeur + * @param array $keys Tableau des champs cles noms => valeur * @return int <0 if KO, >=0 if OK */ - function DDLCreateTable($table,$fields,$primary_key,$type,$unique_keys="",$fulltext_keys="",$keys="") + function DDLCreateTable($table,$fields,$primary_key,$type,$unique_keys=null,$fulltext_keys=null,$keys=null) { + // FIXME: $fulltext_keys parameter is unused + // cles recherchees dans le tableau des descriptions (fields) : type,value,attribute,null,default,extra // ex. : $fields['rowid'] = array('type'=>'int','value'=>'11','null'=>'not null','extra'=> 'auto_increment'); $sql = "create table ".$table."("; @@ -820,7 +824,7 @@ class DoliDBMssql extends DoliDB if($primary_key != "") $pk = "primary key(".$primary_key.")"; - if($unique_keys != "") + if(is_array($unique_keys)) { $i = 0; foreach($unique_keys as $key => $value) @@ -829,7 +833,7 @@ class DoliDBMssql extends DoliDB $i++; } } - if($keys != "") + if(is_array($keys)) { $i = 0; foreach($keys as $key => $value) @@ -841,9 +845,9 @@ class DoliDBMssql extends DoliDB $sql .= implode(',',$sqlfields); if($primary_key != "") $sql .= ",".$pk; - if($unique_keys != "") + if(is_array($unique_keys)) $sql .= ",".implode(',',$sqluq); - if($keys != "") + if(is_array($keys)) $sql .= ",".implode(',',$sqlk); $sql .=") type=".$type; @@ -859,7 +863,7 @@ class DoliDBMssql extends DoliDB * * @param string $table Name of table * @param string $field Optionnel : Name of field if we want description of field - * @return resource Resource + * @return false|resource|true Resource */ function DDLDescTable($table,$field="") { @@ -1047,7 +1051,7 @@ class DoliDBMssql extends DoliDB // FIXME: Dummy method // TODO: Implement - return ''; + return array(); } /** @@ -1123,7 +1127,7 @@ class DoliDBMssql extends DoliDB * * @param string $table Table name which contains fields * @param mixed $fields String for one field or array of string for multiple field - * @return boolean|multitype:object + * @return false|object */ function GetFieldInformation($table,$fields) { $sql="SELECT * from INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='".$this->escape($table)."' AND COLUMN_NAME"; diff --git a/htdocs/core/db/mysql.class.php b/htdocs/core/db/mysql.class.php index 64efc837bcd..30a7780afdf 100644 --- a/htdocs/core/db/mysql.class.php +++ b/htdocs/core/db/mysql.class.php @@ -21,13 +21,13 @@ /** * \file htdocs/core/db/mysql.class.php - * \brief Class file to manage Dolibarr database access for a Mysql database + * \brief Class file to manage Dolibarr database access for a MySQL database */ require_once DOL_DOCUMENT_ROOT .'/core/db/DoliDB.class.php'; /** - * Class to manage Dolibarr database access for a Mysql database + * Class to manage Dolibarr database access for a MySQL database using the mysql extension */ class DoliDBMysql extends DoliDB { @@ -37,7 +37,7 @@ class DoliDBMysql extends DoliDB const LABEL='MySQL'; //! Version min database const VERSIONMIN='4.1.0'; - //! Resultset of last query + /** @var resource Resultset of last query */ private $_results; /** @@ -69,8 +69,8 @@ class DoliDBMysql extends DoliDB if (! function_exists("mysql_connect")) { - $this->connected = 0; - $this->ok = 0; + $this->connected = false; + $this->ok = false; $this->error="Mysql PHP functions for using MySql driver are not available in this version of PHP. Try to use another driver."; dol_syslog(get_class($this)."::DoliDBMysql : Mysql PHP functions for using Mysql driver are not available in this version of PHP. Try to use another driver.",LOG_ERR); return $this->ok; @@ -78,8 +78,8 @@ class DoliDBMysql extends DoliDB if (! $host) { - $this->connected = 0; - $this->ok = 0; + $this->connected = false; + $this->ok = false; $this->error=$langs->trans("ErrorWrongHostParameter"); dol_syslog(get_class($this)."::DoliDBMysql : Erreur Connect, wrong host parameters",LOG_ERR); return $this->ok; @@ -89,14 +89,14 @@ class DoliDBMysql extends DoliDB $this->db = $this->connect($host, $user, $pass, $name, $port); if ($this->db) { - $this->connected = 1; - $this->ok = 1; + $this->connected = true; + $this->ok = true; } else { // host, login ou password incorrect - $this->connected = 0; - $this->ok = 0; + $this->connected = false; + $this->ok = false; $this->error=mysql_error(); dol_syslog(get_class($this)."::DoliDBMysql : Erreur Connect mysql_error=".$this->error,LOG_ERR); } @@ -106,9 +106,9 @@ class DoliDBMysql extends DoliDB { if ($this->select_db($name)) { - $this->database_selected = 1; + $this->database_selected = true; $this->database_name = $name; - $this->ok = 1; + $this->ok = true; // If client connected with different charset than Dolibarr HTML output $clientmustbe=''; @@ -122,9 +122,9 @@ class DoliDBMysql extends DoliDB } else { - $this->database_selected = 0; + $this->database_selected = false; $this->database_name = ''; - $this->ok = 0; + $this->ok = false; $this->error=$this->error(); dol_syslog(get_class($this)."::DoliDBMysql : Erreur Select_db ".$this->error,LOG_ERR); } @@ -132,7 +132,7 @@ class DoliDBMysql extends DoliDB else { // Pas de selection de base demandee, ok ou ko - $this->database_selected = 0; + $this->database_selected = false; if ($this->connected) { @@ -177,14 +177,14 @@ class DoliDBMysql extends DoliDB } /** - * Connexion to server + * Connection to server * * @param string $host database server host * @param string $login login * @param string $passwd password * @param string $name name of database (not used for mysql, used for pgsql) * @param integer $port Port of database server - * @return resource Database access handler + * @return resource|false Database access handler * @see close */ function connect($host, $login, $passwd, $name, $port=0) @@ -219,7 +219,7 @@ class DoliDBMysql extends DoliDB */ function getDriverInfo() { - return mysqli_get_client_info(); + return mysql_get_client_info(); } @@ -234,7 +234,7 @@ class DoliDBMysql extends DoliDB if ($this->db) { if ($this->transaction_opened > 0) dol_syslog(get_class($this)."::close Closing a connection with an opened transaction depth=".$this->transaction_opened,LOG_ERR); - $this->connected=0; + $this->connected=false; return mysql_close($this->db); } return false; @@ -247,7 +247,7 @@ class DoliDBMysql extends DoliDB * @param int $usesavepoint 0=Default mode, 1=Run a savepoint before and a rollbock to savepoint if error (this allow to have some request with errors inside global transactions). * Note that with Mysql, this parameter is not used as Myssql can already commit a transaction even if one request is in error, without using savepoints. * @param string $type Type of SQL order ('ddl' for insert, update, select, delete or 'dml' for create, alter...) - * @return resource Resultset of answer + * @return resource|true|false Resultset of answer */ function query($query,$usesavepoint=0,$type='auto') { @@ -287,8 +287,8 @@ class DoliDBMysql extends DoliDB /** * Renvoie la ligne courante (comme un objet) pour le curseur resultset * - * @param Resultset $resultset Curseur de la requete voulue - * @return Object Object result line or false if KO or end of cursor + * @param resource $resultset Curseur de la requete voulue + * @return resource|false Object result line or false if KO or end of cursor */ function fetch_object($resultset) { @@ -300,7 +300,7 @@ class DoliDBMysql extends DoliDB /** * Return datas as an array * - * @param Resultset $resultset Resultset of request + * @param resource $resultset Resultset of request * @return array Array */ function fetch_array($resultset) @@ -327,7 +327,7 @@ class DoliDBMysql extends DoliDB /** * Return number of lines for result of a SELECT * - * @param Resultset $resultset Resulset of requests + * @param resource $resultset Resulset of requests * @return int Nb of lines * @see affected_rows */ @@ -341,7 +341,7 @@ class DoliDBMysql extends DoliDB /** * Renvoie le nombre de lignes dans le resultat d'une requete INSERT, DELETE ou UPDATE * - * @param resultset $resultset Curseur de la requete voulue + * @param resource $resultset Curseur de la requete voulue * @return int Nombre de lignes * @see num_rows */ @@ -358,10 +358,10 @@ class DoliDBMysql extends DoliDB /** * Free last resultset used. * - * @param resultset $resultset Curseur de la requete voulue + * @param resource $resultset Curseur de la requete voulue * @return void */ - function free($resultset=0) + function free($resultset=null) { // If resultset not provided, we take the last used by connexion if (! is_resource($resultset)) { $resultset=$this->_results; } @@ -558,7 +558,7 @@ class DoliDBMysql extends DoliDB * @param string $charset Charset used to store data * @param string $collation Charset used to sort data * @param string $owner Username of database owner - * @return resource resource defined if OK, null if KO + * @return false|resource|true resource defined if OK, null if KO */ function DDLCreateDb($database,$charset='',$collation='',$owner='') { @@ -634,11 +634,13 @@ class DoliDBMysql extends DoliDB * @param string $type Type de la table * @param array $unique_keys Tableau associatifs Nom de champs qui seront clef unique => valeur * @param array $fulltext_keys Tableau des Nom de champs qui seront indexes en fulltext - * @param string $keys Tableau des champs cles noms => valeur + * @param array $keys Tableau des champs cles noms => valeur * @return int <0 if KO, >=0 if OK */ - function DDLCreateTable($table,$fields,$primary_key,$type,$unique_keys="",$fulltext_keys="",$keys="") + function DDLCreateTable($table,$fields,$primary_key,$type,$unique_keys=null,$fulltext_keys=null,$keys=null) { + // FIXME: $fulltext_keys parameter is unused + // cles recherchees dans le tableau des descriptions (fields) : type,value,attribute,null,default,extra // ex. : $fields['rowid'] = array('type'=>'int','value'=>'11','null'=>'not null','extra'=> 'auto_increment'); $sql = "CREATE TABLE ".$table."("; @@ -673,7 +675,7 @@ class DoliDBMysql extends DoliDB if($primary_key != "") $pk = "primary key(".$primary_key.")"; - if($unique_keys != "") + if(is_array($unique_keys)) { $i = 0; foreach($unique_keys as $key => $value) @@ -682,7 +684,7 @@ class DoliDBMysql extends DoliDB $i++; } } - if($keys != "") + if(is_array($keys)) { $i = 0; foreach($keys as $key => $value) @@ -694,9 +696,9 @@ class DoliDBMysql extends DoliDB $sql .= implode(',',$sqlfields); if($primary_key != "") $sql .= ",".$pk; - if($unique_keys != "") + if(is_array($unique_keys)) $sql .= ",".implode(',',$sqluq); - if($keys != "") + if(is_array($keys)) $sql .= ",".implode(',',$sqlk); $sql .=") engine=".$type; @@ -712,7 +714,7 @@ class DoliDBMysql extends DoliDB * * @param string $table Name of table * @param string $field Optionnel : Name of field if we want description of field - * @return resource Resource + * @return false|resource|true Resource */ function DDLDescTable($table,$field="") { diff --git a/htdocs/core/db/mysqli.class.php b/htdocs/core/db/mysqli.class.php index 1126f6c8a57..591789fcd89 100644 --- a/htdocs/core/db/mysqli.class.php +++ b/htdocs/core/db/mysqli.class.php @@ -21,13 +21,13 @@ /** * \file htdocs/core/db/mysqli.class.php - * \brief Class file to manage Dolibarr database access for a Mysql database + * \brief Class file to manage Dolibarr database access for a MySQL database */ require_once DOL_DOCUMENT_ROOT .'/core/db/DoliDB.class.php'; /** - * Class to manage Dolibarr database access for a Mysql database + * Class to manage Dolibarr database access for a MySQL database using the MySQLi extension */ class DoliDBMysqli extends DoliDB { @@ -36,8 +36,8 @@ class DoliDBMysqli extends DoliDB //! Database label const LABEL='MySQL'; //! Version min database - const VERSIONMIN='4.1.0'; - //! Resultset of last query + const VERSIONMIN='4.1.3'; + /** @var mysqli_result Resultset of last query */ private $_results; /** @@ -69,8 +69,8 @@ class DoliDBMysqli extends DoliDB if (! function_exists("mysqli_connect")) { - $this->connected = 0; - $this->ok = 0; + $this->connected = false; + $this->ok = false; $this->error="Mysqli PHP functions for using Mysqli driver are not available in this version of PHP. Try to use another driver."; dol_syslog(get_class($this)."::DoliDBMysqli : Mysqli PHP functions for using Mysqli driver are not available in this version of PHP. Try to use another driver.",LOG_ERR); return $this->ok; @@ -78,8 +78,8 @@ class DoliDBMysqli extends DoliDB if (! $host) { - $this->connected = 0; - $this->ok = 0; + $this->connected = false; + $this->ok = false; $this->error=$langs->trans("ErrorWrongHostParameter"); dol_syslog(get_class($this)."::DoliDBMysqli : Erreur Connect, wrong host parameters",LOG_ERR); return $this->ok; @@ -91,14 +91,14 @@ class DoliDBMysqli extends DoliDB if ($this->db) { - $this->connected = 1; - $this->ok = 1; + $this->connected = true; + $this->ok = true; } else { // host, login ou password incorrect - $this->connected = 0; - $this->ok = 0; + $this->connected = false; + $this->ok = false; $this->error=mysqli_connect_error(); dol_syslog(get_class($this)."::DoliDBMysqli : Erreur Connect mysqli_connect_error=".$this->error,LOG_ERR); } @@ -108,9 +108,9 @@ class DoliDBMysqli extends DoliDB { if ($this->select_db($name)) { - $this->database_selected = 1; + $this->database_selected = true; $this->database_name = $name; - $this->ok = 1; + $this->ok = true; // If client connected with different charset than Dolibarr HTML output $clientmustbe=''; @@ -124,9 +124,9 @@ class DoliDBMysqli extends DoliDB } else { - $this->database_selected = 0; + $this->database_selected = false; $this->database_name = ''; - $this->ok = 0; + $this->ok = false; $this->error=$this->error(); dol_syslog(get_class($this)."::DoliDBMysqli : Erreur Select_db ".$this->error,LOG_ERR); } @@ -134,7 +134,7 @@ class DoliDBMysqli extends DoliDB else { // Pas de selection de base demandee, ok ou ko - $this->database_selected = 0; + $this->database_selected = false; if ($this->connected) { @@ -187,7 +187,7 @@ class DoliDBMysqli extends DoliDB * @param string $passwd password * @param string $name name of database (not used for mysql, used for pgsql) * @param integer $port Port of database server - * @return resource Database access handler + * @return mysqli Database access handler * @see close */ function connect($host, $login, $passwd, $name, $port=0) @@ -230,7 +230,7 @@ class DoliDBMysqli extends DoliDB /** * Close database connexion * - * @return boolean True if disconnect successfull, false otherwise + * @return bool True if disconnect successfull, false otherwise * @see connect */ function close() @@ -238,7 +238,7 @@ class DoliDBMysqli extends DoliDB if ($this->db) { if ($this->transaction_opened > 0) dol_syslog(get_class($this)."::close Closing a connection with an opened transaction depth=".$this->transaction_opened,LOG_ERR); - $this->connected=0; + $this->connected=false; return mysqli_close($this->db); } return false; @@ -251,7 +251,7 @@ class DoliDBMysqli extends DoliDB * @param int $usesavepoint 0=Default mode, 1=Run a savepoint before and a rollbock to savepoint if error (this allow to have some request with errors inside global transactions). * Note that with Mysql, this parameter is not used as Myssql can already commit a transaction even if one request is in error, without using savepoints. * @param string $type Type of SQL order ('ddl' for insert, update, select, delete or 'dml' for create, alter...) - * @return resource Resultset of answer + * @return bool|mysqli_result Resultset of answer */ function query($query,$usesavepoint=0,$type='auto') { @@ -290,8 +290,8 @@ class DoliDBMysqli extends DoliDB /** * Renvoie la ligne courante (comme un objet) pour le curseur resultset * - * @param Resultset $resultset Curseur de la requete voulue - * @return Object Object result line or false if KO or end of cursor + * @param mysqli_result $resultset Curseur de la requete voulue + * @return object|null Object result line or null if KO or end of cursor */ function fetch_object($resultset) { @@ -304,8 +304,8 @@ class DoliDBMysqli extends DoliDB /** * Return datas as an array * - * @param Resultset $resultset Resultset of request - * @return array Array + * @param mysqli_result $resultset Resultset of request + * @return array|null Array or null if KO or end of cursor */ function fetch_array($resultset) { @@ -317,8 +317,8 @@ class DoliDBMysqli extends DoliDB /** * Return datas as an array * - * @param resource $resultset Resultset of request - * @return array Array + * @param mysqli_result $resultset Resultset of request + * @return array|null|0 Array or null if KO or end of cursor or 0 if resultset is bool */ function fetch_row($resultset) { @@ -338,8 +338,8 @@ class DoliDBMysqli extends DoliDB /** * Return number of lines for result of a SELECT * - * @param Resultset $resultset Resulset of requests - * @return int Nb of lines + * @param mysqli_result $resultset Resulset of requests + * @return int Nb of lines * @see affected_rows */ function num_rows($resultset) @@ -352,8 +352,8 @@ class DoliDBMysqli extends DoliDB /** * Renvoie le nombre de lignes dans le resultat d'une requete INSERT, DELETE ou UPDATE * - * @param resultset $resultset Curseur de la requete voulue - * @return int Nombre de lignes + * @param mysqli_result $resultset Curseur de la requete voulue + * @return int Nombre de lignes * @see num_rows */ function affected_rows($resultset) @@ -369,10 +369,10 @@ class DoliDBMysqli extends DoliDB /** * Libere le dernier resultset utilise sur cette connexion * - * @param resultset $resultset Curseur de la requete voulue + * @param mysqli_result $resultset Curseur de la requete voulue * @return void */ - function free($resultset=0) + function free($resultset=null) { // If resultset not provided, we take the last used by connexion if (! is_object($resultset)) { $resultset=$this->_results; } @@ -401,8 +401,7 @@ class DoliDBMysqli extends DoliDB if (! $this->connected) { // Si il y a eu echec de connexion, $this->db n'est pas valide. return 'DB_ERROR_FAILED_TO_CONNECT'; - } - else { + } else { // Constants to convert a MySql error code to a generic Dolibarr error code $errorcode_map = array( 1004 => 'DB_ERROR_CANNOT_CREATE', @@ -434,8 +433,7 @@ class DoliDBMysqli extends DoliDB 1451 => 'DB_ERROR_CHILD_EXISTS' ); - if (isset($errorcode_map[mysqli_errno($this->db)])) - { + if (isset($errorcode_map[mysqli_errno($this->db)])) { return $errorcode_map[mysqli_errno($this->db)]; } $errno=mysqli_errno($this->db); @@ -464,7 +462,7 @@ class DoliDBMysqli extends DoliDB * * @param string $tab Table name concerned by insert. Ne sert pas sous MySql mais requis pour compatibilite avec Postgresql * @param string $fieldid Field name - * @return int Id of row + * @return int|string Id of row */ function last_insert_id($tab,$fieldid='rowid') { @@ -562,7 +560,7 @@ class DoliDBMysqli extends DoliDB * @param string $charset Charset used to store data * @param string $collation Charset used to sort data * @param string $owner Username of database owner - * @return resource resource defined if OK, null if KO + * @return bool|mysqli_result resource defined if OK, null if KO */ function DDLCreateDb($database,$charset='',$collation='',$owner='') { @@ -638,11 +636,13 @@ class DoliDBMysqli extends DoliDB * @param string $type Type de la table * @param array $unique_keys Tableau associatifs Nom de champs qui seront clef unique => valeur * @param array $fulltext_keys Tableau des Nom de champs qui seront indexes en fulltext - * @param string $keys Tableau des champs cles noms => valeur + * @param array $keys Tableau des champs cles noms => valeur * @return int <0 if KO, >=0 if OK */ - function DDLCreateTable($table,$fields,$primary_key,$type,$unique_keys="",$fulltext_keys="",$keys="") + function DDLCreateTable($table,$fields,$primary_key,$type,$unique_keys=null,$fulltext_keys=null,$keys=null) { + // FIXME: $fulltext_keys parameter is unused + // cles recherchees dans le tableau des descriptions (fields) : type,value,attribute,null,default,extra // ex. : $fields['rowid'] = array('type'=>'int','value'=>'11','null'=>'not null','extra'=> 'auto_increment'); $sql = "CREATE TABLE ".$table."("; @@ -677,8 +677,7 @@ class DoliDBMysqli extends DoliDB if($primary_key != "") $pk = "primary key(".$primary_key.")"; - if($unique_keys != "") - { + if(is_array($unique_keys)) { $i = 0; foreach($unique_keys as $key => $value) { @@ -686,7 +685,7 @@ class DoliDBMysqli extends DoliDB $i++; } } - if($keys != "") + if(is_array($keys)) { $i = 0; foreach($keys as $key => $value) @@ -700,7 +699,7 @@ class DoliDBMysqli extends DoliDB $sql .= ",".$pk; if($unique_keys != "") $sql .= ",".implode(',',$sqluq); - if($keys != "") + if(is_array($keys)) $sql .= ",".implode(',',$sqlk); $sql .=") engine=".$type; @@ -716,7 +715,7 @@ class DoliDBMysqli extends DoliDB * * @param string $table Name of table * @param string $field Optionnel : Name of field if we want description of field - * @return resource Resultset x (x->Field, x->Type, ...) + * @return bool|mysqli_result Resultset x (x->Field, x->Type, ...) */ function DDLDescTable($table,$field="") { @@ -763,14 +762,10 @@ class DoliDBMysqli extends DoliDB $sql.= " ".$field_position; dol_syslog(get_class($this)."::DDLAddField ".$sql,LOG_DEBUG); - if(! $this->query($sql)) - { - return -1; - } - else - { + if($this->query($sql)) { return 1; } + return -1; } /** @@ -808,12 +803,11 @@ class DoliDBMysqli extends DoliDB { $sql= "ALTER TABLE ".$table." DROP COLUMN `".$field_name."`"; dol_syslog(get_class($this)."::DDLDropField ".$sql,LOG_DEBUG); - if (! $this->query($sql)) - { - $this->error=$this->lasterror(); - return -1; + if ($this->query($sql)) { + return 1; } - else return 1; + $this->error=$this->lasterror(); + return -1; } @@ -883,7 +877,7 @@ class DoliDBMysqli extends DoliDB /** * Return list of available charset that can be used to store data in database * - * @return array List of Charset + * @return array|null List of Charset */ function getListOfCharacterSet() { @@ -926,7 +920,7 @@ class DoliDBMysqli extends DoliDB /** * Return list of available collation that can be used for database * - * @return array Liste of Collation + * @return array|null Liste of Collation */ function getListOfCollation() { diff --git a/htdocs/core/db/pgsql.class.php b/htdocs/core/db/pgsql.class.php index e946d39e143..0466c2ad96b 100644 --- a/htdocs/core/db/pgsql.class.php +++ b/htdocs/core/db/pgsql.class.php @@ -45,7 +45,7 @@ class DoliDBPgsql extends DoliDB var $forcecollate=''; // Can't be static as it may be forced with a dynamic value //! Version min database const VERSIONMIN='8.4.0'; // Version min database - //! Resultset of last query + /** @var resource Resultset of last query */ private $_results; public $unescapeslashquot; @@ -80,8 +80,8 @@ class DoliDBPgsql extends DoliDB if (! function_exists("pg_connect")) { - $this->connected = 0; - $this->ok = 0; + $this->connected = false; + $this->ok = false; $this->error="Pgsql PHP functions are not available in this version of PHP"; dol_syslog(get_class($this)."::DoliDBPgsql : Pgsql PHP functions are not available in this version of PHP",LOG_ERR); return $this->ok; @@ -89,8 +89,8 @@ class DoliDBPgsql extends DoliDB if (! $host) { - $this->connected = 0; - $this->ok = 0; + $this->connected = false; + $this->ok = false; $this->error=$langs->trans("ErrorWrongHostParameter"); dol_syslog(get_class($this)."::DoliDBPgsql : Erreur Connect, wrong host parameters",LOG_ERR); return $this->ok; @@ -102,14 +102,14 @@ class DoliDBPgsql extends DoliDB if ($this->db) { - $this->connected = 1; - $this->ok = 1; + $this->connected = true; + $this->ok = true; } else { // host, login ou password incorrect - $this->connected = 0; - $this->ok = 0; + $this->connected = false; + $this->ok = false; $this->error='Host, login or password incorrect'; dol_syslog(get_class($this)."::DoliDBPgsql : Erreur Connect ".$this->error,LOG_ERR); } @@ -119,15 +119,15 @@ class DoliDBPgsql extends DoliDB { if ($this->select_db($name)) { - $this->database_selected = 1; + $this->database_selected = true; $this->database_name = $name; - $this->ok = 1; + $this->ok = true; } else { - $this->database_selected = 0; + $this->database_selected = false; $this->database_name = ''; - $this->ok = 0; + $this->ok = false; $this->error=$this->error(); dol_syslog(get_class($this)."::DoliDBPgsql : Erreur Select_db ".$this->error,LOG_ERR); } @@ -135,7 +135,7 @@ class DoliDBPgsql extends DoliDB else { // Pas de selection de base demandee, ok ou ko - $this->database_selected = 0; + $this->database_selected = false; } return $this->ok; @@ -147,10 +147,10 @@ class DoliDBPgsql extends DoliDB * * @param string $line SQL request line to convert * @param string $type Type of SQL order ('ddl' for insert, update, select, delete or 'dml' for create, alter...) - * @param string $unescapeslashquot Unescape slash quote with quote quote + * @param bool $unescapeslashquot Unescape slash quote with quote quote * @return string SQL request line converted */ - static function convertSQLFromMysql($line,$type='auto',$unescapeslashquot=0) + static function convertSQLFromMysql($line,$type='auto',$unescapeslashquot=false) { // Removed empty line if this is a comment line for SVN tagging if (preg_match('/^--\s\$Id/i',$line)) { @@ -353,7 +353,7 @@ class DoliDBPgsql extends DoliDB * On compare juste manuellement si la database choisie est bien celle activee par la connexion * * @param string $database Name of database - * @return boolean true if OK, false if KO + * @return bool true if OK, false if KO */ function select_db($database) { @@ -369,7 +369,7 @@ class DoliDBPgsql extends DoliDB * @param string $passwd Password * @param string $name Name of database (not used for mysql, used for pgsql) * @param integer $port Port of database server - * @return resource Database access handler + * @return false|resource Database access handler * @see close */ function connect($host, $login, $passwd, $name, $port=0) @@ -451,7 +451,7 @@ class DoliDBPgsql extends DoliDB if ($this->db) { if ($this->transaction_opened > 0) dol_syslog(get_class($this)."::close Closing a connection with an opened transaction depth=".$this->transaction_opened,LOG_ERR); - $this->connected=0; + $this->connected=false; return pg_close($this->db); } return false; @@ -463,7 +463,7 @@ class DoliDBPgsql extends DoliDB * @param string $query SQL query string * @param int $usesavepoint 0=Default mode, 1=Run a savepoint before and a rollback to savepoint if error (this allow to have some request with errors inside global transactions). * @param string $type Type of SQL order ('ddl' for insert, update, select, delete or 'dml' for create, alter...) - * @return resource Resultset of answer + * @return false|resource Resultset of answer */ function query($query,$usesavepoint=0,$type='auto') { @@ -530,8 +530,8 @@ class DoliDBPgsql extends DoliDB /** * Renvoie la ligne courante (comme un objet) pour le curseur resultset * - * @param Resultset $resultset Curseur de la requete voulue - * @return Object Object result line or false if KO or end of cursor + * @param resource $resultset Curseur de la requete voulue + * @return false|object Object result line or false if KO or end of cursor */ function fetch_object($resultset) { @@ -544,7 +544,7 @@ class DoliDBPgsql extends DoliDB * Return datas as an array * * @param resource $resultset Resultset of request - * @return array Array + * @return false|array Array */ function fetch_array($resultset) { @@ -557,7 +557,7 @@ class DoliDBPgsql extends DoliDB * Return datas as an array * * @param resource $resultset Resultset of request - * @return array Array + * @return false|array Array */ function fetch_row($resultset) { @@ -569,8 +569,8 @@ class DoliDBPgsql extends DoliDB /** * Return number of lines for result of a SELECT * - * @param Resultset $resultset Resulset of requests - * @return int Nb of lines + * @param resourse $resultset Resulset of requests + * @return int Nb of lines, -1 on error * @see affected_rows */ function num_rows($resultset) @@ -583,7 +583,7 @@ class DoliDBPgsql extends DoliDB /** * Renvoie le nombre de lignes dans le resultat d'une requete INSERT, DELETE ou UPDATE * - * @param Resultset $resultset Result set of request + * @param resource $resultset Result set of request * @return int Nb of lines * @see num_rows */ @@ -600,10 +600,10 @@ class DoliDBPgsql extends DoliDB /** * Libere le dernier resultset utilise sur cette connexion * - * @param Resultset $resultset Result set of request + * @param resource $resultset Result set of request * @return void */ - function free($resultset=0) + function free($resultset=null) { // If resultset not provided, we take the last used by connexion if (! is_resource($resultset)) { $resultset=$this->_results; } @@ -746,7 +746,7 @@ class DoliDBPgsql extends DoliDB * * @param string $tab Table name concerned by insert. Ne sert pas sous MySql mais requis pour compatibilite avec Postgresql * @param string $fieldid Field name - * @return int Id of row + * @return string Id of row */ function last_insert_id($tab,$fieldid='rowid') { @@ -827,7 +827,7 @@ class DoliDBPgsql extends DoliDB * @param string $charset Charset used to store data * @param string $collation Charset used to sort data * @param string $owner Username of database owner - * @return resource resource defined if OK, null if KO + * @return false|resource resource defined if OK, null if KO */ function DDLCreateDb($database,$charset='',$collation='',$owner='') { @@ -910,11 +910,13 @@ class DoliDBPgsql extends DoliDB * @param string $type Type de la table * @param array $unique_keys Tableau associatifs Nom de champs qui seront clef unique => valeur * @param array $fulltext_keys Tableau des Nom de champs qui seront indexes en fulltext - * @param string $keys Tableau des champs cles noms => valeur + * @param array $keys Tableau des champs cles noms => valeur * @return int <0 if KO, >=0 if OK */ - function DDLCreateTable($table,$fields,$primary_key,$type,$unique_keys="",$fulltext_keys="",$keys="") + function DDLCreateTable($table,$fields,$primary_key,$type,$unique_keys=null,$fulltext_keys=null,$keys=null) { + // FIXME: $fulltext_keys parameter is unused + // cles recherchees dans le tableau des descriptions (fields) : type,value,attribute,null,default,extra // ex. : $fields['rowid'] = array('type'=>'int','value'=>'11','null'=>'not null','extra'=> 'auto_increment'); $sql = "create table ".$table."("; @@ -944,7 +946,7 @@ class DoliDBPgsql extends DoliDB if($primary_key != "") $pk = "primary key(".$primary_key.")"; - if($unique_keys != "") + if(is_array($unique_keys)) { $i = 0; foreach($unique_keys as $key => $value) @@ -953,7 +955,7 @@ class DoliDBPgsql extends DoliDB $i++; } } - if($keys != "") + if(is_array($keys)) { $i = 0; foreach($keys as $key => $value) @@ -965,9 +967,9 @@ class DoliDBPgsql extends DoliDB $sql .= implode(',',$sqlfields); if($primary_key != "") $sql .= ",".$pk; - if($unique_keys != "") + if(is_array($unique_keys)) $sql .= ",".implode(',',$sqluq); - if($keys != "") + if(is_array($keys)) $sql .= ",".implode(',',$sqlk); $sql .=") type=".$type; @@ -1007,7 +1009,7 @@ class DoliDBPgsql extends DoliDB * * @param string $table Name of table * @param string $field Optionnel : Name of field if we want description of field - * @return resource Resultset x (x->attname) + * @return false|resource Resultset x (x->attname) */ function DDLDescTable($table,$field="") { @@ -1052,8 +1054,7 @@ class DoliDBPgsql extends DoliDB dol_syslog($sql,LOG_DEBUG); if(! $this -> query($sql)) - return -1; - else + return -1; return 1; } @@ -1078,8 +1079,7 @@ class DoliDBPgsql extends DoliDB dol_syslog($sql,LOG_DEBUG); if (! $this->query($sql)) - return -1; - else + return -1; return 1; } @@ -1099,7 +1099,7 @@ class DoliDBPgsql extends DoliDB $this->error=$this->lasterror(); return -1; } - else return 1; + return 1; } /** diff --git a/htdocs/core/db/sqlite.class.php b/htdocs/core/db/sqlite.class.php index c1819151a35..647cd3a6798 100644 --- a/htdocs/core/db/sqlite.class.php +++ b/htdocs/core/db/sqlite.class.php @@ -37,7 +37,7 @@ class DoliDBSqlite extends DoliDB const LABEL='PDO Sqlite'; //! Version min database const VERSIONMIN='3.0.0'; - //! Resultset of last query + /** @var PDOStatement Resultset of last query */ private $_results; /** @@ -54,7 +54,7 @@ class DoliDBSqlite extends DoliDB */ function __construct($type, $host, $user, $pass, $name='', $port=0) { - global $conf,$langs; + global $conf; // Note that having "static" property for "$forcecharset" and "$forcecollate" will make error here in strict mode, so they are not static if (! empty($conf->db->character_set)) $this->forcecharset=$conf->db->character_set; @@ -70,8 +70,8 @@ class DoliDBSqlite extends DoliDB /*if (! function_exists("sqlite_query")) { - $this->connected = 0; - $this->ok = 0; + $this->connected = false; + $this->ok = false; $this->error="Sqlite PHP functions for using Sqlite driver are not available in this version of PHP. Try to use another driver."; dol_syslog(get_class($this)."::DoliDBSqlite : Sqlite PHP functions for using Sqlite driver are not available in this version of PHP. Try to use another driver.",LOG_ERR); return $this->ok; @@ -79,8 +79,8 @@ class DoliDBSqlite extends DoliDB /*if (! $host) { - $this->connected = 0; - $this->ok = 0; + $this->connected = false; + $this->ok = false; $this->error=$langs->trans("ErrorWrongHostParameter"); dol_syslog(get_class($this)."::DoliDBSqlite : Erreur Connect, wrong host parameters",LOG_ERR); return $this->ok; @@ -92,9 +92,9 @@ class DoliDBSqlite extends DoliDB if ($this->db) { - $this->connected = 1; - $this->ok = 1; - $this->database_selected = 1; + $this->connected = true; + $this->ok = true; + $this->database_selected = true; $this->database_name = $name; $this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); @@ -102,15 +102,15 @@ class DoliDBSqlite extends DoliDB else { // host, login ou password incorrect - $this->connected = 0; - $this->ok = 0; - $this->database_selected = 0; + $this->connected = false; + $this->ok = false; + $this->database_selected = false; $this->database_name = ''; //$this->error=sqlite_connect_error(); dol_syslog(get_class($this)."::DoliDBSqlite : Erreur Connect ".$this->error,LOG_ERR); } - return $this->ok; + return (int) $this->ok; } @@ -285,6 +285,7 @@ class DoliDBSqlite extends DoliDB function select_db($database) { dol_syslog(get_class($this)."::select_db database=".$database, LOG_DEBUG); + // FIXME: sqlite_select_db() does not exist return sqlite_select_db($this->db,$database); } @@ -297,12 +298,12 @@ class DoliDBSqlite extends DoliDB * @param string $passwd password * @param string $name name of database (not used for mysql, used for pgsql) * @param integer $port Port of database server - * @return resource Database access handler + * @return PDO Database access handler * @see close */ function connect($host, $login, $passwd, $name, $port=0) { - global $conf,$main_data_dir; + global $main_data_dir; dol_syslog(get_class($this)."::connect name=".$name,LOG_DEBUG); @@ -352,7 +353,7 @@ class DoliDBSqlite extends DoliDB /** * Close database connexion * - * @return boolean True if disconnect successfull, false otherwise + * @return bool True if disconnect successfull, false otherwise * @see connect */ function close() @@ -360,7 +361,7 @@ class DoliDBSqlite extends DoliDB if ($this->db) { if ($this->transaction_opened > 0) dol_syslog(get_class($this)."::close Closing a connection with an opened transaction depth=".$this->transaction_opened,LOG_ERR); - $this->connected=0; + $this->connected=false; $this->db=null; // Clean this->db return true; } @@ -374,13 +375,11 @@ class DoliDBSqlite extends DoliDB * @param int $usesavepoint 0=Default mode, 1=Run a savepoint before and a rollbock to savepoint if error (this allow to have some request with errors inside global transactions). * Note that with Mysql, this parameter is not used as Myssql can already commit a transaction even if one request is in error, without using savepoints. * @param string $type Type of SQL order ('ddl' for insert, update, select, delete or 'dml' for create, alter...) - * @return resource Resultset of answer + * @return PDOStatement Resultset of answer */ function query($query,$usesavepoint=0,$type='auto') { - $errmsg=''; - - $ret=''; + $ret=null; $query = trim($query); $this->error = 0; @@ -429,8 +428,8 @@ class DoliDBSqlite extends DoliDB /** * Renvoie la ligne courante (comme un objet) pour le curseur resultset * - * @param Resultset $resultset Curseur de la requete voulue - * @return Object Object result line or false if KO or end of cursor + * @param PDOStatement $resultset Curseur de la requete voulue + * @return false|object Object result line or false if KO or end of cursor */ function fetch_object($resultset) { @@ -443,8 +442,8 @@ class DoliDBSqlite extends DoliDB /** * Return datas as an array * - * @param Resultset $resultset Resultset of request - * @return array Array + * @param PDOStatement $resultset Resultset of request + * @return false|array Array or false if KO or end of cursor */ function fetch_array($resultset) { @@ -456,8 +455,8 @@ class DoliDBSqlite extends DoliDB /** * Return datas as an array * - * @param resource $resultset Resultset of request - * @return array Array + * @param PDOStatement $resultset Resultset of request + * @return false|array Array or false if KO or end of cursor */ function fetch_row($resultset) { @@ -477,7 +476,7 @@ class DoliDBSqlite extends DoliDB /** * Return number of lines for result of a SELECT * - * @param Resultset $resultset Resulset of requests + * @param PDOStatement $resultset Resulset of requests * @return int Nb of lines * @see affected_rows */ @@ -491,7 +490,7 @@ class DoliDBSqlite extends DoliDB /** * Return number of lines for result of a SELECT * - * @param Resultset $resultset Resulset of requests + * @param PDOStatement $resultset Resulset of requests * @return int Nb of lines * @see affected_rows */ @@ -508,10 +507,10 @@ class DoliDBSqlite extends DoliDB /** * Free last resultset used. * - * @param integer $resultset Curseur de la requete voulue + * @param PDOStatement $resultset Curseur de la requete voulue * @return void */ - function free($resultset=0) + function free($resultset=null) { // If resultset not provided, we take the last used by connexion if (! is_object($resultset)) { $resultset=$this->_results; } @@ -709,7 +708,7 @@ class DoliDBSqlite extends DoliDB * @param string $charset Charset used to store data * @param string $collation Charset used to sort data * @param string $owner Username of database owner - * @return resource resource defined if OK, null if KO + * @return PDOStatement resource defined if OK, null if KO */ function DDLCreateDb($database,$charset='',$collation='',$owner='') { @@ -786,11 +785,13 @@ class DoliDBSqlite extends DoliDB * @param string $type Type de la table * @param array $unique_keys Tableau associatifs Nom de champs qui seront clef unique => valeur * @param array $fulltext_keys Tableau des Nom de champs qui seront indexes en fulltext - * @param string $keys Tableau des champs cles noms => valeur + * @param array $keys Tableau des champs cles noms => valeur * @return int <0 if KO, >=0 if OK */ - function DDLCreateTable($table,$fields,$primary_key,$type,$unique_keys="",$fulltext_keys="",$keys="") + function DDLCreateTable($table,$fields,$primary_key,$type,$unique_keys=null,$fulltext_keys=null,$keys=null) { + // FIXME: $fulltext_keys parameter is unused + // cles recherchees dans le tableau des descriptions (fields) : type,value,attribute,null,default,extra // ex. : $fields['rowid'] = array('type'=>'int','value'=>'11','null'=>'not null','extra'=> 'auto_increment'); $sql = "create table ".$table."("; @@ -820,7 +821,7 @@ class DoliDBSqlite extends DoliDB if($primary_key != "") $pk = "primary key(".$primary_key.")"; - if($unique_keys != "") + if(is_array($unique_keys)) { $i = 0; foreach($unique_keys as $key => $value) @@ -829,7 +830,7 @@ class DoliDBSqlite extends DoliDB $i++; } } - if($keys != "") + if(is_array($keys)) { $i = 0; foreach($keys as $key => $value) @@ -841,9 +842,9 @@ class DoliDBSqlite extends DoliDB $sql .= implode(',',$sqlfields); if($primary_key != "") $sql .= ",".$pk; - if($unique_keys != "") + if(is_array($unique_keys)) $sql .= ",".implode(',',$sqluq); - if($keys != "") + if(is_array($keys)) $sql .= ",".implode(',',$sqlk); $sql .=") type=".$type; diff --git a/htdocs/core/db/sqlite3.class.php b/htdocs/core/db/sqlite3.class.php index 1024dafd0cc..68d7ab672db 100644 --- a/htdocs/core/db/sqlite3.class.php +++ b/htdocs/core/db/sqlite3.class.php @@ -4,6 +4,7 @@ * Copyright (C) 2004-2011 Laurent Destailleur * Copyright (C) 2006 Andre Cianfarani * Copyright (C) 2005-2009 Regis Houssin + * Copyright (C) 2015 Raphaël Doursenaud * * 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 @@ -21,13 +22,13 @@ /** * \file htdocs/core/db/sqlite.class.php - * \brief Class file to manage Dolibarr database access for a Sqlite database + * \brief Class file to manage Dolibarr database access for a SQLite database */ require_once DOL_DOCUMENT_ROOT .'/core/db/DoliDB.class.php'; /** - * Class to manage Dolibarr database access for a Sqlite database + * Class to manage Dolibarr database access for a SQLite database */ class DoliDBSqlite3 extends DoliDB { @@ -37,8 +38,8 @@ class DoliDBSqlite3 extends DoliDB const LABEL='Sqlite3'; //! Version min database const VERSIONMIN='3.0.0'; - //! Resultset of last query - private $_results; + /** @var SQLite3Result Resultset of last query */ + private $_results; const WEEK_MONDAY_FIRST=1; const WEEK_YEAR = 2; @@ -58,7 +59,7 @@ class DoliDBSqlite3 extends DoliDB */ function __construct($type, $host, $user, $pass, $name='', $port=0) { - global $conf,$langs; + global $conf; // Note that having "static" property for "$forcecharset" and "$forcecollate" will make error here in strict mode, so they are not static if (! empty($conf->db->character_set)) $this->forcecharset=$conf->db->character_set; @@ -74,8 +75,8 @@ class DoliDBSqlite3 extends DoliDB /*if (! function_exists("sqlite_query")) { - $this->connected = 0; - $this->ok = 0; + $this->connected = false; + $this->ok = false; $this->error="Sqlite PHP functions for using Sqlite driver are not available in this version of PHP. Try to use another driver."; dol_syslog(get_class($this)."::DoliDBSqlite3 : Sqlite PHP functions for using Sqlite driver are not available in this version of PHP. Try to use another driver.",LOG_ERR); return $this->ok; @@ -83,8 +84,8 @@ class DoliDBSqlite3 extends DoliDB /*if (! $host) { - $this->connected = 0; - $this->ok = 0; + $this->connected = false; + $this->ok = false; $this->error=$langs->trans("ErrorWrongHostParameter"); dol_syslog(get_class($this)."::DoliDBSqlite3 : Erreur Connect, wrong host parameters",LOG_ERR); return $this->ok; @@ -96,9 +97,9 @@ class DoliDBSqlite3 extends DoliDB if ($this->db) { - $this->connected = 1; - $this->ok = 1; - $this->database_selected = 1; + $this->connected = true; + $this->ok = true; + $this->database_selected = true; $this->database_name = $name; $this->addCustomFunction('IF'); @@ -114,9 +115,9 @@ class DoliDBSqlite3 extends DoliDB else { // host, login ou password incorrect - $this->connected = 0; - $this->ok = 0; - $this->database_selected = 0; + $this->connected = false; + $this->ok = false; + $this->database_selected = false; $this->database_name = ''; //$this->error=sqlite_connect_error(); dol_syslog(get_class($this)."::DoliDBSqlite3 : Error Connect ".$this->error,LOG_ERR); @@ -304,6 +305,7 @@ class DoliDBSqlite3 extends DoliDB function select_db($database) { dol_syslog(get_class($this)."::select_db database=".$database, LOG_DEBUG); + // FIXME: sqlite_select_db() does not exist return sqlite_select_db($this->db,$database); } @@ -316,12 +318,12 @@ class DoliDBSqlite3 extends DoliDB * @param string $passwd password * @param string $name name of database (not used for mysql, used for pgsql) * @param integer $port Port of database server - * @return resource Database access handler + * @return SQLite3 Database access handler * @see close */ function connect($host, $login, $passwd, $name, $port=0) { - global $conf,$main_data_dir; + global $main_data_dir; dol_syslog(get_class($this)."::connect name=".$name,LOG_DEBUG); @@ -372,7 +374,7 @@ class DoliDBSqlite3 extends DoliDB /** * Close database connexion * - * @return boolean True if disconnect successfull, false otherwise + * @return bool True if disconnect successfull, false otherwise * @see connect */ function close() @@ -380,9 +382,9 @@ class DoliDBSqlite3 extends DoliDB if ($this->db) { if ($this->transaction_opened > 0) dol_syslog(get_class($this)."::close Closing a connection with an opened transaction depth=".$this->transaction_opened,LOG_ERR); - $this->connected=0; + $this->connected=false; $this->db->close(); - $this->db=null; // Clean this->db + unset($this->db); // Clean this->db return true; } return false; @@ -395,13 +397,11 @@ class DoliDBSqlite3 extends DoliDB * @param int $usesavepoint 0=Default mode, 1=Run a savepoint before and a rollbock to savepoint if error (this allow to have some request with errors inside global transactions). * Note that with Mysql, this parameter is not used as Myssql can already commit a transaction even if one request is in error, without using savepoints. * @param string $type Type of SQL order ('ddl' for insert, update, select, delete or 'dml' for create, alter...) - * @return resource Resultset of answer + * @return SQLite3Result Resultset of answer */ function query($query,$usesavepoint=0,$type='auto') { - $errmsg=''; - - $ret=''; + $ret=null; $query = trim($query); $this->error = 0; @@ -492,8 +492,8 @@ class DoliDBSqlite3 extends DoliDB /** * Renvoie la ligne courante (comme un objet) pour le curseur resultset * - * @param Resultset $resultset Curseur de la requete voulue - * @return Object Object result line or false if KO or end of cursor + * @param SQLite3Result $resultset Curseur de la requete voulue + * @return false|object Object result line or false if KO or end of cursor */ function fetch_object($resultset) { @@ -504,14 +504,15 @@ class DoliDBSqlite3 extends DoliDB if ($ret) { return (object) $ret; } + return false; } /** * Return datas as an array * - * @param Resultset $resultset Resultset of request - * @return array Array + * @param SQLite3Result $resultset Resultset of request + * @return false|array Array or false if KO or end of cursor */ function fetch_array($resultset) { @@ -519,16 +520,14 @@ class DoliDBSqlite3 extends DoliDB if (! is_object($resultset)) { $resultset=$this->_results; } //return $resultset->fetch(PDO::FETCH_ASSOC); $ret = $resultset->fetchArray(SQLITE3_ASSOC); - if ($ret) { - return (array) $ret; - } + return $ret; } /** * Return datas as an array * - * @param resource $resultset Resultset of request - * @return array Array + * @param SQLite3Result $resultset Resultset of request + * @return false|array Array or false if KO or end of cursor */ function fetch_row($resultset) { @@ -541,19 +540,21 @@ class DoliDBSqlite3 extends DoliDB else { // si le curseur est un booleen on retourne la valeur 0 - return 0; + return false; } } /** * Return number of lines for result of a SELECT * - * @param Resultset $resultset Resulset of requests + * @param SQLite3Result $resultset Resulset of requests * @return int Nb of lines * @see affected_rows */ function num_rows($resultset) { + // FIXME: SQLite3Result does not have a queryString member + // If resultset not provided, we take the last used by connexion if (! is_object($resultset)) { $resultset=$this->_results; } if (preg_match("/^SELECT/i", $resultset->queryString)) { @@ -565,12 +566,14 @@ class DoliDBSqlite3 extends DoliDB /** * Return number of lines for result of a SELECT * - * @param Resultset $resultset Resulset of requests + * @param SQLite3Result $resultset Resulset of requests * @return int Nb of lines * @see affected_rows */ function affected_rows($resultset) { + // FIXME: SQLite3Result does not have a queryString member + // If resultset not provided, we take the last used by connexion if (! is_object($resultset)) { $resultset=$this->_results; } if (preg_match("/^SELECT/i", $resultset->queryString)) { @@ -585,10 +588,10 @@ class DoliDBSqlite3 extends DoliDB /** * Free last resultset used. * - * @param integer $resultset Curseur de la requete voulue + * @param SQLite3Result $resultset Curseur de la requete voulue * @return void */ - function free($resultset=0) + function free($resultset=null) { // If resultset not provided, we take the last used by connexion if (! is_object($resultset)) { $resultset=$this->_results; } @@ -789,7 +792,7 @@ class DoliDBSqlite3 extends DoliDB * @param string $charset Charset used to store data * @param string $collation Charset used to sort data * @param string $owner Username of database owner - * @return resource resource defined if OK, null if KO + * @return SQLite3Result resource defined if OK, null if KO */ function DDLCreateDb($database,$charset='',$collation='',$owner='') { @@ -866,11 +869,13 @@ class DoliDBSqlite3 extends DoliDB * @param string $type Type de la table * @param array $unique_keys Tableau associatifs Nom de champs qui seront clef unique => valeur * @param array $fulltext_keys Tableau des Nom de champs qui seront indexes en fulltext - * @param string $keys Tableau des champs cles noms => valeur + * @param array $keys Tableau des champs cles noms => valeur * @return int <0 if KO, >=0 if OK */ - function DDLCreateTable($table,$fields,$primary_key,$type,$unique_keys="",$fulltext_keys="",$keys="") + function DDLCreateTable($table,$fields,$primary_key,$type,$unique_keys=null,$fulltext_keys=null,$keys=null) { + // FIXME: $fulltext_keys parameter is unused + // cles recherchees dans le tableau des descriptions (fields) : type,value,attribute,null,default,extra // ex. : $fields['rowid'] = array('type'=>'int','value'=>'11','null'=>'not null','extra'=> 'auto_increment'); $sql = "create table ".$table."("; @@ -900,7 +905,7 @@ class DoliDBSqlite3 extends DoliDB if($primary_key != "") $pk = "primary key(".$primary_key.")"; - if($unique_keys != "") + if(is_array($unique_keys)) { $i = 0; foreach($unique_keys as $key => $value) @@ -909,7 +914,7 @@ class DoliDBSqlite3 extends DoliDB $i++; } } - if($keys != "") + if(is_array($keys)) { $i = 0; foreach($keys as $key => $value) @@ -921,16 +926,15 @@ class DoliDBSqlite3 extends DoliDB $sql .= implode(',',$sqlfields); if($primary_key != "") $sql .= ",".$pk; - if($unique_keys != "") + if(is_array($unique_keys)) $sql .= ",".implode(',',$sqluq); - if($keys != "") + if(is_array($keys)) $sql .= ",".implode(',',$sqlk); $sql .=") type=".$type; dol_syslog($sql,LOG_DEBUG); if(! $this -> query($sql)) - return -1; - else + return -1; return 1; } @@ -939,7 +943,7 @@ class DoliDBSqlite3 extends DoliDB * * @param string $table Name of table * @param string $field Optionnel : Name of field if we want description of field - * @return resource Resource + * @return SQLite3Result Resource */ function DDLDescTable($table,$field="") { @@ -990,10 +994,7 @@ class DoliDBSqlite3 extends DoliDB { return -1; } - else - { - return 1; - } + return 1; } /** @@ -1014,8 +1015,7 @@ class DoliDBSqlite3 extends DoliDB dol_syslog(get_class($this)."::DDLUpdateField ".$sql,LOG_DEBUG); if (! $this->query($sql)) - return -1; - else + return -1; return 1; } @@ -1035,7 +1035,7 @@ class DoliDBSqlite3 extends DoliDB $this->error=$this->lasterror(); return -1; } - else return 1; + return 1; } @@ -1082,7 +1082,6 @@ class DoliDBSqlite3 extends DoliDB { return -1; } - return 1; } @@ -1141,6 +1140,7 @@ class DoliDBSqlite3 extends DoliDB */ function getPathOfDump() { + // FIXME: not for SQLite $fullpathofdump='/pathtomysqldump/mysqldump'; $resql=$this->query('SHOW VARIABLES LIKE \'basedir\''); @@ -1160,6 +1160,7 @@ class DoliDBSqlite3 extends DoliDB */ function getPathOfRestore() { + // FIXME: not for SQLite $fullpathofimport='/pathtomysql/mysql'; $resql=$this->query('SHOW VARIABLES LIKE \'basedir\''); @@ -1425,10 +1426,10 @@ class DoliDBSqlite3 extends DoliDB /** * calc_daynr * - * @param string $year Year - * @param string $month Month - * @param string $day Day - * @return string La date formatee. + * @param int $year Year + * @param int $month Month + * @param int $day Day + * @return int Formatted date */ private static function calc_daynr($year, $month, $day) { $y = $year; @@ -1446,8 +1447,9 @@ class DoliDBSqlite3 extends DoliDB /** * calc_weekday * - * @param string $daynr ??? - * @param string $sunday_first_day_of_week ??? + * @param int $daynr ??? + * @param bool $sunday_first_day_of_week ??? + * @return int */ private static function calc_weekday($daynr, $sunday_first_day_of_week) { $ret = floor(($daynr + 5 + ($sunday_first_day_of_week ? 1 : 0)) % 7); diff --git a/htdocs/core/filemanagerdol/browser/default/frmactualfolder.php b/htdocs/core/filemanagerdol/browser/default/frmactualfolder.php index d1dccba42c4..38ee8909216 100644 --- a/htdocs/core/filemanagerdol/browser/default/frmactualfolder.php +++ b/htdocs/core/filemanagerdol/browser/default/frmactualfolder.php @@ -106,7 +106,7 @@ window.onload = function()   - / + /       diff --git a/htdocs/core/filemanagerdol/browser/default/frmfolders.php b/htdocs/core/filemanagerdol/browser/default/frmfolders.php index 6dce1154dff..5feb1fedf70 100644 --- a/htdocs/core/filemanagerdol/browser/default/frmfolders.php +++ b/htdocs/core/filemanagerdol/browser/default/frmfolders.php @@ -220,7 +220,7 @@ window.onload = function() - +
diff --git a/htdocs/core/filemanagerdol/browser/default/frmresourceslist.php b/htdocs/core/filemanagerdol/browser/default/frmresourceslist.php index 19d0df5c346..cf6306f9672 100644 --- a/htdocs/core/filemanagerdol/browser/default/frmresourceslist.php +++ b/htdocs/core/filemanagerdol/browser/default/frmresourceslist.php @@ -77,7 +77,7 @@ oListManager.GetFolderRowHtml = function( folderName, folderPath ) '' + sLink + '<\/a>' + - '<\/td> ' + + '<\/td> ' + sLink + folderName + '<\/a>' + @@ -100,7 +100,7 @@ oListManager.GetFileRowHtml = function( fileName, fileUrl, fileSize ) sLink + fileName + '<\/a>' + - '<\/td> ' + + '<\/td> ' + fileSize + ' KB' + '<\/td><\/tr>' ; diff --git a/htdocs/core/lib/ajax.lib.php b/htdocs/core/lib/ajax.lib.php index 8d6bc704e21..0b119ada66a 100644 --- a/htdocs/core/lib/ajax.lib.php +++ b/htdocs/core/lib/ajax.lib.php @@ -132,7 +132,7 @@ function ajax_autocompleter($selected, $htmlname, $url, $urloption='', $minLengt // Disable an element if (options.option_disabled) { if (ui.item.disabled) { - $("#" + options.option_disabled).attr("disabled", "disabled"); + $("#" + options.option_disabled).prop("disabled", true); if (options.error) { $.jnotify(options.error, "error", true); // Output with jnotify the error message } @@ -145,7 +145,7 @@ function ajax_autocompleter($selected, $htmlname, $url, $urloption='', $minLengt } if (options.disabled) { $.each(options.disabled, function(key, value) { - $("#" + value).attr("disabled", "disabled"); + $("#" + value).prop("disabled", true); }); } if (options.show) { @@ -524,7 +524,7 @@ function ajax_object_onoff($object, $code, $field, $text_on, $text_off, $input=a // Disable another element if (input.disabled && input.disabled.length > 0) { $.each(input.disabled, function(key,value) { - $("#" + value).attr("disabled", true); + $("#" + value).prop("disabled", true); if ($("#" + value).hasClass("butAction") == true) { $("#" + value).removeClass("butAction"); $("#" + value).addClass("butActionRefused"); diff --git a/htdocs/core/lib/security.lib.php b/htdocs/core/lib/security.lib.php index f00abaa339f..644e2702a74 100644 --- a/htdocs/core/lib/security.lib.php +++ b/htdocs/core/lib/security.lib.php @@ -331,163 +331,189 @@ function restrictedArea($user, $features, $objectid=0, $dbtablename='', $feature // is linked to a company allowed to $user. if (! empty($objectid) && $objectid > 0) { - foreach ($featuresarray as $feature) - { - $sql=''; - - $check = array('adherent','banque','user','usergroup','produit','service','produit|service','categorie'); // Test on entity only (Objects with no link to company) - $checksoc = array('societe'); // Test for societe object - $checkother = array('contact'); // Test on entity and link to societe. Allowed if link is empty (Ex: contacts...). - $checkproject = array('projet'); // Test for project object - $nocheck = array('barcode','stock','fournisseur'); // No test - $checkdefault = 'all other not already defined'; // Test on entity and link to third party. Not allowed if link is empty (Ex: invoice, orders...). - - // If dbtable not defined, we use same name for table than module name - if (empty($dbtablename)) $dbtablename = $feature; - - // Check permission for object with entity - if (in_array($feature,$check)) - { - $sql = "SELECT dbt.".$dbt_select; - $sql.= " FROM ".MAIN_DB_PREFIX.$dbtablename." as dbt"; - $sql.= " WHERE dbt.".$dbt_select." = ".$objectid; - if (($feature == 'user' || $feature == 'usergroup') && ! empty($conf->multicompany->enabled) && $conf->entity == 1 && $user->admin && ! $user->entity) - { - $sql.= " AND dbt.entity IS NOT NULL"; - } - else - { - $sql.= " AND dbt.entity IN (".getEntity($sharedelement, 1).")"; - } - } - else if (in_array($feature,$checksoc)) // We check feature = checksoc - { - // If external user: Check permission for external users - if ($user->societe_id > 0) - { - if ($user->societe_id <> $objectid) accessforbidden(); - } - // If internal user: Check permission for internal users that are restricted on their objects - else if (! empty($conf->societe->enabled) && ($user->rights->societe->lire && ! $user->rights->societe->client->voir)) - { - $sql = "SELECT sc.fk_soc"; - $sql.= " FROM (".MAIN_DB_PREFIX."societe_commerciaux as sc"; - $sql.= ", ".MAIN_DB_PREFIX."societe as s)"; - $sql.= " WHERE sc.fk_soc = ".$objectid; - $sql.= " AND sc.fk_user = ".$user->id; - $sql.= " AND sc.fk_soc = s.rowid"; - $sql.= " AND s.entity IN (".getEntity($sharedelement, 1).")"; - } - // If multicompany and internal users with all permissions, check user is in correct entity - else if (! empty($conf->multicompany->enabled)) - { - $sql = "SELECT s.rowid"; - $sql.= " FROM ".MAIN_DB_PREFIX."societe as s"; - $sql.= " WHERE s.rowid = ".$objectid; - $sql.= " AND s.entity IN (".getEntity($sharedelement, 1).")"; - } - } - else if (in_array($feature,$checkother)) - { - // If external user: Check permission for external users - if ($user->societe_id > 0) - { - $sql = "SELECT dbt.rowid"; - $sql.= " FROM ".MAIN_DB_PREFIX.$dbtablename." as dbt"; - $sql.= " WHERE dbt.rowid = ".$objectid; - $sql.= " AND dbt.fk_soc = ".$user->societe_id; - } - // If internal user: Check permission for internal users that are restricted on their objects - else if (! empty($conf->societe->enabled) && ($user->rights->societe->lire && ! $user->rights->societe->client->voir)) - { - $sql = "SELECT dbt.rowid"; - $sql.= " FROM ".MAIN_DB_PREFIX.$dbtablename." as dbt"; - $sql.= " LEFT JOIN ".MAIN_DB_PREFIX."societe_commerciaux as sc ON dbt.fk_soc = sc.fk_soc AND sc.fk_user = '".$user->id."'"; - $sql.= " WHERE dbt.rowid = ".$objectid; - $sql.= " AND (dbt.fk_soc IS NULL OR sc.fk_soc IS NOT NULL)"; // Contact not linked to a company or to a company of user - $sql.= " AND dbt.entity IN (".getEntity($sharedelement, 1).")"; - } - // If multicompany and internal users with all permissions, check user is in correct entity - else if (! empty($conf->multicompany->enabled)) - { - $sql = "SELECT dbt.rowid"; - $sql.= " FROM ".MAIN_DB_PREFIX.$dbtablename." as dbt"; - $sql.= " WHERE dbt.rowid = ".$objectid; - $sql.= " AND dbt.entity IN (".getEntity($sharedelement, 1).")"; - } - } - else if (in_array($feature,$checkproject)) - { - if (! empty($conf->projet->enabled) && ! $user->rights->projet->all->lire) - { - include_once DOL_DOCUMENT_ROOT.'/projet/class/project.class.php'; - $projectstatic=new Project($db); - $tmps=$projectstatic->getProjectsAuthorizedForUser($user,0,1,0); - $tmparray=explode(',',$tmps); - if (! in_array($objectid,$tmparray)) accessforbidden(); - } - else - { - $sql = "SELECT dbt.".$dbt_select; - $sql.= " FROM ".MAIN_DB_PREFIX.$dbtablename." as dbt"; - $sql.= " WHERE dbt.".$dbt_select." = ".$objectid; - $sql.= " AND dbt.entity IN (".getEntity($sharedelement, 1).")"; - } - } - else if (! in_array($feature,$nocheck)) // By default we check with link to third party - { - // If external user: Check permission for external users - if ($user->societe_id > 0) - { - if (empty($dbt_keyfield)) dol_print_error('','Param dbt_keyfield is required but not defined'); - $sql = "SELECT dbt.".$dbt_keyfield; - $sql.= " FROM ".MAIN_DB_PREFIX.$dbtablename." as dbt"; - $sql.= " WHERE dbt.rowid = ".$objectid; - $sql.= " AND dbt.".$dbt_keyfield." = ".$user->societe_id; - } - // If internal user: Check permission for internal users that are restricted on their objects - else if (! empty($conf->societe->enabled) && ($user->rights->societe->lire && ! $user->rights->societe->client->voir)) - { - if (empty($dbt_keyfield)) dol_print_error('','Param dbt_keyfield is required but not defined'); - $sql = "SELECT sc.fk_soc"; - $sql.= " FROM ".MAIN_DB_PREFIX.$dbtablename." as dbt"; - $sql.= ", ".MAIN_DB_PREFIX."societe as s"; - $sql.= ", ".MAIN_DB_PREFIX."societe_commerciaux as sc"; - $sql.= " WHERE dbt.".$dbt_select." = ".$objectid; - $sql.= " AND sc.fk_soc = dbt.".$dbt_keyfield; - $sql.= " AND dbt.".$dbt_keyfield." = s.rowid"; - $sql.= " AND s.entity IN (".getEntity($sharedelement, 1).")"; - $sql.= " AND sc.fk_user = ".$user->id; - } - // If multicompany and internal users with all permissions, check user is in correct entity - else if (! empty($conf->multicompany->enabled)) - { - $sql = "SELECT dbt.".$dbt_select; - $sql.= " FROM ".MAIN_DB_PREFIX.$dbtablename." as dbt"; - $sql.= " WHERE dbt.".$dbt_select." = ".$objectid; - $sql.= " AND dbt.entity IN (".getEntity($sharedelement, 1).")"; - } - } - - //print "sql=".$sql."
"; - if ($sql) - { - $resql=$db->query($sql); - if ($resql) - { - if ($db->num_rows($resql) == 0) accessforbidden(); - } - else - { - accessforbidden(); - } - } - } + $ok = checkUserAccessToObject($user, $featuresarray,$objectid,$dbtablename,$feature2,$dbt_keyfield,$dbt_select); + return $ok ? 1 : accessforbidden(); } return 1; } +/** + * Check access by user to object + * + * @param User $user User to check + * @param array $featuresarray Features/modules to check + * @param int $objectid Object ID if we want to check a particular record (optional) is linked to a owned thirdparty (optional). + * @param string $dbtablename 'TableName&SharedElement' with Tablename is table where object is stored. SharedElement is an optional key to define where to check entity. Not used if objectid is null (optional) + * @param string $feature2 Feature to check, second level of permission (optional). Can be or check with 'level1|level2'. + * @param string $dbt_keyfield Field name for socid foreign key if not fk_soc. Not used if objectid is null (optional) + * @param string $dbt_select Field name for select if not rowid. Not used if objectid is null (optional) + * + * @return bool True if user has access, False otherwise + */ +function checkUserAccessToObject($user, $featuresarray, $objectid=0, $dbtablename='', $feature2='', $dbt_keyfield='', $dbt_select='') +{ + global $db, $conf; + + // More parameters + $params = explode('&', $dbtablename); + $dbtablename=(! empty($params[0]) ? $params[0] : ''); + $sharedelement=(! empty($params[1]) ? $params[1] : $dbtablename); + + foreach ($featuresarray as $feature) + { + $sql=''; + + $check = array('adherent','banque','user','usergroup','produit','service','produit|service','categorie'); // Test on entity only (Objects with no link to company) + $checksoc = array('societe'); // Test for societe object + $checkother = array('contact'); // Test on entity and link to societe. Allowed if link is empty (Ex: contacts...). + $checkproject = array('projet'); // Test for project object + $nocheck = array('barcode','stock','fournisseur'); // No test + $checkdefault = 'all other not already defined'; // Test on entity and link to third party. Not allowed if link is empty (Ex: invoice, orders...). + + // If dbtable not defined, we use same name for table than module name + if (empty($dbtablename)) $dbtablename = $feature; + + // Check permission for object with entity + if (in_array($feature,$check)) + { + $sql = "SELECT dbt.".$dbt_select; + $sql.= " FROM ".MAIN_DB_PREFIX.$dbtablename." as dbt"; + $sql.= " WHERE dbt.".$dbt_select." = ".$objectid; + if (($feature == 'user' || $feature == 'usergroup') && ! empty($conf->multicompany->enabled) && $conf->entity == 1 && $user->admin && ! $user->entity) + { + $sql.= " AND dbt.entity IS NOT NULL"; + } + else + { + $sql.= " AND dbt.entity IN (".getEntity($sharedelement, 1).")"; + } + } + else if (in_array($feature,$checksoc)) // We check feature = checksoc + { + // If external user: Check permission for external users + if ($user->societe_id > 0) + { + if ($user->societe_id <> $objectid) return false; + } + // If internal user: Check permission for internal users that are restricted on their objects + else if (! empty($conf->societe->enabled) && ($user->rights->societe->lire && ! $user->rights->societe->client->voir)) + { + $sql = "SELECT sc.fk_soc"; + $sql.= " FROM (".MAIN_DB_PREFIX."societe_commerciaux as sc"; + $sql.= ", ".MAIN_DB_PREFIX."societe as s)"; + $sql.= " WHERE sc.fk_soc = ".$objectid; + $sql.= " AND sc.fk_user = ".$user->id; + $sql.= " AND sc.fk_soc = s.rowid"; + $sql.= " AND s.entity IN (".getEntity($sharedelement, 1).")"; + } + // If multicompany and internal users with all permissions, check user is in correct entity + else if (! empty($conf->multicompany->enabled)) + { + $sql = "SELECT s.rowid"; + $sql.= " FROM ".MAIN_DB_PREFIX."societe as s"; + $sql.= " WHERE s.rowid = ".$objectid; + $sql.= " AND s.entity IN (".getEntity($sharedelement, 1).")"; + } + } + else if (in_array($feature,$checkother)) + { + // If external user: Check permission for external users + if ($user->societe_id > 0) + { + $sql = "SELECT dbt.rowid"; + $sql.= " FROM ".MAIN_DB_PREFIX.$dbtablename." as dbt"; + $sql.= " WHERE dbt.rowid = ".$objectid; + $sql.= " AND dbt.fk_soc = ".$user->societe_id; + } + // If internal user: Check permission for internal users that are restricted on their objects + else if (! empty($conf->societe->enabled) && ($user->rights->societe->lire && ! $user->rights->societe->client->voir)) + { + $sql = "SELECT dbt.rowid"; + $sql.= " FROM ".MAIN_DB_PREFIX.$dbtablename." as dbt"; + $sql.= " LEFT JOIN ".MAIN_DB_PREFIX."societe_commerciaux as sc ON dbt.fk_soc = sc.fk_soc AND sc.fk_user = '".$user->id."'"; + $sql.= " WHERE dbt.rowid = ".$objectid; + $sql.= " AND (dbt.fk_soc IS NULL OR sc.fk_soc IS NOT NULL)"; // Contact not linked to a company or to a company of user + $sql.= " AND dbt.entity IN (".getEntity($sharedelement, 1).")"; + } + // If multicompany and internal users with all permissions, check user is in correct entity + else if (! empty($conf->multicompany->enabled)) + { + $sql = "SELECT dbt.rowid"; + $sql.= " FROM ".MAIN_DB_PREFIX.$dbtablename." as dbt"; + $sql.= " WHERE dbt.rowid = ".$objectid; + $sql.= " AND dbt.entity IN (".getEntity($sharedelement, 1).")"; + } + } + else if (in_array($feature,$checkproject)) + { + if (! empty($conf->projet->enabled) && ! $user->rights->projet->all->lire) + { + include_once DOL_DOCUMENT_ROOT.'/projet/class/project.class.php'; + $projectstatic=new Project($db); + $tmps=$projectstatic->getProjectsAuthorizedForUser($user,0,1,0); + $tmparray=explode(',',$tmps); + if (! in_array($objectid,$tmparray)) return false; + } + else + { + $sql = "SELECT dbt.".$dbt_select; + $sql.= " FROM ".MAIN_DB_PREFIX.$dbtablename." as dbt"; + $sql.= " WHERE dbt.".$dbt_select." = ".$objectid; + $sql.= " AND dbt.entity IN (".getEntity($sharedelement, 1).")"; + } + } + else if (! in_array($feature,$nocheck)) // By default we check with link to third party + { + // If external user: Check permission for external users + if ($user->societe_id > 0) + { + if (empty($dbt_keyfield)) dol_print_error('','Param dbt_keyfield is required but not defined'); + $sql = "SELECT dbt.".$dbt_keyfield; + $sql.= " FROM ".MAIN_DB_PREFIX.$dbtablename." as dbt"; + $sql.= " WHERE dbt.rowid = ".$objectid; + $sql.= " AND dbt.".$dbt_keyfield." = ".$user->societe_id; + } + // If internal user: Check permission for internal users that are restricted on their objects + else if (! empty($conf->societe->enabled) && ($user->rights->societe->lire && ! $user->rights->societe->client->voir)) + { + if (empty($dbt_keyfield)) dol_print_error('','Param dbt_keyfield is required but not defined'); + $sql = "SELECT sc.fk_soc"; + $sql.= " FROM ".MAIN_DB_PREFIX.$dbtablename." as dbt"; + $sql.= ", ".MAIN_DB_PREFIX."societe as s"; + $sql.= ", ".MAIN_DB_PREFIX."societe_commerciaux as sc"; + $sql.= " WHERE dbt.".$dbt_select." = ".$objectid; + $sql.= " AND sc.fk_soc = dbt.".$dbt_keyfield; + $sql.= " AND dbt.".$dbt_keyfield." = s.rowid"; + $sql.= " AND s.entity IN (".getEntity($sharedelement, 1).")"; + $sql.= " AND sc.fk_user = ".$user->id; + } + // If multicompany and internal users with all permissions, check user is in correct entity + else if (! empty($conf->multicompany->enabled)) + { + $sql = "SELECT dbt.".$dbt_select; + $sql.= " FROM ".MAIN_DB_PREFIX.$dbtablename." as dbt"; + $sql.= " WHERE dbt.".$dbt_select." = ".$objectid; + $sql.= " AND dbt.entity IN (".getEntity($sharedelement, 1).")"; + } + } + + //print "sql=".$sql."
"; + if ($sql) + { + $resql=$db->query($sql); + if ($resql) + { + if ($db->num_rows($resql) == 0) return false; + } + else + { + return false; + } + } + } + return true; +} /** * Show a message to say access is forbidden and stop program diff --git a/htdocs/core/modules/modApi.class.php b/htdocs/core/modules/modApi.class.php new file mode 100644 index 00000000000..2bdcbe03c54 --- /dev/null +++ b/htdocs/core/modules/modApi.class.php @@ -0,0 +1,252 @@ + + * Copyright (C) 2004-2012 Laurent Destailleur + * Copyright (C) 2015 Jean-François Ferry + * + * 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 . + */ + +/** + * \defgroup api Module Api + * \brief Descriptor file for Api modulee + * \file htdocs/api/core/modules/modApi.class.php + * \ingroup api + * \brief Description and activation file for module Api + */ +include_once DOL_DOCUMENT_ROOT .'/core/modules/DolibarrModules.class.php'; + + +/** + * Description and activation class for module Api + */ +class modApi extends DolibarrModules +{ + /** + * Constructor. Define names, constants, directories, boxes, permissions + * + * @param DoliDB $db Database handler + */ + function __construct($db) + { + global $langs,$conf; + + $this->db = $db; + + // Id for module (must be unique). + // Use here a free id (See in Home -> System information -> Dolibarr for list of used modules id). + $this->numero = 2610; + // Key text used to identify module (for permissions, menus, etc...) + $this->rights_class = 'api'; + + // Family can be 'crm','financial','hr','projects','products','ecm','technic','other' + // It is used to group modules in module setup page + $this->family = "technic"; + // Module label (no space allowed), used if translation string 'ModuleXXXName' not found (where XXX is value of numeric property 'numero' of module) + $this->name = preg_replace('/^mod/i','',get_class($this)); + // Module description, used if translation string 'ModuleXXXDesc' not found (where XXX is value of numeric property 'numero' of module) + $this->description = "REST interface"; + // Possible values for version are: 'development', 'experimental', 'dolibarr' or 'dolibarr_deprecated' or version + $this->version = 'development'; + // Key used in llx_const table to save module status enabled/disabled (where MYMODULE is value of property name of module in uppercase) + $this->const_name = 'MAIN_MODULE_'.strtoupper($this->name); + // Where to store the module in setup page (0=common,1=interface,2=others,3=very specific) + $this->special = 1; + // Name of image file used for this module. + // If file is in theme/yourtheme/img directory under name object_pictovalue.png, use this->picto='pictovalue' + // If file is in module/img directory under name object_pictovalue.png, use this->picto='pictovalue@module' + $this->picto='technic'; + + + $this->module_parts = array(); + + // Data directories to create when module is enabled. + // Example: this->dirs = array("/api/temp"); + $this->dirs = array(); + + // Config pages. Put here list of php page, stored into api/admin directory, to use to setup module. + $this->config_page_url = array("api.php@api"); + + // Dependencies + $this->hidden = false; // A condition to hide module + $this->depends = array(); // List of modules id that must be enabled if this module is enabled + $this->requiredby = array(); // List of modules id to disable if this one is disabled + $this->conflictwith = array(); // List of modules id this module is in conflict with + $this->phpmin = array(5,3); // Minimum version of PHP required by module + $this->langfiles = array("other"); + + // Constants + // List of particular constants to add when module is enabled (key, 'chaine', value, desc, visible, 'current' or 'allentities', deleteonunactive) + // Example: $this->const=array(0=>array('MYMODULE_MYNEWCONST1','chaine','myvalue','This is a constant to add',1), + // 1=>array('MYMODULE_MYNEWCONST2','chaine','myvalue','This is another constant to add',0, 'current', 1) + // ); + $this->const = array(); + + // Array to add new pages in new tabs + // Example: $this->tabs = array('objecttype:+tabname1:Title1:mylangfile@api:$user->rights->api->read:/api/mynewtab1.php?id=__ID__', // To add a new tab identified by code tabname1 + // 'objecttype:+tabname2:SUBSTITUTION_Title2:mylangfile@api:$user->rights->othermodule->read:/api/mynewtab2.php?id=__ID__', // To add another new tab identified by code tabname2. Label will be result of calling all substitution functions on 'Title2' key. + // 'objecttype:-tabname:NU:conditiontoremove'); // To remove an existing tab identified by code tabname + // where objecttype can be + // 'categories_x' to add a tab in category view (replace 'x' by type of category (0=product, 1=supplier, 2=customer, 3=member) + // 'contact' to add a tab in contact view + // 'contract' to add a tab in contract view + // 'group' to add a tab in group view + // 'intervention' to add a tab in intervention view + // 'invoice' to add a tab in customer invoice view + // 'invoice_supplier' to add a tab in supplier invoice view + // 'member' to add a tab in fundation member view + // 'opensurveypoll' to add a tab in opensurvey poll view + // 'order' to add a tab in customer order view + // 'order_supplier' to add a tab in supplier order view + // 'payment' to add a tab in payment view + // 'payment_supplier' to add a tab in supplier payment view + // 'product' to add a tab in product view + // 'propal' to add a tab in propal view + // 'project' to add a tab in project view + // 'stock' to add a tab in stock view + // 'thirdparty' to add a tab in third party view + // 'user' to add a tab in user view + $this->tabs = array(); + + // Dictionaries + if (! isset($conf->api->enabled)) + { + $conf->api=new stdClass(); + $conf->api->enabled=0; + } + $this->dictionaries=array(); + /* Example: + if (! isset($conf->api->enabled)) $conf->api->enabled=0; // This is to avoid warnings + $this->dictionaries=array( + 'langs'=>'mylangfile@api', + 'tabname'=>array(MAIN_DB_PREFIX."table1",MAIN_DB_PREFIX."table2",MAIN_DB_PREFIX."table3"), // List of tables we want to see into dictonnary editor + 'tablib'=>array("Table1","Table2","Table3"), // Label of tables + 'tabsql'=>array('SELECT f.rowid as rowid, f.code, f.label, f.active FROM '.MAIN_DB_PREFIX.'table1 as f','SELECT f.rowid as rowid, f.code, f.label, f.active FROM '.MAIN_DB_PREFIX.'table2 as f','SELECT f.rowid as rowid, f.code, f.label, f.active FROM '.MAIN_DB_PREFIX.'table3 as f'), // Request to select fields + 'tabsqlsort'=>array("label ASC","label ASC","label ASC"), // Sort order + 'tabfield'=>array("code,label","code,label","code,label"), // List of fields (result of select to show dictionary) + 'tabfieldvalue'=>array("code,label","code,label","code,label"), // List of fields (list of fields to edit a record) + 'tabfieldinsert'=>array("code,label","code,label","code,label"), // List of fields (list of fields for insert) + 'tabrowid'=>array("rowid","rowid","rowid"), // Name of columns with primary key (try to always name it 'rowid') + 'tabcond'=>array($conf->api->enabled,$conf->api->enabled,$conf->api->enabled) // Condition to show each dictionary + ); + */ + + // Boxes + // Add here list of php file(s) stored in core/boxes that contains class to show a box. + $this->boxes = array(); // List of boxes + // Example: + //$this->boxes=array(array(0=>array('file'=>'myboxa.php','note'=>'','enabledbydefaulton'=>'Home'),1=>array('file'=>'myboxb.php','note'=>''),2=>array('file'=>'myboxc.php','note'=>''));); + + // Permissions + $this->rights = array(); // Permission array used by this module + $r=0; + + // Add here list of permission defined by an id, a label, a boolean and two constant strings. + // Example: + // $this->rights[$r][0] = $this->numero + $r; // Permission id (must not be already used) + // $this->rights[$r][1] = 'Permision label'; // Permission label + // $this->rights[$r][3] = 1; // Permission by default for new user (0/1) + // $this->rights[$r][4] = 'level1'; // In php code, permission will be checked by test if ($user->rights->permkey->level1->level2) + // $this->rights[$r][5] = 'level2'; // In php code, permission will be checked by test if ($user->rights->permkey->level1->level2) + // $r++; + + + // Main menu entries + $this->menu = array(); // List of menus to add + $r=0; + + // Add here entries to declare new menus + // + // Example to declare a new Top Menu entry and its Left menu entry: + // $this->menu[$r]=array( 'fk_menu'=>0, // Put 0 if this is a top menu + // 'type'=>'top', // This is a Top menu entry + // 'titre'=>'Api top menu', + // 'mainmenu'=>'api', + // 'leftmenu'=>'api', + // 'url'=>'/api/pagetop.php', + // 'langs'=>'mylangfile@api', // Lang file to use (without .lang) by module. File must be in langs/code_CODE/ directory. + // 'position'=>100, + // 'enabled'=>'$conf->api->enabled', // Define condition to show or hide menu entry. Use '$conf->api->enabled' if entry must be visible if module is enabled. + // 'perms'=>'1', // Use 'perms'=>'$user->rights->api->level1->level2' if you want your menu with a permission rules + // 'target'=>'', + // 'user'=>2); // 0=Menu for internal users, 1=external users, 2=both + // $r++; + // + // Example to declare a Left Menu entry into an existing Top menu entry: + // $this->menu[$r]=array( 'fk_menu'=>'fk_mainmenu=xxx', // Use 'fk_mainmenu=xxx' or 'fk_mainmenu=xxx,fk_leftmenu=yyy' where xxx is mainmenucode and yyy is a leftmenucode + // 'type'=>'left', // This is a Left menu entry + // 'titre'=>'Api left menu', + // 'mainmenu'=>'xxx', + // 'leftmenu'=>'api', + // 'url'=>'/api/pagelevel2.php', + // 'langs'=>'mylangfile@api', // Lang file to use (without .lang) by module. File must be in langs/code_CODE/ directory. + // 'position'=>100, + // 'enabled'=>'$conf->api->enabled', // Define condition to show or hide menu entry. Use '$conf->api->enabled' if entry must be visible if module is enabled. Use '$leftmenu==\'system\'' to show if leftmenu system is selected. + // 'perms'=>'1', // Use 'perms'=>'$user->rights->api->level1->level2' if you want your menu with a permission rules + // 'target'=>'', + // 'user'=>2); // 0=Menu for internal users, 1=external users, 2=both + // $r++; + + + // Exports + $r=1; + + // Example: + // $this->export_code[$r]=$this->rights_class.'_'.$r; + // $this->export_label[$r]='CustomersInvoicesAndInvoiceLines'; // Translation key (used only if key ExportDataset_xxx_z not found) + // $this->export_enabled[$r]='1'; // Condition to show export in list (ie: '$user->id==3'). Set to 1 to always show when module is enabled. + // $this->export_permission[$r]=array(array("facture","facture","export")); + // $this->export_fields_array[$r]=array('s.rowid'=>"IdCompany",'s.nom'=>'CompanyName','s.address'=>'Address','s.zip'=>'Zip','s.town'=>'Town','s.fk_pays'=>'Country','s.phone'=>'Phone','s.siren'=>'ProfId1','s.siret'=>'ProfId2','s.ape'=>'ProfId3','s.idprof4'=>'ProfId4','s.code_compta'=>'CustomerAccountancyCode','s.code_compta_fournisseur'=>'SupplierAccountancyCode','f.rowid'=>"InvoiceId",'f.facnumber'=>"InvoiceRef",'f.datec'=>"InvoiceDateCreation",'f.datef'=>"DateInvoice",'f.total'=>"TotalHT",'f.total_ttc'=>"TotalTTC",'f.tva'=>"TotalVAT",'f.paye'=>"InvoicePaid",'f.fk_statut'=>'InvoiceStatus','f.note'=>"InvoiceNote",'fd.rowid'=>'LineId','fd.description'=>"LineDescription",'fd.price'=>"LineUnitPrice",'fd.tva_tx'=>"LineVATRate",'fd.qty'=>"LineQty",'fd.total_ht'=>"LineTotalHT",'fd.total_tva'=>"LineTotalTVA",'fd.total_ttc'=>"LineTotalTTC",'fd.date_start'=>"DateStart",'fd.date_end'=>"DateEnd",'fd.fk_product'=>'ProductId','p.ref'=>'ProductRef'); + // $this->export_entities_array[$r]=array('s.rowid'=>"company",'s.nom'=>'company','s.address'=>'company','s.zip'=>'company','s.town'=>'company','s.fk_pays'=>'company','s.phone'=>'company','s.siren'=>'company','s.siret'=>'company','s.ape'=>'company','s.idprof4'=>'company','s.code_compta'=>'company','s.code_compta_fournisseur'=>'company','f.rowid'=>"invoice",'f.facnumber'=>"invoice",'f.datec'=>"invoice",'f.datef'=>"invoice",'f.total'=>"invoice",'f.total_ttc'=>"invoice",'f.tva'=>"invoice",'f.paye'=>"invoice",'f.fk_statut'=>'invoice','f.note'=>"invoice",'fd.rowid'=>'invoice_line','fd.description'=>"invoice_line",'fd.price'=>"invoice_line",'fd.total_ht'=>"invoice_line",'fd.total_tva'=>"invoice_line",'fd.total_ttc'=>"invoice_line",'fd.tva_tx'=>"invoice_line",'fd.qty'=>"invoice_line",'fd.date_start'=>"invoice_line",'fd.date_end'=>"invoice_line",'fd.fk_product'=>'product','p.ref'=>'product'); + // $this->export_sql_start[$r]='SELECT DISTINCT '; + // $this->export_sql_end[$r] =' FROM ('.MAIN_DB_PREFIX.'facture as f, '.MAIN_DB_PREFIX.'facturedet as fd, '.MAIN_DB_PREFIX.'societe as s)'; + // $this->export_sql_end[$r] .=' LEFT JOIN '.MAIN_DB_PREFIX.'product as p on (fd.fk_product = p.rowid)'; + // $this->export_sql_end[$r] .=' WHERE f.fk_soc = s.rowid AND f.rowid = fd.fk_facture'; + // $this->export_sql_order[$r] .=' ORDER BY s.nom'; + // $r++; + } + + /** + * Function called when module is enabled. + * The init function add constants, boxes, permissions and menus (defined in constructor) into Dolibarr database. + * It also creates data directories + * + * @param string $options Options when enabling module ('', 'noboxes') + * @return int 1 if OK, 0 if KO + */ + function init($options='') + { + $sql = array(); + + $result=$this->_load_tables('/api/sql/'); + + return $this->_init($sql, $options); + } + + /** + * Function called when module is disabled. + * Remove from database constants, boxes and permissions from Dolibarr database. + * Data directories are not deleted + * + * @param string $options Options when enabling module ('', 'noboxes') + * @return int 1 if OK, 0 if KO + */ + function remove($options='') + { + $sql = array(); + + return $this->_remove($sql, $options); + } + +} + diff --git a/htdocs/core/tpl/admin_extrafields_add.tpl.php b/htdocs/core/tpl/admin_extrafields_add.tpl.php index 293a363edfd..44de526942f 100644 --- a/htdocs/core/tpl/admin_extrafields_add.tpl.php +++ b/htdocs/core/tpl/admin_extrafields_add.tpl.php @@ -42,27 +42,27 @@ if (GETPOST('type') == "separate") { - print "jQuery('#size, #unique, #required, #default_value').val('').attr('disabled','disabled');"; + print "jQuery('#size, #unique, #required, #default_value').val('').prop('disabled', true);"; print 'jQuery("#value_choice").hide();'; } ?> - if (type == 'date') { size.val('').attr('disabled','disabled'); unique.removeAttr('disabled','disabled'); jQuery("#value_choice").hide();jQuery("#helpchkbxlst").hide(); } - else if (type == 'datetime') { size.val('').attr('disabled','disabled'); unique.removeAttr('disabled','disabled'); jQuery("#value_choice").hide(); jQuery("#helpchkbxlst").hide();} + if (type == 'date') { size.val('').prop('disabled', true); unique.removeAttr('disabled'); jQuery("#value_choice").hide();jQuery("#helpchkbxlst").hide(); } + else if (type == 'datetime') { size.val('').prop('disabled', true); unique.removeAttr('disabled'); jQuery("#value_choice").hide(); jQuery("#helpchkbxlst").hide();} else if (type == 'double') { size.val('24,8').removeAttr('disabled'); unique.removeAttr('disabled','disabled'); jQuery("#value_choice").hide(); jQuery("#helpchkbxlst").hide();} - else if (type == 'int') { size.val('10').removeAttr('disabled'); unique.removeAttr('disabled','disabled'); jQuery("#value_choice").hide(); jQuery("#helpchkbxlst").hide();} - else if (type == 'text') { size.val('2000').removeAttr('disabled'); unique.attr('disabled','disabled').removeAttr('checked'); jQuery("#value_choice").hide();jQuery("#helpchkbxlst").hide(); } + else if (type == 'int') { size.val('10').removeAttr('disabled'); unique.removeAttr('disabled'); jQuery("#value_choice").hide(); jQuery("#helpchkbxlst").hide();} + else if (type == 'text') { size.val('2000').removeAttr('disabled'); unique.prop('disabled', true).removeAttr('checked'); jQuery("#value_choice").hide();jQuery("#helpchkbxlst").hide(); } else if (type == 'varchar') { size.val('255').removeAttr('disabled'); unique.removeAttr('disabled','disabled'); jQuery("#value_choice").hide();jQuery("#helpchkbxlst").hide(); } - else if (type == 'boolean') { size.val('').attr('disabled','disabled'); unique.attr('disabled','disabled'); jQuery("#value_choice").hide();jQuery("#helpchkbxlst").hide();} - else if (type == 'price') { size.val('').attr('disabled','disabled'); unique.attr('disabled','disabled'); jQuery("#value_choice").hide();jQuery("#helpchkbxlst").hide();} - else if (type == 'select') { size.val('').attr('disabled','disabled'); unique.attr('disabled','disabled'); jQuery("#value_choice").show();jQuery("#helpselect").show();jQuery("#helpsellist").hide();jQuery("#helpchkbxlst").hide();jQuery("#helplink").hide();} - else if (type == 'link') { size.val('').attr('disabled','disabled'); unique.attr('disabled','disabled'); jQuery("#value_choice").show();jQuery("#helpselect").hide();jQuery("#helpsellist").hide();jQuery("#helpchkbxlst").hide();jQuery("#helplink").show();} - else if (type == 'sellist') { size.val('').attr('disabled','disabled'); unique.attr('disabled','disabled'); jQuery("#value_choice").show();jQuery("#helpselect").hide();jQuery("#helpsellist").show();jQuery("#helpchkbxlst").hide();jQuery("#helplink").hide();} - else if (type == 'radio') { size.val('').attr('disabled','disabled'); unique.attr('disabled','disabled'); jQuery("#value_choice").show();jQuery("#helpselect").show();jQuery("#helpsellist").hide();jQuery("#helpchkbxlst").hide();jQuery("#helplink").hide();} - else if (type == 'checkbox') { size.val('').attr('disabled','disabled'); unique.attr('disabled','disabled'); jQuery("#value_choice").show();jQuery("#helpselect").show();jQuery("#helpsellist").hide();jQuery("#helpchkbxlst").hide();jQuery("#helplink").hide();} - else if (type == 'chkbxlst') { size.val('').attr('disabled','disabled'); unique.attr('disabled','disabled'); jQuery("#value_choice").show();jQuery("#helpselect").hide();jQuery("#helpsellist").hide();jQuery("#helpchkbxlst").show();jQuery("#helplink").hide();} - else if (type == 'separate') { size.val('').attr('disabled','disabled'); unique.attr('disabled','disabled'); required.val('').attr('disabled','disabled'); default_value.val('').attr('disabled','disabled'); jQuery("#value_choice").hide();jQuery("#helpselect").hide();jQuery("#helpsellist").hide();jQuery("#helpchkbxlst").hide();jQuery("#helplink").hide();} - else size.val('').attr('disabled','disabled'); + else if (type == 'boolean') { size.val('').prop('disabled', true); unique.prop('disabled', true); jQuery("#value_choice").hide();jQuery("#helpchkbxlst").hide();} + else if (type == 'price') { size.val('').prop('disabled', true); unique.prop('disabled', true); jQuery("#value_choice").hide();jQuery("#helpchkbxlst").hide();} + else if (type == 'select') { size.val('').prop('disabled', true); unique.prop('disabled', true); jQuery("#value_choice").show();jQuery("#helpselect").show();jQuery("#helpsellist").hide();jQuery("#helpchkbxlst").hide();jQuery("#helplink").hide();} + else if (type == 'link') { size.val('').prop('disabled', true); unique.prop('disabled', true); jQuery("#value_choice").show();jQuery("#helpselect").hide();jQuery("#helpsellist").hide();jQuery("#helpchkbxlst").hide();jQuery("#helplink").show();} + else if (type == 'sellist') { size.val('').prop('disabled', true); unique.prop('disabled', true); jQuery("#value_choice").show();jQuery("#helpselect").hide();jQuery("#helpsellist").show();jQuery("#helpchkbxlst").hide();jQuery("#helplink").hide();} + else if (type == 'radio') { size.val('').prop('disabled', true); unique.prop('disabled', true); jQuery("#value_choice").show();jQuery("#helpselect").show();jQuery("#helpsellist").hide();jQuery("#helpchkbxlst").hide();jQuery("#helplink").hide();} + else if (type == 'checkbox') { size.val('').prop('disabled', true); unique.prop('disabled', true); jQuery("#value_choice").show();jQuery("#helpselect").show();jQuery("#helpsellist").hide();jQuery("#helpchkbxlst").hide();jQuery("#helplink").hide();} + else if (type == 'chkbxlst') { size.val('').prop('disabled', true); unique.prop('disabled', true); jQuery("#value_choice").show();jQuery("#helpselect").hide();jQuery("#helpsellist").hide();jQuery("#helpchkbxlst").show();jQuery("#helplink").hide();} + else if (type == 'separate') { size.val('').prop('disabled', true); unique.prop('disabled', true); required.val('').prop('disabled', true); default_value.val('').prop('disabled', true); jQuery("#value_choice").hide();jQuery("#helpselect").hide();jQuery("#helpsellist").hide();jQuery("#helpchkbxlst").hide();jQuery("#helplink").hide();} + else size.val('').prop('disabled', true); } init_typeoffields(''); jQuery("#type").change(function() { diff --git a/htdocs/core/tpl/admin_extrafields_edit.tpl.php b/htdocs/core/tpl/admin_extrafields_edit.tpl.php index 91b861381ae..9c283b83867 100644 --- a/htdocs/core/tpl/admin_extrafields_edit.tpl.php +++ b/htdocs/core/tpl/admin_extrafields_edit.tpl.php @@ -25,15 +25,15 @@ var size = jQuery("#size"); var unique = jQuery("#unique"); var required = jQuery("#required"); - if (type == 'date') { size.attr('disabled','disabled'); } - else if (type == 'datetime') { size.attr('disabled','disabled'); } + if (type == 'date') { size.prop('disabled', true); } + else if (type == 'datetime') { size.prop('disabled', true); } else if (type == 'double') { size.removeAttr('disabled'); } else if (type == 'int') { size.removeAttr('disabled'); } - else if (type == 'text') { size.removeAttr('disabled'); unique.attr('disabled','disabled').removeAttr('checked'); } + else if (type == 'text') { size.removeAttr('disabled'); unique.prop('disabled', true).removeAttr('checked'); } else if (type == 'varchar') { size.removeAttr('disabled'); } - else if (type == 'boolean') { size.val('').attr('disabled','disabled'); unique.attr('disabled','disabled');} - else if (type == 'price') { size.val('').attr('disabled','disabled'); unique.attr('disabled','disabled');} - else size.val('').attr('disabled','disabled'); + else if (type == 'boolean') { size.val('').prop('disabled', true); unique.prop('disabled', true);} + else if (type == 'price') { size.val('').prop('disabled', true); unique.prop('disabled', true);} + else size.val('').prop('disabled', true); } init_typeoffields(jQuery("#type").val()); }); diff --git a/htdocs/core/tpl/objectline_create.tpl.php b/htdocs/core/tpl/objectline_create.tpl.php index 0d18cd9b8b5..9c6cfd2e1be 100644 --- a/htdocs/core/tpl/objectline_create.tpl.php +++ b/htdocs/core/tpl/objectline_create.tpl.php @@ -583,8 +583,8 @@ function setforfree() { jQuery("#idprod").val(''); jQuery("#idprodfournprice").val('0'); // Set cursor on not selected product jQuery("#search_idprodfournprice").val(''); - jQuery("#prod_entry_mode_free").attr('checked',true); - jQuery("#prod_entry_mode_predef").attr('checked',false); + jQuery("#prod_entry_mode_free").prop('checked',true); + jQuery("#prod_entry_mode_predef").prop('checked',false); jQuery("#price_ht").show(); jQuery("#price_ttc").show(); // May no exists jQuery("#tva_tx").show(); @@ -601,8 +601,8 @@ function setforfree() { } function setforpredef() { jQuery("#select_type").val(-1); - jQuery("#prod_entry_mode_free").attr('checked',false); - jQuery("#prod_entry_mode_predef").attr('checked',true); + jQuery("#prod_entry_mode_free").prop('checked',false); + jQuery("#prod_entry_mode_predef").prop('checked',true); jQuery("#price_ht").hide(); jQuery("#title_up_ht").hide(); jQuery("#price_ttc").hide(); // May no exists diff --git a/htdocs/core/tpl/objectline_edit.tpl.php b/htdocs/core/tpl/objectline_edit.tpl.php index bc23438dfbc..d64ef7a2b3e 100644 --- a/htdocs/core/tpl/objectline_edit.tpl.php +++ b/htdocs/core/tpl/objectline_edit.tpl.php @@ -99,7 +99,7 @@ $coldisplay=-1; // We remove first td $doleditor=new DolEditor('product_desc',$line->description,'',164,$toolbarname,'',false,true,$enable,$nbrows,'98%'); $doleditor->Create(); } else { - print ''; + print ''; } ?> @@ -113,19 +113,19 @@ $coldisplay=-1; // We remove first td if ($this->situation_counter == 1 || !$this->situation_cycle_ref) { print '' . $form->load_tva('tva_tx',$line->tva_tx,$seller,$buyer,0,$line->info_bits,$line->product_type) . ''; } else { - print '%'; + print '%'; } $coldisplay++; print 'situation_counter > 1) print ' readonly="readonly"'; + if ($this->situation_counter > 1) print ' readonly'; print '>'; if ($inputalsopricewithtax) { $coldisplay++; print 'situation_counter > 1) print ' readonly="readonly"'; + if ($this->situation_counter > 1) print ' readonly'; print '>'; } ?> @@ -136,7 +136,7 @@ $coldisplay=-1; // We remove first td // must also not be output for most entities (proposal, intervention, ...) //if($line->qty > $line->stock) print img_picto($langs->trans("StockTooLow"),"warning", 'style="vertical-align: bottom;"')." "; print 'situation_counter > 1) print ' readonly="readonly"'; + if ($this->situation_counter > 1) print ' readonly'; print '>'; } else { ?>   @@ -152,10 +152,10 @@ $coldisplay=-1; // We remove first td } ?> - + info_bits & 2) != 2) { print 'situation_counter > 1) print ' readonly="readonly"'; + if ($this->situation_counter > 1) print ' readonly'; print '>%'; } else { ?>   @@ -164,7 +164,7 @@ $coldisplay=-1; // We remove first td situation_cycle_ref) { $coldisplay++; - print '%'; + print '%'; } if (! empty($usemargins)) { diff --git a/htdocs/core/tpl/objectline_view.tpl.php b/htdocs/core/tpl/objectline_view.tpl.php index 593421fe527..0ca056cfbb4 100644 --- a/htdocs/core/tpl/objectline_view.tpl.php +++ b/htdocs/core/tpl/objectline_view.tpl.php @@ -148,7 +148,7 @@ if (empty($usemargins)) $usemargins=0; global->PRODUCT_USE_UNITS) { - print ''; + print ''; $label = $line->getLabelOfUnit('short'); if ($label !== '') { print $langs->trans($label); @@ -169,7 +169,7 @@ if (empty($usemargins)) $usemargins=0; if ($this->situation_cycle_ref) { $coldisplay++; - print '' . $line->situation_percent . '%'; + print '' . $line->situation_percent . '%'; } if ($usemargins && ! empty($conf->margin->enabled) && empty($user->societe_id)) diff --git a/htdocs/expedition/index.php b/htdocs/expedition/index.php index 5ecd563a613..2d5d0e207c6 100644 --- a/htdocs/expedition/index.php +++ b/htdocs/expedition/index.php @@ -93,7 +93,7 @@ if ($resql) { $var=!$var; $obj = $db->fetch_object($resql); - print ""; + print "'; $shipment->id=$obj->rowid; $shipment->ref=$obj->ref; print $shipment->getNomUrl(1); diff --git a/htdocs/fourn/commande/orderstoinvoice.php b/htdocs/fourn/commande/orderstoinvoice.php index d35d318d4a2..a18031de4d4 100644 --- a/htdocs/fourn/commande/orderstoinvoice.php +++ b/htdocs/fourn/commande/orderstoinvoice.php @@ -386,10 +386,10 @@ if (($action != 'create' && $action != 'add') || ! empty($mesgs)) { diff --git a/htdocs/fourn/facture/list.php b/htdocs/fourn/facture/list.php index 0ebd93a9ad8..887fa7c6fab 100644 --- a/htdocs/fourn/facture/list.php +++ b/htdocs/fourn/facture/list.php @@ -7,7 +7,8 @@ * Copyright (C) 2013 Cédric Salvador * Copyright (C) 2015 Marcos García * Copyright (C) 2015 juanjo Menent - * + * Copyright (C) 2015 Abbes Bahfir + * 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 diff --git a/htdocs/includes/restler/ApcCache.php b/htdocs/includes/restler/ApcCache.php new file mode 100644 index 00000000000..55b22107726 --- /dev/null +++ b/htdocs/includes/restler/ApcCache.php @@ -0,0 +1,114 @@ + + * @copyright 2013 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class ApcCache implements iCache +{ + /** + * The namespace that all of the cached entries will be stored under. This allows multiple APIs to run concurrently. + * + * @var string + */ + static public $namespace = 'restler'; + + /** + * store data in the cache + * + * + * @param string $name + * @param mixed $data + * + * @return boolean true if successful + */ + public function set($name, $data) + { + function_exists('apc_store') || $this->apcNotAvailable(); + + try { + return apc_store(self::$namespace . "-" . $name, $data); + } catch + (\Exception $exception) { + return false; + } + } + + private function apcNotAvailable() + { + throw new \Exception('APC is not available for use as Restler Cache. Please make sure the module is installed. http://php.net/manual/en/apc.installation.php'); + } + + /** + * retrieve data from the cache + * + * + * @param string $name + * @param bool $ignoreErrors + * + * @throws \Exception + * @return mixed + */ + public function get($name, $ignoreErrors = false) + { + function_exists('apc_fetch') || $this->apcNotAvailable(); + + try { + return apc_fetch(self::$namespace . "-" . $name); + } catch (\Exception $exception) { + if (!$ignoreErrors) { + throw $exception; + } + return null; + } + } + + /** + * delete data from the cache + * + * + * @param string $name + * @param bool $ignoreErrors + * + * @throws \Exception + * @return boolean true if successful + */ + public function clear($name, $ignoreErrors = false) + { + function_exists('apc_delete') || $this->apcNotAvailable(); + + try { + apc_delete(self::$namespace . "-" . $name); + } catch (\Exception $exception) { + if (!$ignoreErrors) { + throw $exception; + } + } + } + + /** + * check if the given name is cached + * + * + * @param string $name + * + * @return boolean true if cached + */ + public function isCached($name) + { + function_exists('apc_exists') || $this->apcNotAvailable(); + return apc_exists(self::$namespace . "-" . $name); + } + +} \ No newline at end of file diff --git a/htdocs/includes/restler/AutoLoader.php b/htdocs/includes/restler/AutoLoader.php new file mode 100644 index 00000000000..9bb34bad9cf --- /dev/null +++ b/htdocs/includes/restler/AutoLoader.php @@ -0,0 +1,438 @@ + + * @copyright 2012 Luracast + * @version 3.0.0rc5 + */ +class AutoLoader +{ + protected static $instance, // the singleton instance reference + $perfectLoaders, // used to keep the ideal list of loaders + $rogueLoaders = array(), // other auto loaders now unregistered + $classMap = array(), // the class to include file mapping + $aliases = array( // aliases and prefixes instead of null list aliases + 'Luracast\\Restler' => null, + 'Luracast\\Restler\\Format' => null, + 'Luracast\\Restler\\Data' => null, + 'Luracast\\Restler\\Filter' => null, + ); + + /** + * Singleton instance facility. + * + * @static + * @return AutoLoader the current instance or new instance if none exists. + */ + public static function instance() + { + static::$instance = static::$instance ?: new static(); + return static::thereCanBeOnlyOne(); + } + + /** + * Helper function to add a path to the include path. + * AutoLoader uses the include path to discover classes. + * + * @static + * + * @param $path string absolute or relative path. + * + * @return bool false if the path cannot be resolved + * or the resolved absolute path. + */ + public static function addPath($path) { + if (false === $path = stream_resolve_include_path($path)) + return false; + else + set_include_path($path.PATH_SEPARATOR.get_include_path()); + return $path; + } + + /** + * Other autoLoaders interfere and cause duplicate class loading. + * AutoLoader is capable enough to handle all standards so no need + * for others stumbling about. + * + * @return callable the one true auto loader. + */ + public static function thereCanBeOnlyOne() { + if (static::$perfectLoaders === spl_autoload_functions()) + return static::$instance; + + if (false !== $loaders = spl_autoload_functions()) + if (0 < $count = count($loaders)) + for ($i = 0, static::$rogueLoaders += $loaders; + $i < $count && false != ($loader = $loaders[$i]); + $i++) + if ($loader !== static::$perfectLoaders[0]) + spl_autoload_unregister($loader); + + return static::$instance; + } + + /** + * Seen this before cache handler. + * Facilitates both lookup and persist operations as well as convenience, + * load complete map functionality. The key can only be given a non falsy + * value once, this will be truthy for life. + * + * @param $key mixed class name considered or a collection of + * classMap entries + * @param $value mixed optional not required when doing a query on + * key. Default is false we haven't seen this + * class. Most of the time it will be the filename + * for include and is set to true if we are unable + * to load this class iow true == it does not exist. + * value may also be a callable auto loader function. + * + * @return mixed The known value for the key or false if key has no value + */ + public static function seen($key, $value = false) + { + if (is_array($key)) { + static::$classMap = $key + static::$classMap; + return false; + } + + if (empty(static::$classMap[$key])) + static::$classMap[$key] = $value; + + if (is_string($alias = static::$classMap[$key])) + if (isset(static::$classMap[$alias])) + return static::$classMap[$alias]; + + return static::$classMap[$key]; + } + + /** + * Protected constructor to enforce singleton pattern. + * Populate a default include path. + * All possible includes cant possibly be catered for and if you + * require another path then simply add it calling set_include_path. + */ + protected function __construct() + { + static::$perfectLoaders = array($this); + + if (false === static::seen('__include_path')) { + + $paths = explode(PATH_SEPARATOR, get_include_path()); + $slash = DIRECTORY_SEPARATOR; + $dir = dirname(__DIR__); + $source_dir = dirname($dir); + $dir = dirname($source_dir); + + foreach ( + array( + array($source_dir), + array($dir, '..', '..', 'composer'), + array($dir, 'vendor', 'composer'), + array($dir, '..', '..', '..', 'php'), + array($dir, 'vendor', 'php')) + as $includePath) + if (false !== $path = stream_resolve_include_path( + implode($slash, $includePath) + )) + if ('composer' == end($includePath) && + false !== $classmapPath = stream_resolve_include_path( + "$path{$slash}autoload_classmap.php" + ) + ) { + static::seen(static::loadFile( + $classmapPath + )); + $paths = array_merge( + $paths, + array_values(static::loadFile( + "$path{$slash}autoload_namespaces.php" + )) + ); + } else + $paths[] = $path; + + $paths = array_filter(array_map( + function ($path) { + if (false == $realPath = @realpath($path)) + return null; + return $realPath . DIRECTORY_SEPARATOR; + }, + $paths + )); + natsort($paths); + static::seen( + '__include_path', + implode(PATH_SEPARATOR, array_unique($paths)) + ); + } + + set_include_path(static::seen('__include_path')); + } + + /** + * Attempt to include the path location. + * Called from a static context which will not expose the AutoLoader + * instance itself. + * + * @param $path string location of php file on the include path + * + * @return bool|mixed returns reference obtained from the include or false + */ + private static function loadFile($path) + { + return \Luracast_Restler_autoloaderInclude($path); + } + + /** + * Attempt to load class with namespace prefixes. + * + * @param $className string class name + * + * @return bool|mixed reference to discovered include or false + */ + private function loadPrefixes($className) + { + $currentClass = $className; + if (false !== $pos = strrpos($className, '\\')) + $className = substr($className, $pos); + else + $className = "\\$className"; + + for ( + $i = 0, + $file = false, + $count = count(static::$aliases), + $prefixes = array_keys(static::$aliases); + $i < $count + && false === $file + && false === $file = $this->discover( + $variant = $prefixes[$i++].$className, + $currentClass + ); + $file = $this->loadAliases($variant) + ); + + return $file; + } + + /** + * Attempt to load configured aliases based on namespace part of class name. + * + * @param $className string fully qualified class name. + * + * @return bool|mixed reference to discovered include or false + */ + private function loadAliases($className) + { + $file = false; + if (preg_match('/(.+)(\\\\\w+$)/U', $className, $parts)) + for ( + $i = 0, + $aliases = isset(static::$aliases[$parts[1]]) + ? static::$aliases[$parts[1]] : array(), + $count = count($aliases); + $i < $count && false === $file; + $file = $this->discover( + "{$aliases[$i++]}$parts[2]", + $className + ) + ) ; + + return $file; + } + + /** + * Load from rogueLoaders as last resort. + * It may happen that a custom auto loader may load classes in a unique way, + * these classes cannot be seen otherwise nor should we attempt to cover every + * possible deviation. If we still can't find a class, as a last resort, we will + * run through the list of rogue loaders and verify if we succeeded. + * + * @param $className string className that can't be found + * @param null $loader callable loader optional when the loader is known + * + * @return bool false unless className now exists + */ + private function loadLastResort($className, $loader = null) { + $loaders = array_unique(static::$rogueLoaders); + if (isset($loader)) { + if (false === array_search($loader, $loaders)) + static::$rogueLoaders[] = $loader; + return $this->loadThisLoader($className, $loader); + } + foreach ($loaders as $loader) + if (false !== $file = $this->loadThisLoader($className, $loader)) + return $file; + + return false; + } + + /** + * Helper for loadLastResort. + * Use loader with $className and see if className exists. + * + * @param $className string name of a class to load + * @param $loader callable autoLoader method + * + * @return bool false unless className exists + */ + private function loadThisLoader($className, $loader) { + if (is_callable($loader) + && false !== $file = $loader($className) + && $this->exists($className, $loader)) + return $file; + return false; + } + + /** + * Create an alias for class. + * + * @param $className string the name of the alias class + * @param $currentClass string the current class this alias references + */ + private function alias($className, $currentClass) + { + if ($className != $currentClass + && false !== strpos($className, $currentClass)) + if (!class_exists($currentClass, false) + && class_alias($className, $currentClass)) + static::seen($currentClass, $className); + } + + /** + * Discovery process. + * + * @param $className string class name to discover + * @param $currentClass string optional name of current class when + * looking up an alias + * + * @return bool|mixed resolved include reference or false + */ + private function discover($className, $currentClass = null) + { + $currentClass = $currentClass ?: $className; + + /** The short version we've done this before and found it in cache */ + if (false !== $file = static::seen($className)) { + if (!$this->exists($className)) + if (is_callable($file)) + $file = $this->loadLastResort($className, $file); + elseif($file = stream_resolve_include_path($file)) + $file = static::loadFile($file); + + $this->alias($className, $currentClass); + return $file; + } + + /** We did not find it in cache, lets look for it shall we */ + + /** replace \ with / and _ in CLASS NAME with / = PSR-0 in 3 lines */ + $file = preg_replace("/\\\|_(?=\w+$)/", DIRECTORY_SEPARATOR, $className); + if (false === $file = stream_resolve_include_path("$file.php")) + return false; + + /** have we loaded this file before could this be an alias */ + if (in_array($file, get_included_files())) { + if (false !== $sameFile = array_search($file, static::$classMap)) + if (!$this->exists($className, $file)) + if (false !== strpos($sameFile, $className)) + $this->alias($sameFile, $className); + + return $file; + } + + $state = array_merge(get_declared_classes(), get_declared_interfaces()); + + if (false !== $result = static::loadFile($file)) { + + if ($this->exists($className, $file)) + $this->alias($className, $currentClass); + elseif (false != $diff = array_diff( + array_merge(get_declared_classes(), get_declared_interfaces()), $state)) + foreach ($diff as $autoLoaded) + if ($this->exists($autoLoaded, $file)) + if (false !== strpos($autoLoaded, $className)) + $this->alias($autoLoaded, $className); + + if (!$this->exists($currentClass)) + $result = false; + } + + return $result; + } + + /** + * Checks whether supplied string exists in a loaded class or interface. + * As a convenience the supplied $mapping can be the value for seen. + * + * @param $className string The class or interface to verify + * @param $mapping string (optional) value for map/seen if found to exist + * + * @return bool whether the class/interface exists without calling auto loader + */ + private function exists($className, $mapping = null) + { + if (class_exists($className, false) + || interface_exists($className, false)) + if (isset($mapping)) + return static::seen($className, $mapping); + else + return true; + return false; + } + + /** + * Auto loader callback through __invoke object as function. + * + * @param $className string class/interface name to auto load + * + * @return mixed|null the reference from the include or null + */ + public function __invoke($className) + { + if (empty($className)) + return false; + + if (false !== $includeReference = $this->discover($className)) + return $includeReference; + + static::thereCanBeOnlyOne(); + + if (false !== $includeReference = $this->loadAliases($className)) + return $includeReference; + + if (false !== $includeReference = $this->loadPrefixes($className)) + return $includeReference; + + if (false !== $includeReference = $this->loadLastResort($className)) + return $includeReference; + + static::seen($className, true); + return null; + } +} +} + +namespace { + /** + * Include function in the root namespace to include files optimized + * for the global context. + * + * @param $path string path of php file to include into the global context. + * + * @return mixed|bool false if the file could not be included. + */ + function Luracast_Restler_autoloaderInclude($path) { + return include $path; + } +} + diff --git a/htdocs/includes/restler/CommentParser.php b/htdocs/includes/restler/CommentParser.php new file mode 100644 index 00000000000..839983b73db --- /dev/null +++ b/htdocs/includes/restler/CommentParser.php @@ -0,0 +1,466 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class CommentParser +{ + /** + * name for the embedded data + * + * @var string + */ + public static $embeddedDataName = 'properties'; + /** + * Regular Expression pattern for finding the embedded data and extract + * the inner information. It is used with preg_match. + * + * @var string + */ + public static $embeddedDataPattern + = '/```(\w*)[\s]*(([^`]*`{0,2}[^`]+)*)```/ms'; + /** + * Pattern will have groups for the inner details of embedded data + * this index is used to locate the data portion. + * + * @var int + */ + public static $embeddedDataIndex = 2; + /** + * Delimiter used to split the array data. + * + * When the name portion is of the embedded data is blank auto detection + * will be used and if URLEncodedFormat is detected as the data format + * the character specified will be used as the delimiter to find split + * array data. + * + * @var string + */ + public static $arrayDelimiter = ','; + + /** + * character sequence used to escape \@ + */ + const escapedAtChar = '\\@'; + + /** + * character sequence used to escape end of comment + */ + const escapedCommendEnd = '{@*}'; + + /** + * Instance of Restler class injected at runtime. + * + * @var Restler + */ + public $restler; + /** + * Comment information is parsed and stored in to this array. + * + * @var array + */ + private $_data = array(); + + /** + * Parse the comment and extract the data. + * + * @static + * + * @param $comment + * @param bool $isPhpDoc + * + * @return array associative array with the extracted values + */ + public static function parse($comment, $isPhpDoc = true) + { + $p = new self(); + if (empty($comment)) { + return $p->_data; + } + + if ($isPhpDoc) { + $comment = self::removeCommentTags($comment); + } + + $p->extractData($comment); + return $p->_data; + + } + + /** + * Removes the comment tags from each line of the comment. + * + * @static + * + * @param string $comment PhpDoc style comment + * + * @return string comments with out the tags + */ + public static function removeCommentTags($comment) + { + $pattern = '/(^\/\*\*)|(^\s*\**[ \/]?)|\s(?=@)|\s\*\//m'; + return preg_replace($pattern, '', $comment); + } + + /** + * Extracts description and long description, uses other methods to get + * parameters. + * + * @param $comment + * + * @return array + */ + private function extractData($comment) + { + //to use @ as part of comment we need to + $comment = str_replace( + array(self::escapedCommendEnd, self::escapedAtChar), + array('*/', '@'), + $comment); + + $description = array(); + $longDescription = array(); + $params = array(); + + $mode = 0; // extract short description; + $comments = preg_split("/(\r?\n)/", $comment); + // remove first blank line; + array_shift($comments); + $addNewline = false; + foreach ($comments as $line) { + $line = trim($line); + $newParam = false; + if (empty ($line)) { + if ($mode == 0) { + $mode++; + } else { + $addNewline = true; + } + continue; + } elseif ($line{0} == '@') { + $mode = 2; + $newParam = true; + } + switch ($mode) { + case 0 : + $description[] = $line; + if (count($description) > 3) { + // if more than 3 lines take only first line + $longDescription = $description; + $description[] = array_shift($longDescription); + $mode = 1; + } elseif (substr($line, -1) == '.') { + $mode = 1; + } + break; + case 1 : + if ($addNewline) { + $line = ' ' . $line; + } + $longDescription[] = $line; + break; + case 2 : + $newParam + ? $params[] = $line + : $params[count($params) - 1] .= ' ' . $line; + } + $addNewline = false; + } + $description = implode(' ', $description); + $longDescription = implode(' ', $longDescription); + $description = preg_replace('/\s+/msu', ' ', $description); + $longDescription = preg_replace('/\s+/msu', ' ', $longDescription); + list($description, $d1) + = $this->parseEmbeddedData($description); + list($longDescription, $d2) + = $this->parseEmbeddedData($longDescription); + $this->_data = compact('description', 'longDescription'); + $d2 += $d1; + if (!empty($d2)) { + $this->_data[self::$embeddedDataName] = $d2; + } + foreach ($params as $key => $line) { + list(, $param, $value) = preg_split('/\@|\s/', $line, 3) + + array('', '', ''); + list($value, $embedded) = $this->parseEmbeddedData($value); + $value = array_filter(preg_split('/\s+/msu', $value)); + $this->parseParam($param, $value, $embedded); + } + return $this->_data; + } + + /** + * Parse parameters that begin with (at) + * + * @param $param + * @param array $value + * @param array $embedded + */ + private function parseParam($param, array $value, array $embedded) + { + $data = & $this->_data; + $allowMultiple = false; + switch ($param) { + case 'param' : + $value = $this->formatParam($value); + $allowMultiple = true; + break; + case 'var' : + $value = $this->formatVar($value); + break; + case 'return' : + $value = $this->formatReturn($value); + break; + case 'class' : + $data = & $data[$param]; + list ($param, $value) = $this->formatClass($value); + break; + case 'access' : + $value = reset($value); + break; + case 'expires' : + case 'status' : + $value = intval(reset($value)); + break; + case 'throws' : + $value = $this->formatThrows($value); + $allowMultiple = true; + break; + case 'author': + $value = $this->formatAuthor($value); + $allowMultiple = true; + break; + case 'header' : + case 'link': + case 'example': + case 'todo': + $allowMultiple = true; + //don't break, continue with code for default: + default : + $value = implode(' ', $value); + } + if (!empty($embedded)) { + if (is_string($value)) { + $value = array('description' => $value); + } + $value[self::$embeddedDataName] = $embedded; + } + if (empty ($data[$param])) { + if ($allowMultiple) { + $data[$param] = array( + $value + ); + } else { + $data[$param] = $value; + } + } elseif ($allowMultiple) { + $data[$param][] = $value; + } elseif ($param == 'param') { + $arr = array( + $data[$param], + $value + ); + $data[$param] = $arr; + } else { + if (!is_string($value) && isset($value[self::$embeddedDataName]) + && isset($data[$param][self::$embeddedDataName]) + ) { + $value[self::$embeddedDataName] + += $data[$param][self::$embeddedDataName]; + } + $data[$param] = $value + $data[$param]; + } + } + + /** + * Parses the inline php doc comments and embedded data. + * + * @param $subject + * + * @return array + * @throws Exception + */ + private function parseEmbeddedData($subject) + { + $data = array(); + + //parse {@pattern } tags specially + while (preg_match('|(?s-m)({@pattern (/.+/[imsxuADSUXJ]*)})|', $subject, $matches)) { + $subject = str_replace($matches[0], '', $subject); + $data['pattern'] = $matches[2]; + } + while (preg_match('/{@(\w+)\s?([^}]*)}/ms', $subject, $matches)) { + $subject = str_replace($matches[0], '', $subject); + if ($matches[2] == 'true' || $matches[2] == 'false') { + $matches[2] = $matches[2] == 'true'; + } elseif ($matches[2] == '') { + $matches[2] = true; + } + if ($matches[1] == 'pattern') { + throw new Exception('Inline pattern tag should follow {@pattern /REGEX_PATTERN_HERE/} format and can optionally include PCRE modifiers following the ending `/`'); + } elseif (false !== strpos($matches[2], static::$arrayDelimiter)) { + $matches[2] = explode(static::$arrayDelimiter, $matches[2]); + } + $data[$matches[1]] = $matches[2]; + } + + while (preg_match(self::$embeddedDataPattern, $subject, $matches)) { + $subject = str_replace($matches[0], '', $subject); + $str = $matches[self::$embeddedDataIndex]; + if (isset ($this->restler) + && self::$embeddedDataIndex > 1 + && !empty ($matches[1]) + ) { + $extension = $matches[1]; + $formatMap = $this->restler->getFormatMap(); + if (isset ($formatMap[$extension])) { + /** + * @var \Luracast\Restler\Format\iFormat + */ + $format = $formatMap[$extension]; + $format = new $format(); + $data = $format->decode($str); + } + } else { // auto detect + if ($str{0} == '{') { + $d = json_decode($str, true); + if (json_last_error() != JSON_ERROR_NONE) { + throw new Exception('Error parsing embedded JSON data' + . " $str"); + } + $data = $d + $data; + } else { + parse_str($str, $d); + //clean up + $d = array_filter($d); + foreach ($d as $key => $val) { + $kt = trim($key); + if ($kt != $key) { + unset($d[$key]); + $key = $kt; + $d[$key] = $val; + } + if (is_string($val)) { + if ($val == 'true' || $val == 'false') { + $d[$key] = $val == 'true' ? true : false; + } else { + $val = explode(self::$arrayDelimiter, $val); + if (count($val) > 1) { + $d[$key] = $val; + } else { + $d[$key] = + preg_replace('/\s+/msu', ' ', + $d[$key]); + } + } + } + } + $data = $d + $data; + } + } + } + return array($subject, $data); + } + + private function formatThrows(array $value) + { + $r = array(); + $r['code'] = count($value) && is_numeric($value[0]) + ? intval(array_shift($value)) : 500; + $reason = implode(' ', $value); + $r['reason'] = empty($reason) ? '' : $reason; + return $r; + } + + private function formatClass(array $value) + { + $param = array_shift($value); + + if (empty($param)) { + $param = 'Unknown'; + } + $value = implode(' ', $value); + return array( + ltrim($param, '\\'), + array('description' => $value) + ); + } + + private function formatAuthor(array $value) + { + $r = array(); + $email = end($value); + if ($email{0} == '<') { + $email = substr($email, 1, -1); + array_pop($value); + $r['email'] = $email; + } + $r['name'] = implode(' ', $value); + return $r; + } + + private function formatReturn(array $value) + { + $data = explode('|', array_shift($value)); + $r = array( + 'type' => count($data) == 1 ? $data[0] : $data + ); + $r['description'] = implode(' ', $value); + return $r; + } + + private function formatParam(array $value) + { + $r = array(); + $data = array_shift($value); + if (empty($data)) { + $r['type'] = 'mixed'; + } elseif ($data{0} == '$') { + $r['name'] = substr($data, 1); + $r['type'] = 'mixed'; + } else { + $data = explode('|', $data); + $r['type'] = count($data) == 1 ? $data[0] : $data; + + $data = array_shift($value); + if (!empty($data) && $data{0} == '$') { + $r['name'] = substr($data, 1); + } + } + if ($value) { + $r['description'] = implode(' ', $value); + } + return $r; + } + + private function formatVar(array $value) + { + $r = array(); + $data = array_shift($value); + if (empty($data)) { + $r['type'] = 'mixed'; + } elseif ($data{0} == '$') { + $r['name'] = substr($data, 1); + $r['type'] = 'mixed'; + } else { + $data = explode('|', $data); + $r['type'] = count($data) == 1 ? $data[0] : $data; + } + if ($value) { + $r['description'] = implode(' ', $value); + } + return $r; + } +} diff --git a/htdocs/includes/restler/Compose.php b/htdocs/includes/restler/Compose.php new file mode 100644 index 00000000000..c4a6f03409c --- /dev/null +++ b/htdocs/includes/restler/Compose.php @@ -0,0 +1,71 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + */ +class Compose implements iCompose +{ + /** + * @var bool When restler is not running in production mode, this value will + * be checked to include the debug information on error response + */ + public static $includeDebugInfo = true; + /** + * Current Restler instance + * Injected at runtime + * + * @var Restler + */ + public $restler; + + /** + * Result of an api call is passed to this method + * to create a standard structure for the data + * + * @param mixed $result can be a primitive or array or object + * + * @return mixed + */ + public function response($result) + { + //TODO: check Defaults::language and change result accordingly + return $result; + } + + /** + * When the api call results in RestException this method + * will be called to return the error message + * + * @param RestException $exception exception that has reasons for failure + * + * @return array + */ + public function message(RestException $exception) + { + //TODO: check Defaults::language and change result accordingly + $r = array( + 'error' => array( + 'code' => $exception->getCode(), + 'message' => $exception->getErrorMessage(), + ) + $exception->getDetails() + ); + if (!Scope::get('Restler')->getProductionMode() && self::$includeDebugInfo) { + $r += array( + 'debug' => array( + 'source' => $exception->getSource(), + 'stages' => $exception->getStages(), + ) + ); + } + return $r; + } +} \ No newline at end of file diff --git a/htdocs/includes/restler/Data/ApiMethodInfo.php b/htdocs/includes/restler/Data/ApiMethodInfo.php new file mode 100644 index 00000000000..c97c27098a9 --- /dev/null +++ b/htdocs/includes/restler/Data/ApiMethodInfo.php @@ -0,0 +1,55 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class ApiMethodInfo extends ValueObject +{ + /** + * @var string target url + */ + public $url; + /** + * @var string + */ + public $className; + /** + * @var string + */ + public $methodName; + /** + * @var array parameters to be passed to the api method + */ + public $parameters = array(); + /** + * @var array information on parameters in the form of array(name => index) + */ + public $arguments = array(); + /** + * @var array default values for parameters if any + * in the form of array(index => value) + */ + public $defaults = array(); + /** + * @var array key => value pair of method meta information + */ + public $metadata = array(); + /** + * @var int access level + * 0 - @public - available for all + * 1 - @hybrid - both public and protected (enhanced info for authorized) + * 2 - @protected comment - only for authenticated users + * 3 - protected method - only for authenticated users + */ + public $accessLevel = 0; +} \ No newline at end of file diff --git a/htdocs/includes/restler/Data/Arr.php b/htdocs/includes/restler/Data/Arr.php new file mode 100644 index 00000000000..727f14d8080 --- /dev/null +++ b/htdocs/includes/restler/Data/Arr.php @@ -0,0 +1,33 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + */ +class Arr +{ + /** + * Deep copy given array + * + * @param array $arr + * + * @return array + */ + public static function copy(array $arr) + { + $copy = array(); + foreach ($arr as $key => $value) { + if (is_array($value)) $copy[$key] = static::copy($value); + else if (is_object($value)) $copy[$key] = clone $value; + else $copy[$key] = $value; + } + return $copy; + } +} \ No newline at end of file diff --git a/htdocs/includes/restler/Data/Invalid.php b/htdocs/includes/restler/Data/Invalid.php new file mode 100644 index 00000000000..3eaec18017d --- /dev/null +++ b/htdocs/includes/restler/Data/Invalid.php @@ -0,0 +1,20 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class Invalid extends Exception +{ + +} \ No newline at end of file diff --git a/htdocs/includes/restler/Data/Object.php b/htdocs/includes/restler/Data/Object.php new file mode 100644 index 00000000000..b34f82f0b79 --- /dev/null +++ b/htdocs/includes/restler/Data/Object.php @@ -0,0 +1,157 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class Object +{ + /** + * @var bool|string|callable + */ + public static $stringEncoderFunction = false; + /** + * @var bool|string|callable + */ + public static $numberEncoderFunction = false; + /** + * @var array key value pairs for fixing value types using functions. + * For example + * + * 'id'=>'intval' will make sure all values of the id properties + * will be converted to integers intval function + * 'password'=> null will remove all the password entries + */ + public static $fix = array(); + /** + * @var string character that is used to identify sub objects + * + * For example + * + * when Object::$separatorChar = '.'; + * + * array('my.object'=>true) will result in + * + * array( + * 'my'=>array('object'=>true) + * ); + */ + public static $separatorChar = null; + /** + * @var bool set it to true when empty arrays, blank strings, null values + * to be automatically removed from response + */ + public static $removeEmpty = false; + /** + * @var bool set it to true to remove all null values from the result + */ + public static $removeNull = false; + + /** + * Convenience function that converts the given object + * in to associative array + * + * @static + * + * @param mixed $object that needs to be converted + * + * @param bool $forceObjectTypeWhenEmpty when set to true outputs + * actual type (array or + * object) rather than + * always an array when the + * array/object is empty + * + * @return array + */ + public static function toArray($object, + $forceObjectTypeWhenEmpty = false) + { + //if ($object instanceof JsonSerializable) { //wont work on PHP < 5.4 + if (is_object($object)) { + if (method_exists($object, 'jsonSerialize')) { + $object = $object->jsonSerialize(); + } elseif (method_exists($object, '__sleep')) { + $properties = $object->__sleep(); + $array = array(); + foreach ($properties as $key) { + $value = self::toArray($object->{$key}, + $forceObjectTypeWhenEmpty); + if (self::$stringEncoderFunction && is_string($value)) { + $value = self::$stringEncoderFunction ($value); + } elseif (self::$numberEncoderFunction && is_numeric($value)) { + $value = self::$numberEncoderFunction ($value); + } + $array [$key] = $value; + } + return $array; + } + } + if (is_array($object) || is_object($object)) { + $count = 0; + $array = array(); + foreach ($object as $key => $value) { + if ( + is_string(self::$separatorChar) && + false !== strpos($key, self::$separatorChar) + ) { + list($key, $obj) = explode(self::$separatorChar, $key, 2); + $object[$key][$obj] = $value; + $value = $object[$key]; + } + if (self::$removeEmpty && empty($value) && !is_numeric($value) && !is_bool($value)) { + continue; + } elseif (self::$removeNull && is_null($value)) { + continue; + } + if (array_key_exists($key, self::$fix)) { + if (isset(self::$fix[$key])) { + $value = call_user_func(self::$fix[$key], $value); + } else { + continue; + } + } + $value = self::toArray($value, $forceObjectTypeWhenEmpty); + if (self::$stringEncoderFunction && is_string($value)) { + $value = self::$encoderFunctionName ($value); + } elseif (self::$numberEncoderFunction && is_numeric($value)) { + $value = self::$numberEncoderFunction ($value); + } + $array [$key] = $value; + $count++; + } + return $forceObjectTypeWhenEmpty && $count == 0 ? $object : $array; + } + + return $object; + } + + public function __get($name) + { + isset(self::$fix[$name]) ? self::$fix[$name] : null; + } + + public function __set($name, $function) + { + self::$fix[$name] = $function; + } + + public function __isset($name) + { + return isset(self::$fix[$name]); + } + + public function __unset($name) + { + unset(self::$fix[$name]); + } +} + diff --git a/htdocs/includes/restler/Data/String.php b/htdocs/includes/restler/Data/String.php new file mode 100644 index 00000000000..268ca40c115 --- /dev/null +++ b/htdocs/includes/restler/Data/String.php @@ -0,0 +1,84 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + */ +class String +{ + /** + * Given haystack contains the needle or not? + * + * @param string $haystack + * @param string $needle + * @param bool $caseSensitive + * + * @return bool + */ + public static function contains($haystack, $needle, $caseSensitive = true) + { + if (empty($needle)) + return true; + return $caseSensitive + ? strpos($haystack, $needle) !== false + : stripos($haystack, $needle) !== false; + } + + /** + * Given haystack begins with the needle or not? + * + * @param string $haystack + * @param string $needle + * + * @return bool + */ + public static function beginsWith($haystack, $needle) + { + $length = strlen($needle); + return (substr($haystack, 0, $length) === $needle); + } + + /** + * Given haystack ends with the needle or not? + * + * @param string $haystack + * @param string $needle + * + * @return bool + */ + public static function endsWith($haystack, $needle) + { + $length = strlen($needle); + if ($length == 0) { + return true; + } + return (substr($haystack, -$length) === $needle); + } + + + /** + * Convert camelCased or underscored string in to a title + * + * @param string $name + * + * @return string + */ + public static function title($name) + { + return + ucwords( + preg_replace( + array('/(?<=[^A-Z])([A-Z])/', '/(?<=[^0-9])([0-9])/', '/(_)/'), + array(' $0', ' $0', ' '), + $name + ) + ); + } +} \ No newline at end of file diff --git a/htdocs/includes/restler/Data/ValidationInfo.php b/htdocs/includes/restler/Data/ValidationInfo.php new file mode 100644 index 00000000000..aacc3398c9a --- /dev/null +++ b/htdocs/includes/restler/Data/ValidationInfo.php @@ -0,0 +1,273 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class ValidationInfo implements iValueObject +{ + /** + * @var mixed given value for the parameter + */ + public $value; + /** + * @var string proper name for given parameter + */ + public $label; + /** + * @var string html element that can be used to represent the parameter for + * input + */ + public $field; + /** + * @var mixed default value for the parameter + */ + public $default; + /** + * Name of the variable being validated + * + * @var string variable name + */ + public $name; + + /** + * @var bool is it required or not + */ + public $required; + + /** + * @var string body or header or query where this parameter is coming from + * in the http request + */ + public $from; + + /** + * Data type of the variable being validated. + * It will be mostly string + * + * @var string|array multiple types are specified it will be of + * type array otherwise it will be a string + */ + public $type; + + /** + * When the type is array, this field is used to define the type of the + * contents of the array + * + * @var string|null when all the items in an array are of certain type, we + * can set this property. It will be null if the items can be of any type + */ + public $contentType; + + /** + * Should we attempt to fix the value? + * When set to false validation class should throw + * an exception or return false for the validate call. + * When set to true it will attempt to fix the value if possible + * or throw an exception or return false when it cant be fixed. + * + * @var boolean true or false + */ + public $fix = false; + + /** + * @var array of children to be validated + */ + public $children = null; + + // ================================================================== + // + // VALUE RANGE + // + // ------------------------------------------------------------------ + /** + * Given value should match one of the values in the array + * + * @var array of choices to match to + */ + public $choice; + /** + * If the type is string it will set the lower limit for length + * else will specify the lower limit for the value + * + * @var number minimum value + */ + public $min; + /** + * If the type is string it will set the upper limit limit for length + * else will specify the upper limit for the value + * + * @var number maximum value + */ + public $max; + + // ================================================================== + // + // REGEX VALIDATION + // + // ------------------------------------------------------------------ + /** + * RegEx pattern to match the value + * + * @var string regular expression + */ + public $pattern; + + // ================================================================== + // + // CUSTOM VALIDATION + // + // ------------------------------------------------------------------ + /** + * Rules specified for the parameter in the php doc comment. + * It is passed to the validation method as the second parameter + * + * @var array custom rule set + */ + public $rules; + + /** + * Specifying a custom error message will override the standard error + * message return by the validator class + * + * @var string custom error response + */ + public $message; + + // ================================================================== + // + // METHODS + // + // ------------------------------------------------------------------ + + /** + * Name of the method to be used for validation. + * It will be receiving two parameters $input, $rules (array) + * + * @var string validation method name + */ + public $method; + + /** + * Instance of the API class currently being called. It will be null most of + * the time. Only when method is defined it will contain an instance. + * This behavior is for lazy loading of the API class + * + * @var null|object will be null or api class instance + */ + public $apiClassInstance = null; + + public static function numericValue($value) + { + return ( int )$value == $value + ? ( int )$value + : floatval($value); + } + + public static function arrayValue($value) + { + return is_array($value) ? $value : array( + $value + ); + } + + public static function stringValue($value, $glue = ',') + { + return is_array($value) + ? implode($glue, $value) + : ( string )$value; + } + + public static function booleanValue($value) + { + return is_bool($value) + ? $value + : $value !== 'false'; + } + + public static function filterArray(array $data, $keepNumericKeys) + { + $r = array(); + foreach ($data as $key => $value) { + if (is_numeric($key)) { + if ($keepNumericKeys) { + $r[$key] = $value; + } + } elseif (!$keepNumericKeys) { + $r[$key] = $value; + } + } + return $r; + } + + public function __toString() + { + return ' new ValidationInfo() '; + } + + private function getProperty(array &$from, $property) + { + $p = Util::nestedValue($from, $property); + unset($from[$property]); + $p2 = Util::nestedValue( + $from, CommentParser::$embeddedDataName, $property + ); + unset($from[CommentParser::$embeddedDataName][$property]); + + if ($property == 'type' && $p == 'array' && $p2) { + $this->contentType = $p2; + return $p; + } + $r = is_null($p2) ? (is_null($p) ? null : $p) : $p2; + if (!is_null($r)) { + if ($property == 'min' || $property == 'max') { + return static::numericValue($r); + } elseif ($property == 'required' || $property == 'fix') { + return static::booleanValue($r); + } elseif ($property == 'choice') { + return static::arrayValue($r); + } elseif ($property == 'pattern') { + return static::stringValue($r); + } + } + return $r; + } + + public function __construct(array $info) + { + $properties = get_object_vars($this); + unset($properties['contentType']); + foreach ($properties as $property => $value) { + $this->{$property} = $this->getProperty($info, $property); + } + $inner = Util::nestedValue($info, 'properties'); + $this->rules = !empty($inner) ? $inner + $info : $info; + unset($this->rules['properties']); + if (is_string($this->type) && $this->type == 'integer') { + $this->type = 'int'; + } + } + + /** + * Magic Method used for creating instance at run time + */ + public static function __set_state(array $info) + { + $o = new self ($info); + return $o; + } +} + diff --git a/htdocs/includes/restler/Data/Validator.php b/htdocs/includes/restler/Data/Validator.php new file mode 100644 index 00000000000..5e1940ba1b6 --- /dev/null +++ b/htdocs/includes/restler/Data/Validator.php @@ -0,0 +1,626 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class Validator implements iValidate +{ + public static $holdException = false; + public static $exceptions = array(); + + /** + * Validate alphabetic characters. + * + * Check that given value contains only alphabetic characters. + * + * @param $input + * @param ValidationInfo $info + * + * @return string + * + * @throws Invalid + */ + public static function alpha($input, ValidationInfo $info = null) + { + if (ctype_alpha($input)) { + return $input; + } + if ($info && $info->fix) { + //remove non alpha characters + return preg_replace("/[^a-z]/i", "", $input); + } + throw new Invalid('Expecting only alphabetic characters.'); + } + + /** + * Validate alpha numeric characters. + * + * Check that given value contains only alpha numeric characters. + * + * @param $input + * @param ValidationInfo $info + * + * @return string + * + * @throws Invalid + */ + public static function alphanumeric($input, ValidationInfo $info = null) + { + if (ctype_alnum($input)) { + return $input; + } + if ($info && $info->fix) { + //remove non alpha numeric and space characters + return preg_replace("/[^a-z0-9 ]/i", "", $input); + } + throw new Invalid('Expecting only alpha numeric characters.'); + } + + /** + * Validate printable characters. + * + * Check that given value contains only printable characters. + * + * @param $input + * @param ValidationInfo $info + * + * @return string + * + * @throws Invalid + */ + public static function printable($input, ValidationInfo $info = null) + { + if (ctype_print($input)) { + return $input; + } + if ($info && $info->fix) { + //remove non printable characters + return preg_replace('/[\x00-\x1F\x80-\xFF]/', '', $input); + } + throw new Invalid('Expecting only printable characters.'); + } + + /** + * Validate hexadecimal digits. + * + * Check that given value contains only hexadecimal digits. + * + * @param $input + * @param ValidationInfo $info + * + * @return string + * + * @throws Invalid + */ + public static function hex($input, ValidationInfo $info = null) + { + if (ctype_xdigit($input)) { + return $input; + } + throw new Invalid('Expecting only hexadecimal digits.'); + } + + /** + * Validate Telephone number + * + * Check if the given value is numeric with or without a `+` prefix + * + * @param $input + * @param ValidationInfo $info + * + * @return string + * + * @throws Invalid + */ + public static function tel($input, ValidationInfo $info = null) + { + if (is_numeric($input) && '-' != substr($input, 0, 1)) { + return $input; + } + throw new Invalid('Expecting phone number, a numeric value ' . + 'with optional `+` prefix'); + } + + /** + * Validate Email + * + * Check if the given string is a valid email + * + * @param String $input + * @param ValidationInfo $info + * + * @return string + * @throws Invalid + */ + public static function email($input, ValidationInfo $info = null) + { + $r = filter_var($input, FILTER_VALIDATE_EMAIL); + if ($r) { + return $r; + } elseif ($info && $info->fix) { + $r = filter_var($input, FILTER_SANITIZE_EMAIL); + return static::email($r); + } + throw new Invalid('Expecting email in `name@example.com` format'); + } + + /** + * Validate IP Address + * + * Check if the given string is a valid ip address + * + * @param String $input + * @param ValidationInfo $info + * + * @return string + * @throws Invalid + */ + public static function ip($input, ValidationInfo $info = null) + { + $r = filter_var($input, FILTER_VALIDATE_IP); + if ($r) + return $r; + + throw new Invalid('Expecting IP address in IPV6 or IPV4 format'); + } + + /** + * Validate Url + * + * Check if the given string is a valid url + * + * @param String $input + * @param ValidationInfo $info + * + * @return string + * @throws Invalid + */ + public static function url($input, ValidationInfo $info = null) + { + $r = filter_var($input, FILTER_VALIDATE_URL); + if ($r) { + return $r; + } elseif ($info && $info->fix) { + $r = filter_var($input, FILTER_SANITIZE_URL); + return static::url($r); + } + throw new Invalid('Expecting url in `http://example.com` format'); + } + + /** + * MySQL Date + * + * Check if the given string is a valid date in YYYY-MM-DD format + * + * @param String $input + * @param ValidationInfo $info + * + * @return string + * @throws Invalid + */ + public static function date($input, ValidationInfo $info = null) + { + if ( + preg_match( + '#^(?P\d{2}|\d{4})-(?P\d{1,2})-(?P\d{1,2})$#', + $input, + $date + ) + && checkdate($date['month'], $date['day'], $date['year']) + ) { + return $input; + } + throw new Invalid( + 'Expecting date in `YYYY-MM-DD` format, such as `' + . date("Y-m-d") . '`' + ); + } + + /** + * MySQL DateTime + * + * Check if the given string is a valid date and time in YYY-MM-DD HH:MM:SS format + * + * @param String $input + * @param ValidationInfo $info + * + * @return string + * @throws Invalid + */ + public static function datetime($input, ValidationInfo $info = null) + { + if ( + preg_match('/^(?P19\d\d|20\d\d)\-(?P0[1-9]|1[0-2])\-' . + '(?P0\d|[1-2]\d|3[0-1]) (?P0\d|1\d|2[0-3]' . + ')\:(?P[0-5][0-9])\:(?P[0-5][0-9])$/', + $input, $date) + && checkdate($date['month'], $date['day'], $date['year']) + ) { + return $input; + } + throw new Invalid( + 'Expecting date and time in `YYYY-MM-DD HH:MM:SS` format, such as `' + . date("Y-m-d H:i:s") . '`' + ); + } + + /** + * Alias for Time + * + * Check if the given string is a valid time in HH:MM:SS format + * + * @param String $input + * @param ValidationInfo $info + * + * @return string + * @throws Invalid + */ + public static function time24($input, ValidationInfo $info = null) + { + return static::time($input, $info); + } + + /** + * Time + * + * Check if the given string is a valid time in HH:MM:SS format + * + * @param String $input + * @param ValidationInfo $info + * + * @return string + * @throws Invalid + */ + public static function time($input, ValidationInfo $info = null) + { + if (preg_match('/^([01]?[0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]$/', $input)) { + return $input; + } + throw new Invalid( + 'Expecting time in `HH:MM:SS` format, such as `' + . date("H:i:s") . '`' + ); + } + + /** + * Time in 12 hour format + * + * Check if the given string is a valid time 12 hour format + * + * @param String $input + * @param ValidationInfo $info + * + * @return string + * @throws Invalid + */ + public static function time12($input, ValidationInfo $info = null) + { + if (preg_match( + '/^([1-9]|1[0-2]|0[1-9]){1}(:[0-5][0-9])?\s?([aApP][mM]{1})?$/', + $input) + ) { + return $input; + } + throw new Invalid( + 'Expecting time in 12 hour format, such as `08:00AM` and `10:05:11`' + ); + } + + /** + * Unix Timestamp + * + * Check if the given value is a valid timestamp + * + * @param String $input + * @param ValidationInfo $info + * + * @return int + * @throws Invalid + */ + public static function timestamp($input, ValidationInfo $info = null) + { + if ((string)(int)$input == $input + && ($input <= PHP_INT_MAX) + && ($input >= ~PHP_INT_MAX) + ) { + return (int)$input; + } + throw new Invalid('Expecting unix timestamp, such as ' . time()); + } + + /** + * Validate the given input + * + * Validates the input and attempts to fix it when fix is requested + * + * @param mixed $input + * @param ValidationInfo $info + * @param null $full + * + * @throws \Exception + * @return array|bool|float|int|mixed|null|number|string + */ + public static function validate($input, ValidationInfo $info, $full = null) + { + $html = Scope::get('Restler')->responseFormat instanceof HtmlFormat; + $name = $html ? "$info->label" : "`$info->name`"; + try { + if (is_null($input)) { + if ($info->required) { + throw new RestException (400, + "$name is required."); + } + return null; + } + $error = isset ($info->message) + ? $info->message + : "Invalid value specified for $name"; + + //if a validation method is specified + if (!empty($info->method)) { + $method = $info->method; + $info->method = ''; + $r = self::validate($input, $info); + return $info->apiClassInstance->{$method} ($r); + } + + // when type is an array check if it passes for any type + if (is_array($info->type)) { + //trace("types are ".print_r($info->type, true)); + $types = $info->type; + foreach ($types as $type) { + $info->type = $type; + try { + $r = self::validate($input, $info); + if ($r !== false) { + return $r; + } + } catch (RestException $e) { + // just continue + } + } + throw new RestException (400, $error); + } + + //patterns are supported only for non numeric types + if (isset ($info->pattern) + && $info->type != 'int' + && $info->type != 'float' + && $info->type != 'number' + ) { + if (!preg_match($info->pattern, $input)) { + throw new RestException (400, $error); + } + } + + if (isset ($info->choice)) { + if (is_array($input)) { + foreach ($input as $i) { + if (!in_array($i, $info->choice)) { + $error .= ". Expected one of (" . implode(',', $info->choice) . ")."; + throw new RestException (400, $error); + } + } + } elseif (!in_array($input, $info->choice)) { + $error .= ". Expected one of (" . implode(',', $info->choice) . ")."; + throw new RestException (400, $error); + } + } + + if (method_exists($class = get_called_class(), $info->type) && $info->type != 'validate') { + try { + return call_user_func("$class::$info->type", $input, $info); + } catch (Invalid $e) { + throw new RestException(400, $error . '. ' . $e->getMessage()); + } + } + + switch ($info->type) { + case 'int' : + case 'float' : + case 'number' : + if (!is_numeric($input)) { + $error .= '. Expecting ' + . ($info->type == 'int' ? 'integer' : 'numeric') + . ' value'; + break; + } + if ($info->type == 'int' && (int)$input != $input) { + if ($info->fix) { + $r = (int)$input; + } else { + $error .= '. Expecting integer value'; + break; + } + } else { + $r = $info->numericValue($input); + } + if (isset ($info->min) && $r < $info->min) { + if ($info->fix) { + $r = $info->min; + } else { + $error .= ". Minimum required value is $info->min."; + break; + } + } + if (isset ($info->max) && $r > $info->max) { + if ($info->fix) { + $r = $info->max; + } else { + $error .= ". Maximum allowed value is $info->max."; + break; + } + } + return $r; + + case 'string' : + if (!is_string($input)) { + $error .= '. Expecting alpha numeric value'; + break; + } + if ($info->required && $input === '') { + $error = "$name is required."; + break; + } + $r = strlen($input); + if (isset ($info->min) && $r < $info->min) { + if ($info->fix) { + $input = str_pad($input, $info->min, $input); + } else { + $char = $info->min > 1 ? 'characters' : 'character'; + $error .= ". Minimum $info->min $char required."; + break; + } + } + if (isset ($info->max) && $r > $info->max) { + if ($info->fix) { + $input = substr($input, 0, $info->max); + } else { + $char = $info->max > 1 ? 'characters' : 'character'; + $error .= ". Maximum $info->max $char allowed."; + break; + } + } + return $input; + + case 'bool': + case 'boolean': + if ($input === 'true' || $input === true) return true; + if (is_numeric($input)) return $input > 0; + return false; + + case 'array': + if ($info->fix && is_string($input)) { + $input = explode(CommentParser::$arrayDelimiter, $input); + } + if (is_array($input)) { + $contentType = + Util::nestedValue($info, 'contentType') ? : null; + if ($info->fix) { + if ($contentType == 'indexed') { + $input = $info->filterArray($input, true); + } elseif ($contentType == 'associative') { + $input = $info->filterArray($input, true); + } + } elseif ( + $contentType == 'indexed' && + array_values($input) != $input + ) { + $error .= '. Expecting a list of items but an item is given'; + break; + } elseif ( + $contentType == 'associative' && + array_values($input) == $input && + count($input) + ) { + $error .= '. Expecting an item but a list is given'; + break; + } + $r = count($input); + if (isset ($info->min) && $r < $info->min) { + $item = $info->max > 1 ? 'items' : 'item'; + $error .= ". Minimum $info->min $item required."; + break; + } + if (isset ($info->max) && $r > $info->max) { + if ($info->fix) { + $input = array_slice($input, 0, $info->max); + } else { + $item = $info->max > 1 ? 'items' : 'item'; + $error .= ". Maximum $info->max $item allowed."; + break; + } + } + if ( + isset($contentType) && + $contentType != 'associative' && + $contentType != 'indexed' + ) { + $name = $info->name; + $info->type = $contentType; + unset($info->contentType); + foreach ($input as $key => $chinput) { + $info->name = "{$name}[$key]"; + $input[$key] = static::validate($chinput, $info); + } + } + return $input; + } elseif (isset($contentType)) { + $error .= '. Expecting items of type ' . + ($html ? "$contentType" : "`$contentType`"); + break; + } + break; + case 'mixed': + case 'unknown_type': + case 'unknown': + case null: //treat as unknown + return $input; + default : + if (!is_array($input)) { + break; + } + //do type conversion + if (class_exists($info->type)) { + $input = $info->filterArray($input, false); + $implements = class_implements($info->type); + if ( + is_array($implements) && + in_array('Luracast\\Restler\\Data\\iValueObject', $implements) + ) { + return call_user_func( + "{$info->type}::__set_state", $input + ); + } + $class = $info->type; + $instance = new $class(); + if (is_array($info->children)) { + if ( + empty($input) || + !is_array($input) || + $input === array_values($input) + ) { + $error .= '. Expecting an item of type ' . + ($html ? "$info->type" : "`$info->type`"); + break; + } + foreach ($info->children as $key => $value) { + $cv = new ValidationInfo($value); + if (array_key_exists($key, $input) || $cv->required) { + $instance->{$key} = static::validate( + Util::nestedValue($input, $key), + $cv + ); + } + + } + } + return $instance; + } + } + throw new RestException (400, $error); + } catch (\Exception $e) { + static::$exceptions[] = $e; + if (static::$holdException) { + return null; + } + throw $e; + } + } +} \ No newline at end of file diff --git a/htdocs/includes/restler/Data/ValueObject.php b/htdocs/includes/restler/Data/ValueObject.php new file mode 100644 index 00000000000..811c170acbd --- /dev/null +++ b/htdocs/includes/restler/Data/ValueObject.php @@ -0,0 +1,61 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class ValueObject implements iValueObject +{ + + public function __toString() + { + return ' new ' . get_called_class() . '() '; + } + + public static function __set_state(array $properties) + { + $class = get_called_class(); + $instance = new $class (); + $vars = get_object_vars($instance); + foreach ($properties as $property => $value) { + if (property_exists($instance, $property)) { + // see if the property is accessible + if (array_key_exists($property, $vars)) { + $instance->{$property} = $value; + } else { + $method = 'set' . ucfirst($property); + if (method_exists($instance, $method)) { + call_user_func(array( + $instance, + $method + ), $value); + } + } + } + } + return $instance; + } + + public function __toArray() + { + $r = get_object_vars($this); + $methods = get_class_methods($this); + foreach ($methods as $m) { + if (substr($m, 0, 3) == 'get') { + $r [lcfirst(substr($m, 3))] = @$this->{$m} (); + } + } + return $r; + } + +} + diff --git a/htdocs/includes/restler/Data/iValidate.php b/htdocs/includes/restler/Data/iValidate.php new file mode 100644 index 00000000000..8ed28b27b1b --- /dev/null +++ b/htdocs/includes/restler/Data/iValidate.php @@ -0,0 +1,32 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +interface iValidate { + + /** + * method used for validation. + * + * @param mixed $input + * data that needs to be validated + * @param ValidationInfo $info + * information to be used for validation + * @return boolean false in case of failure or fixed value in the expected + * type + * @throws \Luracast\Restler\RestException 400 with information about the + * failed + * validation + */ + public static function validate($input, ValidationInfo $info); +} + diff --git a/htdocs/includes/restler/Data/iValueObject.php b/htdocs/includes/restler/Data/iValueObject.php new file mode 100644 index 00000000000..4597cdac825 --- /dev/null +++ b/htdocs/includes/restler/Data/iValueObject.php @@ -0,0 +1,39 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +interface iValueObject +{ + + /** + * This static method is called for creating an instance of the class by + * passing the initiation values as an array. + * + * @static + * @abstract + * + * @param array $properties + * + * @return iValueObject + */ + public static function __set_state(array $properties); + + /** + * This method provides a string representation for the instance + * + * @return string + */ + public function __toString(); +} + diff --git a/htdocs/includes/restler/Defaults.php b/htdocs/includes/restler/Defaults.php new file mode 100644 index 00000000000..98b97202495 --- /dev/null +++ b/htdocs/includes/restler/Defaults.php @@ -0,0 +1,360 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class Defaults +{ + // ================================================================== + // + // Class Mappings + // + // ------------------------------------------------------------------ + + /** + * @var string of name of the class that implements + * \Luracast\Restler\iCache the cache class to be used + */ + public static $cacheClass = 'Luracast\\Restler\\HumanReadableCache'; + + /** + * @var string full path of the directory where all the generated files will + * be kept. When set to null (default) it will use the cache folder that is + * in the same folder as index.php (gateway) + */ + public static $cacheDirectory; + + /** + * @var string of name of the class that implements + * \Luracast\Restler\Data\iValidate the validator class to be used + */ + public static $validatorClass = 'Luracast\\Restler\\Data\\Validator'; + + /** + * @var string name of the class that implements \Luracast\Restler\iCompose + * the class to be used to compose the response + */ + public static $composeClass = 'Luracast\\Restler\\Compose'; + + // ================================================================== + // + // Routing + // + // ------------------------------------------------------------------ + + /** + * @var bool should auto routing for public and protected api methods + * should be enabled by default or not. Set this to false to get + * Restler 1.0 style behavior + */ + public static $autoRoutingEnabled = true; + + /** + * @var boolean avoids creating multiple routes that can increase the + * ambiguity when set to true. when a method parameter is optional it is + * not mapped to the url and should only be used in request body or as + * query string `/resource?id=value`. When a parameter is required and is + * scalar, it will be mapped as part of the url `/resource/{id}` + */ + public static $smartAutoRouting = true; + + /** + * @var boolean enables more ways of finding the parameter data in the request. + * If you need backward compatibility with Restler 2 or below turn this off + */ + public static $smartParameterParsing = true; + + // ================================================================== + // + // API Version Management + // + // ------------------------------------------------------------------ + + /** + * @var null|string name that is used for vendor specific media type and + * api version using the Accept Header for example + * application/vnd.{vendor}-v1+json + * + * Keep this null if you do not want to use vendor MIME for specifying api version + */ + public static $apiVendor = null; + + /** + * @var bool set it to true to force vendor specific MIME for versioning. + * It will be automatically set to true when Defaults::$vendor is not + * null and client is requesting for the custom MIME type + */ + public static $useVendorMIMEVersioning = false; + + /** + * @var bool set it to true to use enableUrl based versioning + */ + public static $useUrlBasedVersioning = false; + + + // ================================================================== + // + // Request + // + // ------------------------------------------------------------------ + + /** + * @var string name to be used for the method parameter to capture the + * entire request data + */ + public static $fullRequestDataName = 'request_data'; + + /** + * @var string name of the property that can sent through $_GET or $_POST to + * override the http method of the request. Set it to null or + * blank string to disable http method override through request + * parameters. + */ + public static $httpMethodOverrideProperty = 'http_method'; + + /** + * @var bool should auto validating api parameters should be enabled by + * default or not. Set this to false to avoid validation. + */ + public static $autoValidationEnabled = true; + + /** + * @var string name of the class that implements iUser interface to identify + * the user for caching purposes + */ + public static $userIdentifierClass = 'Luracast\\Restler\\User'; + + // ================================================================== + // + // Response + // + // ------------------------------------------------------------------ + + /** + * @var bool HTTP status codes are set on all responses by default. + * Some clients (like flash, mobile) have trouble dealing with non-200 + * status codes on error responses. + * + * You can set it to true to force a HTTP 200 status code on all responses, + * even when errors occur. If you suppress status codes, look for an error + * response to determine if an error occurred. + */ + public static $suppressResponseCode = false; + + public static $supportedCharsets = array('utf-8', 'iso-8859-1'); + public static $supportedLanguages = array('en', 'en-US'); + + public static $charset = 'utf-8'; + public static $language = 'en'; + + /** + * @var bool when set to true, it will exclude the response body + */ + public static $emptyBodyForNullResponse = true; + + /** + * @var bool enables CORS support + */ + public static $crossOriginResourceSharing = false; + public static $accessControlAllowOrigin = '*'; + public static $accessControlAllowMethods = + 'GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD'; + + // ================================================================== + // + // Header + // + // ------------------------------------------------------------------ + + /** + * @var array default Cache-Control template that used to set the + * Cache-Control header and has two values, first one is used when + * Defaults::$headerExpires is 0 and second one when it has some time + * value specified. When only one value is specified it will be used for + * both cases + */ + public static $headerCacheControl = array( + 'no-cache, must-revalidate', + + /* "public, " or "private, " will be prepended based on api method + * called (public or protected) + */ + 'max-age={expires}, must-revalidate', + + ); + + + /** + * @var int sets the content to expire immediately when set to zero + * alternatively you can specify the number of seconds the content will + * expire. This setting can be altered at api level using php doc comment + * with @expires numOfSeconds + */ + public static $headerExpires = 0; + + // ================================================================== + // + // Access Control + // + // ------------------------------------------------------------------ + + /** + * @var null|callable if the api methods are under access control mechanism + * you can attach a function here that returns true or false to determine + * visibility of a protected api method. this function will receive method + * info as the only parameter. + */ + public static $accessControlFunction = null; + + /** + * @var int set the default api access mode + * value of 0 = public api + * value of 1 = hybrid api using `@access hybrid` comment + * value of 2 = protected api using `@access protected` comment + * value of 3 = protected api using `protected function` method + */ + public static $apiAccessLevel = 0; + + /** + * @var string authentication method to be called in iAuthenticate + * Interface + */ + public static $authenticationMethod = '__isAllowed'; + + /** + * @var int time in milliseconds for bandwidth throttling, + * which is the minimum response time for each api request. You can + * change it per api method by setting `@throttle 3000` in php doc + * comment either at the method level or class level + */ + public static $throttle = 0; + + // ================================================================== + // + // Overrides for API User + // + // ------------------------------------------------------------------ + + /** + * @var array use 'alternativeName'=> 'actualName' to set alternative + * names that can be used to represent the api method parameters and/or + * static properties of Defaults + */ + public static $aliases = array( + /** + * suppress_response_codes=true as an URL parameter to force + * a HTTP 200 status code on all responses + */ + 'suppress_response_codes' => 'suppressResponseCode', + ); + + /** + * @var array determines the defaults that can be overridden by the api + * user by passing them as URL parameters + */ + public static $overridables = array( + 'suppressResponseCode', + ); + + /** + * @var array contains validation details for defaults to be used when + * set through URL parameters + */ + public static $validation = array( + 'suppressResponseCode' => array('type' => 'bool'), + 'headerExpires' => array('type' => 'int', 'min' => 0), + ); + + // ================================================================== + // + // Overrides API Developer + // + // ------------------------------------------------------------------ + + /** + * @var array determines what are the phpdoc comment tags that will + * override the Defaults here with their values + */ + public static $fromComments = array( + + /** + * use PHPDoc comments such as the following + * ` + * + * @cache no-cache, must-revalidate` to set the Cache-Control header + * for a specific api method + */ + 'cache' => 'headerCacheControl', + + /** + * use PHPDoc comments such as the following + * ` + * + * @expires 50` to set the Expires header + * for a specific api method + */ + 'expires' => 'headerExpires', + + /** + * use PHPDoc comments such as the following + * ` + * + * @throttle 300` + * to set the bandwidth throttling for 300 milliseconds + * for a specific api method + */ + 'throttle' => 'throttle', + + /** + * enable or disable smart auto routing from method comments + * this one is hardwired so cant be turned off + * it is placed here just for documentation purpose + */ + 'smart-auto-routing' => 'smartAutoRouting', + ); + + // ================================================================== + // + // Util + // + // ------------------------------------------------------------------ + + /** + * Use this method to set value to a static properly of Defaults when + * you want to make sure only proper values are taken in with the help of + * validation + * + * @static + * + * @param string $name name of the static property + * @param mixed $value value to set the property to + * + * @return bool + */ + public static function setProperty($name, $value) + { + if (!property_exists(__CLASS__, $name)) return false; + if (@is_array(Defaults::$validation[$name])) { + $info = new ValidationInfo(Defaults::$validation[$name]); + $value = Validator::validate($value, $info); + } + Defaults::$$name = $value; + return true; + } + +} + diff --git a/htdocs/includes/restler/EventDispatcher.php b/htdocs/includes/restler/EventDispatcher.php new file mode 100644 index 00000000000..8e913bc563d --- /dev/null +++ b/htdocs/includes/restler/EventDispatcher.php @@ -0,0 +1,98 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +use Closure; + +class EventDispatcher +{ + private $listeners = array(); + protected static $_waitList = array(); + + public static $self; + protected $events = array(); + + public function __construct() { + static::$self = $this; + if (!empty(static::$_waitList)) { + foreach (static::$_waitList as $param) { + call_user_func_array(array($this,$param[0]), $param[1]); + } + } + } + + public static function __callStatic($eventName, $params) + { + if (0 === strpos($eventName, 'on')) { + if(static::$self){ + return call_user_func_array(array(static::$self, $eventName), $params); + } + static::$_waitList[] = func_get_args(); + return false; + } + } + + public function __call($eventName, $params) + { + if (0 === strpos($eventName, 'on')) { + if (!@is_array($this->listeners[$eventName])) + $this->listeners[$eventName] = array(); + $this->listeners[$eventName][] = $params[0]; + } + return $this; + } + + public static function addListener($eventName, Closure $callback) + { + return static::$eventName($callback); + } + + public function on(array $eventHandlers) + { + for ( + $count = count($eventHandlers), + $events = array_map( + 'ucfirst', + $keys = array_keys( + $eventHandlers = array_change_key_case( + $eventHandlers, + CASE_LOWER + ) + ) + ), + $i = 0; + $i < $count; + call_user_func( + array($this, "on{$events[$i]}"), + $eventHandlers[$keys[$i++]] + ) + ); + } + + /** + * Fire an event to notify all listeners + * + * @param string $eventName name of the event + * @param array $params event related data + */ + protected function dispatch($eventName, array $params = array()) + { + $this->events[] = $eventName; + $params = func_get_args(); + $eventName = 'on'.ucfirst(array_shift($params)); + if (isset($this->listeners[$eventName])) + foreach ($this->listeners[$eventName] as $callback) + call_user_func_array($callback, $params); + } + +} + diff --git a/htdocs/includes/restler/Filter/RateLimit.php b/htdocs/includes/restler/Filter/RateLimit.php new file mode 100644 index 00000000000..99539f70eff --- /dev/null +++ b/htdocs/includes/restler/Filter/RateLimit.php @@ -0,0 +1,178 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class RateLimit implements iFilter, iUseAuthentication +{ + /** + * @var \Luracast\Restler\Restler; + */ + public $restler; + /** + * @var int + */ + public static $usagePerUnit = 1200; + /** + * @var int + */ + public static $authenticatedUsagePerUnit = 5000; + /** + * @var string + */ + public static $unit = 'hour'; + /** + * @var string group the current api belongs to + */ + public static $group = 'common'; + + protected static $units = array( + 'second' => 1, + 'minute' => 60, + 'hour' => 3600, // 60*60 seconds + 'day' => 86400, // 60*60*24 seconds + 'week' => 604800, // 60*60*24*7 seconds + 'month' => 2592000, // 60*60*24*30 seconds + ); + + /** + * @var array all paths beginning with any of the following will be excluded + * from documentation + */ + public static $excludedPaths = array('resources'); + + + /** + * @param string $unit + * @param int $usagePerUnit + * @param int $authenticatedUsagePerUnit set it to false to give unlimited access + * + * @throws \InvalidArgumentException + * @return void + */ + public static function setLimit( + $unit, $usagePerUnit, $authenticatedUsagePerUnit = null + ) + { + static::$unit = $unit; + static::$usagePerUnit = $usagePerUnit; + static::$authenticatedUsagePerUnit = + is_null($authenticatedUsagePerUnit) ? $usagePerUnit : $authenticatedUsagePerUnit; + } + + public function __isAllowed() + { + if (static::$authenticatedUsagePerUnit + == static::$usagePerUnit + ) return $this->check(); + return null; + } + + public function __setAuthenticationStatus($isAuthenticated = false) + { + header('X-Auth-Status: ' . ($isAuthenticated ? 'true' : 'false')); + $this->check($isAuthenticated); + } + + private static function validate($unit) + { + if (!isset(static::$units[$unit])) + throw new \InvalidArgumentException( + 'Rate Limit time unit should be ' + . implode('|', array_keys(static::$units)) . '.' + ); + } + + private function check($isAuthenticated = false) + { + $path = $this->restler->url; + foreach (static::$excludedPaths as $exclude) { + if (empty($exclude) && empty($path)) { + return true; + } elseif (0 === strpos($path, $exclude)) { + return true; + } + } + static::validate(static::$unit); + $timeUnit = static::$units[static::$unit]; + $maxPerUnit = $isAuthenticated + ? static::$authenticatedUsagePerUnit + : static::$usagePerUnit; + if ($maxPerUnit) { + $user = Defaults::$userIdentifierClass; + if (!method_exists($user, 'getUniqueIdentifier')) { + throw new \UnexpectedValueException('`Defaults::$userIdentifierClass` must implement `iIdentifyUser` interface'); + } + $id = "RateLimit_" . $maxPerUnit . '_per_' . static::$unit + . '_for_' . static::$group + . '_' . $user::getUniqueIdentifier(); + $lastRequest = $this->restler->cache->get($id, true) + ? : array('time' => 0, 'used' => 0); + $time = $lastRequest['time']; + $diff = time() - $time; # in seconds + $used = $lastRequest['used']; + + header("X-RateLimit-Limit: $maxPerUnit per " . static::$unit); + if ($diff >= $timeUnit) { + $used = 1; + $time = time(); + } elseif ($used >= $maxPerUnit) { + header("X-RateLimit-Remaining: 0"); + $wait = $timeUnit - $diff; + sleep(1); + throw new RestException(429, + 'Rate limit of ' . $maxPerUnit . ' request' . + ($maxPerUnit > 1 ? 's' : '') . ' per ' + . static::$unit . ' exceeded. Please wait for ' + . static::duration($wait) . '.' + ); + } else { + $used++; + } + $remainingPerUnit = $maxPerUnit - $used; + header("X-RateLimit-Remaining: $remainingPerUnit"); + $this->restler->cache->set($id, + array('time' => $time, 'used' => $used)); + } + return true; + } + + private function duration($secs) + { + $units = array( + 'week' => (int)($secs / 86400 / 7), + 'day' => $secs / 86400 % 7, + 'hour' => $secs / 3600 % 24, + 'minute' => $secs / 60 % 60, + 'second' => $secs % 60); + + $ret = array(); + + //$unit = 'days'; + foreach ($units as $k => $v) { + if ($v > 0) { + $ret[] = $v > 1 ? "$v {$k}s" : "$v $k"; + //$unit = $k; + } + } + $i = count($ret) - 1; + if ($i) { + $ret[$i] = 'and ' . $ret[$i]; + } + return implode(' ', $ret); //." $unit."; + } +} \ No newline at end of file diff --git a/htdocs/includes/restler/Flash.php b/htdocs/includes/restler/Flash.php new file mode 100644 index 00000000000..f18768e64ff --- /dev/null +++ b/htdocs/includes/restler/Flash.php @@ -0,0 +1,146 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class Flash //implements \JsonSerializable +{ + const SUCCESS = 'success'; + const INFO = 'info'; + const WARNING = 'warning'; + const DANGER = 'danger'; + + /** + * @var Flash + */ + private static $instance; + private $usedOnce = false; + + /** + * Flash a success message to user + * + * @param string $message + * @param string $header + * + * @return Flash + */ + public static function success($message, $header = '') + { + return static::message($message, $header, Flash::SUCCESS); + } + + /** + * Flash a info message to user + * + * @param string $message + * @param string $header + * + * @return Flash + */ + public static function info($message, $header = '') + { + return static::message($message, $header, Flash::INFO); + } + + /** + * Flash a warning message to user + * + * @param string $message + * @param string $header + * + * @return Flash + */ + public static function warning($message, $header = '') + { + return static::message($message, $header, Flash::WARNING); + } + + /** + * Flash a error message to user + * + * @param string $message + * @param string $header + * + * @return Flash + */ + public static function danger($message, $header = '') + { + return static::message($message, $header, Flash::DANGER); + } + + /** + * Flash a message to user + * + * @param string $text message text + * @param string $header + * @param string $type + * + * @return Flash + */ + public static function message($text, $header = '', $type = Flash::WARNING) + { + return static::set(array('message' => $text, 'header' => $header, 'type' => $type)); + } + + /** + * Set some data for one time use + * + * @param array $data array of key value pairs {@type associative} + * + * @return Flash + */ + public static function set(array $data) + { + if (!static::$instance) + static::$instance = new Flash(); + if (!isset($_SESSION['flash'])) + $_SESSION['flash'] = array(); + $_SESSION['flash'] += $data; + HtmlFormat::$data['flash'] = static::$instance; + return static::$instance; + } + + public function __get($name) + { + $this->usedOnce = true; + return Util::nestedValue($_SESSION, 'flash', $name); + } + + public function __isset($name) + { + return !is_null(Util::nestedValue($_SESSION, 'flash', $name)); + } + + public function __destruct() + { + if ($this->usedOnce) + unset($_SESSION['flash']); + } + + /** + * Specify data which should be serialized to JSON + * @link http://php.net/manual/en/jsonserializable.jsonserialize.php + * @return mixed data which can be serialized by json_encode, + * which is a value of any type other than a resource. + */ + public function jsonSerialize() + { + $this->usedOnce = true; + return isset($_SESSION['flash']) + ? $_SESSION['flash'] + : array(); + } +} \ No newline at end of file diff --git a/htdocs/includes/restler/Format/AmfFormat.php b/htdocs/includes/restler/Format/AmfFormat.php new file mode 100644 index 00000000000..e477badb01c --- /dev/null +++ b/htdocs/includes/restler/Format/AmfFormat.php @@ -0,0 +1,45 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class AmfFormat extends Format +{ + const MIME = 'application/x-amf'; + const EXTENSION = 'amf'; + + public function encode($data, $humanReadable = false) + { + + $stream = new OutputStream(); + $serializer = new Serializer($stream); + $serializer->writeTypeMarker($data); + + return $stream->getStream(); + } + + public function decode($data) + { + $stream = new InputStream(substr($data, 1)); + $deserializer = new Deserializer($stream); + + return $deserializer->readTypeMarker(); + } +} + diff --git a/htdocs/includes/restler/Format/CsvFormat.php b/htdocs/includes/restler/Format/CsvFormat.php new file mode 100644 index 00000000000..c0dedf1b515 --- /dev/null +++ b/htdocs/includes/restler/Format/CsvFormat.php @@ -0,0 +1,181 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class CsvFormat extends Format implements iDecodeStream +{ + + const MIME = 'text/csv'; + const EXTENSION = 'csv'; + public static $delimiter = ','; + public static $enclosure = '"'; + public static $escape = '\\'; + public static $haveHeaders = null; + + /** + * Encode the given data in the csv format + * + * @param array $data + * resulting data that needs to + * be encoded in the given format + * @param boolean $humanReadable + * set to TRUE when restler + * is not running in production mode. Formatter has to + * make the encoded output more human readable + * + * @return string encoded string + * + * @throws RestException 500 on unsupported data + */ + public function encode($data, $humanReadable = false) + { + $char = Object::$separatorChar; + Object::$separatorChar = false; + $data = Object::toArray($data); + Object::$separatorChar = $char; + if (is_array($data) && array_values($data) == $data) { + //if indexed array + $lines = array(); + $row = array_shift($data); + if (array_values($row) != $row) { + $lines[] = static::putRow(array_keys($row)); + } + $lines[] = static::putRow(array_values($row)); + foreach ($data as $row) { + $lines[] = static::putRow(array_values($row)); + } + return implode(PHP_EOL, $lines) . PHP_EOL; + } + throw new RestException( + 500, + 'Unsupported data for ' . strtoupper(static::EXTENSION) . ' format' + ); + } + + protected static function putRow($data) + { + $fp = fopen('php://temp', 'r+'); + fputcsv($fp, $data, static::$delimiter, static::$enclosure); + rewind($fp); + $data = fread($fp, 1048576); + fclose($fp); + return rtrim($data, PHP_EOL); + } + + /** + * Decode the given data from the csv format + * + * @param string $data + * data sent from client to + * the api in the given format. + * + * @return array associative array of the parsed data + */ + public function decode($data) + { + $decoded = array(); + + if (empty($data)) { + return $decoded; + } + + $lines = array_filter(explode(PHP_EOL, $data)); + + $keys = false; + $row = static::getRow(array_shift($lines)); + + if (is_null(static::$haveHeaders)) { + //try to guess with the given data + static::$haveHeaders = !count(array_filter($row, 'is_numeric')); + } + + static::$haveHeaders ? $keys = $row : $decoded[] = $row; + + while (($row = static::getRow(array_shift($lines), $keys)) !== FALSE) + $decoded [] = $row; + + $char = Object::$separatorChar; + Object::$separatorChar = false; + $decoded = Object::toArray($decoded); + Object::$separatorChar = $char; + return $decoded; + } + + protected static function getRow($data, $keys = false) + { + if (empty($data)) { + return false; + } + $line = str_getcsv( + $data, + static::$delimiter, + static::$enclosure, + static::$escape + ); + + $row = array(); + foreach ($line as $key => $value) { + if (is_numeric($value)) + $value = floatval($value); + if ($keys) { + if (isset($keys [$key])) + $row [$keys [$key]] = $value; + } else { + $row [$key] = $value; + } + } + if ($keys) { + for ($i = count($row); $i < count($keys); $i++) { + $row[$keys[$i]] = null; + } + } + return $row; + } + + /** + * Decode the given data stream + * + * @param string $stream A stream resource with data + * sent from client to the api + * in the given format. + * + * @return array associative array of the parsed data + */ + public function decodeStream($stream) + { + $decoded = array(); + + $keys = false; + $row = static::getRow(stream_get_line($stream, 0, PHP_EOL)); + if (is_null(static::$haveHeaders)) { + //try to guess with the given data + static::$haveHeaders = !count(array_filter($row, 'is_numeric')); + } + + static::$haveHeaders ? $keys = $row : $decoded[] = $row; + + while (($row = static::getRow(stream_get_line($stream, 0, PHP_EOL), $keys)) !== FALSE) + $decoded [] = $row; + + $char = Object::$separatorChar; + Object::$separatorChar = false; + $decoded = Object::toArray($decoded); + Object::$separatorChar = $char; + return $decoded; + } +} \ No newline at end of file diff --git a/htdocs/includes/restler/Format/Format.php b/htdocs/includes/restler/Format/Format.php new file mode 100644 index 00000000000..d04f74cf4ac --- /dev/null +++ b/htdocs/includes/restler/Format/Format.php @@ -0,0 +1,140 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +abstract class Format implements iFormat +{ + /** + * override in the extending class + */ + const MIME = 'text/plain'; + /** + * override in the extending class + */ + const EXTENSION = 'txt'; + + /** + * @var string charset encoding defaults to UTF8 + */ + protected $charset='utf-8'; + + /** + * Injected at runtime + * + * @var \Luracast\Restler\Restler + */ + public $restler; + + /** + * Get MIME type => Extension mappings as an associative array + * + * @return array list of mime strings for the format + * @example array('application/json'=>'json'); + */ + public function getMIMEMap() + { + return array( + static::MIME => static::EXTENSION + ); + } + + /** + * Set the selected MIME type + * + * @param string $mime + * MIME type + */ + public function setMIME($mime) + { + //do nothing + } + + /** + * Content-Type field of the HTTP header can send a charset + * parameter in the HTTP header to specify the character + * encoding of the document. + * This information is passed + * here so that Format class can encode data accordingly + * Format class may choose to ignore this and use its + * default character set. + * + * @param string $charset + * Example utf-8 + */ + public function setCharset($charset) + { + $this->charset = $charset; + } + + /** + * Content-Type accepted by the Format class + * + * @return string $charset Example utf-8 + */ + public function getCharset() + { + return $this->charset; + } + + /** + * Get selected MIME type + */ + public function getMIME() + { + return static::MIME; + } + + /** + * Set the selected file extension + * + * @param string $extension + * file extension + */ + public function setExtension($extension) + { + //do nothing; + } + + /** + * Get the selected file extension + * + * @return string file extension + */ + public function getExtension() + { + return static::EXTENSION; + } + + /** + * @return boolean is parsing the request supported? + */ + public function isReadable() + { + return true; + } + + /** + * @return boolean is composing response supported? + */ + public function isWritable() + { + return true; + } + + public function __toString() + { + return $this->getExtension(); + } + +} + diff --git a/htdocs/includes/restler/Format/HtmlFormat.php b/htdocs/includes/restler/Format/HtmlFormat.php new file mode 100644 index 00000000000..65d2bb6279b --- /dev/null +++ b/htdocs/includes/restler/Format/HtmlFormat.php @@ -0,0 +1,485 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class HtmlFormat extends Format +{ + public static $mime = 'text/html'; + public static $extension = 'html'; + public static $view; + public static $errorView = 'debug.php'; + public static $template = 'php'; + public static $handleSession = true; + + public static $useSmartViews = true; + /** + * @var null|string defaults to template named folder in Defaults::$cacheDirectory + */ + public static $cacheDirectory = null; + /** + * @var array global key value pair to be supplied to the templates. All + * keys added here will be available as a variable inside the template + */ + public static $data = array(); + /** + * @var string set it to the location of your the view files. Defaults to + * views folder which is same level as vendor directory. + */ + public static $viewPath; + /** + * @var array template and its custom extension key value pair + */ + public static $customTemplateExtensions = array('blade' => 'blade.php'); + /** + * @var bool used internally for error handling + */ + protected static $parseViewMetadata = true; + /** + * @var Restler; + */ + public $restler; + + public function __construct() + { + //============ SESSION MANAGEMENT =============// + if (static::$handleSession) { + if (session_start() && isset($_SESSION['flash'])) { + static::$data['flash'] = $_SESSION['flash']; + unset($_SESSION['flash']); + } + } + if (!static::$viewPath) { + $array = explode('vendor', __DIR__, 2); + static::$viewPath = $array[0] . 'views'; + } + } + + public static function blade(array $data, $debug = true) + { + if (!class_exists('\Illuminate\View\View', true)) + throw new RestException(500, + 'Blade templates require laravel view classes to be installed using `composer install`'); + $resolver = new EngineResolver(); + $files = new Filesystem(); + $compiler = new BladeCompiler($files, static::$cacheDirectory); + $engine = new CompilerEngine($compiler); + $resolver->register('blade', function () use ($engine) { + return $engine; + }); + + /** @var Restler $restler */ + $restler = Scope::get('Restler'); + + //Lets expose shortcuts for our classes + spl_autoload_register(function ($className) use ($restler) { + if (isset($restler->apiMethodInfo->metadata['scope'][$className])) { + return class_alias($restler->apiMethodInfo->metadata['scope'][$className], $className); + } + if (isset(Scope::$classAliases[$className])) { + return class_alias(Scope::$classAliases[$className], $className); + } + return false; + }, true, true); + + $viewFinder = new FileViewFinder($files, array(static::$viewPath)); + $factory = new Factory($resolver, $viewFinder, new Dispatcher()); + $path = $viewFinder->find(self::$view); + $view = new View($factory, $engine, self::$view, $path, $data); + $factory->callCreator($view); + return $view->render(); + } + + public static function twig(array $data, $debug = true) + { + if (!class_exists('\Twig_Environment', true)) + throw new RestException(500, + 'Twig templates require twig classes to be installed using `composer install`'); + $loader = new \Twig_Loader_Filesystem(static::$viewPath); + $twig = new \Twig_Environment($loader, array( + 'cache' => static::$cacheDirectory, + 'debug' => $debug, + 'use_strict_variables' => $debug, + )); + if ($debug) + $twig->addExtension(new \Twig_Extension_Debug()); + + $twig->addFunction( + new \Twig_SimpleFunction( + 'form', + 'Luracast\Restler\UI\Forms::get', + array('is_safe' => array('html')) + ) + ); + $twig->addFunction( + new \Twig_SimpleFunction( + 'form_key', + 'Luracast\Restler\UI\Forms::key' + ) + ); + $twig->addFunction( + new \Twig_SimpleFunction( + 'nav', + 'Luracast\Restler\UI\Nav::get' + ) + ); + + $twig->registerUndefinedFunctionCallback(function ($name) { + if ( + isset(HtmlFormat::$data[$name]) && + is_callable(HtmlFormat::$data[$name]) + ) { + return new \Twig_SimpleFunction( + $name, + HtmlFormat::$data[$name] + ); + } + return false; + }); + + $template = $twig->loadTemplate(static::getViewFile()); + return $template->render($data); + } + + public static function handlebar(array $data, $debug = true) + { + return static::mustache($data, $debug); + } + + public static function mustache(array $data, $debug = true) + { + if (!class_exists('\Mustache_Engine', true)) + throw new RestException( + 500, + 'Mustache/Handlebar templates require mustache classes ' . + 'to be installed using `composer install`' + ); + if (!isset($data['nav'])) + $data['nav'] = array_values(Nav::get()); + $options = array( + 'loader' => new \Mustache_Loader_FilesystemLoader( + static::$viewPath, + array('extension' => static::getViewExtension()) + ), + 'helpers' => array( + 'form' => function ($text, \Mustache_LambdaHelper $m) { + $params = explode(',', $m->render($text)); + return call_user_func_array( + 'Luracast\Restler\UI\Forms::get', + $params + ); + }, + ) + ); + if (!$debug) + $options['cache'] = static::$cacheDirectory; + $m = new \Mustache_Engine($options); + return $m->render(static::getViewFile(), $data); + } + + public static function php(array $data, $debug = true) + { + if (static::$view == 'debug') + static::$viewPath = dirname(__DIR__) . '/views'; + $view = static::getViewFile(true); + + if (!is_readable($view)) { + throw new RestException( + 500, + "view file `$view` is not readable. " . + 'Check for file presence and file permissions' + ); + } + + $path = static::$viewPath . DIRECTORY_SEPARATOR; + $template = function ($view) use ($data, $path) { + $form = function () { + return call_user_func_array( + 'Luracast\Restler\UI\Forms::get', + func_get_args() + ); + }; + if (!isset($data['form'])) + $data['form'] = $form; + $nav = function () { + return call_user_func_array( + 'Luracast\Restler\UI\Nav::get', + func_get_args() + ); + }; + if (!isset($data['nav'])) + $data['nav'] = $nav; + + $_ = function () use ($data, $path) { + extract($data); + $args = func_get_args(); + $task = array_shift($args); + switch ($task) { + case 'require': + case 'include': + $file = $path . $args[0]; + if (is_readable($file)) { + if ( + isset($args[1]) && + ($arrays = Util::nestedValue($data, $args[1])) + ) { + $str = ''; + foreach ($arrays as $arr) { + extract($arr); + $str .= include $file; + } + return $str; + } else { + return include $file; + } + } + break; + case 'if': + if (count($args) < 2) + $args[1] = ''; + if (count($args) < 3) + $args[2] = ''; + return $args[0] ? $args[1] : $args[2]; + break; + default: + if (isset($data[$task]) && is_callable($data[$task])) + return call_user_func_array($data[$task], $args); + } + return ''; + }; + extract($data); + return @include $view; + }; + $value = $template($view); + if (is_string($value)) + return $value; + } + + /** + * Encode the given data in the format + * + * @param array $data resulting data that needs to + * be encoded in the given format + * @param boolean $humanReadable set to TRUE when restler + * is not running in production mode. + * Formatter has to make the encoded + * output more human readable + * + * @throws \Exception + * @return string encoded string + */ + public function encode($data, $humanReadable = false) + { + if (!is_readable(static::$viewPath)) { + throw new \Exception( + 'The views directory `' + . self::$viewPath . '` should exist with read permission.' + ); + } + static::$data['basePath'] = dirname($_SERVER['SCRIPT_NAME']); + static::$data['baseUrl'] = $this->restler->getBaseUrl(); + static::$data['currentPath'] = $this->restler->url; + + try { + $exception = $this->restler->exception; + $success = is_null($exception); + $error = $success ? null : $exception->getMessage(); + $data = array( + 'response' => Object::toArray($data), + 'stages' => $this->restler->getEvents(), + 'success' => $success, + 'error' => $error + ); + $info = $data['api'] = $this->restler->apiMethodInfo; + $metadata = Util::nestedValue( + $this->restler, 'apiMethodInfo', 'metadata' + ); + $view = $success ? 'view' : 'errorView'; + $value = false; + if (static::$parseViewMetadata && isset($metadata[$view])) { + if (is_array($metadata[$view])) { + self::$view = $metadata[$view]['description']; + $value = Util::nestedValue( + $metadata[$view], 'properties', 'value' + ); + } else { + self::$view = $metadata[$view]; + } + } elseif (!self::$view) { + $file = static::$viewPath . '/' . $this->restler->url . '.' . static::getViewExtension(); + self::$view = static::$useSmartViews && is_readable($file) + ? $this->restler->url + : static::$errorView; + } + if ( + isset($metadata['param']) + && (!$value || 0 === strpos($value, 'request')) + ) { + $params = $metadata['param']; + foreach ($params as $index => &$param) { + $index = intval($index); + if (is_numeric($index)) { + $param['value'] = $this + ->restler + ->apiMethodInfo + ->parameters[$index]; + } + } + $data['request']['parameters'] = $params; + } + if ($value) { + $data = Util::nestedValue($data, explode('.', $value)); + } + $data += static::$data; + if (false === ($i = strrpos(self::$view, '.'))) { + $template = self::$template; + } else { + self::$template = $template = substr(self::$view, $i + 1); + self::$view = substr(self::$view, 0, $i); + } + if (!static::$cacheDirectory) { + static::$cacheDirectory = Defaults::$cacheDirectory . DIRECTORY_SEPARATOR . $template; + if (!file_exists(static::$cacheDirectory)) { + if (!mkdir(static::$cacheDirectory)) { + throw new RestException(500, 'Unable to create cache directory `' . static::$cacheDirectory . '`'); + } + } + } + if (method_exists($class = get_called_class(), $template)) { + return call_user_func("$class::$template", $data, $humanReadable); + } + throw new RestException(500, "Unsupported template system `$template`"); + } catch (Exception $e) { + static::$parseViewMetadata = false; + $this->reset(); + throw $e; + } + } + + public static function getViewExtension() + { + return isset(static::$customTemplateExtensions[static::$template]) + ? static::$customTemplateExtensions[static::$template] + : static::$template; + } + + public static function getViewFile($fullPath = false, $includeExtension = true) + { + $v = $fullPath ? static::$viewPath . '/' : ''; + $v .= static::$view; + if ($includeExtension) + $v .= '.' . static::getViewExtension(); + return $v; + } + + private function reset() + { + static::$mime = 'text/html'; + static::$extension = 'html'; + static::$view = 'debug'; + static::$template = 'php'; + } + + /** + * Decode the given data from the format + * + * @param string $data + * data sent from client to + * the api in the given format. + * + * @return array associative array of the parsed data + * + * @throws RestException + */ + public function decode($data) + { + throw new RestException(500, 'HtmlFormat is write only'); + } + + /** + * @return bool false as HTML format is write only + */ + public function isReadable() + { + return false; + } + + /** + * Get MIME type => Extension mappings as an associative array + * + * @return array list of mime strings for the format + * @example array('application/json'=>'json'); + */ + public function getMIMEMap() + { + return array( + static::$mime => static::$extension + ); + } + + /** + * Set the selected MIME type + * + * @param string $mime MIME type + */ + public function setMIME($mime) + { + static::$mime = $mime; + } + + /** + * Get selected MIME type + */ + public function getMIME() + { + return static::$mime; + } + + /** + * Get the selected file extension + * + * @return string file extension + */ + public function getExtension() + { + return static::$extension; + } + + /** + * Set the selected file extension + * + * @param string $extension file extension + */ + public function setExtension($extension) + { + static::$extension = $extension; + } +} diff --git a/htdocs/includes/restler/Format/JsFormat.php b/htdocs/includes/restler/Format/JsFormat.php new file mode 100644 index 00000000000..067fd1aaa37 --- /dev/null +++ b/htdocs/includes/restler/Format/JsFormat.php @@ -0,0 +1,48 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class JsFormat extends JsonFormat +{ + const MIME = 'text/javascript'; + const EXTENSION = 'js'; + + public static $callbackMethodName = 'parseResponse'; + public static $callbackOverrideQueryString = 'callback'; + public static $includeHeaders = true; + + public function encode($data, $human_readable = false) + { + $r = array(); + if (static::$includeHeaders) { + $r['meta'] = array(); + foreach (headers_list() as $header) { + list($h, $v) = explode(': ', $header, 2); + $r['meta'][$h] = $v; + } + } + $r['data'] = $data; + if (isset($_GET[static::$callbackOverrideQueryString])) { + static::$callbackMethodName + = (string) $_GET[static::$callbackOverrideQueryString]; + } + return static::$callbackMethodName . '(' + . parent::encode($r, $human_readable) . ');'; + } + + public function isReadable() + { + return false; + } +} diff --git a/htdocs/includes/restler/Format/JsonFormat.php b/htdocs/includes/restler/Format/JsonFormat.php new file mode 100644 index 00000000000..1b7fd938c3c --- /dev/null +++ b/htdocs/includes/restler/Format/JsonFormat.php @@ -0,0 +1,210 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class JsonFormat extends Format +{ + /** + * @var boolean|null shim for json_encode option JSON_PRETTY_PRINT set + * it to null to use smart defaults + */ + public static $prettyPrint = null; + + /** + * @var boolean|null shim for json_encode option JSON_UNESCAPED_SLASHES + * set it to null to use smart defaults + */ + public static $unEscapedSlashes = null; + + /** + * @var boolean|null shim for json_encode JSON_UNESCAPED_UNICODE set it + * to null to use smart defaults + */ + public static $unEscapedUnicode = null; + + /** + * @var boolean|null shim for json_decode JSON_BIGINT_AS_STRING set it to + * null to + * use smart defaults + */ + public static $bigIntAsString = null; + + const MIME = 'application/json'; + const EXTENSION = 'json'; + + public function encode($data, $humanReadable = false) + { + if (!is_null(self::$prettyPrint)) { + $humanReadable = self::$prettyPrint; + } + if (is_null(self::$unEscapedSlashes)) { + self::$unEscapedSlashes = $humanReadable; + } + if (is_null(self::$unEscapedUnicode)) { + self::$unEscapedUnicode = $this->charset == 'utf-8'; + } + $options = 0; + if ((PHP_MAJOR_VERSION == 5 && PHP_MINOR_VERSION >= 4) // PHP >= 5.4 + || PHP_MAJOR_VERSION > 5 // PHP >= 6.0 + ) { + if ($humanReadable) $options |= JSON_PRETTY_PRINT; + if (self::$unEscapedSlashes) $options |= JSON_UNESCAPED_SLASHES; + if (self::$bigIntAsString) $options |= JSON_BIGINT_AS_STRING; + if (self::$unEscapedUnicode) $options |= JSON_UNESCAPED_UNICODE; + return json_encode( + Object::toArray($data, true), $options + ); + } + + $result = json_encode(Object::toArray($data, true)); + if ($humanReadable) $result = $this->formatJson($result); + if (self::$unEscapedUnicode) { + $result = preg_replace_callback('/\\\u(\w\w\w\w)/', + function($matches) + { + if (function_exists('mb_convert_encoding')) + { + return mb_convert_encoding(pack('H*', $matches[1]), 'UTF-8', 'UTF-16BE'); + } + else + { + return iconv('UTF-16BE','UTF-8',pack('H*', $matches[1])); + } + } + , $result); + } + if (self::$unEscapedSlashes) $result = str_replace('\/', '/', $result); + return $result; + } + + public function decode($data) + { + $options = 0; + if (self::$bigIntAsString) { + if ((PHP_MAJOR_VERSION == 5 && PHP_MINOR_VERSION >= 4) // PHP >= 5.4 + || PHP_MAJOR_VERSION > 5 // PHP >= 6.0 + ) { + $options |= JSON_BIGINT_AS_STRING; + } else { + $data = preg_replace( + '/:\s*(\-?\d+(\.\d+)?([e|E][\-|\+]\d+)?)/', + ': "$1"', $data + ); + } + } + $decoded = json_decode($data, $options); + if (function_exists('json_last_error')) { + switch (json_last_error()) { + case JSON_ERROR_NONE : + return Object::toArray($decoded); + break; + case JSON_ERROR_DEPTH : + $message = 'maximum stack depth exceeded'; + break; + case JSON_ERROR_STATE_MISMATCH : + $message = 'underflow or the modes mismatch'; + break; + case JSON_ERROR_CTRL_CHAR : + $message = 'unexpected control character found'; + break; + case JSON_ERROR_SYNTAX : + $message = 'malformed JSON'; + break; + case JSON_ERROR_UTF8 : + $message = 'malformed UTF-8 characters, possibly ' . + 'incorrectly encoded'; + break; + default : + $message = 'unknown error'; + break; + } + throw new RestException (400, 'Error parsing JSON, ' . $message); + } elseif (strlen($data) && $decoded === null || $decoded === $data) { + throw new RestException (400, 'Error parsing JSON'); + } + + return Object::toArray($decoded); + } + + /** + * Pretty print JSON string + * + * @param string $json + * + * @return string formatted json + */ + private function formatJson($json) + { + $tab = ' '; + $newJson = ''; + $indentLevel = 0; + $inString = false; + $len = strlen($json); + for ($c = 0; $c < $len; $c++) { + $char = $json [$c]; + switch ($char) { + case '{' : + case '[' : + if (!$inString) { + $newJson .= $char . "\n" . + str_repeat($tab, $indentLevel + 1); + $indentLevel++; + } else { + $newJson .= $char; + } + break; + case '}' : + case ']' : + if (!$inString) { + $indentLevel--; + $newJson .= "\n" . + str_repeat($tab, $indentLevel) . $char; + } else { + $newJson .= $char; + } + break; + case ',' : + if (!$inString) { + $newJson .= ",\n" . + str_repeat($tab, $indentLevel); + } else { + $newJson .= $char; + } + break; + case ':' : + if (!$inString) { + $newJson .= ': '; + } else { + $newJson .= $char; + } + break; + case '"' : + if ($c == 0) { + $inString = true; + } elseif ($c > 0 && $json [$c - 1] != '\\') { + $inString = !$inString; + } + default : + $newJson .= $char; + break; + } + } + + return $newJson; + } +} + diff --git a/htdocs/includes/restler/Format/MultiFormat.php b/htdocs/includes/restler/Format/MultiFormat.php new file mode 100644 index 00000000000..2c93cd04fe9 --- /dev/null +++ b/htdocs/includes/restler/Format/MultiFormat.php @@ -0,0 +1,144 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +abstract class MultiFormat implements iFormat +{ + /** + * override in the extending class + */ + const MIME = 'text/plain,text/html'; + /** + * override in the extending class + */ + const EXTENSION = 'txt,html'; + + /** + * @var string charset encoding defaults to UTF8 + */ + protected $charset='utf-8'; + + public static $mime; + public static $extension; + + /** + * Injected at runtime + * + * @var \Luracast\Restler\Restler + */ + public $restler; + + /** + * Get MIME type => Extension mappings as an associative array + * + * @return array list of mime strings for the format + * @example array('application/json'=>'json'); + */ + public function getMIMEMap() + { + $extensions = explode(',',static::EXTENSION); + $mimes = explode(',',static::MIME); + $count = max(count($extensions), count($mimes)); + $extensions += array_fill(0, $count, end($extensions)); + $mimes += array_fill(0, $count, end($mimes)); + return array_combine($mimes,$extensions); + } + + /** + * Set the selected MIME type + * + * @param string $mime + * MIME type + */ + public function setMIME($mime) + { + static::$mime = $mime; + } + + /** + * Content-Type field of the HTTP header can send a charset + * parameter in the HTTP header to specify the character + * encoding of the document. + * This information is passed + * here so that Format class can encode data accordingly + * Format class may choose to ignore this and use its + * default character set. + * + * @param string $charset + * Example utf-8 + */ + public function setCharset($charset) + { + $this->charset = $charset; + } + + /** + * Content-Type accepted by the Format class + * + * @return string $charset Example utf-8 + */ + public function getCharset() + { + return $this->charset; + } + + /** + * Get selected MIME type + */ + public function getMIME() + { + return static::$mime; + } + + /** + * Set the selected file extension + * + * @param string $extension + * file extension + */ + public function setExtension($extension) + { + static::$extension = $extension; + } + + /** + * Get the selected file extension + * + * @return string file extension + */ + public function getExtension() + { + return static::$extension; + } + + /** + * @return boolean is parsing the request supported? + */ + public function isReadable() + { + return true; + } + + /** + * @return boolean is composing response supported? + */ + public function isWritable() + { + return true; + } + + public function __toString() + { + return $this->getExtension(); + } +} + diff --git a/htdocs/includes/restler/Format/PlistFormat.php b/htdocs/includes/restler/Format/PlistFormat.php new file mode 100644 index 00000000000..2c645eb0dfe --- /dev/null +++ b/htdocs/includes/restler/Format/PlistFormat.php @@ -0,0 +1,91 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class PlistFormat extends MultiFormat +{ + /** + * @var boolean set it to true binary plist is preferred + */ + public static $compact = null; + const MIME = 'application/xml,application/x-plist'; + const EXTENSION = 'plist'; + + public function setMIME($mime) + { + static::$mime = $mime; + static::$compact = $mime == 'application/x-plist'; + } + + /** + * Encode the given data in plist format + * + * @param array $data + * resulting data that needs to + * be encoded in plist format + * @param boolean $humanReadable + * set to true when restler + * is not running in production mode. Formatter has to + * make the encoded output more human readable + * + * @return string encoded string + */ + public function encode($data, $humanReadable = false) + { + //require_once 'CFPropertyList.php'; + if (!isset(self::$compact)) { + self::$compact = !$humanReadable; + } + /** + * + * @var CFPropertyList + */ + $plist = new CFPropertyList (); + $td = new CFTypeDetector (); + $guessedStructure = $td->toCFType( + Object::toArray($data) + ); + $plist->add($guessedStructure); + + return self::$compact + ? $plist->toBinary() + : $plist->toXML(true); + } + + /** + * Decode the given data from plist format + * + * @param string $data + * data sent from client to + * the api in the given format. + * + * @return array associative array of the parsed data + */ + public function decode($data) + { + //require_once 'CFPropertyList.php'; + $plist = new CFPropertyList (); + $plist->parse($data); + + return $plist->toArray(); + } +} + diff --git a/htdocs/includes/restler/Format/TsvFormat.php b/htdocs/includes/restler/Format/TsvFormat.php new file mode 100644 index 00000000000..f67ebfcd677 --- /dev/null +++ b/htdocs/includes/restler/Format/TsvFormat.php @@ -0,0 +1,24 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class TsvFormat extends CsvFormat +{ + const MIME = 'text/csv'; + const EXTENSION = 'csv'; + public static $delimiter = "\t"; + public static $enclosure = '"'; + public static $escape = '\\'; + public static $haveHeaders = null; +} \ No newline at end of file diff --git a/htdocs/includes/restler/Format/UploadFormat.php b/htdocs/includes/restler/Format/UploadFormat.php new file mode 100644 index 00000000000..aaebf6b5695 --- /dev/null +++ b/htdocs/includes/restler/Format/UploadFormat.php @@ -0,0 +1,145 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class UploadFormat extends Format +{ + const MIME = 'multipart/form-data'; + const EXTENSION = 'post'; + public static $errors = array( + 0 => false, + 1 => "The uploaded file exceeds the maximum allowed size", + 2 => "The uploaded file exceeds the maximum allowed size", + 3 => "The uploaded file was only partially uploaded", + 4 => "No file was uploaded", + 6 => "Missing a temporary folder" + ); + /** + * use it if you need to restrict uploads based on file type + * setting it as an empty array allows all file types + * default is to allow only png and jpeg images + * + * @var array + */ + public static $allowedMimeTypes = array('image/jpeg', 'image/png'); + /** + * use it to restrict uploads based on file size + * set it to 0 to allow all sizes + * please note that it upload restrictions in the server + * takes precedence so it has to be lower than or equal to that + * default value is 1MB (1024x1024)bytes + * usual value for the server is 8388608 + * + * @var int + */ + public static $maximumFileSize = 1048576; + /** + * Your own validation function for validating each uploaded file + * it can return false or throw an exception for invalid file + * use anonymous function / closure in PHP 5.3 and above + * use function name in other cases + * + * @var Callable + */ + public static $customValidationFunction; + /** + * Since exceptions are triggered way before at the `get` stage + * + * @var bool + */ + public static $suppressExceptionsAsError = false; + + protected static function checkFile(& $file, $doMimeCheck = false, $doSizeCheck = false) + { + try { + if ($file['error']) { + //server is throwing an error + //assume that the error is due to maximum size limit + throw new RestException($file['error'] > 5 ? 500 : 413, static::$errors[$file['error']]); + } + $typeElements = explode('/', $file['type']); + $genericType = $typeElements[0].'/*'; + if ( + $doMimeCheck + && !( + in_array($file['type'], self::$allowedMimeTypes) + || in_array($genericType, self::$allowedMimeTypes) + ) + ) { + throw new RestException(403, "File type ({$file['type']}) is not supported."); + } + if ($doSizeCheck && $file['size'] > self::$maximumFileSize) { + throw new RestException(413, "Uploaded file ({$file['name']}) is too big."); + } + if (self::$customValidationFunction) { + if (!call_user_func(self::$customValidationFunction, $file)) { + throw new RestException(403, "File ({$file['name']}) is not supported."); + } + } + } catch (RestException $e) { + if (static::$suppressExceptionsAsError) { + $file['error'] = $e->getCode() == 413 ? 1 : 6; + $file['exception'] = $e; + } else { + throw $e; + } + } + } + + public function encode($data, $humanReadable = false) + { + throw new RestException(500, 'UploadFormat is read only'); + } + + public function decode($data) + { + $doMimeCheck = !empty(self::$allowedMimeTypes); + $doSizeCheck = self::$maximumFileSize ? TRUE : FALSE; + //validate + foreach ($_FILES as & $file) { + if (is_array($file['error'])) { + foreach ($file['error'] as $i => $error) { + $innerFile = array(); + foreach ($file as $property => $value) { + $innerFile[$property] = $value[$i]; + } + if ($innerFile['name']) + static::checkFile($innerFile, $doMimeCheck, $doSizeCheck); + + if (isset($innerFile['exception'])) { + $file['error'][$i] = $innerFile['error']; + $file['exception'] = $innerFile['exception']; + break; + } + } + } else { + if ($file['name']) + static::checkFile($file, $doMimeCheck, $doSizeCheck); + if (isset($innerFile['exception'])) { + break; + } + } + } + //sort file order if needed; + return UrlEncodedFormat::decoderTypeFix($_FILES + $_POST); + } + + function isWritable() + { + return false; + } + +} diff --git a/htdocs/includes/restler/Format/UrlEncodedFormat.php b/htdocs/includes/restler/Format/UrlEncodedFormat.php new file mode 100644 index 00000000000..a37d5d6c5cf --- /dev/null +++ b/htdocs/includes/restler/Format/UrlEncodedFormat.php @@ -0,0 +1,58 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class UrlEncodedFormat extends Format +{ + const MIME = 'application/x-www-form-urlencoded'; + const EXTENSION = 'post'; + + public function encode($data, $humanReadable = false) + { + return http_build_query(static::encoderTypeFix($data)); + } + + public function decode($data) + { + parse_str($data, $r); + return self::decoderTypeFix($r); + } + + public static function encoderTypeFix(array $data) + { + foreach ($data as $k => $v) { + if (is_bool($v)) { + $data[$k] = $v = $v ? 'true' : 'false'; + } elseif (is_array($v)) { + $data[$k] = $v = static::decoderTypeFix($v); + } + } + return $data; + } + + public static function decoderTypeFix(array $data) + { + foreach ($data as $k => $v) { + if ($v === 'true' || $v === 'false') { + $data[$k] = $v = $v === 'true'; + } elseif (is_array($v)) { + $data[$k] = $v = static::decoderTypeFix($v); + } elseif (empty($v) && $v != 0) { + unset($data[$k]); + } + } + return $data; + } +} + diff --git a/htdocs/includes/restler/Format/XmlFormat.php b/htdocs/includes/restler/Format/XmlFormat.php new file mode 100644 index 00000000000..52fe50c6755 --- /dev/null +++ b/htdocs/includes/restler/Format/XmlFormat.php @@ -0,0 +1,348 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class XmlFormat extends Format +{ + const MIME = 'application/xml'; + const EXTENSION = 'xml'; + + // ================================================================== + // + // Properties related to reading/parsing/decoding xml + // + // ------------------------------------------------------------------ + public static $importSettingsFromXml = false; + public static $parseAttributes = true; + public static $parseNamespaces = true; + public static $parseTextNodeAsProperty = true; + + // ================================================================== + // + // Properties related to writing/encoding xml + // + // ------------------------------------------------------------------ + public static $useTextNodeProperty = true; + public static $useNamespaces = true; + public static $cdataNames = array(); + + // ================================================================== + // + // Common Properties + // + // ------------------------------------------------------------------ + public static $attributeNames = array(); + public static $textNodeName = 'text'; + public static $namespaces = array(); + public static $namespacedProperties = array(); + /** + * Default name for the root node. + * + * @var string $rootNodeName + */ + public static $rootName = 'response'; + public static $defaultTagName = 'item'; + + /** + * When you decode an XML its structure is copied to the static vars + * we can use this function to echo them out and then copy paste inside + * our service methods + * + * @return string PHP source code to reproduce the configuration + */ + public static function exportCurrentSettings() + { + $s = 'XmlFormat::$rootName = "' . (self::$rootName) . "\";\n"; + $s .= 'XmlFormat::$attributeNames = ' . + (var_export(self::$attributeNames, true)) . ";\n"; + $s .= 'XmlFormat::$defaultTagName = "' . + self::$defaultTagName . "\";\n"; + $s .= 'XmlFormat::$parseAttributes = ' . + (self::$parseAttributes ? 'true' : 'false') . ";\n"; + $s .= 'XmlFormat::$parseNamespaces = ' . + (self::$parseNamespaces ? 'true' : 'false') . ";\n"; + if (self::$parseNamespaces) { + $s .= 'XmlFormat::$namespaces = ' . + (var_export(self::$namespaces, true)) . ";\n"; + $s .= 'XmlFormat::$namespacedProperties = ' . + (var_export(self::$namespacedProperties, true)) . ";\n"; + } + + return $s; + } + + public function encode($data, $humanReadable = false) + { + $data = Object::toArray($data); + $xml = new XMLWriter(); + $xml->openMemory(); + $xml->startDocument('1.0', $this->charset); + if ($humanReadable) { + $xml->setIndent(true); + $xml->setIndentString(' '); + } + static::$useNamespaces && isset(static::$namespacedProperties[static::$rootName]) + ? + $xml->startElementNs( + static::$namespacedProperties[static::$rootName], + static::$rootName, + static::$namespaces[static::$namespacedProperties[static::$rootName]] + ) + : + $xml->startElement(static::$rootName); + if (static::$useNamespaces) { + foreach (static::$namespaces as $prefix => $ns) { + if (isset(static::$namespacedProperties[static::$rootName]) + && static::$namespacedProperties[static::$rootName] == $prefix + ) + continue; + $prefix = 'xmlns' . (empty($prefix) ? '' : ':' . $prefix); + $xml->writeAttribute($prefix, $ns); + } + } + $this->write($xml, $data, static::$rootName); + $xml->endElement(); + return $xml->outputMemory(); + } + + public function write(XMLWriter $xml, $data, $parent) + { + $text = array(); + if (is_array($data)) { + if (static::$useTextNodeProperty && isset($data[static::$textNodeName])) { + $text [] = $data[static::$textNodeName]; + unset($data[static::$textNodeName]); + } + $attributes = array_flip(static::$attributeNames); + //make sure we deal with attributes first + $temp = array(); + foreach ($data as $key => $value) { + if (isset($attributes[$key])) { + $temp[$key] = $data[$key]; + unset($data[$key]); + } + } + $data = array_merge($temp, $data); + foreach ($data as $key => $value) { + if (is_numeric($key)) { + if (!is_array($value)) { + $text [] = $value; + continue; + } + $key = static::$defaultTagName; + } + $useNS = static::$useNamespaces + && !empty(static::$namespacedProperties[$key]) + && false === strpos($key, ':'); + if (is_array($value)) { + if ($value == array_values($value)) { + //numeric array, create siblings + foreach ($value as $v) { + $useNS + ? $xml->startElementNs( + static::$namespacedProperties[$key], + $key, + null + ) + : $xml->startElement($key); + $this->write($xml, $v, $key); + $xml->endElement(); + } + } else { + $useNS + ? $xml->startElementNs( + static::$namespacedProperties[$key], + $key, + null + ) + : $xml->startElement($key); + $this->write($xml, $value, $key); + $xml->endElement(); + } + continue; + } elseif (is_bool($value)) { + $value = $value ? 'true' : 'false'; + } + if (isset($attributes[$key])) { + $xml->writeAttribute($useNS ? static::$namespacedProperties[$key] . ':' . $key : $key, $value); + } else { + $useNS + ? + $xml->startElementNs( + static::$namespacedProperties[$key], + $key, + null + ) + : $xml->startElement($key); + $this->write($xml, $value, $key); + $xml->endElement(); + } + } + } else { + $text [] = (string)$data; + } + if (!empty($text)) { + if (count($text) == 1) { + in_array($parent, static::$cdataNames) + ? $xml->writeCdata(implode('', $text)) + : $xml->text(implode('', $text)); + } else { + foreach ($text as $t) { + $xml->writeElement(static::$textNodeName, $t); + } + } + } + } + + public function decode($data) + { + try { + if ($data == '') { + return array(); + } + libxml_use_internal_errors(true); + $xml = simplexml_load_string($data, + "SimpleXMLElement", LIBXML_NOBLANKS | LIBXML_NOCDATA | LIBXML_COMPACT); + if (false === $xml) { + $error = libxml_get_last_error(); + throw new RestException(400, 'Malformed XML. ' + . trim($error->message, "\r\n") . ' at line ' . $error->line); + } + libxml_clear_errors(); + if (static::$importSettingsFromXml) { + static::$attributeNames = array(); + static::$namespacedProperties = array(); + static::$namespaces = array(); + static::$rootName = $xml->getName(); + $namespaces = $xml->getNamespaces(); + if (count($namespaces)) { + $p = strpos($data, $xml->getName()); + if ($p && $data{$p - 1} == ':') { + $s = strpos($data, '<') + 1; + $prefix = substr($data, $s, $p - $s - 1); + static::$namespacedProperties[static::$rootName] = $prefix; + } + } + } + $data = $this->read($xml); + if (count($data) == 1 && isset($data[static::$textNodeName])) + $data = $data[static::$textNodeName]; + return $data; + } catch (\RuntimeException $e) { + throw new RestException(400, + "Error decoding request. " . $e->getMessage()); + } + } + + public function read(SimpleXMLElement $xml, $namespaces = null) + { + $r = array(); + $text = (string)$xml; + + if (static::$parseAttributes) { + $attributes = $xml->attributes(); + foreach ($attributes as $key => $value) { + if (static::$importSettingsFromXml + && !in_array($key, static::$attributeNames) + ) { + static::$attributeNames[] = $key; + } + $r[$key] = static::setType((string)$value); + } + } + $children = $xml->children(); + foreach ($children as $key => $value) { + if (isset($r[$key])) { + if (is_array($r[$key])) { + if ($r[$key] != array_values($r[$key])) + $r[$key] = array($r[$key]); + } else { + $r[$key] = array($r[$key]); + } + $r[$key][] = $this->read($value, $namespaces); + } else { + $r[$key] = $this->read($value); + } + } + + if (static::$parseNamespaces) { + if (is_null($namespaces)) + $namespaces = $xml->getDocNamespaces(true); + foreach ($namespaces as $prefix => $ns) { + static::$namespaces[$prefix] = $ns; + if (static::$parseAttributes) { + $attributes = $xml->attributes($ns); + foreach ($attributes as $key => $value) { + if (isset($r[$key])) { + $key = "{$prefix}:$key"; + } + if (static::$importSettingsFromXml + && !in_array($key, static::$attributeNames) + ) { + static::$namespacedProperties[$key] = $prefix; + static::$attributeNames[] = $key; + } + $r[$key] = static::setType((string)$value); + } + } + $children = $xml->children($ns); + foreach ($children as $key => $value) { + if (static::$importSettingsFromXml) + static::$namespacedProperties[$key] = $prefix; + if (isset($r[$key])) { + if (is_array($r[$key])) { + if ($r[$key] != array_values($r[$key])) + $r[$key] = array($r[$key]); + } else { + $r[$key] = array($r[$key]); + } + $r[$key][] = $this->read($value, $namespaces); + } else { + $r[$key] = $this->read($value, $namespaces); + } + } + } + } + + if (empty($text) && $text !== '0') { + if (empty($r)) return null; + } else { + empty($r) + ? $r = static::setType($text) + : ( + static::$parseTextNodeAsProperty + ? $r[static::$textNodeName] = static::setType($text) + : $r[] = static::setType($text) + ); + } + return $r; + } + + public static function setType($value) + { + if (empty($value) && $value !== '0') + return null; + if ($value == 'true') + return true; + if ($value == 'false') + return true; + return $value; + } +} + diff --git a/htdocs/includes/restler/Format/YamlFormat.php b/htdocs/includes/restler/Format/YamlFormat.php new file mode 100644 index 00000000000..d87ad7bbc99 --- /dev/null +++ b/htdocs/includes/restler/Format/YamlFormat.php @@ -0,0 +1,36 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class YamlFormat extends Format +{ + const MIME = 'text/plain'; + const EXTENSION = 'yaml'; + + public function encode($data, $humanReadable = false) + { +// require_once 'sfyaml.php'; + return @Yaml::dump(Object::toArray($data)); + } + + public function decode($data) + { +// require_once 'sfyaml.php'; + return Yaml::parse($data); + } +} + diff --git a/htdocs/includes/restler/Format/iDecodeStream.php b/htdocs/includes/restler/Format/iDecodeStream.php new file mode 100644 index 00000000000..8f2765fcfd8 --- /dev/null +++ b/htdocs/includes/restler/Format/iDecodeStream.php @@ -0,0 +1,30 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +interface iDecodeStream +{ + + /** + * Decode the given data stream + * + * @param string $stream A stream resource with data + * sent from client to the api + * in the given format. + * + * @return array associative array of the parsed data + */ + public function decodeStream($stream); + +} \ No newline at end of file diff --git a/htdocs/includes/restler/Format/iFormat.php b/htdocs/includes/restler/Format/iFormat.php new file mode 100644 index 00000000000..60c6f0a7ede --- /dev/null +++ b/htdocs/includes/restler/Format/iFormat.php @@ -0,0 +1,109 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +interface iFormat +{ + /** + * Get MIME type => Extension mappings as an associative array + * + * @return array list of mime strings for the format + * @example array('application/json'=>'json'); + */ + public function getMIMEMap(); + + /** + * Set the selected MIME type + * + * @param string $mime + * MIME type + */ + public function setMIME($mime); + + /** + * Content-Type field of the HTTP header can send a charset + * parameter in the HTTP header to specify the character + * encoding of the document. + * This information is passed + * here so that Format class can encode data accordingly + * Format class may choose to ignore this and use its + * default character set. + * + * @param string $charset + * Example utf-8 + */ + public function setCharset($charset); + + /** + * Content-Type accepted by the Format class + * + * @return string $charset Example utf-8 + */ + public function getCharset(); + + /** + * Get selected MIME type + */ + public function getMIME(); + + /** + * Set the selected file extension + * + * @param string $extension + * file extension + */ + public function setExtension($extension); + + /** + * Get the selected file extension + * + * @return string file extension + */ + public function getExtension(); + + /** + * Encode the given data in the format + * + * @param array $data + * resulting data that needs to + * be encoded in the given format + * @param boolean $humanReadable + * set to TRUE when restler + * is not running in production mode. Formatter has to + * make the encoded output more human readable + * @return string encoded string + */ + public function encode($data, $humanReadable = false); + + /** + * Decode the given data from the format + * + * @param string $data + * data sent from client to + * the api in the given format. + * @return array associative array of the parsed data + */ + public function decode($data); + + /** + * @return boolean is parsing the request supported? + */ + public function isReadable(); + + /** + * @return boolean is composing response supported? + */ + public function isWritable(); +} + diff --git a/htdocs/includes/restler/HumanReadableCache.php b/htdocs/includes/restler/HumanReadableCache.php new file mode 100644 index 00000000000..f134605e4d1 --- /dev/null +++ b/htdocs/includes/restler/HumanReadableCache.php @@ -0,0 +1,129 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class HumanReadableCache implements iCache +{ + /** + * @var string path of the folder to hold cache files + */ + public static $cacheDir; + + public function __construct() + { + if (is_null(self::$cacheDir)) { + self::$cacheDir = Defaults::$cacheDirectory; + } + } + + /** + * store data in the cache + * + * @param string $name + * @param mixed $data + * + * @throws \Exception + * @return boolean true if successful + */ + public function set($name, $data) + { + if (is_array($data)) { + $s = '$o = array();' . PHP_EOL . PHP_EOL; + $s .= '// ** THIS IS AN AUTO GENERATED FILE.' + . ' DO NOT EDIT MANUALLY ** '; + foreach ($data as $key => $value) { + $s .= PHP_EOL . PHP_EOL . + "//==================== $key ====================" + . PHP_EOL . PHP_EOL; + if (is_array($value)) { + $s .= '$o[\'' . $key . '\'] = array();'; + foreach ($value as $ke => $va) { + $s .= PHP_EOL . PHP_EOL . "//==== $key $ke ====" + . PHP_EOL . PHP_EOL; + $s .= '$o[\'' . $key . '\'][\'' . $ke . '\'] = ' . + str_replace(' ', ' ', + var_export($va, true)) . ';'; + } + } else { + $s .= '$o[\'' . $key . '\'] = ' + . var_export($value, true) . ';'; + } + } + $s .= PHP_EOL . 'return $o;'; + } else { + $s = 'return ' . var_export($data, true) . ';'; + } + $file = $this->_file($name); + $r = @file_put_contents($file, "throwException(); + } + return $r; + } + + /** + * retrieve data from the cache + * + * @param string $name + * @param bool $ignoreErrors + * + * @return mixed + */ + public function get($name, $ignoreErrors = false) + { + $file = $this->_file($name); + if (file_exists($file)) { + return include($file); + } + } + + /** + * delete data from the cache + * + * @param string $name + * @param bool $ignoreErrors + * + * @return boolean true if successful + */ + public function clear($name, $ignoreErrors = false) + { + return @unlink($this->_file($name)); + } + + /** + * check if the given name is cached + * + * @param string $name + * + * @return boolean true if cached + */ + public function isCached($name) + { + return file_exists($this->_file($name)); + } + + private function _file($name) + { + return self::$cacheDir . '/' . $name . '.php'; + } + + private function throwException() + { + throw new \Exception( + 'The cache directory `' + . self::$cacheDir . '` should exist with write permission.' + ); + } +} + diff --git a/htdocs/includes/restler/README.md b/htdocs/includes/restler/README.md new file mode 100644 index 00000000000..26d47f174bc --- /dev/null +++ b/htdocs/includes/restler/README.md @@ -0,0 +1,8 @@ +Luracast Restler Framework +========================== + +Restler is a simple and effective multi-format Web API Server written in PHP. + +This repository contains just the framework files for installing the framework core using composer require statements. + +For more information, usage examples, pull requests, and issues go to the [Main Repository](https://github.com/Luracast/Restler) \ No newline at end of file diff --git a/htdocs/includes/restler/Redirect.php b/htdocs/includes/restler/Redirect.php new file mode 100644 index 00000000000..7c494bc3c19 --- /dev/null +++ b/htdocs/includes/restler/Redirect.php @@ -0,0 +1,53 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class Redirect +{ + /** + * Redirect to given url + * + * @param string $url relative path or full url + * @param array $params associative array of query parameters + * @param array $flashData associative array of properties to be set in $_SESSION for one time use + * @param int $status http status code to send the response with ideally 301 or 302 + * + * @return array + */ + public static function to($url, array $params = array(), array $flashData = array(), $status = 302) + { + $url = ltrim($url, '/'); + /** @var $r Restler */ + $r = Scope::get('Restler'); + $base = $r->getBaseUrl() . '/'; + if (0 !== strpos($url, 'http')) + $url = $base . $url; + if (!empty($flashData) || $base . $r->url !== $url || Util::getRequestMethod() != 'GET') { + if ($r->responseFormat instanceof JsonFormat) + return array('redirect' => $url); + if (!empty($params)) { + $url .= '?' . http_build_query($params); + } + Flash::set($flashData); + header( + "{$_SERVER['SERVER_PROTOCOL']} $status " . + (isset(RestException::$codes[$status]) ? RestException::$codes[$status] : '') + ); + header("Location: $url"); + die(''); + } + return array(); + } +} \ No newline at end of file diff --git a/htdocs/includes/restler/Resources.php b/htdocs/includes/restler/Resources.php new file mode 100644 index 00000000000..23d2407c486 --- /dev/null +++ b/htdocs/includes/restler/Resources.php @@ -0,0 +1,1006 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class Resources implements iUseAuthentication, iProvideMultiVersionApi +{ + /** + * @var bool should protected resources be shown to unauthenticated users? + */ + public static $hideProtected = true; + /** + * @var bool should we use format as extension? + */ + public static $useFormatAsExtension = true; + /** + * @var bool should we include newer apis in the list? works only when + * Defaults::$useUrlBasedVersioning is set to true; + */ + public static $listHigherVersions = true; + /** + * @var array all http methods specified here will be excluded from + * documentation + */ + public static $excludedHttpMethods = array('OPTIONS'); + /** + * @var array all paths beginning with any of the following will be excluded + * from documentation + */ + public static $excludedPaths = array(); + /** + * @var bool + */ + public static $placeFormatExtensionBeforeDynamicParts = true; + /** + * @var bool should we group all the operations with the same url or not + */ + public static $groupOperations = false; + /** + * @var null|callable if the api methods are under access control mechanism + * you can attach a function here that returns true or false to determine + * visibility of a protected api method. this function will receive method + * info as the only parameter. + */ + public static $accessControlFunction = null; + /** + * @var array type mapping for converting data types to javascript / swagger + */ + public static $dataTypeAlias = array( + 'string' => 'string', + 'int' => 'int', + 'number' => 'float', + 'float' => 'float', + 'bool' => 'boolean', + 'boolean' => 'boolean', + 'NULL' => 'null', + 'array' => 'Array', + 'object' => 'Object', + 'stdClass' => 'Object', + 'mixed' => 'string', + 'DateTime' => 'Date' + ); + /** + * @var array configurable symbols to differentiate public, hybrid and + * protected api + */ + public static $apiDescriptionSuffixSymbols = array( + 0 => '  ', //public api + 1 => '  ', //hybrid api + 2 => '  ', //protected api + ); + + /** + * Injected at runtime + * + * @var Restler instance of restler + */ + public $restler; + /** + * @var string when format is not used as the extension this property is + * used to set the extension manually + */ + public $formatString = ''; + protected $_models; + protected $_bodyParam; + /** + * @var bool|stdClass + */ + protected $_fullDataRequested = false; + protected $crud = array( + 'POST' => 'create', + 'GET' => 'retrieve', + 'PUT' => 'update', + 'DELETE' => 'delete', + 'PATCH' => 'partial update' + ); + protected static $prefixes = array( + 'get' => 'retrieve', + 'index' => 'list', + 'post' => 'create', + 'put' => 'update', + 'patch' => 'modify', + 'delete' => 'remove', + ); + protected $_authenticated = false; + protected $cacheName = ''; + + public function __construct() + { + if (static::$useFormatAsExtension) { + $this->formatString = '.{format}'; + } + } + + /** + * This method will be called first for filter classes and api classes so + * that they can respond accordingly for filer method call and api method + * calls + * + * + * @param bool $isAuthenticated passes true when the authentication is + * done, false otherwise + * + * @return mixed + */ + public function __setAuthenticationStatus($isAuthenticated = false) + { + $this->_authenticated = $isAuthenticated; + } + + /** + * pre call for get($id) + * + * if cache is present, use cache + */ + public function _pre_get_json($id) + { + $userClass = Defaults::$userIdentifierClass; + $this->cacheName = $userClass::getCacheIdentifier() . '_resources_' . $id; + if ($this->restler->getProductionMode() + && !$this->restler->refreshCache + && $this->restler->cache->isCached($this->cacheName) + ) { + //by pass call, compose, postCall stages and directly send response + $this->restler->composeHeaders(); + die($this->restler->cache->get($this->cacheName)); + } + } + + /** + * post call for get($id) + * + * create cache if in production mode + * + * @param $responseData + * + * @internal param string $data composed json output + * + * @return string + */ + public function _post_get_json($responseData) + { + if ($this->restler->getProductionMode()) { + $this->restler->cache->set($this->cacheName, $responseData); + } + return $responseData; + } + + /** + * @access hybrid + * + * @param string $id + * + * @throws RestException + * @return null|stdClass + * + * @url GET {id} + */ + public function get($id = '') + { + $version = $this->restler->getRequestedApiVersion(); + if (empty($id)) { + //do nothing + } elseif (false !== ($pos = strpos($id, '-v'))) { + //$version = intval(substr($id, $pos + 2)); + $id = substr($id, 0, $pos); + } elseif ($id{0} == 'v' && is_numeric($v = substr($id, 1))) { + $id = ''; + //$version = $v; + } elseif ($id == 'root' || $id == 'index') { + $id = ''; + } + $this->_models = new stdClass(); + $r = null; + $count = 0; + + $tSlash = !empty($id); + $target = empty($id) ? '' : $id; + $tLen = strlen($target); + + $filter = array(); + + $routes + = Util::nestedValue(Routes::toArray(), "v$version") + ? : array(); + + $prefix = Defaults::$useUrlBasedVersioning ? "/v$version" : ''; + + foreach ($routes as $value) { + foreach ($value as $httpMethod => $route) { + if (in_array($httpMethod, static::$excludedHttpMethods)) { + continue; + } + $fullPath = $route['url']; + if ($fullPath !== $target && !String::beginsWith($fullPath, $target)) { + continue; + } + $fLen = strlen($fullPath); + if ($tSlash) { + if ($fLen != $tLen && !String::beginsWith($fullPath, $target . '/')) + continue; + } elseif ($fLen > $tLen + 1 && $fullPath{$tLen + 1} != '{' && !String::beginsWith($fullPath, '{')) { + //when mapped to root exclude paths that have static parts + //they are listed else where under that static part name + continue; + } + + if (!static::verifyAccess($route)) { + continue; + } + foreach (static::$excludedPaths as $exclude) { + if (empty($exclude)) { + if ($fullPath == $exclude) + continue 2; + } elseif (String::beginsWith($fullPath, $exclude)) { + continue 2; + } + } + $m = $route['metadata']; + if ($id == '' && $m['resourcePath'] != '') { + continue; + } + if (isset($filter[$httpMethod][$fullPath])) { + continue; + } + $filter[$httpMethod][$fullPath] = true; + // reset body params + $this->_bodyParam = array( + 'required' => false, + 'description' => array() + ); + $count++; + $className = $this->_noNamespace($route['className']); + if (!$r) { + $resourcePath = '/' + . trim($m['resourcePath'], '/'); + $r = $this->_operationListing($resourcePath); + } + $parts = explode('/', $fullPath); + $pos = count($parts) - 1; + if (count($parts) == 1 && $httpMethod == 'GET') { + } else { + for ($i = 0; $i < count($parts); $i++) { + if (strlen($parts[$i]) && $parts[$i]{0} == '{') { + $pos = $i - 1; + break; + } + } + } + $nickname = $this->_nickname($route); + $index = static::$placeFormatExtensionBeforeDynamicParts && $pos > 0 ? $pos : 0; + if (!empty($parts[$index])) + $parts[$index] .= $this->formatString; + + $fullPath = implode('/', $parts); + $description = isset( + $m['classDescription']) + ? $m['classDescription'] + : $className . ' API'; + if (empty($m['description'])) { + $m['description'] = $this->restler->getProductionMode() + ? '' + : 'routes to ' + . $route['className'] + . '::' + . $route['methodName'] . '();'; + } + if (empty($m['longDescription'])) { + $m['longDescription'] = $this->restler->getProductionMode() + ? '' + : 'Add PHPDoc long description to ' + . "$className::" + . $route['methodName'] . '();' + . ' (the api method) to write here'; + } + $operation = $this->_operation( + $route, + $nickname, + $httpMethod, + $m['description'], + $m['longDescription'] + ); + if (isset($m['throws'])) { + foreach ($m['throws'] as $exception) { + $operation->errorResponses[] = array( + 'reason' => $exception['reason'], + 'code' => $exception['code']); + } + } + if (isset($m['param'])) { + foreach ($m['param'] as $param) { + //combine body params as one + $p = $this->_parameter($param); + if ($p->paramType == 'body') { + $this->_appendToBody($p); + } else { + $operation->parameters[] = $p; + } + } + } + if ( + count($this->_bodyParam['description']) || + ( + $this->_fullDataRequested && + $httpMethod != 'GET' && + $httpMethod != 'DELETE' + ) + ) { + $operation->parameters[] = $this->_getBody(); + } + if (isset($m['return']['type'])) { + $responseClass = $m['return']['type']; + if (is_string($responseClass)) { + if (class_exists($responseClass)) { + $this->_model($responseClass); + $operation->responseClass + = $this->_noNamespace($responseClass); + } elseif (strtolower($responseClass) == 'array') { + $operation->responseClass = 'Array'; + $rt = $m['return']; + if (isset( + $rt[CommentParser::$embeddedDataName]['type']) + ) { + $rt = $rt[CommentParser::$embeddedDataName] + ['type']; + if (class_exists($rt)) { + $this->_model($rt); + $operation->responseClass .= '[' . + $this->_noNamespace($rt) . ']'; + } + } + } + } + } + $api = false; + + if (static::$groupOperations) { + foreach ($r->apis as $a) { + if ($a->path == "/$fullPath") { + $api = $a; + break; + } + } + } + + if (!$api) { + $api = $this->_api("$prefix/$fullPath", $description); + $r->apis[] = $api; + } + + $api->operations[] = $operation; + } + } + if (!$count) { + throw new RestException(404); + } + if (!is_null($r)) + $r->models = $this->_models; + usort( + $r->apis, + function ($a, $b) { + $order = array( + 'GET' => 1, + 'POST' => 2, + 'PUT' => 3, + 'PATCH' => 4, + 'DELETE' => 5 + ); + return + $a->operations[0]->httpMethod == + $b->operations[0]->httpMethod + ? $a->path > $b->path + : $order[$a->operations[0]->httpMethod] > + $order[$b->operations[0]->httpMethod]; + + } + ); + return $r; + } + + protected function _nickname(array $route) + { + static $hash = array(); + $method = $route['methodName']; + if (isset(static::$prefixes[$method])) { + $method = static::$prefixes[$method]; + } else { + $method = str_replace( + array_keys(static::$prefixes), + array_values(static::$prefixes), + $method + ); + } + while (isset($hash[$method]) && $route['url'] != $hash[$method]) { + //create another one + $method .= '_'; + } + $hash[$method] = $route['url']; + return $method; + } + + protected function _noNamespace($className) + { + $className = explode('\\', $className); + return end($className); + } + + protected function _operationListing($resourcePath = '/') + { + $r = $this->_resourceListing(); + $r->resourcePath = $resourcePath; + $r->models = new stdClass(); + return $r; + } + + protected function _resourceListing() + { + $r = new stdClass(); + $r->apiVersion = (string)$this->restler->_requestedApiVersion; + $r->swaggerVersion = "1.1"; + $r->basePath = $this->restler->getBaseUrl(); + $r->produces = $this->restler->getWritableMimeTypes(); + $r->consumes = $this->restler->getReadableMimeTypes(); + $r->apis = array(); + return $r; + } + + protected function _api($path, $description = '') + { + $r = new stdClass(); + $r->path = $path; + $r->description = + empty($description) && $this->restler->getProductionMode() + ? 'Use PHPDoc comment to describe here' + : $description; + $r->operations = array(); + return $r; + } + + protected function _operation( + $route, + $nickname, + $httpMethod = 'GET', + $summary = 'description', + $notes = 'long description', + $responseClass = 'void' + ) + { + //reset body params + $this->_bodyParam = array( + 'required' => false, + 'description' => array() + ); + + $r = new stdClass(); + $r->httpMethod = $httpMethod; + $r->nickname = $nickname; + $r->responseClass = $responseClass; + + $r->parameters = array(); + + $r->summary = $summary . ($route['accessLevel'] > 2 + ? static::$apiDescriptionSuffixSymbols[2] + : static::$apiDescriptionSuffixSymbols[$route['accessLevel']] + ); + $r->notes = $notes; + + $r->errorResponses = array(); + return $r; + } + + protected function _parameter($param) + { + $r = new stdClass(); + $r->name = $param['name']; + $r->description = !empty($param['description']) + ? $param['description'] . '.' + : ($this->restler->getProductionMode() + ? '' + : 'add @param {type} $' . $r->name + . ' {comment} to describe here'); + //paramType can be path or query or body or header + $r->paramType = Util::nestedValue($param, CommentParser::$embeddedDataName, 'from') ? : 'query'; + $r->required = isset($param['required']) && $param['required']; + if (isset($param['default'])) { + $r->defaultValue = $param['default']; + } elseif (isset($param[CommentParser::$embeddedDataName]['example'])) { + $r->defaultValue + = $param[CommentParser::$embeddedDataName]['example']; + } + $r->allowMultiple = false; + $type = 'string'; + if (isset($param['type'])) { + $type = $param['type']; + if (is_array($type)) { + $type = array_shift($type); + } + if ($type == 'array') { + $contentType = Util::nestedValue( + $param, + CommentParser::$embeddedDataName, + 'type' + ); + if ($contentType) { + if ($contentType == 'indexed') { + $type = 'Array'; + } elseif ($contentType == 'associative') { + $type = 'Object'; + } else { + $type = "Array[$contentType]"; + } + if (Util::isObjectOrArray($contentType)) { + $this->_model($contentType); + } + } elseif (isset(static::$dataTypeAlias[$type])) { + $type = static::$dataTypeAlias[$type]; + } + } elseif (Util::isObjectOrArray($type)) { + $this->_model($type); + } elseif (isset(static::$dataTypeAlias[$type])) { + $type = static::$dataTypeAlias[$type]; + } + } + $r->dataType = $type; + if (isset($param[CommentParser::$embeddedDataName])) { + $p = $param[CommentParser::$embeddedDataName]; + if (isset($p['min']) && isset($p['max'])) { + $r->allowableValues = array( + 'valueType' => 'RANGE', + 'min' => $p['min'], + 'max' => $p['max'], + ); + } elseif (isset($p['choice'])) { + $r->allowableValues = array( + 'valueType' => 'LIST', + 'values' => $p['choice'] + ); + } + } + return $r; + } + + protected function _appendToBody($p) + { + if ($p->name === Defaults::$fullRequestDataName) { + $this->_fullDataRequested = $p; + unset($this->_bodyParam['names'][Defaults::$fullRequestDataName]); + return; + } + $this->_bodyParam['description'][$p->name] + = "$p->name" + . ' : ' . $p->dataType . ' ' + . ($p->required ? ' (required) - ' : ' - ') + . $p->description; + $this->_bodyParam['required'] = $p->required + || $this->_bodyParam['required']; + $this->_bodyParam['names'][$p->name] = $p; + } + + protected function _getBody() + { + $r = new stdClass(); + $n = isset($this->_bodyParam['names']) + ? array_values($this->_bodyParam['names']) + : array(); + if (count($n) == 1) { + if (isset($this->_models->{$n[0]->dataType})) { + // ============ custom class =================== + $r = $n[0]; + $c = $this->_models->{$r->dataType}; + $a = $c->properties; + $r->description = "Paste JSON data here"; + if (count($a)) { + $r->description .= " with the following" + . (count($a) > 1 ? ' properties.' : ' property.'); + foreach ($a as $k => $v) { + $r->description .= "
$k : " + . $v['type'] . ' ' + . (isset($v['required']) ? '(required)' : '') + . ' - ' . $v['description']; + } + } + $r->defaultValue = "{\n \"" + . implode("\": \"\",\n \"", + array_keys($c->properties)) + . "\": \"\"\n}"; + return $r; + } elseif (false !== ($p = strpos($n[0]->dataType, '['))) { + // ============ array of custom class =============== + $r = $n[0]; + $t = substr($r->dataType, $p + 1, -1); + if ($c = Util::nestedValue($this->_models, $t)) { + $a = $c->properties; + $r->description = "Paste JSON data here"; + if (count($a)) { + $r->description .= " with an array of objects with the following" + . (count($a) > 1 ? ' properties.' : ' property.'); + foreach ($a as $k => $v) { + $r->description .= "
$k : " + . $v['type'] . ' ' + . (isset($v['required']) ? '(required)' : '') + . ' - ' . $v['description']; + } + } + $r->defaultValue = "[\n {\n \"" + . implode("\": \"\",\n \"", + array_keys($c->properties)) + . "\": \"\"\n }\n]"; + return $r; + } else { + $r->description = "Paste JSON data here with an array of $t values."; + $r->defaultValue = "[ ]"; + return $r; + } + } elseif ($n[0]->dataType == 'Array') { + // ============ array =============================== + $r = $n[0]; + $r->description = "Paste JSON array data here" + . ($r->required ? ' (required) . ' : '. ') + . "
$r->description"; + $r->defaultValue = "[\n {\n \"" + . "property\" : \"\"\n }\n]"; + return $r; + } elseif ($n[0]->dataType == 'Object') { + // ============ object ============================== + $r = $n[0]; + $r->description = "Paste JSON object data here" + . ($r->required ? ' (required) . ' : '. ') + . "
$r->description"; + $r->defaultValue = "{\n \"" + . "property\" : \"\"\n}"; + return $r; + } + } + $p = array_values($this->_bodyParam['description']); + $r->name = 'REQUEST_BODY'; + $r->description = "Paste JSON data here"; + if (count($p) == 0 && $this->_fullDataRequested) { + $r->required = $this->_fullDataRequested->required; + $r->defaultValue = "{\n \"property\" : \"\"\n}"; + } else { + $r->description .= " with the following" + . (count($p) > 1 ? ' properties.' : ' property.') + . '
' + . implode("
", $p); + $r->required = $this->_bodyParam['required']; + // Create default object that includes parameters to be submitted + $defaultObject = new \StdClass(); + foreach ($this->_bodyParam['names'] as $name => $values) { + if (!$values->required) + continue; + if (class_exists($values->dataType)) { + $myClassName = $values->dataType; + $defaultObject->$name = new $myClassName(); + } else { + $defaultObject->$name = ''; + } + } + $r->defaultValue = Scope::get('JsonFormat')->encode($defaultObject, true); + } + $r->paramType = 'body'; + $r->allowMultiple = false; + $r->dataType = 'Object'; + return $r; + } + + protected function _model($className, $instance = null) + { + $id = $this->_noNamespace($className); + if (isset($this->_models->{$id})) { + return; + } + $properties = array(); + if (!$instance) { + if (!class_exists($className)) + return; + $instance = new $className(); + } + $data = get_object_vars($instance); + $reflectionClass = new \ReflectionClass($className); + foreach ($data as $key => $value) { + + $propertyMetaData = null; + + try { + $property = $reflectionClass->getProperty($key); + if ($c = $property->getDocComment()) { + $propertyMetaData = Util::nestedValue( + CommentParser::parse($c), + 'var' + ); + } + } catch (\ReflectionException $e) { + } + + if (is_null($propertyMetaData)) { + $type = $this->getType($value, true); + $description = ''; + } else { + $type = Util::nestedValue( + $propertyMetaData, + 'type' + ) ? : $this->getType($value, true); + $description = Util::nestedValue( + $propertyMetaData, + 'description' + ) ? : ''; + + if (class_exists($type)) { + $this->_model($type); + $type = $this->_noNamespace($type); + } + } + + if (isset(static::$dataTypeAlias[$type])) { + $type = static::$dataTypeAlias[$type]; + } + $properties[$key] = array( + 'type' => $type, + 'description' => $description + ); + if (Util::nestedValue( + $propertyMetaData, + CommentParser::$embeddedDataName, + 'required' + ) + ) { + $properties[$key]['required'] = true; + } + if ($type == 'Array') { + $itemType = Util::nestedValue( + $propertyMetaData, + CommentParser::$embeddedDataName, + 'type' + ) ? : + (count($value) + ? $this->getType(end($value), true) + : 'string'); + if (class_exists($itemType)) { + $this->_model($itemType); + $itemType = $this->_noNamespace($itemType); + } + $properties[$key]['item'] = array( + 'type' => $itemType, + /*'description' => '' */ //TODO: add description + ); + } else if (preg_match('/^Array\[(.+)\]$/', $type, $matches)) { + $itemType = $matches[1]; + $properties[$key]['type'] = 'Array'; + $properties[$key]['item']['type'] = $itemType; + + if (class_exists($itemType)) { + $this->_model($itemType); + } + } + } + if (!empty($properties)) { + $model = new stdClass(); + $model->id = $id; + $model->properties = $properties; + $this->_models->{$id} = $model; + } + } + + /** + * Find the data type of the given value. + * + * + * @param mixed $o given value for finding type + * + * @param bool $appendToModels if an object is found should we append to + * our models list? + * + * @return string + * + * @access private + */ + public function getType($o, $appendToModels = false) + { + if (is_object($o)) { + $oc = get_class($o); + if ($appendToModels) { + $this->_model($oc, $o); + } + return $this->_noNamespace($oc); + } + if (is_array($o)) { + if (count($o)) { + $child = end($o); + if (Util::isObjectOrArray($child)) { + $childType = $this->getType($child, $appendToModels); + return "Array[$childType]"; + } + } + return 'array'; + } + if (is_bool($o)) return 'boolean'; + if (is_numeric($o)) return is_float($o) ? 'float' : 'int'; + return 'string'; + } + + /** + * pre call for index() + * + * if cache is present, use cache + */ + public function _pre_index_json() + { + $userClass = Defaults::$userIdentifierClass; + $this->cacheName = $userClass::getCacheIdentifier() + . '_resources-v' + . $this->restler->_requestedApiVersion; + if ($this->restler->getProductionMode() + && !$this->restler->refreshCache + && $this->restler->cache->isCached($this->cacheName) + ) { + //by pass call, compose, postCall stages and directly send response + $this->restler->composeHeaders(); + die($this->restler->cache->get($this->cacheName)); + } + } + + /** + * post call for index() + * + * create cache if in production mode + * + * @param $responseData + * + * @internal param string $data composed json output + * + * @return string + */ + public function _post_index_json($responseData) + { + if ($this->restler->getProductionMode()) { + $this->restler->cache->set($this->cacheName, $responseData); + } + return $responseData; + } + + /** + * @access hybrid + * @return \stdClass + */ + public function index() + { + if (!static::$accessControlFunction && Defaults::$accessControlFunction) + static::$accessControlFunction = Defaults::$accessControlFunction; + $version = $this->restler->getRequestedApiVersion(); + $allRoutes = Util::nestedValue(Routes::toArray(), "v$version"); + $r = $this->_resourceListing(); + $map = array(); + if (isset($allRoutes['*'])) { + $this->_mapResources($allRoutes['*'], $map, $version); + unset($allRoutes['*']); + } + $this->_mapResources($allRoutes, $map, $version); + foreach ($map as $path => $description) { + if (!String::contains($path, '{')) { + //add id + $r->apis[] = array( + 'path' => $path . $this->formatString, + 'description' => $description + ); + } + } + if (Defaults::$useUrlBasedVersioning && static::$listHigherVersions) { + $nextVersion = $version + 1; + if ($nextVersion <= $this->restler->getApiVersion()) { + list($status, $data) = $this->_loadResource("/v$nextVersion/resources.json"); + if ($status == 200) { + $r->apis = array_merge($r->apis, $data->apis); + $r->apiVersion = $data->apiVersion; + } + } + + } + return $r; + } + + protected function _loadResource($url) + { + $ch = curl_init($this->restler->getBaseUrl() . $url + . (empty($_GET) ? '' : '?' . http_build_query($_GET))); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); + curl_setopt($ch, CURLOPT_TIMEOUT, 15); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, array( + 'Accept:application/json', + )); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); + $result = json_decode(curl_exec($ch)); + $http_status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); + return array($http_status, $result); + } + + protected function _mapResources(array $allRoutes, array &$map, $version = 1) + { + foreach ($allRoutes as $fullPath => $routes) { + $path = explode('/', $fullPath); + $resource = isset($path[0]) ? $path[0] : ''; + if ($resource == 'resources' || String::endsWith($resource, 'index')) + continue; + foreach ($routes as $httpMethod => $route) { + if (in_array($httpMethod, static::$excludedHttpMethods)) { + continue; + } + if (!static::verifyAccess($route)) { + continue; + } + + foreach (static::$excludedPaths as $exclude) { + if (empty($exclude)) { + if ($fullPath == $exclude) + continue 2; + } elseif (String::beginsWith($fullPath, $exclude)) { + continue 2; + } + } + + $res = $resource + ? ($version == 1 ? "/resources/$resource" : "/v$version/resources/$resource-v$version") + : ($version == 1 ? "/resources/root" : "/v$version/resources/root-v$version"); + + if (empty($map[$res])) { + $map[$res] = isset( + $route['metadata']['classDescription']) + ? $route['metadata']['classDescription'] : ''; + } + } + } + } + + /** + * Maximum api version supported by the api class + * @return int + */ + public static function __getMaximumSupportedVersion() + { + return Scope::get('Restler')->getApiVersion(); + } + + /** + * Verifies that the requesting user is allowed to view the docs for this API + * + * @param $route + * + * @return boolean True if the user should be able to view this API's docs + */ + protected function verifyAccess($route) + { + if ($route['accessLevel'] < 2) { + return true; + } + if ( + static::$hideProtected + && !$this->_authenticated + && $route['accessLevel'] > 1 + ) { + return false; + } + if ($this->_authenticated + && static::$accessControlFunction + && (!call_user_func( + static::$accessControlFunction, $route['metadata'])) + ) { + return false; + } + return true; + } +} diff --git a/htdocs/includes/restler/RestException.php b/htdocs/includes/restler/RestException.php new file mode 100644 index 00000000000..763ec8361f1 --- /dev/null +++ b/htdocs/includes/restler/RestException.php @@ -0,0 +1,138 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + */ + +class RestException extends Exception +{ + /** + * HTTP status codes + * + * @var array + */ + public static $codes = array( + 100 => 'Continue', + 101 => 'Switching Protocols', + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative Information', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 306 => '(Unused)', + 307 => 'Temporary Redirect', + 400 => 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Timeout', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Request Entity Too Large', + 414 => 'Request-URI Too Long', + 415 => 'Unsupported Media Type', + 416 => 'Requested Range Not Satisfiable', + 417 => 'Expectation Failed', + 429 => 'Too Many Requests', //still in draft but used for rate limiting + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Timeout', + 505 => 'HTTP Version Not Supported' + ); + private $details; + private $stage; + + /** + * @param string $httpStatusCode http status code + * @param string|null $errorMessage error message + * @param array $details any extra detail about the exception + * @param Exception $previous previous exception if any + */ + public function __construct($httpStatusCode, $errorMessage = null, array $details = array(), Exception $previous = null) + { + $events = Scope::get('Restler')->getEvents(); + if(count($events)<= 1){ + $this->stage = 'setup'; + } else { + $this->stage = $previous ? $events[count($events)-2] : end($events); + } + $this->details = $details; + parent::__construct($errorMessage, $httpStatusCode, $previous); + } + + /** + * Get extra details about the exception + * + * @return array details array + */ + public function getDetails() + { + return $this->details; + } + + public function getStage() + { + return $this->stage; + } + + public function getStages() + { + $e = Scope::get('Restler')->getEvents(); + $i = array_search($this->stage, $e); + return array( + 'success' => array_slice($e, 0, $i), + 'failure' => array_slice($e, $i), + ); + } + + public function getErrorMessage() + { + $statusCode = $this->getCode(); + $message = $this->getMessage(); + if (isset(RestException::$codes[$statusCode])) { + $message = RestException::$codes[$statusCode] . + (empty($message) ? '' : ': ' . $message); + } + return $message; + } + + public function getSource() + { + $e = $this; + while ($e->getPrevious()) { + $e = $e->getPrevious(); + } + return basename($e->getFile()) . ':' + . $e->getLine() . ' at ' + . $this->getStage() . ' stage'; + } +} + diff --git a/htdocs/includes/restler/Restler.php b/htdocs/includes/restler/Restler.php new file mode 100644 index 00000000000..99c00a89ed3 --- /dev/null +++ b/htdocs/includes/restler/Restler.php @@ -0,0 +1,1451 @@ + + * + * @category Framework + * @package Restler + * @author R.Arul Kumaran + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class Restler extends EventDispatcher +{ + const VERSION = '3.0.0rc5'; + + // ================================================================== + // + // Public variables + // + // ------------------------------------------------------------------ + /** + * Reference to the last exception thrown + * @var RestException + */ + public $exception = null; + /** + * Used in production mode to store the routes and more + * + * @var iCache + */ + public $cache; + /** + * URL of the currently mapped service + * + * @var string + */ + public $url; + /** + * Http request method of the current request. + * Any value between [GET, PUT, POST, DELETE] + * + * @var string + */ + public $requestMethod; + /** + * Requested data format. + * Instance of the current format class + * which implements the iFormat interface + * + * @var iFormat + * @example jsonFormat, xmlFormat, yamlFormat etc + */ + public $requestFormat; + /** + * Response data format. + * + * Instance of the current format class + * which implements the iFormat interface + * + * @var iFormat + * @example jsonFormat, xmlFormat, yamlFormat etc + */ + public $responseFormat; + /** + * Http status code + * + * @var int + */ + public $responseCode=200; + /** + * @var string base url of the api service + */ + protected $baseUrl; + /** + * @var bool Used for waiting till verifying @format + * before throwing content negotiation failed + */ + protected $requestFormatDiffered = false; + /** + * method information including metadata + * + * @var ApiMethodInfo + */ + public $apiMethodInfo; + /** + * @var int for calculating execution time + */ + protected $startTime; + /** + * When set to false, it will run in debug mode and parse the + * class files every time to map it to the URL + * + * @var boolean + */ + protected $productionMode = false; + public $refreshCache = false; + /** + * Caching of url map is enabled or not + * + * @var boolean + */ + protected $cached; + /** + * @var int + */ + protected $apiVersion = 1; + /** + * @var int + */ + protected $requestedApiVersion = 1; + /** + * @var int + */ + protected $apiMinimumVersion = 1; + /** + * @var array + */ + protected $apiVersionMap = array(); + /** + * Associated array that maps formats to their respective format class name + * + * @var array + */ + protected $formatMap = array(); + /** + * List of the Mime Types that can be produced as a response by this API + * + * @var array + */ + protected $writableMimeTypes = array(); + /** + * List of the Mime Types that are supported for incoming requests by this API + * + * @var array + */ + protected $readableMimeTypes = array(); + /** + * Associated array that maps formats to their respective format class name + * + * @var array + */ + protected $formatOverridesMap = array('extensions' => array()); + /** + * list of filter classes + * + * @var array + */ + protected $filterClasses = array(); + /** + * instances of filter classes that are executed after authentication + * + * @var array + */ + protected $postAuthFilterClasses = array(); + + + // ================================================================== + // + // Protected variables + // + // ------------------------------------------------------------------ + + /** + * Data sent to the service + * + * @var array + */ + protected $requestData = array(); + /** + * list of authentication classes + * + * @var array + */ + protected $authClasses = array(); + /** + * list of error handling classes + * + * @var array + */ + protected $errorClasses = array(); + protected $authenticated = false; + protected $authVerified = false; + /** + * @var mixed + */ + protected $responseData; + + /** + * Constructor + * + * @param boolean $productionMode When set to false, it will run in + * debug mode and parse the class files + * every time to map it to the URL + * + * @param bool $refreshCache will update the cache when set to true + */ + public function __construct($productionMode = false, $refreshCache = false) + { + parent::__construct(); + $this->startTime = time(); + Util::$restler = $this; + Scope::set('Restler', $this); + $this->productionMode = $productionMode; + if (is_null(Defaults::$cacheDirectory)) { + Defaults::$cacheDirectory = dirname($_SERVER['SCRIPT_FILENAME']) . + DIRECTORY_SEPARATOR . 'cache'; + } + $this->cache = new Defaults::$cacheClass(); + $this->refreshCache = $refreshCache; + // use this to rebuild cache every time in production mode + if ($productionMode && $refreshCache) { + $this->cached = false; + } + } + + /** + * Main function for processing the api request + * and return the response + * + * @throws Exception when the api service class is missing + * @throws RestException to send error response + */ + public function handle() + { + try { + try { + try { + $this->get(); + } catch (Exception $e) { + $this->requestData + = array(Defaults::$fullRequestDataName => array()); + if (!$e instanceof RestException) { + $e = new RestException( + 500, + $this->productionMode ? null : $e->getMessage(), + array(), + $e + ); + } + $this->route(); + throw $e; + } + if (Defaults::$useVendorMIMEVersioning) + $this->responseFormat = $this->negotiateResponseFormat(); + $this->route(); + } catch (Exception $e) { + $this->negotiate(); + if (!$e instanceof RestException) { + $e = new RestException( + 500, + $this->productionMode ? null : $e->getMessage(), + array(), + $e + ); + } + throw $e; + } + $this->negotiate(); + $this->preAuthFilter(); + $this->authenticate(); + $this->postAuthFilter(); + $this->validate(); + $this->preCall(); + $this->call(); + $this->compose(); + $this->postCall(); + $this->respond(); + } catch (Exception $e) { + try{ + $this->message($e); + } catch (Exception $e2) { + $this->message($e2); + } + } + } + + /** + * read the request details + * + * Find out the following + * - baseUrl + * - url requested + * - version requested (if url based versioning) + * - http verb/method + * - negotiate content type + * - request data + * - set defaults + */ + protected function get() + { + $this->dispatch('get'); + if (empty($this->formatMap)) { + $this->setSupportedFormats('JsonFormat'); + } + $this->url = $this->getPath(); + $this->requestMethod = Util::getRequestMethod(); + $this->requestFormat = $this->getRequestFormat(); + $this->requestData = $this->getRequestData(false); + + //parse defaults + foreach ($_GET as $key => $value) { + if (isset(Defaults::$aliases[$key])) { + $_GET[Defaults::$aliases[$key]] = $value; + unset($_GET[$key]); + $key = Defaults::$aliases[$key]; + } + if (in_array($key, Defaults::$overridables)) { + Defaults::setProperty($key, $value); + } + } + } + + /** + * Returns a list of the mime types (e.g. ["application/json","application/xml"]) that the API can respond with + * @return array + */ + public function getWritableMimeTypes() + { + return $this->writableMimeTypes; + } + + /** + * Returns the list of Mime Types for the request that the API can understand + * @return array + */ + public function getReadableMimeTypes() + { + return $this->readableMimeTypes; + } + + /** + * Call this method and pass all the formats that should be supported by + * the API Server. Accepts multiple parameters + * + * @param string ,... $formatName class name of the format class that + * implements iFormat + * + * @example $restler->setSupportedFormats('JsonFormat', 'XmlFormat'...); + * @throws Exception + */ + public function setSupportedFormats($format = null /*[, $format2...$farmatN]*/) + { + $args = func_get_args(); + $extensions = array(); + $throwException = $this->requestFormatDiffered; + $this->writableMimeTypes = $this->readableMimeTypes = array(); + foreach ($args as $className) { + + $obj = Scope::get($className); + + if (!$obj instanceof iFormat) + throw new Exception('Invalid format class; must implement ' . + 'iFormat interface'); + if ($throwException && get_class($obj) == get_class($this->requestFormat)) { + $throwException = false; + } + + foreach ($obj->getMIMEMap() as $mime => $extension) { + if($obj->isWritable()){ + $this->writableMimeTypes[]=$mime; + $extensions[".$extension"] = true; + } + if($obj->isReadable()) + $this->readableMimeTypes[]=$mime; + if (!isset($this->formatMap[$extension])) + $this->formatMap[$extension] = $className; + if (!isset($this->formatMap[$mime])) + $this->formatMap[$mime] = $className; + } + } + if ($throwException) { + throw new RestException( + 403, + 'Content type `' . $this->requestFormat->getMIME() . '` is not supported.' + ); + } + $this->formatMap['default'] = $args[0]; + $this->formatMap['extensions'] = array_keys($extensions); + } + + /** + * Call this method and pass all the formats that can be used to override + * the supported formats using `@format` comment. Accepts multiple parameters + * + * @param string ,... $formatName class name of the format class that + * implements iFormat + * + * @example $restler->setOverridingFormats('JsonFormat', 'XmlFormat'...); + * @throws Exception + */ + public function setOverridingFormats($format = null /*[, $format2...$farmatN]*/) + { + $args = func_get_args(); + $extensions = array(); + foreach ($args as $className) { + + $obj = Scope::get($className); + + if (!$obj instanceof iFormat) + throw new Exception('Invalid format class; must implement ' . + 'iFormat interface'); + + foreach ($obj->getMIMEMap() as $mime => $extension) { + if (!isset($this->formatOverridesMap[$extension])) + $this->formatOverridesMap[$extension] = $className; + if (!isset($this->formatOverridesMap[$mime])) + $this->formatOverridesMap[$mime] = $className; + if($obj->isWritable()) + $extensions[".$extension"] = true; + } + } + $this->formatOverridesMap['extensions'] = array_keys($extensions); + } + + /** + * Parses the request url and get the api path + * + * @return string api path + */ + protected function getPath() + { + // fix SCRIPT_NAME for PHP 5.4 built-in web server + if (false === strpos($_SERVER['SCRIPT_NAME'], '.php')) + $_SERVER['SCRIPT_NAME'] + = '/' . Util::removeCommonPath($_SERVER['SCRIPT_FILENAME'], $_SERVER['DOCUMENT_ROOT']); + + $fullPath = urldecode($_SERVER['REQUEST_URI']); + $path = Util::removeCommonPath( + $fullPath, + $_SERVER['SCRIPT_NAME'] + ); + $port = isset($_SERVER['SERVER_PORT']) ? $_SERVER['SERVER_PORT'] : '80'; + $https = $port == '443' || + (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https') || // Amazon ELB + (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on'); + + $baseUrl = ($https ? 'https://' : 'http://') . $_SERVER['SERVER_NAME']; + + if (!$https && $port != '80' || $https && $port != '443') + $baseUrl .= ':' . $port; + + $this->baseUrl = rtrim($baseUrl + . substr($fullPath, 0, strlen($fullPath) - strlen($path)), '/'); + + $path = rtrim(strtok($path, '?'), '/'); //remove query string and trailing slash if found any + $path = str_replace( + array_merge( + $this->formatMap['extensions'], + $this->formatOverridesMap['extensions'] + ), + '', + $path + ); + if (Defaults::$useUrlBasedVersioning && strlen($path) && $path{0} == 'v') { + $version = intval(substr($path, 1)); + if ($version && $version <= $this->apiVersion) { + $this->requestedApiVersion = $version; + $path = explode('/', $path, 2); + $path = $path[1]; + } + } else { + $this->requestedApiVersion = $this->apiMinimumVersion; + } + return $path; + } + + /** + * Parses the request to figure out format of the request data + * + * @throws RestException + * @return iFormat any class that implements iFormat + * @example JsonFormat + */ + protected function getRequestFormat() + { + $format = null ; + // check if client has sent any information on request format + if ( + !empty($_SERVER['CONTENT_TYPE']) || + ( + !empty($_SERVER['HTTP_CONTENT_TYPE']) && + $_SERVER['CONTENT_TYPE'] = $_SERVER['HTTP_CONTENT_TYPE'] + ) + ) { + $mime = $_SERVER['CONTENT_TYPE']; + if (false !== $pos = strpos($mime, ';')) { + $mime = substr($mime, 0, $pos); + } + if ($mime == UrlEncodedFormat::MIME) + $format = Scope::get('UrlEncodedFormat'); + elseif (isset($this->formatMap[$mime])) { + $format = Scope::get($this->formatMap[$mime]); + $format->setMIME($mime); + } elseif (!$this->requestFormatDiffered && isset($this->formatOverridesMap[$mime])) { + //if our api method is not using an @format comment + //to point to this $mime, we need to throw 403 as in below + //but since we don't know that yet, we need to defer that here + $format = Scope::get($this->formatOverridesMap[$mime]); + $format->setMIME($mime); + $this->requestFormatDiffered = true; + } else { + throw new RestException( + 403, + "Content type `$mime` is not supported." + ); + } + } + if(!$format){ + $format = Scope::get($this->formatMap['default']); + } + return $format; + } + + public function getRequestStream() + { + static $tempStream = false; + if (!$tempStream) { + $tempStream = fopen('php://temp', 'r+'); + $rawInput = fopen('php://input', 'r'); + stream_copy_to_stream($rawInput, $tempStream); + } + rewind($tempStream); + return $tempStream; + } + + /** + * Parses the request data and returns it + * + * @param bool $includeQueryParameters + * + * @return array php data + */ + public function getRequestData($includeQueryParameters = true) + { + $get = UrlEncodedFormat::decoderTypeFix($_GET); + if ($this->requestMethod == 'PUT' + || $this->requestMethod == 'PATCH' + || $this->requestMethod == 'POST' + ) { + if (!empty($this->requestData)) { + return $includeQueryParameters + ? $this->requestData + $get + : $this->requestData; + } + + $stream = $this->getRequestStream(); + if($stream === FALSE) + return array(); + $r = $this->requestFormat instanceof iDecodeStream + ? $this->requestFormat->decodeStream($stream) + : $this->requestFormat->decode(stream_get_contents($stream)); + + $r = is_array($r) + ? array_merge($r, array(Defaults::$fullRequestDataName => $r)) + : array(Defaults::$fullRequestDataName => $r); + return $includeQueryParameters + ? $r + $get + : $r; + } + return $includeQueryParameters ? $get : array(); //no body + } + + /** + * Find the api method to execute for the requested Url + */ + protected function route() + { + $this->dispatch('route'); + + $params = $this->getRequestData(); + + //backward compatibility for restler 2 and below + if (!Defaults::$smartParameterParsing) { + $params = $params + array(Defaults::$fullRequestDataName => $params); + } + + $this->apiMethodInfo = $o = Routes::find( + $this->url, $this->requestMethod, + $this->requestedApiVersion, $params + ); + //set defaults based on api method comments + if (isset($o->metadata)) { + foreach (Defaults::$fromComments as $key => $defaultsKey) { + if (array_key_exists($key, $o->metadata)) { + $value = $o->metadata[$key]; + Defaults::setProperty($defaultsKey, $value); + } + } + } + if (!isset($o->className)) + throw new RestException(404); + + if(isset($this->apiVersionMap[$o->className])){ + Scope::$classAliases[Util::getShortName($o->className)] + = $this->apiVersionMap[$o->className][$this->requestedApiVersion]; + } + + foreach ($this->authClasses as $auth) { + if (isset($this->apiVersionMap[$auth])) { + Scope::$classAliases[$auth] = $this->apiVersionMap[$auth][$this->requestedApiVersion]; + } elseif (isset($this->apiVersionMap[Scope::$classAliases[$auth]])) { + Scope::$classAliases[$auth] + = $this->apiVersionMap[Scope::$classAliases[$auth]][$this->requestedApiVersion]; + } + } + } + + /** + * Negotiate the response details such as + * - cross origin resource sharing + * - media type + * - charset + * - language + */ + protected function negotiate() + { + $this->dispatch('negotiate'); + $this->negotiateCORS(); + $this->responseFormat = $this->negotiateResponseFormat(); + $this->negotiateCharset(); + $this->negotiateLanguage(); + } + + protected function negotiateCORS() + { + if ( + $this->requestMethod == 'OPTIONS' + && Defaults::$crossOriginResourceSharing + ) { + if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'])) + header('Access-Control-Allow-Methods: ' + . Defaults::$accessControlAllowMethods); + + if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'])) + header('Access-Control-Allow-Headers: ' + . $_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']); + + header('Access-Control-Allow-Origin: ' . + (Defaults::$accessControlAllowOrigin == '*' ? $_SERVER['HTTP_ORIGIN'] : Defaults::$accessControlAllowOrigin)); + header('Access-Control-Allow-Credentials: true'); + + exit(0); + } + } + + // ================================================================== + // + // Protected functions + // + // ------------------------------------------------------------------ + + /** + * Parses the request to figure out the best format for response. + * Extension, if present, overrides the Accept header + * + * @throws RestException + * @return iFormat + * @example JsonFormat + */ + protected function negotiateResponseFormat() + { + $metadata = Util::nestedValue($this, 'apiMethodInfo', 'metadata'); + //check if the api method insists on response format using @format comment + + if ($metadata && isset($metadata['format'])) { + $formats = explode(',', (string)$metadata['format']); + foreach ($formats as $i => $f) { + $f = trim($f); + if (!in_array($f, $this->formatOverridesMap)) + throw new RestException( + 500, + "Given @format is not present in overriding formats. Please call `\$r->setOverridingFormats('$f');` first." + ); + $formats[$i] = $f; + } + call_user_func_array(array($this, 'setSupportedFormats'), $formats); + } + + // check if client has specified an extension + /** @var $format iFormat*/ + $format = null; + $extensions = explode( + '.', + parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) + ); + while ($extensions) { + $extension = array_pop($extensions); + $extension = explode('/', $extension); + $extension = array_shift($extension); + if ($extension && isset($this->formatMap[$extension])) { + $format = Scope::get($this->formatMap[$extension]); + $format->setExtension($extension); + // echo "Extension $extension"; + return $format; + } + } + // check if client has sent list of accepted data formats + if (isset($_SERVER['HTTP_ACCEPT'])) { + $acceptList = Util::sortByPriority($_SERVER['HTTP_ACCEPT']); + foreach ($acceptList as $accept => $quality) { + if (isset($this->formatMap[$accept])) { + $format = Scope::get($this->formatMap[$accept]); + $format->setMIME($accept); + //echo "MIME $accept"; + // Tell cache content is based on Accept header + @header('Vary: Accept'); + + return $format; + } elseif (false !== ($index = strrpos($accept, '+'))) { + $mime = substr($accept, 0, $index); + if (is_string(Defaults::$apiVendor) + && 0 === stripos($mime, + 'application/vnd.' + . Defaults::$apiVendor . '-v') + ) { + $extension = substr($accept, $index + 1); + if (isset($this->formatMap[$extension])) { + //check the MIME and extract version + $version = intval(substr($mime, + 18 + strlen(Defaults::$apiVendor))); + if ($version > 0 && $version <= $this->apiVersion) { + $this->requestedApiVersion = $version; + $format = Scope::get($this->formatMap[$extension]); + $format->setExtension($extension); + // echo "Extension $extension"; + Defaults::$useVendorMIMEVersioning = true; + @header('Vary: Accept'); + + return $format; + } + } + } + + } + } + } else { + // RFC 2616: If no Accept header field is + // present, then it is assumed that the + // client accepts all media types. + $_SERVER['HTTP_ACCEPT'] = '*/*'; + } + if (strpos($_SERVER['HTTP_ACCEPT'], '*') !== false) { + if (false !== strpos($_SERVER['HTTP_ACCEPT'], 'application/*')) { + $format = Scope::get('JsonFormat'); + } elseif (false !== strpos($_SERVER['HTTP_ACCEPT'], 'text/*')) { + $format = Scope::get('XmlFormat'); + } elseif (false !== strpos($_SERVER['HTTP_ACCEPT'], '*/*')) { + $format = Scope::get($this->formatMap['default']); + } + } + if (empty($format)) { + // RFC 2616: If an Accept header field is present, and if the + // server cannot send a response which is acceptable according to + // the combined Accept field value, then the server SHOULD send + // a 406 (not acceptable) response. + $format = Scope::get($this->formatMap['default']); + $this->responseFormat = $format; + throw new RestException( + 406, + 'Content negotiation failed. ' . + 'Try `' . $format->getMIME() . '` instead.' + ); + } else { + // Tell cache content is based at Accept header + @header("Vary: Accept"); + return $format; + } + } + + protected function negotiateCharset() + { + if (isset($_SERVER['HTTP_ACCEPT_CHARSET'])) { + $found = false; + $charList = Util::sortByPriority($_SERVER['HTTP_ACCEPT_CHARSET']); + foreach ($charList as $charset => $quality) { + if (in_array($charset, Defaults::$supportedCharsets)) { + $found = true; + Defaults::$charset = $charset; + break; + } + } + if (!$found) { + if (strpos($_SERVER['HTTP_ACCEPT_CHARSET'], '*') !== false) { + //use default charset + } else { + throw new RestException( + 406, + 'Content negotiation failed. ' . + 'Requested charset is not supported' + ); + } + } + } + } + + protected function negotiateLanguage() + { + if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { + $found = false; + $langList = Util::sortByPriority($_SERVER['HTTP_ACCEPT_LANGUAGE']); + foreach ($langList as $lang => $quality) { + foreach (Defaults::$supportedLanguages as $supported) { + if (strcasecmp($supported, $lang) == 0) { + $found = true; + Defaults::$language = $supported; + break 2; + } + } + } + if (!$found) { + if (strpos($_SERVER['HTTP_ACCEPT_LANGUAGE'], '*') !== false) { + //use default language + } else { + //ignore + } + } + } + } + + /** + * Filer api calls before authentication + */ + protected function preAuthFilter() + { + if (empty($this->filterClasses)) { + return; + } + $this->dispatch('preAuthFilter'); + foreach ($this->filterClasses as $filterClass) { + /** + * @var iFilter + */ + $filterObj = Scope::get($filterClass); + + if (!$filterObj instanceof iFilter) { + throw new RestException ( + 500, 'Filter Class ' . + 'should implement iFilter'); + } else if (!($ok = $filterObj->__isAllowed())) { + if (is_null($ok) + && $filterObj instanceof iUseAuthentication + ) { + //handle at authentication stage + $this->postAuthFilterClasses[] = $filterClass; + continue; + } + throw new RestException(403); //Forbidden + } + } + } + + protected function authenticate() + { + $o = & $this->apiMethodInfo; + $accessLevel = max(Defaults::$apiAccessLevel, + $o->accessLevel); + try { + if ($accessLevel || count($this->postAuthFilterClasses)) { + $this->dispatch('authenticate'); + if (!count($this->authClasses)) { + throw new RestException( + 403, + 'at least one Authentication Class is required' + ); + } + foreach ($this->authClasses as $authClass) { + $authObj = Scope::get($authClass); + if (!method_exists($authObj, + Defaults::$authenticationMethod) + ) { + throw new RestException ( + 500, 'Authentication Class ' . + 'should implement iAuthenticate'); + } elseif ( + !$authObj->{Defaults::$authenticationMethod}() + ) { + throw new RestException(401); + } + } + $this->authenticated = true; + } + $this->authVerified = true; + } catch (RestException $e) { + $this->authVerified = true; + if ($accessLevel > 1) { //when it is not a hybrid api + throw ($e); + } else { + $this->authenticated = false; + } + } + } + + /** + * Filer api calls after authentication + */ + protected function postAuthFilter() + { + if(empty($this->postAuthFilterClasses)) { + return; + } + $this->dispatch('postAuthFilter'); + foreach ($this->postAuthFilterClasses as $filterClass) { + Scope::get($filterClass); + } + } + + protected function validate() + { + if (!Defaults::$autoValidationEnabled) { + return; + } + $this->dispatch('validate'); + + $o = & $this->apiMethodInfo; + foreach ($o->metadata['param'] as $index => $param) { + $info = & $param [CommentParser::$embeddedDataName]; + if (!isset ($info['validate']) + || $info['validate'] != false + ) { + if (isset($info['method'])) { + $info ['apiClassInstance'] = Scope::get($o->className); + } + //convert to instance of ValidationInfo + $info = new ValidationInfo($param); + $validator = Defaults::$validatorClass; + //if(!is_subclass_of($validator, 'Luracast\\Restler\\Data\\iValidate')) { + //changed the above test to below for addressing this php bug + //https://bugs.php.net/bug.php?id=53727 + if (function_exists("$validator::validate")) { + throw new \UnexpectedValueException( + '`Defaults::$validatorClass` must implement `iValidate` interface' + ); + } + $valid = $o->parameters[$index]; + $o->parameters[$index] = null; + if (empty(Validator::$exceptions)) + $o->metadata['param'][$index]['autofocus'] = true; + $valid = $validator::validate( + $valid, $info + ); + $o->parameters[$index] = $valid; + unset($o->metadata['param'][$index]['autofocus']); + } + } + } + + protected function call() + { + $this->dispatch('call'); + $o = & $this->apiMethodInfo; + $accessLevel = max(Defaults::$apiAccessLevel, + $o->accessLevel); + $object = Scope::get($o->className); + switch ($accessLevel) { + case 3 : //protected method + $reflectionMethod = new \ReflectionMethod( + $object, + $o->methodName + ); + $reflectionMethod->setAccessible(true); + $result = $reflectionMethod->invokeArgs( + $object, + $o->parameters + ); + break; + default : + $result = call_user_func_array(array( + $object, + $o->methodName + ), $o->parameters); + } + $this->responseData = $result; + } + + protected function compose() + { + $this->dispatch('compose'); + $this->composeHeaders(); + /** + * @var iCompose Default Composer + */ + $compose = Scope::get(Defaults::$composeClass); + $this->responseData = is_null($this->responseData) && + Defaults::$emptyBodyForNullResponse + ? '' + : $this->responseFormat->encode( + $compose->response($this->responseData), + !$this->productionMode + ); + } + + public function composeHeaders(RestException $e = null) + { + //only GET method should be cached if allowed by API developer + $expires = $this->requestMethod == 'GET' ? Defaults::$headerExpires : 0; + if(!is_array(Defaults::$headerCacheControl)) + Defaults::$headerCacheControl = array(Defaults::$headerCacheControl); + $cacheControl = Defaults::$headerCacheControl[0]; + if ($expires > 0) { + $cacheControl = $this->apiMethodInfo->accessLevel + ? 'private, ' : 'public, '; + $cacheControl .= end(Defaults::$headerCacheControl); + $cacheControl = str_replace('{expires}', $expires, $cacheControl); + $expires = gmdate('D, d M Y H:i:s \G\M\T', time() + $expires); + } + @header('Cache-Control: ' . $cacheControl); + @header('Expires: ' . $expires); + @header('X-Powered-By: Luracast Restler v' . Restler::VERSION); + + if (Defaults::$crossOriginResourceSharing + && isset($_SERVER['HTTP_ORIGIN']) + ) { + header('Access-Control-Allow-Origin: ' . + (Defaults::$accessControlAllowOrigin == '*' + ? $_SERVER['HTTP_ORIGIN'] + : Defaults::$accessControlAllowOrigin) + ); + header('Access-Control-Allow-Credentials: true'); + header('Access-Control-Max-Age: 86400'); + } + + $this->responseFormat->setCharset(Defaults::$charset); + $charset = $this->responseFormat->getCharset() + ? : Defaults::$charset; + + @header('Content-Type: ' . ( + Defaults::$useVendorMIMEVersioning + ? 'application/vnd.' + . Defaults::$apiVendor + . "-v{$this->requestedApiVersion}" + . '+' . $this->responseFormat->getExtension() + : $this->responseFormat->getMIME()) + . '; charset=' . $charset + ); + + @header('Content-Language: ' . Defaults::$language); + + if (isset($this->apiMethodInfo->metadata['header'])) { + foreach ($this->apiMethodInfo->metadata['header'] as $header) + @header($header, true); + } + $code = 200; + if (!Defaults::$suppressResponseCode) { + if ($e) { + $code = $e->getCode(); + } elseif (isset($this->apiMethodInfo->metadata['status'])) { + $code = $this->apiMethodInfo->metadata['status']; + } + } + $this->responseCode = $code; + @header( + "{$_SERVER['SERVER_PROTOCOL']} $code " . + (isset(RestException::$codes[$code]) ? RestException::$codes[$code] : '') + ); + } + + protected function respond() + { + $this->dispatch('respond'); + //handle throttling + if (Defaults::$throttle) { + $elapsed = time() - $this->startTime; + if (Defaults::$throttle / 1e3 > $elapsed) { + usleep(1e6 * (Defaults::$throttle / 1e3 - $elapsed)); + } + } + if ($this->responseCode == 401) { + $authString = count($this->authClasses) + ? Scope::get($this->authClasses[0])->__getWWWAuthenticateString() + : 'Unknown'; + @header('WWW-Authenticate: ' . $authString, false); + } + echo $this->responseData; + $this->dispatch('complete'); + exit; + } + + protected function message(Exception $exception) + { + $this->dispatch('message'); + + if (!$exception instanceof RestException) { + $exception = new RestException( + 500, + $this->productionMode ? null : $exception->getMessage(), + array(), + $exception + ); + } + + $this->exception = $exception; + + $method = 'handle' . $exception->getCode(); + $handled = false; + foreach ($this->errorClasses as $className) { + if (method_exists($className, $method)) { + $obj = Scope::get($className); + if ($obj->$method()) + $handled = true; + } + } + if ($handled) { + return; + } + if (!isset($this->responseFormat)) { + $this->responseFormat = Scope::get('JsonFormat'); + } + $this->composeHeaders($exception); + /** + * @var iCompose Default Composer + */ + $compose = Scope::get(Defaults::$composeClass); + $this->responseData = $this->responseFormat->encode( + $compose->message($exception), + !$this->productionMode + ); + $this->respond(); + } + + /** + * Provides backward compatibility with older versions of Restler + * + * @param int $version restler version + * + * @throws \OutOfRangeException + */ + public function setCompatibilityMode($version = 2) + { + if ($version <= intval(self::VERSION) && $version > 0) { + require __DIR__."/compatibility/restler{$version}.php"; + return; + } + throw new \OutOfRangeException(); + } + + /** + * @param int $version maximum version number supported + * by the api + * @param int $minimum minimum version number supported + * (optional) + * + * @throws InvalidArgumentException + * @return void + */ + public function setAPIVersion($version = 1, $minimum = 1) + { + if (!is_int($version) && $version < 1) { + throw new InvalidArgumentException + ('version should be an integer greater than 0'); + } + $this->apiVersion = $version; + if (is_int($minimum)) { + $this->apiMinimumVersion = $minimum; + } + } + + /** + * Classes implementing iFilter interface can be added for filtering out + * the api consumers. + * + * It can be used for rate limiting based on usage from a specific ip + * address or filter by country, device etc. + * + * @param $className + */ + public function addFilterClass($className) + { + $this->filterClasses[] = $className; + } + + /** + * protected methods will need at least one authentication class to be set + * in order to allow that method to be executed + * + * @param string $className of the authentication class + * @param string $resourcePath optional url prefix for mapping + */ + public function addAuthenticationClass($className, $resourcePath = null) + { + $this->authClasses[] = $className; + $this->addAPIClass($className, $resourcePath); + } + + /** + * Add api classes through this method. + * + * All the public methods that do not start with _ (underscore) + * will be will be exposed as the public api by default. + * + * All the protected methods that do not start with _ (underscore) + * will exposed as protected api which will require authentication + * + * @param string $className name of the service class + * @param string $resourcePath optional url prefix for mapping, uses + * lowercase version of the class name when + * not specified + * + * @return null + * + * @throws Exception when supplied with invalid class name + */ + public function addAPIClass($className, $resourcePath = null) + { + try{ + if ($this->productionMode && is_null($this->cached)) { + $routes = $this->cache->get('routes'); + if (isset($routes) && is_array($routes)) { + $this->apiVersionMap = $routes['apiVersionMap']; + unset($routes['apiVersionMap']); + Routes::fromArray($routes); + $this->cached = true; + } else { + $this->cached = false; + } + } + if (isset(Scope::$classAliases[$className])) { + $className = Scope::$classAliases[$className]; + } + if (!$this->cached) { + $maxVersionMethod = '__getMaximumSupportedVersion'; + if (class_exists($className)) { + if (method_exists($className, $maxVersionMethod)) { + $max = $className::$maxVersionMethod(); + for ($i = 1; $i <= $max; $i++) { + $this->apiVersionMap[$className][$i] = $className; + } + } else { + $this->apiVersionMap[$className][1] = $className; + } + } + //versioned api + if (false !== ($index = strrpos($className, '\\'))) { + $name = substr($className, 0, $index) + . '\\v{$version}' . substr($className, $index); + } else if (false !== ($index = strrpos($className, '_'))) { + $name = substr($className, 0, $index) + . '_v{$version}' . substr($className, $index); + } else { + $name = 'v{$version}\\' . $className; + } + + for ($version = $this->apiMinimumVersion; + $version <= $this->apiVersion; + $version++) { + + $versionedClassName = str_replace('{$version}', $version, + $name); + if (class_exists($versionedClassName)) { + Routes::addAPIClass($versionedClassName, + Util::getResourcePath( + $className, + $resourcePath + ), + $version + ); + if (method_exists($versionedClassName, $maxVersionMethod)) { + $max = $versionedClassName::$maxVersionMethod(); + for ($i = $version; $i <= $max; $i++) { + $this->apiVersionMap[$className][$i] = $versionedClassName; + } + } else { + $this->apiVersionMap[$className][$version] = $versionedClassName; + } + } elseif (isset($this->apiVersionMap[$className][$version])) { + Routes::addAPIClass($this->apiVersionMap[$className][$version], + Util::getResourcePath( + $className, + $resourcePath + ), + $version + ); + } + } + + } + } catch (Exception $e) { + $e = new Exception( + "addAPIClass('$className') failed. ".$e->getMessage(), + $e->getCode(), + $e + ); + $this->setSupportedFormats('JsonFormat'); + $this->message($e); + } + } + + /** + * Add class for custom error handling + * + * @param string $className of the error handling class + */ + public function addErrorClass($className) + { + $this->errorClasses[] = $className; + } + + /** + * Associated array that maps formats to their respective format class name + * + * @return array + */ + public function getFormatMap() + { + return $this->formatMap; + } + + /** + * API version requested by the client + * @return int + */ + public function getRequestedApiVersion() + { + return $this->requestedApiVersion; + } + + /** + * When false, restler will run in debug mode and parse the class files + * every time to map it to the URL + * + * @return bool + */ + public function getProductionMode() + { + return $this->productionMode; + } + + /** + * Chosen API version + * + * @return int + */ + public function getApiVersion() + { + return $this->apiVersion; + } + + /** + * Base Url of the API Service + * + * @return string + * + * @example http://localhost/restler3 + * @example http://restler3.com + */ + public function getBaseUrl() + { + return $this->baseUrl; + } + + /** + * List of events that fired already + * + * @return array + */ + public function getEvents() + { + return $this->events; + } + + /** + * Magic method to expose some protected variables + * + * @param string $name name of the hidden property + * + * @return null|mixed + */ + public function __get($name) + { + if ($name{0} == '_') { + $hiddenProperty = substr($name, 1); + if (isset($this->$hiddenProperty)) { + return $this->$hiddenProperty; + } + } + return null; + } + + /** + * Store the url map cache if needed + */ + public function __destruct() + { + if ($this->productionMode && !$this->cached) { + $this->cache->set( + 'routes', + Routes::toArray() + + array('apiVersionMap' => $this->apiVersionMap) + ); + } + } + + /** + * pre call + * + * call _pre_{methodName)_{extension} if exists with the same parameters as + * the api method + * + * @example _pre_get_json + * + */ + protected function preCall() + { + $o = & $this->apiMethodInfo; + $preCall = '_pre_' . $o->methodName . '_' + . $this->requestFormat->getExtension(); + + if (method_exists($o->className, $preCall)) { + $this->dispatch('preCall'); + call_user_func_array(array( + Scope::get($o->className), + $preCall + ), $o->parameters); + } + } + + /** + * post call + * + * call _post_{methodName}_{extension} if exists with the composed and + * serialized (applying the repose format) response data + * + * @example _post_get_json + */ + protected function postCall() + { + $o = & $this->apiMethodInfo; + $postCall = '_post_' . $o->methodName . '_' . + $this->responseFormat->getExtension(); + if (method_exists($o->className, $postCall)) { + $this->dispatch('postCall'); + $this->responseData = call_user_func(array( + Scope::get($o->className), + $postCall + ), $this->responseData); + } + } +} diff --git a/htdocs/includes/restler/Routes.php b/htdocs/includes/restler/Routes.php new file mode 100644 index 00000000000..74a02eba935 --- /dev/null +++ b/htdocs/includes/restler/Routes.php @@ -0,0 +1,696 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class Routes +{ + public static $prefixingParameterNames = array( + 'id' + ); + protected static $routes = array(); + + protected static $models = array(); + + /** + * Route the public and protected methods of an Api class + * + * @param string $className + * @param string $resourcePath + * @param int $version + * + * @throws RestException + */ + public static function addAPIClass($className, $resourcePath = '', $version = 1) + { + + /* + * Mapping Rules + * ============= + * + * - Optional parameters should not be mapped to URL + * - If a required parameter is of primitive type + * - If one of the self::$prefixingParameterNames + * - Map it to URL + * - Else If request method is POST/PUT/PATCH + * - Map it to body + * - Else If request method is GET/DELETE + * - Map it to body + * - If a required parameter is not primitive type + * - Do not include it in URL + */ + $class = new ReflectionClass($className); + try { + $classMetadata = CommentParser::parse($class->getDocComment()); + } catch (Exception $e) { + throw new RestException(500, "Error while parsing comments of `$className` class. " . $e->getMessage()); + } + $classMetadata['scope'] = $scope = static::scope($class); + $methods = $class->getMethods(ReflectionMethod::IS_PUBLIC + + ReflectionMethod::IS_PROTECTED); + foreach ($methods as $method) { + $methodUrl = strtolower($method->getName()); + //method name should not begin with _ + if ($methodUrl{0} == '_') { + continue; + } + $doc = $method->getDocComment(); + + try { + $metadata = CommentParser::parse($doc) + $classMetadata; + } catch (Exception $e) { + throw new RestException(500, "Error while parsing comments of `{$className}::{$method->getName()}` method. " . $e->getMessage()); + } + //@access should not be private + if (isset($metadata['access']) + && $metadata['access'] == 'private' + ) { + continue; + } + $arguments = array(); + $defaults = array(); + $params = $method->getParameters(); + $position = 0; + $pathParams = array(); + $allowAmbiguity + = (isset($metadata['smart-auto-routing']) + && $metadata['smart-auto-routing'] != 'true') + || !Defaults::$smartAutoRouting; + $metadata['resourcePath'] = $resourcePath; + if (isset($classMetadata['description'])) { + $metadata['classDescription'] = $classMetadata['description']; + } + if (isset($classMetadata['classLongDescription'])) { + $metadata['classLongDescription'] + = $classMetadata['longDescription']; + } + if (!isset($metadata['param'])) { + $metadata['param'] = array(); + } + if (isset($metadata['return']['type'])) { + if ($qualified = Scope::resolve($metadata['return']['type'], $scope)) + list($metadata['return']['type'], $metadata['return']['children']) = + static::getTypeAndModel(new ReflectionClass($qualified), $scope); + } else { + //assume return type is array + $metadata['return']['type'] = 'array'; + } + foreach ($params as $param) { + $children = array(); + $type = + $param->isArray() ? 'array' : $param->getClass(); + $arguments[$param->getName()] = $position; + $defaults[$position] = $param->isDefaultValueAvailable() ? + $param->getDefaultValue() : null; + if (!isset($metadata['param'][$position])) { + $metadata['param'][$position] = array(); + } + $m = & $metadata ['param'] [$position]; + $m ['name'] = $param->getName(); + if (empty($m['label'])) + $m['label'] = static::label($m['name']); + if (is_null($type) && isset($m['type'])) { + $type = $m['type']; + } + if ($m['name'] == 'email' && empty($m[CommentParser::$embeddedDataName]['type']) && $type == 'string') + $m[CommentParser::$embeddedDataName]['type'] = 'email'; + $m ['default'] = $defaults [$position]; + $m ['required'] = !$param->isOptional(); + $contentType = Util::nestedValue( + $m, + CommentParser::$embeddedDataName, + 'type' + ); + if ($contentType && $qualified = Scope::resolve($contentType, $scope)) { + list($m[CommentParser::$embeddedDataName]['type'], $children) = static::getTypeAndModel( + new ReflectionClass($qualified), $scope + ); + } + if ($type instanceof ReflectionClass) { + list($type, $children) = static::getTypeAndModel($type, $scope); + } elseif ($type && is_string($type) && $qualified = Scope::resolve($type, $scope)) { + list($type, $children) + = static::getTypeAndModel(new ReflectionClass($qualified), $scope); + } + if (isset($type)) { + $m['type'] = $type; + } + $m['children'] = $children; + + if ($m['name'] == Defaults::$fullRequestDataName) { + $from = 'body'; + if (!isset($m['type'])) { + $type = $m['type'] = 'array'; + } + + } elseif (isset($m[CommentParser::$embeddedDataName]['from'])) { + $from = $m[CommentParser::$embeddedDataName]['from']; + } else { + if ((isset($type) && Util::isObjectOrArray($type)) + ) { + $from = 'body'; + if (!isset($type)) { + $type = $m['type'] = 'array'; + } + } elseif ($m['required'] && in_array($m['name'], static::$prefixingParameterNames)) { + $from = 'path'; + } else { + $from = 'body'; + } + } + $m[CommentParser::$embeddedDataName]['from'] = $from; + if (!isset($m['type'])) { + $type = $m['type'] = static::type($defaults[$position]); + } + + if ($allowAmbiguity || $from == 'path') { + $pathParams [] = $position; + } + $position++; + } + $accessLevel = 0; + if ($method->isProtected()) { + $accessLevel = 3; + } elseif (isset($metadata['access'])) { + if ($metadata['access'] == 'protected') { + $accessLevel = 2; + } elseif ($metadata['access'] == 'hybrid') { + $accessLevel = 1; + } + } elseif (isset($metadata['protected'])) { + $accessLevel = 2; + } + /* + echo " access level $accessLevel for $className::" + .$method->getName().$method->isProtected().PHP_EOL; + */ + + // take note of the order + $call = array( + 'url' => null, + 'className' => $className, + 'path' => rtrim($resourcePath, '/'), + 'methodName' => $method->getName(), + 'arguments' => $arguments, + 'defaults' => $defaults, + 'metadata' => $metadata, + 'accessLevel' => $accessLevel, + ); + // if manual route + if (preg_match_all( + '/@url\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)' + . '[ \t]*\/?(\S*)/s', + $doc, $matches, PREG_SET_ORDER + ) + ) { + foreach ($matches as $match) { + $httpMethod = $match[1]; + $url = rtrim($resourcePath . $match[2], '/'); + //deep copy the call, as it may change for each @url + $copy = unserialize(serialize($call)); + foreach ($copy['metadata']['param'] as $i => $p) { + $inPath = + strpos($url, '{' . $p['name'] . '}') || + strpos($url, ':' . $p['name']); + if ($inPath) { + $copy['metadata']['param'][$i][CommentParser::$embeddedDataName]['from'] = 'path'; + } elseif ($httpMethod == 'GET' || $httpMethod == 'DELETE') { + $copy['metadata']['param'][$i][CommentParser::$embeddedDataName]['from'] = 'query'; + } elseif ($p[CommentParser::$embeddedDataName]['from'] == 'path') { + $copy['metadata']['param'][$i][CommentParser::$embeddedDataName]['from'] = 'body'; + } + } + $url = preg_replace_callback('/{[^}]+}|:[^\/]+/', + function ($matches) use ($call) { + $match = trim($matches[0], '{}:'); + $index = $call['arguments'][$match]; + return '{' . + Routes::typeChar(isset( + $call['metadata']['param'][$index]['type']) + ? $call['metadata']['param'][$index]['type'] + : null) + . $index . '}'; + }, $url); + static::addPath($url, $copy, $httpMethod, $version); + } + //if auto route enabled, do so + } elseif (Defaults::$autoRoutingEnabled) { + // no configuration found so use convention + if (preg_match_all( + '/^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)/i', + $methodUrl, $matches) + ) { + $httpMethod = strtoupper($matches[0][0]); + $methodUrl = substr($methodUrl, strlen($httpMethod)); + } else { + $httpMethod = 'GET'; + } + if ($methodUrl == 'index') { + $methodUrl = ''; + } + $url = empty($methodUrl) ? rtrim($resourcePath, '/') + : $resourcePath . $methodUrl; + $lastPathParam = array_keys($pathParams); + $lastPathParam = end($lastPathParam); + for ($position = 0; $position < count($params); $position++) { + $from = $metadata['param'][$position][CommentParser::$embeddedDataName]['from']; + if ($from == 'body' && ($httpMethod == 'GET' || + $httpMethod == 'DELETE') + ) { + $call['metadata']['param'][$position][CommentParser::$embeddedDataName]['from'] + = 'query'; + } + } + if (empty($pathParams) || $allowAmbiguity) { + static::addPath($url, $call, $httpMethod, $version); + } + foreach ($pathParams as $position) { + if (!empty($url)) + $url .= '/'; + $url .= '{' . + static::typeChar(isset($call['metadata']['param'][$position]['type']) + ? $call['metadata']['param'][$position]['type'] + : null) + . $position . '}'; + if ($allowAmbiguity || $position == $lastPathParam) { + static::addPath($url, $call, $httpMethod, $version); + } + } + } + } + } + + /** + * @access private + */ + public static function typeChar($type = null) + { + if (!$type) { + return 's'; + } + switch ($type{0}) { + case 'i': + case 'f': + return 'n'; + } + return 's'; + } + + protected static function addPath($path, array $call, + $httpMethod = 'GET', $version = 1) + { + $call['url'] = preg_replace_callback( + "/\{\S(\d+)\}/", + function ($matches) use ($call) { + return '{' . + $call['metadata']['param'][$matches[1]]['name'] . '}'; + }, + $path + ); + //check for wildcard routes + if (substr($path, -1, 1) == '*') { + $path = rtrim($path, '/*'); + static::$routes["v$version"]['*'][$path][$httpMethod] = $call; + } else { + static::$routes["v$version"][$path][$httpMethod] = $call; + //create an alias with index if the method name is index + if ($call['methodName'] == 'index') + static::$routes["v$version"][ltrim("$path/index", '/')][$httpMethod] = $call; + } + } + + /** + * Find the api method for the given url and http method + * + * @param string $path Requested url path + * @param string $httpMethod GET|POST|PUT|PATCH|DELETE etc + * @param int $version Api Version number + * @param array $data Data collected from the request + * + * @throws RestException + * @return ApiMethodInfo + */ + public static function find($path, $httpMethod, + $version = 1, array $data = array()) + { + $p = Util::nestedValue(static::$routes, "v$version"); + if (!$p) { + throw new RestException( + 404, + $version == 1 ? '' : "Version $version is not supported" + ); + } + $status = 404; + $message = null; + $methods = array(); + if (isset($p[$path][$httpMethod])) { + //================== static routes ========================== + return static::populate($p[$path][$httpMethod], $data); + } elseif (isset($p['*'])) { + //================== wildcard routes ======================== + uksort($p['*'], function ($a, $b) { + return strlen($b) - strlen($a); + }); + foreach ($p['*'] as $key => $value) { + if (strpos($path, $key) === 0 && isset($value[$httpMethod])) { + //path found, convert rest of the path to parameters + $path = substr($path, strlen($key) + 1); + $call = ApiMethodInfo::__set_state($value[$httpMethod]); + $call->parameters = empty($path) + ? array() + : explode('/', $path); + return $call; + } + } + } + //================== dynamic routes ============================= + //add newline char if trailing slash is found + if (substr($path, -1) == '/') + $path .= PHP_EOL; + //if double slash is found fill in newline char; + $path = str_replace('//', '/' . PHP_EOL . '/', $path); + ksort($p); + foreach ($p as $key => $value) { + if (!isset($value[$httpMethod])) { + continue; + } + $regex = str_replace(array('{', '}'), + array('(?P<', '>[^/]+)'), $key); + if (preg_match_all(":^$regex$:i", $path, $matches, PREG_SET_ORDER)) { + $matches = $matches[0]; + $found = true; + foreach ($matches as $k => $v) { + if (is_numeric($k)) { + unset($matches[$k]); + continue; + } + $index = intval(substr($k, 1)); + $details = $value[$httpMethod]['metadata']['param'][$index]; + if ($k{0} == 's' || strpos($k, static::pathVarTypeOf($v)) === 0) { + //remove the newlines + $data[$details['name']] = trim($v, PHP_EOL); + } else { + $status = 400; + $message = 'invalid value specified for `' + . $details['name'] . '`'; + $found = false; + break; + } + } + if ($found) { + return static::populate($value[$httpMethod], $data); + } + } + } + if ($status == 404) { + //check if other methods are allowed + if (isset($p[$path])) { + $status = 405; + $methods = array_keys($p[$path]); + } + } + if ($status == 405) { + header('Allow: ' . implode(', ', $methods)); + } + throw new RestException($status, $message); + } + + /** + * Populates the parameter values + * + * @param array $call + * @param $data + * + * @return ApiMethodInfo + * + * @access private + */ + protected static function populate(array $call, $data) + { + $call['parameters'] = $call['defaults']; + $p = & $call['parameters']; + foreach ($data as $key => $value) { + if (isset($call['arguments'][$key])) { + $p[$call['arguments'][$key]] = $value; + } + } + if (Defaults::$smartParameterParsing && 'post' != (string)Util::$restler->requestFormat) { + if ( + count($p) == 1 && + ($m = Util::nestedValue($call, 'metadata', 'param', 0)) && + !array_key_exists($m['name'], $data) && + array_key_exists(Defaults::$fullRequestDataName, $data) && + !is_null($d = $data[Defaults::$fullRequestDataName]) && + isset($m['type']) && + static::typeMatch($m['type'], $d) + ) { + $p[0] = $d; + } else { + $bodyParamCount = 0; + $lastBodyParamIndex = -1; + $lastM = null; + foreach ($call['metadata']['param'] as $k => $m) { + if ($m[CommentParser::$embeddedDataName]['from'] == 'body') { + $bodyParamCount++; + $lastBodyParamIndex = $k; + $lastM = $m; + } + } + if ( + $bodyParamCount == 1 && + !array_key_exists($lastM['name'], $data) && + array_key_exists(Defaults::$fullRequestDataName, $data) && + !is_null($d = $data[Defaults::$fullRequestDataName]) + ) { + $p[$lastBodyParamIndex] = $d; + } + } + } + $r = ApiMethodInfo::__set_state($call); + $modifier = "_modify_{$r->methodName}_api"; + if (method_exists($r->className, $modifier)) { + $stage = end(Scope::get('Restler')->getEvents()); + if (empty($stage)) + $stage = 'setup'; + $r = Scope::get($r->className)->$modifier($r, $stage) ? : $r; + } + return $r; + } + + /** + * @access private + */ + protected static function pathVarTypeOf($var) + { + if (is_numeric($var)) { + return 'n'; + } + if ($var === 'true' || $var === 'false') { + return 'b'; + } + return 's'; + } + + protected static function typeMatch($type, $var) + { + switch ($type) { + case 'boolean': + case 'bool': + return is_bool($var); + case 'array': + case 'object': + return is_array($var); + case 'string': + case 'int': + case 'integer': + case 'float': + case 'number': + return is_scalar($var); + } + return true; + } + + /** + * Get the type and associated model + * + * @param ReflectionClass $class + * @param array $scope + * + * @throws RestException + * @throws \Exception + * @return array + * + * @access protected + */ + protected static function getTypeAndModel(ReflectionClass $class, array $scope) + { + $className = $class->getName(); + if (isset(static::$models[$className])) { + return static::$models[$className]; + } + $children = array(); + try { + $props = $class->getProperties(ReflectionProperty::IS_PUBLIC); + foreach ($props as $prop) { + $name = $prop->getName(); + $child = array('name' => $name); + if ($c = $prop->getDocComment()) { + $child += Util::nestedValue(CommentParser::parse($c), 'var'); + } else { + $o = $class->newInstance(); + $p = $prop->getValue($o); + if (is_object($p)) { + $child['type'] = get_class($p); + } elseif (is_array($p)) { + $child['type'] = 'array'; + if (count($p)) { + $pc = reset($p); + if (is_object($pc)) { + $child['contentType'] = get_class($pc); + } + } + } + } + $child += array( + 'type' => $child['name'] == 'email' ? 'email' : 'string', + 'label' => static::label($child['name']) + ); + isset($child[CommentParser::$embeddedDataName]) + ? $child[CommentParser::$embeddedDataName] += array('required' => true) + : $child[CommentParser::$embeddedDataName]['required'] = true; + if ($qualified = Scope::resolve($child['type'], $scope)) { + list($child['type'], $child['children']) + = static::getTypeAndModel(new ReflectionClass($qualified), $scope); + } elseif ( + ($contentType = Util::nestedValue($child, CommentParser::$embeddedDataName, 'type')) && + ($qualified = Scope::resolve($contentType, $scope)) + ) { + list($child['contentType'], $child['children']) + = static::getTypeAndModel(new ReflectionClass($qualified), $scope); + } + $children[$name] = $child; + } + } catch (Exception $e) { + if (String::endsWith($e->getFile(), 'CommentParser.php')) { + throw new RestException(500, "Error while parsing comments of `$className` class. " . $e->getMessage()); + } + throw $e; + } + static::$models[$className] = array($className, $children); + return static::$models[$className]; + } + + /** + * Import previously created routes from cache + * + * @param array $routes + */ + public static function fromArray(array $routes) + { + static::$routes = $routes; + } + + /** + * Export current routes for cache + * + * @return array + */ + public static function toArray() + { + return static::$routes; + } + + public static function type($var) + { + if (is_object($var)) return get_class($var); + if (is_array($var)) return 'array'; + if (is_bool($var)) return 'boolean'; + if (is_numeric($var)) return is_float($var) ? 'float' : 'int'; + return 'string'; + } + + /** + * Create a label from name of the parameter or property + * + * Convert `camelCase` style names into proper `Title Case` names + * + * @param string $name + * + * @return string + */ + public static function label($name) + { + return ucfirst(preg_replace(array('/(?<=[^A-Z])([A-Z])/', '/(?<=[^0-9])([0-9])/'), ' $0', $name)); + } + + public static function scope(ReflectionClass $class) + { + $namespace = $class->getNamespaceName(); + $imports = array( + '*' => empty($namespace) ? '' : $namespace . '\\' + ); + $file = file_get_contents($class->getFileName()); + $tokens = token_get_all($file); + $namespace = ''; + $alias = ''; + $reading = false; + $last = 0; + foreach ($tokens as $token) { + if (is_string($token)) { + if ($reading && ',' == $token) { + //===== STOP =====// + $reading = false; + if (!empty($namespace)) + $imports[$alias] = trim($namespace, '\\'); + //===== START =====// + $reading = true; + $namespace = ''; + $alias = ''; + } else { + //===== STOP =====// + $reading = false; + if (!empty($namespace)) + $imports[$alias] = trim($namespace, '\\'); + } + } elseif (T_USE == $token[0]) { + //===== START =====// + $reading = true; + $namespace = ''; + $alias = ''; + } elseif ($reading) { + //echo token_name($token[0]) . ' ' . $token[1] . PHP_EOL; + switch ($token[0]) { + case T_WHITESPACE: + continue 2; + case T_STRING: + $alias = $token[1]; + if (T_AS == $last) { + break; + } + //don't break; + case T_NS_SEPARATOR: + $namespace .= $token[1]; + break; + } + $last = $token[0]; + } + } + return $imports; + } +} \ No newline at end of file diff --git a/htdocs/includes/restler/Scope.php b/htdocs/includes/restler/Scope.php new file mode 100644 index 00000000000..a6b1baae57d --- /dev/null +++ b/htdocs/includes/restler/Scope.php @@ -0,0 +1,190 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class Scope +{ + public static $classAliases = array( + + //Core + 'Restler' => 'Luracast\Restler\Restler', + + //Format classes + 'AmfFormat' => 'Luracast\Restler\Format\AmfFormat', + 'JsFormat' => 'Luracast\Restler\Format\JsFormat', + 'JsonFormat' => 'Luracast\Restler\Format\JsonFormat', + 'HtmlFormat' => 'Luracast\Restler\Format\HtmlFormat', + 'PlistFormat' => 'Luracast\Restler\Format\PlistFormat', + 'UploadFormat' => 'Luracast\Restler\Format\UploadFormat', + 'UrlEncodedFormat' => 'Luracast\Restler\Format\UrlEncodedFormat', + 'XmlFormat' => 'Luracast\Restler\Format\XmlFormat', + 'YamlFormat' => 'Luracast\Restler\Format\YamlFormat', + 'CsvFormat' => 'Luracast\Restler\Format\CsvFormat', + 'TsvFormat' => 'Luracast\Restler\Format\TsvFormat', + + //Filter classes + 'RateLimit' => 'Luracast\Restler\Filter\RateLimit', + + //UI classes + 'Forms' => 'Luracast\Restler\UI\Forms', + 'Nav' => 'Luracast\Restler\UI\Nav', + 'Emmet' => 'Luracast\Restler\UI\Emmet', + 'T' => 'Luracast\Restler\UI\Tags', + + //API classes + 'Resources' => 'Luracast\Restler\Resources', + + //Cache classes + 'HumanReadableCache' => 'Luracast\Restler\HumanReadableCache', + 'ApcCache' => 'Luracast\Restler\ApcCache', + + //Utility classes + 'Object' => 'Luracast\Restler\Data\Object', + 'String' => 'Luracast\Restler\Data\String', + 'Arr' => 'Luracast\Restler\Data\Arr', + + //Exception + 'RestException' => 'Luracast\Restler\RestException' + ); + public static $properties = array(); + protected static $instances = array(); + protected static $registry = array(); + + public static function register($name, Callable $function, $singleton = true) + { + static::$registry[$name] = (object)compact('function', 'singleton'); + } + + public static function set($name, $instance) + { + static::$instances[$name] = (object)array('instance' => $instance); + } + + public static function get($name) + { + $r = null; + $initialized = false; + $properties = array(); + if (array_key_exists($name, static::$instances)) { + $initialized = true; + $r = static::$instances[$name]->instance; + } elseif (!empty(static::$registry[$name])) { + $function = static::$registry[$name]->function; + $r = $function(); + if (static::$registry[$name]->singleton) + static::$instances[$name] = (object)array('instance' => $r); + } else { + $fullName = $name; + if (isset(static::$classAliases[$name])) { + $fullName = static::$classAliases[$name]; + } + if (class_exists($fullName)) { + $shortName = Util::getShortName($name); + $r = new $fullName(); + static::$instances[$name] = (object)array('instance' => $r); + if ($name != 'Restler') { + $r->restler = static::get('Restler'); + $m = Util::nestedValue($r->restler, 'apiMethodInfo', 'metadata'); + if ($m) { + $properties = Util::nestedValue( + $m, 'class', $fullName, + CommentParser::$embeddedDataName + ) ? : (Util::nestedValue( + $m, 'class', $shortName, + CommentParser::$embeddedDataName + ) ? : array()); + } else { + static::$instances[$name]->initPending = true; + } + } + } + } + if ( + $r instanceof iUseAuthentication && + static::get('Restler')->_authVerified && + !isset(static::$instances[$name]->authVerified) + ) { + static::$instances[$name]->authVerified = true; + $r->__setAuthenticationStatus + (static::get('Restler')->_authenticated); + } + if (isset(static::$instances[$name]->initPending)) { + $m = Util::nestedValue(static::get('Restler'), 'apiMethodInfo', 'metadata'); + $fullName = $name; + if (class_exists($name)) { + $shortName = Util::getShortName($name); + } else { + $shortName = $name; + if (isset(static::$classAliases[$name])) + $fullName = static::$classAliases[$name]; + } + if ($m) { + $properties = Util::nestedValue( + $m, 'class', $fullName, + CommentParser::$embeddedDataName + ) ? : (Util::nestedValue( + $m, 'class', $shortName, + CommentParser::$embeddedDataName + ) ? : array()); + unset(static::$instances[$name]->initPending); + $initialized = false; + } + } + if (!$initialized && is_object($r)) { + $properties += static::$properties; + $objectVars = get_object_vars($r); + $className = get_class($r); + foreach ($properties as $property => $value) { + if (property_exists($className, $property)) { + //if not a static property + array_key_exists($property, $objectVars) + ? $r->{$property} = $value + : $r::$$property = $value; + } + } + } + return $r; + } + + /** + * Get fully qualified class name for the given scope + * + * @param string $className + * @param array $scope local scope + * + * @return string|boolean returns the class name or false + */ + public static function resolve($className, array $scope) + { + if (empty($className) || !is_string($className)) + return false; + $divider = '\\'; + $qualified = false; + if ($className{0} == $divider) { + $qualified = trim($className, $divider); + } elseif (array_key_exists($className, $scope)) { + $qualified = $scope[$className]; + } else { + $qualified = $scope['*'] . $className; + } + if (class_exists($qualified)) + return $qualified; + if (isset(static::$classAliases[$className])) { + $qualified = static::$classAliases[$className]; + if (class_exists($qualified)) + return $qualified; + } + return false; + } +} diff --git a/htdocs/includes/restler/UI/Emmet.php b/htdocs/includes/restler/UI/Emmet.php new file mode 100644 index 00000000000..658cdea91bd --- /dev/null +++ b/htdocs/includes/restler/UI/Emmet.php @@ -0,0 +1,383 @@ ++^[=" ]{$@-#}'; + + /** + * Create the needed tag hierarchy from emmet string + * + * @param string $string + * + * @param array|string $data + * + * @return array|T + */ + public static function make($string, $data = null) + { + if (!strlen($string)) + return array(); + + $implicitTag = + function () use (& $tag) { + if (empty($tag->tag)) { + switch ($tag->parent->tag) { + case 'ul': + case 'ol': + $tag->tag = 'li'; + break; + case 'em': + $tag->tag = 'span'; + break; + case 'table': + case 'tbody': + case 'thead': + case 'tfoot': + $tag->tag = 'tr'; + break; + case 'tr': + $tag->tag = 'td'; + break; + case 'select': + case 'optgroup': + $tag->tag = 'option'; + break; + default: + $tag->tag = 'div'; + } + } + }; + + $parseText = + function ( + $text, $round, $total, $data, $delimiter = null + ) + use ( + & $tokens, & $tag + ) { + $digits = 0; + if ($delimiter == null) + $delimiter = array( + '.' => true, + '#' => true, + '*' => true, + '>' => true, + '+' => true, + '^' => true, + '[' => true, + ']' => true, + '=' => true, + ); + while (!empty($tokens) && + !isset($delimiter[$t = array_shift($tokens)])) { + while ('$' === $t) { + $digits++; + $t = array_shift($tokens); + } + if ($digits) { + $negative = false; + $offset = 0; + if ('@' == $t) { + if ('-' == ($t = array_shift($tokens))) { + $negative = true; + if (is_numeric(reset($tokens))) { + $offset = array_shift($tokens); + } + } elseif (is_numeric($t)) { + $offset = $t; + } else { + array_unshift($tokens, $t); + } + } elseif ('#' == ($h = array_shift($tokens))) { + if (!empty($t)) { + $data = Util::nestedValue($data, $t); + if (is_null($data)) { + return null; + } + } + if (is_numeric($data)) { + $text .= sprintf("%0{$digits}d", (int)$data); + } elseif (is_string($data)) { + $text .= $data; + } + $digits = 0; + continue; + } else { + array_unshift($tokens, $t, $h); + } + if ($negative) { + $n = $total + 1 - $round + $offset; + } else { + $n = $round + $offset; + } + $text .= sprintf("%0{$digits}d", $n); + $digits = 0; + } else { + $text .= $t; + } + } + if (isset($t)) + array_unshift($tokens, $t); + return $text; + }; + + $parseAttributes = + function (Callable $self, $round, $total, $data) + use (& $tokens, & $tag, $parseText) { + $a = $parseText( + '', $round, $total, $data + ); + if (is_null($a)) + return; + if ('=' == ($v = array_shift($tokens))) { + //value + if ('"' == ($v = array_shift($tokens))) { + $text = ''; + $tag->$a($parseText( + $text, $round, $total, $data, + array('"' => true) + )); + } else { + array_unshift($tokens, $v); + $text = ''; + $tag->$a($parseText( + $text, $round, $total, $data, + array(' ' => true, ']' => true) + )); + } + if (' ' == ($v = array_shift($tokens))) { + $self($self, $round, $total, $data); + } + } elseif (']' == $v) { + //end + $tag->$a(''); + return; + } elseif (' ' == $v) { + $tag->$a(''); + $self($self, $round, $total, $data); + } + }; + + $tokens = static::tokenize($string); + $tag = new T(array_shift($tokens)); + $parent = $root = new T; + + $parse = + function ( + Callable $self, $round = 1, $total = 1 + ) + use ( + & $tokens, & $parent, & $tag, & $data, + $parseAttributes, $implicitTag, $parseText + ) { + $offsetTokens = null; + $parent[] = $tag; + $isInChild = false; + while ($tokens) { + switch (array_shift($tokens)) { + //class + case '.': + $offsetTokens = array_values($tokens); + array_unshift($offsetTokens, '.'); + $implicitTag(); + $e = array_filter(explode(' ', $tag->class)); + $e[] = $parseText('', $round, $total, $data); + $tag->class(implode(' ', array_unique($e))); + break; + //id + case '#': + $offsetTokens = array_values($tokens); + array_unshift($offsetTokens, '#'); + $implicitTag(); + $tag->id( + $parseText( + array_shift($tokens), $round, $total, $data + ) + ); + break; + //attributes + case '[': + $offsetTokens = array_values($tokens); + array_unshift($offsetTokens, '['); + $implicitTag(); + $parseAttributes( + $parseAttributes, $round, $total, $data + ); + break; + //child + case '{': + $text = ''; + $tag[] = $parseText( + $text, $round, $total, $data, array('}' => true) + ); + break; + case '>': + $isInChild = true; + $offsetTokens = null; + if ('{' == ($t = array_shift($tokens))) { + array_unshift($tokens, $t); + $child = new T(); + $tag[] = $child; + $parent = $tag; + $tag = $child; + } elseif ('[' == $t) { + array_unshift($tokens, $t); + } else { + $child = new T($t); + $tag[] = $child; + $parent = $tag; + $tag = $child; + } + break; + //sibling + case '+': + $offsetTokens = null; + if (!$isInChild && $round != $total) { + $tokens = array(); + break; + } + if ('{' == ($t = array_shift($tokens))) { + $tag = $tag->parent; + array_unshift($tokens, $t); + break; + } elseif ('[' == $t) { + array_unshift($tokens, $t); + } else { + $child = new T($t); + $tag = $tag->parent; + $tag[] = $child; + $tag = $child; + } + break; + //sibling of parent + case '^': + if ($round != $total) { + $tokens = array(); + break; + } + $tag = $tag->parent; + if ($tag->parent) + $tag = $tag->parent; + while ('^' == ($t = array_shift($tokens))) { + if ($tag->parent) + $tag = $tag->parent; + } + $child = new T($t); + $tag[] = $child; + $tag = $child; + break; + //clone + case '*': + $times = array_shift($tokens); + $removeCount = 2; + $delimiter = array( + '.' => true, + '#' => true, + '*' => true, + '>' => true, + '+' => true, + '^' => true, + '[' => true, + ']' => true, + '=' => true, + ); + if (!is_numeric($times)) { + if (is_string($times)) { + if (!isset($delimiter[$times])) { + $data = Util::nestedValue($data, $times) + ? : $data; + } else { + array_unshift($tokens, $times); + $removeCount = 1; + } + } + $indexed = array_values($data); + $times = is_array($data) && $indexed == $data + ? count($data) : 0; + } + $source = $tag; + if (!empty($offsetTokens)) { + if (false !== strpos($source->class, ' ')) { + $class = explode(' ', $source->class); + array_pop($class); + $class = implode(' ', $class); + } else { + $class = null; + } + $tag->class($class); + $star = array_search('*', $offsetTokens); + array_splice($offsetTokens, $star, $removeCount); + $remainingTokens = $offsetTokens; + } else { + $remainingTokens = $tokens; + } + $source->parent = null; + $sourceData = $data; + $currentParent = $parent; + for ($i = 1; $i <= $times; $i++) { + $tag = clone $source; + $parent = $currentParent; + $data = is_array($sourceData) + && isset($sourceData[$i - 1]) + ? $sourceData[$i - 1] + : @(string)$sourceData; + $tokens = array_values($remainingTokens); + $self($self, $i, $times); + } + $round = 1; + $offsetTokens = null; + $tag = $source; + $tokens = array(); //$remainingTokens; + break; + } + } + }; + $parse($parse); + return count($root) == 1 ? $root[0] : $root; + } + + public static function tokenize($string) + { + $r = array(); + $f = strtok($string, static::DELIMITERS); + $pos = 0; + do { + $start = $pos; + $pos = strpos($string, $f, $start); + $tokens = array(); + for ($i = $start; $i < $pos; $i++) { + $token = $string{$i}; + if (('#' == $token || '.' == $token) && + (!empty($tokens) || $i == 0) + ) { + $r[] = ''; + } + $r[] = $tokens[] = $token; + } + $pos += strlen($f); + $r[] = $f; + } while (false != ($f = strtok(static::DELIMITERS))); + for ($i = $pos; $i < strlen($string); $i++) { + $token = $string{$i}; + $r[] = $tokens[] = $token; + } + return $r; + /* sample output produced by ".row*3>.col*3" + [0] => div + [1] => . + [2] => row + [3] => * + [4] => 3 + [5] => > + [6] => div + [7] => . + [8] => col + [9] => * + [10] => 4 + */ + } +} \ No newline at end of file diff --git a/htdocs/includes/restler/UI/FormStyles.php b/htdocs/includes/restler/UI/FormStyles.php new file mode 100644 index 00000000000..486cb972e4f --- /dev/null +++ b/htdocs/includes/restler/UI/FormStyles.php @@ -0,0 +1,59 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class FormStyles +{ + public static $html = array( + 'form' => 'form[role=form id=$id# name=$name# method=$method# action=$action# enctype=$enctype#]', + 'input' => '.row>section>label{$label#}^input[name=$name# value=$value# type=$type# required=$required# autofocus=$autofocus# placeholder=$default# accept=$accept#]', + 'textarea' => '.row>label{$label#}^textarea[name=$name# required=$required# autofocus=$autofocus# placeholder=$default# rows=3]{$value#}', + 'radio' => '.row>section>label{$label#}^span>label*options>input[name=$name# value=$value# type=radio checked=$selected# required=$required#]+{ $text#}', + 'select' => '.row>label{$label#}^select[name=$name# required=$required#]>option[value]+option[value=$value# selected=$selected#]{$text#}*options', + 'submit' => '.row>label{   }^button[type=submit]{$label#}', + 'fieldset' => 'fieldset>legend{$label#}', + 'checkbox' => '.row>label>input[name=$name# value=$value# type=checkbox checked=$selected# required=$required# autofocus=$autofocus# accept=$accept#]+{$label#}', + //------------- TYPE BASED STYLES ---------------------// + 'checkbox-array' => 'fieldset>legend{$label#}+section*options>label>input[name=$name# value=$value# type=checkbox checked=$selected# required=$required# autofocus=$autofocus# accept=$accept#]+{ $text#}', + 'select-array' => 'label{$label#}+select[name=$name# required=$required# multiple style="height: auto;background-image: none; outline: inherit;"]>option[value=$value# selected=$selected#]{$text#}*options', + ); + public static $bootstrap3 = array( + 'form' => 'form[role=form id=$id# name=$name# method=$method# action=$action# enctype=$enctype#]', + 'input' => '.form-group>label{$label#}+input.form-control[name=$name# value=$value# type=$type# required=$required# autofocus=$autofocus# placeholder=$default# accept=$accept#]', + 'textarea' => '.form-group>label{$label#}+textarea.form-control[name=$name# required=$required# autofocus=$autofocus# placeholder=$default# rows=3]{$value#}', + 'radio' => 'fieldset>legend{$label#}>.radio*options>label>input.radio[name=$name# value=$value# type=radio checked=$selected# required=$required#]{$text#}', + 'select' => '.form-group>label{$label#}+select.form-control[name=$name# multiple=$multiple# required=$required#]>option[value]+option[value=$value# selected=$selected#]{$text#}*options', + 'submit' => 'button.btn.btn-primary[type=submit]{$label#}', + 'fieldset' => 'fieldset>legend{$label#}', + 'checkbox' => '.checkbox>label>input[name=$name# value=$value# type=checkbox checked=$selected# required=$required# autofocus=$autofocus# accept=$accept#]+{$label#}', + //------------- TYPE BASED STYLES ---------------------// + 'checkbox-array' => 'fieldset>legend{$label#}>.checkbox*options>label>input[name=$name# value=$value# type=checkbox checked=$selected# required=$required#]{$text#}', + 'select-array' => '.form-group>label{$label#}+select.form-control[name=$name# multiple=$multiple# required=$required#] size=$options#>option[value=$value# selected=$selected#]{$text#}*options', + //------------- CUSTOM STYLES ---------------------// + 'radio-inline' => '.form-group>label{$label# :  }+label.radio-inline*options>input.radio[name=$name# value=$value# type=radio checked=$selected# required=$required#]+{$text#}', + ); + public static $foundation5 = array( + 'form' => 'form[id=$id# name=$name# method=$method# action=$action# enctype=$enctype#]', + 'input' => 'label{$label#}+input[name=$name# value=$value# type=$type# required=$required# autofocus=$autofocus# placeholder=$default# accept=$accept#]', + 'textarea' => 'label{$label#}+textarea[name=$name# required=$required# autofocus=$autofocus# placeholder=$default# rows=3]{$value#}', + 'radio' => 'label{$label# :  }+label.radio-inline*options>input.radio[name=$name# value=$value# type=radio checked=$selected# required=$required#]+{$text#}', + 'select' => 'label{$label#}+select[name=$name# required=$required#]>option[value]+option[value=$value# selected=$selected#]{$text#}*options', + 'submit' => 'button.button[type=submit]{$label#}', + 'fieldset' => 'fieldset>legend{$label#}', + 'checkbox' => 'label>input[name=$name# value=$value# type=checkbox checked=$selected# required=$required# autofocus=$autofocus# accept=$accept#]+{ $label#}', + //------------- TYPE BASED STYLES ---------------------// + 'checkbox-array' => 'fieldset>legend{$label#}+label*options>input[name=$name# value=$value# type=checkbox checked=$selected# required=$required# autofocus=$autofocus# accept=$accept#]+{ $text#}', + 'select-array' => 'label{$label#}+select[name=$name# required=$required# multiple style="height: auto;background-image: none; outline: inherit;"]>option[value=$value# selected=$selected#]{$text#}*options', + //------------- CUSTOM STYLES ---------------------// + ); +} \ No newline at end of file diff --git a/htdocs/includes/restler/UI/Forms.php b/htdocs/includes/restler/UI/Forms.php new file mode 100644 index 00000000000..289f40b6f0f --- /dev/null +++ b/htdocs/includes/restler/UI/Forms.php @@ -0,0 +1,434 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class Forms implements iFilter +{ + const FORM_KEY = 'form_key'; + public static $filterFormRequestsOnly = false; + + public static $excludedPaths = array(); + + public static $style; + /** + * @var bool should we fill up the form using given data? + */ + public static $preFill = true; + /** + * @var ValidationInfo + */ + public static $validationInfo = null; + protected static $inputTypes = array( + 'hidden', + 'password', + 'button', + 'image', + 'file', + 'reset', + 'submit', + 'search', + 'checkbox', + 'radio', + 'email', + 'text', + 'color', + 'date', + 'datetime', + 'datetime-local', + 'email', + 'month', + 'number', + 'range', + 'search', + 'tel', + 'time', + 'url', + 'week', + ); + protected static $fileUpload = false; + private static $key = array(); + /** + * @var ApiMethodInfo; + */ + private static $info; + + /** + * Get the form + * + * @param string $method http method to submit the form + * @param string $action relative path from the web root. When set to null + * it uses the current api method's path + * @param bool $dataOnly if you want to render the form yourself use this + * option + * @param string $prefix used for adjusting the spacing in front of + * form elements + * @param string $indent used for adjusting indentation + * + * @return array|T + * + * @throws \Luracast\Restler\RestException + */ + public static function get($method = 'POST', $action = null, $dataOnly = false, $prefix = '', $indent = ' ') + { + if (!static::$style) + static::$style = FormStyles::$html; + + try { + /** @var Restler $restler */ + $restler = Scope::get('Restler'); + if (is_null($action)) + $action = $restler->url; + + $info = $restler->url == $action + && Util::getRequestMethod() == $method + ? $restler->apiMethodInfo + : Routes::find( + trim($action, '/'), + $method, + $restler->getRequestedApiVersion(), + static::$preFill || + ($restler->requestMethod == $method && + $restler->url == $action) + ? $restler->getRequestData() + : array() + ); + + } catch (RestException $e) { + //echo $e->getErrorMessage(); + $info = false; + } + if (!$info) + throw new RestException(500, 'invalid action path for form `' . $method . ' ' . $action . '`'); + static::$info = $info; + $m = $info->metadata; + $r = static::fields($dataOnly); + if ($method != 'GET' && $method != 'POST') { + if (empty(Defaults::$httpMethodOverrideProperty)) + throw new RestException( + 500, + 'Forms require `Defaults::\$httpMethodOverrideProperty`' . + "for supporting HTTP $method" + ); + if ($dataOnly) { + $r[] = array( + 'tag' => 'input', + 'name' => Defaults::$httpMethodOverrideProperty, + 'type' => 'hidden', + 'value' => 'method', + ); + } else { + $r[] = T::input() + ->name(Defaults::$httpMethodOverrideProperty) + ->value($method) + ->type('hidden'); + } + + $method = 'POST'; + } + if (session_id() != '') { + $form_key = static::key($method, $action); + if ($dataOnly) { + $r[] = array( + 'tag' => 'input', + 'name' => static::FORM_KEY, + 'type' => 'hidden', + 'value' => 'hidden', + ); + } else { + $key = T::input() + ->name(static::FORM_KEY) + ->type('hidden') + ->value($form_key); + $r[] = $key; + } + } + + $s = array( + 'tag' => 'button', + 'type' => 'submit', + 'label' => + Util::nestedValue($m, 'return', CommentParser::$embeddedDataName, 'label') + ? : 'Submit' + ); + + if (!$dataOnly) + $s = Emmet::make(static::style('submit', $m), $s); + $r[] = $s; + $t = array( + 'action' => $restler->getBaseUrl() . '/' . rtrim($action, '/'), + 'method' => $method, + ); + if (static::$fileUpload) { + static::$fileUpload = false; + $t['enctype'] = 'multipart/form-data'; + } + if (!$dataOnly) { + $t = Emmet::make(static::style('form', $m), $t); + $t->prefix = $prefix; + $t->indent = $indent; + $t[] = $r; + } else { + $t['fields'] = $r; + } + return $t; + } + + public static function style($name, array $metadata, $type = '') + { + return isset($metadata[CommentParser::$embeddedDataName][$name]) + ? $metadata[CommentParser::$embeddedDataName][$name] + : (!empty($type) && isset(static::$style["$name-$type"]) + ? static::$style["$name-$type"] + : (isset(static::$style[$name]) + ? static::$style[$name] + : null + ) + ); + } + + public static function fields($dataOnly = false) + { + $m = static::$info->metadata; + $params = $m['param']; + $values = static::$info->parameters; + $r = array(); + foreach ($params as $k => $p) { + $value = Util::nestedValue($values, $k); + if ( + is_scalar($value) || + ($p['type'] == 'array' && is_array($value) && $value == array_values($value)) || + is_object($value) && $p['type'] == get_class($value) + ) + $p['value'] = $value; + static::$validationInfo = $v = new ValidationInfo($p); + if ($v->from == 'path') + continue; + if (!empty($v->children)) { + $t = Emmet::make(static::style('fieldset', $m), array('label' => $v->label)); + foreach ($v->children as $n => $c) { + $value = Util::nestedValue($v->value, $n); + if ( + is_scalar($value) || + ($c['type'] == 'array' && is_array($value) && $value == array_values($value)) || + is_object($value) && $c['type'] == get_class($value) + ) + $c['value'] = $value; + static::$validationInfo = $vc = new ValidationInfo($c); + if ($vc->from == 'path') + continue; + $vc->name = $v->name . '[' . $vc->name . ']'; + $t [] = static::field($vc, $dataOnly); + } + $r[] = $t; + static::$validationInfo = null; + } else { + $f = static::field($v, $dataOnly); + $r [] = $f; + } + static::$validationInfo = null; + } + return $r; + } + + /** + * @param ValidationInfo $p + * + * @param bool $dataOnly + * + * @return array|T + */ + public static function field(ValidationInfo $p, $dataOnly = false) + { + if (is_string($p->value)) { + //prevent XSS attacks + $p->value = htmlspecialchars($p->value, ENT_QUOTES | ENT_HTML401, 'UTF-8'); + } + $type = $p->field ? : static::guessFieldType($p); + $tag = in_array($type, static::$inputTypes) + ? 'input' : $type; + $options = array(); + $name = $p->name; + $multiple = null; + if ($p->type == 'array' && $p->contentType != 'associative') { + $name .= '[]'; + $multiple = true; + } + if ($p->choice) { + foreach ($p->choice as $i => $choice) { + $option = array('name' => $name, 'value' => $choice); + $option['text'] = isset($p->rules['select'][$i]) + ? $p->rules['select'][$i] + : $choice; + if ($choice == $p->value) + $option['selected'] = true; + $options[] = $option; + } + } elseif ($p->type == 'boolean' || $p->type == 'bool') { + if (String::beginsWith($type, 'radio')) { + $options[] = array('name' => $p->name, 'text' => ' Yes ', + 'value' => 'true'); + $options[] = array('name' => $p->name, 'text' => ' No ', + 'value' => 'false'); + if ($p->value || $p->default) + $options[0]['selected'] = true; + } else { + $r = array( + 'tag' => $tag, + 'name' => $name, + 'type' => $type, + 'label' => $p->label, + 'value' => 'true', + 'default' => $p->default, + ); + $r['text'] = 'Yes'; + if ($p->default) { + $r['selected'] = true; + } + } + } + if (empty($r)) { + $r = array( + 'tag' => $tag, + 'name' => $name, + 'type' => $type, + 'label' => $p->label, + 'value' => $p->value, + 'default' => $p->default, + 'options' => & $options, + 'multiple' => $multiple, + ); + } + if ($type == 'file') { + static::$fileUpload = true; + $r['accept'] = implode(', ', UploadFormat::$allowedMimeTypes); + } + + if (true === $p->required) + $r['required'] = true; + if (isset($p->rules['autofocus'])) + $r['autofocus'] = true; + /* + echo "
";
+        print_r($r);
+        echo "
"; + */ + if ($dataOnly) + return $r; + if (isset($p->rules['form'])) + return Emmet::make($p->rules['form'], $r); + $m = static::$info->metadata; + $t = Emmet::make(static::style($type, $m, $p->type) ? : static::style($tag, $m, $p->type), $r); + return $t; + } + + protected static function guessFieldType(ValidationInfo $p, $type = 'type') + { + if (in_array($p->$type, static::$inputTypes)) + return $p->$type; + if ($p->choice) + return $p->type == 'array' ? 'checkbox' : 'select'; + switch ($p->$type) { + case 'boolean': + return 'radio'; + case 'int': + case 'number': + case 'float': + return 'number'; + case 'array': + return static::guessFieldType($p, 'contentType'); + } + if ($p->name == 'password') + return 'password'; + return 'text'; + } + + /** + * Get the form key + * + * @param string $method http method for form key + * @param string $action relative path from the web root. When set to null + * it uses the current api method's path + * + * @return string generated form key + */ + public static function key($method = 'POST', $action = null) + { + if (is_null($action)) + $action = Scope::get('Restler')->url; + $target = "$method $action"; + if (empty(static::$key[$target])) + static::$key[$target] = md5($target . User::getIpAddress() . uniqid(mt_rand())); + $_SESSION[static::FORM_KEY] = static::$key; + return static::$key[$target]; + } + + /** + * Access verification method. + * + * API access will be denied when this method returns false + * + * @return boolean true when api access is allowed false otherwise + * + * @throws RestException 403 security violation + */ + public function __isAllowed() + { + if (session_id() == '') { + session_start(); + } + /** @var Restler $restler */ + $restler = $this->restler; + $url = $restler->url; + foreach (static::$excludedPaths as $exclude) { + if (empty($exclude)) { + if ($url == $exclude) + return true; + } elseif (String::beginsWith($url, $exclude)) { + return true; + } + } + $check = static::$filterFormRequestsOnly + ? $restler->requestFormat instanceof UrlEncodedFormat || $restler->requestFormat instanceof UploadFormat + : true; + if (!empty($_POST) && $check) { + if ( + isset($_POST[static::FORM_KEY]) && + ($target = Util::getRequestMethod() . ' ' . $restler->url) && + isset($_SESSION[static::FORM_KEY][$target]) && + $_POST[static::FORM_KEY] == $_SESSION[static::FORM_KEY][$target] + ) { + return true; + } + throw new RestException(403, 'Insecure form submission'); + } + return true; + } +} \ No newline at end of file diff --git a/htdocs/includes/restler/UI/Nav.php b/htdocs/includes/restler/UI/Nav.php new file mode 100644 index 00000000000..7245dbddf8d --- /dev/null +++ b/htdocs/includes/restler/UI/Nav.php @@ -0,0 +1,208 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class Nav +{ + public static $root = 'home'; + /** + * @var null|callable if the api methods are under access control mechanism + * you can attach a function here that returns true or false to determine + * visibility of a protected api method. this function will receive method + * info as the only parameter. + */ + public static $accessControlFunction = null; + /** + * @var array all paths beginning with any of the following will be excluded + * from documentation. if an empty string is given it will exclude the root + */ + public static $excludedPaths = array(''); + /** + * @var array prefix additional menu items with one of the following syntax + * [$path => $text] + * [$path] + * [$path => ['text' => $text, 'url' => $url]] + */ + public static $prepends = array(); + /** + * @var array suffix additional menu items with one of the following syntax + * [$path => $text] + * [$path] + * [$path => ['text' => $text, 'url' => $url]] + */ + public static $appends = array(); + + public static $addExtension = true; + + protected static $extension = ''; + + public static function get($for = '', $activeUrl = null) + { + if (!static::$accessControlFunction && Defaults::$accessControlFunction) + static::$accessControlFunction = Defaults::$accessControlFunction; + /** @var Restler $restler */ + $restler = Scope::get('Restler'); + if (static::$addExtension) + static::$extension = '.' . $restler->responseFormat->getExtension(); + if (is_null($activeUrl)) + $activeUrl = $restler->url; + + $tree = array(); + foreach (static::$prepends as $path => $text) { + $url = null; + if (is_array($text)) { + if (isset($text['url'])) { + $url = $text['url']; + $text = $text['text']; + } else { + $url = current(array_keys($text)); + $text = current($text); + } + } + if (is_numeric($path)) { + $path = $text; + $text = null; + } + if (empty($for) || 0 === strpos($path, "$for/")) + static::build($tree, $path, $url, $text, $activeUrl); + } + $routes = Routes::toArray(); + $routes = $routes['v' . $restler->getRequestedApiVersion()]; + foreach ($routes as $value) { + foreach ($value as $httpMethod => $route) { + if ($httpMethod != 'GET') { + continue; + } + $path = $route['url']; + if (false !== strpos($path, '{')) + continue; + if ($route['accessLevel'] > 1 && !Util::$restler->_authenticated) + continue; + foreach (static::$excludedPaths as $exclude) { + if (empty($exclude)) { + if (empty($path)) + continue 2; + } elseif (0 === strpos($path, $exclude)) { + continue 2; + } + } + if ($restler->_authenticated + && static::$accessControlFunction + && (!call_user_func( + static::$accessControlFunction, $route['metadata'])) + ) { + continue; + } + $text = Util::nestedValue( + $route, + 'metadata', + CommentParser::$embeddedDataName, + 'label' + ); + if (empty($for) || 0 === strpos($path, "$for/")) + static::build($tree, $path, null, $text, $activeUrl); + } + } + foreach (static::$appends as $path => $text) { + $url = null; + if (is_array($text)) { + if (isset($text['url'])) { + $url = $text['url']; + $text = $text['text']; + } else { + $url = current(array_keys($text)); + $text = current($text); + } + } + if (is_numeric($path)) { + $path = $text; + $text = null; + } + if (empty($for) || 0 === strpos($path, "$for/")) + static::build($tree, $path, $url, $text, $activeUrl); + } + if (!empty($for)) { + $for = explode('/', $for); + $p = & $tree; + foreach ($for as $f) { + if (isset($p[$f]['children'])) { + $p = & $p[$f]['children']; + } else { + return array(); + } + } + return $p; + } + return $tree; + } + + protected static function build(&$tree, $path, + $url = null, $text = null, $activeUrl = null) + { + $parts = explode('/', $path); + if (count($parts) == 1 && empty($parts[0])) + $parts = array(static::$root); + $p = & $tree; + $end = end($parts); + foreach ($parts as $part) { + if (!isset($p[$part])) { + $p[$part] = array( + 'href' => '#', + 'text' => static::title($part) + ); + if ($part == $end) { + $p[$part]['class'] = $part; + if ($text) + $p[$part]['text'] = $text; + if (is_null($url)) { + if (empty($path) && !empty(static::$extension)) + $path = 'index'; + $p[$part]['href'] = Util::$restler->getBaseUrl() + . '/' . $path . static::$extension; + } else { + if (empty($url) && !empty(static::$extension)) + $url = 'index'; + $p[$part]['href'] = $url . static::$extension; + } + if ($path == $activeUrl) { + $p[$part]['active'] = true; + } + } + $p[$part]['children'] = array(); + + } + $p = & $p[$part]['children']; + } + + } + + protected static function title($name) + { + if (empty($name)) { + $name = static::$root; + } else { + $name = ltrim($name, '#'); + } + return ucfirst(preg_replace(array('/(?<=[^A-Z])([A-Z])/', '/(?<=[^0-9])([0-9])/'), ' $0', $name)); + } + +} \ No newline at end of file diff --git a/htdocs/includes/restler/UI/Tags.php b/htdocs/includes/restler/UI/Tags.php new file mode 100644 index 00000000000..b6302a28236 --- /dev/null +++ b/htdocs/includes/restler/UI/Tags.php @@ -0,0 +1,282 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + * + * ============================ magic properties ============================== + * @property Tags parent parent tag + * ============================== magic methods =============================== + * @method Tags name(string $value) name attribute + * @method Tags action(string $value) action attribute + * @method Tags placeholder(string $value) placeholder attribute + * @method Tags value(string $value) value attribute + * @method Tags required(boolean $value) required attribute + * @method Tags class(string $value) required attribute + * + * =========================== static magic methods ============================ + * @method static Tags form() creates a html form + * @method static Tags input() creates a html input element + * @method static Tags button() creates a html button element + * + */ +class Tags implements ArrayAccess, Countable +{ + public static $humanReadable = true; + public static $initializer = null; + protected static $instances = array(); + public $prefix = ''; + public $indent = ' '; + public $tag; + protected $attributes = array(); + protected $children = array(); + protected $_parent; + + public function __construct($name = null, array $children = array()) + { + $this->tag = $name; + $c = array(); + foreach ($children as $child) { + is_array($child) + ? $c = array_merge($c, $child) + : $c [] = $child; + } + $this->markAsChildren($c); + $this->children = $c; + if (static::$initializer) + call_user_func_array(static::$initializer, array(& $this)); + } + + /** + * Get Tag by id + * + * Retrieve a tag by its id attribute + * + * @param string $id + * + * @return Tags|null + */ + public static function byId($id) + { + return Util::nestedValue(static::$instances, $id); + } + + /** + * @param $name + * @param array $children + * + * @return Tags + */ + public static function __callStatic($name, array $children) + { + return new static($name, $children); + } + + public function toString($prefix = '', $indent = ' ') + { + $this->prefix = $prefix; + $this->indent = $indent; + return $this->__toString(); + } + + public function __toString() + { + $children = ''; + if (static::$humanReadable) { + $lineBreak = false; + foreach ($this->children as $key => $child) { + $prefix = $this->prefix; + if (!is_null($this->tag)) + $prefix .= $this->indent; + if ($child instanceof $this) { + $child->prefix = $prefix; + $child->indent = $this->indent; + $children .= PHP_EOL . $child; + $lineBreak = true; + } else { + $children .= $child; + } + } + if ($lineBreak) + $children .= PHP_EOL . $this->prefix; + } else { + $children = implode('', $this->children); + } + if (is_null($this->tag)) + return $children; + $attributes = ''; + foreach ($this->attributes as $attribute => &$value) + $attributes .= " $attribute=\"$value\""; + + if (count($this->children)) + return static::$humanReadable + ? "$this->prefix<{$this->tag}{$attributes}>" + . "$children" + . "tag}>" + : "<{$this->tag}{$attributes}>$childrentag}>"; + + return "$this->prefix<{$this->tag}{$attributes}/>"; + } + + public function toArray() + { + $r = array(); + $r['attributes'] = $this->attributes; + $r['tag'] = $this->tag; + $children = array(); + foreach ($this->children as $key => $child) { + $children[$key] = $child instanceof $this + ? $child->toArray() + : $child; + } + $r['children'] = $children; + return $r; + } + + /** + * Set the id attribute of the current tag + * + * @param string $value + * + * @return string + */ + public function id($value) + { + $this->attributes['id'] = isset($value) + ? (string)$value + : Util::nestedValue($this->attributes, 'name'); + static::$instances[$value] = $this; + return $this; + } + + public function __get($name) + { + if ('parent' == $name) + return $this->_parent; + if (isset($this->attributes[$name])) + return $this->attributes[$name]; + return; + } + + public function __set($name, $value) + { + if ('parent' == $name) { + if ($this->_parent) { + unset($this->_parent[array_search($this, $this->_parent->children)]); + } + if (!empty($value)) { + $value[] = $this; + } + } + } + + public function __isset($name) + { + return isset($this->attributes[$name]); + } + + /** + * @param $attribute + * @param $value + * + * @return Tags + */ + public function __call($attribute, $value) + { + if (is_null($value)) { + return isset($this->attributes[$attribute]) + ? $this->attributes[$attribute] + : null; + } + $value = $value[0]; + if (is_null($value)) { + unset($this->attributes[$attribute]); + return $this; + } + $this->attributes[$attribute] = is_bool($value) + ? ($value ? 'true' : 'false') + : @(string)$value; + return $this; + } + + public function offsetGet($index) + { + if ($this->offsetExists($index)) { + return $this->children[$index]; + } + return false; + } + + public function offsetExists($index) + { + return isset($this->children[$index]); + } + + public function offsetSet($index, $value) + { + if ($index) { + $this->children[$index] = $value; + } elseif (is_array($value)) { + $c = array(); + foreach ($value as $child) { + is_array($child) + ? $c = array_merge($c, $child) + : $c [] = $child; + } + $this->markAsChildren($c); + $this->children += $c; + } else { + $c = array($value); + $this->markAsChildren($c); + $this->children[] = $value; + } + return true; + } + + public function offsetUnset($index) + { + $this->children[$index]->_parent = null; + unset($this->children[$index]); + return true; + } + + public function getContents() + { + return $this->children; + } + + public function count() + { + return count($this->children); + } + + private function markAsChildren(& $children) + { + foreach ($children as $i => $child) { + if (is_string($child)) + continue; + if (!is_object($child)) { + unset($children[$i]); + continue; + } + //echo $child; + if (isset($child->_parent) && $child->_parent != $this) { + //remove from current parent + unset($child->_parent[array_search($child, $child->_parent->children)]); + } + $child->_parent = $this; + } + } +} \ No newline at end of file diff --git a/htdocs/includes/restler/User.php b/htdocs/includes/restler/User.php new file mode 100644 index 00000000000..ea7ff79d603 --- /dev/null +++ b/htdocs/includes/restler/User.php @@ -0,0 +1,100 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class User implements iIdentifyUser +{ + private static $initialized = false; + public static $id = null; + public static $cacheId = null; + public static $ip; + public static $browser = ''; + public static $platform = ''; + + public static function init() + { + static::$initialized = true; + static::$ip = static::getIpAddress(); + } + + public static function getUniqueIdentifier($includePlatform = false) + { + if (!static::$initialized) static::init(); + return static::$id ? : base64_encode('ip:' . ($includePlatform + ? static::$ip . '-' . static::$platform + : static::$ip + )); + } + + public static function getIpAddress($ignoreProxies = false) + { + foreach (array('HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', + 'HTTP_X_FORWARDED', 'HTTP_X_CLUSTER_CLIENT_IP', + 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', + 'REMOTE_ADDR') as $key) { + if (array_key_exists($key, $_SERVER) === true) { + foreach (explode(',', $_SERVER[$key]) as $ip) { + $ip = trim($ip); // just to be safe + + if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 + | FILTER_FLAG_NO_PRIV_RANGE + | FILTER_FLAG_NO_RES_RANGE) !== false + ) { + return $ip; + } + } + } + } + } + + /** + * Authentication classes should call this method + * + * @param string $id user id as identified by the authentication classes + * + * @return void + */ + public static function setUniqueIdentifier($id) + { + static::$id = $id; + } + + /** + * User identity to be used for caching purpose + * + * When the dynamic cache service places an object in the cache, it needs to + * label it with a unique identifying string known as a cache ID. This + * method gives that identifier + * + * @return string + */ + public static function getCacheIdentifier() + { + return static::$cacheId ?: static::$id; + } + + /** + * User identity for caching purpose + * + * In a role based access control system this will be based on role + * + * @param $id + * + * @return void + */ + public static function setCacheIdentifier($id) + { + static::$cacheId = $id; + } +} diff --git a/htdocs/includes/restler/Util.php b/htdocs/includes/restler/Util.php new file mode 100644 index 00000000000..0a1fc8a40ea --- /dev/null +++ b/htdocs/includes/restler/Util.php @@ -0,0 +1,201 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +class Util +{ + /** + * @var Restler instance injected at runtime + */ + public static $restler; + + /** + * verify if the given data type string is scalar or not + * + * @static + * + * @param string $type data type as string + * + * @return bool true or false + */ + public static function isObjectOrArray($type) + { + if (is_array($type)) { + foreach ($type as $t) { + if (static::isObjectOrArray($t)) { + return true; + } + } + return false; + } + return !(boolean)strpos('|bool|boolean|int|float|string|', $type); + } + + /** + * Get the value deeply nested inside an array / object + * + * Using isset() to test the presence of nested value can give a false positive + * + * This method serves that need + * + * When the deeply nested property is found its value is returned, otherwise + * false is returned. + * + * @param array $from array to extract the value from + * @param string|array $key ... pass more to go deeply inside the array + * alternatively you can pass a single array + * + * @return null|mixed null when not found, value otherwise + */ + public static function nestedValue($from, $key/**, $key2 ... $key`n` */) + { + if (is_array($key)) { + $keys = $key; + } else { + $keys = func_get_args(); + array_shift($keys); + } + foreach ($keys as $key) { + if (is_array($from) && isset($from[$key])) { + $from = $from[$key]; + continue; + } elseif (is_object($from) && isset($from->{$key})) { + $from = $from->{$key}; + continue; + } + return null; + } + return $from; + } + + public static function getResourcePath($className, + $resourcePath = null, + $prefix = '') + { + if (is_null($resourcePath)) { + if (Defaults::$autoRoutingEnabled) { + $resourcePath = strtolower($className); + if (false !== ($index = strrpos($className, '\\'))) + $resourcePath = substr($resourcePath, $index + 1); + if (false !== ($index = strrpos($resourcePath, '_'))) + $resourcePath = substr($resourcePath, $index + 1); + } else { + $resourcePath = ''; + } + } else + $resourcePath = trim($resourcePath, '/'); + if (strlen($resourcePath) > 0) + $resourcePath .= '/'; + return $prefix . $resourcePath; + } + + /** + * Compare two strings and remove the common + * sub string from the first string and return it + * + * @static + * + * @param string $fromPath + * @param string $usingPath + * @param string $char + * optional, set it as + * blank string for char by char comparison + * + * @return string + */ + public static function removeCommonPath($fromPath, $usingPath, $char = '/') + { + if (empty($fromPath)) + return ''; + $fromPath = explode($char, $fromPath); + $usingPath = explode($char, $usingPath); + while (count($usingPath)) { + if (count($fromPath) && $fromPath[0] == $usingPath[0]) { + array_shift($fromPath); + } else { + break; + } + array_shift($usingPath); + } + return implode($char, $fromPath); + } + + /** + * Parses the request to figure out the http request type + * + * @static + * + * @return string which will be one of the following + * [GET, POST, PUT, PATCH, DELETE] + * @example GET + */ + public static function getRequestMethod() + { + $method = $_SERVER['REQUEST_METHOD']; + if (isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) { + $method = $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']; + } elseif ( + !empty(Defaults::$httpMethodOverrideProperty) + && isset($_REQUEST[Defaults::$httpMethodOverrideProperty]) + ) { + // support for exceptional clients who can't set the header + $m = strtoupper($_REQUEST[Defaults::$httpMethodOverrideProperty]); + if ($m == 'PUT' || $m == 'DELETE' || + $m == 'POST' || $m == 'PATCH' + ) { + $method = $m; + } + } + // support for HEAD request + if ($method == 'HEAD') { + $method = 'GET'; + } + return $method; + } + + /** + * Pass any content negotiation header such as Accept, + * Accept-Language to break it up and sort the resulting array by + * the order of negotiation. + * + * @static + * + * @param string $accept header value + * + * @return array sorted by the priority + */ + public static function sortByPriority($accept) + { + $acceptList = array(); + $accepts = explode(',', strtolower($accept)); + if (!is_array($accepts)) { + $accepts = array($accepts); + } + foreach ($accepts as $pos => $accept) { + $parts = explode(';q=', trim($accept)); + $type = array_shift($parts); + $quality = count($parts) ? + floatval(array_shift($parts)) : + (1000 - $pos) / 1000; + $acceptList[$type] = $quality; + } + arsort($acceptList); + return $acceptList; + } + + public static function getShortName($className) + { + $className = explode('\\', $className); + return end($className); + } +} + diff --git a/htdocs/includes/restler/compatibility/iAuthenticate.php b/htdocs/includes/restler/compatibility/iAuthenticate.php new file mode 100644 index 00000000000..0463df948f1 --- /dev/null +++ b/htdocs/includes/restler/compatibility/iAuthenticate.php @@ -0,0 +1,10 @@ +isFile() + && 'php' === $fileInfo->getExtension() + && ctype_lower($fileInfo->getBasename('.php')) + && preg_match( + '/^ *(class|interface|abstract +class)' + . ' +([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)/m', + file_get_contents($fileInfo->getPathname()), + $matches + ) + ) + $classMap[$matches[2]] = $fileInfo->getPathname(); + +AutoLoader::seen($classMap); + +//changes in iAuthenticate +Defaults::$authenticationMethod = '__isAuthenticated'; + +include __DIR__ . '/iAuthenticate.php'; + +//changes in auto routing +Defaults::$smartAutoRouting = false; +Defaults::$smartParameterParsing = false; +Defaults::$autoValidationEnabled = false; + +//changes in parsing embedded data in comments +CommentParser::$embeddedDataPattern = '/\((\S+)\)/ms'; +CommentParser::$embeddedDataIndex = 1; \ No newline at end of file diff --git a/htdocs/includes/restler/composer.json b/htdocs/includes/restler/composer.json new file mode 100644 index 00000000000..1e096989b01 --- /dev/null +++ b/htdocs/includes/restler/composer.json @@ -0,0 +1,70 @@ +{ + "name":"restler/framework", + "description":"Just the Restler Framework without the tests and examples", + "type":"library", + "keywords":["server","api","framework","REST"], + "homepage":"http://luracast.com/products/restler/", + "license":"LGPL-2.1", + "authors":[ + { + "name":"Luracast", + "email":"arul@luracast.com" + }, + { + "name":"Nick nickl- Lombard", + "email":"github@jigsoft.co.za" + } + ], + "extra":{ + "branch-alias":{ + "master":"v3.0.x-dev" + } + }, + "suggest":{ + "luracast/explorer":"Restler's very own api explorer (see require-dev for details)", + "rodneyrehm/plist":"Restler supports tho Apple plist xml format (see require-dev for details)", + "zendframework/zendamf":"Support for the amf document format (see require-dev for details)", + "symfony/yaml":"Restler can produce content in yaml format as well (see require-dev for details)", + "twig/twig":"Restler can render HtmlView using twig templates (see require-dev for details)", + "mustache/mustache":"Restler can render HtmlView using mustache/handlebar templates (see require-dev for details)", + "bshaffer/oauth2-server-php":"Restler can provide OAuth2 authentication using this library (see require-dev for details)" + }, + "require":{ + "php":">=5.3.0" + }, + "require-dev":{ + "luracast/explorer":"*", + "rodneyrehm/plist":"dev-master", + "zendframework/zendamf":"dev-master", + "symfony/yaml":"*", + "mustache/mustache": "dev-master", + "twig/twig": "v1.13.0", + "bshaffer/oauth2-server-php":"v1.0" + }, + "repositories":[ + { + "type":"vcs", + "url":"https://github.com/zendframework/ZendAmf.git" + }, + { + "type":"package", + "package":{ + "name":"luracast/explorer", + "version":"v3.0.0", + "dist":{ + "type":"zip", + "url":"https://github.com/Luracast/Restler-API-Explorer/zipball/v3.0.0" + } + } + } + ], + "autoload":{ + "psr-0":{ + "Luracast\\Restler":"" + } + }, + "target-dir": "Luracast/Restler", + "replace": { + "luracast/restler":"3.*" + } +} \ No newline at end of file diff --git a/htdocs/includes/restler/composer.lock b/htdocs/includes/restler/composer.lock new file mode 100644 index 00000000000..914abc5e16d --- /dev/null +++ b/htdocs/includes/restler/composer.lock @@ -0,0 +1,23 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "This file is @generated automatically" + ], + "hash": "ee84444dcf34101555d20a813d528c44", + "packages": [], + "packages-dev": null, + "aliases": [], + "minimum-stability": "stable", + "stability-flags": { + "rodneyrehm/plist": 20, + "zendframework/zendamf": 20, + "mustache/mustache": 20 + }, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=5.3.0" + }, + "platform-dev": [] +} diff --git a/htdocs/includes/restler/iAuthenticate.php b/htdocs/includes/restler/iAuthenticate.php new file mode 100644 index 00000000000..6e71f0fc291 --- /dev/null +++ b/htdocs/includes/restler/iAuthenticate.php @@ -0,0 +1,25 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +interface iAuthenticate extends iFilter +{ + /** + * @return string string to be used with WWW-Authenticate header + * @example Basic + * @example Digest + * @example OAuth + */ + public function __getWWWAuthenticateString(); +} diff --git a/htdocs/includes/restler/iCache.php b/htdocs/includes/restler/iCache.php new file mode 100755 index 00000000000..53a7a9da6a7 --- /dev/null +++ b/htdocs/includes/restler/iCache.php @@ -0,0 +1,63 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +interface iCache +{ + /** + * store data in the cache + * + * @abstract + * + * @param string $name + * @param mixed $data + * + * @return boolean true if successful + */ + public function set($name, $data); + + /** + * retrieve data from the cache + * + * @abstract + * + * @param string $name + * @param bool $ignoreErrors + * + * @return mixed + */ + public function get($name, $ignoreErrors = false); + + /** + * delete data from the cache + * + * @abstract + * + * @param string $name + * @param bool $ignoreErrors + * + * @return boolean true if successful + */ + public function clear($name, $ignoreErrors = false); + + /** + * check if the given name is cached + * + * @abstract + * + * @param string $name + * + * @return boolean true if cached + */ + public function isCached($name); +} + diff --git a/htdocs/includes/restler/iCompose.php b/htdocs/includes/restler/iCompose.php new file mode 100644 index 00000000000..a37d24d867d --- /dev/null +++ b/htdocs/includes/restler/iCompose.php @@ -0,0 +1,36 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +interface iCompose { + /** + * Result of an api call is passed to this method + * to create a standard structure for the data + * + * @param mixed $result can be a primitive or array or object + */ + public function response($result); + + /** + * When the api call results in RestException this method + * will be called to return the error message + * + * @param RestException $exception exception that has reasons for failure + * + * @return + */ + public function message(RestException $exception); +} \ No newline at end of file diff --git a/htdocs/includes/restler/iFilter.php b/htdocs/includes/restler/iFilter.php new file mode 100644 index 00000000000..40205e5def8 --- /dev/null +++ b/htdocs/includes/restler/iFilter.php @@ -0,0 +1,30 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +interface iFilter +{ + /** + * Access verification method. + * + * API access will be denied when this method returns false + * + * @abstract + * @return boolean true when api access is allowed false otherwise + */ + public function __isAllowed(); + +} + diff --git a/htdocs/includes/restler/iIdentifyUser.php b/htdocs/includes/restler/iIdentifyUser.php new file mode 100644 index 00000000000..9ba061d3327 --- /dev/null +++ b/htdocs/includes/restler/iIdentifyUser.php @@ -0,0 +1,63 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +interface iIdentifyUser +{ + /** + * A way to uniquely identify the current api consumer + * + * When his user id is known it should be used otherwise ip address + * can be used + * + * @param bool $includePlatform Should we consider user alone or should + * consider the application/platform/device + * as well for generating unique id + * + * @return string + */ + public static function getUniqueIdentifier($includePlatform = false); + + /** + * User identity to be used for caching purpose + * + * When the dynamic cache service places an object in the cache, it needs to + * label it with a unique identifying string known as a cache ID. This + * method gives that identifier + * + * @return string + */ + public static function getCacheIdentifier(); + + /** + * Authentication classes should call this method + * + * @param string $id user id as identified by the authentication classes + * + * @return void + */ + public static function setUniqueIdentifier($id); + + /** + * User identity for caching purpose + * + * In a role based access control system this will be based on role + * + * @param $id + * + * @return void + */ + public static function setCacheIdentifier($id); +} \ No newline at end of file diff --git a/htdocs/includes/restler/iProvideMultiVersionApi.php b/htdocs/includes/restler/iProvideMultiVersionApi.php new file mode 100644 index 00000000000..ed74dd1b9e6 --- /dev/null +++ b/htdocs/includes/restler/iProvideMultiVersionApi.php @@ -0,0 +1,11 @@ + + * @copyright 2010 Luracast + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://luracast.com/products/restler/ + * @version 3.0.0rc5 + */ +interface iUseAuthentication +{ + /** + * This method will be called first for filter classes and api classes so + * that they can respond accordingly for filer method call and api method + * calls + * + * @abstract + * + * @param bool $isAuthenticated passes true when the authentication is + * done false otherwise + * + * @return mixed + */ + public function __setAuthenticationStatus($isAuthenticated=false); +} + diff --git a/htdocs/includes/restler/vendor/autoload.php b/htdocs/includes/restler/vendor/autoload.php new file mode 100644 index 00000000000..bf60b992d40 --- /dev/null +++ b/htdocs/includes/restler/vendor/autoload.php @@ -0,0 +1,7 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Autoload; + +/** + * ClassLoader implements a PSR-0 class loader + * + * See https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-0.md + * + * $loader = new \Composer\Autoload\ClassLoader(); + * + * // register classes with namespaces + * $loader->add('Symfony\Component', __DIR__.'/component'); + * $loader->add('Symfony', __DIR__.'/framework'); + * + * // activate the autoloader + * $loader->register(); + * + * // to enable searching the include path (eg. for PEAR packages) + * $loader->setUseIncludePath(true); + * + * In this example, if you try to use a class in the Symfony\Component + * namespace or one of its children (Symfony\Component\Console for instance), + * the autoloader will first look for the class under the component/ + * directory, and it will then fallback to the framework/ directory if not + * found before giving up. + * + * This class is loosely based on the Symfony UniversalClassLoader. + * + * @author Fabien Potencier + * @author Jordi Boggiano + */ +class ClassLoader +{ + // PSR-4 + private $prefixLengthsPsr4 = array(); + private $prefixDirsPsr4 = array(); + private $fallbackDirsPsr4 = array(); + + // PSR-0 + private $prefixesPsr0 = array(); + private $fallbackDirsPsr0 = array(); + + private $useIncludePath = false; + private $classMap = array(); + + private $classMapAuthoritative = false; + + public function getPrefixes() + { + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', $this->prefixesPsr0); + } + + return array(); + } + + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param array $classMap Class to filename map + */ + public function addClassMap(array $classMap) + { + if ($this->classMap) { + $this->classMap = array_merge($this->classMap, $classMap); + } else { + $this->classMap = $classMap; + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, either + * appending or prepending to the ones previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + */ + public function add($prefix, $paths, $prepend = false) + { + if (!$prefix) { + if ($prepend) { + $this->fallbackDirsPsr0 = array_merge( + (array) $paths, + $this->fallbackDirsPsr0 + ); + } else { + $this->fallbackDirsPsr0 = array_merge( + $this->fallbackDirsPsr0, + (array) $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset($this->prefixesPsr0[$first][$prefix])) { + $this->prefixesPsr0[$first][$prefix] = (array) $paths; + + return; + } + if ($prepend) { + $this->prefixesPsr0[$first][$prefix] = array_merge( + (array) $paths, + $this->prefixesPsr0[$first][$prefix] + ); + } else { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $this->prefixesPsr0[$first][$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, either + * appending or prepending to the ones previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-0 base directories + * @param bool $prepend Whether to prepend the directories + * + * @throws \InvalidArgumentException + */ + public function addPsr4($prefix, $paths, $prepend = false) + { + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + $this->fallbackDirsPsr4 = array_merge( + (array) $paths, + $this->fallbackDirsPsr4 + ); + } else { + $this->fallbackDirsPsr4 = array_merge( + $this->fallbackDirsPsr4, + (array) $paths + ); + } + } elseif (!isset($this->prefixDirsPsr4[$prefix])) { + // Register directories for a new namespace. + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + (array) $paths, + $this->prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $this->prefixDirsPsr4[$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, + * replacing any others previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 base directories + */ + public function set($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr0 = (array) $paths; + } else { + $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, + * replacing any others previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * + * @throws \InvalidArgumentException + */ + public function setPsr4($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr4 = (array) $paths; + } else { + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } + } + + /** + * Turns on searching the include path for class files. + * + * @param bool $useIncludePath + */ + public function setUseIncludePath($useIncludePath) + { + $this->useIncludePath = $useIncludePath; + } + + /** + * Can be used to check if the autoloader uses the include path to check + * for classes. + * + * @return bool + */ + public function getUseIncludePath() + { + return $this->useIncludePath; + } + + /** + * Turns off searching the prefix and fallback directories for classes + * that have not been registered with the class map. + * + * @param bool $classMapAuthoritative + */ + public function setClassMapAuthoritative($classMapAuthoritative) + { + $this->classMapAuthoritative = $classMapAuthoritative; + } + + /** + * Should class lookup fail if not found in the current class map? + * + * @return bool + */ + public function isClassMapAuthoritative() + { + return $this->classMapAuthoritative; + } + + /** + * Registers this instance as an autoloader. + * + * @param bool $prepend Whether to prepend the autoloader or not + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + } + + /** + * Unregisters this instance as an autoloader. + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * @return bool|null True if loaded, null otherwise + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + includeFile($file); + + return true; + } + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class The name of the class + * + * @return string|false The path if found, false otherwise + */ + public function findFile($class) + { + // work around for PHP 5.3.0 - 5.3.2 https://bugs.php.net/50731 + if ('\\' == $class[0]) { + $class = substr($class, 1); + } + + // class map lookup + if (isset($this->classMap[$class])) { + return $this->classMap[$class]; + } + if ($this->classMapAuthoritative) { + return false; + } + + $file = $this->findFileWithExtension($class, '.php'); + + // Search for Hack files if we are running on HHVM + if ($file === null && defined('HHVM_VERSION')) { + $file = $this->findFileWithExtension($class, '.hh'); + } + + if ($file === null) { + // Remember that this class does not exist. + return $this->classMap[$class] = false; + } + + return $file; + } + + private function findFileWithExtension($class, $ext) + { + // PSR-4 lookup + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; + + $first = $class[0]; + if (isset($this->prefixLengthsPsr4[$first])) { + foreach ($this->prefixLengthsPsr4[$first] as $prefix => $length) { + if (0 === strpos($class, $prefix)) { + foreach ($this->prefixDirsPsr4[$prefix] as $dir) { + if (is_file($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) { + return $file; + } + } + } + } + } + + // PSR-4 fallback dirs + foreach ($this->fallbackDirsPsr4 as $dir) { + if (is_file($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // PSR-0 lookup + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; + } + + if (isset($this->prefixesPsr0[$first])) { + foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (is_file($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // PSR-0 fallback dirs + foreach ($this->fallbackDirsPsr0 as $dir) { + if (is_file($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + + // PSR-0 include paths. + if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { + return $file; + } + } +} + +/** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + */ +function includeFile($file) +{ + include $file; +} diff --git a/htdocs/includes/restler/vendor/composer/autoload_classmap.php b/htdocs/includes/restler/vendor/composer/autoload_classmap.php new file mode 100644 index 00000000000..7a91153b0d8 --- /dev/null +++ b/htdocs/includes/restler/vendor/composer/autoload_classmap.php @@ -0,0 +1,9 @@ + array($baseDir . '/'), +); diff --git a/htdocs/includes/restler/vendor/composer/autoload_psr4.php b/htdocs/includes/restler/vendor/composer/autoload_psr4.php new file mode 100644 index 00000000000..b265c64a22f --- /dev/null +++ b/htdocs/includes/restler/vendor/composer/autoload_psr4.php @@ -0,0 +1,9 @@ + $path) { + $loader->set($namespace, $path); + } + + $map = require __DIR__ . '/autoload_psr4.php'; + foreach ($map as $namespace => $path) { + $loader->setPsr4($namespace, $path); + } + + $classMap = require __DIR__ . '/autoload_classmap.php'; + if ($classMap) { + $loader->addClassMap($classMap); + } + + spl_autoload_register(array('ComposerAutoloaderInite65e15efc7e9ea1f2b7ba7fa697ba485', 'autoload'), true, true); + + $loader->register(true); + + return $loader; + } + + public static function autoload($class) + { + $dir = dirname(dirname(__DIR__)) . '/'; + $prefixes = array('Luracast\\Restler'); + foreach ($prefixes as $prefix) { + if (0 !== strpos($class, $prefix)) { + continue; + } + $path = $dir . implode('/', array_slice(explode('\\', $class), 2)).'.php'; + if (!$path = stream_resolve_include_path($path)) { + return false; + } + require $path; + + return true; + } + } +} + +function composerRequiree65e15efc7e9ea1f2b7ba7fa697ba485($file) +{ + require $file; +} diff --git a/htdocs/includes/restler/vendor/composer/installed.json b/htdocs/includes/restler/vendor/composer/installed.json new file mode 100644 index 00000000000..fe51488c706 --- /dev/null +++ b/htdocs/includes/restler/vendor/composer/installed.json @@ -0,0 +1 @@ +[] diff --git a/htdocs/includes/restler/views/debug.css b/htdocs/includes/restler/views/debug.css new file mode 100644 index 00000000000..130fdb5acc0 --- /dev/null +++ b/htdocs/includes/restler/views/debug.css @@ -0,0 +1,441 @@ +@CHARSET "UTF-8"; + +h2, h3 { + color: #ffffff; + background-color: #363636; + margin-bottom: 0; + padding: 10px; + -webkit-box-shadow: 4px 4px 4px 2px rgba(0, 0, 0, .4); + box-shadow: 4px 4px 4px 2px rgba(0, 0, 0, .4); +} + +body { + margin: 0; + padding: 0; + font-family: Helvetica, Arial, sans-serif; + font-size: 14px; + color: #000; + background-image: url(data:image/png;base64,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); +} + +article { + left: 20px; + right: 20px; + position: absolute; + overflow: auto; +} + +header { + color: #ffffff; + display: block; + background-color: #000000; + margin: 0; + padding: 10px; +} + +ul { + padding: 20px; + list-style-type: none; + border: 1px solid #BBBBBB; + background-color: #F9F9F9; + margin-top: 0; +} + +ul ul { + border: none; + border-top: 1px solid #BBBBBB; + border-left: 2px dotted #CCCCCC; + background-color: #EFEFEF; +} + +ul ul ul { + background-color: #E0E0E0; +} + +ul ul ul ul { + background-color: #CFCFCF; +} + +ul ul ul ul ul { + background-color: #E0E0E0; +} + +ul ul ul ul ul ul { + background-color: #EFEFEF; +} + +ul ul ul ul ul ul ul { + background-color: #FFFFFF; +} + +li { + padding: 4px +} + +a { + color: #2368AF; +} + +pre { + padding: 20px; + background-color: #EEEEEE; + border: 1px solid #BBBBBB; + overflow: auto; + margin-top: 0; + -webkit-box-shadow: 4px 4px 4px 2px rgba(0, 0, 0, .4); + box-shadow: 4px 4px 4px 2px rgba(0, 0, 0, .4); +} + +.shadow { + -webkit-box-shadow: 4px 4px 4px 2px rgba(0, 0, 0, .4); + box-shadow: 4px 4px 4px 2px rgba(0, 0, 0, .4); +} + +pre.header { + padding: 10px; + margin-bottom: 0; + background: rgb(250,251,252); /* Old browsers */ + background: -moz-linear-gradient(top, rgba(250,251,252,1) 0%, rgba(240,244,246,1) 50%, rgba(226,232,236,1) 51%, rgba(243,249,253,1) 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(250,251,252,1)), color-stop(50%,rgba(240,244,246,1)), color-stop(51%,rgba(226,232,236,1)), color-stop(100%,rgba(243,249,253,1))); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, rgba(250,251,252,1) 0%,rgba(240,244,246,1) 50%,rgba(226,232,236,1) 51%,rgba(243,249,253,1) 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, rgba(250,251,252,1) 0%,rgba(240,244,246,1) 50%,rgba(226,232,236,1) 51%,rgba(243,249,253,1) 100%); /* Opera 11.10+ */ + background: -ms-linear-gradient(top, rgba(250,251,252,1) 0%,rgba(240,244,246,1) 50%,rgba(226,232,236,1) 51%,rgba(243,249,253,1) 100%); /* IE10+ */ + background: linear-gradient(to bottom, rgba(250,251,252,1) 0%,rgba(240,244,246,1) 50%,rgba(226,232,236,1) 51%,rgba(243,249,253,1) 100%); /* W3C */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#fafbfc', endColorstr='#f3f9fd',GradientType=0 ); /* IE6-9 */ + +} + +/* +.info,.success,.warning,.error { + display: block; + border: 1px solid; + margin: 6px 0px; + padding: 8px 4px 8px 24px; + background-repeat: no-repeat; + background-position: 5px center; +} + +.info { + color: #00529B; + background-color: #BDE5F8; + background-image: url('information.png'); +} + +.success { + color: #4F8A10; + background-color: #DFF2BF; + background-image: url('success.png'); +} + +.warning { + color: #9F6000; + background-color: #FEEFB3; + background-image: url('warning.png'); +} + +.error { + color: #D8000C; + background-color: #FFBABA; + background-image: url('error.png'); +} +*/ +right { + position: relative; + right: -6px; + top: -4px; + float: right; +} +icon { + position: relative; + padding: 0; + margin: 0; + left:0; + padding-left:28px; + top: 15px; +} + + +icon:before { + left:0; + display: inline-block; + content: ""; + position: absolute; +} + +icon:after { + left:0; + display: inline-block; + content: ""; + position: absolute; +} + +/* SUCCESS +------------------------------------------------------------------------------------------------------------------------------- */ + +.success { + color:#7F913C; +} + +.success:before { + left:3px; + width:16px; + height:16px; + margin-top:-8px; + /* css3 */ + -webkit-border-radius:16px; + -moz-border-radius:16px; + border-radius:16px; + background:#7F913C; +} + +.success:after { + left:8px; + width:3px; + height:8px; + border-width:0 2px 2px 0; + border-style:solid; + border-color:#fff; + margin-top:-6px; + -webkit-transform:rotate(45deg); + -moz-transform:rotate(45deg); + -ms-transform:rotate(45deg); + -o-transform:rotate(45deg); + transform:rotate(45deg); +} + +/* Alternative style */ + +.success-alt { + color:#7F913C; +} + +.success-alt:before { + left:6px; + width:5px; + height:12px; + border-width:0 5px 5px 0; + border-style:solid; + border-color:#7F913C; + margin-top:-11px; + -webkit-transform:rotate(45deg); + -moz-transform:rotate(45deg); + -ms-transform:rotate(45deg); + -o-transform:rotate(45deg); + transform:rotate(45deg); +} + +/* COMMENT +------------------------------------------------------------------------------------------------------------------------------- */ + +.comment a:before { + width:16px; + height:10px; + margin-top:-8px; + /* css3 */ + -webkit-border-radius:2px; + -moz-border-radius:2px; + border-radius:2px; +} + +.comment a:after { + left:8px; + border:2px solid transparent; + border-top-color:#c55500; + border-left-color:#c55500; + margin-top:2px; + background:transparent; +} + +.comment a:hover:after, +.comment a:focus:after, +.comment a:active:after { + border-top-color:#730800; + border-left-color:#730800; +} + + +/* WARNING +------------------------------------------------------------------------------------------------------------------------------- */ + +.warning:before { + content:"!"; + z-index:2; + left:8px; + margin-top:-8px; + font-size:14px; + font-weight:bold; + color:#000; +} + +.warning:after { + z-index:1; + border-width:0 11px 18px; + border-style:solid; + border-color:#F8D201 transparent; + margin-top:-10px; + background:transparent; +} + +/* DENIED +------------------------------------------------------------------------------------------------------------------------------- */ + +.denied { + color:#C00000; +} + +.denied:before { + left:3px; + width:10px; + height:10px; + border:3px solid #C00000; + margin-top:-8px; + background:transparent; + /* css3 */ + -webkit-border-radius:16px; + -moz-border-radius:16px; + border-radius:16px; +} + +.denied:after { + left:6px; + width:11px; + height:3px; + margin-top:-2px; + background:#C00000; + /* css3 */ + -webkit-transform:rotate(-45deg); + -moz-transform:rotate(-45deg); + -ms-transform:rotate(-45deg); + -o-transform:rotate(-45deg); + transform:rotate(-45deg); +} + +#breadcrumbs-one{ + background: #eee; + border-width: 2px; + border-style: solid; + border-color: #f5f5f5 #e5e5e5 #ccc; + box-shadow: 0 0 2px rgba(0,0,0,.2); + overflow: hidden; +} + +#breadcrumbs-one li{ + float: left; +} + +#breadcrumbs-one a{ + padding: .3em 1em .3em 2em; + float: left; + text-decoration: none; + color: #444; + position: relative; + text-shadow: 0 1px 0 rgba(255,255,255,.5); + background-color: #ddd; + background-image: linear-gradient(to right, #f5f5f5, #ddd); + background-image: linear-gradient(right , rgb(221,221,221) 10%, rgb(245,245,245) 90%); + background-image: -o-linear-gradient(right , rgb(221,221,221) 10%, rgb(245,245,245) 90%); + background-image: -moz-linear-gradient(right , rgb(221,221,221) 10%, rgb(245,245,245) 90%); + background-image: -webkit-linear-gradient(right , rgb(221,221,221) 10%, rgb(245,245,245) 90%); + background-image: -ms-linear-gradient(right , rgb(221,221,221) 10%, rgb(245,245,245) 90%); + + background-image: -webkit-gradient( + linear, + right top, + left top, + color-stop(0.1, rgb(221,221,221)), + color-stop(0.9, rgb(245,245,245)) + ); +} + +#breadcrumbs-one li:first-child a{ + padding-left: 1em; + border-radius: 5px 0 0 5px; +} + +#breadcrumbs-one a.failure:hover, +#breadcrumbs-one a:hover{ + background: #fff; +} + +#breadcrumbs-one a.failure{ + background: #fff5f5; + background-image: linear-gradient(right , rgb(242,228,228) 10%, rgb(255,245,245) 90%); + background-image: -o-linear-gradient(right , rgb(242,228,228) 10%, rgb(255,245,245) 90%); + background-image: -moz-linear-gradient(right , rgb(242,228,228) 10%, rgb(255,245,245) 90%); + background-image: -webkit-linear-gradient(right , rgb(242,228,228) 10%, rgb(255,245,245) 90%); + background-image: -ms-linear-gradient(right , rgb(242,228,228) 10%, rgb(255,245,245) 90%); + + background-image: -webkit-gradient( + linear, + right top, + left top, + color-stop(0.1, rgb(242,228,228)), + color-stop(0.9, rgb(255,245,245)) + ); +} +#breadcrumbs-one a.failure::after{ + border-left-color: #f2e4e4; +} + +#breadcrumbs-one a::after, +#breadcrumbs-one a::before{ + content: ""; + position: absolute; + top: 50%; + margin-top: -1.5em; + border-top: 1.5em solid transparent; + border-bottom: 1.5em solid transparent; + border-left: 1em solid; + right: -1em; +} + +#breadcrumbs-one a::after{ + z-index: 2; + border-left-color: #ddd; +} + +#breadcrumbs-one a::before{ + border-left-color: #ccc; + right: -1.1em; + z-index: 1; +} + +#breadcrumbs-one a:hover::after{ + border-left-color: #fff; +} + +#breadcrumbs-one .current, +#breadcrumbs-one .current:hover{ + font-weight: bold; + background: none; +} + +#breadcrumbs-one .current::after, +#breadcrumbs-one .current::before{ + content: normal; +} + +.state { + background: #f00; + width: 8px; + height: 8px; + border-radius: 50%; + display: inline-block; + border: #000000 solid 3px; + position: relative; + z-index: 10; +} + +.state:after { + content: ""; + border-right: #000000 solid 4px;; + position: relative; + top: 8px; + left: 2px; + +} + +li > strong { + min-width: 80px; + display: inline-block; +} \ No newline at end of file diff --git a/htdocs/includes/restler/views/debug.php b/htdocs/includes/restler/views/debug.php new file mode 100644 index 00000000000..346c229c5d5 --- /dev/null +++ b/htdocs/includes/restler/views/debug.php @@ -0,0 +1,169 @@ +_exceptions; + if (count($source)) { + $source = end($source); + $traces = array(); + do { + $traces += $source->getTrace(); + } while ($source = $source->getPrevious()); + $traces += debug_backtrace(); + $call_trace + = parse_backtrace($traces, 0); + } else { + $call_trace + = parse_backtrace(debug_backtrace()); + } + +} +exceptions(); + +function parse_backtrace($raw, $skip = 1) +{ + $output = ""; + foreach ($raw as $entry) { + if ($skip-- > 0) { + continue; + } + //$output .= print_r($entry, true) . "\n"; + $output .= "\nFile: " . $entry['file'] . " (Line: " . $entry['line'] . ")\n"; + if (isset($entry['class'])) + $output .= $entry['class'] . "::"; + $output .= $entry['function'] + . "( " . json_encode($entry['args']) . " )\n"; + } + return $output; +} + + +//print_r(get_defined_vars()); +//print_r($response); +$icon; +if ($success && isset($api)) { + $arguments = implode(', ', $api->parameters); + $icon = ""; + $title = "{$api->className}::" + . "{$api->methodName}({$arguments})"; +} else { + if (isset($response['error']['message'])) { + $icon = ''; + $title = end(explode(':',$response['error']['message'])); + } else { + $icon = ''; + $title = 'No Matching Resource'; + } +} +function render($data, $shadow=true) +{ + $r = ''; + if (empty($data)) + return $r; + $r .= $shadow ? "
    \n": "
      \n"; + if (is_array($data)) { + // field name + foreach ($data as $key => $value) { + $r .= '
    • '; + $r .= is_numeric($key) + ? "[$key] " + : "$key: "; + $r .= ''; + if (is_array($value)) { + // recursive + $r .= render($value,false); + } else { + // value, with hyperlinked hyperlinks + if (is_bool($value)) { + $value = $value ? 'true' : 'false'; + } + $value = htmlentities($value, ENT_COMPAT, 'UTF-8'); + if (strpos($value, 'http://') === 0) { + $r .= '' . $value . ''; + } else { + $r .= $value; + } + } + $r .= "
    • \n"; + } + } elseif (is_bool($data)) { + $r .= '
    • ' . ($data ? 'true' : 'false') . '
    • '; + } else { + $r .= "
    • $data
    • "; + } + $r .= "
    \n"; + return $r; +} +$reqHeadersArr = array(); +$requestHeaders = $_SERVER['REQUEST_METHOD'] . ' ' . $_SERVER['REQUEST_URI'] . ' ' . $_SERVER['SERVER_PROTOCOL'] . PHP_EOL; +foreach ($reqHeadersArr as $key => $value) { + if ($key == 'Host') + continue; + $requestHeaders .= "$key: $value" . PHP_EOL; +} +// $requestHeaders = $this->encode(apache_request_headers(), FALSE, +// FALSE); +$responseHeaders = implode(PHP_EOL, headers_list()).PHP_EOL.'Status: HTTP/1.1 '; +$responseHeaders .= Util::$restler->responseCode.' '.\Luracast\Restler\RestException::$codes[Util::$restler->responseCode]; + +?> + + + + <?php echo $title?> + + + + + +
    +

    +
    +
    + +

    Response:

    +
    + +

    Additional Template Data:

    + +

    Restler v

    +
    + + \ No newline at end of file diff --git a/htdocs/install/check.php b/htdocs/install/check.php index 15ba736acd5..4149691302e 100644 --- a/htdocs/install/check.php +++ b/htdocs/install/check.php @@ -296,9 +296,9 @@ else $conf->db->user = $dolibarr_main_db_user; $conf->db->pass = $dolibarr_main_db_pass; $db=getDoliDBInstance($conf->db->type,$conf->db->host,$conf->db->user,$conf->db->pass,$conf->db->name,$conf->db->port); - if ($db->connected == 1 && $db->database_selected == 1) + if ($db->connected && $db->database_selected) { - $ok=1; + $ok=true; } } } diff --git a/htdocs/install/etape1.php b/htdocs/install/etape1.php index 32f85a0860d..5817af81e9b 100644 --- a/htdocs/install/etape1.php +++ b/htdocs/install/etape1.php @@ -637,7 +637,7 @@ if (! $error && $db->connected && $action == "set") $db=getDoliDBInstance($conf->db->type,$conf->db->host,$conf->db->user,$conf->db->pass,$conf->db->name,$conf->db->port); - if ($db->connected == 1) + if ($db->connected) { dolibarr_install_syslog("etape1: connexion to server by user ".$conf->db->user." is ok", LOG_DEBUG); print ""; @@ -648,7 +648,7 @@ if (! $error && $db->connected && $action == "set") print ""; // si acces serveur ok et acces base ok, tout est ok, on ne va pas plus loin, on a meme pas utilise le compte root. - if ($db->database_selected == 1) + if ($db->database_selected) { dolibarr_install_syslog("etape1: connexion to database : ".$conf->db->name.", by user : ".$conf->db->user." is ok", LOG_DEBUG); print ""; diff --git a/htdocs/install/etape2.php b/htdocs/install/etape2.php index 8c0e3f8ab5c..8de44812222 100644 --- a/htdocs/install/etape2.php +++ b/htdocs/install/etape2.php @@ -88,7 +88,7 @@ if ($action == "set") $db=getDoliDBInstance($conf->db->type,$conf->db->host,$conf->db->user,$conf->db->pass,$conf->db->name,$conf->db->port); - if ($db->connected == 1) + if ($db->connected) { print ""; print $langs->trans("ServerConnection")." : ".$conf->db->host.'Ok'; @@ -101,7 +101,7 @@ if ($action == "set") if ($ok) { - if($db->database_selected == 1) + if($db->database_selected) { dolibarr_install_syslog("etape2: Connexion successful to database : ".$conf->db->name); } @@ -139,7 +139,7 @@ if ($action == "set") // To say sql requests are escaped for mysql so we need to unescape them - $db->unescapeslashquot=1; + $db->unescapeslashquot=true; /************************************************************************************** diff --git a/htdocs/install/etape4.php b/htdocs/install/etape4.php index 54639505701..fc4f47f4611 100644 --- a/htdocs/install/etape4.php +++ b/htdocs/install/etape4.php @@ -71,7 +71,7 @@ print ''; $db=getDoliDBInstance($conf->db->type,$conf->db->host,$conf->db->user,$conf->db->pass,$conf->db->name,$conf->db->port); -if ($db->ok == 1) +if ($db->ok) { print ''; diff --git a/htdocs/install/etape5.php b/htdocs/install/etape5.php index d6a5d086b31..da871508176 100644 --- a/htdocs/install/etape5.php +++ b/htdocs/install/etape5.php @@ -159,7 +159,7 @@ if ($action == "set" || empty($action) || preg_match('/upgrade/i',$action)) $result=$objMod->init(); if (! $result) print 'ERROR in activating module file='.$file; - if ($db->connected == 1) + if ($db->connected) { $conf->setValues($db); @@ -255,7 +255,7 @@ if ($action == "set" || empty($action) || preg_match('/upgrade/i',$action)) // If upgrade elseif (empty($action) || preg_match('/upgrade/i',$action)) { - if ($db->connected == 1) + if ($db->connected) { $conf->setValues($db); diff --git a/htdocs/install/fileconf.php b/htdocs/install/fileconf.php index 1256c5b215a..97a52ff6c34 100644 --- a/htdocs/install/fileconf.php +++ b/htdocs/install/fileconf.php @@ -487,8 +487,8 @@ jQuery(document).ready(function() { function init_needroot() { - /*alert(jQuery("#db_create_database").attr("checked")); */ - if (jQuery("#db_create_database").attr("checked") || jQuery("#db_create_user").attr("checked")) + /*alert(jQuery("#db_create_database").prop("checked")); */ + if (jQuery("#db_create_database").is(":checked") || jQuery("#db_create_user").is(":checked")) { jQuery(".hideroot").show(); jQuery(".needroot").removeAttr('disabled'); @@ -496,7 +496,7 @@ jQuery(document).ready(function() { else { jQuery(".hideroot").hide(); - jQuery(".needroot").attr('disabled','disabled'); + jQuery(".needroot").prop('disabled', true); } } diff --git a/htdocs/install/mysql/migration/3.7.0-3.8.0.sql b/htdocs/install/mysql/migration/3.7.0-3.8.0.sql index 0da15c78e74..28f4ab69f71 100755 --- a/htdocs/install/mysql/migration/3.7.0-3.8.0.sql +++ b/htdocs/install/mysql/migration/3.7.0-3.8.0.sql @@ -85,7 +85,7 @@ ALTER TABLE llx_projet_task MODIFY COLUMN duration_effective real DEFAULT 0 NULL ALTER TABLE llx_projet_task MODIFY COLUMN planned_workload real DEFAULT 0 NULL; -ALTER TABLE llx_commande_fournisseur MODIFY COLUMN date_livraison datetime; +ALTER TABLE llx_commande_fournisseur MODIFY COLUMN date_livraison datetime; -- Add id commandefourndet in llx_commande_fournisseur_dispatch to correct /fourn/commande/dispatch.php display when several times same product in supplier order ALTER TABLE llx_commande_fournisseur_dispatch ADD COLUMN fk_commandefourndet INTEGER NOT NULL DEFAULT 0 AFTER fk_product; @@ -112,12 +112,12 @@ ALTER TABLE llx_product_price ADD COLUMN fk_price_expression integer DEFAULT NUL --create table for user conf of printing driver -CREATE TABLE llx_printing +CREATE TABLE llx_printing ( rowid integer AUTO_INCREMENT PRIMARY KEY, tms timestamp, datec datetime, - printer_name text NOT NULL, + printer_name text NOT NULL, printer_location text NOT NULL, printer_id varchar(255) NOT NULL, copy integer NOT NULL DEFAULT '1', @@ -199,7 +199,7 @@ CREATE TABLE llx_expensereport ( total_ht double(24,8) DEFAULT 0, total_tva double(24,8) DEFAULT 0, localtax1 double(24,8) DEFAULT 0, -- amount total localtax1 - localtax2 double(24,8) DEFAULT 0, -- amount total localtax2 + localtax2 double(24,8) DEFAULT 0, -- amount total localtax2 total_ttc double(24,8) DEFAULT 0, date_debut date NOT NULL, date_fin date NOT NULL, @@ -292,11 +292,11 @@ ALTER TABLE llx_commande_fournisseurdet ADD COLUMN special_code integer DEFAULT ALTER TABLE llx_commande_fournisseurdet ADD COLUMN rang integer DEFAULT 0; ALTER TABLE llx_commande_fournisseurdet ADD COLUMN fk_parent_line integer NULL after fk_commande; -ALTER TABLE llx_projet ADD COLUMN date_close datetime DEFAULT NULL; +ALTER TABLE llx_projet ADD COLUMN date_close datetime DEFAULT NULL; ALTER TABLE llx_projet ADD COLUMN fk_user_close integer DEFAULT NULL; - + -- Module AskPriceSupplier -- CREATE TABLE llx_askpricesupplier ( rowid integer NOT NULL AUTO_INCREMENT PRIMARY KEY, @@ -602,6 +602,12 @@ ALTER TABLE llx_user DROP INDEX idx_user_fk_societe; ALTER TABLE llx_user CHANGE COLUMN fk_societe fk_soc INTEGER; ALTER TABLE llx_user ADD INDEX idx_user_fk_societe (fk_soc); +-- API module +ALTER TABLE llx_user ADD api_key VARCHAR(128) DEFAULT NULL AFTER pass_temp; +ALTER TABLE llx_user ADD INDEX idx_user_api_key (api_key); + + + ALTER TABLE llx_actioncomm ADD COLUMN email_msgid varchar(256); ALTER TABLE llx_actioncomm ADD COLUMN email_from varchar(256); ALTER TABLE llx_actioncomm ADD COLUMN email_sender varchar(256); @@ -609,4 +615,6 @@ ALTER TABLE llx_actioncomm ADD COLUMN email_to varchar(256); ALTER TABLE llx_actioncomm ADD COLUMN errors_to varchar(256); ALTER TABLE llx_actioncomm ADD COLUMN recurid varchar(128); +ALTER TABLE llx_stcomm ADD COLUMN picto varchar(128); + diff --git a/htdocs/install/mysql/tables/llx_c_stcomm.sql b/htdocs/install/mysql/tables/llx_c_stcomm.sql index 66a56709f21..a9e9d47384c 100644 --- a/htdocs/install/mysql/tables/llx_c_stcomm.sql +++ b/htdocs/install/mysql/tables/llx_c_stcomm.sql @@ -1,6 +1,6 @@ -- ======================================================================== -- Copyright (C) 2001-2002,2004 Rodolphe Quiedeville --- Copyright (C) 2004 Laurent Destailleur +-- Copyright (C) 2004-2015 Laurent Destailleur -- -- 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 @@ -22,6 +22,7 @@ create table llx_c_stcomm id integer PRIMARY KEY, code varchar(12) NOT NULL, libelle varchar(30), + picto varchar(128), active tinyint default 1 NOT NULL )ENGINE=innodb; diff --git a/htdocs/install/mysql/tables/llx_user.key.sql b/htdocs/install/mysql/tables/llx_user.key.sql index 127d45046d6..49916b68cb8 100644 --- a/htdocs/install/mysql/tables/llx_user.key.sql +++ b/htdocs/install/mysql/tables/llx_user.key.sql @@ -25,3 +25,4 @@ ALTER TABLE llx_user ADD INDEX idx_user_fk_societe (fk_soc); ALTER TABLE llx_user ADD UNIQUE INDEX uk_user_fk_socpeople (fk_socpeople); ALTER TABLE llx_user ADD UNIQUE INDEX uk_user_fk_member (fk_member); +ALTER TABLE llx_user ADD UNIQUE INDEX uk_user_api_key (api_key); diff --git a/htdocs/install/mysql/tables/llx_user.sql b/htdocs/install/mysql/tables/llx_user.sql index a4c04f4e864..c673331687a 100644 --- a/htdocs/install/mysql/tables/llx_user.sql +++ b/htdocs/install/mysql/tables/llx_user.sql @@ -34,6 +34,7 @@ create table llx_user pass varchar(32), pass_crypted varchar(128), pass_temp varchar(32), -- temporary password when asked for forget password + api_key varchar(128), civility varchar(6), lastname varchar(50), firstname varchar(50), diff --git a/htdocs/install/repair.php b/htdocs/install/repair.php index a8bbfdd09af..786a8efb430 100644 --- a/htdocs/install/repair.php +++ b/htdocs/install/repair.php @@ -101,7 +101,7 @@ $conf->db->dolibarr_main_db_cryptkey = isset($dolibarr_main_db_cryptkey)?$doliba $db=getDoliDBInstance($conf->db->type,$conf->db->host,$conf->db->user,$conf->db->pass,$conf->db->name,$conf->db->port); -if ($db->connected == 1) +if ($db->connected) { print '"; @@ -117,7 +117,7 @@ else if ($ok) { - if($db->database_selected == 1) + if($db->database_selected) { print '"; diff --git a/htdocs/install/upgrade.php b/htdocs/install/upgrade.php index c001a771f06..9c2832f3623 100644 --- a/htdocs/install/upgrade.php +++ b/htdocs/install/upgrade.php @@ -141,7 +141,7 @@ if (! GETPOST("action") || preg_match('/upgrade/i',GETPOST('action'))) include_once DOL_DOCUMENT_ROOT.'/core/class/hookmanager.class.php'; $hookmanager=new HookManager($db); - if ($db->connected == 1) + if ($db->connected) { print '\n"; @@ -157,7 +157,7 @@ if (! GETPOST("action") || preg_match('/upgrade/i',GETPOST('action'))) if ($ok) { - if($db->database_selected == 1) + if($db->database_selected) { print '\n"; diff --git a/htdocs/install/upgrade2.php b/htdocs/install/upgrade2.php index 8d63f02ddc8..091218513af 100644 --- a/htdocs/install/upgrade2.php +++ b/htdocs/install/upgrade2.php @@ -134,7 +134,7 @@ if (! GETPOST("action") || preg_match('/upgrade/i',GETPOST('action'))) include_once DOL_DOCUMENT_ROOT.'/core/class/hookmanager.class.php'; $hookmanager=new HookManager($db); - if ($db->connected != 1) + if (!$db->connected) { print ''; dolibarr_install_syslog('upgrade2: Failed to connect to database : '.$conf->db->name.' on '.$conf->db->host.' for user '.$conf->db->user, LOG_ERR); @@ -143,7 +143,7 @@ if (! GETPOST("action") || preg_match('/upgrade/i',GETPOST('action'))) if (! $error) { - if($db->database_selected == 1) + if($db->database_selected) { dolibarr_install_syslog('upgrade2: Database connection successfull : '.$dolibarr_main_db_name); } diff --git a/htdocs/langs/en_US/admin.lang b/htdocs/langs/en_US/admin.lang index 6e0e1d602b7..e5604ffdb20 100755 --- a/htdocs/langs/en_US/admin.lang +++ b/htdocs/langs/en_US/admin.lang @@ -526,6 +526,8 @@ Module2500Name=Electronic Content Management Module2500Desc=Save and share documents Module2600Name=WebServices Module2600Desc=Enable the Dolibarr web services server +Module2610Name=Api +Module2610Desc=Enable the Dolibarr REST API Module2650Name=WebServices (client) Module2650Desc=Enable the Dolibarr web services client (Can be used to push data/requests to external servers. Supplier orders supported only for the moment) Module2700Name=Gravatar @@ -1558,6 +1560,13 @@ WebServicesSetup=Webservices module setup WebServicesDesc=By enabling this module, Dolibarr become a web service server to provide miscellaneous web services. WSDLCanBeDownloadedHere=WSDL descriptor files of provided services can be download here EndPointIs=SOAP clients must send their requests to the Dolibarr endpoint available at Url +##### API #### +ApiSetup=API module setup +ApiDesc=By enabling this module, Dolibarr become a REST server to provide miscellaneous web services. +KeyForApiAccess=Key to use API (parameter "api_key") +ApiEndPointIs=You can access to the API at url +ApiExporerIs=You can explore the API at url +OnlyActiveElementsAreExposed=Only elements from enabled modules are exposed ##### Bank ##### BankSetupModule=Bank module setup FreeLegalTextOnChequeReceipts=Free text on cheque receipts diff --git a/htdocs/product/class/api_product.class.php b/htdocs/product/class/api_product.class.php new file mode 100644 index 00000000000..1ce63411cd3 --- /dev/null +++ b/htdocs/product/class/api_product.class.php @@ -0,0 +1,273 @@ + + * + * 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 . + */ + + use Luracast\Restler\RestException; + + require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php'; + +/** + * API class for product object + * + * @smart-auto-routing false + * @access protected + * @class DolibarrApiAccess {@requires user,external} + * + */ +class ProductApi extends DolibarrApi +{ + /** + * @var array $FIELDS Mandatory fields, checked when create and update object + */ + static $FIELDS = array( + 'ref', + 'label' + ); + + /** + * @var Product $product {@type Product} + */ + public $product; + + /** + * Constructor + * + * @url product/ + * + */ + function __construct() + { + global $db, $conf; + $this->db = $db; + $this->product = new Product($this->db); + } + + /** + * Get properties of a product object + * + * Return an array with product informations + * + * @param int $id ID of product + * @param string $ref Product ref + * @param string $ref_ext Product ref ext + * @return array|mixed data without useless information + * + * @url GET product/{id} + * @throws RestException + */ + function get($id='', $ref='', $ref_ext='') + { + if(! DolibarrApiAccess::$user->rights->produit->lire) { + throw new RestException(401); + } + + $result = $this->product->fetch($id,$ref,$ref_ext); + if( ! $result ) { + throw new RestException(404, 'Product not found'); + } + + if( ! DolibarrApi::_checkAccessToResource('product',$this->product->id)) { + throw new RestException(401, 'Access not allowed for login '.DolibarrApiAccess::$user->login); + } + + $this->product->load_stock(); + + return $this->_cleanObjectDatas($this->product); + } + + /** + * List products + * + * Get a list of products + * + * @param int $mode Use this param to filter list (0 for all, 1 for only product, 2 for only service) + * @param mixed $to_sell Filter products to sell (1) or not to sell (0) + * @param mixed $to_buy Filter products to nuy (1) or not to buy (0) + * @param string $sortfield Sort field + * @param string $sortorder Sort order + * @param int $limit Limit for list + * @param int $page Page number + * + * @return array Array of product objects + * + * @url GET /product/list + */ + function getList($mode=0, $to_sell='', $to_buy='', $sortfield = "p.ref", $sortorder = 'ASC', $limit = 0, $page = 0) { + global $db, $conf; + + $obj_ret = array(); + + $socid = DolibarrApiAccess::$user->societe_id ? DolibarrApiAccess::$user->societe_id : ''; + + $sql ="SELECT rowid, ref, ref_ext"; + $sql.= " FROM ".MAIN_DB_PREFIX."product as p"; + $sql.= ' WHERE p.entity IN ('.getEntity('product', 1).')'; + + // Show products + if ($mode == 1) $sql.= " AND p.fk_product_type = 0"; + // Show services + if ($mode == 2) $sql.= " AND p.fk_product_type = 1"; + // Show product on sell + if ($to_sell) $sql.= " AND p.to_sell = ".$db->escape($to_sell); + // Show product on buy + if ($to_buy) $sql.= " AND p.to_nuy = ".$db->escape($to_nuy); + + $nbtotalofrecords = 0; + if (empty($conf->global->MAIN_DISABLE_FULL_SCANLIST)) + { + $result = $db->query($sql); + $nbtotalofrecords = $db->num_rows($result); + } + + $sql.= $db->order($sortfield, $sortorder); + if ($limit) { + if ($page < 0) + { + $page = 0; + } + $offset = $limit * $page; + + $sql.= $db->plimit($limit + 1, $offset); + } + + $result = $db->query($sql); + if ($result) + { + $num = $db->num_rows($result); + while ($i < $num) + { + $obj = $db->fetch_object($result); + $product_static = new Product($db); + if($product_static->fetch($obj->rowid)) { + $obj_ret[] = parent::_cleanObjectDatas($product_static); + } + $i++; + } + } + else { + throw new RestException(503, 'Error when retrieve product list'); + } + if( ! count($obj_ret)) { + throw new RestException(404, 'No product found'); + } + return $obj_ret; + } + + /** + * Create product object + * + * @param array $request_data Request data + * @return int ID of product + * + * @url POST product/ + */ + function post($request_data = NULL) + { + if(! DolibarrApiAccess::$user->rights->produit->creer) { + throw new RestException(401); + } + // Check mandatory fields + $result = $this->_validate($request_data); + + foreach($request_data as $field => $value) { + $this->product->$field = $value; + } + $result = $this->product->create(DolibarrApiAccess::$user); + if($result < 0) { + throw new RestException(503,'Error when creating product : '.$this->product->error); + } + + return $this->product->id; + + } + + /** + * Update product + * + * @param int $id Id of product to update + * @param array $request_data Datas + * @return int + * + * @url PUT product/{id} + */ + function put($id, $request_data = NULL) + { + if(! DolibarrApiAccess::$user->rights->produit->creer) { + throw new RestException(401); + } + + $result = $this->product->fetch($id); + if( ! $result ) { + throw new RestException(404, 'Product not found'); + } + + if( ! DolibarrApi::_checkAccessToResource('product',$this->product->id)) { + throw new RestException(401, 'Access not allowed for login '.DolibarrApiAccess::$user->login); + } + + foreach($request_data as $field => $value) { + $this->product->$field = $value; + } + + if($this->product->update($id, DolibarrApiAccess::$user,1,'','','update')) + return $this->get ($id); + + return false; + } + + /** + * Delete product + * + * @param int $id Product ID + * @return array + * + * @url DELETE product/{id} + */ + function delete($id) + { + if(! DolibarrApiAccess::$user->rights->product->supprimer) { + throw new RestException(401); + } + $result = $this->product->fetch($id); + if( ! $result ) { + throw new RestException(404, 'Product not found'); + } + + if( ! DolibarrApi::_checkAccessToResource('product',$this->product->id)) { + throw new RestException(401, 'Access not allowed for login '.DolibarrApiAccess::$user->login); + } + + return $this->product->delete($id); + } + + /** + * Validate fields before create or update object + * + * @param array $data Datas to validate + * @return array + * @throws RestException + */ + function _validate($data) + { + $product = array(); + foreach (ProductApi::$FIELDS as $field) { + if (!isset($data[$field])) + throw new RestException(400, "$field field missing"); + $product[$field] = $data[$field]; + } + return $product; + } +} diff --git a/htdocs/product/stock/tpl/stockcorrection.tpl.php b/htdocs/product/stock/tpl/stockcorrection.tpl.php index 8f656045ee3..501272983cd 100644 --- a/htdocs/product/stock/tpl/stockcorrection.tpl.php +++ b/htdocs/product/stock/tpl/stockcorrection.tpl.php @@ -29,8 +29,8 @@ $langs->load("productbatch"); jQuery(document).ready(function() { function init_price() { - if (jQuery("#mouvement").val() == \'0\') jQuery("#unitprice").removeAttr(\'disabled\'); - else jQuery("#unitprice").attr(\'disabled\',\'disabled\'); + if (jQuery("#mouvement").val() == \'0\') jQuery("#unitprice").removeAttr("disabled"); + else jQuery("#unitprice").prop("disabled", true); } init_price(); jQuery("#mouvement").change(function() { @@ -116,4 +116,4 @@ $langs->load("productbatch"); print ''; ?> - \ No newline at end of file + diff --git a/htdocs/projet/tasks/contact.php b/htdocs/projet/tasks/contact.php index 0dbcc007db1..e6dd2792118 100644 --- a/htdocs/projet/tasks/contact.php +++ b/htdocs/projet/tasks/contact.php @@ -464,7 +464,7 @@ if ($id > 0 || ! empty($ref)) print ''; // Icon update et delete - print '
    '.$langs->trans("DolibarrAdminLogin").' :'; print '
    '; print $langs->trans("ServerConnection")." : $dolibarr_main_db_host".$langs->trans("OK")."
    '; print $langs->trans("DatabaseConnection")." : ".$dolibarr_main_db_name."".$langs->trans("OK")."
    '; print $langs->trans("ServerConnection")." : $dolibarr_main_db_host".$langs->trans("OK")."
    '; print $langs->trans("DatabaseConnection")." : ".$dolibarr_main_db_name."".$langs->trans("OK")."
    '.$langs->trans("ErrorFailedToConnectToDatabase",$conf->db->name).''.$langs->trans('Error').'
    '; + print ''; if ($user->rights->projet->creer) { print ' '; diff --git a/htdocs/public/api/.htaccess b/htdocs/public/api/.htaccess new file mode 100644 index 00000000000..a8106809018 --- /dev/null +++ b/htdocs/public/api/.htaccess @@ -0,0 +1,15 @@ +# +# Apache configuration file to use API +# + +DirectoryIndex index.php + + RewriteEngine On + RewriteRule ^$ index.php [QSA,L] + RewriteCond %{REQUEST_FILENAME} !-f + RewriteCond %{REQUEST_FILENAME} !-d + RewriteRule ^(.*)$ index.php [QSA,L] + + + php_flag display_errors On + \ No newline at end of file diff --git a/htdocs/public/api/explorer/css/screen.css b/htdocs/public/api/explorer/css/screen.css new file mode 100644 index 00000000000..e16ad7b6a73 --- /dev/null +++ b/htdocs/public/api/explorer/css/screen.css @@ -0,0 +1,1538 @@ +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +menu, nav, output, ruby, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} + +body { + line-height: 1; +} + +ol, ul { + list-style: none; +} + +table { + border-collapse: collapse; + border-spacing: 0; +} + +caption, th, td { + text-align: left; + font-weight: normal; + vertical-align: middle; +} + +q, blockquote { + quotes: none; +} + +q:before, q:after, blockquote:before, blockquote:after { + content: ""; + content: none; +} + +a img { + border: none; +} + +article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section, summary { + display: block; +} + +h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { + text-decoration: none; +} + +h1 a:hover, h2 a:hover, h3 a:hover, h4 a:hover, h5 a:hover, h6 a:hover { + text-decoration: underline; +} + +h1 span.divider, h2 span.divider, h3 span.divider, h4 span.divider, h5 span.divider, h6 span.divider { + color: #aaaaaa; +} + +h1 { + color: #547f00; + color: black; + font-size: 1.5em; + line-height: 1.3em; + padding: 10px 0 10px 0; + font-family: "Droid Sans", sans-serif; + font-weight: bold; +} + +h2 { + color: #89bf04; + color: black; + font-size: 1.3em; + padding: 10px 0 10px 0; +} + +h2 a { + color: black; +} + +h2 span.sub { + font-size: 0.7em; + color: #999999; + font-style: italic; +} + +h2 span.sub a { + color: #777777; +} + +h3 { + color: black; + font-size: 1.1em; + padding: 10px 0 10px 0; +} + +div.heading_with_menu { + float: none; + clear: both; + overflow: hidden; + display: block; +} + +div.heading_with_menu h1, div.heading_with_menu h2, div.heading_with_menu h3, div.heading_with_menu h4, div.heading_with_menu h5, div.heading_with_menu h6 { + display: block; + clear: none; + float: left; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + -ms-box-sizing: border-box; + box-sizing: border-box; + width: 60%; +} + +div.heading_with_menu ul { + display: block; + clear: none; + float: right; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + -ms-box-sizing: border-box; + box-sizing: border-box; + margin-top: 10px; +} + +.body-textarea { + width: 300px; + height: 100px; +} + +p { + line-height: 1.4em; + padding: 0 0 10px 0; + color: #333333; +} + +ol { + margin: 0px 0 10px 0; + padding: 0 0 0 18px; + list-style-type: decimal; +} + +ol li { + padding: 5px 0px; + font-size: 0.9em; + color: #333333; +} + +mark { + padding: 2px; + color: #7e7b6d; + font-weight: bold; + background: rgba(255, 255, 255, 0.8); +} + +tag { + font-family: Verdana,Arial,Helvetica,sans-serif; + background-color: #999999; + padding: 1px 3px 2px; + font-size: xx-small; + font-weight: bold; + color: #ffffff; + letter-spacing: 1px; + white-space: nowrap; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +.markdown h3 { + color: #547f00; +} + +.markdown h4 { + color: #666666; +} + +.markdown pre { + font-family: "Anonymous Pro", "Menlo", "Consolas", "Bitstream Vera Sans Mono", "Courier New", monospace; + background-color: #fcf6db; + border: 1px solid black; + border-color: #e5e0c6; + padding: 10px; + margin: 0 0 10px 0; +} + +.markdown pre code { + line-height: 1.6em; +} + +.markdown p code, .markdown li code { + font-family: "Anonymous Pro", "Menlo", "Consolas", "Bitstream Vera Sans Mono", "Courier New", monospace; + background-color: #f0f0f0; + color: black; + padding: 1px 3px; +} + +.markdown ol, .markdown ul { + font-family: "Droid Sans", sans-serif; + margin: 5px 0 10px 0; + padding: 0 0 0 18px; + list-style-type: disc; +} + +.markdown ol li, .markdown ul li { + padding: 3px 0px; + line-height: 1.4em; + color: #333333; +} + +div.gist { + margin: 20px 0 25px 0 !important; +} + +p.big, div.big p { + font-size: 1em; + margin-bottom: 10px; +} + +span.weak { + color: #666666; +} + +span.blank, span.empty { + color: #888888; + font-style: italic; +} + +a { + color: #547f00; +} + +strong { + font-family: "Droid Sans", sans-serif; + font-weight: bold; + font-weight: bold; +} + +.code { + font-family: "Anonymous Pro", "Menlo", "Consolas", "Bitstream Vera Sans Mono", "Courier New", monospace; +} + +pre { + font-family: "Anonymous Pro", "Menlo", "Consolas", "Bitstream Vera Sans Mono", "Courier New", monospace; + background-color: #fcf6db; + border: 1px solid black; + border-color: #e5e0c6; + padding: 10px; + /* white-space: pre-line */ +} + +pre code { + line-height: 1.6em; +} + +.required { + font-weight: bold; +} + +table.fullwidth { + width: 100%; +} + +table thead tr th { + padding: 5px; + font-size: 0.9em; + color: #666666; + border-bottom: 1px solid #999999; +} + +table tbody tr.offset { + background-color: #f5f5f5; +} + +table tbody tr td { + padding: 6px; + font-size: 0.9em; + border-bottom: 1px solid #cccccc; + vertical-align: top; + line-height: 1.3em; +} + +table tbody tr:last-child td { + border-bottom: none; +} + +table tbody tr.offset { + background-color: #f0f0f0; +} + +form.form_box { + background-color: #ebf3f9; + border: 1px solid black; + border-color: #c3d9ec; + padding: 10px; +} + +form.form_box label { + color: #0f6ab4 !important; +} + +form.form_box input[type=submit] { + display: block; + padding: 10px; +} + +form.form_box p { + font-size: 0.9em; + padding: 0 0 15px 0; + color: #7e7b6d; +} + +form.form_box p a { + color: #646257; +} + +form.form_box p strong { + color: black; +} + +form.form_box p.weak { + font-size: 0.8em; +} + +form.formtastic fieldset.inputs ol li p.inline-hints { + margin-left: 0; + font-style: italic; + font-size: 0.9em; + margin: 0; +} + +form.formtastic fieldset.inputs ol li label { + display: block; + clear: both; + width: auto; + padding: 0 0 3px 0; + color: #666666; +} + +form.formtastic fieldset.inputs ol li label abbr { + padding-left: 3px; + color: #888888; +} + +form.formtastic fieldset.inputs ol li.required label { + color: black; +} + +form.formtastic fieldset.inputs ol li.string input, form.formtastic fieldset.inputs ol li.url input, form.formtastic fieldset.inputs ol li.numeric input { + display: block; + padding: 4px; + width: auto; + clear: both; +} + +form.formtastic fieldset.inputs ol li.string input.title, form.formtastic fieldset.inputs ol li.url input.title, form.formtastic fieldset.inputs ol li.numeric input.title { + font-size: 1.3em; +} + +form.formtastic fieldset.inputs ol li.text textarea { + font-family: "Droid Sans", sans-serif; + height: 250px; + padding: 4px; + display: block; + clear: both; +} + +form.formtastic fieldset.inputs ol li.select select { + display: block; + clear: both; +} + +form.formtastic fieldset.inputs ol li.boolean { + float: none; + clear: both; + overflow: hidden; + display: block; +} + +form.formtastic fieldset.inputs ol li.boolean input { + display: block; + float: left; + clear: none; + margin: 0 5px 0 0; +} + +form.formtastic fieldset.inputs ol li.boolean label { + display: block; + float: left; + clear: none; + margin: 0; + padding: 0; +} + +form.formtastic fieldset.buttons { + margin: 0; + padding: 0; +} + +form.fullwidth ol li.string input, form.fullwidth ol li.url input, form.fullwidth ol li.text textarea, form.fullwidth ol li.numeric input { + width: 500px !important; +} + +body { + font-family: "Droid Sans", sans-serif; +} + +body #content_message { + margin: 10px 15px; + font-style: italic; + color: #999999; +} + +body #header { + background-color: #646257; + padding: 14px; +} + +body #header a#logo { + font-size: 1.5em; + font-weight: bold; + text-decoration: none; + background: transparent url(http://luracast.com/images/restler3-icon.png) no-repeat left center; + padding: 20px 0 20px 40px; + color: white; +} + +body #header form#api_selector { + display: block; + clear: none; + float: right; +} + +body #header form#api_selector .input { + display: block; + clear: none; + float: left; + margin: 0 10px 0 0; +} + +body #header form#api_selector .input input { + font-size: 0.9em; + padding: 3px; + margin: 0; +} + +body #header form#api_selector .input input#input_baseUrl { + width: 400px; +} + +body #header form#api_selector .input input#input_apiKey { + width: 200px; +} + +body #header form#api_selector .input a#explore { + display: block; + text-decoration: none; + font-weight: bold; + padding: 6px 8px; + font-size: 0.9em; + color: white; + background-color: #000000; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + -o-border-radius: 4px; + -ms-border-radius: 4px; + -khtml-border-radius: 4px; + border-radius: 4px; +} + +body #header form#api_selector .input a#explore:hover { + background-color: #a41e22; +} + +body p#colophon { + margin: 0 15px 40px 15px; + padding: 10px 0; + font-size: 0.8em; + border-top: 1px solid #dddddd; + font-family: "Droid Sans", sans-serif; + color: #999999; + font-style: italic; +} + +body p#colophon a { + text-decoration: none; + color: #547f00; +} + +body ul#resources { + font-family: "Droid Sans", sans-serif; + font-size: 0.9em; +} + +body ul#resources li.resource { + border-bottom: 1px solid #dddddd; +} + +body ul#resources li.resource:last-child { + border-bottom: none; +} + +body ul#resources li.resource div.heading { + border: 1px solid transparent; + float: none; + clear: both; + overflow: hidden; + display: block; +} + +body ul#resources li.resource div.heading h2 { + color: #999999; + padding-left: 0px; + display: block; + clear: none; + float: left; + font-family: "Droid Sans", sans-serif; + font-weight: bold; +} + +body ul#resources li.resource div.heading h2 a { + color: #999999; +} + +body ul#resources li.resource div.heading h2 a:hover { + color: black; +} + +body ul#resources li.resource div.heading ul.options { + float: none; + clear: both; + overflow: hidden; + margin: 0; + padding: 0; + display: block; + clear: none; + float: right; + margin: 14px 10px 0 0; +} + +body ul#resources li.resource div.heading ul.options li { + float: left; + clear: none; + margin: 0; + padding: 2px 10px; + border-right: 1px solid #dddddd; +} + +body ul#resources li.resource div.heading ul.options li:first-child, body ul#resources li.resource div.heading ul.options li.first { + padding-left: 0; +} + +body ul#resources li.resource div.heading ul.options li:last-child, body ul#resources li.resource div.heading ul.options li.last { + padding-right: 0; + border-right: none; +} + +body ul#resources li.resource div.heading ul.options li { + color: #666666; + font-size: 0.9em; +} + +body ul#resources li.resource div.heading ul.options li a { + color: #aaaaaa; + text-decoration: none; +} + +body ul#resources li.resource div.heading ul.options li a:hover { + text-decoration: underline; + color: black; +} + +body ul#resources li.resource:hover div.heading h2 a, body ul#resources li.resource.active div.heading h2 a { + color: black; +} + +body ul#resources li.resource:hover div.heading ul.options li a, body ul#resources li.resource.active div.heading ul.options li a { + color: #555555; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get { + float: none; + clear: both; + overflow: hidden; + display: block; + margin: 0 0 10px 0; + padding: 0 0 0 0px; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading { + float: none; + clear: both; + overflow: hidden; + display: block; + margin: 0 0 0 0; + padding: 0; + background-color: #e7f0f7; + border: 1px solid black; + border-color: #c3d9ec; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading h3 { + display: block; + clear: none; + float: left; + width: auto; + margin: 0; + padding: 0; + line-height: 1.1em; + color: black; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading h3 span { + margin: 0; + padding: 0; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading h3 span.http_method a { + text-transform: uppercase; + background-color: #0f6ab4; + text-decoration: none; + color: white; + display: inline-block; + width: 50px; + font-size: 0.7em; + text-align: center; + padding: 7px 0 4px 0; + -moz-border-radius: 2px; + -webkit-border-radius: 2px; + -o-border-radius: 2px; + -ms-border-radius: 2px; + -khtml-border-radius: 2px; + border-radius: 2px; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading h3 span.path { + padding-left: 10px; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading h3 span.path a { + color: black; + text-decoration: none; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading h3 span.path a:hover { + text-decoration: underline; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options { + float: none; + clear: both; + overflow: hidden; + margin: 0; + padding: 0; + display: block; + clear: none; + float: right; + margin: 6px 10px 0 0; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options li { + float: left; + clear: none; + margin: 0; + padding: 2px 10px; + border-right: 1px solid #dddddd; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options li:first-child, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options li.first { + padding-left: 0; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options li:last-child, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options li.last { + padding-right: 0; + border-right: none; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options li { + border-right-color: #c3d9ec; + color: #0f6ab4; + font-size: 0.9em; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options li a { + color: #0f6ab4; + text-decoration: none; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options li a:hover, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options li a:active, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options li a.active { + text-decoration: underline; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.content { + background-color: #ebf3f9; + border: 1px solid black; + border-color: #c3d9ec; + border-top: none; + padding: 10px; + -moz-border-radius-bottomleft: 6px; + -webkit-border-bottom-left-radius: 6px; + -o-border-bottom-left-radius: 6px; + -ms-border-bottom-left-radius: 6px; + -khtml-border-bottom-left-radius: 6px; + border-bottom-left-radius: 6px; + -moz-border-radius-bottomright: 6px; + -webkit-border-bottom-right-radius: 6px; + -o-border-bottom-right-radius: 6px; + -ms-border-bottom-right-radius: 6px; + -khtml-border-bottom-right-radius: 6px; + border-bottom-right-radius: 6px; + margin: 0 0 20px 0; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.content h4 { + color: #0f6ab4; + font-size: 1.1em; + margin: 0; + padding: 15px 0 5px 0px; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.content form input[type='text'].error { + outline: 2px solid black; + outline-color: #cc0000; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.content div.sandbox_header { + float: none; + clear: both; + overflow: hidden; + display: block; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.content div.sandbox_header input.submit { + display: block; + clear: none; + float: left; + padding: 6px 8px; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.content div.sandbox_header img { + display: block; + display: block; + clear: none; + float: right; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.content div.sandbox_header a { + padding: 4px 0 0 10px; + color: #6fa5d2; + display: inline-block; + font-size: 0.9em; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.content div.response div.block { + background-color: #fcf6db; + border: 1px solid black; + border-color: #e5e0c6; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.content div.response div.block pre { + font-family: "Anonymous Pro", "Menlo", "Consolas", "Bitstream Vera Sans Mono", "Courier New", monospace; + padding: 10px; + font-size: 0.9em; + max-height: 400px; + overflow-y: auto; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post { + float: none; + clear: both; + overflow: hidden; + display: block; + margin: 0 0 10px 0; + padding: 0 0 0 0px; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading { + float: none; + clear: both; + overflow: hidden; + display: block; + margin: 0 0 0 0; + padding: 0; + background-color: #e7f6ec; + border: 1px solid black; + border-color: #c3e8d1; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading h3 { + display: block; + clear: none; + float: left; + width: auto; + margin: 0; + padding: 0; + line-height: 1.1em; + color: black; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading h3 span { + margin: 0; + padding: 0; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading h3 span.http_method a { + text-transform: uppercase; + background-color: #10a54a; + text-decoration: none; + color: white; + display: inline-block; + width: 50px; + font-size: 0.7em; + text-align: center; + padding: 7px 0 4px 0; + -moz-border-radius: 2px; + -webkit-border-radius: 2px; + -o-border-radius: 2px; + -ms-border-radius: 2px; + -khtml-border-radius: 2px; + border-radius: 2px; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading h3 span.path { + padding-left: 10px; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading h3 span.path a { + color: black; + text-decoration: none; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading h3 span.path a:hover { + text-decoration: underline; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options { + float: none; + clear: both; + overflow: hidden; + margin: 0; + padding: 0; + display: block; + clear: none; + float: right; + margin: 6px 10px 0 0; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li { + float: left; + clear: none; + margin: 0; + padding: 2px 10px; + border-right: 1px solid #dddddd; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li:first-child, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li.first { + padding-left: 0; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li:last-child, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li.last { + padding-right: 0; + border-right: none; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li { + border-right-color: #c3e8d1; + color: #10a54a; + font-size: 0.9em; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li a { + color: #10a54a; + text-decoration: none; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li a:hover, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li a:active, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li a.active { + text-decoration: underline; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.content { + background-color: #ebf7f0; + border: 1px solid black; + border-color: #c3e8d1; + border-top: none; + padding: 10px; + -moz-border-radius-bottomleft: 6px; + -webkit-border-bottom-left-radius: 6px; + -o-border-bottom-left-radius: 6px; + -ms-border-bottom-left-radius: 6px; + -khtml-border-bottom-left-radius: 6px; + border-bottom-left-radius: 6px; + -moz-border-radius-bottomright: 6px; + -webkit-border-bottom-right-radius: 6px; + -o-border-bottom-right-radius: 6px; + -ms-border-bottom-right-radius: 6px; + -khtml-border-bottom-right-radius: 6px; + border-bottom-right-radius: 6px; + margin: 0 0 20px 0; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.content h4 { + color: #10a54a; + font-size: 1.1em; + margin: 0; + padding: 15px 0 5px 0px; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.content form input[type='text'].error { + outline: 2px solid black; + outline-color: #cc0000; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.content div.sandbox_header { + float: none; + clear: both; + overflow: hidden; + display: block; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.content div.sandbox_header input.submit { + display: block; + clear: none; + float: left; + padding: 6px 8px; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.content div.sandbox_header img { + display: block; + display: block; + clear: none; + float: right; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.content div.sandbox_header a { + padding: 4px 0 0 10px; + color: #6fc992; + display: inline-block; + font-size: 0.9em; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.content div.response div.block { + background-color: #fcf6db; + border: 1px solid black; + border-color: #e5e0c6; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.content div.response div.block pre { + font-family: "Anonymous Pro", "Menlo", "Consolas", "Bitstream Vera Sans Mono", "Courier New", monospace; + padding: 10px; + font-size: 0.9em; + max-height: 400px; + overflow-y: auto; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put { + float: none; + clear: both; + overflow: hidden; + display: block; + margin: 0 0 10px 0; + padding: 0 0 0 0px; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading { + float: none; + clear: both; + overflow: hidden; + display: block; + margin: 0 0 0 0; + padding: 0; + background-color: #f9f2e9; + border: 1px solid black; + border-color: #f0e0ca; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading h3 { + display: block; + clear: none; + float: left; + width: auto; + margin: 0; + padding: 0; + line-height: 1.1em; + color: black; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading h3 span { + margin: 0; + padding: 0; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading h3 span.http_method a { + text-transform: uppercase; + background-color: #c5862b; + text-decoration: none; + color: white; + display: inline-block; + width: 50px; + font-size: 0.7em; + text-align: center; + padding: 7px 0 4px 0; + -moz-border-radius: 2px; + -webkit-border-radius: 2px; + -o-border-radius: 2px; + -ms-border-radius: 2px; + -khtml-border-radius: 2px; + border-radius: 2px; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading h3 span.path { + padding-left: 10px; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading h3 span.path a { + color: black; + text-decoration: none; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading h3 span.path a:hover { + text-decoration: underline; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options { + float: none; + clear: both; + overflow: hidden; + margin: 0; + padding: 0; + display: block; + clear: none; + float: right; + margin: 6px 10px 0 0; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li { + float: left; + clear: none; + margin: 0; + padding: 2px 10px; + border-right: 1px solid #dddddd; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li:first-child, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li.first { + padding-left: 0; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li:last-child, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li.last { + padding-right: 0; + border-right: none; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li { + border-right-color: #f0e0ca; + color: #c5862b; + font-size: 0.9em; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li a { + color: #c5862b; + text-decoration: none; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li a:hover, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li a:active, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li a.active { + text-decoration: underline; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.content { + background-color: #faf5ee; + border: 1px solid black; + border-color: #f0e0ca; + border-top: none; + padding: 10px; + -moz-border-radius-bottomleft: 6px; + -webkit-border-bottom-left-radius: 6px; + -o-border-bottom-left-radius: 6px; + -ms-border-bottom-left-radius: 6px; + -khtml-border-bottom-left-radius: 6px; + border-bottom-left-radius: 6px; + -moz-border-radius-bottomright: 6px; + -webkit-border-bottom-right-radius: 6px; + -o-border-bottom-right-radius: 6px; + -ms-border-bottom-right-radius: 6px; + -khtml-border-bottom-right-radius: 6px; + border-bottom-right-radius: 6px; + margin: 0 0 20px 0; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.content h4 { + color: #c5862b; + font-size: 1.1em; + margin: 0; + padding: 15px 0 5px 0px; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.content form input[type='text'].error { + outline: 2px solid black; + outline-color: #cc0000; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.content div.sandbox_header { + float: none; + clear: both; + overflow: hidden; + display: block; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.content div.sandbox_header input.submit { + display: block; + clear: none; + float: left; + padding: 6px 8px; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.content div.sandbox_header img { + display: block; + display: block; + clear: none; + float: right; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.content div.sandbox_header a { + padding: 4px 0 0 10px; + color: #dcb67f; + display: inline-block; + font-size: 0.9em; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.content div.response div.block { + background-color: #fcf6db; + border: 1px solid black; + border-color: #e5e0c6; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.content div.response div.block pre { + font-family: "Anonymous Pro", "Menlo", "Consolas", "Bitstream Vera Sans Mono", "Courier New", monospace; + padding: 10px; + font-size: 0.9em; + max-height: 400px; + overflow-y: auto; +} + + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch { + float: none; + clear: both; + overflow: hidden; + display: block; + margin: 0 0 10px 0; + padding: 0 0 0 0px; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading { + float: none; + clear: both; + overflow: hidden; + display: block; + margin: 0 0 0 0; + padding: 0; + background-color: #FCE9E3; + border: 1px solid black; + border-color: #F5D5C3; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading h3 { + display: block; + clear: none; + float: left; + width: auto; + margin: 0; + padding: 0; + line-height: 1.1em; + color: black; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading h3 span { + margin: 0; + padding: 0; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading h3 span.http_method a { + text-transform: uppercase; + background-color: #D38042; + text-decoration: none; + color: white; + display: inline-block; + width: 50px; + font-size: 0.7em; + text-align: center; + padding: 7px 0 4px 0; + -moz-border-radius: 2px; + -webkit-border-radius: 2px; + -o-border-radius: 2px; + -ms-border-radius: 2px; + -khtml-border-radius: 2px; + border-radius: 2px; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading h3 span.path { + padding-left: 10px; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading h3 span.path a { + color: black; + text-decoration: none; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading h3 span.path a:hover { + text-decoration: underline; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading ul.options { + float: none; + clear: both; + overflow: hidden; + margin: 0; + padding: 0; + display: block; + clear: none; + float: right; + margin: 6px 10px 0 0; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading ul.options li { + float: left; + clear: none; + margin: 0; + padding: 2px 10px; + border-right: 1px solid #dddddd; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading ul.options li:first-child, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading ul.options li.first { + padding-left: 0; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading ul.options li:last-child, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading ul.options li.last { + padding-right: 0; + border-right: none; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading ul.options li { + border-right-color: #f0cecb; + color: #D38042; + font-size: 0.9em; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading ul.options li a { + color: #D38042; + text-decoration: none; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading ul.options li a:hover, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading ul.options li a:active, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.heading ul.options li a.active { + text-decoration: underline; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.content { + background-color: #faf0ef; + border: 1px solid black; + border-color: #f0cecb; + border-top: none; + padding: 10px; + -moz-border-radius-bottomleft: 6px; + -webkit-border-bottom-left-radius: 6px; + -o-border-bottom-left-radius: 6px; + -ms-border-bottom-left-radius: 6px; + -khtml-border-bottom-left-radius: 6px; + border-bottom-left-radius: 6px; + -moz-border-radius-bottomright: 6px; + -webkit-border-bottom-right-radius: 6px; + -o-border-bottom-right-radius: 6px; + -ms-border-bottom-right-radius: 6px; + -khtml-border-bottom-right-radius: 6px; + border-bottom-right-radius: 6px; + margin: 0 0 20px 0; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.content h4 { + color: #D38042; + font-size: 1.1em; + margin: 0; + padding: 15px 0 5px 0px; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.content form input[type='text'].error { + outline: 2px solid black; + outline-color: #F5D5C3; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.content div.sandbox_header { + float: none; + clear: both; + overflow: hidden; + display: block; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.content div.sandbox_header input.submit { + display: block; + clear: none; + float: left; + padding: 6px 8px; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.content div.sandbox_header img { + display: block; + display: block; + clear: none; + float: right; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.content div.sandbox_header a { + padding: 4px 0 0 10px; + color: #dcb67f; + display: inline-block; + font-size: 0.9em; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.content div.response div.block { + background-color: #fcf6db; + border: 1px solid black; + border-color: #e5e0c6; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.patch div.content div.response div.block pre { + font-family: "Anonymous Pro", "Menlo", "Consolas", "Bitstream Vera Sans Mono", "Courier New", monospace; + padding: 10px; + font-size: 0.9em; + max-height: 400px; + overflow-y: auto; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete { + float: none; + clear: both; + overflow: hidden; + display: block; + margin: 0 0 10px 0; + padding: 0 0 0 0px; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading { + float: none; + clear: both; + overflow: hidden; + display: block; + margin: 0 0 0 0; + padding: 0; + background-color: #f5e8e8; + border: 1px solid black; + border-color: #e8c6c7; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading h3 { + display: block; + clear: none; + float: left; + width: auto; + margin: 0; + padding: 0; + line-height: 1.1em; + color: black; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading h3 span { + margin: 0; + padding: 0; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading h3 span.http_method a { + text-transform: uppercase; + background-color: #a41e22; + text-decoration: none; + color: white; + display: inline-block; + width: 50px; + font-size: 0.7em; + text-align: center; + padding: 7px 0 4px 0; + -moz-border-radius: 2px; + -webkit-border-radius: 2px; + -o-border-radius: 2px; + -ms-border-radius: 2px; + -khtml-border-radius: 2px; + border-radius: 2px; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading h3 span.path { + padding-left: 10px; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading h3 span.path a { + color: black; + text-decoration: none; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading h3 span.path a:hover { + text-decoration: underline; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading ul.options { + float: none; + clear: both; + overflow: hidden; + margin: 0; + padding: 0; + display: block; + clear: none; + float: right; + margin: 6px 10px 0 0; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading ul.options li { + float: left; + clear: none; + margin: 0; + padding: 2px 10px; + border-right: 1px solid #dddddd; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading ul.options li:first-child, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading ul.options li.first { + padding-left: 0; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading ul.options li:last-child, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading ul.options li.last { + padding-right: 0; + border-right: none; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading ul.options li { + border-right-color: #e8c6c7; + color: #a41e22; + font-size: 0.9em; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading ul.options li a { + color: #a41e22; + text-decoration: none; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading ul.options li a:hover, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading ul.options li a:active, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.heading ul.options li a.active { + text-decoration: underline; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.content { + background-color: #f7eded; + border: 1px solid black; + border-color: #e8c6c7; + border-top: none; + padding: 10px; + -moz-border-radius-bottomleft: 6px; + -webkit-border-bottom-left-radius: 6px; + -o-border-bottom-left-radius: 6px; + -ms-border-bottom-left-radius: 6px; + -khtml-border-bottom-left-radius: 6px; + border-bottom-left-radius: 6px; + -moz-border-radius-bottomright: 6px; + -webkit-border-bottom-right-radius: 6px; + -o-border-bottom-right-radius: 6px; + -ms-border-bottom-right-radius: 6px; + -khtml-border-bottom-right-radius: 6px; + border-bottom-right-radius: 6px; + margin: 0 0 20px 0; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.content h4 { + color: #a41e22; + font-size: 1.1em; + margin: 0; + padding: 15px 0 5px 0px; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.content form input[type='text'].error { + outline: 2px solid black; + outline-color: #cc0000; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.content div.sandbox_header { + float: none; + clear: both; + overflow: hidden; + display: block; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.content div.sandbox_header input.submit { + display: block; + clear: none; + float: left; + padding: 6px 8px; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.content div.sandbox_header img { + display: block; + display: block; + clear: none; + float: right; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.content div.sandbox_header a { + padding: 4px 0 0 10px; + color: #c8787a; + display: inline-block; + font-size: 0.9em; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.content div.response div.block { + background-color: #fcf6db; + border: 1px solid black; + border-color: #e5e0c6; +} + +body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.delete div.content div.response div.block pre { + font-family: "Anonymous Pro", "Menlo", "Consolas", "Bitstream Vera Sans Mono", "Courier New", monospace; + padding: 10px; + font-size: 0.9em; + max-height: 400px; + overflow-y: auto; +} + + +.model-signature { + font-family: "Droid Sans", sans-serif; + font-size: 1em; + line-height: 1.5em; +} +.model-signature span { + font-size: 0.9em; + line-height: 1.5em; +} +.model-signature span:nth-child(odd) { color:#333; } +.model-signature span:nth-child(even) { color:#C5862B; } diff --git a/htdocs/public/api/explorer/images/pet_store_api.png b/htdocs/public/api/explorer/images/pet_store_api.png new file mode 100644 index 00000000000..f9f9cd4aeb3 Binary files /dev/null and b/htdocs/public/api/explorer/images/pet_store_api.png differ diff --git a/htdocs/public/api/explorer/images/throbber.gif b/htdocs/public/api/explorer/images/throbber.gif new file mode 100644 index 00000000000..06393889242 Binary files /dev/null and b/htdocs/public/api/explorer/images/throbber.gif differ diff --git a/htdocs/public/api/explorer/index.html b/htdocs/public/api/explorer/index.html new file mode 100644 index 00000000000..277292f818d --- /dev/null +++ b/htdocs/public/api/explorer/index.html @@ -0,0 +1,95 @@ + + + Restler API Explorer + + + + + + + + + + + + + + + + + + + + + +
    +   +
    + +
    + +
    + + + + \ No newline at end of file diff --git a/htdocs/public/api/explorer/lib/backbone-min.js b/htdocs/public/api/explorer/lib/backbone-min.js new file mode 100644 index 00000000000..c1c0d4fff28 --- /dev/null +++ b/htdocs/public/api/explorer/lib/backbone-min.js @@ -0,0 +1,38 @@ +// Backbone.js 0.9.2 + +// (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc. +// Backbone may be freely distributed under the MIT license. +// For all details and documentation: +// http://backbonejs.org +(function(){var l=this,y=l.Backbone,z=Array.prototype.slice,A=Array.prototype.splice,g;g="undefined"!==typeof exports?exports:l.Backbone={};g.VERSION="0.9.2";var f=l._;!f&&"undefined"!==typeof require&&(f=require("underscore"));var i=l.jQuery||l.Zepto||l.ender;g.setDomLibrary=function(a){i=a};g.noConflict=function(){l.Backbone=y;return this};g.emulateHTTP=!1;g.emulateJSON=!1;var p=/\s+/,k=g.Events={on:function(a,b,c){var d,e,f,g,j;if(!b)return this;a=a.split(p);for(d=this._callbacks||(this._callbacks= +{});e=a.shift();)f=(j=d[e])?j.tail:{},f.next=g={},f.context=c,f.callback=b,d[e]={tail:g,next:j?j.next:f};return this},off:function(a,b,c){var d,e,h,g,j,q;if(e=this._callbacks){if(!a&&!b&&!c)return delete this._callbacks,this;for(a=a?a.split(p):f.keys(e);d=a.shift();)if(h=e[d],delete e[d],h&&(b||c))for(g=h.tail;(h=h.next)!==g;)if(j=h.callback,q=h.context,b&&j!==b||c&&q!==c)this.on(d,j,q);return this}},trigger:function(a){var b,c,d,e,f,g;if(!(d=this._callbacks))return this;f=d.all;a=a.split(p);for(g= +z.call(arguments,1);b=a.shift();){if(c=d[b])for(e=c.tail;(c=c.next)!==e;)c.callback.apply(c.context||this,g);if(c=f){e=c.tail;for(b=[b].concat(g);(c=c.next)!==e;)c.callback.apply(c.context||this,b)}}return this}};k.bind=k.on;k.unbind=k.off;var o=g.Model=function(a,b){var c;a||(a={});b&&b.parse&&(a=this.parse(a));if(c=n(this,"defaults"))a=f.extend({},c,a);b&&b.collection&&(this.collection=b.collection);this.attributes={};this._escapedAttributes={};this.cid=f.uniqueId("c");this.changed={};this._silent= +{};this._pending={};this.set(a,{silent:!0});this.changed={};this._silent={};this._pending={};this._previousAttributes=f.clone(this.attributes);this.initialize.apply(this,arguments)};f.extend(o.prototype,k,{changed:null,_silent:null,_pending:null,idAttribute:"id",initialize:function(){},toJSON:function(){return f.clone(this.attributes)},get:function(a){return this.attributes[a]},escape:function(a){var b;if(b=this._escapedAttributes[a])return b;b=this.get(a);return this._escapedAttributes[a]=f.escape(null== +b?"":""+b)},has:function(a){return null!=this.get(a)},set:function(a,b,c){var d,e;f.isObject(a)||null==a?(d=a,c=b):(d={},d[a]=b);c||(c={});if(!d)return this;d instanceof o&&(d=d.attributes);if(c.unset)for(e in d)d[e]=void 0;if(!this._validate(d,c))return!1;this.idAttribute in d&&(this.id=d[this.idAttribute]);var b=c.changes={},h=this.attributes,g=this._escapedAttributes,j=this._previousAttributes||{};for(e in d){a=d[e];if(!f.isEqual(h[e],a)||c.unset&&f.has(h,e))delete g[e],(c.silent?this._silent: +b)[e]=!0;c.unset?delete h[e]:h[e]=a;!f.isEqual(j[e],a)||f.has(h,e)!=f.has(j,e)?(this.changed[e]=a,c.silent||(this._pending[e]=!0)):(delete this.changed[e],delete this._pending[e])}c.silent||this.change(c);return this},unset:function(a,b){(b||(b={})).unset=!0;return this.set(a,null,b)},clear:function(a){(a||(a={})).unset=!0;return this.set(f.clone(this.attributes),a)},fetch:function(a){var a=a?f.clone(a):{},b=this,c=a.success;a.success=function(d,e,f){if(!b.set(b.parse(d,f),a))return!1;c&&c(b,d)}; +a.error=g.wrapError(a.error,b,a);return(this.sync||g.sync).call(this,"read",this,a)},save:function(a,b,c){var d,e;f.isObject(a)||null==a?(d=a,c=b):(d={},d[a]=b);c=c?f.clone(c):{};if(c.wait){if(!this._validate(d,c))return!1;e=f.clone(this.attributes)}a=f.extend({},c,{silent:!0});if(d&&!this.set(d,c.wait?a:c))return!1;var h=this,i=c.success;c.success=function(a,b,e){b=h.parse(a,e);if(c.wait){delete c.wait;b=f.extend(d||{},b)}if(!h.set(b,c))return false;i?i(h,a):h.trigger("sync",h,a,c)};c.error=g.wrapError(c.error, +h,c);b=this.isNew()?"create":"update";b=(this.sync||g.sync).call(this,b,this,c);c.wait&&this.set(e,a);return b},destroy:function(a){var a=a?f.clone(a):{},b=this,c=a.success,d=function(){b.trigger("destroy",b,b.collection,a)};if(this.isNew())return d(),!1;a.success=function(e){a.wait&&d();c?c(b,e):b.trigger("sync",b,e,a)};a.error=g.wrapError(a.error,b,a);var e=(this.sync||g.sync).call(this,"delete",this,a);a.wait||d();return e},url:function(){var a=n(this,"urlRoot")||n(this.collection,"url")||t(); +return this.isNew()?a:a+("/"==a.charAt(a.length-1)?"":"/")+encodeURIComponent(this.id)},parse:function(a){return a},clone:function(){return new this.constructor(this.attributes)},isNew:function(){return null==this.id},change:function(a){a||(a={});var b=this._changing;this._changing=!0;for(var c in this._silent)this._pending[c]=!0;var d=f.extend({},a.changes,this._silent);this._silent={};for(c in d)this.trigger("change:"+c,this,this.get(c),a);if(b)return this;for(;!f.isEmpty(this._pending);){this._pending= +{};this.trigger("change",this,a);for(c in this.changed)!this._pending[c]&&!this._silent[c]&&delete this.changed[c];this._previousAttributes=f.clone(this.attributes)}this._changing=!1;return this},hasChanged:function(a){return!arguments.length?!f.isEmpty(this.changed):f.has(this.changed,a)},changedAttributes:function(a){if(!a)return this.hasChanged()?f.clone(this.changed):!1;var b,c=!1,d=this._previousAttributes,e;for(e in a)if(!f.isEqual(d[e],b=a[e]))(c||(c={}))[e]=b;return c},previous:function(a){return!arguments.length|| +!this._previousAttributes?null:this._previousAttributes[a]},previousAttributes:function(){return f.clone(this._previousAttributes)},isValid:function(){return!this.validate(this.attributes)},_validate:function(a,b){if(b.silent||!this.validate)return!0;var a=f.extend({},this.attributes,a),c=this.validate(a,b);if(!c)return!0;b&&b.error?b.error(this,c,b):this.trigger("error",this,c,b);return!1}});var r=g.Collection=function(a,b){b||(b={});b.model&&(this.model=b.model);b.comparator&&(this.comparator=b.comparator); +this._reset();this.initialize.apply(this,arguments);a&&this.reset(a,{silent:!0,parse:b.parse})};f.extend(r.prototype,k,{model:o,initialize:function(){},toJSON:function(a){return this.map(function(b){return b.toJSON(a)})},add:function(a,b){var c,d,e,g,i,j={},k={},l=[];b||(b={});a=f.isArray(a)?a.slice():[a];c=0;for(d=a.length;c=b))this.iframe=i('