Added URL Checker into tools menu.
This commit is contained in:
@@ -51,6 +51,7 @@ RUN echo '' && \
|
||||
# create /usr/bin/php that points to /opt/bin/frankenphp php-cli "$@" \
|
||||
echo '#!/bin/sh' > /usr/bin/php && \
|
||||
echo 'exec /opt/bin/frankenphp php-cli "$@"' >> /usr/bin/php && chmod +x /usr/bin/php && \
|
||||
ln -s /usr/bin/nano /usr/bin/pico && \
|
||||
# Create basic directories.
|
||||
bash -c 'umask 0000 && mkdir -p /temp_data/ /opt/{app,bin,config} /config/{backup,cache,config,db,debug,logs,webhooks,profiler}' && \
|
||||
# Link console.
|
||||
|
||||
72
FAQ.md
72
FAQ.md
@@ -7,7 +7,7 @@ To turn on automatic import or export tasks:
|
||||
1. Go to the `Tasks` page in the WebUI.
|
||||
2. Enable the tasks you want to schedule for automatic execution.
|
||||
|
||||
By default:
|
||||
By default, the tasks are scheduled:
|
||||
|
||||
- **Import** task runs every **1 hour**
|
||||
- **Export** task runs every **1 hour and 30 minutes**
|
||||
@@ -17,7 +17,9 @@ If you want to customize the schedule, you can do so by adding environment varia
|
||||
- **WS_CRON_IMPORT_AT**
|
||||
- **WS_CRON_EXPORT_AT**
|
||||
|
||||
You can set these variables from the <code>Env</code> page.
|
||||
You can set these variables from the <!--i:fa-tasks--> **Tasks**, underneath the task click on the timer, and it will
|
||||
take you to the <!--i:fa-cogs--> **Env** page, where you can set the cron expression directly. Or you can go to
|
||||
<!--i:fa-cogs--> **Env** page and add the variables manually.
|
||||
|
||||
> [!NOTE]
|
||||
> A great tool to validate your cron expression is [crontab.guru](https://crontab.guru/)
|
||||
@@ -119,7 +121,7 @@ state is preserved.
|
||||
|
||||
However, if the new backend's state is incorrect, it may unintentionally override your accurate local watch history.
|
||||
|
||||
## How to Fix the the play state
|
||||
## How to Fix the play state
|
||||
|
||||
To synchronize both backends correctly:
|
||||
|
||||
@@ -221,7 +223,7 @@ digits, we’ll automatically prefix it with `user_`.
|
||||
|
||||
----
|
||||
|
||||
# Does WatchState requires Webhooks to work?
|
||||
# Does WatchState require Webhooks to work?
|
||||
|
||||
No, webhooks are **not required** for the tool to function. You can use the built-in **scheduled tasks** or manually run
|
||||
**import/export operations** on demand through the WebUI or console.
|
||||
@@ -421,7 +423,7 @@ After updating the environment variables, **restart the container** to apply the
|
||||
|
||||
# How to get WatchState working with YouTube content/library?
|
||||
|
||||
Due to the nature on how people name their youtube files i had to pick something specific for it to work cross supported
|
||||
Due to the nature on how people name their youtube files I had to pick something specific for it to work cross supported
|
||||
media agents. Please visit [this link](https://github.com/arabcoders/jf-ytdlp-info-reader-plugin#usage) to know how to
|
||||
name your files. Please be aware these plugins and scanners `REQUIRE`
|
||||
that you have a `yt-dlp` `.info.json` files named exactly as your media file.
|
||||
@@ -454,44 +456,22 @@ relevant data if they are not matching correctly, and we hopefully can resolve i
|
||||
|
||||
# How to check if the container able to communicate with the media backends?
|
||||
|
||||
If you having problem adding a backend to `WatchState`, it most likely network related problem, where the container
|
||||
If you're having problem adding a backend to `WatchState`, it most likely network related problem, where the container
|
||||
isn't able to communicate with the media backend. Thus, you will get errors. To make sure the container is able to
|
||||
communicate with the media backend, you can run the following command and check the output.
|
||||
communicate with the media backend, run the following tests via
|
||||
<!--i:fa-tools--> **Tools** > <!--i:fa-external-link--> **URL Checker**.
|
||||
|
||||
If the command fails for any reason, then you most likely have network related problem or invalid apikey/token.
|
||||
|
||||
## For Plex.
|
||||
|
||||
```bash
|
||||
$ docker exec -ti watchstate bash
|
||||
$ curl -H "Accept: application/json" -H "X-Plex-Token: [PLEX_TOKEN]" http://[PLEX_URL]/
|
||||
```
|
||||
From the `Pre-defined` templates select the media server you want to test against and replace the following with your
|
||||
info
|
||||
|
||||
* Replace `[PLEX_TOKEN]` with your plex token.
|
||||
* Replace `[PLEX_URL]` with your plex url. The one you selected when prompted by the command.
|
||||
* Replace `[API_KEY]` with your jellyfin/emby api key.
|
||||
* Replace `[IP:port]` with your media backend host or ip:port. it can be a host or ip:port i.e. `media.mydomain.ltd`
|
||||
or `172.23.0.11:8096`.
|
||||
|
||||
```
|
||||
{"MediaContainer":{"size":25,...}}
|
||||
```
|
||||
|
||||
If everything is working correctly you should see something like this previous json output.
|
||||
|
||||
## For Jellyfin & Emby.
|
||||
|
||||
```bash
|
||||
$ docker exec -ti watchstate bash
|
||||
$ curl -v -H "Accept: application/json" -H "X-MediaBrowser-Token: [BACKEND_API_KEY]" http://[BACKEND_HOST]/System/Info
|
||||
```
|
||||
|
||||
* Replace `[BACKEND_API_KEY]` with your jellyfin/emby api key.
|
||||
* Replace `[BACKEND_HOST]` with your jellyfin/emby host. it can be a host or ip:port i.e. `jf.mydomain.ltd`
|
||||
or `172.23.0.11:8096`
|
||||
|
||||
```
|
||||
{"OperatingSystemDisplayName":"Linux","HasPendingRestart":false,"IsShuttingDown":false,...}}
|
||||
```
|
||||
|
||||
If everything is working correctly you should see something like this previous json output.
|
||||
If everything is working correctly you should see `200 Status code response.` with green text. this good indicator that
|
||||
the container is able to communicate with the media backend. If you see `403` or `404` or any other error code, please
|
||||
check your backend settings and make sure the token is correct and the ip:port is reachable from the container.
|
||||
|
||||
----
|
||||
|
||||
@@ -581,10 +561,10 @@ services:
|
||||
```
|
||||
|
||||
This setup should work for VAAPI encoding in `x86_64` containers, There are currently an issue with nvidia h264_nvenc
|
||||
encoding, the alpine build for`ffmpeg` doesn't include the codec. i am looking for a way include the codec without
|
||||
encoding, the alpine build for`ffmpeg` doesn't include the codec. I am looking for a way include the codec without
|
||||
ballooning the image size by 600MB+. If you have a solution please let me know.
|
||||
|
||||
Please know that your `video`, `render` group id might be different then mine, you can run the follow command in docker
|
||||
Please know that your `video`, `render` group id might be different from mine, you can run the follow command in docker
|
||||
host server to get the group ids for both groups.
|
||||
|
||||
```bash
|
||||
@@ -690,11 +670,7 @@ To check if there is any watch progress events being registered, You can go to `
|
||||
`on_progress` events, if you are seeing those, this means the progress is being synced. Check the `Tasks logs` to see
|
||||
the event log.
|
||||
|
||||
If this is setup and working you may be ok with changing the `WS_CRON_IMPORT_AT/WS_CRON_EXPORT_AT` schedule to something
|
||||
less frequent as the sync progress working will update the progress near realtime. For example you could change these
|
||||
tasks to run daily instead of hourly.
|
||||
|
||||
```
|
||||
WS_CRON_IMPORT_AT=0 11 * * *
|
||||
WS_CRON_EXPORT_AT=30 11 * * *
|
||||
```
|
||||
If this is set up and working you may be ok with changing the `WS_CRON_IMPORT_AT/WS_CRON_EXPORT_AT` schedule to
|
||||
something less frequent as the sync progress working will update the progress near realtime. For example, you could
|
||||
change these tasks to run daily to know how to do that please
|
||||
check [this FAQ entry](#how-to-enable-scheduledautomatic-tasks).
|
||||
|
||||
@@ -232,13 +232,19 @@ return (function () {
|
||||
'pcre.jit' => 1,
|
||||
'opcache.enable' => 1,
|
||||
'opcache.memory_consumption' => 128,
|
||||
'opcache.interned_strings_buffer' => 8,
|
||||
'opcache.interned_strings_buffer' => 16,
|
||||
'opcache.max_accelerated_files' => 10000,
|
||||
'opcache.max_wasted_percentage' => 5,
|
||||
'opcache.validate_timestamps' => $inContainer ? 0 : 1,
|
||||
'expose_php' => 0,
|
||||
'date.timezone' => ag($config, 'tz', 'UTC'),
|
||||
'zend.assertions' => -1
|
||||
'zend.assertions' => -1,
|
||||
'short_open_tag' => 0,
|
||||
'opcache.jit' => 'disabled',
|
||||
'opcache.jit_buffer_size' => 0,
|
||||
// @TODO: keep jit disabled for now, as it is not stable yet,. and we haven't tested it with frankenphp
|
||||
//'opcache.jit' => $inContainer ? 'tracing' : 'disabled',
|
||||
//'opcache.jit_buffer_size' => $inContainer ? '128M' : 0,
|
||||
],
|
||||
'fpm' => [
|
||||
'global' => [
|
||||
|
||||
@@ -163,7 +163,7 @@ html {
|
||||
}
|
||||
|
||||
.has-text-purple {
|
||||
color: #5f00d1;
|
||||
color: #5f00d1 !important;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 769px), print {
|
||||
|
||||
@@ -72,6 +72,11 @@
|
||||
<span>Processes</span>
|
||||
</NuxtLink>
|
||||
|
||||
<NuxtLink class="navbar-item" to="/url_check" @click.native="(e) => changeRoute(e)">
|
||||
<span class="icon"><i class="fas fa-external-link"/></span>
|
||||
<span>URL Checker</span>
|
||||
</NuxtLink>
|
||||
|
||||
<hr class="navbar-divider">
|
||||
|
||||
<NuxtLink class="navbar-item" to="/parity" @click.native="(e) => changeRoute(e)">
|
||||
|
||||
448
frontend/pages/url_check.vue
Normal file
448
frontend/pages/url_check.vue
Normal file
@@ -0,0 +1,448 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="columns is-multiline">
|
||||
|
||||
<div class="column is-12 is-clearfix is-unselectable">
|
||||
<span class="title is-4">
|
||||
<span class="icon"><i class="fas fa-external-link"/></span>
|
||||
URL Checker
|
||||
</span>
|
||||
<div class="is-pulled-right" v-if="response.response?.status">
|
||||
<div class="field is-grouped">
|
||||
<p class="control">
|
||||
<button class="button is-info" @click="() => copyText(JSON.stringify(response,null,2))"
|
||||
v-tooltip.bottom="'Copy request & response.'">
|
||||
<span class="icon"><i class="fas fa-copy"/></span>
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="is-hidden-mobile">
|
||||
<span class="subtitle">Check if <strong>WatchState</strong> is able to communicate with the given URL.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-12">
|
||||
<h1 class="title is-4 is-clickable" @click="toggleForm">
|
||||
<span class="icon" v-if="response.response.status && !invalid_form">
|
||||
<i class="fas" :class="{'fa-arrow-up': toggle_form, 'fa-arrow-down': !toggle_form}"/>
|
||||
</span>
|
||||
Request Form
|
||||
</h1>
|
||||
<Message message_class="has-background-warning-80 has-text-dark" v-if="has_template_values()">
|
||||
<p>
|
||||
The form contains <strong>template values</strong> <code>[...]</code>. Please make sure to replace them
|
||||
with the actual values.
|
||||
</p>
|
||||
</Message>
|
||||
</div>
|
||||
|
||||
<div class="column is-12" v-if="toggle_form || !item.url">
|
||||
<form @submit.prevent="check_url">
|
||||
<div class="box content">
|
||||
<div class="field">
|
||||
<label class="label is-unselectable" for="url">Pre-defined template</label>
|
||||
<div class="control">
|
||||
<div class="select is-fullwidth">
|
||||
<select v-model="use_template" :disabled="is_loading">
|
||||
<option value="" v-text="'Select a template'" disabled/>
|
||||
<option v-for="template in templates" :key="template.key" :value="template.key"
|
||||
v-text="template.key"/>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help is-bold">
|
||||
<span class="icon"><i class="fas fa-info-circle"/></span>
|
||||
Gives a pre-defined template for the URL to check.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label is-unselectable" for="url">URL</label>
|
||||
<div class="field is-grouped">
|
||||
<div class="control">
|
||||
<div class="select is-fullwidth">
|
||||
<select v-model="item.method" :disabled="is_loading">
|
||||
<option v-for="method in methods" :key="method" :value="method" v-text="method"/>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control is-expanded has-icons-left">
|
||||
<input class="input" type="text" id="url" v-model="item.url" autocomplete="off"
|
||||
placeholder="https://example.com/api/v1/" :disabled="is_loading">
|
||||
<div class="icon is-left"><i class="fas fa-link"/></div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help is-bold">
|
||||
<span class="icon"><i class="fas fa-info-circle"/></span>
|
||||
The URL to check. It must be a valid URL.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label is-unselectable">
|
||||
Headers -
|
||||
<NuxtLink @click="add_header()" v-text="'Add'"/>
|
||||
</label>
|
||||
|
||||
<div class="control mb-2" v-for="(header, index) in item.headers" :key="header.index">
|
||||
<div class="field is-grouped">
|
||||
<div class="control is-expanded">
|
||||
<input class="input" type="text" v-model="header.key" placeholder="Header Key" required
|
||||
:disabled="is_loading">
|
||||
</div>
|
||||
<div class="control is-expanded">
|
||||
<input class="input" type="text" v-model="header.value" placeholder="Header Value" required
|
||||
:disabled="is_loading">
|
||||
</div>
|
||||
<div class="control">
|
||||
<button class="button is-danger" type="button" @click="item.headers.splice(index, 1)"
|
||||
:disabled="is_loading">
|
||||
<span class="icon"><i class="fas fa-times"/></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field is-grouped">
|
||||
<div class="control is-expanded">
|
||||
<button class="button is-fullwidth is-primary" type="submit" :disabled="invalid_form || is_loading"
|
||||
:class="{'is-loading': is_loading}">
|
||||
<span class="icon-text">
|
||||
<span class="icon"><i class="fas fa-paper-plane"/></span>
|
||||
<span>Send Request</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="control is-expanded">
|
||||
<button class="button is-fullwidth is-danger" type="button" @click="reset_form" :disabled="is_loading">
|
||||
<span class="icon-text">
|
||||
<span class="icon"><i class="fas fa-times"/></span>
|
||||
<span>Reset Form</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="column is-6-tablet is-12-mobile" v-if="response.response.status">
|
||||
<div class="card">
|
||||
<div class="card-header is-clickable" @click="toggle_request = !toggle_request">
|
||||
<div class="card-header-title is-block is-ellipsis">
|
||||
<span class="is-underlined has-text-danger">{{ response.request.method }}</span>
|
||||
{{ response.request.url }}
|
||||
</div>
|
||||
<button class="card-header-icon">
|
||||
<span class="icon">
|
||||
<i class="fas" :class="{'fa-arrow-up': toggle_request, 'fa-arrow-down': !toggle_request}"/>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-content content p-0 m-0" v-if="toggle_request">
|
||||
<div style="height: 300px" class="is-overflow-auto">
|
||||
<div class="table-container">
|
||||
<table class="table is-fullwidth is-hoverable is-striped" style="table-layout: fixed;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="has-text-centered" style="min-width:150px">Header</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody v-if="Object.keys(response.request?.headers ?? {}).length > 0">
|
||||
<tr v-for="(v,k) in response.request.headers" :key="k">
|
||||
<td class="is-vcentered is-ellipsis">
|
||||
<abbr :title="uc_words(k)" v-text="uc_words(k)" class="is-pointer-help"/>
|
||||
</td>
|
||||
<td class="is-vcentered">{{ v }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-else>
|
||||
<tr>
|
||||
<td colspan="2" class="has-text-centered">No request headers found.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-6-tablet is-12-mobile" v-if="response.response.status">
|
||||
<div class="card">
|
||||
<div class="card-header is-clickable" @click="toggle_response = !toggle_response">
|
||||
<div class="card-header-title is-block is-ellipsis">
|
||||
<span class="is-underlined" :class="colorStatus(response.response.status)">{{
|
||||
response.response.status
|
||||
}}</span>
|
||||
Status code response.
|
||||
</div>
|
||||
<button class="card-header-icon">
|
||||
<span class="icon">
|
||||
<i class="fas" :class="{'fa-arrow-up': toggle_response, 'fa-arrow-down': !toggle_response}"/>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-content content p-0 m-0" v-if="toggle_response">
|
||||
<div style="height: 300px" class="is-overflow-auto">
|
||||
<div class="table-container">
|
||||
<table class="table is-fullwidth is-bordered is-hoverable is-striped" style="table-layout: fixed;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="has-text-centered" style="width:150px">Header</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody v-if="Object.keys(response.response?.headers ?? {}).length > 0">
|
||||
<tr v-for="(v,k) in response.response.headers" :key="k">
|
||||
<td class="is-vcentered is-ellipsis">
|
||||
<abbr :title="uc_words(k)" v-text="uc_words(k)" class="is-pointer-help"/>
|
||||
</td>
|
||||
<td class="is-vcentered" :class="colorize(k)">{{ v }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-else>
|
||||
<tr>
|
||||
<td colspan="2" class="has-text-centered">No response headers found.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-12" v-if="response.response.status">
|
||||
<div class="card">
|
||||
<div class="card-header is-clickable" @click="toggle_body = !toggle_body">
|
||||
<div class="card-header-title is-block is-ellipsis" :class="colorStatus(response.response.status)">
|
||||
( <span class="is-underlined">{{ response.response.status }}</span> ) Response Body
|
||||
</div>
|
||||
<button class="card-header-icon">
|
||||
<span class="icon">
|
||||
<i class="fas" :class="{'fa-arrow-up': toggle_body, 'fa-arrow-down': !toggle_body}"/>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-content content p-0 m-0" v-if="toggle_body">
|
||||
<div style="max-height: 300px" class="is-overflow-auto">
|
||||
<pre><code>{{
|
||||
response.response.body ? tryParse(response.response.body) : 'Empty body'
|
||||
}}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-12">
|
||||
<Message message_class="has-background-info-90 has-text-dark" :toggle="show_page_tips"
|
||||
@toggle="show_page_tips = !show_page_tips" :use-toggle="true" title="Tips" icon="fas fa-info-circle">
|
||||
<ul>
|
||||
<li>
|
||||
Values in the form with <code>[...]</code> are <strong>template values</strong>. If they are part of a
|
||||
string, please only replace the bracket and the value inside it. For example, <code>[ip:port]</code>
|
||||
should be replaced with <code>192.168.8.1:8096</code>.
|
||||
</li>
|
||||
<li>
|
||||
If you see a <span class="has-text-success">green status code (200-299)</span>, it means the request was
|
||||
successful.
|
||||
</li>
|
||||
<li>
|
||||
If you see a <span class="has-text-danger">red status code (400-499)</span>, it means the request was
|
||||
rejected. by the target or the WatchState.
|
||||
</li>
|
||||
<li>
|
||||
If you see a <span class="has-text-warning">yellow status code (300-399)</span>, it means the request was
|
||||
redirected. This is not necessarily an error or successful request, but you should check the response and
|
||||
follow the redirect.
|
||||
</li>
|
||||
<li>
|
||||
If you see a <span class="has-text-purple">purple status code (500+)</span>, it means the server
|
||||
encountered an error.
|
||||
</li>
|
||||
<li>You can add this special header <code>ws-timeout</code> to control the connection timeout for the http
|
||||
library.
|
||||
</li>
|
||||
</ul>
|
||||
</Message>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import request from "~/utils/request.js";
|
||||
import {notification} from "~/utils/index.js";
|
||||
import Message from "~/components/Message.vue";
|
||||
import {useStorage} from "@vueuse/core";
|
||||
|
||||
useHead({title: 'URL Checker'})
|
||||
|
||||
const show_page_tips = useStorage('show_page_tips', true)
|
||||
const toggle_form = ref(true)
|
||||
const toggle_request = ref(true)
|
||||
const toggle_response = ref(true)
|
||||
const toggle_body = ref(true)
|
||||
const use_template = ref("")
|
||||
const templates = ref([
|
||||
{
|
||||
"key": "Plex Media Server",
|
||||
"override": {
|
||||
"method": "GET",
|
||||
"url": "http://[ip:port]/library/sections",
|
||||
"headers": [
|
||||
{"key": "Accept", "value": "application/json"},
|
||||
{"key": "X-Plex-Token", "value": "[PLEX_TOKEN]"},
|
||||
]
|
||||
},
|
||||
},
|
||||
{
|
||||
"key": "Jellyfin/Emby Server",
|
||||
"override": {
|
||||
"method": "GET",
|
||||
"url": "http://[ip:port]/items",
|
||||
"headers": [
|
||||
{"key": "Accept", "value": "application/json"},
|
||||
{"key": "X-MediaBrowser-Token", "value": "[API_KEY]"},
|
||||
]
|
||||
},
|
||||
},
|
||||
])
|
||||
const methods = ref(['GET', 'POST', 'PUT', 'PATCH', 'HEAD', 'DELETE'])
|
||||
const item = ref({"url": "", "method": "GET", "headers": []})
|
||||
const is_loading = ref(false)
|
||||
|
||||
const default_response = {
|
||||
request: {url: "", method: "GET", headers: []},
|
||||
response: {status: null, headers: [], body: ""}
|
||||
}
|
||||
const response = ref(default_response)
|
||||
|
||||
watch(use_template, async newValue => {
|
||||
if ("" === newValue) {
|
||||
return
|
||||
}
|
||||
|
||||
const template = templates.value.find(t => t.key === newValue)
|
||||
if (!template) {
|
||||
notification('error', 'Error', 'Template not found')
|
||||
return
|
||||
}
|
||||
|
||||
item.value = template.override
|
||||
await nextTick()
|
||||
|
||||
use_template.value = ""
|
||||
})
|
||||
|
||||
const reset_form = async () => item.value = default_response.request
|
||||
|
||||
const invalid_form = computed(() => {
|
||||
if (!item.value.url) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (!item.value.method) {
|
||||
return true
|
||||
}
|
||||
|
||||
try {
|
||||
new URL(item.value.url)
|
||||
} catch (e) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
const has_template_values = () => {
|
||||
if (/\[.+?]/.test(item.value.url)) {
|
||||
return true
|
||||
}
|
||||
|
||||
for (const header of item.value.headers) {
|
||||
if (/\[.+?]/.test(header.key) || /\[.+?]/.test(header.value)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const add_header = (k, v) => item.value.headers.push({key: k ?? "", value: v ?? ""})
|
||||
|
||||
const check_url = async () => {
|
||||
if (true === invalid_form.value) {
|
||||
notification('error', 'Error', 'Please fill in all required fields.')
|
||||
return
|
||||
}
|
||||
|
||||
if (has_template_values() && !confirm('The form contains template values. Do you want to continue?')) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
response.value = default_response
|
||||
await nextTick()
|
||||
|
||||
is_loading.value = true
|
||||
const resp = await request('/system/url/check', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(item.value),
|
||||
});
|
||||
|
||||
const json = await parse_api_response(resp)
|
||||
|
||||
if (200 !== resp.status) {
|
||||
notification('error', 'Error', `${json.error.code ?? resp.status}: ${json.error.message ?? 'Unknown error'}`)
|
||||
return;
|
||||
}
|
||||
|
||||
response.value = json;
|
||||
toggle_form.value = false
|
||||
toggle_request.value = false
|
||||
} catch (e) {
|
||||
notification('error', `failed to send request. ${e}`);
|
||||
} finally {
|
||||
is_loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const uc_words = str => str.replace(/_/g, ' ').replace(/\b\w/g, char => char.toUpperCase());
|
||||
|
||||
const tryParse = body => {
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(body), null, 2)
|
||||
} catch (e) {
|
||||
return body
|
||||
}
|
||||
}
|
||||
|
||||
const colorStatus = status => {
|
||||
if (status >= 200 && status < 300) {
|
||||
return 'has-text-success'
|
||||
} else if (status >= 300 && status < 400) {
|
||||
return 'has-text-warning'
|
||||
} else if (status >= 400 && status < 500) {
|
||||
return 'has-text-danger'
|
||||
} else if (status >= 500) {
|
||||
return 'has-text-purple'
|
||||
}
|
||||
}
|
||||
|
||||
const toggleForm = () => {
|
||||
if (!item.value.url) {
|
||||
toggle_form.value = true
|
||||
return
|
||||
}
|
||||
|
||||
toggle_form.value = !toggle_form.value
|
||||
}
|
||||
const colorize = k => k.toLowerCase().startsWith('ws-') ? 'has-text-danger' : ''
|
||||
|
||||
onMounted(() => disableOpacity())
|
||||
onBeforeUnmount(() => enableOpacity())
|
||||
</script>
|
||||
98
src/API/System/UrlChecker.php
Normal file
98
src/API/System/UrlChecker.php
Normal file
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\API\System;
|
||||
|
||||
use App\Libs\Attributes\Route\Post;
|
||||
use App\Libs\DataUtil;
|
||||
use App\Libs\Enums\Http\Method;
|
||||
use App\Libs\Enums\Http\Status;
|
||||
use Psr\Http\Message\ResponseInterface as iResponse;
|
||||
use Psr\Http\Message\ServerRequestInterface as iRequest;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface as iHttp;
|
||||
use Throwable;
|
||||
|
||||
final class UrlChecker
|
||||
{
|
||||
public const string URL = '%{api.prefix}/system/url/check';
|
||||
|
||||
#[Post(self::URL . '[/]', name: 'system.url.check')]
|
||||
public function __invoke(iRequest $request, iHttp $client): iResponse
|
||||
{
|
||||
$params = DataUtil::fromRequest($request);
|
||||
|
||||
if (null === ($url = $params->get('url', null))) {
|
||||
return api_error('No url was given.', Status::BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (false === isValidURL($url)) {
|
||||
return api_error('Invalid url.', Status::BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (null === ($method = Method::tryFrom(strtoupper($params->get('method', 'GET'))))) {
|
||||
return api_error('Invalid request method.', Status::BAD_REQUEST);
|
||||
}
|
||||
|
||||
$headers = [];
|
||||
|
||||
$timeout = 10;
|
||||
|
||||
foreach ($params->get('headers', []) as $header) {
|
||||
$headerKey = ag($header, 'key');
|
||||
$headerValue = ag($header, 'value');
|
||||
if (empty($headerKey) || empty($headerValue)) {
|
||||
continue;
|
||||
}
|
||||
if ('ws-timeout' === $headerKey) {
|
||||
$timeout = (int)$headerValue;
|
||||
continue;
|
||||
}
|
||||
$headers[$headerKey] = $headerValue;
|
||||
}
|
||||
|
||||
try {
|
||||
$response = $client->request($method->value, $url, [
|
||||
'timeout' => $timeout,
|
||||
'headers' => $headers
|
||||
]);
|
||||
$flattenedHeaders = [];
|
||||
foreach ($response->getHeaders(false) as $key => $value) {
|
||||
if (is_array($value)) {
|
||||
$flattenedHeaders[$key] = implode(', ', $value);
|
||||
} else {
|
||||
$flattenedHeaders[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return api_response(Status::OK, [
|
||||
'request' => [
|
||||
'url' => $url,
|
||||
'method' => $method->value,
|
||||
'headers' => $headers,
|
||||
],
|
||||
'response' => [
|
||||
'status' => $response->getStatusCode(),
|
||||
'headers' => $flattenedHeaders,
|
||||
'body' => $response->getContent(false),
|
||||
],
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
return api_response(Status::OK, [
|
||||
'request' => [
|
||||
'url' => $url,
|
||||
'method' => $method->value,
|
||||
'headers' => $headers,
|
||||
],
|
||||
'response' => [
|
||||
'status' => Status::INTERNAL_SERVER_ERROR->value,
|
||||
'headers' => [
|
||||
'WS-Exception' => $e::class,
|
||||
'WS-Error' => $e->getMessage(),
|
||||
],
|
||||
'body' => $e->getMessage(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user