<?php

/**
 * @package Dotclear
 * @subpackage Backend
 *
 * @copyright Olivier Meunier & Association Dotclear
 * @copyright AGPL-3.0
 */
declare(strict_types=1);

namespace Dotclear\Process\Backend;

use ArrayObject;
use Dotclear\App;
use Dotclear\Helper\Date;
use Dotclear\Helper\File\File;
use Dotclear\Helper\Html\Form\Dd;
use Dotclear\Helper\Html\Form\Div;
use Dotclear\Helper\Html\Form\Dl;
use Dotclear\Helper\Html\Form\Dt;
use Dotclear\Helper\Html\Form\Img;
use Dotclear\Helper\Html\Form\Link;
use Dotclear\Helper\Html\Form\None;
use Dotclear\Helper\Html\Form\Note;
use Dotclear\Helper\Html\Form\Para;
use Dotclear\Helper\Html\Form\Set;
use Dotclear\Helper\Html\Form\Strong;
use Dotclear\Helper\Html\Form\Text;
use Dotclear\Helper\Html\Html;
use Dotclear\Helper\Html\XmlTag;
use Dotclear\Helper\Network\Feed\Reader;
use Dotclear\Helper\Network\Http;
use Dotclear\Helper\Process\TraitProcess;
use Dotclear\Helper\Text as TextHelper;
use Dotclear\Interface\Module\ModulesInterface;
use Dotclear\Module\Store;
use Dotclear\Plugin\antispam\Antispam;
use Exception;
use stdClass;

/**
 * @since 2.27 Before as admin/services.php
 */
class Rest
{
    use TraitProcess;

    public static function init(): bool
    {
        App::rest()->addFunction('getPostsCount', self::getPostsCount(...));
        App::rest()->addFunction('getCommentsCount', self::getCommentsCount(...));
        App::rest()->addFunction('checkNewsUpdate', self::checkNewsUpdate(...));
        App::rest()->addFunction('checkCoreUpdate', self::checkCoreUpdate(...));
        App::rest()->addFunction('checkStoreUpdate', self::checkStoreUpdate(...));
        App::rest()->addFunction('getPostById', self::getPostById(...));
        App::rest()->addFunction('getCommentById', self::getCommentById(...));
        App::rest()->addFunction('quickPost', self::quickPost(...));
        App::rest()->addFunction('getZipMediaContent', self::getZipMediaContent(...));
        App::rest()->addFunction('getMeta', self::getMeta(...));
        App::rest()->addFunction('delMeta', self::delMeta(...));
        App::rest()->addFunction('setPostMeta', self::setPostMeta(...));
        App::rest()->addFunction('searchMeta', self::searchMeta(...));
        App::rest()->addFunction('searchMetadata', self::searchMetadata(...));
        App::rest()->addFunction('setSectionFold', self::setSectionFold(...));
        App::rest()->addFunction('setDashboardPositions', self::setDashboardPositions(...));
        App::rest()->addFunction('setListsOptions', self::setListsOptions(...));

        App::rest()->addFunction('webAuthnRegistration', self::webAuthnRegistration(...));
        App::rest()->addFunction('webAuthnAuthentication', self::webAuthnAuthentication(...));

        return self::status(true);
    }

    public static function process(): bool
    {
        if (App::rest()->serveRestRequests()) {
            App::rest()->serve();
        }

        return true;
    }

    // REST Methods below
    // ------------------

    /**
     * REST method to get the posts count (JSON).
     *
     * @return     array<string, string>  The posts count.
     */
    public static function getPostsCount(): array
    {
        $count = App::blog()->getPosts([], true)->cardinal();

        return [
            'ret' => sprintf(__('%d post', '%d posts', $count), $count),
        ];
    }

    /**
     * REST method to get the comments count (JSON).
     *
     * @return     array<string, string>  The comments count.
     */
    public static function getCommentsCount(): array
    {
        $count = App::blog()->getComments([], true)->cardinal();

        return [
            'ret' => sprintf(__('%d comment', '%d comments', $count), $count),
        ];
    }

    /**
     * REST method to check Dotclear news (JSON).
     *
     * @throws     Exception
     *
     * @return     array<string, mixed>    returned data
     */
    public static function checkNewsUpdate(): array
    {
        # Dotclear news

        $data = [
            'check' => false,
            'ret'   => __('Dotclear news not available.'),
        ];

        if (App::auth()->prefs()->get('dashboard')->getBool('dcnews')) {
            try {
                if ('' === $rss_news = App::backend()->resources()->entry('rss_news', 'Dotclear')) {
                    throw new Exception();
                }

                $feed_reader = new Reader();
                $feed_reader->setCacheDir(App::config()->cacheRoot());
                $feed_reader->setTimeout(2);
                $feed_reader->setUserAgent('Dotclear - https://dotclear.org/');
                $feed = $feed_reader->parse($rss_news);
                if ($feed) {
                    $news = function () use ($feed) {
                        $i = 1;
                        foreach ($feed->items as $item) {
                            $title = isset($item->title) && is_string($title = $item->title) ? $title : '';
                            if ($title !== '') {
                                $link    = isset($item->link)    && is_string($link = $item->link) ? $link : '';
                                $pubdate = isset($item->pubdate) && is_string($pubdate = $item->pubdate) ? $pubdate : '';
                                $content = isset($item->content) && is_string($content = $item->content) ? $content : '';
                                yield (new Set())
                                    ->items([
                                        (new Dt())
                                            ->items([
                                                $link !== '' ?
                                                (new Link())
                                                    ->separator(' ')
                                                    ->class('outgoing')
                                                    ->title($title)
                                                    ->href($link)
                                                    ->items([
                                                        (new Text(null, $title)),
                                                        (new Img('images/outgoing-link.svg'))
                                                            ->alt(''),
                                                    ]) :
                                                (new Text(null, $title)),
                                            ]),
                                        (new Dd())
                                            ->items([
                                                (new Para())
                                                    ->separator(' ')
                                                    ->items([
                                                        (new Strong(Date::dt2str(__('%d %B %Y:'), $pubdate, 'Europe/Paris'))),
                                                        (new Text('em', TextHelper::cutString(Html::clean($content), 120) . '...')),
                                                    ]),
                                            ]),
                                    ]);
                                $i++;
                                if ($i > 2) {
                                    break;
                                }
                            }
                        }
                    };
                    $ret = (new Div('ajax-news'))
                        ->class(['box', 'medium', 'dc-box'])
                        ->items([
                            (new Text('h3', __('Dotclear news'))),
                            (new Dl('news'))
                                ->items([
                                    ... $news(),
                                ]),
                        ])
                    ->render();

                    $data = [
                        'check' => true,
                        'ret'   => $ret,
                    ];
                }
            } catch (Exception) {
                // Ignore exceptions
            }
        }

        return $data;
    }

    /**
     * REST method to check Dotclear update (JSON).
     *
     * @return     array<string, mixed>    returned data
     */
    public static function checkCoreUpdate(): array
    {
        // Dotclear updates notifications

        $data = [
            'check' => false,
            'ret'   => __('Dotclear update not available.'),
        ];

        if (App::auth()->isSuperAdmin()
            && !App::config()->coreNotUpdate()
            && is_readable(App::config()->digestsRoot())
            && !App::auth()->prefs()->get('dashboard')->getBool('nodcupdate')
        ) {
            App::task()->addContext('UPGRADE');
            $new_v        = App::upgrade()->update()->check(App::config()->dotclearVersion(), false);
            $version_info = $new_v ? App::upgrade()->update()->getInfoURL() : '';

            if (App::upgrade()->update()->getNotify() && $new_v) {
                // Check PHP version required
                if (version_compare(phpversion(), (string) App::upgrade()->update()->getPHPVersion()) >= 0) {
                    $ret = (new Div('ajax-update'))
                        ->class('dc-update')
                        ->items([
                            (new Text('h3', sprintf(__('Dotclear %s is available!'), $new_v))),
                            (new Para())
                                ->class('form-buttons')
                                ->items([
                                    (new Link())
                                        ->class(['button', 'submit'])
                                        ->href(App::backend()->url()->get('upgrade.upgrade'))
                                        ->text(__('Upgrade now')),
                                    (new Link())
                                        ->class(['button', 'reset'])
                                        ->href(App::backend()->url()->get('upgrade.upgrade', ['hide_msg' => 1]))
                                        ->text(__('Remind me later')),
                                ]),
                            $version_info ?
                                (new Para())
                                    ->class('updt-info')
                                    ->items([
                                        (new Link())
                                            ->href($version_info)
                                            ->text(__('Information about this version')),
                                    ]) :
                                (new None()),
                        ])
                    ->render();
                } else {
                    $ret = (new Note())
                        ->class('info')
                        ->text(sprintf(
                            __('A new version of Dotclear is available but needs PHP version ≥ %1$s, your\'s is currently %2$s.'),
                            App::upgrade()->update()->getPHPVersion(),
                            phpversion()
                        ))
                    ->render();
                }

                $data = [
                    'check' => true,
                    'ret'   => $ret,
                ];
            } elseif (version_compare(phpversion(), App::config()->nextRequiredPhp(), '<')) {
                if (!App::auth()->prefs()->get('interface')->getBool('hidemoreinfo')) {
                    $ret = (new Note())
                        ->class('info')
                        ->text(sprintf(
                            __('The next versions of Dotclear will not support PHP version < %1$s, your\'s is currently %2$s.'),
                            App::config()->nextRequiredPhp(),
                            phpversion()
                        ))
                    ->render();
                    $data = [
                        'check' => true,
                        'ret'   => $ret,
                    ];
                }
            }
        }

        return $data;
    }

    /**
     * REST method to check store (JSON)
     *
     * @param      array<string, string>     $get    The get
     * @param      array<string, string>     $post   The post
     *
     * @throws     Exception
     *
     * @return     array<string, mixed>    returned data
     */
    public static function checkStoreUpdate(array $get, array $post): array
    {
        # Dotclear store updates notifications

        $data = [
            'ret'   => __('No updates are available.'),
            'new'   => false,
            'check' => false,
            'nb'    => 0,
        ];

        if (empty($post['store'])) {
            throw new Exception('No store type');
        }

        if ($post['store'] === 'themes') {
            // load once themes
            if (App::themes()->isEmpty() && App::blog()->isDefined()) {
                App::themes()->loadModules(App::blog()->themesPath(), 'admin', App::lang()->getLang());
            }

            $mod = App::themes();
            $url = App::config()->storeThemeUrl();
        } elseif ($post['store'] === 'plugins') {
            $mod = App::plugins();
            $url = App::config()->storePluginUrl();
        } else {
            $param = new ArrayObject([
                'mod' => [],
                'url' => null,
            ]);
            # --BEHAVIOR-- restCheckStoreUpdate -- string, ArrayObject{mod:ModulesInterface[], url:string}
            App::behavior()->callBehavior('restCheckStoreUpdateV2', $post['store'], $param);

            [$mod, $url] = $param;
            if ($mod === [] || $url === null) {
                throw new Exception('Unknown store type');
            }
        }

        if (!($mod instanceof ModulesInterface)) {
            throw new Exception('Unknown store type');
        }

        $repo = new Store($mod, $url, false, false, false);
        $upd  = $repo->getDefines(true);

        $tmp = new ArrayObject($upd);

        # --BEHAVIOR-- afterCheckStoreUpdate -- string, ArrayObject<int, ModuleDefine>
        App::behavior()->callBehavior('afterCheckStoreUpdate', $post['store'], $tmp);

        $upd = $tmp->getArrayCopy();

        if (!empty($upd)) {
            return [
                'ret'   => sprintf(__('An update is available.', '%s updates are available.', count($upd)), count($upd)),
                'new'   => $repo->hasNewUdpates(),
                'check' => true,
                'nb'    => count($upd),
            ];
        }

        return $data;
    }

    /**
     * REST method to get post by ID (JSON)
     *
     * @param      array<string, string>     $get    The get
     *
     * @throws     Exception
     *
     * @return     array<string, mixed>    returned data
     */
    public static function getPostById(array $get): array
    {
        if (empty($get['id'])) {
            throw new Exception('No post ID');
        }

        $params = ['post_id' => (int) $get['id']];

        if (isset($get['post_type'])) {
            $params['post_type'] = $get['post_type'];
        }

        $rs = App::blog()->getPosts($params);

        if ($rs->isEmpty()) {
            throw new Exception('No post for this ID');
        }

        $metadata  = [];
        $post_meta = $rs->strField('post_meta');
        if ($post_meta !== '') {
            $meta = @unserialize($post_meta);
            if (is_array($meta)) {
                foreach ($meta as $type => $values) {
                    if (is_array($values)) {
                        foreach ($values as $value) {
                            $metadata[$type][] = $value;
                        }
                    }
                }
            }
        }

        return [
            'id' => $rs->intField('post_id'),

            'blog_id'            => $rs->strField('blog_id', true),
            'user_id'            => $rs->strField('user_id', true),
            'cat_id'             => $rs->intField('cat_id', true),
            'post_dt'            => $rs->strField('post_dt', true),
            'post_creadt'        => $rs->strField('post_creadt', true),
            'post_upddt'         => $rs->strField('post_upddt', true),
            'post_format'        => $rs->strField('post_format', true),
            'post_url'           => $rs->strField('post_url', true),
            'post_lang'          => $rs->strField('post_lang', true),
            'post_title'         => $rs->strField('post_title', true),
            'post_excerpt'       => $rs->strField('post_excerpt', true),
            'post_excerpt_xhtml' => $rs->strField('post_excerpt_xhtml', true),
            'post_content'       => $rs->strField('post_content', true),
            'post_content_xhtml' => $rs->strField('post_content_xhtml', true),
            'post_notes'         => $rs->strField('post_notes', true),
            'post_status'        => $rs->intField('post_status', true),
            'post_selected'      => $rs->boolField('post_selected'),
            'post_open_comment'  => $rs->boolField('post_open_comment'),
            'post_open_tb'       => $rs->boolField('post_open_tb'),
            'nb_comment'         => $rs->intField('nb_comment'),
            'nb_trackback'       => $rs->intField('nb_trackback'),
            'user_name'          => $rs->strField('user_name', true),
            'user_firstname'     => $rs->strField('user_firstname', true),
            'user_displayname'   => $rs->strField('user_displayname', true),
            'user_email'         => $rs->strField('user_email', true),
            'user_url'           => $rs->strField('user_url', true),
            'cat_title'          => $rs->strField('cat_title', true),
            'cat_url'            => $rs->strField('cat_url', true),

            'post_display_content' => $rs->getContent(true),
            'post_display_excerpt' => $rs->getExcerpt(true),

            'post_meta' => $metadata,
        ];
    }

    /**
     * REST method to get comment by ID (JSON)
     *
     * @param      array<string, string>     $get    The get
     *
     * @throws     Exception
     *
     * @return     array<string, mixed>    returned data
     */
    public static function getCommentById(array $get): array
    {
        if (empty($get['id'])) {
            throw new Exception('No comment ID');
        }

        $rs = App::blog()->getComments(['comment_id' => (int) $get['id']]);

        if ($rs->isEmpty()) {
            throw new Exception('No comment for this ID');
        }

        return [
            'id' => $rs->intField('comment_id'),

            'comment_dt'        => $rs->strField('comment_dt', true),
            'comment_upddt'     => $rs->strField('comment_upddt', true),
            'comment_author'    => $rs->strField('comment_author', true),
            'comment_site'      => $rs->strField('comment_site', true),
            'comment_content'   => $rs->strField('comment_content', true),
            'comment_trackback' => $rs->boolField('comment_trackback'),
            'comment_status'    => $rs->intField('comment_status'),
            'post_title'        => $rs->strField('post_title', true),
            'post_url'          => $rs->strField('post_url', true),
            'post_id'           => $rs->intField('post_id', true),
            'post_dt'           => $rs->strField('post_dt', true),
            'user_id'           => $rs->strField('user_id', true),

            'comment_display_content' => $rs->getContent(true),

            'comment_ip'        => App::auth()->userID() ? $rs->strField('comment_ip', true) : '',
            'comment_email'     => App::auth()->userID() ? $rs->strField('comment_email', true) : '',
            'comment_spam_disp' => App::auth()->userID() ? Antispam::statusMessage($rs) : '',
        ];
    }

    /**
     * REST method to create a quick post (JSON)
     *
     * @param      array<string, string>     $get    The get
     * @param      array<string, string>     $post   The post
     *
     * @return     array<string, mixed>    returned data
     */
    public static function quickPost(array $get, array $post): array
    {
        # Create category
        if (!empty($post['new_cat_title']) && App::auth()->check(App::auth()->makePermissions([
            App::auth()::PERMISSION_CATEGORIES,
        ]), App::blog()->id())) {
            $cur_cat = App::blog()->categories()->openCategoryCursor();
            $cur_cat->setStrField('cat_title', $post['new_cat_title']);
            $cur_cat->setStrField('cat_url', '');

            $parent_cat = $post['new_cat_parent'] ?? '';

            # --BEHAVIOR-- adminBeforeCategoryCreate -- Cursor
            App::behavior()->callBehavior('adminBeforeCategoryCreate', $cur_cat);

            $post['cat_id'] = App::blog()->addCategory($cur_cat, (int) $parent_cat);

            # --BEHAVIOR-- adminAfterCategoryCreate -- Cursor, int
            App::behavior()->callBehavior('adminAfterCategoryCreate', $cur_cat, $post['cat_id']);
        }

        // Variable data helpers
        $_Str = fn (mixed $var, string $default = ''): string => $var !== null && is_string($val = $var) ? $val : $default;
        $_Int = fn (mixed $var, int $default = 0): int => $var !== null && is_numeric($val = $var) ? (int) $val : $default;

        $cur = App::blog()->openPostCursor();

        $cur->setStrField('post_title', $_Str($post['post_title']));
        $cur->setStrField('user_id', App::auth()->userID());
        $cur->setStrField('post_content', $_Str($post['post_content']));
        $cur->setStrField('post_format', $_Str($post['post_format'], 'xhtml'));
        $cur->setStrField('post_lang', $_Str($post['post_lang']));
        $cur->setIntField('post_status', $_Int($post['post_status'], App::status()->post()::UNPUBLISHED));
        $cur->setBoolField('post_open_comment', App::blog()->settings()->get('system')->getBool('allow_comments', false));
        $cur->setBoolField('post_open_tb', App::blog()->settings()->get('system')->getBool('allow_trackbacks', false));

        if (isset($post['cat_id']) && $post['cat_id'] !== '') {
            $cur->setIntField('cat_id', (int) $post['cat_id']);
        }

        # --BEHAVIOR-- adminBeforePostCreate -- Cursor
        App::behavior()->callBehavior('adminBeforePostCreate', $cur);

        $return_id = App::blog()->addPost($cur);

        # --BEHAVIOR-- adminAfterPostCreate -- Cursor, int
        App::behavior()->callBehavior('adminAfterPostCreate', $cur, $return_id);

        $post = App::blog()->getPosts(['post_id' => $return_id]);

        return [
            'id'     => $return_id,
            'status' => $post->intField('post_status'),
            'url'    => $post->getURL(),
        ];
    }

    /**
     * REST method to get Zip content list (JSON)
     *
     * @param      array<string, string>     $get    The get
     *
     * @throws     Exception
     *
     * @return     list<string>    returned data
     */
    public static function getZipMediaContent(array $get): array
    {
        if (empty($get['id'])) {
            throw new Exception('No media ID');
        }

        $id = (int) $get['id'];

        if (!App::auth()->check(App::auth()->makePermissions([
            App::auth()::PERMISSION_MEDIA,
            App::auth()::PERMISSION_MEDIA_ADMIN,
        ]), App::blog()->id())) {
            throw new Exception('Permission denied');
        }

        $file = null;

        try {
            $file = App::media()->getFile($id);
        } catch (Exception) {
            // Ignore exceptions
        }

        if (!$file instanceof File || $file->type != 'application/zip' || !$file->editable) {
            throw new Exception('Not a valid file');
        }

        return array_keys(App::media()->getZipContent($file));
    }

    /**
     * REST method to get metadata (JSON)
     *
     * @param      array<string, string>     $get    The get
     *
     * @return     array<int, array<string, mixed>>    returned data
     */
    public static function getMeta(array $get): array
    {
        $postid   = $get['postId']   ?? null;
        $limit    = $get['limit']    ?? null;
        $metaId   = $get['metaId']   ?? null;
        $metaType = $get['metaType'] ?? null;

        $sortby = $get['sortby'] ?? 'meta_type,asc';

        $rs = App::meta()->getMetadata([
            'meta_type' => $metaType,
            'limit'     => $limit,
            'meta_id'   => $metaId,
            'post_id'   => $postid, ]);
        $rs = App::meta()->computeMetaStats($rs);

        $sortby = explode(',', $sortby);
        $sort   = $sortby[0];
        $order  = $sortby[1] ?? 'asc';

        $sort = match ($sort) {
            'metaId'   => 'meta_id_lower',
            'count'    => 'count',
            'metaType' => 'meta_type',
            default    => 'meta_type',
        };

        $rs->sort($sort, $order);

        $data = [];
        while ($rs->fetch()) {
            $meta_id = $rs->strField('meta_id');
            if ($meta_id !== '') {
                $data[] = [
                    'meta_id'      => $meta_id,
                    'type'         => $rs->strField('meta_type', true),
                    'uri'          => rawurlencode($meta_id),
                    'count'        => $rs->intField('count', true),
                    'percent'      => $rs->intField('percent', true),
                    'roundpercent' => $rs->intField('roundpercent', true),
                ];
            }
        }

        return $data;
    }

    /**
     * REST method to set post metadata (JSON)
     *
     * @param      array<string, string>     $get    The get
     * @param      array<string, string>     $post   The post
     *
     * @return     true
     */
    public static function setPostMeta(array $get, array $post): bool
    {
        if (empty($post['postId'])) {
            throw new Exception('No post ID');
        }

        if (empty($post['meta']) && $post['meta'] !== '0') {
            throw new Exception('No meta');
        }

        if (empty($post['metaType'])) {
            throw new Exception('No meta type');
        }

        # Get previous meta for post
        $post_meta = App::meta()->getMetadata([
            'meta_type' => $post['metaType'],
            'post_id'   => $post['postId'], ]);
        $pm = [];
        while ($post_meta->fetch()) {
            $pm[] = $post_meta->strField('meta_id');
        }

        foreach (App::meta()->splitMetaValues($post['meta']) as $m) {
            if (!in_array($m, $pm)) {
                App::meta()->setPostMeta($post['postId'], $post['metaType'], $m);
            }
        }

        return true;
    }

    /**
     * REST method to get metadata (JSON)
     *
     * @param      array<string, string>     $get    The get
     * @param      array<string, string>     $post   The post
     *
     * @return     true
     */
    public static function delMeta(array $get, array $post): bool
    {
        if (empty($post['postId'])) {
            throw new Exception('No post ID');
        }

        if (empty($post['metaId']) && $post['metaId'] !== '0') {
            throw new Exception('No meta ID');
        }

        if (empty($post['metaType'])) {
            throw new Exception('No meta type');
        }

        App::meta()->delPostMeta($post['postId'], $post['metaType'], $post['metaId']);

        return true;
    }

    /**
     * REST method to search metadata (XML)
     *
     * Used with jquery.autocomplete()
     *
     * @param      mixed                    $unused     Was dcCore instance
     * @param      array<string, string>    $get        The get
     *
     * @deprecated Since 2.29, use searchMetadata instead
     */
    public static function searchMeta(mixed $unused, array $get): XmlTag
    {
        $q = $get['q'] ?? null;

        $metaType = $get['metaType'] ?? null;

        $sortby = $get['sortby'] ?? 'meta_type,asc';

        $rs = App::meta()->getMetadata(['meta_type' => $metaType]);
        $rs = App::meta()->computeMetaStats($rs);

        $sortby = explode(',', $sortby);
        $sort   = $sortby[0];
        $order  = $sortby[1] ?? 'asc';

        $sort = match ($sort) {
            'metaId'   => 'meta_id_lower',
            'count'    => 'count',
            'metaType' => 'meta_type',
            default    => 'meta_type',
        };

        $rs->lexicalSort($sort, $order);

        $rsp = new XmlTag();

        // 1st loop looking at the beginning
        while ($rs->fetch()) {
            $meta_id = $rs->strField('meta_id');
            if ($meta_id !== '' && mb_stripos($meta_id, (string) $q) === 0) {
                $metaTag               = new XmlTag('meta');
                $metaTag->type         = $rs->strField('meta_type');
                $metaTag->uri          = rawurlencode($meta_id);
                $metaTag->count        = $rs->intField('count');
                $metaTag->percent      = $rs->intField('percent');
                $metaTag->roundpercent = $rs->intField('roundpercent');
                $metaTag->CDATA($meta_id);

                $rsp->insertNode($metaTag);
            }
        }

        // 2nd loop looking anywhere
        $rs->moveStart();
        while ($rs->fetch()) {
            $meta_id = $rs->strField('meta_id');
            if ($meta_id !== '' && mb_stripos($meta_id, (string) $q) > 0) {
                $metaTag               = new XmlTag('meta');
                $metaTag->type         = $rs->strField('meta_type');
                $metaTag->uri          = rawurlencode($meta_id);
                $metaTag->count        = $rs->intField('count');
                $metaTag->percent      = $rs->intField('percent');
                $metaTag->roundpercent = $rs->intField('roundpercent');
                $metaTag->CDATA($meta_id);

                $rsp->insertNode($metaTag);
            }
        }

        return $rsp;
    }

    /**
     * REST method to search metadata (JSON)
     *
     * Used with jquery.autocomplete()
     *
     * @param      array<string, string>     $get    The get
     *
     * @return     array<int, array<string, mixed>>
     */
    public static function searchMetadata(array $get): array
    {
        $q        = $get['q']        ?? null;
        $metaType = $get['metaType'] ?? null;

        $sortby = $get['sortby'] ?? 'meta_type,asc';

        $rs = App::meta()->getMetadata(['meta_type' => $metaType]);
        $rs = App::meta()->computeMetaStats($rs);

        $sortby = explode(',', $sortby);
        $sort   = $sortby[0];
        $order  = $sortby[1] ?? 'asc';

        $sort = match ($sort) {
            'metaId'   => 'meta_id_lower',
            'count'    => 'count',
            'metaType' => 'meta_type',
            default    => 'meta_type',
        };

        $rs->lexicalSort($sort, $order);

        $data = [];

        // 1st loop looking at the beginning
        while ($rs->fetch()) {
            $meta_id = $rs->strField('meta_id');
            if ($meta_id !== '' && mb_stripos($meta_id, (string) $q) === 0) {
                $data[] = [
                    'meta_id'      => $meta_id,
                    'type'         => $rs->strField('meta_type', true),
                    'uri'          => rawurlencode($meta_id),
                    'count'        => $rs->intField('count', true),
                    'percent'      => $rs->intField('percent', true),
                    'roundpercent' => $rs->intField('roundpercent', true),
                ];
            }
        }

        // 2nd loop looking anywhere
        $rs->moveStart();
        while ($rs->fetch()) {
            $meta_id = $rs->strField('meta_id');
            if ($meta_id !== '' && mb_stripos($meta_id, (string) $q) > 0) {
                $data[] = [
                    'meta_id'      => $meta_id,
                    'type'         => $rs->strField('meta_type', true),
                    'uri'          => rawurlencode($meta_id),
                    'count'        => $rs->intField('count', true),
                    'percent'      => $rs->intField('percent', true),
                    'roundpercent' => $rs->intField('roundpercent', true),
                ];
            }
        }

        return $data;
    }

    /**
     * REST method to store section folding position (JSON)
     *
     * @param      array<string, string>     $get    The get
     * @param      array<string, string>     $post   The post
     *
     * @return     true
     */
    public static function setSectionFold(array $get, array $post): bool
    {
        if (empty($post['section'])) {
            throw new Exception('No section name');
        }

        $section           = $post['section'];
        $status            = isset($post['value']) && ($post['value'] != 0);
        $unfolded_sections = App::auth()->prefs()->get('toggles')->getStr('unfolded_sections', false);
        $toggles           = $unfolded_sections !== '' ? explode(',', trim($unfolded_sections)) : [];

        $k = array_search($section, $toggles, true);
        if ($status) {
            // true == Fold section ==> remove it from unfolded list
            if ($k !== false) {
                unset($toggles[$k]);
            }
        } elseif ($k === false) {
            // false == unfold section ==> add it to unfolded list
            $toggles[] = $section;
        }

        // Remove empty values if any
        $toggles = array_filter($toggles);
        // Sort in alphabetic order
        sort($toggles);
        // Store resulting list
        App::auth()->prefs()->get('toggles')->put('unfolded_sections', implode(',', $toggles), App::userWorkspace()::WS_STRING);

        return true;
    }

    /**
     * REST method to store dashboard module's positions (JSON)
     *
     * @param      array<string, string>     $get    The get
     * @param      array<string, string>     $post   The post
     *
     * @throws     Exception
     *
     * @return     true
     */
    public static function setDashboardPositions(array $get, array $post): bool
    {
        if (empty($post['id'])) {
            throw new Exception('No zone name');
        }

        if (empty($post['list'])) {
            throw new Exception('No sorted list of id');
        }

        $zone  = $post['id'];
        $order = $post['list'];

        App::auth()->prefs()->get('dashboard')->put($zone, $order, App::userWorkspace()::WS_STRING);

        return true;
    }

    /**
     * REST method to store dashboard module's positions (JSON)
     *
     * @param      array<string, mixed>     $get    The get
     * @param      array<string, mixed>     $post   The post
     *
     * @throws     Exception
     *
     * @return     array<string, string>
     */
    public static function setListsOptions(array $get, array $post): array
    {
        if (empty($post['id']) || !is_string($post['id'])) {
            throw new Exception('No list name');
        }

        $filters = App::backend()->userPref()->getUserFilters();
        if ($filters === []) {
            throw new Exception('No user filters');
        }

        $list = [];
        foreach ($filters as $filter) {
            $type = $filter->getType();

            if ($type === $post['id']) {
                // Get new values
                $list[$type] = [
                    isset($post['sort'])  && is_string($post['sort']) ? $post['sort'] : $filter->getSortBy(),
                    isset($post['order']) && is_string($post['order']) ? $post['order'] : $filter->getOrder(),
                    isset($post['nb'])    && is_string($post['nb']) ? (int) $post['nb'] : $filter->getNb(),
                ];
            } else {
                // Keep existing values
                $list[$type] = [
                    $filter->getSortBy(),
                    $filter->getOrder(),
                    $filter->getNb(),
                ];
            }
        }

        App::auth()->prefs()->get('interface')->put('sorts', $list, App::userWorkspace()::WS_ARRAY);

        return [
            'msg' => __('List options saved.'),
        ];
    }

    /**
     * REST method to register passkey using webauthn.
     *
     * @param      array<string, string>     $get    The get
     * @param      array<string, string>     $post   The post
     *
     * @return     array<string, string|stdClass>
     */
    public static function webAuthnRegistration(array $get, array $post): array
    {
        if (App::auth()->userID() == '') {
            throw new Exception('unknown user id');
        }

        if (App::backend()->auth()->webauthn() === false) {
            throw new Exception('Can not use WebAuthn authentication');
        }

        switch ($post['step'] ?? '') {
            case 'prepare':
                return [
                    'message'   => 'ok',
                    'arguments' => App::backend()->auth()->webauthn()->prepareCreate(),
                ];

            case 'process':
                App::backend()->auth()->webauthn()->processCreate(
                    App::backend()->auth()->webauthn()->store()->decodeValue($post['client'] ?? ''),
                    App::backend()->auth()->webauthn()->store()->decodeValue($post['attestation'] ?? ''),
                    App::backend()->auth()->webauthn()->store()->decodeValue($post['transports'] ?? ''),
                    $post['label'] ?? ''
                );

                return [
                    'message' => __('Authentication key successfully registered.'),
                ];

            default:
                throw new Exception('unknown step');
        }
    }

    /**
     * REST method to authenticate user using webauthn.
     *
     * @param      array<string, string>     $get    The get
     * @param      array<string, string>     $post   The post
     *
     * @return     array<string, string|stdClass|bool>
     */
    public static function webAuthnAuthentication(array $get, array $post): array
    {
        if (App::auth()->userID() != '') {
            throw new Exception('user allready logged in');
        }

        if (App::backend()->auth()->webauthn() === false) {
            throw new Exception('Can not use WebAuthn authentication');
        }

        switch ($post['step'] ?? '') {
            case 'prepare':
                return [
                    'message'   => 'ok',
                    'arguments' => App::backend()->auth()->webauthn()->prepareGet(),
                ];

            case 'process':
                $data = App::backend()->auth()->webauthn()->processGet(
                    App::backend()->auth()->webauthn()->store()->decodeValue($post['id'] ?? ''),
                    App::backend()->auth()->webauthn()->store()->decodeValue($post['client'] ?? ''),
                    App::backend()->auth()->webauthn()->store()->decodeValue($post['authenticator'] ?? ''),
                    App::backend()->auth()->webauthn()->store()->decodeValue($post['signature'] ?? ''),
                    App::backend()->auth()->webauthn()->store()->decodeValue($post['user'] ?? '')
                );

                if ($data !== ''
                    && App::auth()->checkUser($data, null, null, false)
                    && App::auth()->findUserBlog() !== false
                ) {
                    // if ok, sign in user in session then page MUST be reloaded (see auth.js)
                    App::session()->set('sess_user_id', $data);
                    App::session()->set('sess_browser_uid', Http::browserUID(App::config()->masterKey()));

                    return [
                        'message' => 'user found',
                    ];
                }

                return [
                    'success' => false,
                    'message' => __('This key is not registered.'),
                ];

            default:
                throw new Exception('unknown step');
        }
    }
}
