4in1_ws_web/skin/admin.phps
2024-11-05 03:47:14 +03:00

731 lines
26 KiB
PHP

<?php
namespace skin\admin;
use PostLanguage;
use Stringable;
use function skin\base\layout;
// login page
// ----------
function login($ctx) {
$html = <<<HTML
<form action="/admin/login/" method="post" class="form-layout-h" name="admin_login">
<input type="hidden" name="token" value="{$ctx->csrf('adminlogin')}" />
<div class="form-field-wrap clearfix">
<div class="form-field-label">{$ctx->lang('admin_login')}:</div>
<div class="form-field">
<input class="form-field-input" type="text" name="login" size="50" />
</div>
</div>
<div class="form-field-wrap clearfix">
<div class="form-field-label">{$ctx->lang('admin_password')}:</div>
<div class="form-field">
<input class="form-field-input" type="password" name="password" size="50" />
</div>
</div>
<div class="form-field-wrap clearfix">
<div class="form-field-label"></div>
<div class="form-field">
<button type="submit">{$ctx->lang('sign_in')}</button>
</div>
</div>
</form>
HTML;
$js = <<<JS
document.forms.admin_login.login.focus();
JS;
return [$html, $js];
}
// index page
// ----------
function index($ctx, $admin_login) {
return <<<HTML
<div class="admin-page">
Authorized as <b>{$admin_login}</b> | <a href="/admin/logout/?token={$ctx->csrf('logout')}">Sign out</a><br>
<!--<a href="/admin/log/">Log</a><br/>-->
<a href="/admin/uploads/">Uploads</a><br>
<a href="/admin/errors/">{$ctx->lang('admin_errors')}</a><br>
<a href="/admin/auth-log/">{$ctx->lang('admin_auth_log')}</a><br>
<a href="/admin/actions-log/">{$ctx->lang('admin_actions_log')}</a><br>
</div>
HTML;
}
// uploads page
// ------------
function uploads($ctx, $uploads, $error, array $langs) {
return <<<HTML
{$ctx->if_true($error, $ctx->formError, $error)}
{$ctx->bc([
['text' => $ctx->lang('admin_title'), 'url' => '/admin/'],
['text' => $ctx->lang('blog_uploads')],
])}
<div class="blog-upload-form">
<form action="/admin/uploads/" method="post" enctype="multipart/form-data" class="form-layout-h">
<input type="hidden" name="token" value="{$ctx->csrf('addupl')}" />
<div class="form-field-wrap clearfix">
<div class="form-field-label">{$ctx->lang('blog_upload_form_file')}:</div>
<div class="form-field">
<input type="file" name="files[]" multiple>
</div>
</div>
<div class="form-field-wrap clearfix">
<div class="form-field-label">{$ctx->lang('blog_upload_form_custom_name')}:</div>
<div class="form-field">
<input type="text" name="name">
</div>
</div>
{$ctx->for_each($langs,
fn($l) => $ctx->uploads_form_note_field($l))}
<div class="form-field-wrap clearfix">
<div class="form-field-label"></div>
<div class="form-field">
<input type="submit" value="Upload">
</div>
</div>
</form>
</div>
<div class="blog-upload-list">
{$ctx->for_each($uploads, fn($u) => $ctx->uploads_item(
id: $u->id,
name: $u->name,
direct_url: $u->getDirectUrl(),
note_ru: $u->noteRu,
note_en: $u->noteEn,
markdown: $u->getMarkdown(),
size: $u->getSize(),
))}
</div>
HTML;
}
function uploads_form_note_field($ctx, PostLanguage $lang) {
$label = $ctx->lang('blog_upload_form_note');
$label .= ' ('.$lang->name.')';
return <<<HTML
<div class="form-field-wrap clearfix">
<div class="form-field-label">{$label}:</div>
<div class="form-field">
<input type="text" name="note_{$lang->value}" size="55">
</div>
</div>
HTML;
}
function uploads_item($ctx, $id, $direct_url, $note_en, $note_ru, $markdown, $name, $size) {
$fix_nl_re = '/(\r)?\n/';
$as_note_ru = jsonEncode(preg_replace($fix_nl_re, '\n', addslashes($note_ru)));
$as_note_en = jsonEncode(preg_replace($fix_nl_re, '\n', addslashes($note_en)));
return <<<HTML
<div class="blog-upload-item">
<div class="blog-upload-item-actions">
<a href="javascript:void(0)" onclick="var mdel = ge('upload{$id}_md'); mdel.style.display = (mdel.style.display === 'none' ? 'block' : 'none')">{$ctx->lang('blog_upload_show_md')}</a>
| <a href="javascript:void(0)" onclick='BlogUploadList.submitNoteEdit("/admin/uploads/edit_note/{$id}/?lang=ru&token={$ctx->csrf('editupl'.$id)}", prompt("Note (Ru):", {$as_note_ru}))'>Edit note Ru</a>
| <a href="javascript:void(0)" onclick='BlogUploadList.submitNoteEdit("/admin/uploads/edit_note/{$id}/?lang=en&token={$ctx->csrf('editupl'.$id)}", prompt("Note (En):", {$as_note_en}))'>Edit note En</a>
| <a href="/admin/uploads/delete/{$id}/?token={$ctx->csrf('delupl'.$id)}" onclick="return confirm('{$ctx->lang('blog_upload_delete_confirmation')}')">{$ctx->lang('blog_upload_delete')}</a>
</div>
<div class="blog-upload-item-name"><a href="{$direct_url}">{$name}</a></div>
<div class="blog-upload-item-info">{$size}</div>
{$ctx->if_true($note_en,
fn() => '<div class="blog-upload-item-note"><span>En</span>'.$note_en.'</div>')}
{$ctx->if_true($note_ru,
fn() => '<div class="blog-upload-item-note"><span>Ru</span>'.$note_ru.'</div>')}
<div class="blog-upload-item-md" id="upload{$id}_md" style="display: none">
<input type="text" value="{$markdown}" onclick="this.select()" readonly style="width: 100%">
</div>
</div>
HTML;
}
function postForm(\SkinContext $ctx,
string|Stringable $title,
string|Stringable $text,
string|Stringable $short_name,
string|Stringable $source_url,
string|Stringable $keywords,
array $langs,
array $js_texts,
string|Stringable|null $date = null,
bool $is_edit = false,
?bool $saved = null,
?bool $visible = null,
?bool $toc = null,
string|Stringable|null $post_url = null,
?int $post_id = null,
?string $lang = null): array {
$form_url = !$is_edit ? '/articles/write/' : $post_url.'edit/';
// breadcrumbs
$bc_tree = [
['url' => '/articles/?lang='.$lang, 'text' => $ctx->lang('articles')]
];
if ($is_edit) {
$bc_tree[] = ['url' => $post_url.'?lang='.$lang, 'text' => $ctx->lang('blog_view_post')];
} else {
$bc_tree[] = ['text' => $ctx->lang('blog_new_post')];
}
$html = <<<HTML
<div class="form-error" id="form-error" style="display:none"></div>
{$ctx->if_true($saved, fn() => '<div class="form-success">'.$ctx->lang('info_saved').'</div>')}
{$ctx->bc($bc_tree, 'padding-bottom: 12px')}
<table cellpadding="0" cellspacing="0" class="blog-write-table">
<tr>
<td id="form_first_cell">
<form class="blog-write-form form-layout-v" name="postForm" action="{$form_url}" method="post">
<div class="form-field-wrap clearfix">
<div class="form-field-label">{$ctx->lang('blog_write_form_title')}</div>
<div class="form-field">
<input class="form-field-input" type="text" name="title" value="{$title}" />
</div>
</div>
<div class="form-field-wrap clearfix">
<div class="form-field-label">{$ctx->lang('blog_write_form_text')}</div>
<div class="form-field">
<textarea class="form-field-input" name="text" wrap="soft">{$text}</textarea><br/>
<a class="blog-write-form-toggle-link" id="toggle_wrap" href="">{$ctx->lang('blog_write_form_toggle_wrap')}</a>
</div>
</div>
<div class="form-field-wrap clearfix">
<table class="blog-write-options-table">
<tr>
<td>
<div class="clearfix">
<div class="form-field-label">{$ctx->lang('blog_post_options')}</div>
<div class="form-field">
<label for="visible_cb"><input type="checkbox" id="visible_cb" name="visible"{$ctx->if_true($visible, ' checked="checked"')}> {$ctx->lang('blog_write_form_visible')}</label>
</div>
</div>
</td>
<td>
<div class="clearfix">
<div class="form-field-label">{$ctx->lang('blog_text_options')}</div>
<div class="form-field">
<label for="toc_cb"><input type="checkbox" id="toc_cb" name="toc"{$ctx->if_true($toc, ' checked="checked"')}> {$ctx->lang('blog_write_form_toc')}</label>
&nbsp;<select name="lang">
{$ctx->for_each($langs, fn($l) => '<option value="'.$l->value.'"'.($l->value == $lang ? ' selected="selected"' : '').'>'.$l->value.'</option>')}
</select>
</div>
</div>
</td>
<td>
<div class="clearfix">
<div class="form-field-label">{$ctx->lang('blog_write_form_date')}</div>
<div class="form-field">
<input type="date" name="date"{$ctx->if_true($date, ' value="'.$date.'"')}>
</div>
</div>
</td>
</tr>
<tr>
<td colspan="3">
<div class="clearfix">
<table width="100%" cellspacing="0" cellpadding="0" style="table-layout: fixed; width: 100%; border-collapse: collapse">
<tr>
<td width="50%" style="width: 50% !important;">
<div class="form-field-label">{$ctx->lang('blog_write_form_keywords')}</div>
<div class="form-field">
<input class="form-field-input" type="text" name="keywords" value="{$keywords}" />
</div>
</td>
<td width="50%" style="width: 50% !important;">
<div class="form-field-label">{$ctx->lang('blog_write_form_source')}</div>
<div class="form-field">
<input class="form-field-input" type="text" name="source_url" value="{$source_url}" />
</div>
</td>
</tr>
</table>
</div>
</td>
</tr>
<tr>
<td colspan="2">
<div class="clearfix">
<div class="form-field-label">{$ctx->lang('blog_write_form_short_name')}</div>
<div class="form-field">
<input class="form-field-input" type="text" name="{$ctx->if_then_else($is_edit, 'new_short_name', 'short_name')}" value="{$short_name}" />
</div>
</div>
</td>
<td>
<div class="form-field-label">&nbsp;</div>
<div class="form-field">
<button type="submit" name="submit_btn"><b>{$ctx->lang($is_edit ? 'save' : 'blog_write_form_submit_btn')}</b></button>
</div>
</td>
</tr>
</table>
</div>
</form>
<div id="form_placeholder"></div>
</td>
<td>
<div class="blog-write-form-preview post_text" id="preview_html"></div>
</td>
</tr>
</table>
HTML;
$js_params = [
'langs' => array_map(fn($lang) => $lang->value, $langs),
'token' => $is_edit ? csrf_get('editpost'.$post_id) : csrf_get('post_add')
];
if ($is_edit)
$js_params += [
'edit' => true,
'id' => $post_id,
'texts' => $js_texts
];
$js_params = jsonEncode($js_params);
$js = <<<JAVASCRIPT
cur.form = new AdminWriteEditForm({$js_params});
JAVASCRIPT;
return [$html, $js];
}
function pageForm($ctx,
string|Stringable $title,
string|Stringable $text,
string|Stringable $short_name,
array $langs,
bool $is_edit = false,
?bool $saved = null,
bool $visible = false,
bool $render_title = false,
null|string|Stringable $parent = null,
?array $js_text = null): array {
$form_url = '/'.$short_name.'/'.($is_edit ? 'edit' : 'create').'/';
// breadcrumbs
if ($is_edit) {
$bc_html = $ctx->bc([
['url' => '/'.$short_name.'/', 'text' => $ctx->lang('view_page')]
], 'padding-bottom: 12px');
} else {
$bc_html = '';
}
$html = <<<HTML
<div class="form-error" id="form-error" style="display:none"></div>
{$ctx->if_true($saved, fn() => '<div class="form-success">'.$ctx->lang('info_saved').'</div>')}
{$bc_html}
<table cellpadding="0" cellspacing="0" class="blog-write-table">
<tr>
<td id="form_first_cell">
<form class="blog-write-form form-layout-v" name="pageForm" action="{$form_url}" method="post">
<div class="form-field-wrap clearfix">
<div class="form-field-label">{$ctx->lang('pages_write_form_title')}</div>
<div class="form-field">
<input class="form-field-input" type="text" name="title" value="{$title}" />
</div>
</div>
<div class="form-field-wrap clearfix">
<div class="form-field-label">{$ctx->lang('pages_write_form_text')}</div>
<div class="form-field">
<textarea class="form-field-input" name="text" wrap="soft">{$text}</textarea><br/>
<a class="blog-write-form-toggle-link" id="toggle_wrap" href="">{$ctx->lang('pages_write_form_toggle_wrap')}</a>
</div>
</div>
{$ctx->if_then_else($is_edit,
fn() => $ctx->pageFormEditOptions($short_name, $parent, $visible, $render_title),
fn() => $ctx->pageFormAddOptions($short_name))}
</form>
<div id="form_placeholder"></div>
</td>
<td>
<div class="blog-write-form-preview post_text" id="preview_html"></div>
</td>
</tr>
</table>
HTML;
$js_params = [
'pages' => true,
'edit' => $is_edit,
'token' => $is_edit ? $ctx->csrf('editpage'.$short_name) : $ctx->csrf('addpage'),
'langs' => array_map(fn($lang) => $lang->value, $langs), // still needed for draft erasing
];
if ($js_text !== null)
$js_params['text'] = $js_text;
$js_params = jsonEncode($js_params);
$js = <<<JS
cur.form = new AdminWriteEditForm({$js_params});
JS;
return [$html, $js];
}
function pageFormEditOptions($ctx, $short_name, $parent, $visible, $render_title) {
return <<<HTML
<div class="form-field-wrap clearfix">
<table class="blog-write-options-table">
<tr>
<td width="40%">
<div class="clearfix">
<div class="form-field-label">{$ctx->lang('pages_write_form_short_name')}</div>
<div class="form-field">
<input class="form-field-input" type="text" name="new_short_name" value="{$short_name}" />
</div>
</div>
</td>
<td width="30%">
<div class="clearfix">
<div class="form-field-label">{$ctx->lang('pages_write_form_parent')}</div>
<div class="form-field">
<input class="form-field-input" type="text" name="parent" value="{$parent}" />
</div>
</div>
</td>
<td width="30%">
<div class="clearfix">
<div class="form-field-label">{$ctx->lang('pages_write_form_options')}</div>
<div class="form-field">
<label for="visible_cb"><input type="checkbox" id="visible_cb" name="visible"{$ctx->if_true($visible, ' checked="checked"')}> {$ctx->lang('pages_write_form_visible')}</label>
<label for="render_title_cb"><input type="checkbox" id="render_title_cb" name="render_title"{$ctx->if_true($render_title, ' checked="checked"')}> {$ctx->lang('pages_write_form_render_title')}</label>
</div>
</div>
</td>
</tr>
<tr>
<td rowspan="3">
<button type="submit" name="submit_btn"><b>{$ctx->lang('pages_write_form_submit_btn')}</b></button>
</td>
</tr>
</table>
</div>
HTML;
}
// TODO: add visible and reader_title checkbox here
function pageFormAddOptions($ctx, $short_name) {
return <<<HTML
<div class="form-field-wrap clearfix">
<div class="form-field-label"></div>
<div class="form-field">
<button type="submit" name="submit_btn"><b>{$ctx->lang('pages_write_form_submit_btn')}</b></button>
</div>
</div>
<input name="short_name" value="{$short_name}" type="hidden" />
HTML;
}
function pageNew($ctx, $short_name) {
return <<<HTML
<div class="page">
<div class="empty">
<a href="/{$short_name}/create/">{$ctx->lang('pages_create')}</a>
</div>
</div>
HTML;
}
// misc
function formError($ctx, $error) {
return <<<HTML
<div class="form-error">{$ctx->lang('error')}: {$error}</div>
HTML;
}
function markdownPreview($ctx, $unsafe_html, $title) {
return <<<HTML
<div class="blog-post" id="blog_post">
{$ctx->if_true($title, '<div class="blog-post-title"><h1>'.$title.'</h1></div>')}
<div class="blog-post-text">{$unsafe_html}</div>
</div>
HTML;
}
// ----------------------------------------------------
// ---------------------- ERRORS ----------------------
// ----------------------------------------------------
function errors($ctx,
array $list,
int $count,
int $pn_page,
int $pn_pages,
string $url,
?string $ip_filter = null,
?int $ip = null,
?string $query = null,
?string $url_query = null,
?string $file_query = null,
?string $line_query = null) {
return <<<HTML
{$ctx->bc([
['text' => $ctx->lang('admin_title'), 'url' => '/admin/'],
['text' => $ctx->lang('admin_errors')],
])}
<form action="/admin/errors/" method="get" class="admin_common_query_form">
{$ctx->if_true($ip, fn() => '<input type="hidden" name="ip" value="'.$ip.'" />')}
<input type="text" name="query" placeholder="text_like" value="{$query}" />
<input type="text" name="url_query" placeholder="url_like" value="{$url_query}" />
<input type="text" name="file_query" placeholder="file" value="{$file_query}" />
<input type="text" name="line_query" placeholder="line" value="{$line_query}" style="width: 50px" />
<input class="blue" type="submit" value="query" />
</form>
{$ctx->if_then_else(!empty($list),
fn() => $ctx->errors_table($list),
fn() => '<div class="empty_block">Error log is empty.</div>')}
{$ctx->pagenav($pn_page, $pn_pages, $url.'page={page}')}
HTML;
}
function errors_table($ctx,
array $list) {
return <<<HTML
<table border="1" width="100%" cellpadding="0" cellspacing="0" class="admin-error-log">
<thead>
<tr>
<th width="5%">Time</th>
<th width="20%">Source</th>
<th>Error</th>
</tr>
</thead>
<tbody>
{$ctx->for_each($list, fn($item) => $ctx->errors_table_item(
date: $item['date'],
is_cli: (bool)$item['is_cli'],
is_custom: (bool)$item['custom'],
user_agent: $item['ua'],
ip: (int)$item['ip'],
ip_s: $item['ip_s'],
full_url: $item['full_url'],
url: $item['url'],
file: $item['file'],
line: (int)$item['line'],
admin_id: (int)$item['admin_id'],
nl2br_text: $item['text'],
num: (int)$item['num'],
errtype: $item['errtype'] ?? null,
time: $item['time'],
stacktrace: $item['stacktrace'],
item_id: (int)$item['id']
))}
</tbody>
</table>
HTML;
}
function errors_table_item($ctx,
int $item_id,
string $date,
bool $is_cli,
bool $is_custom,
string $user_agent,
int $ip,
string $ip_s,
string $full_url,
string $url,
string $file,
int $line,
int $admin_id,
string $nl2br_text,
int $num,
?string $errtype,
string $time,
string $stacktrace) {
return <<<HTML
<tr>
<td>
{$date}
</td>
<td>
{$ctx->if_then_else(!$is_cli,
fn() => '<span class="admin-error-log-num"><a href="/admin/errors/?ip='.$ip.'">'.$ip_s.'</a></span> <a class="admin-error-log-link" href="'.$full_url.'">'.$url.'</a><br/>'.$user_agent,
fn() => '<span class="admin-error-log-num">cmd</span>')}
</td>
<td class="admin_error_log_ms">
{$ctx->if_true($admin_id,
fn() => '<span class="admin-error-log-num">admin='.$admin_id.'</span>')}
{$ctx->if_then_else($is_custom,
fn() => '<span class="admin-error-log-num">'.$num.', '.$time.'</span> <b>'.$file.'</b>:'.$line.'<br/>'
.'<span class="admin-error-log-num">'.$errtype.'</span> '.$nl2br_text,
fn() => '<span class="admin-error-log-num">'.$num.', '.$time.'</span> '.$nl2br_text)}
{$ctx->if_true($stacktrace,
fn() => $ctx->errors_table_item_stacktrace($item_id, $stacktrace))}
</td>
</tr>
HTML;
}
function errors_table_item_stacktrace($ctx, $item_id, $nl2br_stacktrace) {
return <<<HTML
<div class="admin-error-log-stacktrace-wrap">
<a href="javascript:void(0)" onclick="toggle(ge('admin_error_log_stacktrace{$item_id}'))">Show/hide stacktrace</a>
<div id="admin_error_log_stacktrace{$item_id}" style="display: none">{$nl2br_stacktrace}</div>
</div>
HTML;
}
// ------------------------------------------------------
// ---------------------- AUTH LOG ----------------------
// ------------------------------------------------------
function auth_log($ctx, array $list, int $pn_page, int $pn_pages) {
return <<<HTML
{$ctx->bc([
['text' => $ctx->lang('admin_title'), 'url' => '/admin/'],
['text' => $ctx->lang('admin_auth_log')],
])}
{$ctx->if_then_else(!empty($list),
fn() => $ctx->auth_log_table($list),
fn() => '<div class="empty_block">Auth log is empty.</div>')}
{$ctx->pagenav($pn_page, $pn_pages, '/admin/auth-log/?page={page}')}
HTML;
}
function auth_log_table($ctx, array $list) {
return <<<HTML
<table border="1" width="100%" cellpadding="0" cellspacing="0" class="admin-error-log">
<thead>
<tr>
<th width="20%">Admin</th>
<th width="15%">Time</th>
<th width="10%">IP</th>
<th width="55%">User-Agent</th>
</tr>
</thead>
<tbody>
{$ctx->for_each($list,
fn($item) => $ctx->auth_log_table_item(
date: $item['date'],
ip: $item['ip'],
user_agent: $item['ua'],
admin_login: $item['login'],
admin_id: (int)$item['admin_id'],
activity_ts: $item['activity_ts_s']))}
</tbody>
</table>
HTML;
}
function auth_log_table_item($ctx, $date, $ip, $user_agent, string $admin_login, int $admin_id, string $activity_ts) {
return <<<HTML
<tr>
<td>
<span title="Last activity: {$activity_ts}">{$admin_login}</span> (id={$admin_id})
</td>
<td>{$date}</td>
<td>{$ip}</td>
<td>{$user_agent}</td>
</tr>
HTML;
}
// ---------------------------------------------------------
// ---------------------- ACTIONS LOG ----------------------
// ---------------------------------------------------------
function actions_log($ctx,
array $list,
array $admin_logins,
string $url,
array $action_types,
int $pn_page,
int $pn_pages) {
return <<<HTML
{$ctx->bc([
['text' => $ctx->lang('admin_title'), 'url' => '/admin/'],
['text' => $ctx->lang('admin_actions_log')],
])}
{$ctx->if_then_else(!empty($list),
fn() => $ctx->actions_log_table($list, $admin_logins, $action_types),
fn() => '<div class="empty_block">Actions log is empty.</div>')}
{$ctx->pagenav($pn_page, $pn_pages, $url.'page={page}')}
HTML;
}
function actions_log_table($ctx, array $list, array $admin_logins, array $action_types) {
return <<<HTML
<table border="1" width="100%" cellpadding="0" cellspacing="0" class="admin-error-log">
<thead>
<tr>
<th width="9%">Time</th>
<th width="14%">Who</th>
<th width="11%">Action</th>
<th>Data</th>
</tr>
</thead>
<tbody>
{$ctx->for_each($list,
fn(\AdminActions\BaseAction $item) => $ctx->actions_log_table_item(
date: $item->getDate(),
ip: $item->getIPv4(),
is_cli: $item->isCommandLineAction(),
admin_login: $admin_logins[$item->getAdminId()],
action_name: $item->getActionName(),
unsafe_data: $item->renderHtml()))}
</tbody>
</table>
HTML;
}
function actions_log_table_item($ctx,
string $date,
string $ip,
bool $is_cli,
string $admin_login,
string $action_name,
string $unsafe_data) {
return <<<HTML
<tr>
<td>{$date}</td>
<td>
{$ctx->if_then_else(!$is_cli,
fn() => $admin_login.', '.$ip,
fn() => 'console')}
</td>
<td>{$action_name}</td>
<td>{$unsafe_data}</td>
</tr>
HTML;
}