(2010-) * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt * @package API * @since 0.10.0 * */ abstract class ApiCommand extends ApiParameter { /** * debug flag * * @var boolean */ private $debug = false; /** * is admin flag * * @var boolean */ private $is_admin = false; /** * internal user data array * * @var array */ private $user_data = null; /** * logger interface * * @var \Froxlor\FroxlorLogger */ private $logger = null; /** * mail interface * * @var \Froxlor\System\Mailer */ private $mail = null; /** * whether the call is an internal one or not * * @var boolean */ private $internal_call = false; /** * language strings array * * @var array */ protected $lng = null; /** * froxlor version * * @var string */ protected $version = null; /** * froxlor dbversion * * @var int */ protected $dbversion = null; /** * froxlor version-branding * * @var string */ protected $branding = null; /** * * @param array $header * optional, passed via API * @param array $params * optional, array of parameters (var=>value) for the command * @param array $userinfo * optional, passed via WebInterface (instead of $header) * @param boolean $internal * optional whether called internally, default false * * @throws \Exception */ public function __construct($header = null, $params = null, $userinfo = null, $internal = false) { parent::__construct($params); $this->version = \Froxlor\Froxlor::VERSION; $this->dbversion = \Froxlor\Froxlor::DBVERSION; $this->branding = \Froxlor\Froxlor::BRANDING; if (! empty($header)) { $this->readUserData($header); } elseif (! empty($userinfo)) { $this->user_data = $userinfo; $this->is_admin = (isset($userinfo['adminsession']) && $userinfo['adminsession'] == 1 && $userinfo['adminid'] > 0) ? true : false; } else { throw new \Exception("Invalid user data", 500); } $this->logger = \Froxlor\FroxlorLogger::getInstanceOf($this->user_data); // check whether the user is deactivated if ($this->getUserDetail('deactivated') == 1) { $this->logger()->logAction(\Froxlor\FroxlorLogger::LOG_ERROR, LOG_INFO, "[API] User '" . $this->getUserDetail('loginnname') . "' tried to use API but is deactivated"); throw new \Exception("Account suspended", 406); } $this->initLang(); /** * Initialize the mailingsystem */ $this->mail = new \Froxlor\System\Mailer(true); if ($this->debug) { $this->logger()->logAction(\Froxlor\FroxlorLogger::LOG_ERROR, LOG_DEBUG, "[API] " . get_called_class() . ": " . json_encode($params, JSON_UNESCAPED_SLASHES)); } // set internal call flag $this->internal_call = $internal; } /** * initialize global $lng variable to have * localized strings available for the ApiCommands */ private function initLang() { global $lng; // query the whole table $result_stmt = \Froxlor\Database\Database::query("SELECT * FROM `" . TABLE_PANEL_LANGUAGE . "`"); $langs = array(); // presort languages while ($row = $result_stmt->fetch(\PDO::FETCH_ASSOC)) { $langs[$row['language']][] = $row; } // set default language before anything else to // ensure that we can display messages $language = \Froxlor\Settings::Get('panel.standardlanguage'); if (isset($this->user_data['language']) && isset($langs[$this->user_data['language']])) { // default: use language from session, #277 $language = $this->user_data['language']; } elseif (isset($this->user_data['def_language'])) { $language = $this->user_data['def_language']; } // include every english language file we can get foreach ($langs['English'] as $value) { include_once \Froxlor\FileDir::makeSecurePath(\Froxlor\Froxlor::getInstallDir() . '/' . $value['file']); } // now include the selected language if its not english if ($language != 'English') { if (isset($langs[$language])) { foreach ($langs[$language] as $value) { include_once \Froxlor\FileDir::makeSecurePath(\Froxlor\Froxlor::getInstallDir() . '/' . $value['file']); } } else { if ($this->debug) { $this->logger()->logAction(\Froxlor\FroxlorLogger::LOG_ERROR, LOG_DEBUG, "[API] unable to include user-language '" . $language . "'. Not found in database.", 404); } } } // last but not least include language references file include_once \Froxlor\FileDir::makeSecurePath(\Froxlor\Froxlor::getInstallDir() . '/lng/lng_references.php'); // set array for ApiCommand $this->lng = $lng; } /** * returns an instance of the wanted ApiCommand (e.g. * Customers, Domains, etc); * this is used widely in the WebInterface * * @param array $userinfo * array of user-data * @param array $params * array of parameters for the command * @param boolean $internal * optional whether called internally, default false * * @return ApiCommand * @throws \Exception */ public static function getLocal($userinfo = null, $params = null, $internal = false) { return new static(null, $params, $userinfo, $internal); } /** * admin flag * * @return boolean */ protected function isAdmin() { return $this->is_admin; } /** * internal call flag * * @return boolean */ protected function isInternal() { return $this->internal_call; } /** * return field from user-table * * @param string $detail * * @return string */ protected function getUserDetail($detail = null) { return (isset($this->user_data[$detail]) ? $this->user_data[$detail] : null); } /** * return user-data array * * @return array */ protected function getUserData() { return $this->user_data; } /** * return SQL when parameter $sql_search is given via API * * @param array $sql_search * optional array with index = fieldname, and value = array with 'op' => operator (one of <, > or =), LIKE is used if left empty and 'value' => searchvalue * @param array $query_fields * optional array of placeholders mapped to the actual value which is used in the API commands when executing the statement [internal] * @param boolean $append * optional append to WHERE clause rather then create new one, default false [internal] * * @return string */ protected function getSearchWhere(&$query_fields = array(), $append = false) { $search = $this->getParam('sql_search', true, array()); $condition = ''; if (! empty($search)) { if ($append == true) { $condition = ' AND '; } else { $condition = ' WHERE '; } $ops = array( '<', '>', '=' ); $first = true; foreach ($search as $field => $valoper) { $cleanfield = str_replace(".", "", $field); $sortfield = explode('.', $field); foreach ($sortfield as $id => $sfield) { if (substr($sfield, - 1, 1) != '`') { $sfield .= '`'; } if ($sfield[0] != '`') { $sfield = '`' . $sfield; } $sortfield[$id] = $sfield; } $field = implode('.', $sortfield); if (preg_match('/^([a-z0-9\-\._`]+)$/i', $field) == false) { // skip continue; } if (! $first) { $condition .= ' AND '; } if (! is_array($valoper) || ! isset($valoper['op']) || empty($valoper['op'])) { $condition .= $field . ' LIKE :' . $cleanfield; if (! is_array($valoper)) { $query_fields[':' . $cleanfield] = '%' . $valoper . '%'; } else { $query_fields[':' . $cleanfield] = '%' . $valoper['value'] . '%'; } } elseif (in_array($valoper['op'], $ops)) { $condition .= $field . ' ' . $valoper['op'] . ':' . $cleanfield; $query_fields[':' . $cleanfield] = $valoper['value'] ?? ''; } elseif (strtolower($valoper['op']) == 'in' && is_array($valoper['value']) && count($valoper['value']) > 0) { $condition .= $field . ' ' . $valoper['op'] . ' ('; foreach ($valoper['value'] as $incnt => $invalue) { if (!is_numeric($incnt)) { // skip continue; } if (!empty($invalue) && preg_match('/^([a-z0-9\-\._`]+)$/i', $invalue) == false) { // skip continue; } $condition .= ":" . $cleanfield . $incnt . ", "; $query_fields[':' . $cleanfield . $incnt] = $invalue ?? ''; } $condition = substr($condition, 0, - 2) . ')'; } else { continue; } if ($first) { $first = false; } } } return $condition; } /** * return LIMIT clause when at least $sql_limit parameter is given via API * * @param int $sql_limit * optional, limit resultset, default 0 * @param int $sql_offset * optional, offset for limitation, default 0 * * @return string */ protected function getLimit() { $limit = $this->getParam('sql_limit', true, 0); $offset = $this->getParam('sql_offset', true, 0); if (! is_numeric($limit)) { $limit = 0; } if (! is_numeric($offset)) { $offset = 0; } if ($limit > 0) { return ' LIMIT ' . $offset . ',' . $limit; } return ''; } /** * return ORDER BY clause if parameter $sql_orderby parameter is given via API * * @param array $sql_orderby * optional array with index = fieldname and value = ASC|DESC * @param boolean $append * optional append to ORDER BY clause rather then create new one, default false [internal] * * @return string */ protected function getOrderBy($append = false) { $orderby = $this->getParam('sql_orderby', true, array()); $order = ""; if (! empty($orderby)) { if ($append) { $order .= ", "; } else { $order .= " ORDER BY "; } $nat_fields = [ '`c`.`loginname`', '`a`.`loginname`', '`adminname`', '`databasename`', '`username`' ]; foreach ($orderby as $field => $by) { $sortfield = explode('.', $field); foreach ($sortfield as $id => $sfield) { if (substr($sfield, - 1, 1) != '`') { $sfield .= '`'; } if ($sfield[0] != '`') { $sfield = '`' . $sfield; } $sortfield[$id] = $sfield; } $field = implode('.', $sortfield); if (preg_match('/^([a-z0-9\-\._`]+)$/i', $field) == false) { // skip continue; } $by = strtoupper($by); if (! in_array($by, [ 'ASC', 'DESC' ])) { $by = 'ASC'; } if (\Froxlor\Settings::Get('panel.natsorting') == 1 && in_array($field, $nat_fields)) { // Acts similar to php's natsort(), found in one comment at http://my.opera.com/cpr/blog/show.dml/160556 $order .= "CONCAT( IF( ASCII( LEFT( " . $field . ", 5 ) ) > 57, LEFT( " . $field . ", 1 ), 0 ), IF( ASCII( RIGHT( " . $field . ", 1 ) ) > 57, LPAD( " . $field . ", 255, '0' ), LPAD( CONCAT( " . $field . ", '-' ), 255, '0' ) )) " . $by . ", "; } else { $order .= $field . " " . $by . ", "; } } $order = substr($order, 0, - 2); } return $order; } /** * return logger instance * * @return \Froxlor\FroxlorLogger */ protected function logger() { return $this->logger; } /** * return mailer instance * * @return \Froxlor\System\Mailer */ protected function mailer() { return $this->mail; } /** * call an api-command internally * * @param string $command * @param array|null $params * @param boolean $internal * optional whether called internally, default false * * * @return array */ protected function apiCall($command = null, $params = null, $internal = false) { $_command = explode(".", $command); $module = __NAMESPACE__ . "\Commands\\" . $_command[0]; $function = $_command[1]; $json_result = $module::getLocal($this->getUserData(), $params, $internal)->{$function}(); return json_decode($json_result, true)['data']; } /** * return api-compatible response in JSON format and send corresponding http-header * * @param int $status * @param string $status_message * @param mixed $data * * @return string json-encoded response message */ protected function response($status, $status_message, $data = null) { if (isset($_SERVER["SERVER_PROTOCOL"]) && ! empty($_SERVER["SERVER_PROTOCOL"])) { $resheader = $_SERVER["SERVER_PROTOCOL"] . " " . $status; if (! empty($status_message)) { $resheader .= ' ' . str_replace("\n", " ", $status_message); } header($resheader); } $response = array(); $response['status'] = $status; $response['status_message'] = $status_message; $response['data'] = $data; $json_response = json_encode($response, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); return $json_response; } /** * returns an array of customers the current user can access * * @param string $customer_hide_option * optional, when called as customer, some options might be hidden due to the panel.customer_hide_options ettings * * @throws \Exception * @return array */ protected function getAllowedCustomerIds($customer_hide_option = '') { $customer_ids = array(); if ($this->isAdmin()) { // if we're an admin, list all ftp-users of all the admins customers // or optionally for one specific customer identified by id or loginname $customerid = $this->getParam('customerid', true, 0); $loginname = $this->getParam('loginname', true, ''); if (! empty($customerid) || ! empty($loginname)) { $_result = $this->apiCall('Customers.get', array( 'id' => $customerid, 'loginname' => $loginname )); $custom_list_result = array( $_result ); } else { $_custom_list_result = $this->apiCall('Customers.listing'); $custom_list_result = $_custom_list_result['list']; } foreach ($custom_list_result as $customer) { $customer_ids[] = $customer['customerid']; } } else { if (! $this->isInternal() && ! empty($customer_hide_option) && \Froxlor\Settings::IsInList('panel.customer_hide_options', $customer_hide_option)) { throw new \Exception("You cannot access this resource", 405); } $customer_ids = array( $this->getUserDetail('customerid') ); } if (empty($customer_ids)) { throw new \Exception("Required resource unsatisfied.", 405); } return $customer_ids; } /** * returns an array of customer data for customer, or by customer-id/loginname for admin/reseller * * @param int $customerid * optional, required if loginname is empty * @param string $loginname * optional, required of customerid is empty * @param string $customer_resource_check * optional, when called as admin, check the resources of the target customer * * @throws \Exception * @return array */ protected function getCustomerData($customer_resource_check = '') { if ($this->isAdmin()) { $customerid = $this->getParam('customerid', true, 0); $loginname = $this->getParam('loginname', true, ''); $customer = $this->apiCall('Customers.get', array( 'id' => $customerid, 'loginname' => $loginname )); // check whether the customer has enough resources if (! empty($customer_resource_check) && $customer[$customer_resource_check . '_used'] >= $customer[$customer_resource_check] && $customer[$customer_resource_check] != '-1') { throw new \Exception("Customer has no more resources available", 406); } } else { $customer = $this->getUserData(); } return $customer; } /** * increase/decrease a resource field for customers/admins * * @param string $table * @param string $keyfield * @param int $key * @param string $operator * @param string $resource * @param string $extra * @param int $step */ protected static function updateResourceUsage($table = null, $keyfield = null, $key = null, $operator = '+', $resource = null, $extra = null, $step = 1) { $stmt = \Froxlor\Database\Database::prepare(" UPDATE `" . $table . "` SET `" . $resource . "` = `" . $resource . "` " . $operator . " " . (int) $step . " " . $extra . " WHERE `" . $keyfield . "` = :key "); \Froxlor\Database\Database::pexecute($stmt, array( 'key' => $key ), true, true); } /** * return email template content from database or global language file if not found in DB * * @param array $customerdata * @param string $group * @param string $varname * @param array $replace_arr * @param string $default * * @return string */ protected function getMailTemplate($customerdata = null, $group = null, $varname = null, $replace_arr = array(), $default = "") { // get template $stmt = \Froxlor\Database\Database::prepare(" SELECT `value` FROM `" . TABLE_PANEL_TEMPLATES . "` WHERE `adminid`= :adminid AND `language`= :lang AND `templategroup`= :group AND `varname`= :var "); $result = \Froxlor\Database\Database::pexecute_first($stmt, array( "adminid" => $customerdata['adminid'], "lang" => $customerdata['def_language'], "group" => $group, "var" => $varname ), true, true); $content = $default; if ($result) { $content = $result['value'] ?? $default; } // @fixme html_entity_decode $content = html_entity_decode(\Froxlor\PhpHelper::replaceVariables($content, $replace_arr)); return $content; } /** * read user data from database by api-request-header fields * * @param array $header * api-request header * * @throws \Exception * @return boolean */ private function readUserData($header = null) { $sel_stmt = \Froxlor\Database\Database::prepare("SELECT * FROM `api_keys` WHERE `apikey` = :ak AND `secret` = :as"); $result = \Froxlor\Database\Database::pexecute_first($sel_stmt, array( 'ak' => $header['apikey'], 'as' => $header['secret'] ), true, true); if ($result) { // admin or customer? if ($result['customerid'] == 0 && $result['adminid'] > 0) { $this->is_admin = true; $table = 'panel_admins'; $key = "adminid"; } elseif ($result['customerid'] > 0 && $result['adminid'] > 0) { $this->is_admin = false; $table = 'panel_customers'; $key = "customerid"; } else { // neither adminid is > 0 nor customerid is > 0 - sorry man, no way throw new \Exception("Invalid API credentials", 400); } $sel_stmt = \Froxlor\Database\Database::prepare("SELECT * FROM `" . $table . "` WHERE `" . $key . "` = :id"); $this->user_data = \Froxlor\Database\Database::pexecute_first($sel_stmt, array( 'id' => ($this->is_admin ? $result['adminid'] : $result['customerid']) ), true, true); if ($this->is_admin) { $this->user_data['adminsession'] = 1; } return true; } throw new \Exception("Invalid API credentials", 400); } }