Commit bca8c51e authored by Carsten  Rose's avatar Carsten Rose
Browse files

Start refactor to extend HTTP_STATUS Code in exception.

parent 10d77d42
......@@ -9,6 +9,9 @@
..
.. --------------------------------------------------
.. Best Practice T3 reST https://docs.typo3.org/typo3cms/drafts/github/xperseguers/RstPrimer/
.. Italic *italic*
.. Bold **bold**
.. Code ``text``
.. External Links: `Bootstrap <http://getbootstrap.com/>`_
.. Add Images: https://wiki.typo3.org/ReST_Syntax#Images ...
..
......@@ -7459,7 +7462,7 @@ To define a QFQ form becomes a REST form by enable one or more of:
Form: Access > Permit REST: get / insert / update / delete
Endpoint
^^^^^^^^
--------
.. tip::
......@@ -7498,7 +7501,7 @@ Only the last <level> of an URI will be processed. The former ones are just to f
GET - Read
^^^^^^^^^^
----------
A REST (GET) form has two modes: ::
......@@ -7524,39 +7527,69 @@ Form:
+-------------------+------------------------------------------------------------------------------+
| Attribute | Description |
+===================+==============================================================================+
| name=<level> | Level name in URI |
| name | *<level>* Mandatory. Level name (Endpoint) in URI. |
+-------------------+------------------------------------------------------------------------------+
| permitNew=rest | The form can be loaded in REST mode with mising parameter 'id' or 'id=0' |
| table | Mandatory. Name of the primary table |
+-------------------+------------------------------------------------------------------------------+
| permitEdit=rest | The form can be loaded in REST mode with parameter 'id' > 0 |
| Permit REST | *get* Mandatory. The form can be loaded in REST mode. |
+-------------------+------------------------------------------------------------------------------+
Form.parameter:
+-------------------+----------------------------------------------------------------------------------+
| Attribute | Description |
+===================+==================================================================================+
| restSqlData | Mandatory. SQL query selects content shown in data mode. |
| | | ``restSqlData={{!SELECT id, name, gender FROM Person WHERE id='{{r:T0}}'' }}`` |
+-------------------+----------------------------------------------------------------------------------+
| restSqlList | Mandatory. SQL query selects content shown in data mode. |
| | | ``restSqlData={{!SELECT id, name FROM Person }}`` |
+-------------------+----------------------------------------------------------------------------------+
| restParam | Optional. CSV list of variable names. E.g.: ``restParam=pId,adrId`` |
+-------------------+----------------------------------------------------------------------------------+
| restToken | Optional. User defined string or dynamic token (see below). |
+-------------------+----------------------------------------------------------------------------------+
.. note:
There are no `special-column-names`_ available in `restSqlData` or `restSqlList`. Also there are no
SIPs possible, cause REST typically does not offer sessions/cookies (which are necessary for SIPs).
POST - Insert
-------------
Form:
+-------------------+------------------------------------------------------------------------------+
| Attribute | Description |
+===================+==============================================================================+
| restSqlData | SQL query selects content shown in data mode. |
| | `restSqlData={{!SELECT id, name, gender FROM Person WHERE id='{{r:T0}}'' }}` |
+-------------------+------------------------------------------------------------------------------+
| restSqlList | SQL query selects content shown in data mode. |
| | `restSqlData={{!SELECT id, name FROM Person }}` |
| name | *<level>* Mandatory. Level name (Endpoint) in URI. |
+-------------------+------------------------------------------------------------------------------+
| restParam | Optional. CSV list of variable names. E.g.: `restParam=pId,adrId` |
| table | Mandatory. Name of the primary table |
+-------------------+------------------------------------------------------------------------------+
| restToken | Optional. User defined string. For dynamic token see below. |
| Permit REST | *insert* Mandatory. The form can be loaded in REST mode. |
+-------------------+------------------------------------------------------------------------------+
| id | Missing or '0'.
.. note:
PUT - Update
------------
DELETE - Delete
---------------
There are no `special-column-names`_ available in `restSqlData` or `restSqlList`. Also there are no
SIPs possible, cause REST typically does not offer sessions/cookies (which are necessary for SIPs).
Authorization
-------------
By default, the REST API is public accessible.
A QFQ form is only acessible via REST API, if ``Form.permitREST`` enables the HTTP Method (get, post, put, delete)
``Permit New`` or ``Permit Edit`` don't apply to QFQ forms called via REST.
.. important::
By default, the REST API is public accessible.
If this is not wished, HTTP AUTH might be used (configured via webserver) or the
QFQ internal 'HTTP header token based authorization'.
......@@ -7564,10 +7597,9 @@ QFQ internal 'HTTP header token based authorization'.
Token based authorization
^^^^^^^^^^^^^^^^^^^^^^^^^
A form will require a 'token based authorization', as soon as there is a `form.parameter.restToken` defined.
Therefore the HTTP Header 'Authorization' has to be set with `token=<secret token>`. The 'secret token' will
be checked against the server. Using HTTPS, such token can't be sniffed and will typically not be logged in
any server logs.
A form will require a 'token based authorization', as soon as there is a ``form.parameter.restToken`` defined.
Therefore the HTTP Header 'Authorization' has to be set with ``token=<secret token>``. The 'secret token' will
be checked against the server.
Example: ::
......@@ -7578,9 +7610,11 @@ Example: ::
The static setup with `form.parameter.restToken=myCrypticString0123456789 is fine, as long as only one token
exist. In case of multiple tokens, replace the static string against a SQL query.
General: The HTML Header Authorization token is available in STORE_CLIENT via '`{{Authorization:C:alnumx}}`.
.. tip::
The HTML Header Authorization token is available in STORE_CLIENT via '`{{Authorization:C:alnumx}}`.
For example all created tokens are saved in a table 'Auth' with a column 'token'. Define: ::
Best Practice: For example all created tokens are saved in a table 'Auth' with a column 'token'. Define::
form.parameter.restToken={{SELECT a.token FROM Auth AS a WHERE a.token='{{Authorization:C:alnumx}}' }}
......
......@@ -19,6 +19,7 @@ $restId = array();
$restForm = array();
$status = 'HTTP/1.0 409 Bad Request';
$data = array();
try {
try {
......@@ -39,13 +40,13 @@ try {
if ($id != 0) {
throw new UserFormException('Method POST needs no id or id=0', ERROR_REST_INVALID_ID);
}
$_POST = json_decode(file_get_contents('php://input'), true);
$data = json_decode(file_get_contents('php://input'), true);
break;
case REQUEST_METHOD_PUT:
if ($id == 0) {
throw new UserFormException('Method PUT needs an id>0', ERROR_REST_INVALID_ID);
}
$_POST = json_decode(file_get_contents('php://input'), true);
$data = json_decode(file_get_contents('php://input'), true);
break;
case REQUEST_METHOD_DELETE:
if ($id == 0) {
......@@ -56,20 +57,33 @@ try {
break;
}
if ($data === null) {
throw new NotAcceptableResponseException(
json_encode([ERROR_MESSAGE_TO_USER => 'Invalid JSON',
ERROR_MESSAGE_SUPPORT => json_last_error_msg()]), ERROR_INVALID_VALUE);
}
if(!empty($data)){
$_POST = $data;
}
$qfq = new QuickFormQuery(['bodytext' => $bodytext]);
$answer = $qfq->rest($restId, $restForm);
$status = 'HTTP/1.0 200 OK';
} catch (qfq\CodeException $e) {
$answer[API_MESSAGE] = $e->formatMessage();
$status=$e->getHttpStatus();
} catch (qfq\UserFormException $e) {
$answer[API_MESSAGE] = $e->formatMessage();
$status=$e->getHttpStatus();
} catch (qfq\DbException $e) {
$answer[API_MESSAGE] = $e->formatMessage();
$status=$e->getHttpStatus();
}
} catch (\Exception $e) {
$answer[API_MESSAGE] = "Generic Exception: " . $e->getMessage();
}
......
......@@ -141,6 +141,7 @@ const KVP_VALUE_GIVEN = 'value_given';
const ERROR_MESSAGE_TO_USER = 'toUser'; // always shown to the user.
const ERROR_MESSAGE_SUPPORT = 'support'; // Message to help the developer to understand the problem.
const ERROR_MESSAGE_OS = 'os'; // Error message from the OS - like 'file not found' or specific SQL problem
const ERROR_MESSAGE_HTTP_STATUS = 'httpStatus'; // HTTP Status Code to report
// QFQ Error Codes
const ERROR_UNKNOW_SANITIZE_CLASS = 1001;
......@@ -1762,3 +1763,5 @@ const ATTRIBUTE_DATA_REFERENCE = 'data-reference';
// REST
const HTTP_HEADER_AUTHORIZATION = 'Authorization';
const HTTP_401 = '401 Unauthorized';
const HTTP_403 = '403 Forbidden';
\ No newline at end of file
......@@ -546,7 +546,7 @@ class QuickFormQuery {
if ($formMode == FORM_REST) {
$data = ['id' => $rc];
$flagApiStructureReGroup=false;
$flagApiStructureReGroup = false;
break;
}
......@@ -588,7 +588,7 @@ class QuickFormQuery {
break;
case FORM_REST:
$flagApiStructureReGroup=false;
$flagApiStructureReGroup = false;
$data = $this->doRestGet();
break;
......@@ -596,7 +596,7 @@ class QuickFormQuery {
throw new CodeException("This statement should never be reached", ERROR_CODE_SHOULD_NOT_HAPPEN);
}
if ($flagApiStructureReGroup && is_array($data) ) {
if ($flagApiStructureReGroup && is_array($data)) {
// $data['element-update']=...
$data = $this->groupElementUpdateEntries($data);
}
......@@ -632,6 +632,7 @@ class QuickFormQuery {
* If not: throw an exception.
*
* @param string|array $serverToken
* @throws RestException
* @throws CodeException
* @throws UserFormException
*/
......@@ -642,7 +643,8 @@ class QuickFormQuery {
return;
}
if ($serverToken === $this->store::getVar(HTTP_HEADER_AUTHORIZATION, STORE_CLIENT . STORE_EMPTY, SANITIZE_ALLOW_ALL)) {
$clientToken = $this->store::getVar(HTTP_HEADER_AUTHORIZATION, STORE_CLIENT, SANITIZE_ALLOW_ALL);
if ($serverToken === $clientToken) {
return;
}
......@@ -650,7 +652,17 @@ class QuickFormQuery {
$seconds = $this->store::getVar(SYSTEM_SECURITY_FAILED_AUTH_DELAY, STORE_SYSTEM);
sleep($seconds);
throw new UserFormException('Missing or wrong authorization token', ERROR_REST_AUTHORIZATION);
if ($clientToken == false) {
throw new RestException(json_encode([ERROR_MESSAGE_TO_USER => 'Missing authorization token',
ERROR_MESSAGE_SUPPORT => "Missing HTTP Header: " . HTTP_HEADER_AUTHORIZATION,
ERROR_MESSAGE_HTTP_STATUS => HTTP_401
]), ERROR_REST_AUTHORIZATION);
}
throw new RestException(json_encode([ERROR_MESSAGE_TO_USER => 'Authorization token not accepted',
ERROR_MESSAGE_SUPPORT => "Missing HTTP Header: " . HTTP_HEADER_AUTHORIZATION,
ERROR_MESSAGE_HTTP_STATUS => HTTP_401
]), ERROR_REST_AUTHORIZATION);
}
/**
......@@ -1423,9 +1435,9 @@ class QuickFormQuery {
* @param string $formMode
*
* @return bool 'true' if SIP exists, else 'false'
* @throws \qfq\CodeException
* @throws \qfq\UserFormException
* @internal param $foundInStore
* @throws RestException
* @throws CodeException
* @throws UserFormException
*/
private function validateForm($formNameFoundInStore, $formMode, &$formModeNew) {
......@@ -1458,7 +1470,7 @@ class QuickFormQuery {
$method = $this->store::getVar(CLIENT_REQUEST_METHOD, STORE_CLIENT);
if (false === Support::findInSet(strtolower($method), $this->formSpec[F_REST_METHOD])) {
throw new UserFormException("Form '" . $this->formSpec[F_NAME] . "' is not allowed with method '$method'", ERROR_FORM_REST);
throw new RestException("Endpoint '" . $this->formSpec[F_NAME] . "' is not allowed with method '$method'", ERROR_FORM_REST);
}
$this->restCheckAuthToken($this->formSpec[F_REST_TOKEN] ?? '');
......@@ -1467,6 +1479,11 @@ class QuickFormQuery {
case REQUEST_METHOD_GET:
break;
case REQUEST_METHOD_POST:
if ($r != 0) {
throw new RestException('Mode GET with id>0 is forbidden', ERROR_UNKNOWN_MODE);
}
$formModeNew = FORM_SAVE;
break;
case REQUEST_METHOD_PUT:
$formModeNew = FORM_SAVE;
break;
......@@ -1474,7 +1491,7 @@ class QuickFormQuery {
$formModeNew = FORM_DELETE;
break;
default:
throw new CodeException('Unknown Request Method: ' . $method, ERROR_UNKNOWN_MODE);
throw new RestException('Unknown Request Method: ' . $method, ERROR_UNKNOWN_MODE);
}
} else {
......
......@@ -29,10 +29,11 @@ require_once(__DIR__ . '/../helper/Support.php');
*
* Throw with message for User and message for Support.
*
* throw new UserFormException( json_encode(
* [ERROR_MESSAGE_TO_USER => 'Failed: chmod',
* ERROR_MESSAGE_SUPPORT => "Failed: chmod $mode '$pathFileName'"]),
* ERROR_IO_CHMOD);
throw new UserFormException( json_encode(
[ERROR_MESSAGE_TO_USER => 'Failed: chmod',
ERROR_MESSAGE_SUPPORT => "Failed: chmod $mode '$pathFileName'",
ERROR_MESSAGE_HTTP_STATUS => 'HTTP/1.0 409 Bad Request' ]),
ERROR_IO_CHMOD);
*
* @package qfq
*/
......@@ -46,6 +47,8 @@ class AbstractException extends \Exception {
protected $file = '';
protected $line = '';
protected $httpStatusCode = '400 Bad Request';
/**
* $this->getMessage() might give
* a) a simple string, or
......@@ -92,10 +95,17 @@ class AbstractException extends \Exception {
$msg = $this->getMessage();
$arrMsg = json_decode($msg, true);
if ($arrMsg === null) {
$arrShow[EXCEPTION_MESSAGE] = $msg;
$arrMsg[ERROR_MESSAGE_TO_USER] = $msg;
} else {
$arrShow[EXCEPTION_MESSAGE] = $arrMsg[ERROR_MESSAGE_TO_USER];
if (isset($arrMsg[ERROR_MESSAGE_HTTP_STATUS])) {
$this->httpStatusCode = $arrMsg[ERROR_MESSAGE_HTTP_STATUS];
}
}
$arrDebugHidden[EXCEPTION_FILE] = $this->getFile();
......@@ -137,7 +147,7 @@ class AbstractException extends \Exception {
if (!empty($os = $arrMerged[ERROR_MESSAGE_OS] ?? '')) {
// [ mysqli: 1146 ] Table 'qfq_db.UNKNOWN_TABLE' doesn't exist
$before=$this->getTableToken( html_entity_decode($arrMerged[ERROR_MESSAGE_OS],ENT_QUOTES));
$before = $this->getTableToken(html_entity_decode($arrMerged[ERROR_MESSAGE_OS], ENT_QUOTES));
$arrMerged[EXCEPTION_SQL_FINAL] = $this->sqlHighlightError($arrMerged[ERROR_MESSAGE_OS], 'mysqli: 1146', $arrMerged[EXCEPTION_SQL_FINAL], $before, "' doesn't exist");
$arrMerged[EXCEPTION_SQL_FINAL] = $this->sqlHighlightError($arrMerged[ERROR_MESSAGE_OS], 'mysqli: 1064', $arrMerged[EXCEPTION_SQL_FINAL], "the right syntax to use near '", "' at line [0-9]*$");
// [ mysqli: 1054 ] Unknown column "noPsp.pspElement' in 'field list" | "... in 'order clause'"
......@@ -173,6 +183,13 @@ class AbstractException extends \Exception {
}
/**
* @return string
*/
public function getHttpStatus() {
return $this->$this->httpStatusCode;
}
/**
* Extract 'beforeMatch', incl. dynamic db name as token to do underlining later.
* E.g.: "[ mysqli: 1146 ] Table 'qfq_db.UNKNOWN_TABLE' doesn't exist"
......@@ -184,8 +201,8 @@ class AbstractException extends \Exception {
private function getTableToken($os) {
$subject = "Table '.*' ";
$arr = preg_match("/$subject/", $os, $matches);
$arr= explode('.', $matches[0]??'');
return ($arr[0]??'') . '.';
$arr = explode('.', $matches[0] ?? '');
return ($arr[0] ?? '') . '.';
}
/**
......
......@@ -21,10 +21,10 @@ require_once(__DIR__ . '/AbstractException.php');
*
* Throw with message for User and message for Support.
*
* throw new UserFormException( json_encode(
* [ERROR_MESSAGE_TO_USER => 'Failed: chmod',
* ERROR_MESSAGE_SUPPORT => "Failed: chmod $mode '$pathFileName'"]),
* ERROR_IO_CHMOD);
throw new UserFormException( json_encode(
[ERROR_MESSAGE_TO_USER => 'Failed: chmod',
ERROR_MESSAGE_SUPPORT => "Failed: chmod $mode '$pathFileName'"]),
ERROR_IO_CHMOD);
*
* @package qfq\exceptions
*/
......
......@@ -38,6 +38,13 @@ class DbException extends AbstractException {
* [ERROR_MESSAGE_TO_USER] 'toUser' - shown in the client to the user - no details here!!!
* [ERROR_MESSAGE_SUPPORT] 'support' - help for the developer
* [ERROR_MESSAGE_OS] 'os' - message from the OS, like 'file not found'
*
throw new UserFormException( json_encode(
[ERROR_MESSAGE_TO_USER => 'Failed: chmod',
ERROR_MESSAGE_SUPPORT => "Failed: chmod $mode '$pathFileName'",
ERROR_MESSAGE_OS => 'os' - message from the OS, like 'file not found'
ERROR_MESSAGE_HTTP_STATUS => 'HTTP/1.0 409 Bad Request' ]),
ERROR_IO_CHMOD);
*
* @return string HTML formatted error string
* @return string
......
......@@ -21,10 +21,11 @@ require_once(__DIR__ . '/AbstractException.php');
*
* Throw with message for User and message for Support.
*
* throw new UserFormException( json_encode(
* [ERROR_MESSAGE_TO_USER => 'Failed: chmod',
* ERROR_MESSAGE_SUPPORT => "Failed: chmod $mode '$pathFileName'"]),
* ERROR_IO_CHMOD);
throw new UserFormException( json_encode(
[ERROR_MESSAGE_TO_USER => 'Failed: chmod',
ERROR_MESSAGE_SUPPORT => "Failed: chmod $mode '$pathFileName'",
ERROR_MESSAGE_HTTP_STATUS => 'HTTP/1.0 409 Bad Request' ]),
ERROR_IO_CHMOD);
*
* @package qfq\exceptions
*/
......
<?php
/**
* Created by PhpStorm.
* User: crose
* Date: 21.02.19
* Time: 23:47
*/
namespace qfq;
require_once(__DIR__ . '/AbstractException.php');
/**
* Class NotAcceptableResponseException
*
* Thrown by API call with bad content type
*
* Throw with ONE message
*
* throw new UserFormException('Failed: chmod ....', ERROR_IO_CHMOD);
*
* Throw with message for User and message for Support.
*
* throw new UserFormException( json_encode(
* [ERROR_MESSAGE_TO_USER => 'Failed: chmod',
* ERROR_MESSAGE_SUPPORT => "Failed: chmod $mode '$pathFileName'"]),
* ERROR_IO_CHMOD);
*
* @package qfq\exceptions
*/
class NotAcceptableResponseException extends AbstractException {
/**
* $this->getMessage() might give a) a simple string or b) an JSON String.
*
* JSON String: There are 3+1 different messages:
* [ERROR_MESSAGE_TO_USER] 'toUser' - shown in the client to the user - no details here!!!
* [ERROR_MESSAGE_SUPPORT] 'support' - help for the developer
* [ERROR_MESSAGE_OS] 'os' - message from the OS, like 'file not found'
*
* @return string HTML formatted error string
* @throws CodeException
* @throws UserFormException
*/
public function formatMessage() {
$this->messageArrayDebug[EXCEPTION_TYPE] = 'Not Acceptable Response';
return parent::formatException();
}
}
\ No newline at end of file
<?php
/**
* Created by PhpStorm.
* User: crose
* Date: 21.02.19
* Time: 23:47
*/
namespace qfq;
require_once(__DIR__ . '/AbstractException.php');
/**
* Class RestException
*
* Thrown by API call with violated access rules
*
* Throw with ONE message
*
* throw new UserFormException('Failed: chmod ....', ERROR_IO_CHMOD);
*
* Throw with message for User and message for Support.
*
* throw new UserFormException( json_encode(
* [ERROR_MESSAGE_TO_USER => 'Failed: chmod',
* ERROR_MESSAGE_SUPPORT => "Failed: chmod $mode '$pathFileName'"]),
* ERROR_IO_CHMOD);
*
* @package qfq\exceptions
*/
class RestException extends AbstractException {
/**
* $this->getMessage() might give a) a simple string or b) an JSON String.
*
* JSON String: There are 3+1 different messages:
* [ERROR_MESSAGE_TO_USER] 'toUser' - shown in the client to the user - no details here!!!
* [ERROR_MESSAGE_SUPPORT] 'support' - help for the developer
* [ERROR_MESSAGE_OS] 'os' - message from the OS, like 'file not found'
*
* @return string HTML formatted error string
* @throws CodeException
* @throws UserFormException
*/
public function formatMessage() {
$this->messageArrayDebug[EXCEPTION_TYPE] = 'Access Forbidden Exception';
return parent::formatException();
}
}
\ No newline at end of file
......@@ -21,10 +21,11 @@ require_once(__DIR__ . '/AbstractException.php');
*
* Throw with message for User and message for Support.
*
* throw new UserFormException( json_encode(
* [ERROR_MESSAGE_TO_USER => 'Failed: chmod',
* ERROR_MESSAGE_SUPPORT => "Failed: chmod $mode '$pathFileName'"]),
* ERROR_IO_CHMOD);
throw new UserFormException( json_encode(
[ERROR_MESSAGE_TO_USER => 'Failed: chmod',
ERROR_MESSAGE_SUPPORT => "Failed: chmod $mode '$pathFileName'",
ERROR_MESSAGE_HTTP_STATUS => 'HTTP/1.0 409 Bad Request' ]),
ERROR_IO_CHMOD);
*
* @package qfq\exceptions
*/
......
......@@ -22,10 +22,11 @@ require_once(__DIR__ . '/AbstractException.php');
*
* Throw with message for User and message for Support.
*
* throw new UserFormException( json_encode(
* [ERROR_MESSAGE_TO_USER => 'Failed: chmod',
* ERROR_MESSAGE_SUPPORT => "Failed: chmod $mode '$pathFileName'"]),
* ERROR_IO_CHMOD);
throw new UserFormException( json_encode(
[ERROR_MESSAGE_TO_USER => 'Failed: chmod',
ERROR_MESSAGE_SUPPORT => "Failed: chmod $mode '$pathFileName'",
ERROR_MESSAGE_HTTP_STATUS => 'HTTP/1.0 409 Bad Request' ]),
ERROR_IO_CHMOD);
*
* Call
*
......
......@@ -21,10 +21,11 @@ require_once(__DIR__ . '/AbstractException.php');
*
* Throw with message for User and message for Support.
*
* throw new UserFormException( json_encode(
* [ERROR_MESSAGE_TO_USER => 'Failed: chmod',
* ERROR_MESSAGE_SUPPORT => "Failed: chmod $mode '$pathFileName'"]),
* ERROR_IO_CHMOD);
throw new UserFormException( json_encode(
[ERROR_MESSAGE_TO_USER => 'Failed: chmod',
ERROR_MESSAGE_SUPPORT => "Failed: chmod $mode '$pathFileName'",
ERROR_MESSAGE_HTTP_STATUS => 'HTTP/1.0 409 Bad Request' ]),
ERROR_IO_CHMOD);
*
* @package qfq\exceptions
*/
......
......@@ -40,6 +40,7 @@ class Logger {
*
* @param string $mode