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 * API server version (used in HTTP header)
44 const API_SERVER_VERSION = 0.1;
47 * Storing output from API commands in numeric array.
52 * Storing error from API commands as integer value.
57 * Storing currently used Director name for bconsole commands.
62 * Web interface User name that sent request to API.
63 * Null value means administrator, any other value means normal user
68 private $public_endpoints = array('auth', 'token', 'welcome', 'catalog', 'dbsize', 'directors');
75 const GET_METHOD = 'GET';
77 // create new elemenet
78 const POST_METHOD = 'POST';
81 const PUT_METHOD = 'PUT';
84 const DELETE_METHOD = 'DELETE';
87 * Get request, login user and do request action.
90 * @param mixed $params onInit action params
93 public function onInit($params) {
94 parent::onInit($params);
96 * Workaround to bug in PHP 5.6 by FastCGI that caused general protection error.
97 * @TODO: Check on newer PHP if it is already fixed.
99 // @TODO: Move it to API module.
100 //$db_params = $this->getModule('api_config')->getConfig('db');
101 //APIDbModule::getAPIDbConnection($db_params);
103 // set Director to bconsole execution
104 $this->director = $this->Request->contains('director') ? $this->Request['director'] : null;
107 $config = $this->getModule('api_config')->getConfig('api');
108 Logging::$debug_enabled = (array_key_exists('debug', $config) && $config['debug'] == 1);
109 $headers = $this->getRequest()->getHeaders(CASE_LOWER);
110 if (array_key_exists('auth_type', $config) && array_key_exists('authorization', $headers) && preg_match('/^\w+ [\w=]+$/', $headers['authorization']) === 1) {
111 list($type, $token) = explode(' ', $headers['authorization'], 2);
112 if ($config['auth_type'] === 'oauth2' && $type === 'Bearer') {
113 // deleting expired tokens
114 $this->getModule('oauth2_token')->deleteExpiredTokens();
116 $auth = TokenRecord::findByPk($token);
117 if (is_array($auth)) {
118 if ($this->isScopeValid($auth['scope'])) {
121 $this->init_auth($auth);
123 // Scopes error. Access to not allowed resource
124 header(OAuth2::HEADER_UNAUTHORIZED);
125 $url = $this->getRequest()->getUrl()->getPath();
126 $this->output = AuthorizationError::MSG_ERROR_ACCESS_ATTEMPT_TO_NOT_ALLOWED_RESOURCE .' Endpoint: ' . $url;
127 $this->error = AuthorizationError::ERROR_ACCESS_ATTEMPT_TO_NOT_ALLOWED_RESOURCE;
132 } elseif ($config['auth_type'] === 'basic' && $type === 'Basic') {
139 if ($is_auth === false) {
140 // Authorization error.
141 header(OAuth2::HEADER_UNAUTHORIZED);
142 $this->output = AuthorizationError::MSG_ERROR_AUTHORIZATION_TO_API_PROBLEM;
143 $this->error = AuthorizationError::ERROR_AUTHORIZATION_TO_API_PROBLEM;
147 switch($_SERVER['REQUEST_METHOD']) {
148 case self::GET_METHOD: {
152 case self::POST_METHOD: {
156 case self::PUT_METHOD: {
160 case self::DELETE_METHOD: {
165 } catch(TException $e) {
166 $this->getModule('logging')->log(
168 "Method: {$_SERVER['REQUEST_METHOD']} $e",
169 Logging::CATEGORY_APPLICATION,
173 if ($e instanceof BException) {
174 $this->output = $e->getErrorMessage();
175 $this->error = $e->getErrorCode();
177 $this->output = GenericError::MSG_ERROR_INTERNAL_ERROR . ' ' . $e->getErrorMessage();
178 $this->error = GenericError::ERROR_INTERNAL_ERROR;
184 * Initialize auth parameters.
186 * @param array $auth token params stored in TokenRecord session
189 private function init_auth(array $auth) {
190 // if client has own bconsole config, assign it here
191 if (array_key_exists('bconsole_cfg_path', $auth) && !empty($auth['bconsole_cfg_path'])) {
192 Bconsole::setCfgPath($auth['bconsole_cfg_path'], true);
197 * Get request result data and pack it in JSON format.
199 * "output": (list) output values
200 * "error" : (integer) result exit code (0 - OK, non-zero - error)
203 * @return string JSON value with output and error values
205 private function getOutput() {
206 $output = array('output' => $this->output, 'error' => $this->error);
207 $this->setOutputHeaders();
208 $json = json_encode($output);
213 * Set output headers to send in response.
215 private function setOutputHeaders() {
216 $response = $this->getResponse();
217 $response->setContentType('application/json');
218 $response->appendHeader('Baculum-API-Version: ' . strval(self::API_SERVER_VERSION));
222 * Return action result which was realized in onInit() method.
223 * On standard output is printed JSON value with request results.
226 * @param mixed $params onInit action params
229 public function onLoad($params) {
230 parent::onLoad($params);
231 echo $this->getOutput();
235 * Changing/updating values via API.
240 private function put() {
241 $id = $this->Request->contains('id') ? $this->Request['id'] : null;
244 * Check if it is possible to read PUT method data.
245 * Note that some clients sends data in PUT request as PHP input stream which
246 * is not possible to read by $_REQUEST data. From this reason, when is
247 * not possible to ready by superglobal $_REQUEST variable, then is try to
248 * read PUT data by PHP input stream.
250 if ($this->Request->contains('update') && is_array($this->Request['update']) && count($this->Request['update']) > 0) {
251 // $_REQUEST available to read
252 $params = (object)$this->Request['update'];
253 $this->set($id, $params);
255 // no possibility to read data from $_REQUEST. Try to load from input stream.
256 $inputstr = file_get_contents("php://input");
259 * Read using chunks for case large updates (over 1000 values).
260 * Otherwise max_input_vars limitation in php.ini can be reached (usually
261 * set to 1000 variables)
262 * @see http://php.net/manual/en/info.configuration.php#ini.max-input-vars
264 $chunks = explode('&', $inputstr);
266 $response_data = array();
267 for($i = 0; $i<count($chunks); $i++) {
268 // if chunks would not be used, then here occurs reach max_input_vars limit
269 parse_str($chunks[$i], $response_el);
270 if (is_array($response_el) && array_key_exists('update', $response_el) && is_array($response_el['update'])) {
271 $key = key($response_el['update']);
272 $response_data['update'][$key] = $response_el['update'][$key];
275 if (is_array($response_data) && array_key_exists('update', $response_data)) {
276 $params = (object)$response_data['update'];
277 $this->set($id, $params);
280 * This case should never occur because it means that there is
281 * given nothing to update.
283 $params = new stdClass;
284 $this->set($id, $params);
290 * Creating new elements.
295 private function post() {
296 $params = new stdClass;
297 if ($this->Request->contains('create') && is_array($this->Request['create']) && count($this->Request['create']) > 0) {
298 $params = (object)$this->Request['create'];
300 $this->create($params);
304 * Deleting element by element ID.
309 private function delete() {
311 if ($this->Request->contains('id')) {
312 $id = intval($this->Request['id']);
318 * Check if request is allowed to access basing on OAuth2 scope.
321 * @param string scopes assigned with token
322 * @return bool true if scope in url and from token are valid, otherwise false
324 private function isScopeValid($scope) {
326 $scopes = explode(' ', $scope);
327 $url = $this->getRequest()->getUrl()->getPath();
328 $params = explode('/', $url);
329 if (count($params) >= 3 && $params[1] === 'api') {
330 if (in_array($params[2], $this->public_endpoints)) {
333 for ($i = 0; $i < count($scopes); $i++) {
334 if ($params[2] === $scopes[$i]) {
345 * Shortcut method for getting application modules instances by
349 * @param string $name application module name
350 * @return object module class instance
352 public function getModule($name) {
353 return $this->Application->getModule($name);
357 * Get Baculum web client version.
359 * @return float client version
361 public function getClientVersion() {
363 $headers = $this->getRequest()->getHeaders(CASE_LOWER);
364 if (array_key_exists('x-baculum-api', $headers)) {
365 $version = floatval($headers['x-baculum-api']);