3 * Bacula(R) - The Network Backup Solution
4 * Baculum - Bacula web interface
6 * Copyright (C) 2013-2017 Kern Sibbald
8 * The main author of Baculum is Marcin Haba.
9 * The original author of Bacula is Kern Sibbald, with contributions
10 * from many others, a complete list can be found in the file AUTHORS.
12 * You may use this file and others of this release according to the
13 * license defined in the LICENSE file, which includes the Affero General
14 * Public License, v3.0 ("AGPLv3") and some additional permissions and
15 * terms pursuant to its AGPLv3 Section 7.
17 * This notice must be preserved when any source code is
18 * conveyed and/or propagated.
20 * Bacula(R) is a registered trademark of Kern Sibbald.
23 Prado::using('System.Web.UI.TPage');
24 Prado::using('System.Exceptions.TException');
25 Prado::using('Application.Common.Class.Errors');
26 Prado::using('Application.Common.Class.OAuth2');
27 Prado::using('Application.Common.Class.Logging');
28 Prado::using('Application.API.Class.BException');
29 Prado::using('Application.API.Class.APIDbModule');
30 Prado::using('Application.API.Class.Bconsole');
31 Prado::using('Application.API.Class.OAuth2.TokenRecord');
34 * Abstract module from which inherits each of API module.
35 * The module contains methods that are common for all API pages.
37 * @author Marcin Haba <marcin.haba@bacula.pl>
39 abstract class BaculumAPIServer extends TPage {
42 * Storing output from API commands in numeric array.
47 * Storing error from API commands as integer value.
52 * Storing currently used Director name for bconsole commands.
57 * Web interface User name that sent request to API.
58 * Null value means administrator, any other value means normal user
63 private $public_endpoints = array('auth', 'token', 'welcome', 'catalog', 'dbsize', 'directors');
70 const GET_METHOD = 'GET';
72 // create new elemenet
73 const POST_METHOD = 'POST';
76 const PUT_METHOD = 'PUT';
79 const DELETE_METHOD = 'DELETE';
82 * Get request, login user and do request action.
85 * @param mixed $params onInit action params
88 public function onInit($params) {
89 parent::onInit($params);
91 * Workaround to bug in PHP 5.6 by FastCGI that caused general protection error.
92 * @TODO: Check on newer PHP if it is already fixed.
94 // @TODO: Move it to API module.
95 //$db_params = $this->getModule('api_config')->getConfig('db');
96 //APIDbModule::getAPIDbConnection($db_params);
98 // set Director to bconsole execution
99 $this->director = $this->Request->contains('director') ? $this->Request['director'] : null;
102 $config = $this->getModule('api_config')->getConfig('api');
103 Logging::$debug_enabled = (array_key_exists('debug', $config) && $config['debug'] == 1);
104 $headers = $this->getRequest()->getHeaders(CASE_LOWER);
105 if (array_key_exists('auth_type', $config) && array_key_exists('authorization', $headers) && preg_match('/^\w+ [\w=]+$/', $headers['authorization']) === 1) {
106 list($type, $token) = explode(' ', $headers['authorization'], 2);
107 if ($config['auth_type'] === 'oauth2' && $type === 'Bearer') {
108 // deleting expired tokens
109 $this->getModule('oauth2_token')->deleteExpiredTokens();
111 $auth = TokenRecord::findByPk($token);
112 if (is_array($auth)) {
113 if ($this->isScopeValid($auth['scope'])) {
116 $this->init_auth($auth);
118 // Scopes error. Access to not allowed resource
119 header(OAuth2::HEADER_UNAUTHORIZED);
120 $url = $this->getRequest()->getUrl()->getPath();
121 $this->output = AuthorizationError::MSG_ERROR_ACCESS_ATTEMPT_TO_NOT_ALLOWED_RESOURCE .' Endpoint: ' . $url;
122 $this->error = AuthorizationError::ERROR_ACCESS_ATTEMPT_TO_NOT_ALLOWED_RESOURCE;
127 } elseif ($config['auth_type'] === 'basic' && $type === 'Basic') {
134 if ($is_auth === false) {
135 // Authorization error.
136 header(OAuth2::HEADER_UNAUTHORIZED);
137 $this->output = AuthorizationError::MSG_ERROR_AUTHORIZATION_TO_API_PROBLEM;
138 $this->error = AuthorizationError::ERROR_AUTHORIZATION_TO_API_PROBLEM;
142 switch($_SERVER['REQUEST_METHOD']) {
143 case self::GET_METHOD: {
147 case self::POST_METHOD: {
151 case self::PUT_METHOD: {
155 case self::DELETE_METHOD: {
160 } catch(TException $e) {
161 $this->getModule('logging')->log(
163 "Method: {$_SERVER['REQUEST_METHOD']} $e",
164 Logging::CATEGORY_APPLICATION,
168 if ($e instanceof BException) {
169 $this->output = $e->getErrorMessage();
170 $this->error = $e->getErrorCode();
172 $this->output = GenericError::MSG_ERROR_INTERNAL_ERROR . ' ' . $e->getErrorMessage();
173 $this->error = GenericError::ERROR_INTERNAL_ERROR;
179 * Initialize auth parameters.
181 * @param array $auth token params stored in TokenRecord session
184 private function init_auth(array $auth) {
185 // if client has own bconsole config, assign it here
186 if (array_key_exists('bconsole_cfg_path', $auth) && !empty($auth['bconsole_cfg_path'])) {
187 Bconsole::setCfgPath($auth['bconsole_cfg_path'], true);
192 * Get request result data and pack it in JSON format.
194 * "output": (list) output values
195 * "error" : (integer) result exit code (0 - OK, non-zero - error)
198 * @return string JSON value with output and error values
200 private function getOutput() {
201 $output = array('output' => $this->output, 'error' => $this->error);
202 $this->setOutputHeaders();
203 $json = json_encode($output);
208 * Set output headers to send in response.
210 private function setOutputHeaders() {
211 $this->getResponse()->setContentType('application/json');
215 * Return action result which was realized in onInit() method.
216 * On standard output is printed JSON value with request results.
219 * @param mixed $params onInit action params
222 public function onLoad($params) {
223 parent::onLoad($params);
224 echo $this->getOutput();
228 * Changing/updating values via API.
233 private function put() {
234 $id = $this->Request->contains('id') ? $this->Request['id'] : null;
237 * Check if it is possible to read PUT method data.
238 * Note that some clients sends data in PUT request as PHP input stream which
239 * is not possible to read by $_REQUEST data. From this reason, when is
240 * not possible to ready by superglobal $_REQUEST variable, then is try to
241 * read PUT data by PHP input stream.
243 if ($this->Request->contains('update') && is_array($this->Request['update']) && count($this->Request['update']) > 0) {
244 // $_REQUEST available to read
245 $params = (object)$this->Request['update'];
246 $this->set($id, $params);
248 // no possibility to read data from $_REQUEST. Try to load from input stream.
249 $inputstr = file_get_contents("php://input");
252 * Read using chunks for case large updates (over 1000 values).
253 * Otherwise max_input_vars limitation in php.ini can be reached (usually
254 * set to 1000 variables)
255 * @see http://php.net/manual/en/info.configuration.php#ini.max-input-vars
257 $chunks = explode('&', $inputstr);
259 $response_data = array();
260 for($i = 0; $i<count($chunks); $i++) {
261 // if chunks would not be used, then here occurs reach max_input_vars limit
262 parse_str($chunks[$i], $response_el);
263 if (is_array($response_el) && array_key_exists('update', $response_el) && is_array($response_el['update'])) {
264 $key = key($response_el['update']);
265 $response_data['update'][$key] = $response_el['update'][$key];
268 if (is_array($response_data) && array_key_exists('update', $response_data)) {
269 $params = (object)$response_data['update'];
270 $this->set($id, $params);
273 * This case should never occur because it means that there is
274 * given nothing to update.
276 $params = new stdClass;
277 $this->set($id, $params);
283 * Creating new elements.
288 private function post() {
289 $params = new stdClass;
290 if ($this->Request->contains('create') && is_array($this->Request['create']) && count($this->Request['create']) > 0) {
291 $params = (object)$this->Request['create'];
293 $this->create($params);
297 * Deleting element by element ID.
302 private function delete() {
304 if ($this->Request->contains('id')) {
305 $id = intval($this->Request['id']);
311 * Check if request is allowed to access basing on OAuth2 scope.
314 * @param string scopes assigned with token
315 * @return bool true if scope in url and from token are valid, otherwise false
317 private function isScopeValid($scope) {
319 $scopes = explode(' ', $scope);
320 $url = $this->getRequest()->getUrl()->getPath();
321 $params = explode('/', $url);
322 if (count($params) >= 3 && $params[1] === 'api') {
323 if (in_array($params[2], $this->public_endpoints)) {
326 for ($i = 0; $i < count($scopes); $i++) {
327 if ($params[2] === $scopes[$i]) {
338 * Shortcut method for getting application modules instances by
342 * @param string $name application module name
343 * @return object module class instance
345 public function getModule($name) {
346 return $this->Application->getModule($name);