*/ namespace ReactPress\Admin; use ReactPress\Admin\Controller; use ReactPress\Includes\Activator; class Admin { /** * The ID of this plugin. * * @since 1.0.0 * @access private * @var string $plugin_name The ID of this plugin. */ private $plugin_name; /** * The version of this plugin. * * @since 1.0.0 * @access private * @var string $version The current version of this plugin. */ private $version; /** * Initialize the class and set its properties. * * @since 1.0.0 * @param string $plugin_name The name of this plugin. * @param string $version The version of this plugin. */ public function __construct($plugin_name, $version) { $this->plugin_name = $plugin_name; $this->version = $version; } /** * Register the stylesheets for the admin area. * * @since 1.0.0 */ public function enqueue_styles() { $valid_pages = ['reactpress']; $page = sanitize_title($_REQUEST['page'] ?? ""); if (in_array($page, $valid_pages)) { wp_enqueue_style($this->plugin_name, plugin_dir_url(__FILE__) . 'css/reactpress-admin.css', array(), $this->version, 'all'); } } /** * Register the JavaScript for the admin area. * * @since 1.0.0 */ public function enqueue_scripts() { $valid_pages = ['reactpress']; $page = sanitize_title($_REQUEST['page'] ?? ""); if (in_array($page, $valid_pages)) { // We need to load jquery and enable wp_localize_script. // Please don't ask me why! wp_enqueue_script($this->plugin_name, plugin_dir_url(__FILE__) . 'js/reactpress-admin.js', array('jquery'), $this->version, false); wp_localize_script($this->plugin_name, "rp", array( 'ajaxurl' => admin_url('admin-ajax.php'), 'apps' => Utils::get_apps(), 'appspath' => REPR_APPS_PATH, )); // React app $plugin_app_dir_url = plugin_dir_url(__FILE__) . 'js/reactpress-admin/'; $react_app_build = $plugin_app_dir_url . 'build/'; $manifest_url = $react_app_build . 'asset-manifest.json'; // Request manifest file. $request = file_get_contents($manifest_url); // If the remote request fails, wp_remote_get() will return a WP_Error, so let’s check if the $request variable is an error: if (!$request) return false; // Convert json to php array. $files_data = json_decode($request); if ($files_data === null) return; if (!property_exists($files_data, 'entrypoints')) return false; // Get assets links. $assets_files = $files_data->entrypoints; $js_files = array_filter( $assets_files, fn ($file_string) => pathinfo($file_string, PATHINFO_EXTENSION) === 'js' ); $css_files = array_filter( $assets_files, fn ($file_string) => pathinfo($file_string, PATHINFO_EXTENSION) === 'css' ); // Load css files. foreach ($css_files as $index => $css_file) { wp_enqueue_style('react-plugin-' . $index, $react_app_build . $css_file); } // Load js files. foreach ($js_files as $index => $js_file) { wp_enqueue_script('react-plugin-' . $index, $react_app_build . $js_file, array('jquery'), 1, true); } } } /** * Add admin menu. * * @return void * @since 1.0.0 */ public function add_admin_menu() { add_menu_page( 'ReactPress', 'ReactPress', 'manage_options', 'reactpress', fn () => require_once('partials/reactpress-admin-display.php'), REPR_MENU_ICON ); } /** * Add own post state (label) for pages used by apps. * (C) https://www.ibenic.com/post-states-labels/ * * @param array $states Array of all registered states. * @param WP_Post $post Post object that we can use. */ function add_post_state($states, $post) { if ('page' === get_post_type($post)) { $repr_apps = get_option('repr_apps') ?? []; $pageslugs = $repr_apps ? array_map(fn ($el) => $el['pageslugs'], $repr_apps) : []; $valid_pages = array_merge(...$pageslugs); if (in_array($post->post_name, $valid_pages)) { $states['reactpress'] = __('ReactPress', 'text-domain'); } } return $states; } /** * Add page template. * (C) https://www.pradipdebnath.com/2019/08/17/how-to-add-page-template-from-plugin-in-wordpress/ * * @param array $templates The list of page templates * @return array $templates The modified list of page templates* * @since 1.0.0 */ public function repr_add_page_template($templates) { $templates[REPR_PLUGIN_PATH . 'templates/empty-react-page-template.php'] = __('ReactPress Canvas', 'text-domain'); $templates[REPR_PLUGIN_PATH . 'templates/react-page-template.php'] = __('ReactPress Full Width', 'text-domain'); return $templates; } /** * https://www.sitepoint.com/wordpress-plugin-updates-right-way/ */ public function check_plugin_version() { if (REPR_VERSION !== get_option('repr_version')) { Activator::activate(); } } public function repr_handle_admin_ajax_request() { /** * Handles all request from the admin frontend. * All request must be of type POST, because the WordPress action * 'wp_ajax_repr_admin_ajax_request' requires it. * * @since 1.0.0 */ $appname = strtolower(sanitize_file_name($_POST['appname'] ?? '')); $app_options_list = Utils::get_apps(); $old_pageslug = sanitize_title_for_query($_POST['old_pageslug'] ?? ''); $pageslug = sanitize_title_for_query($_POST['pageslug'] ?? ''); $param = sanitize_file_name($_REQUEST['param'] ?? ""); $template = sanitize_file_name($_POST['template'] ?? ''); $type = sanitize_file_name($_POST['type'] ?? 'development'); try { if (!empty($param)) { if ($param === "add_url_slug" && $appname && $pageslug) { $app_option = $this->get_app_options($app_options_list, $appname); if ($app_option) { //# Check if the app allows adding of more URL slugs if ($app_option['allowsRouting'] && count($app_option['pageslugs'])) { echo wp_json_encode([ 'status' => 0, 'message' => 'Apps with client-side routing can only have URL slug.' ]); return; } // add slug to existing app_options $this->update_react_page($appname, $pageslug); $is_pageslug_updated = $this->update_pageslug($app_options_list, $appname, '', $pageslug); // only update app data for succesfull slug updates if ($is_pageslug_updated) { $this->add_build_path($appname); if ($app_option['allowsRouting']) { add_rewrite_rule('^' . $pageslug . '/(.*)?', 'index.php?pagename=' . $pageslug, 'top'); Utils::set_public_url_for_dev_server($appname, $pageslug); } flush_rewrite_rules(); $this->write_index_html($appname, $this->get_index_html_content($pageslug)); echo wp_json_encode([ 'status' => 1, 'message' => 'Url Slug created.' ]); } else { echo wp_json_encode([ 'status' => 0, 'message' => 'Url Slug already exists. Slugs must be unique for ALL apps.' ]); } } else { // create new app option $this->update_react_page($appname, $pageslug); $this->add_app_options($app_options_list, $appname, $pageslug); $this->add_build_path($appname); if ($app_option['allowsRouting']) { add_rewrite_rule('^' . $pageslug . '/(.*)?', 'index.php?pagename=' . $pageslug, 'top'); Utils::set_public_url_for_dev_server($appname, $pageslug); } flush_rewrite_rules(); $this->write_index_html($appname, $this->get_index_html_content($pageslug)); echo wp_json_encode([ 'status' => 1, 'message' => 'Url Slug created.' ]); } } elseif ($param === "delete_url_slug" && $appname && $pageslug) { Controller::delete_url_slug($app_options_list, $appname, $pageslug); } elseif ($param === "edit_url_slug" && $appname && $pageslug) { $app_option = $this->get_app_options($app_options_list, $appname); $pageslug_key = array_search($old_pageslug, $app_option['pageslugs']); if (($app_option && $pageslug_key !== false)) { $this->update_react_page_slug($old_pageslug, $pageslug); $is_pageslug_changed = $this->update_pageslug($app_options_list, $appname, $old_pageslug, $pageslug); if ($is_pageslug_changed === false) { echo wp_json_encode([ 'status' => 0, 'message' => 'Url Slug already taken.' ]); return; } $this->add_build_path($appname); Utils::remove_rewrite_rule('^' . $old_pageslug . '/(.*)?'); if ($app_option['allowsRouting']) { add_rewrite_rule('^' . $pageslug . '/(.*)?', 'index.php?pagename=' . $pageslug, 'top'); Utils::unset_public_url_for_dev_server($appname, $pageslug); Utils::set_public_url_for_dev_server($appname, $pageslug); } $this->write_index_html($appname, $this->get_index_html_content($pageslug)); flush_rewrite_rules(); echo wp_json_encode([ 'status' => 1, 'message' => 'Url Slug changed.' ]); } elseif (!$this->does_pageslug_exist($pageslug) && $pageslug) { $this->update_react_page($appname, $pageslug); $this->add_app_options($app_options_list, $appname, $pageslug); $this->add_build_path($appname); Utils::remove_rewrite_rule('^' . $old_pageslug . '/(.*)?'); if ($app_option['allowsRouting']) { add_rewrite_rule('^' . $pageslug . '/(.*)?', 'index.php?pagename=' . $pageslug, 'top'); Utils::unset_public_url_for_dev_server($appname, $pageslug); Utils::set_public_url_for_dev_server($appname, $pageslug); } $this->write_index_html($appname, $this->get_index_html_content($pageslug)); flush_rewrite_rules(); echo wp_json_encode([ 'status' => 1, 'message' => 'Url Slug created.' ]); } else { echo wp_json_encode([ 'status' => 0, 'message' => "There exists another page that already has this page slug.", ]); } } elseif ($param === "toggle_react_routing" && $appname) { Controller::toggle_react_routing($appname); } elseif ($param === "update_index_html" && $appname && $pageslug) { $this->write_index_html($appname, $this->get_index_html_content($pageslug)); echo wp_json_encode([ 'status' => 1, 'message' => 'Index.html updated.', ]); } elseif ($param === 'delete_react_app' && $appname) { $options = get_option('repr_apps'); $is_option_deleted = update_option('repr_apps', array_filter( $options, fn ($el) => $el['appname'] !== $appname )); $is_appdir_removed = repr_delete_directory(Utils::app_path($appname)); if ($is_appdir_removed) { echo wp_json_encode([ 'status' => 1, 'message' => 'App deleted.', ]); } else { echo wp_json_encode([ 'status' => 1, 'message' => "Couldn't remove files. Please remove directory by hand.", ]); } } elseif ($param === "get_react_apps") { $apps = Utils::get_apps(); echo wp_json_encode(['status' => 1, 'apps' => $apps]); } else { echo wp_json_encode([ 'status' => 0, 'message' => "Request unknown.", ]); } } } catch (\Exception $e) { echo (wp_json_encode([ 'status' => 0, 'message' => $e->getMessage(), 'code' => $e->getCode(), // User-defined Exception code 'filename' => $e->getFile(), // Source filename 'line' => $e->getLine(), // Source line 'trace' => $e->getTraceAsString(), // Formated string of trace ])); } finally { wp_die(); } } /** * Checks if the given string is already used as a pageslug of any * post in the current WP site. * @param string $pageslug * @return bool * @since 1.2.0 */ public function does_pageslug_exist(string $pageslug) { global $wpdb; return !empty($wpdb->get_row( $wpdb->prepare("SELECT * FROM " . $wpdb->prefix . "posts WHERE post_name = %s", $pageslug) )); } /** * Downloads the content of the page with the given pageslug and removes the * the react assets of the build page, that we can use the content for our * development server. * * @param string $pageslug * @return string * @since 1.0.0 */ public function get_index_html_content(string $pageslug) { $file_contents = wp_remote_retrieve_body( wp_remote_get(site_url() . '/' . $pageslug, ['timeout' => 1000]) ); $file_contents_arr = explode(PHP_EOL, $file_contents); // filter all build assets out of the file, that they don't conflict // with the dev assets. $filtered_arr = array_filter($file_contents_arr, fn ($el) => !strpos($el, "id='rp-react-app-asset-")); $filtered_contents = implode(PHP_EOL, $filtered_arr); return $filtered_contents; } /** * Writes the given to the index.html file in the app directory of * the given appname. * * @param string $appname * @param string $content * @return bool if the writing of the file succeded or not * @since 1.0.0 */ public function write_index_html(string $appname, string $content) { $index_html_path = sprintf("%s/%s/public/index.html", REPR_APPS_PATH, $appname); return file_put_contents($index_html_path, $content); } /** * Reads the React app uri out of the out.log file of the * app. Which is a splash screen of create-react-app. * * @param string $apppath the directory path of the app * @param integer $max_trys the number of trys to read the uri. * Important when out.log file is still being created. * @return array contains the protocol, the ip and the * port of the uri. * @since 1.0.0 */ function get_app_uri(string $apppath, int $max_trys = 1) { $regex = '/http:\/\/\d+\.\d+\.\d+\.\d+:\d*/'; $matches = []; $trys = 0; while (empty($matches) && $trys < $max_trys) { try { $file_content = file_get_contents("{$apppath}/out.log"); } catch (\Exception $e) { return []; } preg_match($regex, $file_content, $matches); $trys += 1; if ($trys < $max_trys) { sleep(1); } } if (empty($matches)) { return []; } else { return explode(':', $matches[0]); } } /** * Add the right build path to package.json * * @param string $appname * @return int 0 if no success * @since 1.2.0 */ function add_build_path($appname) { $apppath = Utils::app_path($appname); // We need the relative path, that we can deploy our // build app to another server later. $relative_apppath = Utils::app_path($appname, true); $relative_apppath = $relative_apppath ? $relative_apppath : "/wp-content/reactpress/apps/{$appname}/"; $homepage = "{$relative_apppath}/build"; $path_package_json = "{$apppath}/package.json"; $package_json_contents = file_get_contents($path_package_json); if (!$package_json_contents) { return 0; } elseif (stripos($package_json_contents, $homepage)) { return 1; } else { // Add a homepage attribute during the build process and remove it again, // that the developer can build with the public/index.html without WP. file_put_contents( $path_package_json, str_replace("react-scripts build", IS_WINDOWS ? "set PUBLIC_URL={$homepage}&&react-scripts build" : "PUBLIC_URL={$homepage} react-scripts build", $package_json_contents) ); return 2; } return 0; } /** * Helper function to add an element to an array * without mutationg the original array. * * @param array $array * @param [type] $entry * @return void * @since 1.0.0 */ function array_add(array $array, $entry) { return array_merge($array, [$entry]); } /** * Creates or updates a page with the given name and slug. * * @param string $appname * @param string $pageslug * @return void * @since 1.0.0 */ public function update_react_page(string $appname, string $pageslug) { global $wpdb; // check if post_name (which is the slug and should be unique) exist $get_data = $wpdb->get_row( $wpdb->prepare( "SELECT `ID`, `post_content` FROM " . $wpdb->prefix . "posts WHERE post_name = %s", $pageslug ) ); if (!empty($get_data)) { // already we have data with this post name if (strpos($get_data->post_content, '
') !== false) { return ['status' => 'true', 'message' => 'Page with app already exists.']; } $result = wp_update_post( [ 'ID' => $get_data->ID, 'post_content' => $get_data->post_content . '\n' . REPR_REACT_ROOT_TAG ] ); return ['status' => 'true', 'message' => 'Add app to page.']; } else { $result = wp_insert_post( array( 'post_title' => $appname, 'post_name' => $pageslug, 'post_status' => 'publish', 'post_author' => '1', 'post_content' => REPR_REACT_ROOT_TAG, 'post_type' => "page", // Assign page template 'page_template' => REPR_PLUGIN_PATH . 'templates/react-page-template.php', ) ); if ($result) { return ['status' => 'true', 'message' => 'Page created.']; } else { return ['status' => 'false', 'message' => "Couldn't create page."]; } } } /** * Change the slug of a page to the new one. * * @param string $oldSlug * @param string $newSlug * @return void * @since 1.0.0 */ public function update_react_page_slug(string $oldSlug, string $newSlug) { global $wpdb; // check if post_name (which is the slug and should be unique) exist $get_data = $wpdb->get_row( $wpdb->prepare( "SELECT `ID`, `post_content` FROM " . $wpdb->prefix . "posts WHERE post_name = %s", $oldSlug ) ); if (empty($get_data)) { return ['status' => 'false', 'message' => 'Couldn\'t find page.']; } else { $result = wp_update_post( array( 'ID' => $get_data->ID, 'post_content' => $get_data->post_content . '\n' . REPR_REACT_ROOT_TAG, 'post_name' => $newSlug, ) ); if ($result) { return ['status' => 'true', 'message' => 'Page updated.']; } else { return ['status' => 'false', 'message' => "Couldn't update page"]; } } } public function add_app_options(array $app_options_list, string $appname, string $pageslug) { if (!is_array($app_options_list) && $appname && $pageslug) { add_option('repr_apps', [[ 'allowsRouting' => false, 'appname' => $appname, 'pageslugs' => [$pageslug], ]]); } elseif ($appname && $pageslug) { update_option('repr_apps', $this->array_add($app_options_list, [ 'allowsRouting' => false, 'appname' => $appname, 'pageslugs' => [$pageslug], ])); } } /** * Updates the given pagesslug with a new one. * Returns false if pageslug already taken. * @param array $app_options_list * @param string $appname * @param string $old_pageslug * @param string $pageslug * @return false|array * @since 2.0.0 */ public function update_pageslug(array $app_options_list, string $appname, string $old_pageslug, string $pageslug) { foreach ($app_options_list as $key => $val) { // pageslug already exists in an app $is_pageslug_taken = in_array($pageslug, $val['pageslugs']); if ($is_pageslug_taken) { return false; } if ($val['appname'] === $appname) { $pageslug_key = array_search($old_pageslug, $val['pageslugs']); if ($pageslug_key === false) { // create new pageslug array_push($app_options_list[$key]['pageslugs'], $pageslug); } else { // replace old pageslug with new one $app_options_list[$key]['pageslugs'][$pageslug_key] = $pageslug; } } } update_option('repr_apps', $app_options_list); return $app_options_list; } /** * Get the option for the given app name. * @param string $appname * @return mixed */ public function get_app_options(array $app_options_list, string $appname) { $app_options = null; foreach ($app_options_list as $key => $val) { if ($val['appname'] === $appname) { $app_options = $val; } } return $app_options; } } /** * Deletes directory recursively * (C) Paulund https://paulund.co.uk/php-delete-directory-and-files-in-directory * * @param string $dirname * @return bool true if directory deleted * @since 1.0.0 */ function repr_delete_directory(string $dirname): bool { $dir_handle = ''; if (is_dir($dirname)) $dir_handle = opendir($dirname); if (!$dir_handle) return false; while ($file = readdir($dir_handle)) { if ($file != "." && $file != "..") { if (!is_dir($dirname . "/" . $file)) unlink($dirname . "/" . $file); else repr_delete_directory($dirname . '/' . $file); } } closedir($dir_handle); return rmdir($dirname); } /** * Write error to a log file named debug.log in wp-content. * * @param mixed $log The thing you want to log. * @since 1.0.0 */ function repr_log($log) { if (true === WP_DEBUG) { if (is_array($log) || is_object($log)) { error_log(print_r($log, true)); } else { error_log($log); } } return $log; } /** * The html block, the react apps hook to. */ define("REPR_REACT_ROOT_TAG", "\n\n\n\n"); define("REPR_MENU_ICON", "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM6c29kaXBvZGk9Imh0dHA6Ly9zb2RpcG9kaS5zb3VyY2Vmb3JnZS5uZXQvRFREL3NvZGlwb2RpLTAuZHRkIgogICB4bWxuczppbmtzY2FwZT0iaHR0cDovL3d3dy5pbmtzY2FwZS5vcmcvbmFtZXNwYWNlcy9pbmtzY2FwZSIKICAgd2lkdGg9IjI0IgogICBoZWlnaHQ9IjI0IgogICB2aWV3Qm94PSIwIDAgMjQgMjQiCiAgIHZlcnNpb249IjEuMSIKICAgaWQ9InN2Zzg5NyIKICAgc29kaXBvZGk6ZG9jbmFtZT0iYnhsLXJlYWN0LnN2ZyIKICAgaW5rc2NhcGU6dmVyc2lvbj0iMS4wLjEgKDA3NjdmODMwMmEsIDIwMjAtMTAtMTcpIj4KICA8bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGE5MDMiPgogICAgPHJkZjpSREY+CiAgICAgIDxjYzpXb3JrCiAgICAgICAgIHJkZjphYm91dD0iIj4KICAgICAgICA8ZGM6Zm9ybWF0PmltYWdlL3N2Zyt4bWw8L2RjOmZvcm1hdD4KICAgICAgICA8ZGM6dHlwZQogICAgICAgICAgIHJkZjpyZXNvdXJjZT0iaHR0cDovL3B1cmwub3JnL2RjL2RjbWl0eXBlL1N0aWxsSW1hZ2UiIC8+CiAgICAgIDwvY2M6V29yaz4KICAgIDwvcmRmOlJERj4KICA8L21ldGFkYXRhPgogIDxkZWZzCiAgICAgaWQ9ImRlZnM5MDEiIC8+CiAgPHNvZGlwb2RpOm5hbWVkdmlldwogICAgIHBhZ2Vjb2xvcj0iI2ZmZmZmZiIKICAgICBib3JkZXJjb2xvcj0iIzY2NjY2NiIKICAgICBib3JkZXJvcGFjaXR5PSIxIgogICAgIG9iamVjdHRvbGVyYW5jZT0iMTAiCiAgICAgZ3JpZHRvbGVyYW5jZT0iMTAiCiAgICAgZ3VpZGV0b2xlcmFuY2U9IjEwIgogICAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwIgogICAgIGlua3NjYXBlOnBhZ2VzaGFkb3c9IjIiCiAgICAgaW5rc2NhcGU6d2luZG93LXdpZHRoPSIxOTIwIgogICAgIGlua3NjYXBlOndpbmRvdy1oZWlnaHQ9IjEwMTUiCiAgICAgaWQ9Im5hbWVkdmlldzg5OSIKICAgICBzaG93Z3JpZD0iZmFsc2UiCiAgICAgaW5rc2NhcGU6em9vbT0iMzYuMTI1IgogICAgIGlua3NjYXBlOmN4PSIxMiIKICAgICBpbmtzY2FwZTpjeT0iMTIiCiAgICAgaW5rc2NhcGU6d2luZG93LXg9IjE5MjAiCiAgICAgaW5rc2NhcGU6d2luZG93LXk9IjI4IgogICAgIGlua3NjYXBlOndpbmRvdy1tYXhpbWl6ZWQ9IjEiCiAgICAgaW5rc2NhcGU6Y3VycmVudC1sYXllcj0ic3ZnODk3IiAvPgogIDxjaXJjbGUKICAgICBjeD0iMTIiCiAgICAgY3k9IjExLjI0NSIKICAgICByPSIxLjc4NSIKICAgICBpZD0iY2lyY2xlODg3IgogICAgIHN0eWxlPSJmaWxsOiNhMGE1YWE7ZmlsbC1vcGFjaXR5OjEiIC8+CiAgPHBhdGgKICAgICBkPSJNNy4wMDIsMTQuNzk0bC0wLjM5NS0wLjEwMWMtMi45MzQtMC43NDEtNC42MTctMi4wMDEtNC42MTctMy40NTJjMC0xLjQ1MiwxLjY4NC0yLjcxMSw0LjYxNy0zLjQ1MmwwLjM5NS0wLjFMNy4xMTMsOC4wOCBjMC4yOTcsMS4wMjMsMC42NzYsMi4wMjIsMS4xMzYsMi45ODNsMC4wODUsMC4xNzhsLTAuMDg1LDAuMTc4Yy0wLjQ2LDAuOTYzLTAuODQxLDEuOTYxLTEuMTM2LDIuOTg1TDcuMDAyLDE0Ljc5NEw3LjAwMiwxNC43OTR6IE02LjQyNSw4LjY5OWMtMi4yMjksMC42MjgtMy41OTgsMS41ODYtMy41OTgsMi41NDJjMCwwLjk1NCwxLjM2OCwxLjkxMywzLjU5OCwyLjU0YzAuMjczLTAuODY4LDAuNjAzLTEuNzE3LDAuOTg1LTIuNTQgQzcuMDI1LDEwLjQxNiw2LjY5Niw5LjU2Nyw2LjQyNSw4LjY5OXogTTE2Ljk5NywxNC43OTRsLTAuMTEtMC4zOTJjLTAuMjk4LTEuMDI0LTAuNjc3LTIuMDIyLTEuMTM3LTIuOTg0bC0wLjA4NS0wLjE3NyBsMC4wODUtMC4xNzljMC40Ni0wLjk2MSwwLjgzOS0xLjk2LDEuMTM3LTIuOTg0bDAuMTEtMC4zOWwwLjM5NSwwLjFjMi45MzUsMC43NDEsNC42MTcsMiw0LjYxNywzLjQ1MyBjMCwxLjQ1Mi0xLjY4MywyLjcxMS00LjYxNywzLjQ1MkwxNi45OTcsMTQuNzk0eiBNMTYuNTg3LDExLjI0MWMwLjQsMC44NjYsMC43MzMsMS43MTgsMC45ODcsMi41NCBjMi4yMy0wLjYyNywzLjU5OS0xLjU4NiwzLjU5OS0yLjU0YzAtMC45NTYtMS4zNjgtMS45MTMtMy41OTktMi41NDJDMTcuMzAxLDkuNTY3LDE2Ljk3MiwxMC40MTYsMTYuNTg3LDExLjI0MUwxNi41ODcsMTEuMjQxeiIKICAgICBpZD0icGF0aDg4OSIKICAgICBzdHlsZT0iZmlsbDojYTBhNWFhO2ZpbGwtb3BhY2l0eToxIiAvPgogIDxwYXRoCiAgICAgZD0iTTYuNDE5LDguNjk1bC0wLjExLTAuMzlDNS40ODMsNS4zOTcsNS43MzMsMy4zMTQsNi45OTYsMi41ODhjMS4yMzUtMC43MTUsMy4yMjIsMC4xMyw1LjMwMywyLjI2NWwwLjI4NCwwLjI5MiBsLTAuMjg0LDAuMjkxYy0wLjczOSwwLjc2OS0xLjQxNSwxLjU5Ni0yLjAyLDIuNDc0bC0wLjExMywwLjE2Mkw5Ljk3LDguMDg4QzguOTA3LDguMTcxLDcuODUxLDguMzQyLDYuODEzLDguNTk3TDYuNDE5LDguNjk1IEw2LjQxOSw4LjY5NXogTTguMDAxLDMuMTY2Yy0wLjIyNCwwLTAuNDIyLDAuMDQ5LTAuNTg5LDAuMTQ1QzYuNTg0LDMuNzg4LDYuNDM4LDUuNDQ5LDcuMDA4LDcuNjkxIGMwLjg5MS0wLjE5NywxLjc5LTAuMzM4LDIuNjk2LTAuNDE3YzAuNTI1LTAuNzQ1LDEuMDk3LTEuNDUzLDEuNzEzLTIuMTIzQzEwLjExNCwzLjg4NCw4Ljg4NCwzLjE2Niw4LjAwMSwzLjE2Nkw4LjAwMSwzLjE2NnogTTE1Ljk5OCwyMC4xNUwxNS45OTgsMjAuMTVjLTEuMTg4LDAtMi43MTQtMC44OTYtNC4yOTgtMi41MjJsLTAuMjgzLTAuMjkxbDAuMjgzLTAuMjljMC43MzktMC43NywxLjQxNi0xLjU5OSwyLjAyMS0yLjQ3NyBsMC4xMTItMC4xNmwwLjE5NC0wLjAxOWMxLjA2NS0wLjA4MiwyLjEyMi0wLjI1MiwzLjE1OC0wLjUwN2wwLjM5NS0wLjFsMC4xMTEsMC4zOTFjMC44MjIsMi45MDYsMC41NzMsNC45OTItMC42ODgsNS43MTggQzE2LjY5OCwyMC4wNjYsMTYuMzUyLDIwLjE1NSwxNS45OTgsMjAuMTVMMTUuOTk4LDIwLjE1eiBNMTIuNTgzLDE3LjMzYzEuMzAyLDEuMjY3LDIuNTMzLDEuOTg2LDMuNDE1LDEuOTg2bDAsMCBjMC4yMjUsMCwwLjQyMy0wLjA1LDAuNTg5LTAuMTQ1YzAuODI5LTAuNDc4LDAuOTc2LTIuMTQyLDAuNDA0LTQuMzg0Yy0wLjg5LDAuMTk4LTEuNzksMC4zNC0yLjY5OCwwLjQxOSBDMTMuNzcxLDE1Ljk1MSwxMy4xOTksMTYuNjYxLDEyLjU4MywxNy4zM3oiCiAgICAgaWQ9InBhdGg4OTEiCiAgICAgc3R5bGU9ImZpbGw6I2EwYTVhYTtmaWxsLW9wYWNpdHk6MSIgLz4KICA8cGF0aAogICAgIGQ9Ik0xNy41OCw4LjY5NWwtMC4zOTUtMC4wOTljLTEuMDM2LTAuMjU2LTIuMDkzLTAuNDI2LTMuMTU4LTAuNTA5bC0wLjE5NC0wLjAxN2wtMC4xMTItMC4xNjIgYy0wLjYwNC0wLjg3OC0xLjI4MS0xLjcwNS0yLjAyMS0yLjQ3NGwtMC4yODMtMC4yOTFMMTEuNyw0Ljg1M2MyLjA4LTIuMTM0LDQuMDY2LTIuOTc5LDUuMzAzLTIuMjY1IGMxLjI2MiwwLjcyNywxLjUxMywyLjgxLDAuNjg4LDUuNzE3TDE3LjU4LDguNjk1TDE3LjU4LDguNjk1eiBNMTQuMjkzLDcuMjc0YzAuOTU0LDAuMDg1LDEuODU4LDAuMjI4LDIuNjk4LDAuNDE3IGMwLjU3MS0yLjI0MiwwLjQyNS0zLjkwMy0wLjQwNC00LjM4MWMtMC44MjQtMC40NzctMi4zNzUsMC4yNTMtNC4wMDQsMS44NDFDMTMuMTk5LDUuODIxLDEzLjc3MSw2LjUyOSwxNC4yOTMsNy4yNzR6IE04LjAwMSwyMC4xNSBjLTAuMzUzLDAuMDA1LTAuNjk5LTAuMDg0LTEuMDA1LTAuMjU3Yy0xLjI2My0wLjcyNi0xLjUxMy0yLjgxMS0wLjY4OC01LjcxOGwwLjEwOC0wLjM5MWwwLjM5NSwwLjEgYzAuOTY0LDAuMjQzLDIuMDI2LDAuNDE0LDMuMTU4LDAuNTA3bDAuMTk0LDAuMDE5bDAuMTEzLDAuMTZjMC42MDQsMC44NzgsMS4yOCwxLjcwNywyLjAyLDIuNDc3bDAuMjg0LDAuMjlsLTAuMjg0LDAuMjkxIEMxMC43MTMsMTkuMjU1LDkuMTg3LDIwLjE1LDguMDAxLDIwLjE1TDguMDAxLDIwLjE1eiBNNy4wMDgsMTQuNzg4Yy0wLjU3LDIuMjQyLTAuNDI0LDMuOTA2LDAuNDA0LDQuMzg0IGMwLjgyNSwwLjQ3LDIuMzcxLTAuMjU1LDQuMDA1LTEuODQyYy0wLjYxNi0wLjY3LTEuMTg4LTEuMzc5LTEuNzEzLTIuMTIzQzguNzk4LDE1LjEyOCw3Ljg5OCwxNC45ODYsNy4wMDgsMTQuNzg4TDcuMDA4LDE0Ljc4OHoiCiAgICAgaWQ9InBhdGg4OTMiCiAgICAgc3R5bGU9ImZpbGw6I2EwYTVhYTtmaWxsLW9wYWNpdHk6MSIgLz4KICA8cGF0aAogICAgIGQ9Ik0xMiwxNS4zMTNjLTAuNjg3LDAtMS4zOTItMC4wMjktMi4xLTAuMDg4bC0wLjE5Ni0wLjAxN2wtMC4xMTMtMC4xNjJjLTAuMzk4LTAuNTcyLTAuNzc0LTEuMTYzLTEuMTI2LTEuNzY5IGMtMC4zNDktMC42MDctMC42NzItMS4yMjYtMC45NzEtMS44NTlMNy40MSwxMS4yNDFsMC4wODQtMC4xNzljMC4yOTktMC42MzIsMC42MjItMS4yNTIsMC45NzEtMS44NTggYzAuMzQ3LTAuNTk2LDAuNzI2LTEuMTkyLDEuMTI2LTEuNzdsMC4xMTMtMC4xNkw5LjksNy4yNTZjMS4zOTctMC4xMTcsMi44MDEtMC4xMTcsNC4xOTgsMGwwLjE5NCwwLjAxOWwwLjExMywwLjE2IGMwLjc5OSwxLjE0OSwxLjUwMywyLjM2MiwyLjEsMy42MjhsMC4wODMsMC4xNzlsLTAuMDgzLDAuMTc3Yy0wLjU5NywxLjI2OC0xLjI5OSwyLjQ4MS0yLjEsMy42MjhsLTAuMTEzLDAuMTYybC0wLjE5NCwwLjAxNyBDMTMuMzkyLDE1LjI4MywxMi42ODYsMTUuMzEzLDEyLDE1LjMxM0wxMiwxNS4zMTN6IE0xMC4xNjYsMTQuNDA5YzEuMjM1LDAuMDkzLDIuNDMzLDAuMDkzLDMuNjY3LDAgYzAuNjktMS4wMSwxLjMwMS0yLjA2OCwxLjgzMi0zLjE2OGMtMC41MjktMS4xMDItMS4xNDItMi4xNjEtMS44MzItMy4xNjhjLTEuMjIxLTAuMDk0LTIuNDQ2LTAuMDk0LTMuNjY3LDAgYy0wLjY4OSwxLjAwNy0xLjMwNSwyLjA2NS0xLjgzMiwzLjE2OEM4Ljg2NSwxMi4zNDEsOS40NzksMTMuMzk5LDEwLjE2NiwxNC40MDlMMTAuMTY2LDE0LjQwOXoiCiAgICAgaWQ9InBhdGg4OTUiCiAgICAgc3R5bGU9ImZpbGw6I2EwYTVhYTtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtzdHJva2Utb3BhY2l0eToxIiAvPgo8L3N2Zz4K"); define('REPR_REACT_ICON_SVG', ''); define('REPR_LOGO', '');