diff --git a/api/cora/cora.php b/api/cora/cora.php index 986b4f1..9755f4d 100644 --- a/api/cora/cora.php +++ b/api/cora/cora.php @@ -271,6 +271,10 @@ final class cora { $session = session::get_instance(); $session_is_valid = $session->touch($this->api_user['session_key']); + // Make sure the updated session timestamp doesn't get rolled back on + // exception. + $this->database->commit_transaction(); + // Process each request. foreach($this->api_calls as $api_call) { // Store the currently running API call for tracking if an error occurs. @@ -291,7 +295,7 @@ final class cora { // If the request requires a session, make sure it's valid. if($call_type === 'private') { if($session_is_valid === false) { - throw new \Exception('Session is expired.', 1004); + throw new exception('Session is expired.', 1004, false); } } @@ -638,7 +642,8 @@ final class cora { $error_code, $error_file, $error_line, - debug_backtrace(false) + debug_backtrace(false), + true ); die(); // Do not continue execution; shutdown handler will now run. } @@ -655,7 +660,8 @@ final class cora { $e->getCode(), $e->getFile(), $e->getLine(), - $e->getTrace() + $e->getTrace(), + (method_exists($e, 'getReportable') === true ? $e->getReportable() : true) ); die(); // Do not continue execution; shutdown handler will now run. } @@ -671,7 +677,7 @@ final class cora { * @param int $error_line The line of the file the error happened on. * @param array $error_trace The stack trace for the error. */ - public function set_error_response($error_message, $error_code, $error_file, $error_line, $error_trace) { + public function set_error_response($error_message, $error_code, $error_file, $error_line, $error_trace, $reportable) { // There are a few places that call this function to set an error response, // so this can't just be done in the exception handler alone. If an error // occurs, rollback the current transaction. Also only attempt to roll back @@ -706,43 +712,45 @@ final class cora { // Send data to Sentry for error logging. // https://docs.sentry.io/development/sdk-dev/event-payloads/ - $data = [ - 'event_id' => str_replace('-', '', exec('uuidgen -r')), - 'timestamp' => date('c'), - 'logger' => 'cora', - 'platform' => 'php', - 'level' => 'error', - 'tags' => [ - 'error_code' => $error_code - ], - 'extra' => [ - 'api_user_id' => $api_user_id, - 'error_file' => $error_file, - 'error_line' => $error_line, - 'error_trace' => $error_trace - ], - 'exception' => [ - 'type' => 'Exception', - 'value' => $error_message, - 'handled' => false - ], - 'user' => [ - 'id' => $user_id, - 'ip_address' => $_SERVER['REMOTE_ADDR'] - ] - ]; + if ($reportable === true) { + $data = [ + 'event_id' => str_replace('-', '', exec('uuidgen -r')), + 'timestamp' => date('c'), + 'logger' => 'cora', + 'platform' => 'php', + 'level' => 'error', + 'tags' => [ + 'error_code' => $error_code + ], + 'extra' => [ + 'api_user_id' => $api_user_id, + 'error_file' => $error_file, + 'error_line' => $error_line, + 'error_trace' => $error_trace + ], + 'exception' => [ + 'type' => 'Exception', + 'value' => $error_message, + 'handled' => false + ], + 'user' => [ + 'id' => $user_id, + 'ip_address' => $_SERVER['REMOTE_ADDR'] + ] + ]; - exec( - 'curl ' . - '-H "Content-Type: application/json" ' . - '-H "X-Sentry-Auth: Sentry sentry_version=7, sentry_key=' . $this->setting->get('sentry_key') . '" ' . - '--silent ' . // silent; keeps logs out of stderr - '--show-error ' . // override silent on failure - '--max-time 10 ' . - '--connect-timeout 5 ' . - '--data \'' . json_encode($data) . '\' ' . - '"https://sentry.io/api/' . $this->setting->get('sentry_project_id') . '/store/" > /dev/null &' - ); + exec( + 'curl ' . + '-H "Content-Type: application/json" ' . + '-H "X-Sentry-Auth: Sentry sentry_version=7, sentry_key=' . $this->setting->get('sentry_key') . '" ' . + '--silent ' . // silent; keeps logs out of stderr + '--show-error ' . // override silent on failure + '--max-time 10 ' . + '--connect-timeout 5 ' . + '--data \'' . json_encode($data) . '\' ' . + '"https://sentry.io/api/' . $this->setting->get('sentry_project_id') . '/store/" > /dev/null &' + ); + } } /** @@ -779,7 +787,8 @@ final class cora { $error['type'], $error['file'], $error['line'], - debug_backtrace(false) + debug_backtrace(false), + true ); } @@ -824,7 +833,8 @@ final class cora { $e->getCode(), $e->getFile(), $e->getLine(), - $e->getTrace() + $e->getTrace(), + (method_exists($e, 'getReportable') === true ? $e->getReportable() : true) ); $this->set_default_headers(); $this->output_headers(); diff --git a/api/cora/database.php b/api/cora/database.php index aa2fd24..ba1fb29 100644 --- a/api/cora/database.php +++ b/api/cora/database.php @@ -695,18 +695,21 @@ final class database extends \mysqli { } /** - * Actually delete a row from a table by the primary key. + * Set deleted = 1 on the database row. * - * @param string $table The table to delete from. + * @param string $resource The table to delete from. * @param int $id The value of the primary key to delete. * * @return int The number of rows affected by the delete (could be 0). */ - public function delete($table, $id) { - $query = 'delete from ' . $this->escape_identifier($table) . - ' where ' . $this->escape_identifier($table . '_id') . ' = ' . - $this->escape($id); - $this->query($query); + public function delete($resource, $id) { + $table = $this->get_table($resource); + + $attributes = []; + $attributes[$table . '_id'] = $id; + $attributes['deleted'] = true; + + $this->update($resource, $attributes); return $this->affected_rows; } diff --git a/api/cora/exception.php b/api/cora/exception.php new file mode 100644 index 0000000..2250a10 --- /dev/null +++ b/api/cora/exception.php @@ -0,0 +1,25 @@ +reportable = $reportable; + return parent::__construct($message, $code, null); + } + + public function getReportable() { + return $this->reportable; + } +} diff --git a/api/cora/session.php b/api/cora/session.php index 9c8c695..8d15c08 100644 --- a/api/cora/session.php +++ b/api/cora/session.php @@ -225,13 +225,7 @@ final class session { $sessions = $database->read('cora\session', ['session_key' => $session_key]); if(count($sessions) === 1) { - $database->update( - 'cora\session', - [ - 'session_id' => $sessions[0]['session_id'], - 'deleted' => 1 - ] - ); + $database->delete('cora\session', $sessions[0]['session_id']); // Remove these if the current session got logged out. if($session_key === $this->session_key) { $this->session_key = null; diff --git a/api/ecobee.php b/api/ecobee.php index b597f7f..11cdcb9 100644 --- a/api/ecobee.php +++ b/api/ecobee.php @@ -187,7 +187,8 @@ class ecobee extends external_api { [] ); if(count($ecobee_tokens) !== 1) { - throw new Exception('No token for this user'); + $this->api('user', 'log_out', ['all' => true]); + throw new cora\exception('No ecobee access for this user.', 10501, false); } $ecobee_token = $ecobee_tokens[0]; } @@ -236,6 +237,15 @@ class ecobee extends external_api { throw new Exception($response['status']['message']); } } + else if (isset($response['status']) === true && $response['status']['code'] === 16) { + // Token has been deauthorized by user. You must re-request authorization. + if($this::$log_mysql !== 'all') { + $this->log_mysql($curl_response); + } + $this->api('ecobee_token', 'delete', $ecobee_token['ecobee_token_id']); + $this->api('user', 'log_out', ['all' => true]); + throw new cora\exception('Ecobee access was revoked by user.', 10500, false); + } else if (isset($response['status']) === true && $response['status']['code'] !== 0) { // Any other error if($this::$log_mysql !== 'all') { diff --git a/api/ecobee_token.php b/api/ecobee_token.php index dd61ed4..0f6797b 100644 --- a/api/ecobee_token.php +++ b/api/ecobee_token.php @@ -68,7 +68,8 @@ class ecobee_token extends cora\crud { $ecobee_tokens = $database->read( 'ecobee_token', [ - 'user_id' => $this->session->get_user_id() + 'user_id' => $this->session->get_user_id(), + 'deleted' => false ] ); if(count($ecobee_tokens) === 0) { @@ -94,6 +95,7 @@ class ecobee_token extends cora\crud { isset($response['refresh_token']) === false ) { $this->delete($ecobee_token['ecobee_token_id']); + $this->api('user', 'log_out', ['all' => true]); $database->release_lock($lock_name); throw new Exception('Could not refresh ecobee token; ecobee returned no token.', 10002); } @@ -125,10 +127,7 @@ class ecobee_token extends cora\crud { $database = cora\database::get_transactionless_instance(); // Need to delete the token before logging out or else the delete fails. - $return = $database->delete('ecobee_token', $id); - - // Log out - $this->api('user', 'log_out', ['all' => true]); + $return = $database->delete($this->resource, $id); return $return; } diff --git a/api/patreon_token.php b/api/patreon_token.php index 477097c..fb4079e 100644 --- a/api/patreon_token.php +++ b/api/patreon_token.php @@ -74,11 +74,11 @@ class patreon_token extends cora\crud { $lock_name = 'patreon_token->refresh(' . $this->session->get_user_id() . ')'; $database->get_lock($lock_name, 3); - // $patreon_tokens = $this->read(); $patreon_tokens = $database->read( 'patreon_token', [ - 'user_id' => $this->session->get_user_id() + 'user_id' => $this->session->get_user_id(), + 'deleted' => false ] ); if(count($patreon_tokens) === 0) { @@ -130,7 +130,7 @@ class patreon_token extends cora\crud { */ public function delete($id) { $database = cora\database::get_transactionless_instance(); - $return = $database->delete('patreon_token', $id); + $return = $database->delete($this->resource, $id); return $return; } diff --git a/api/user.php b/api/user.php index a6cdd19..cf4128d 100644 --- a/api/user.php +++ b/api/user.php @@ -144,7 +144,9 @@ class user extends cora\crud { } if($all === true) { - $database = cora\database::get_instance(); + // Sometimes I need to log out and then throw an exception. Using the + // transactionless instance makes sure that actually works. + $database = cora\database::get_transactionless_instance(); $sessions = $database->read( 'cora\session', [ @@ -154,7 +156,7 @@ class user extends cora\crud { ); $success = true; foreach($sessions as $session) { - $success &= $this->session->delete($session['session_key']); + $success &= $database->delete('cora\session', $session['session_id']); } return $success; } diff --git a/js/beestat/api.js b/js/beestat/api.js index 46eb389..c53fa0e 100644 --- a/js/beestat/api.js +++ b/js/beestat/api.js @@ -46,10 +46,6 @@ beestat.api.prototype.send = function(opt_api_call) { self.load_(this.responseText); }); - // var endpoint = (window.environment === 'live') - // ? 'https://api.beestat.io/' - // : 'http://' + window.environment + '.api.beestat.io/'; - // this.xhr_.open('POST', endpoint + '?' + query_string); this.xhr_.open('POST', 'api/?' + query_string); this.xhr_.send(); } else { @@ -172,10 +168,12 @@ beestat.api.prototype.load_ = function(response_text) { if ( response.data && ( - response.data.error_code === 1004 || // Session is expired. - response.data.error_code === 10001 || // Could not get first token. - response.data.error_code === 10002 || // Could not refresh ecobee token; no token found. - response.data.error_code === 10003 // Could not refresh ecobee token; ecobee returned no token. + response.data.error_code === 1004 || // Session is expired. + response.data.error_code === 10000 || // Could not get first token. + response.data.error_code === 10001 || // Could not refresh ecobee token; no token found. + response.data.error_code === 10002 || // Could not refresh ecobee token; ecobee returned no token. + response.data.error_code === 10500 || // Ecobee access was revoked by user. + response.data.error_code === 10501 // No ecobee access for this user. ) ) { window.location.href = '/';