Port search API search more than one fields (#14646)
* Fix port search columns * Port search API search more than one fields Fixup port APIs Change validate_column_list api helper to throw a renderable exception on error and return the valid columns DeviceCache::get() can handle a bigger range of input * whitespace * Refactor exceptions a bit * change throws type to be more generic * Lint fixes
This commit is contained in:
parent
e851c9abd4
commit
752bbc1531
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
/**
|
||||
* ApiException.php
|
||||
*
|
||||
* -Description-
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @link https://www.librenms.org
|
||||
*
|
||||
* @copyright 2019 Tony Murray
|
||||
* @author Tony Murray <murraytony@gmail.com>
|
||||
*/
|
||||
|
||||
namespace LibreNMS\Exceptions;
|
||||
|
||||
class ApiClientException extends \Exception
|
||||
{
|
||||
/** @var array */
|
||||
private $output;
|
||||
|
||||
/**
|
||||
* @param string $message
|
||||
* @param array $output
|
||||
*/
|
||||
public function __construct($message = '', $output = [])
|
||||
{
|
||||
parent::__construct($message, 0, null);
|
||||
$this->output = $output;
|
||||
}
|
||||
|
||||
public function getOutput(): array
|
||||
{
|
||||
return $this->output;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
<?php
|
||||
/**
|
||||
/*
|
||||
* ApiException.php
|
||||
*
|
||||
* -Description-
|
||||
|
@ -15,28 +15,40 @@
|
|||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @link https://www.librenms.org
|
||||
*
|
||||
* @copyright 2019 Tony Murray
|
||||
* @package LibreNMS
|
||||
* @link http://librenms.org
|
||||
* @copyright 2022 Tony Murray
|
||||
* @author Tony Murray <murraytony@gmail.com>
|
||||
*/
|
||||
|
||||
namespace LibreNMS\Exceptions;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class ApiException extends \Exception
|
||||
{
|
||||
private $output;
|
||||
|
||||
public function __construct($message = '', $output = [])
|
||||
/**
|
||||
* @param string $message
|
||||
* @param int $code
|
||||
* @param \Throwable|null $previous
|
||||
*/
|
||||
public function __construct($message = '', $code = 400, $previous = null)
|
||||
{
|
||||
parent::__construct($message, 0, null);
|
||||
$this->output = $output;
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
|
||||
public function getOutput()
|
||||
/**
|
||||
* Render the exception into an HTTP response.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
*/
|
||||
public function render($request): JsonResponse
|
||||
{
|
||||
return $this->output;
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => $this->getMessage(),
|
||||
], $this->getCode(), [], JSON_PRETTY_PRINT);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
/*
|
||||
* InvalidTableColumnException.php
|
||||
*
|
||||
* -Description-
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @package LibreNMS
|
||||
* @link http://librenms.org
|
||||
* @copyright 2022 Tony Murray
|
||||
* @author Tony Murray <murraytony@gmail.com>
|
||||
*/
|
||||
|
||||
namespace LibreNMS\Exceptions;
|
||||
|
||||
class InvalidTableColumnException extends ApiException
|
||||
{
|
||||
public function __construct(
|
||||
public readonly array $columns
|
||||
) {
|
||||
parent::__construct('Invalid columns: ' . join(',', $this->columns));
|
||||
}
|
||||
}
|
|
@ -26,7 +26,7 @@
|
|||
namespace App\ApiClients;
|
||||
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
use LibreNMS\Exceptions\ApiException;
|
||||
use LibreNMS\Exceptions\ApiClientException;
|
||||
|
||||
class RipeApi extends BaseApi
|
||||
{
|
||||
|
@ -38,7 +38,7 @@ class RipeApi extends BaseApi
|
|||
/**
|
||||
* Get whois info
|
||||
*
|
||||
* @throws ApiException
|
||||
* @throws ApiClientException
|
||||
*/
|
||||
public function getWhois(string $resource): array
|
||||
{
|
||||
|
@ -52,7 +52,7 @@ class RipeApi extends BaseApi
|
|||
/**
|
||||
* Get Abuse contact
|
||||
*
|
||||
* @throws ApiException
|
||||
* @throws ApiClientException
|
||||
*/
|
||||
public function getAbuseContact(string $resource): mixed
|
||||
{
|
||||
|
@ -64,7 +64,7 @@ class RipeApi extends BaseApi
|
|||
}
|
||||
|
||||
/**
|
||||
* @throws ApiException
|
||||
* @throws ApiClientException
|
||||
*/
|
||||
private function makeApiCall(string $uri, array $options): mixed
|
||||
{
|
||||
|
@ -73,13 +73,13 @@ class RipeApi extends BaseApi
|
|||
if (isset($response_data['status']) && $response_data['status'] == 'ok') {
|
||||
return $response_data;
|
||||
} else {
|
||||
throw new ApiException('RIPE API call failed', $response_data);
|
||||
throw new ApiClientException('RIPE API call failed', $response_data);
|
||||
}
|
||||
} catch (RequestException $e) {
|
||||
$message = 'RIPE API call to ' . $e->getRequest()->getUri() . ' failed: ';
|
||||
$message .= $e->getResponse()->getReasonPhrase() . ' ' . $e->getResponse()->getStatusCode();
|
||||
|
||||
throw new ApiException(
|
||||
throw new ApiClientException(
|
||||
$message,
|
||||
json_decode($e->getResponse()->getBody(), true)
|
||||
);
|
||||
|
|
|
@ -28,7 +28,7 @@ namespace App\Http\Controllers\Ajax;
|
|||
use App\ApiClients\RipeApi;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use LibreNMS\Exceptions\ApiException;
|
||||
use LibreNMS\Exceptions\ApiClientException;
|
||||
|
||||
class RipeNccApiController extends Controller
|
||||
{
|
||||
|
@ -50,7 +50,7 @@ class RipeNccApiController extends Controller
|
|||
'message' => 'Queried',
|
||||
'output' => $output,
|
||||
]);
|
||||
} catch (ApiException $e) {
|
||||
} catch (ApiClientException $e) {
|
||||
$response = $e->getOutput();
|
||||
$message = $e->getMessage();
|
||||
|
||||
|
|
|
@ -87,13 +87,14 @@ Output:
|
|||
}
|
||||
```
|
||||
|
||||
### `search_ports in specific column`
|
||||
### `search_ports in specific field(s)`
|
||||
|
||||
Specific search for ports matching the query.
|
||||
|
||||
Route: `/api/v0/ports/search/:field/:search`
|
||||
|
||||
- search string to search in field specified by field
|
||||
- field: comma separated list of field(s) to search
|
||||
- search: string to search in fields
|
||||
|
||||
Input:
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ use App\Models\Sensor;
|
|||
use App\Models\ServiceTemplate;
|
||||
use App\Models\UserPref;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Router;
|
||||
use Illuminate\Support\Arr;
|
||||
|
@ -35,13 +36,14 @@ use Illuminate\Support\Str;
|
|||
use LibreNMS\Alerting\QueryBuilderParser;
|
||||
use LibreNMS\Config;
|
||||
use LibreNMS\Exceptions\InvalidIpException;
|
||||
use LibreNMS\Exceptions\InvalidTableColumnException;
|
||||
use LibreNMS\Util\Graph;
|
||||
use LibreNMS\Util\IP;
|
||||
use LibreNMS\Util\IPv4;
|
||||
use LibreNMS\Util\Number;
|
||||
use LibreNMS\Util\Rewrite;
|
||||
|
||||
function api_success($result, $result_name, $message = null, $code = 200, $count = null, $extra = null)
|
||||
function api_success($result, $result_name, $message = null, $code = 200, $count = null, $extra = null): JsonResponse
|
||||
{
|
||||
if (isset($result) && ! isset($result_name)) {
|
||||
return api_error(500, 'Result name not specified');
|
||||
|
@ -68,12 +70,12 @@ function api_success($result, $result_name, $message = null, $code = 200, $count
|
|||
return response()->json($output, $code, [], JSON_PRETTY_PRINT);
|
||||
} // end api_success()
|
||||
|
||||
function api_success_noresult($code, $message = null)
|
||||
function api_success_noresult($code, $message = null): JsonResponse
|
||||
{
|
||||
return api_success(null, null, $message, $code);
|
||||
} // end api_success_noresult
|
||||
|
||||
function api_error($statusCode, $message)
|
||||
function api_error($statusCode, $message): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
|
@ -81,7 +83,7 @@ function api_error($statusCode, $message)
|
|||
], $statusCode, [], JSON_PRETTY_PRINT);
|
||||
} // end api_error()
|
||||
|
||||
function api_not_found()
|
||||
function api_not_found(): JsonResponse
|
||||
{
|
||||
return api_error(404, "This API route doesn't exist.");
|
||||
}
|
||||
|
@ -970,25 +972,16 @@ function list_available_wireless_graphs(Illuminate\Http\Request $request)
|
|||
});
|
||||
}
|
||||
|
||||
function get_port_graphs(Illuminate\Http\Request $request)
|
||||
/**
|
||||
* @throws \LibreNMS\Exceptions\ApiException
|
||||
*/
|
||||
function get_port_graphs(Illuminate\Http\Request $request): JsonResponse
|
||||
{
|
||||
$hostname = $request->route('hostname');
|
||||
$columns = $request->get('columns', 'ifName');
|
||||
$device = DeviceCache::get($request->route('hostname'));
|
||||
$columns = validate_column_list($request->get('columns'), 'ports', ['ifName']);
|
||||
|
||||
if (($validate = validate_column_list($columns, 'ports')) !== true) {
|
||||
return $validate;
|
||||
}
|
||||
|
||||
// use hostname as device_id if it's all digits
|
||||
$device_id = ctype_digit($hostname) ? $hostname : getidbyname($hostname);
|
||||
$sql = '';
|
||||
$params = [$device_id];
|
||||
if (! device_permitted($device_id)) {
|
||||
$sql = 'AND `port_id` IN (select `port_id` from `ports_perms` where `user_id` = ?)';
|
||||
array_push($params, Auth::id());
|
||||
}
|
||||
|
||||
$ports = dbFetchRows("SELECT $columns FROM `ports` WHERE `device_id` = ? AND `deleted` = '0' $sql ORDER BY `ifIndex`", $params);
|
||||
$ports = $device->ports()->isNotDeleted()->hasAccess(Auth::user())
|
||||
->select($columns)->orderBy('ifIndex')->get();
|
||||
|
||||
return api_success($ports, 'ports');
|
||||
}
|
||||
|
@ -1052,29 +1045,31 @@ function get_port_info(Illuminate\Http\Request $request)
|
|||
});
|
||||
}
|
||||
|
||||
function search_ports(Illuminate\Http\Request $request)
|
||||
/**
|
||||
* @throws \LibreNMS\Exceptions\ApiException
|
||||
*/
|
||||
function search_ports(Illuminate\Http\Request $request): JsonResponse
|
||||
{
|
||||
$columns = validate_column_list($request->get('columns'), 'ports', ['device_id', 'port_id', 'ifIndex', 'ifName']);
|
||||
$field = $request->route('field');
|
||||
$search = $request->route('search');
|
||||
$columns = $request->get('columns');
|
||||
if (($validate = validate_column_list($columns, 'ports')) !== true) {
|
||||
return $validate;
|
||||
|
||||
// if only field is set, swap values
|
||||
if (empty($search)) {
|
||||
[$field, $search] = [$search, $field];
|
||||
}
|
||||
$fields = validate_column_list($field, 'ports', ['ifAlias', 'ifDescr', 'ifName']);
|
||||
|
||||
$query = Port::hasAccess(Auth::user())
|
||||
->select(['device_id', 'port_id', 'ifIndex', 'ifName', $columns]);
|
||||
|
||||
if (isset($search)) {
|
||||
$query->where($field, 'like', "%$search%");
|
||||
} else {
|
||||
$value = "%$field%";
|
||||
$query->where('ifAlias', 'like', $value)
|
||||
->orWhere('ifDescr', 'like', $value)
|
||||
->orWhere('ifName', 'like', $value);
|
||||
}
|
||||
|
||||
$ports = $query->orderBy('ifName')
|
||||
->get();
|
||||
$ports = Port::hasAccess(Auth::user())
|
||||
->isNotDeleted()
|
||||
->where(function ($query) use ($fields, $search) {
|
||||
foreach ($fields as $field) {
|
||||
$query->orWhere($field, 'like', "%$search%");
|
||||
}
|
||||
})
|
||||
->select($columns)
|
||||
->orderBy('ifName')
|
||||
->get();
|
||||
|
||||
if ($ports->isEmpty()) {
|
||||
return api_error(404, 'No ports found');
|
||||
|
@ -1083,21 +1078,17 @@ function search_ports(Illuminate\Http\Request $request)
|
|||
return api_success($ports, 'ports');
|
||||
}
|
||||
|
||||
function get_all_ports(Illuminate\Http\Request $request)
|
||||
/**
|
||||
* @throws \LibreNMS\Exceptions\ApiException
|
||||
*/
|
||||
function get_all_ports(Illuminate\Http\Request $request): JsonResponse
|
||||
{
|
||||
$columns = $request->get('columns', 'port_id, ifName');
|
||||
if (($validate = validate_column_list($columns, 'ports')) !== true) {
|
||||
return $validate;
|
||||
}
|
||||
$columns = validate_column_list($request->get('columns'), 'ports', ['port_id', 'ifName']);
|
||||
|
||||
$params = [];
|
||||
$sql = '';
|
||||
if (! Auth::user()->hasGlobalRead()) {
|
||||
$sql = ' AND (device_id IN (SELECT device_id FROM devices_perms WHERE user_id = ?) OR port_id IN (SELECT port_id FROM ports_perms WHERE user_id = ?))';
|
||||
array_push($params, Auth::id());
|
||||
array_push($params, Auth::id());
|
||||
}
|
||||
$ports = dbFetchRows("SELECT $columns FROM `ports` WHERE `deleted` = 0 $sql", $params);
|
||||
$ports = Port::hasAccess(Auth::user())
|
||||
->select($columns)
|
||||
->isNotDeleted()
|
||||
->get();
|
||||
|
||||
return api_success($ports, 'ports');
|
||||
}
|
||||
|
@ -1119,7 +1110,7 @@ function get_port_stack(Illuminate\Http\Request $request)
|
|||
});
|
||||
}
|
||||
|
||||
function update_device_port_notes(Illuminate\Http\Request $request): \Illuminate\Http\JsonResponse
|
||||
function update_device_port_notes(Illuminate\Http\Request $request): JsonResponse
|
||||
{
|
||||
$portid = $request->route('portid');
|
||||
|
||||
|
@ -1153,7 +1144,10 @@ function list_alert_rules(Illuminate\Http\Request $request)
|
|||
return api_success($rules->toArray($request), 'rules');
|
||||
}
|
||||
|
||||
function list_alerts(Illuminate\Http\Request $request)
|
||||
/**
|
||||
* @throws \LibreNMS\Exceptions\ApiException
|
||||
*/
|
||||
function list_alerts(Illuminate\Http\Request $request): JsonResponse
|
||||
{
|
||||
$id = $request->route('id');
|
||||
|
||||
|
@ -1191,9 +1185,7 @@ function list_alerts(Illuminate\Http\Request $request)
|
|||
|
||||
if ($request->has('order')) {
|
||||
[$sort_column, $sort_order] = explode(' ', $request->get('order'), 2);
|
||||
if (($res = validate_column_list($sort_column, 'alerts')) !== true) {
|
||||
return $res;
|
||||
}
|
||||
validate_column_list($sort_column, 'alerts');
|
||||
if (in_array($sort_order, ['asc', 'desc'])) {
|
||||
$order = $request->get('order');
|
||||
}
|
||||
|
@ -2618,22 +2610,29 @@ function list_logs(Illuminate\Http\Request $request, Router $router)
|
|||
return api_success($logs, 'logs', null, 200, null, ['total' => $count]);
|
||||
}
|
||||
|
||||
function validate_column_list($columns, $tableName)
|
||||
/**
|
||||
* @throws \LibreNMS\Exceptions\ApiException
|
||||
*/
|
||||
function validate_column_list(?string $columns, string $table, array $default = []): array
|
||||
{
|
||||
if ($columns == '') { // no user input, return default
|
||||
return $default;
|
||||
}
|
||||
|
||||
static $schema;
|
||||
if (is_null($schema)) {
|
||||
$schema = new \LibreNMS\DB\Schema();
|
||||
}
|
||||
|
||||
$column_names = is_array($columns) ? $columns : explode(',', $columns);
|
||||
$valid_columns = $schema->getColumns($tableName);
|
||||
$valid_columns = $schema->getColumns($table);
|
||||
$invalid_columns = array_diff(array_map('trim', $column_names), $valid_columns);
|
||||
|
||||
if (count($invalid_columns) > 0) {
|
||||
return api_error(400, 'Invalid columns: ' . join(',', $invalid_columns));
|
||||
throw new InvalidTableColumnException($invalid_columns);
|
||||
}
|
||||
|
||||
return true;
|
||||
return $column_names;
|
||||
}
|
||||
|
||||
function missing_fields($required_fields, $data)
|
||||
|
|
|
@ -2430,26 +2430,6 @@ parameters:
|
|||
count: 1
|
||||
path: LibreNMS/Device/YamlDiscovery.php
|
||||
|
||||
-
|
||||
message: "#^Method LibreNMS\\\\Exceptions\\\\ApiException\\:\\:__construct\\(\\) has parameter \\$message with no type specified\\.$#"
|
||||
count: 1
|
||||
path: LibreNMS/Exceptions/ApiException.php
|
||||
|
||||
-
|
||||
message: "#^Method LibreNMS\\\\Exceptions\\\\ApiException\\:\\:__construct\\(\\) has parameter \\$output with no type specified\\.$#"
|
||||
count: 1
|
||||
path: LibreNMS/Exceptions/ApiException.php
|
||||
|
||||
-
|
||||
message: "#^Method LibreNMS\\\\Exceptions\\\\ApiException\\:\\:getOutput\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: LibreNMS/Exceptions/ApiException.php
|
||||
|
||||
-
|
||||
message: "#^Property LibreNMS\\\\Exceptions\\\\ApiException\\:\\:\\$output has no type specified\\.$#"
|
||||
count: 1
|
||||
path: LibreNMS/Exceptions/ApiException.php
|
||||
|
||||
-
|
||||
message: "#^Method LibreNMS\\\\Exceptions\\\\AuthenticationException\\:\\:__construct\\(\\) has parameter \\$code with no type specified\\.$#"
|
||||
count: 1
|
||||
|
|
Loading…
Reference in New Issue