Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9856ca7856 | ||
|
|
fa4eda07f9 | ||
|
|
0102f56a36 | ||
|
|
689f688405 |
@@ -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).
|
||||
|
||||
@@ -224,17 +224,27 @@ return (function () {
|
||||
'disable_functions' => null,
|
||||
'display_errors' => 0,
|
||||
'error_log' => $inContainer ? '/proc/self/fd/2' : 'syslog',
|
||||
'syslog.ident' => 'php-fpm',
|
||||
'syslog.ident' => $inContainer ? 'frankenphp' : 'php-fpm',
|
||||
'memory_limit' => '265M',
|
||||
'post_max_size' => '100M',
|
||||
'upload_max_filesize' => '100M',
|
||||
'zend.exception_ignore_args' => $inContainer ? 1 : 0,
|
||||
'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' => [
|
||||
|
||||
@@ -221,6 +221,4 @@ return [
|
||||
'visible' => false,
|
||||
'description' => 'Whether to use old progress endpoint for plex.',
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
|
||||
@@ -163,7 +163,7 @@ html {
|
||||
}
|
||||
|
||||
.has-text-purple {
|
||||
color: #5f00d1;
|
||||
color: #5f00d1 !important;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 769px), print {
|
||||
|
||||
@@ -59,7 +59,8 @@
|
||||
<span>Plex Token</span>
|
||||
</NuxtLink>
|
||||
|
||||
<NuxtLink class="navbar-item" to="/tools/sub_users" @click.native="(e) => changeRoute(e)">
|
||||
<NuxtLink class="navbar-item" to="/tools/sub_users" @click.native="(e) => changeRoute(e)"
|
||||
v-if="'main' === api_user">
|
||||
<span class="icon"><i class="fas fa-users"/></span>
|
||||
<span>Sub Users</span>
|
||||
</NuxtLink>
|
||||
@@ -71,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)">
|
||||
@@ -270,6 +276,8 @@ const showSettings = ref(false)
|
||||
const auth = useAuthStore()
|
||||
const bg_enable = useStorage('bg_enable', true)
|
||||
const bg_opacity = useStorage('bg_opacity', 0.95)
|
||||
const api_user = useStorage('api_user', 'main')
|
||||
|
||||
const api_version = ref()
|
||||
const bgImage = ref({src: '', type: ''})
|
||||
const loadedImages = ref({poster: '', background: ''})
|
||||
|
||||
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>
|
||||
@@ -7,15 +7,24 @@ namespace App\API\System;
|
||||
use App\Libs\Attributes\Route\Get;
|
||||
use App\Libs\Enums\Http\Status;
|
||||
use Psr\Http\Message\ResponseInterface as iResponse;
|
||||
use Psr\Http\Message\ServerRequestInterface as iRequest;
|
||||
|
||||
final class Report
|
||||
{
|
||||
public const string URL = '%{api.prefix}/system/report';
|
||||
|
||||
#[Get(self::URL . '[/]', name: 'system.report')]
|
||||
public function __invoke(iRequest $request): iResponse
|
||||
public function basic_report(): iResponse
|
||||
{
|
||||
return api_response(Status::OK, runCommand('system:report', asArray: true));
|
||||
}
|
||||
|
||||
#[Get(self::URL . '/ini[/]', name: 'system.ini')]
|
||||
public function php_ini(): iResponse
|
||||
{
|
||||
if (false === str_starts_with(getAppVersion(), 'dev')) {
|
||||
return api_error('This endpoint is only available in development mode.', Status::FORBIDDEN);
|
||||
}
|
||||
|
||||
return api_response(Status::OK, ['content' => ini_get_all()]);
|
||||
}
|
||||
}
|
||||
|
||||
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(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,16 @@ final class ReportCommand extends Command
|
||||
|
||||
private const int DEFAULT_LIMIT = 10;
|
||||
|
||||
/**
|
||||
* @var array<string> $sensitive strip sensitive information from the report.
|
||||
*/
|
||||
private array $sensitive = [];
|
||||
|
||||
/**
|
||||
* @var iOutput|null $output The output instance.
|
||||
*/
|
||||
private iOutput|null $output = null;
|
||||
|
||||
/**
|
||||
* Class Constructor.
|
||||
*
|
||||
@@ -45,8 +55,11 @@ final class ReportCommand extends Command
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(private iDB $db, private iImport $mapper, private iLogger $logger)
|
||||
{
|
||||
public function __construct(
|
||||
private readonly iDB $db,
|
||||
private readonly iImport $mapper,
|
||||
private readonly iLogger $logger
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
@@ -94,24 +107,24 @@ final class ReportCommand extends Command
|
||||
protected function runCommand(iInput $input, iOutput $output): int
|
||||
{
|
||||
assert($output instanceof ConsoleOutput, new RuntimeException('Expecting ConsoleOutput instance.'));
|
||||
$output = $output->withNoSuppressor();
|
||||
$this->output = $output->withNoSuppressor();
|
||||
|
||||
$output->writeln('<info>[ Basic Report ]</info>' . PHP_EOL);
|
||||
$output->writeln(r('WatchState version: <flag>{answer}</flag>', ['answer' => getAppVersion()]));
|
||||
$output->writeln(r('PHP version: <flag>{answer}</flag>', ['answer' => PHP_VERSION]));
|
||||
$output->writeln(r('Timezone: <flag>{answer}</flag>', ['answer' => Config::get('tz', 'UTC')]));
|
||||
$output->writeln(r('Data path: <flag>{answer}</flag>', ['answer' => Config::get('path')]));
|
||||
$output->writeln(r('Temp path: <flag>{answer}</flag>', ['answer' => Config::get('tmpDir')]));
|
||||
$output->writeln(
|
||||
$this->filter('<info>[ Basic Report ]</info>' . PHP_EOL);
|
||||
$this->filter(r('WatchState version: <flag>{answer}</flag>', ['answer' => getAppVersion()]));
|
||||
$this->filter(r('PHP version: <flag>{answer}</flag>', ['answer' => PHP_SAPI . '/' . PHP_VERSION]));
|
||||
$this->filter(r('Timezone: <flag>{answer}</flag>', ['answer' => Config::get('tz', 'UTC')]));
|
||||
$this->filter(r('Data path: <flag>{answer}</flag>', ['answer' => Config::get('path')]));
|
||||
$this->filter(r('Temp path: <flag>{answer}</flag>', ['answer' => Config::get('tmpDir')]));
|
||||
$this->filter(
|
||||
r('Database migrated?: <flag>{answer}</flag>', ['answer' => $this->db->isMigrated() ? 'Yes' : 'No'])
|
||||
);
|
||||
$output->writeln(
|
||||
$this->filter(
|
||||
r("Does the '.env' file exists? <flag>{answer}</flag>", [
|
||||
'answer' => file_exists(Config::get('path') . '/config/.env') ? 'Yes' : 'No',
|
||||
])
|
||||
);
|
||||
|
||||
$output->writeln(
|
||||
$this->filter(
|
||||
r('Is the tasks runner working? <flag>{answer}</flag>', [
|
||||
'answer' => (function () {
|
||||
$info = isTaskWorkerRunning(ignoreContainer: true);
|
||||
@@ -123,21 +136,22 @@ final class ReportCommand extends Command
|
||||
})(),
|
||||
])
|
||||
);
|
||||
$output->writeln(r('Running in container? <flag>{answer}</flag>', ['answer' => inContainer() ? 'Yes' : 'No']));
|
||||
|
||||
$output->writeln(r('Report generated at: <flag>{answer}</flag>', ['answer' => gmdate(Date::ATOM)]));
|
||||
$this->filter(r('Running in container? <flag>{answer}</flag>', ['answer' => inContainer() ? 'Yes' : 'No']));
|
||||
|
||||
$output->writeln(PHP_EOL . '<info>[ Backends ]</info>' . PHP_EOL);
|
||||
$this->getBackends($input, $output);
|
||||
$this->filter(r('Report generated at: <flag>{answer}</flag>', ['answer' => gmdate(Date::ATOM)]));
|
||||
|
||||
$output->writeln(PHP_EOL . '<info>[ Log suppression ]</info>' . PHP_EOL);
|
||||
$this->getSuppressor($output);
|
||||
$this->filter(PHP_EOL . '<info>[ Backends ]</info>' . PHP_EOL);
|
||||
$this->getBackends($input);
|
||||
|
||||
$output->writeln('<info>[ Tasks ]</info>' . PHP_EOL);
|
||||
$this->getTasks($output);
|
||||
$output->writeln('<info>[ Logs ]</info>' . PHP_EOL);
|
||||
$this->getLogs($input, $output);
|
||||
$this->printFooter($output);
|
||||
$this->filter(PHP_EOL . '<info>[ Log suppression ]</info>' . PHP_EOL);
|
||||
$this->getSuppressor();
|
||||
|
||||
$this->filter('<info>[ Tasks ]</info>' . PHP_EOL);
|
||||
$this->getTasks();
|
||||
$this->filter('<info>[ Logs ]</info>' . PHP_EOL);
|
||||
$this->getLogs($input);
|
||||
$this->printFooter();
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
@@ -146,18 +160,18 @@ final class ReportCommand extends Command
|
||||
* Get backends and display information about each backend.
|
||||
*
|
||||
* @param iInput $input An instance of the iInput class used for input operations.
|
||||
* @param iOutput $output An instance of the iOutput class used for output operations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function getBackends(iInput $input, iOutput $output): void
|
||||
private function getBackends(iInput $input): void
|
||||
{
|
||||
$includeSample = (bool)$input->getOption('include-db-sample');
|
||||
|
||||
$usersContext = getUsersContext($this->mapper, $this->logger);
|
||||
$this->extractSensitive($usersContext);
|
||||
|
||||
if (count($usersContext) > 1) {
|
||||
$output->writeln(
|
||||
$this->filter(
|
||||
r('Users? {users}' . PHP_EOL, [
|
||||
'users' => implode(', ', array_keys($usersContext)),
|
||||
])
|
||||
@@ -180,7 +194,7 @@ final class ReportCommand extends Command
|
||||
}
|
||||
}
|
||||
|
||||
$output->writeln(
|
||||
$this->filter(
|
||||
r('[ <value>{type} ({version}) ==> {username}@{name}</value> ]' . PHP_EOL, [
|
||||
'name' => $name,
|
||||
'username' => $username,
|
||||
@@ -189,32 +203,32 @@ final class ReportCommand extends Command
|
||||
])
|
||||
);
|
||||
|
||||
$output->writeln(
|
||||
$this->filter(
|
||||
r('Is backend URL HTTPS? <flag>{answer}</flag>', [
|
||||
'answer' => str_starts_with(ag($backend, 'url'), 'https:') ? 'Yes' : 'No',
|
||||
])
|
||||
);
|
||||
|
||||
$output->writeln(
|
||||
$this->filter(
|
||||
r('Has Unique Identifier? <flag>{answer}</flag>', [
|
||||
'answer' => null !== ag($backend, 'uuid') ? 'Yes' : 'No',
|
||||
])
|
||||
);
|
||||
|
||||
$output->writeln(
|
||||
$this->filter(
|
||||
r('Has User? <flag>{answer}</flag>', [
|
||||
'answer' => null !== ag($backend, 'user') ? 'Yes' : 'No',
|
||||
])
|
||||
);
|
||||
|
||||
$output->writeln(
|
||||
$this->filter(
|
||||
r('Export Enabled? <flag>{answer}</flag>', [
|
||||
'answer' => null !== ag($backend, 'export.enabled') ? 'Yes' : 'No',
|
||||
])
|
||||
);
|
||||
|
||||
if (null !== ag($backend, 'export.enabled')) {
|
||||
$output->writeln(
|
||||
$this->filter(
|
||||
r('Time since last export? <flag>{answer}</flag>', [
|
||||
'answer' => null === ag($backend, 'export.lastSync') ? 'Never' : gmdate(
|
||||
Date::ATOM,
|
||||
@@ -224,20 +238,20 @@ final class ReportCommand extends Command
|
||||
);
|
||||
}
|
||||
|
||||
$output->writeln(
|
||||
$this->filter(
|
||||
r('Play state import enabled? <flag>{answer}</flag>', [
|
||||
'answer' => null !== ag($backend, 'import.enabled') ? 'Yes' : 'No',
|
||||
])
|
||||
);
|
||||
|
||||
$output->writeln(
|
||||
$this->filter(
|
||||
r('Metadata only import enabled? <flag>{answer}</flag>', [
|
||||
'answer' => null !== ag($backend, 'options.' . Options::IMPORT_METADATA_ONLY) ? 'Yes' : 'No',
|
||||
])
|
||||
);
|
||||
|
||||
if (null !== ag($backend, 'import.enabled')) {
|
||||
$output->writeln(
|
||||
$this->filter(
|
||||
r('Time since last import? <flag>{answer}</flag>', [
|
||||
'answer' => null === ag($backend, 'import.lastSync') ? 'Never' : gmdate(
|
||||
Date::ATOM,
|
||||
@@ -247,20 +261,20 @@ final class ReportCommand extends Command
|
||||
);
|
||||
}
|
||||
|
||||
$output->writeln(
|
||||
$this->filter(
|
||||
r('Is webhook match user id enabled? <flag>{answer}</flag>', [
|
||||
'answer' => true === (bool)ag($backend, 'webhook.match.user') ? 'Yes' : 'No',
|
||||
])
|
||||
);
|
||||
|
||||
$output->writeln(
|
||||
$this->filter(
|
||||
r('Is webhook match backend unique id enabled? <flag>{answer}</flag>', [
|
||||
'answer' => true === (bool)ag($backend, 'webhook.match.uuid') ? 'Yes' : 'No',
|
||||
])
|
||||
);
|
||||
|
||||
$opts = ag($backend, 'options', []);
|
||||
$output->writeln(
|
||||
$this->filter(
|
||||
r('Has custom options? <flag>{answer}</flag>' . PHP_EOL . '{opts}', [
|
||||
'answer' => count($opts) >= 1 ? 'Yes' : 'No',
|
||||
'opts' => count($opts) >= 1 ? json_encode(
|
||||
@@ -283,7 +297,7 @@ final class ReportCommand extends Command
|
||||
$entries[] = StateEntity::fromArray($row);
|
||||
}
|
||||
|
||||
$output->writeln(
|
||||
$this->filter(
|
||||
r('Sample db entries related to backend.' . PHP_EOL . '{json}', [
|
||||
'json' => count($entries) >= 1 ? json_encode(
|
||||
$entries,
|
||||
@@ -293,7 +307,7 @@ final class ReportCommand extends Command
|
||||
);
|
||||
}
|
||||
|
||||
$output->writeln('');
|
||||
$this->filter('');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -301,33 +315,32 @@ final class ReportCommand extends Command
|
||||
/**
|
||||
* Retrieves the tasks and displays information about each task.
|
||||
*
|
||||
* @param iOutput $output An instance of the iOutput class used for displaying output.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function getTasks(iOutput $output): void
|
||||
private function getTasks(): void
|
||||
{
|
||||
foreach (Config::get('tasks.list', []) as $task) {
|
||||
$output->writeln(
|
||||
$this->filter(
|
||||
r('[ <value>{name}</value> ]' . PHP_EOL, [
|
||||
'name' => ucfirst(ag($task, 'name')),
|
||||
])
|
||||
);
|
||||
$enabled = true === (bool)ag($task, 'enabled');
|
||||
$output->writeln(
|
||||
$this->filter(
|
||||
r('Is Task enabled? <flag>{answer}</flag>', [
|
||||
'answer' => $enabled ? 'Yes' : 'No',
|
||||
])
|
||||
);
|
||||
|
||||
if (true === $enabled) {
|
||||
$output->writeln(
|
||||
$this->filter(
|
||||
r('Which flags are used to run the task? <flag>{answer}</flag>', [
|
||||
'answer' => ag($task, 'args', 'None'),
|
||||
])
|
||||
);
|
||||
|
||||
$output->writeln(
|
||||
$this->filter(
|
||||
r('When the task scheduled to run at? <flag>{answer}</flag>', [
|
||||
'answer' => ag($task, 'timer', '???'),
|
||||
])
|
||||
@@ -335,13 +348,13 @@ final class ReportCommand extends Command
|
||||
|
||||
try {
|
||||
$timer = new CronExpression(ag($task, 'timer', '5 * * * *'));
|
||||
$output->writeln(
|
||||
$this->filter(
|
||||
r('When is the next scheduled run? <flag>{answer}</flag>', [
|
||||
'answer' => gmdate(Date::ATOM, $timer->getNextRunDate()->getTimestamp()),
|
||||
])
|
||||
);
|
||||
} catch (Throwable $e) {
|
||||
$output->writeln(
|
||||
$this->filter(
|
||||
r('Next Run scheduled failed. <error>{answer}</error>', [
|
||||
'answer' => $e->getMessage(),
|
||||
])
|
||||
@@ -350,7 +363,7 @@ final class ReportCommand extends Command
|
||||
}
|
||||
|
||||
/** @noinspection DisconnectedForeachInstructionInspection */
|
||||
$output->writeln('');
|
||||
$this->filter('');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -358,9 +371,8 @@ final class ReportCommand extends Command
|
||||
* Get logs.
|
||||
*
|
||||
* @param iInput $input An instance of the iInput class used for input operations.
|
||||
* @param iOutput $output An instance of the iOutput class used for output operations.
|
||||
*/
|
||||
private function getLogs(iInput $input, iOutput $output): void
|
||||
private function getLogs(iInput $input): void
|
||||
{
|
||||
$todayAffix = makeDate()->format('Ymd');
|
||||
$yesterdayAffix = makeDate('yesterday')->format('Ymd');
|
||||
@@ -371,8 +383,8 @@ final class ReportCommand extends Command
|
||||
if (self::DEFAULT_LIMIT === $limit) {
|
||||
$linesLimit = $type === 'task' ? 75 : self::DEFAULT_LIMIT;
|
||||
}
|
||||
$this->handleLog($output, $type, $todayAffix, $linesLimit);
|
||||
$output->writeln('');
|
||||
$this->handleLog($type, $todayAffix, $linesLimit);
|
||||
$this->filter('');
|
||||
}
|
||||
|
||||
foreach (LogsCommand::getTypes() as $type) {
|
||||
@@ -380,42 +392,37 @@ final class ReportCommand extends Command
|
||||
if (self::DEFAULT_LIMIT === $limit) {
|
||||
$linesLimit = $type === 'task' ? 75 : self::DEFAULT_LIMIT;
|
||||
}
|
||||
$this->handleLog($output, $type, $yesterdayAffix, $linesLimit);
|
||||
$output->writeln('');
|
||||
$this->handleLog($type, $yesterdayAffix, $linesLimit);
|
||||
$this->filter('');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get last X lines from log file.
|
||||
*
|
||||
* @param iOutput $output An instance of the iOutput class used for displaying output.
|
||||
* @param string $type The type of the log.
|
||||
* @param string|int $date The date of the log file.
|
||||
* @param int|string $limit The maximum number of lines to display.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function handleLog(iOutput $output, string $type, string|int $date, int|string $limit): void
|
||||
private function handleLog(string $type, string|int $date, int|string $limit): void
|
||||
{
|
||||
$logFile = Config::get('tmpDir') . '/logs/' . r(
|
||||
'{type}.{date}.log',
|
||||
[
|
||||
'type' => $type,
|
||||
'date' => $date
|
||||
]
|
||||
);
|
||||
$logFile = Config::get('tmpDir') . '/logs/' . r('{type}.{date}.log', ['type' => $type, 'date' => $date]);
|
||||
|
||||
$output->writeln(r('[ <value>{logFile}</value> ]' . PHP_EOL, ['logFile' => $logFile]));
|
||||
$this->filter(r('[ <value>{logFile}</value> ]' . PHP_EOL, [
|
||||
'logFile' => after($logFile, Config::get('tmpDir'))
|
||||
]));
|
||||
|
||||
if (!file_exists($logFile) || filesize($logFile) < 1) {
|
||||
$output->writeln(r('{type} log file is empty or does not exists.', ['type' => $type]));
|
||||
$this->filter(r('{type} log file is empty or does not exists.', ['type' => $type]));
|
||||
return;
|
||||
}
|
||||
|
||||
$file = new SplFileObject($logFile, 'r');
|
||||
|
||||
if ($file->getSize() < 1) {
|
||||
$output->writeln(r('{type} log file is empty or does not exists.', ['type' => $type]));
|
||||
$this->filter(r('{type} log file is empty or does not exists.', ['type' => $type]));
|
||||
$file = null;
|
||||
return;
|
||||
}
|
||||
@@ -433,55 +440,93 @@ final class ReportCommand extends Command
|
||||
continue;
|
||||
}
|
||||
|
||||
$output->writeln($line);
|
||||
$this->filter($line);
|
||||
}
|
||||
}
|
||||
|
||||
private function printFooter(iOutput $output): void
|
||||
private function printFooter(): void
|
||||
{
|
||||
$output->writeln('<info><!-- Notice</info>');
|
||||
$output->writeln(
|
||||
$this->filter('<info><!-- Notice</info>');
|
||||
$this->filter(
|
||||
<<<FOOTER
|
||||
<value>
|
||||
Beware, while we try to make sure no sensitive information is leaked, it's possible
|
||||
that some private information might be leaked via the logs section.
|
||||
Please review the report before posting it.</value>
|
||||
Beware, while we try to make sure no sensitive information is leaked,
|
||||
it's your responsibility to check and review the report before posting it.
|
||||
</value>
|
||||
-->
|
||||
|
||||
FOOTER
|
||||
);
|
||||
}
|
||||
|
||||
private function getSuppressor(ConsoleOutput $output): void
|
||||
private function getSuppressor(): void
|
||||
{
|
||||
$suppressFile = Config::get('path') . '/config/suppress.yaml';
|
||||
|
||||
$output->writeln(
|
||||
$this->filter(
|
||||
r("Does the 'suppress.yaml' file exists? <flag>{answer}</flag>", [
|
||||
'answer' => file_exists($suppressFile) ? 'Yes' : 'No',
|
||||
])
|
||||
);
|
||||
|
||||
if (filesize($suppressFile) > 10) {
|
||||
$output->writeln('');
|
||||
$output->writeln('User defined rules:');
|
||||
$output->writeln('');
|
||||
$this->filter('');
|
||||
$this->filter('User defined rules:');
|
||||
$this->filter('');
|
||||
|
||||
try {
|
||||
$output->writeln(
|
||||
$this->filter(
|
||||
json_encode(
|
||||
Yaml::parseFile($suppressFile),
|
||||
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT
|
||||
)
|
||||
);
|
||||
} catch (Throwable $e) {
|
||||
$output->writeln(r("Error during parsing of '{file}.' '{kind}' was thrown unhandled with '{message}'", [
|
||||
$this->filter(r("Error during parsing of '{file}.' '{kind}' was thrown unhandled with '{message}'", [
|
||||
'kind' => $e::class,
|
||||
'message' => $e->getMessage(),
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
$output->writeln('');
|
||||
$this->filter('');
|
||||
}
|
||||
|
||||
private function filter(string $text): void
|
||||
{
|
||||
foreach ($this->sensitive as $sensitive) {
|
||||
$text = str_ireplace($sensitive, '**HIDDEN**', $text);
|
||||
}
|
||||
|
||||
$this->output?->writeln($text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract tokens from user configs to strip them from final report.
|
||||
*
|
||||
* @param array<UserContext> $usersContext
|
||||
*/
|
||||
private function extractSensitive(array $usersContext): void
|
||||
{
|
||||
$keys = [
|
||||
'token',
|
||||
'options.' . Options::ADMIN_TOKEN,
|
||||
'options.' . Options::PLEX_USER_PIN,
|
||||
'options.' . Options::ADMIN_PLEX_USER_PIN,
|
||||
];
|
||||
|
||||
foreach ($usersContext as $userContext) {
|
||||
foreach ($userContext->config->getAll() as $backend) {
|
||||
foreach ($keys as $key) {
|
||||
if (null === ($val = ag($backend, $key))) {
|
||||
continue;
|
||||
}
|
||||
if (true === in_array($val, $this->sensitive, true)) {
|
||||
continue;
|
||||
}
|
||||
$this->sensitive[] = $val;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user