From ff1174671acd023f682486abb3b4babcaf402cf8 Mon Sep 17 00:00:00 2001 From: katamaz Date: Wed, 7 Jan 2026 22:27:52 +0300 Subject: [PATCH] dx4 v2 --- backend-php/.gitignore | 2 + backend-php/adm/phpliteadmin.config.php | 5 + backend-php/adm/phpliteadmin.php | 6264 +++++++++++++++++ backend-php/admin_api.php | 282 + backend-php/api.php | 238 +- backend-php/k-api.php | 37 +- backend-php/phpinfo.php | 1 - backend-php/t.php | 2 + bun.lock | 104 +- dengi.ps1 | 23 + dx4dashboard/.gitignore | 24 + dx4dashboard/README.md | 75 + dx4dashboard/bun.lock | 569 ++ dx4dashboard/eslint.config.js | 31 + dx4dashboard/index.html | 13 + dx4dashboard/package.json | 35 + dx4dashboard/public/vite.svg | 1 + dx4dashboard/src/App.tsx | 104 + dx4dashboard/src/assets/logo.svg | 33 + dx4dashboard/src/assets/react.svg | 1 + dx4dashboard/src/index.css | 31 + dx4dashboard/src/main.tsx | 10 + dx4dashboard/src/pages/ClassProfile.tsx | 556 ++ dx4dashboard/src/pages/DashPage.tsx | 548 ++ dx4dashboard/src/pages/LoginPage.tsx | 113 + .../src/stilltrash/dengi_components.tsx | 312 + dx4dashboard/src/stilltrash/req_mgr.ts | 128 + dx4dashboard/src/stilltrash/trash.module.css | 11 + dx4dashboard/src/stilltrash/trash.ts | 91 + dx4dashboard/src/types.ts | 1 + dx4dashboard/tsconfig.app.json | 28 + dx4dashboard/tsconfig.json | 7 + dx4dashboard/tsconfig.node.json | 26 + dx4dashboard/vite.config.ts | 15 + index.html | 30 +- package.json | 1 + src/App.jsx | 8 +- src/index.css | 4 + src/pages/AdminProfile.jsx | 212 + src/pages/ClassProfile.jsx | 521 ++ src/pages/ControlPanel.jsx | 9 - src/pages/DX4AppPage.jsx | 270 +- src/pages/LoginPage.jsx | 49 +- src/pages/editor.jsx | 61 +- src/pages/test.jsx | 213 +- src/pages/test_arch0.jsx | 212 + src/trash/dengi.js | 36 +- src/trash/editorTable.jsx | 23 +- src/trash/editor_req_mgr.js | 92 +- src/trash/helpers.js | 49 +- src/trash/md5.js | 167 - src/trash/req_mgr.js | 156 +- src/trash/trash-components.jsx | 206 +- src/trash/trash.module.css | 11 + vite.config.js | 8 +- 55 files changed, 11201 insertions(+), 858 deletions(-) create mode 100644 backend-php/adm/phpliteadmin.config.php create mode 100644 backend-php/adm/phpliteadmin.php create mode 100644 backend-php/admin_api.php delete mode 100644 backend-php/phpinfo.php create mode 100644 backend-php/t.php create mode 100644 dengi.ps1 create mode 100644 dx4dashboard/.gitignore create mode 100644 dx4dashboard/README.md create mode 100644 dx4dashboard/bun.lock create mode 100644 dx4dashboard/eslint.config.js create mode 100644 dx4dashboard/index.html create mode 100644 dx4dashboard/package.json create mode 100644 dx4dashboard/public/vite.svg create mode 100644 dx4dashboard/src/App.tsx create mode 100644 dx4dashboard/src/assets/logo.svg create mode 100644 dx4dashboard/src/assets/react.svg create mode 100644 dx4dashboard/src/index.css create mode 100644 dx4dashboard/src/main.tsx create mode 100644 dx4dashboard/src/pages/ClassProfile.tsx create mode 100644 dx4dashboard/src/pages/DashPage.tsx create mode 100644 dx4dashboard/src/pages/LoginPage.tsx create mode 100644 dx4dashboard/src/stilltrash/dengi_components.tsx create mode 100644 dx4dashboard/src/stilltrash/req_mgr.ts create mode 100644 dx4dashboard/src/stilltrash/trash.module.css create mode 100644 dx4dashboard/src/stilltrash/trash.ts create mode 100644 dx4dashboard/src/types.ts create mode 100644 dx4dashboard/tsconfig.app.json create mode 100644 dx4dashboard/tsconfig.json create mode 100644 dx4dashboard/tsconfig.node.json create mode 100644 dx4dashboard/vite.config.ts create mode 100644 src/pages/AdminProfile.jsx create mode 100644 src/pages/ClassProfile.jsx delete mode 100644 src/pages/ControlPanel.jsx create mode 100644 src/pages/test_arch0.jsx delete mode 100644 src/trash/md5.js create mode 100644 src/trash/trash.module.css diff --git a/backend-php/.gitignore b/backend-php/.gitignore index e69de29..5a95457 100644 --- a/backend-php/.gitignore +++ b/backend-php/.gitignore @@ -0,0 +1,2 @@ +api.lock +_api.lock \ No newline at end of file diff --git a/backend-php/adm/phpliteadmin.config.php b/backend-php/adm/phpliteadmin.config.php new file mode 100644 index 0000000..45f86c3 --- /dev/null +++ b/backend-php/adm/phpliteadmin.config.php @@ -0,0 +1,5 @@ +. +// +// //////////////////////////////////////////////////////////////////////// +// +// Please report any bugs you may encounter to our issue tracker here: +// https://bitbucket.org/phpliteadmin/public/issues?status=new&status=open + +// +// This is sample configuration file +// +// You can configure phpliteadmin in one of 2 ways: +// 1. Rename phpliteadmin.config.sample.php to phpliteadmin.config.php and change parameters in there. +// You can set only your custom settings in phpliteadmin.config.php. All other settings will be set to defaults. +// 2. Change parameters directly in main phpliteadmin.php file +// +// Please see https://bitbucket.org/phpliteadmin/public/wiki/Configuration for more details + +//password to gain access (set an empty password to disable authentication completely) +$password = 'admin'; + +//directory relative to this file to search for databases (if false, manually list databases in the $databases variable) +$directory = '.'; + +//whether or not to scan the subdirectories of the above directory infinitely deep +$subdirectories = false; + +//if the above $directory variable is set to false, you must specify the databases manually in an array as the next variable +//if any of the databases do not exist as they are referenced by their path, they will be created automatically +$databases = array( + array( + 'path'=> 'database1.sqlite', + 'name'=> 'Database 1' + ), + array( + 'path'=> 'database2.sqlite', + 'name'=> 'Database 2' + ), +); + + +/* ---- Interface settings ---- */ + +// Theme! If you want to change theme, save the CSS file in same folder of phpliteadmin or in folder "themes" +$theme = 'phpliteadmin.css'; + +// the default language! If you want to change it, save the language file in same folder of phpliteadmin or in folder "languages" +// More about localizations (downloads, how to translate etc.): https://bitbucket.org/phpliteadmin/public/wiki/Localization +$language = 'en'; + +// set default number of rows. You need to relog after changing the number +$rowsNum = 30; + +// reduce string characters by a number bigger than 10 +$charsNum = 300; + +// maximum number of SQL queries to save in the history +$maxSavedQueries = 10; + +/* ---- Custom functions ---- */ + +//a list of custom functions that can be applied to columns in the databases +//make sure to define every function below if it is not a core PHP function +$custom_functions = array( + 'md5', 'sha1', 'strtotime', + // add the names of your custom functions to this array + /* 'leet_text', */ +); + +// define your custom functions here +/* +function leet_text($value) +{ + return strtr($value, 'eaAsSOl', '344zZ01'); +} +*/ + + +/* ---- Advanced options ---- */ + +//changing the following variable allows multiple phpLiteAdmin installs to work under the same domain. +$cookie_name = 'pla3412'; + +//whether or not to put the app in debug mode where errors are outputted +$debug = false; + +// the user is allowed to create databases with only these extensions +$allowed_extensions = array('db','db3','sqlite','sqlite3'); + +// BLOBs are displayed and edited as hex string +$hexblobs = false; + +// English language-texts. +// Read our wiki on how to translate: https://bitbucket.org/phpliteadmin/public/wiki/Localization +$lang = array( + "direction" => "LTR", + "date_format" => 'g:ia \o\n F j, Y (T)', // see http://php.net/manual/en/function.date.php for what the letters stand for + "ver" => "version", + "for" => "for", + "to" => "to", + "go" => "Go", + "yes" => "Yes", + "no" => "No", + "sql" => "SQL", + "csv" => "CSV", + "csv_tbl" => "Table that CSV pertains to", + "srch" => "Search", + "srch_again" => "Do Another Search", + "login" => "Log In", + "logout" => "Logout", + "view" => "View", // here, the noun SQL view is meant, not the verb "to view" + "confirm" => "Confirm", + "cancel" => "Cancel", + "save_as" => "Save As", + "options" => "Options", + "no_opt" => "No options", + "help" => "Help", + "installed" => "installed", + "not_installed" => "not installed", + "done" => "done", + "insert" => "Insert", + "export" => "Export", + "import" => "Import", + "rename" => "Rename", + "empty" => "Empty", + "drop" => "Drop", + "tbl" => "Table", + "chart" => "Chart", + "err" => "ERROR", + "act" => "Action", + "rec" => "Records", + "col" => "Column", + "cols" => "Columns", + "rows" => "row(s)", + "edit" => "Edit", + "del" => "Delete", + "add" => "Add", + "backup" => "Backup database file", + "before" => "Before", + "after" => "After", + "passwd" => "Password", + "passwd_incorrect" => "Incorrect password.", + "chk_ext" => "Checking supported SQLite PHP extensions", + "autoincrement" => "Autoincrement", + "not_null" => "Not NULL", + "attention" => "Attention", + "none" => "None", + "as_defined" => "As defined", + "expression" => "Expression", + "download" => "Download", + "open_in_browser" => "Open in browser", + + "sqlite_ext" => "SQLite extension", + "sqlite_ext_support" => "It appears that none of the supported SQLite library extensions are available in your installation of PHP. You may not use %s until you install at least one of them.", + "sqlite_v" => "SQLite version", + "sqlite_v_error" => "It appears that your database is of SQLite version %s but your installation of PHP does not contain the necessary extensions to handle this version. To fix the problem, either delete the database and allow %s to create it automatically or recreate it manually as SQLite version %s.", + "report_issue" => "The problem cannot be diagnosed properly. Please file an issue report at", + "sqlite_limit" => "Due to the limitations of SQLite, only the field name and data type can be modified.", + + "php_v" => "PHP version", + "new_version" => "There is a new version!", + + "db_dump" => "database dump", + "db_f" => "database file", + "db_ch" => "Change Database", + "db_event" => "Database Event", + "db_name" => "Database name", + "db_rename" => "Rename Database", + "db_renamed" => "Database '%s' has been renamed to", + "db_del" => "Delete Database", + "db_path" => "Path to database", + "db_size" => "Size of database", + "db_mod" => "Database last modified", + "db_create" => "Create New Database", + "db_vac" => "The database, '%s', has been VACUUMed.", + "db_not_writeable" => "The database, '%s', does not exist and cannot be created because the containing directory, '%s', is not writable. The application is unusable until you make it writable.", + "db_setup" => "There was a problem setting up your database, %s. An attempt will be made to find out what's going on so you can fix the problem more easily", + "db_exists" => "A database, other file or directory of the name '%s' already exists.", + "db_blank" => "The database name cannot be blank.", + + "exported" => "Exported", + "struct" => "Structure", + "struct_for" => "structure for", + "on_tbl" => "on table", + "data_dump" => "Data dump for", + "backup_hint" => "Hint: To backup your database, the easiest way is to %s.", + "backup_hint_linktext" => "download the database-file", + "total_rows" => "a total of %s rows", + "total" => "Total", + "not_dir" => "The directory you specified to scan for databases does not exist or is not a directory.", + "bad_php_directive" => "It appears that the PHP directive, 'register_globals' is enabled. This is bad. You need to disable it before continuing.", + "page_gen" => "Page generated in %s seconds.", + "powered" => "Powered by", + "free_software" => "This is free software.", + "please_donate" => "Please donate.", + "remember" => "Remember me", + "no_db" => "Welcome to %s. It appears that you have selected to scan a directory for databases to manage. However, %s could not find any valid SQLite databases. You may use the form below to create your first database.", + "no_db2" => "The directory you specified does not contain any existing databases to manage, and the directory is not writable. This means you can't create any new databases using %s. Either make the directory writable or manually upload databases to the directory.", + "dir_not_executable" => "The directory you specified cannot be scanned for databases as %s has no execute permissions on it. On Linux, use 'chmod +x %s' to fix this.", + "filesystem_permission_denied" => "Permission denied. Check file system permissions.", + + "create" => "Create", + "created" => "has been created", + "create_tbl" => "Create new table", + "create_tbl_db" => "Create new table on database", + "create_trigger" => "Creating new trigger on table", + "create_index" => "Creating new index on table", + "create_index1" => "Create Index", + "create_view" => "Create new view on database", + + "trigger" => "Trigger", + "triggers" => "Triggers", + "trigger_name" => "Trigger name", + "trigger_act" => "Trigger Action", + "trigger_step" => "Trigger Steps (semicolon terminated)", + "when_exp" => "WHEN expression (type expression without 'WHEN')", + "index" => "Index", + "indexes" => "Indexes", + "index_name" => "Index name", + "name" => "Name", + "unique" => "Unique", + "seq_no" => "Seq. No.", + "emptied" => "has been emptied", + "dropped" => "has been dropped", + "renamed" => "has been renamed to", + "altered" => "has been altered successfully", + "inserted" => "inserted", + "deleted" => "deleted", + "affected" => "affected", + "blank_index" => "Index name must not be blank.", + "one_index" => "You must specify at least one index column.", + "docu" => "Documentation", + "license" => "License", + "proj_site" => "Project Site", + "bug_report" => "This may be a bug that needs to be reported at", + "return" => "Return", + "browse" => "Browse", + "fld" => "Field", + "fld_num" => "Number of Fields", + "fields" => "Fields", + "type" => "Type", + "operator" => "Operator", + "val" => "Value", + "update" => "Update", + "comments" => "Comments", + + "specify_fields" => "You must specify the number of table fields.", + "specify_tbl" => "You must specify a table name.", + "specify_col" => "You must specify a column.", + + "tbl_exists" => "Table of the same name already exists.", + "show" => "Show", + "show_rows" => "Showing %s row(s). ", + "showing" => "Showing", + "showing_rows" => "Showing rows", + "query_time" => "(Query took %s sec)", + "syntax_err" => "There is a problem with the syntax of your query (Query was not executed)", + "run_sql" => "Run SQL query/queries on database '%s'", + "recent_queries" => "Recent Queries", + "full_texts" => "Show full texts", + "no_full_texts" => "Shorten long texts", + + "ques_table_empty" => "Are you sure you want to empty the table(s) '%s'?", + "ques_table_drop" => "Are you sure you want to drop the table(s) / view(s) '%s'?", + "ques_row_delete" => "Are you sure you want to delete row(s) %s from table '%s'?", + "ques_database_delete" => "Are you sure you want to delete the database '%s'?", + "ques_column_delete" => "Are you sure you want to delete column(s) %s from table '%s'?", + "ques_index_delete" => "Are you sure you want to delete index '%s'?", + "ques_trigger_delete" => "Are you sure you want to delete trigger '%s'?", + "ques_primarykey_add" => "Are you sure you want to add a primary key for the column(s) %s in table '%s'?", + + "export_struct" => "Export with structure", + "export_data" => "Export with data", + "add_drop" => "Add DROP TABLE", + "add_transact" => "Add TRANSACTION", + "fld_terminated" => "Fields terminated by", + "fld_enclosed" => "Fields enclosed by", + "fld_escaped" => "Fields escaped by", + "fld_names" => "Field names in first row", + "rep_null" => "Replace NULL by", + "rem_crlf" => "Remove CRLF characters within fields", + "put_fld" => "Put field names in first row", + "null_represent" => "NULL represented by", + "import_suc" => "Import was successful.", + "import_into" => "Import into", + "import_f" => "File to import", + "max_file_size" => "Maximum file size", + "rename_tbl" => "Rename table '%s' to", + + "rows_records" => "row(s) starting from record # ", + "rows_aff" => "row(s) affected. ", + + "as_a" => "as a", + "readonly_tbl" => "'%s' is a view, which means it is a SELECT statement treated as a read-only table. You may not edit or insert records.", + "chk_all" => "Check All", + "unchk_all" => "Uncheck All", + "with_sel" => "With Selected", + + "no_tbl" => "No table in database.", + "no_chart" => "If you can read this, it means the chart could not be generated. The data you are trying to view may not be appropriate for a chart.", + "no_rows" => "There are no rows in the table for the range you selected.", + "no_sel" => "You did not select anything.", + + "chart_type" => "Chart Type", + "chart_bar" => "Bar Chart", + "chart_pie" => "Pie Chart", + "chart_line" => "Line Chart", + "lbl" => "Labels", + "empty_tbl" => "This table is empty.", + "click" => "Click here", + "insert_rows" => "to insert rows.", + "restart_insert" => "Restart insertion with ", + "ignore" => "Ignore", + "func" => "Function", + "new_insert" => "Insert As New Row", + "save_ch" => "Save Changes", + "def_val" => "Default Value", + "prim_key" => "Primary Key", + "tbl_end" => "field(s) at end of table", + "query_used_table" => "Query used to create this table", + "query_used_view" => "Query used to create this view", + "create_index2" => "Create an index on", + "create_trigger2" => "Create a new trigger", + "new_fld" => "Adding new field(s) to table '%s'", + "add_flds" => "Add Fields", + "edit_col" => "Editing column '%s'", + "vac" => "Vacuum", + "vac_desc" => "Large databases sometimes need to be VACUUMed to reduce their footprint on the server. Click the button below to VACUUM the database '%s'.", + "vac_on_empty"=>"Rebuild database file to recover unused space (Vacuum)", + "event" => "Event", + "each_row" => "For Each Row", + "define_index" => "Define index properties", + "dup_val" => "Duplicate values", + "allow" => "Allowed", + "not_allow" => "Not Allowed", + "asc" => "Ascending", + "desc" => "Descending", + "warn0" => "You have been warned.", + "warn_passwd" => "You are using the default password, which can be dangerous. You can change it easily at the top of %s.", + "warn_mbstring" =>"The mbstring extension is not installed or not enabled in your PHP. As long as you stick to ASCII characters, everything will work, but you may experience strange bugs with multibyte characters. Better install and enable mbstring!", + "counting_skipped" => "Counting of records has been skipped for some tables because your database is comparably big and some tables don't have primary keys assigned to them so counting might be slow. Add a primary key to these tables or %sforce counting%s.", + "sel_state" => "Select Statement", + "delimit" => "Delimiter", + "back_top" => "Back to Top", + "choose_f" => "Choose File", + "instead" => "Instead of", + "define_in_col" => "Define index column(s)", + + "delete_only_managed" => "You can only delete databases managed by this tool!", + "rename_only_managed" => "You can only rename databases managed by this tool!", + "db_moved_outside" => "You either tried to move the database into a directory where it cannot be managed anylonger, or the check if you did this failed because of missing rights.", + "extension_not_allowed" => "The extension you provided is not within the list of allowed extensions. Please use one of the following extensions", + "add_allowed_extension" => "You can add extensions to this list by adding your extension to \$allowed_extensions in the configuration.", + "database_not_writable" => "The database-file is not writable, so its content cannot be changed in any way.", + "directory_not_writable" => "The database-file itself is writable, but to write into it, the containing directory needs to be writable as well. This is because SQLite puts temporary files in there for locking.", + "tbl_inexistent" => "Table %s does not exist", + "col_inexistent" => "Column %s does not exist", + + // errors that can happen when ALTER TABLE fails. You don't necessarily have to translate these. + "alter_failed" => "Altering of Table %s failed", + "alter_tbl_name_not_replacable" => "could not replace the table name with the temporary one", + "alter_no_def" => "no ALTER definition", + "alter_parse_failed" =>"failed to parse ALTER definition", + "alter_action_not_recognized" => "ALTER action could not be recognized", + "alter_no_add_col" => "no column to add detected in ALTER statement", + "alter_pattern_mismatch"=>"Pattern did not match on your original CREATE TABLE statement", + "alter_col_not_recognized" => "could not recognize new or old column name", + "alter_unknown_operation" => "Unknown ALTER operation!", + + /* Help documentation */ + "help_doc" => "Help Documentation", + "help1" => "SQLite Library Extensions", + "help1_x" => "%s uses PHP library extensions that allow interaction with SQLite databases. Currently, %s supports PDO, SQLite3, and SQLiteDatabase. Both PDO and SQLite3 deal with version 3 of SQLite, while SQLiteDatabase deals with version 2. So, if your PHP installation includes more than one SQLite library extension, PDO and SQLite3 will take precedence to make use of the better technology. However, if you have existing databases that are of version 2 of SQLite, %s will be forced to use SQLiteDatabase for only those databases. Not all databases need to be of the same version. During the database creation, however, the most advanced extension will be used.", + "help2" => "Creating a New Database", + "help2_x" => "When you create a new database, the name you entered will be appended with the appropriate file extension (.db, .db3, .sqlite, etc.) if you do not include it yourself. The database will be created in the directory you specified as the \$directory variable.", + "help3" => "Tables vs. Views", + "help3_x" => "On the main database page, there is a list of tables and views. Since views are read-only, certain operations will be disabled. These disabled operations will be apparent by their omission in the location where they should appear on the row for a view. If you want to change the data for a view, you need to drop that view and create a new view with the appropriate SELECT statement that queries other existing tables. For more information, see http://en.wikipedia.org/wiki/View_(database)", + "help4" => "Writing a Select Statement for a New View", + "help4_x" => "When you create a new view, you must write an SQL SELECT statement that it will use as its data. A view is simply a read-only table that can be accessed and queried like a regular table, except it cannot be modified through insertion, column editing, or row editing. It is only used for conveniently fetching data.", + "help5" => "Export Structure to SQL File", + "help5_x" => "During the process for exporting to an SQL file, you may choose to include the queries that create the table and columns.", + "help6" => "Export Data to SQL File", + "help6_x" => "During the process for exporting to an SQL file, you may choose to include the queries that populate the table(s) with the current records of the table(s).", + "help7" => "Add Drop Table to Exported SQL File", + "help7_x" => "During the process for exporting to an SQL file, you may choose to include queries to DROP the existing tables before adding them so that problems do not occur when trying to create tables that already exist.", + "help8" => "Add Transaction to Exported SQL File", + "help8_x" => "During the process for exporting to an SQL file, you may choose to wrap the queries around a TRANSACTION so that if an error occurs at any time during the importation process using the exported file, the database can be reverted to its previous state, preventing partially updated data from populating the database.", + "help9" => "Add Comments to Exported SQL File", + "help9_x" => "During the process for exporting to an SQL file, you may choose to include comments that explain each step of the process so that a human can better understand what is happening.", + "help10" => "Partial Indexes", + "help10_x" => "Partial indexes are indexes over a subset of the rows of a table specified by a WHERE clause. Note this requires at least SQLite 3.8.0 and database files with partial indexes won't be readable or writable by older versions. See the SQLite documentation.", + "help11" => "Maximum size of file uploads", + "help11_x" => "The maximum size of file uploads is determined by three PHP settings: upload_max_filesize, post_max_size and memory_limit. The smallest of these three limits the maximum size for file uploads. To upload larger files, adjust these values in your php.ini file." + +); + +// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +// there is no reason for the average user to edit anything below this comment +// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +// Authorization class +// Maintains user's logged-in state and security of application +// + +class Authorization +{ + private $authorized; + private $login_failed; + private $system_password_encrypted; + + public function __construct() + { + // first, make sure a CSRF token is generated + $this->generateToken(); + // second, check for possible CSRF attacks. to protect logins, this is done before checking login + $this->checkToken(); + + // the salt and password encrypting is probably unnecessary protection but is done just + // for the sake of being very secure + if(!isset($_SESSION[COOKIENAME.'_salt']) && !isset($_COOKIE[COOKIENAME.'_salt'])) + { + // create a random salt for this session if a cookie doesn't already exist for it + $_SESSION[COOKIENAME.'_salt'] = self::generateSalt(22); + } + else if(!isset($_SESSION[COOKIENAME.'_salt']) && isset($_COOKIE[COOKIENAME.'_salt'])) + { + // session doesn't exist, but cookie does so grab it + $_SESSION[COOKIENAME.'_salt'] = $_COOKIE[COOKIENAME.'_salt']; + } + + // salted and encrypted password used for checking + $this->system_password_encrypted = md5(SYSTEMPASSWORD."_".$_SESSION[COOKIENAME.'_salt']); + + $this->authorized = + // no password + SYSTEMPASSWORD == '' + // correct password stored in session + || isset($_SESSION[COOKIENAME.'password']) && hash_equals($_SESSION[COOKIENAME.'password'], $this->system_password_encrypted) + // correct password stored in cookie + || isset($_COOKIE[COOKIENAME]) && isset($_COOKIE[COOKIENAME.'_salt']) && hash_equals(md5(SYSTEMPASSWORD."_".$_COOKIE[COOKIENAME.'_salt']), $_COOKIE[COOKIENAME]); + } + + public function attemptGrant($password, $remember) + { + $hashed_password = crypt(SYSTEMPASSWORD, '$2a$07$'.self::generateSalt(22).'$'); + if (hash_equals($hashed_password, crypt($password, $hashed_password))) { + if ($remember) { + // user wants to be remembered, so set a cookie + $expire = time()+60*60*24*30; //set expiration to 1 month from now + setcookie(COOKIENAME, $this->system_password_encrypted, $expire, null, null, null, true); + setcookie(COOKIENAME."_salt", $_SESSION[COOKIENAME.'_salt'], $expire, null, null, null, true); + } else { + // user does not want to be remembered, so destroy any potential cookies + setcookie(COOKIENAME, "", time()-86400, null, null, null, true); + setcookie(COOKIENAME."_salt", "", time()-86400, null, null, null, true); + unset($_COOKIE[COOKIENAME]); + unset($_COOKIE[COOKIENAME.'_salt']); + } + + $_SESSION[COOKIENAME.'password'] = $this->system_password_encrypted; + $this->authorized = true; + return true; + } + + $this->login_failed = true; + return false; + } + + public function revoke() + { + //destroy everything - cookies and session vars + setcookie(COOKIENAME, "", time()-86400, null, null, null, true); + setcookie(COOKIENAME."_salt", "", time()-86400, null, null, null, true); + unset($_COOKIE[COOKIENAME]); + unset($_COOKIE[COOKIENAME.'_salt']); + session_unset(); + session_destroy(); + $this->authorized = false; + // start a new session and generate a new CSRF token for the login form + session_start(); + $this->generateToken(); + } + + public function isAuthorized() + { + return $this->authorized; + } + + public function isFailedLogin() + { + return $this->login_failed; + } + + public function isPasswordDefault() + { + return SYSTEMPASSWORD == 'admin'; + } + + private static function generateSalt($saltSize) + { + $set = 'ABCDEFGHiJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + $setLast = strlen($set) - 1; + $salt = ''; + while ($saltSize-- > 0) { + $salt .= $set[mt_rand(0, $setLast)]; + } + return $salt; + } + + private function generateToken() + { + // generate CSRF token + if (empty($_SESSION[COOKIENAME.'token'])) + { + if (function_exists('random_bytes')) // introduced in PHP 7.0 + { + $_SESSION[COOKIENAME.'token'] = bin2hex(random_bytes(32)); + } + elseif (function_exists('openssl_random_pseudo_bytes')) // introduced in PHP 5.3.0 + { + $_SESSION[COOKIENAME.'token'] = bin2hex(openssl_random_pseudo_bytes(32)); + } + else + { + // For PHP 5.2.x - This case can be removed once we drop support for 5.2.x + $_SESSION[COOKIENAME.'token'] = bin2hex(mcrypt_create_iv(32, MCRYPT_DEV_URANDOM)); + } + } + } + + private function checkToken() + { + // checking CSRF token + if($_SERVER['REQUEST_METHOD'] === 'POST' || isset($_GET['download'])) // all POST forms need tokens! downloads are protected as well + { + if($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['token'])) + $check_token=$_POST['token']; + elseif($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['token'])) + $check_token=$_GET['token']; + + if (!isset($check_token)) + { + die("CSRF token missing"); + } + elseif(!hash_equals($_SESSION[COOKIENAME.'token'], $check_token)) + { + die("CSRF token is wrong - please try to login again"); + } + } + } + +} +// Database class +// Generic database abstraction class to manage interaction with database without worrying about SQLite vs. PHP versions +// +class Database +{ + protected $db; //reference to the DB object + protected $type; //the extension for PHP that handles SQLite + protected $data; + protected $lastResult; + protected $alterError; + protected $debugOutput =''; + + public function __construct($data) + { + global $lang, $params; + $this->data = $data; + try + { + if(!file_exists($this->data["path"]) && !is_writable(dirname($this->data["path"]))) //make sure the containing directory is writable if the database does not exist + { + echo "
"; + printf($lang['db_not_writeable'], htmlencode($this->data["path"]), htmlencode(dirname($this->data["path"]))); + echo $params->getForm(); + echo ""; + echo ""; + echo "

"; + exit(); + } + + $ver = $this->getVersion(); + + switch(true) + { + case ((!isset($data['type']) || $data['type']!=2) && (FORCETYPE=="PDO" || (FORCETYPE==false && class_exists("PDO") && in_array("sqlite", PDO::getAvailableDrivers()) && ($ver==-1 || $ver==3)))): + $this->db = new PDO("sqlite:".$this->data['path']); + if($this->db!=NULL) + { + $this->type = "PDO"; + break; + } + case ((!isset($data['type']) || $data['type']!=2) && (FORCETYPE=="SQLite3" || (FORCETYPE==false && class_exists("SQLite3") && ($ver==-1 || $ver==3)))): + $this->db = new SQLite3($this->data['path']); + if($this->db!=NULL) + { + $this->type = "SQLite3"; + break; + } + case (FORCETYPE=="SQLiteDatabase" || (FORCETYPE==false && class_exists("SQLiteDatabase") && ($ver==-1 || $ver==2))): + $this->db = new SQLiteDatabase($this->data['path']); + if($this->db!=NULL) + { + $this->type = "SQLiteDatabase"; + break; + } + default: + $this->showError(); + exit(); + } + $this->query("PRAGMA foreign_keys = ON"); + } + catch(Exception $e) + { + $this->showError(); + exit(); + } + } + + public function registerUserFunction($ids) + { + // in case a single function id was passed + if (is_string($ids)) + $ids = array($ids); + + if ($this->type == 'PDO') { + foreach ($ids as $id) { + $this->db->sqliteCreateFunction($id, $id, -1); + } + } else { // type is Sqlite3 or SQLiteDatabase + foreach ($ids as $id) { + $this->db->createFunction($id, $id, -1); + } + } + } + + public function getError($complete_msg = false) + { + global $lang, $debug; + $error = "unknown"; + + if($this->alterError!='') + { + $error = $this->alterError; + $this->alterError = ""; + } + else if($this->type=="PDO") + { + $e = $this->db->errorInfo(); + $error = $e[2]; + } + else if($this->type=="SQLite3") + { + $error = $this->db->lastErrorMsg(); + } + else + { + $error = sqlite_error_string($this->db->lastError()); + } + + if($complete_msg) + { + $error = $lang['err'].": ".htmlencode($error); + // do not suggest to report a bug when constraints fail + if(strpos($error, 'constraint failed')===false) + $error.="
".$lang['bug_report'].' '.PROJECT_BUGTRACKER_LINK; + } + + if($debug) + $error .= $this->getDebugOutput(); + + return $error; + } + + function getDebugOutput() + { + return ($this->debugOutput != "" ? "
DEBUG:
".$this->debugOutput : $this->debugOutput); + } + + public function showError() + { + global $lang; + $classPDO = class_exists("PDO"); + $classSQLite3 = class_exists("SQLite3"); + $classSQLiteDatabase = class_exists("SQLiteDatabase"); + if($classPDO) // PDO is there, check if the SQLite driver for PDO is missing + $PDOSqliteDriver = (in_array("sqlite", PDO::getAvailableDrivers() )); + else + $PDOSqliteDriver = false; + echo "
"; + printf($lang['db_setup'], $this->getPath()); + echo ".

".$lang['chk_ext']."...

"; + echo "PDO: ".($classPDO ? $lang['installed'] : $lang['not_installed'])."
"; + echo "PDO SQLite Driver: ".($PDOSqliteDriver ? $lang['installed'] : $lang['not_installed'])."
"; + echo "SQLite3: ".($classSQLite3 ? $lang['installed'] : $lang['not_installed'])."
"; + echo "SQLiteDatabase: ".($classSQLiteDatabase ? $lang['installed'] : $lang['not_installed'])."
"; + echo "
...".$lang['done'].".


"; + if(!$classPDO && !$classSQLite3 && !$classSQLiteDatabase) + printf($lang['sqlite_ext_support'], PROJECT); + else + { + if(!$PDOSqliteDriver && !$classSQLite3 && $this->getVersion()==3) + printf($lang['sqlite_v_error'], 3, PROJECT, 2); + else if(!$classSQLiteDatabase && $this->getVersion()==2) + printf($lang['sqlite_v_error'], 2, PROJECT, 3); + else + { + if(!file_exists($this->getPath())) + { + if(touch($this->getPath())) + { + echo $lang['report_issue'].' '.PROJECT_BUGTRACKER_LINK.'.'; + } + else + { + echo "".$lang['filesystem_permission_denied'].""; + } + } + } + } + echo "

See ".PROJECT_INSTALL_LINK." for help.

"; + + $this->print_db_list(); + + echo "
"; + } + + // print the list of databases + public function print_db_list() + { + global $databases, $lang, $params, $currentDB; + echo "
".$lang['db_ch'].""; + if(sizeof($databases)<10) //if there aren't a lot of databases, just show them as a list of links instead of drop down menu + { + $i=0; + foreach($databases as $database) + { + $i++; + $name = $database['name']; + if(mb_strlen($name)>25) + $name = "...".mb_substr($name, mb_strlen($name)-22, 22); + echo '[' . ($database['readable'] ? 'r':' ' ) . ($database['writable'] && $database['writable_dir'] ? 'w':' ' ) . '] '; + + echo $params->getLink(array('database'=>$database['path'], 'table'=>null), htmlencode($name), ($database == $currentDB? 'active_db': '') ); + echo "  "; + echo $params->getLink(array('download'=>$database['path'], 'table'=>null, 'token'=>$_SESSION[COOKIENAME.'token']), '[↓]', '', $lang['backup']); + + if($i"; + } + } + else //there are a lot of databases - show a drop down menu + { + echo $params->getForm(array('table'=>null), 'get'); + echo ""; + echo ""; + echo ""; + } + echo "
"; + } + + public function __destruct() + { + if($this->db) + $this->close(); + } + + //get the exact PHP extension being used for SQLite + public function getType() + { + return $this->type; + } + + // get the version of the SQLite library + public function getSQLiteVersion() + { + $queryVersion = $this->select("SELECT sqlite_version() AS sqlite_version"); + return $queryVersion['sqlite_version']; + } + + //get the name of the database + public function getName() + { + return $this->data["name"]; + } + + //get the filename of the database + public function getPath() + { + return $this->data["path"]; + } + + //is the db-file writable? + public function isWritable() + { + return $this->data["writable"]; + } + + //is the db-folder writable? + public function isDirWritable() + { + return $this->data["writable_dir"]; + } + + //get the version of the database + public function getVersion() + { + if(file_exists($this->data['path'])) //make sure file exists before getting its contents + { + $content = strtolower(file_get_contents($this->data['path'], NULL, NULL, 0, 40)); //get the first 40 characters of the database file + $p = strpos($content, "** this file contains an sqlite 2"); //this text is at the beginning of every SQLite2 database + if($p!==false) //the text is found - this is version 2 + return 2; + else + return 3; + } + else //return -1 to indicate that it does not exist and needs to be created + { + return -1; + } + } + + //get the size of the database (in KiB) + public function getSize() + { + return round(filesize($this->data["path"])*0.0009765625, 1); + } + + //get the last modified time of database + public function getDate() + { + global $lang; + return date($lang['date_format'], filemtime($this->data['path'])); + } + + //get number of affected rows from last query + public function getAffectedRows() + { + if($this->type=="PDO") + if(!is_object($this->lastResult)) + // in case it was an alter table statement, there is no lastResult object + return 0; + else + return $this->lastResult->rowCount(); + else if($this->type=="SQLite3") + return $this->db->changes(); + else if($this->type=="SQLiteDatabase") + return $this->db->changes(); + } + + public function getTypeOfTable($table) + { + $result = $this->select("SELECT `type` FROM `sqlite_master` WHERE `name`=" . $this->quote($table), 'assoc'); + return $result['type']; + } + + public function getTableInfo($table) + { + return $this->selectArray("PRAGMA table_info(".$this->quote_id($table).")"); + } + + // returns the list of tables (opt. incl. views) as + // array( Tablename => tableType ) with tableType being 'view' or 'table' + public function getTables($alsoViews=true, $alsoInternal=false, $orderBy='name', $orderDirection='ASC') + { + $query = "SELECT name, type FROM sqlite_master " + . "WHERE (type='table'".($alsoViews?" OR type='view'":"").") " + . "AND name!='' ".($alsoInternal? "":" AND name NOT LIKE 'sqlite_%' ") + . "ORDER BY ".$this->quote_id($orderBy)." ".$orderDirection; + $result = $this->selectArray($query); + $list = array(); + for($i=0; $i array(columName) ) + public function getTableDefinitions() + { + $tables = $this->getTables(true, true); + $result = array(); + foreach ($tables as $tableName => $tableType) + { + $tableInfo = $this->getTableInfo($tableName); + $columns = array(); + foreach($tableInfo as $column) + $columns[] = $column['name']; + $result[$tableName] = $columns; + } + return $result; + } + + public function close() + { + if($this->type=="PDO") + $this->db = NULL; + else if($this->type=="SQLite3") + $this->db->close(); + else if($this->type=="SQLiteDatabase") + $this->db = NULL; + } + + public function beginTransaction() + { + $this->query("BEGIN"); + } + + public function commitTransaction() + { + $this->query("COMMIT"); + } + + public function rollbackTransaction() + { + $this->query("ROLLBACK"); + } + + //generic query wrapper + //returns false on error and the query result on success + public function query($query, $ignoreAlterCase=false) + { + global $debug; + if(strtolower(substr(ltrim($query),0,5))=='alter' && $ignoreAlterCase==false) //this query is an ALTER query - call the necessary function + { + preg_match("/^\s*ALTER\s+TABLE\s+(".$this->sqlite_surroundings_preg("+",false,",' \"\[`").")\s+(.*)$/i",$query,$matches); + if(!isset($matches[1]) || !isset($matches[2])) + { + if($debug) echo "SQL?
"; + return false; + } + $tablename = $this->sqliteUnquote($matches[1]); + $alterdefs = $matches[2]; + if($debug) echo "ALTER TABLE QUERY=(".htmlencode($query)."), tablename=($tablename), alterdefs=($alterdefs)
"; + $result = $this->alterTable($tablename, $alterdefs); + } + else //this query is normal - proceed as normal + { + $result = $this->db->query($query); + if($debug) echo "SQL?
"; + } + if($result===false) + return false; + $this->lastResult = $result; + return $result; + } + + //wrapper for an INSERT and returns the ID of the inserted row + public function insert($query) + { + $result = $this->query($query); + if($this->type=="PDO") + return $this->db->lastInsertId(); + else if($this->type=="SQLite3") + return $this->db->lastInsertRowID(); + else if($this->type=="SQLiteDatabase") + return $this->db->lastInsertRowid(); + } + + //returns an array for SELECT + public function select($query, $mode="both") + { + $result = $this->query($query); + if(!$result) //make sure the result is valid + return NULL; + if($this->type=="PDO") + { + if($mode=="assoc") + $mode = PDO::FETCH_ASSOC; + else if($mode=="num") + $mode = PDO::FETCH_NUM; + else + $mode = PDO::FETCH_BOTH; + $ret = $result->fetch($mode); + $result->closeCursor(); + return $ret; + } + else if($this->type=="SQLite3") + { + if($mode=="assoc") + $mode = SQLITE3_ASSOC; + else if($mode=="num") + $mode = SQLITE3_NUM; + else + $mode = SQLITE3_BOTH; + $ret = $result->fetchArray($mode); + $result->finalize(); + return $ret; + } + else if($this->type=="SQLiteDatabase") + { + if($mode=="assoc") + $mode = SQLITE_ASSOC; + else if($mode=="num") + $mode = SQLITE_NUM; + else + $mode = SQLITE_BOTH; + return $result->fetch($mode); + } + } + + //returns an array of arrays after doing a SELECT + public function selectArray($query, $mode="both") + { + $result = $this->query($query); + //make sure the result is valid + if($result=== false || $result===NULL) + return NULL; // error + if(!is_object($result)) // no rows returned + return array(); + if($this->type=="PDO") + { + if($mode=="assoc") + $mode = PDO::FETCH_ASSOC; + else if($mode=="num") + $mode = PDO::FETCH_NUM; + else + $mode = PDO::FETCH_BOTH; + $ret = $result->fetchAll($mode); + $result->closeCursor(); + return $ret; + } + else if($this->type=="SQLite3") + { + if($mode=="assoc") + $mode = SQLITE3_ASSOC; + else if($mode=="num") + $mode = SQLITE3_NUM; + else + $mode = SQLITE3_BOTH; + $arr = array(); + $i = 0; + while($res = $result->fetchArray($mode)) + { + $arr[$i] = $res; + $i++; + } + $result->finalize(); + return $arr; + } + else if($this->type=="SQLiteDatabase") + { + if($mode=="assoc") + $mode = SQLITE_ASSOC; + else if($mode=="num") + $mode = SQLITE_NUM; + else + $mode = SQLITE_BOTH; + return $result->fetchAll($mode); + } + } + + //returns an array of the next row in $result + public function fetch($result, $mode="both") + { + //make sure the result is valid + if($result=== false || $result===NULL) + return NULL; // error + if(!is_object($result)) // no rows returned + return array(); + if($this->type=="PDO") + { + if($mode=="assoc") + $mode = PDO::FETCH_ASSOC; + else if($mode=="num") + $mode = PDO::FETCH_NUM; + else + $mode = PDO::FETCH_BOTH; + return $result->fetch($mode); + } + else if($this->type=="SQLite3") + { + if($result->numColumns() === 0) { + return array(); + } + if($mode=="assoc") + $mode = SQLITE3_ASSOC; + else if($mode=="num") + $mode = SQLITE3_NUM; + else + $mode = SQLITE3_BOTH; + return $result->fetchArray($mode); + } + else if($this->type=="SQLiteDatabase") + { + if($mode=="assoc") + $mode = SQLITE_ASSOC; + else if($mode=="num") + $mode = SQLITE_NUM; + else + $mode = SQLITE_BOTH; + return $result->fetch($mode); + } + } + + public function getColumnName($result, $colNum) + { + //make sure the result is valid + if($result=== false || $result===NULL || !is_object($result)) + return ""; // error or no rows returned + if($this->type=="PDO") + { + $meta = $result->getColumnMeta($colNum); + return $meta['name']; + } + else if($this->type=="SQLite3") + { + return $result->columnName($colNum); + } + else if($this->type=="SQLiteDatabase") + { + return $result->fieldName($colNum); + } + } + + + // SQlite supports multiple ways of surrounding names in quotes: + // single-quotes, double-quotes, backticks, square brackets. + // As sqlite does not keep this strict, we also need to be flexible here. + // This function generates a regex that matches any of the possibilities. + private function sqlite_surroundings_preg($name,$preg_quote=true,$notAllowedCharsIfNone="'\"",$notAllowedName=false) + { + if($name=="*" || $name=="+") + { + if($notAllowedName!==false && $preg_quote) + $notAllowedName = preg_quote($notAllowedName,"/"); + // use possesive quantifiers to save memory + // (There is a bug in PCRE starting in 8.13 and fixed in PCRE 8.36 + // why we can't use posesive quantifiers - See issue #310). + if(version_compare(strstr(constant('PCRE_VERSION'), ' ', true), '8.36', '>=') || + version_compare(strstr(constant('PCRE_VERSION'), ' ', true), '8.12', '<=')) + $posessive='+'; + else + $posessive=''; + + $nameSingle = ($notAllowedName!==false?"(?!".$notAllowedName."')":"")."(?:[^']$name+|'')$name".$posessive; + $nameDouble = ($notAllowedName!==false?"(?!".$notAllowedName."\")":"")."(?:[^\"]$name+|\"\")$name".$posessive; + $nameBacktick = ($notAllowedName!==false?"(?!".$notAllowedName."`)":"")."(?:[^`]$name+|``)$name".$posessive; + $nameSquare = ($notAllowedName!==false?"(?!".$notAllowedName."\])":"")."(?:[^\]]$name+|\]\])$name".$posessive; + $nameNo = ($notAllowedName!==false?"(?!".$notAllowedName."\s)":"")."[^".$notAllowedCharsIfNone."]$name"; + } + else + { + if($preg_quote) $name = preg_quote($name,"/"); + + $nameSingle = str_replace("'","''",$name); + $nameDouble = str_replace('"','""',$name); + $nameBacktick = str_replace('`','``',$name); + $nameSquare = str_replace(']',']]',$name); + $nameNo = $name; + } + + $preg = "(?:'".$nameSingle."'|". // single-quote surrounded or not in quotes (correct SQL for values/new names) + $nameNo."|". // not surrounded (correct SQL if not containing reserved words, spaces or some special chars) + "\"".$nameDouble."\"|". // double-quote surrounded (correct SQL for identifiers) + "`".$nameBacktick."`|". // backtick surrounded (MySQL-Style) + "\[".$nameSquare."\])"; // square-bracket surrounded (MS Access/SQL server-Style) + return $preg; + } + + private function sqliteUnquote($quotedName) + { + $firstChar = $quotedName[0]; + $withoutFirstAndLastChar = substr($quotedName,1,-1); + switch($firstChar) + { + case "'": + case '"': + case '`': + $name = str_replace($firstChar.$firstChar,$firstChar,$withoutFirstAndLastChar); + break; + case '[': + $name = str_replace("]]","]",$withoutFirstAndLastChar); + break; + default: + $name = $quotedName; + } + return $name; + } + + // Returns the last PREG error as a string, '' if no error occured + private function getPregError() + { + $error = preg_last_error(); + switch ($error) + { + case PREG_NO_ERROR: return 'No error'; + case PREG_INTERNAL_ERROR: return 'There is an internal error!'; + case PREG_BACKTRACK_LIMIT_ERROR: return 'Backtrack limit was exhausted!'; + case PREG_RECURSION_LIMIT_ERROR: return 'Recursion limit was exhausted!'; + case PREG_BAD_UTF8_ERROR: return 'Bad UTF8 error!'; + // PREG_BAD_UTF8_OFFSET_ERROR is introduced in PHP 5.3.0, which is not yet required by PLA, so we use its value 5 instead so long + case 5: return 'Bad UTF8 offset error!'; + default: return 'Unknown Error'; + } + } + + // function that is called for an alter table statement in a query + // code borrowed with permission from http://code.jenseng.com/db/ + // this has been completely debugged / rewritten by Christopher Kramer + public function alterTable($table, $alterdefs) + { + global $debug, $lang; + $this->alterError=""; + $errormsg = sprintf($lang['alter_failed'],htmlencode($table)).' - '; + if($debug) $this->debugOutput .= "ALTER TABLE: table=($table), alterdefs=($alterdefs), PCRE version=(".PCRE_VERSION.")

"; + if($alterdefs != '') + { + $recreateQueries = array(); + $resultArr = $this->selectArray("SELECT sql,name,type FROM sqlite_master WHERE tbl_name = ".$this->quote($table)); + if(sizeof($resultArr)<1) + { + $this->alterError = $errormsg . sprintf($lang['tbl_inexistent'], htmlencode($table)); + if($debug) $this->debugOutput .= "ERROR: unknown table

"; + return false; + } + for($i=0; $idebugOutput .= "recreate=(".$row['sql'].";)
"; + } + } + elseif($row['type']=='view') // workaround to rename views + { + $origsql = $row['sql']; + $preg_remove_create_view = "/^\s*+CREATE\s++VIEW\s++".$this->sqlite_surroundings_preg($table)."\s*+(AS\s++SELECT\s++.*+)$/is"; + $origsql_no_create = preg_replace($preg_remove_create_view, '$1', $origsql, 1); + if($debug) $this->debugOutput .= "origsql=($origsql)
preg_remove_create_table=($preg_remove_create_view)
"; + preg_match("/RENAME\s++TO\s++(?:\"((?:[^\"]|\"\")+)\"|'((?:[^']|'')+)')/is", $alterdefs, $matches); + if(isset($matches[1]) && $matches[1]!='') + $newname = $matches[1]; + elseif(isset($matches[2]) && $matches[2]!='') + $newname = $matches[2]; + else + { + $this->alterError = $errormsg . ' could not detect new view name. It needs to be in single or double quotes.'; + if($debug) $this->debugOutput .= "ERROR: could not detect new view name
"; + return false; + } + $dropoldSQL = 'DROP VIEW '.$this->quote_id($table); + $createnewSQL = 'CREATE VIEW '.$this->quote_id($newname).' '.$origsql_no_create; + $alter_transaction = 'BEGIN; ' . $dropoldSQL .'; '. $createnewSQL . '; ' . 'COMMIT;'; + if($debug) $this->debugOutput .= $alter_transaction; + return $this->multiQuery($alter_transaction); + } + else + { + // ALTER the table + $tmpname = 't'.time(); + $origsql = $row['sql']; + $preg_remove_create_table = "/^\s*+CREATE\s++TABLE\s++".$this->sqlite_surroundings_preg($table)."\s*+(\(.*+)$/is"; + $origsql_no_create = preg_replace($preg_remove_create_table, '$1', $origsql, 1); + if($debug) $this->debugOutput .= "origsql=($origsql)
preg_remove_create_table=($preg_remove_create_table)
"; + if($origsql_no_create == $origsql) + { + $this->alterError = $errormsg . $lang['alter_tbl_name_not_replacable']; + if($debug) $this->debugOutput .= "ERROR: could not get rid of CREATE TABLE
"; + return false; + } + $createtemptableSQL = "CREATE TABLE ".$this->quote($tmpname)." ".$origsql_no_create; + if($debug) $this->debugOutput .= "createtemptableSQL=($createtemptableSQL)
"; + $createindexsql = array(); + $preg_alter_part = "/(?:DROP(?! PRIMARY KEY)(?: COLUMN)?|ADD(?! PRIMARY KEY)(?: COLUMN)?|CHANGE(?: COLUMN)?|RENAME TO|ADD PRIMARY KEY|DROP PRIMARY KEY)" // the ALTER command + ."(?:" + ."\s+\(".$this->sqlite_surroundings_preg("+",false,"\"'\[`)")."+\)" // stuff in brackets (in case of ADD PRIMARY KEY) + ."|" // or + ."\s+".$this->sqlite_surroundings_preg("+",false,",'\"\[`") // column names and stuff like this + .")*/i"; + if($debug) + $this->debugOutput .= "preg_alter_part=(".$preg_alter_part.")
"; + preg_match_all($preg_alter_part,$alterdefs,$matches); + $defs = $matches[0]; + + $result_oldcols = $this->getTableInfo($table); + $newcols = array(); + $coltypes = array(); + $primarykey = array(); + foreach($result_oldcols as $column_info) + { + $newcols[$column_info['name']] = $column_info['name']; + $coltypes[$column_info['name']] = $column_info['type']; + if($column_info['pk']) + $primarykey[] = $column_info['name']; + } + $newcolumns = ''; + $oldcolumns = ''; + reset($newcols); + foreach($newcols as $key => $val) + { + $newcolumns .= ($newcolumns?', ':'').$this->quote_id($val); + $oldcolumns .= ($oldcolumns?', ':'').$this->quote_id($key); + } + $copytotempsql = 'INSERT INTO '.$this->quote_id($tmpname).'('.$newcolumns.') SELECT '.$oldcolumns.' FROM '.$this->quote_id($table); + $dropoldsql = 'DROP TABLE '.$this->quote_id($table); + $createtesttableSQL = $createtemptableSQL; + if(count($defs)<1) + { + $this->alterError = $errormsg . $lang['alter_no_def']; + if($debug) $this->debugOutput .= "ERROR: defs<1

"; + return false; + } + foreach($defs as $def) + { + if($debug) $this->debugOutput .= "
def=$def
"; + $preg_parse_def = + "/^(DROP(?! PRIMARY KEY)(?: COLUMN)?|ADD(?! PRIMARY KEY)(?: COLUMN)?|CHANGE(?: COLUMN)?|RENAME TO|ADD PRIMARY KEY|DROP PRIMARY KEY)" // $matches[1]: command + ."(?:" // this is either + ."(?:\s+\((.+)\)\s*$)" // anything in brackets (for ADD PRIMARY KEY) + // then $matches[2] is what there is in brackets + ."|" // OR: + ."\s+(".$this->sqlite_surroundings_preg("+",false," \"'\[`").")"// $matches[3]: (first) column name, possibly including quotes + // (may be quoted in any type of quotes) + // in case of RENAME TO, it is the new a table name + ."(" // $matches[4]: anything after the column name + ."(?:\s+(".$this->sqlite_surroundings_preg("+",false," \"'\[`")."))?" // $matches[5] (optional): a second column name possibly including quotes + // (may be quoted in any type of quotes) + ."\s*" + ."((?:[A-Z]+\s*)+(?:\(\s*[+-]?\s*[0-9]+(?:\s*,\s*[+-]?\s*[0-9]+)?\s*\))?)?\s*" // $matches[6] (optional): a type name + .".*". + ")" + ."?\s*$" + .")?\s*$/i"; // in case of DROP PRIMARY KEY, there is nothing after the command + if($debug) $this->debugOutput .= "preg_parse_def=$preg_parse_def
"; + $parse_def = preg_match($preg_parse_def,$def,$matches); + if($parse_def===false) + { + $this->alterError = $errormsg . $lang['alter_parse_failed']; + if($debug) $this->debugOutput .= "ERROR: !parse_def

"; + return false; + } + if(!isset($matches[1])) + { + $this->alterError = $errormsg . $lang['alter_action_not_recognized']; + if($debug) $this->debugOutput .= "ERROR: !isset(matches[1])

"; + return false; + } + $action = str_replace(' column','',strtolower($matches[1])); + if($action == 'add primary key' && isset($matches[2]) && $matches[2]!='') + $column = $matches[2]; + elseif($action == 'drop primary key') + $column = ''; // DROP PRIMARY KEY has no column definition + elseif(isset($matches[3]) && $matches[3]!='') + $column = $this->sqliteUnquote($matches[3]); + else + $column = ''; + + $column_escaped = str_replace("'","''",$column); + + if($debug) $this->debugOutput .= "action=($action), column=($column), column_escaped=($column_escaped)
"; + + /* we build a regex that devides the CREATE TABLE statement parts: + Part example Group Explanation + 1. CREATE TABLE t... ( $1 + 2. 'col1' ..., 'col2' ..., 'colN' ..., $3 (with col1-colN being columns that are not changed and listed before the col to change) + 3. 'colX' ..., (with colX being the column to change/drop) + 4. 'colX+1' ..., ..., 'colK') $5 (with colX+1-colK being columns after the column to change/drop) + */ + $preg_create_table = "\s*+(CREATE\s++TABLE\s++".preg_quote($this->quote($tmpname),"/")."\s*+\()"; // This is group $1 (keep unchanged) + $preg_column_definiton = "\s*+".$this->sqlite_surroundings_preg("+",true," '\"\[`,",$column)."(?:\s*+".$this->sqlite_surroundings_preg("*",false,"'\",`\[ ").")++"; // catches a complete column definition, even if it is + // 'column' TEXT NOT NULL DEFAULT 'we have a comma, here and a double ''quote!' + // this definition does NOT match columns with the column name $column + if($debug) $this->debugOutput .= "preg_column_definition=(".$preg_column_definiton.")
"; + $preg_columns_before = // columns before the one changed/dropped (keep) + "(?:". + "(". // group $2. Keep this one unchanged! + "(?:". + "$preg_column_definiton,\s*+". // column definition + comma + ")*". // there might be any number of such columns here + $preg_column_definiton. // last column definition + ")". // end of group $2 + ",\s*+" // the last comma of the last column before the column to change. Do not keep it! + .")?"; // there might be no columns before + if($debug) $this->debugOutput .= "preg_columns_before=(".$preg_columns_before.")
"; + $preg_columns_after = "(,\s*(.+))?"; // the columns after the column to drop. This is group $3 (drop) or $4(change) (keep!) + // we could remove the comma using $6 instead of $5, but then we might have no comma at all. + // Keeping it leaves a problem if we drop the first column, so we fix that case in another regex. + $table_new = $table; + + switch($action) + { + case 'add': + if($column=='') + { + $this->alterError = $errormsg . ' (add) - '. $lang['alter_no_add_col']; + return false; + } + $new_col_definition = "'$column_escaped' ".(isset($matches[4])?$matches[4]:''); + $preg_pattern_add = "/^".$preg_create_table. // the CREATE TABLE statement ($1) + "((?:(?!,\s*(?:PRIMARY\s+KEY\s*\(|CONSTRAINT\s|UNIQUE\s*\(|CHECK\s*\(|FOREIGN\s+KEY\s*\()).)*)". // column definitions ($2) + "(.*)\\)\s*$/si"; // table-constraints like PRIMARY KEY(a,b) ($3) and the closing bracket + // append the column definiton in the CREATE TABLE statement + $newSQL = preg_replace($preg_pattern_add, '$1$2, '.strtr($new_col_definition, array('\\' => '\\\\', '$' => '\$')).' $3', $createtesttableSQL).')'; + $preg_error = $this->getPregError(); + if($debug) + { + $this->debugOutput .= $createtesttableSQL."

"; + $this->debugOutput .= $newSQL."

"; + $this->debugOutput .= $preg_pattern_add."

"; + } + if($newSQL==$createtesttableSQL) // pattern did not match, so column adding did not succed + { + $this->alterError = $errormsg . ' (add) - '.$lang['alter_pattern_mismatch'].'. PREG ERROR: '.$preg_error; + return false; + } + $createtesttableSQL = $newSQL; + break; + case 'change': + if(!isset($matches[5])) + { + $this->alterError = $errormsg . ' (change) - '.$lang['alter_col_not_recognized']; + return false; + } + $new_col_name = $matches[5]; + if(!isset($matches[6])) + $new_col_type = ''; + else + $new_col_type = $matches[6]; + $new_col_definition = "$new_col_name $new_col_type"; + $preg_column_to_change = "\s*".$this->sqlite_surroundings_preg($column)."(?:\s+".preg_quote($coltypes[$column]).")?(\s+(?:".$this->sqlite_surroundings_preg("*",false,",'\"`\[").")+)?"; + // replace this part (we want to change this column) + // group $3 contains the column constraints (keep!). the name & data type is replaced. + $preg_pattern_change = "/^".$preg_create_table.$preg_columns_before.$preg_column_to_change.$preg_columns_after."\s*\\)\s*$/s"; + + // replace the column definiton in the CREATE TABLE statement + $newSQL = preg_replace($preg_pattern_change, '$1$2,'.strtr($new_col_definition, array('\\' => '\\\\', '$' => '\$')).'$3$4)', $createtesttableSQL); + $preg_error = $this->getPregError(); + // remove comma at the beginning if the first column is changed + // probably somebody is able to put this into the first regex (using lookahead probably). + $newSQL = preg_replace("/^\s*(CREATE\s+TABLE\s+".preg_quote($this->quote($tmpname),"/")."\s+\(),\s*/",'$1',$newSQL); + if($debug) + { + $this->debugOutput .= "new_col_name=(".$new_col_name."), new_col_type=(".$new_col_type."), preg_column_to_change=(".$preg_column_to_change.")

"; + $this->debugOutput .= $createtesttableSQL."

"; + $this->debugOutput .= $newSQL."

"; + + $this->debugOutput .= $preg_pattern_change."

"; + } + if($newSQL==$createtesttableSQL || $newSQL=="") // pattern did not match, so column removal did not succed + { + $this->alterError = $errormsg . ' (change) - '.$lang['alter_pattern_mismatch'].'. PREG ERROR: '.$preg_error; + return false; + } + $createtesttableSQL = $newSQL; + $newcols[$column] = $this->sqliteUnquote($new_col_name); + break; + case 'drop': + $preg_column_to_drop = "\s*".$this->sqlite_surroundings_preg($column)."\s+(?:".$this->sqlite_surroundings_preg("*",false,",'\"\[`").")+"; // delete this part (we want to drop this column) + $preg_pattern_drop = "/^".$preg_create_table.$preg_columns_before.$preg_column_to_drop.$preg_columns_after."\s*\\)\s*$/s"; + + // remove the column out of the CREATE TABLE statement + $newSQL = preg_replace($preg_pattern_drop, '$1$2$3)', $createtesttableSQL); + $preg_error = $this->getPregError(); + // remove comma at the beginning if the first column is removed + // probably somebody is able to put this into the first regex (using lookahead probably). + $newSQL = preg_replace("/^\s*(CREATE\s+TABLE\s+".preg_quote($this->quote($tmpname),"/")."\s+\(),\s*/",'$1',$newSQL); + if($debug) + { + $this->debugOutput .= $createtesttableSQL."

"; + $this->debugOutput .= $newSQL."

"; + $this->debugOutput .= $preg_pattern_drop."

"; + } + if($newSQL==$createtesttableSQL || $newSQL=="") // pattern did not match, so column removal did not succed + { + $this->alterError = $errormsg . ' (drop) - '.$lang['alter_pattern_mismatch'].'. PREG ERROR: '.$preg_error; + return false; + } + $createtesttableSQL = $newSQL; + unset($newcols[$column]); + break; + case 'rename to': + // don't change column definition at all + $newSQL = $createtesttableSQL; + // only change the name of the table + $table_new = $column; + break; + case 'add primary key': + // we want to add a primary key for the column(s) stored in $column + $newSQL = preg_replace("/\)\s*$/", ", PRIMARY KEY (".$column.") )", $createtesttableSQL); + $createtesttableSQL = $newSQL; + break; + case 'drop primary key': + // we want to drop the primary key + if($debug) $this->debugOutput .= "DROP"; + if(sizeof($primarykey)==1) + { + // if not compound primary key, might be a column constraint -> try removal + $column = $primarykey[0]; + if($debug) $this->debugOutput .= "
Trying to drop column constraint for column $column
"; + /* + TODO: This does not work yet: + CREATE TABLE 't12' ('t1' INTEGER CONSTRAINT "bla" NOT NULL CONSTRAINT 'pk' PRIMARY KEY ); ALTER TABLE "t12" DROP PRIMARY KEY + This does: ! ! + CREATE TABLE 't12' ('t1' INTEGER CONSTRAINT bla NOT NULL CONSTRAINT 'pk' PRIMARY KEY ); ALTER TABLE "t12" DROP PRIMARY KEY + */ + $preg_column_to_change = "(\s*".$this->sqlite_surroundings_preg($column).")". // column ($3) + "(?:". // opt. type and column constraints + "(\s+(?:".$this->sqlite_surroundings_preg("(?:[^PC,'\"`\[]|P(?!RIMARY\s+KEY)|". + "C(?!ONSTRAINT\s+".$this->sqlite_surroundings_preg("+",false," ,'\"\[`")."\s+PRIMARY\s+KEY))",false,",'\"`\[").")*)". // column constraints before PRIMARY KEY ($3) + // primary key constraint (remove this!): + "(?:CONSTRAINT\s+".$this->sqlite_surroundings_preg("+",false," ,'\"\[`")."\s+)?". + "PRIMARY\s+KEY". + "(?:\s+(?:ASC|DESC))?". + "(?:\s+ON\s+CONFLICT\s+(?:ROLLBACK|ABORT|FAIL|IGNORE|REPLACE))?". + "(?:\s+AUTOINCREMENT)?". + "((?:".$this->sqlite_surroundings_preg("*",false,",'\"`\[").")*)". // column constraints after PRIMARY KEY ($4) + ")"; + // replace this part (we want to change this column) + // group $3 (column) $4 (constraints before) and $5 (constraints after) contain the part to keep + $preg_pattern_change = "/^".$preg_create_table.$preg_columns_before.$preg_column_to_change.$preg_columns_after."\s*\\)\s*$/si"; + + // replace the column definiton in the CREATE TABLE statement + $newSQL = preg_replace($preg_pattern_change, '$1$2,$3$4$5$6)', $createtesttableSQL); + // remove comma at the beginning if the first column is changed + // probably somebody is able to put this into the first regex (using lookahead probably). + $newSQL = preg_replace("/^\s*(CREATE\s+TABLE\s+".preg_quote($this->quote($tmpname),"/")."\s+\(),\s*/",'$1',$newSQL); + if($debug) + { + $this->debugOutput .= "preg_column_to_change=(".$preg_column_to_change.")

"; + $this->debugOutput .= $createtesttableSQL."

"; + $this->debugOutput .= $newSQL."

"; + + $this->debugOutput .= $preg_pattern_change."

"; + } + if($newSQL!=$createtesttableSQL && $newSQL!="") // pattern did match, so PRIMARY KEY constraint removed :) + { + $createtesttableSQL = $newSQL; + if($debug) $this->debugOutput .= "
SUCCEEDED
"; + } + else + { + if($debug) $this->debugOutput .= "NO LUCK"; + // TODO: try removing table constraint + return false; + } + $createtesttableSQL = $newSQL; + } else + // TODO: Try removing table constraint + return false; + + break; + default: + if($debug) $this->debugOutput .= 'ERROR: unknown alter operation!

'; + $this->alterError = $errormsg . $lang['alter_unknown_operation']; + return false; + } + } + $droptempsql = 'DROP TABLE '.$this->quote_id($tmpname); + + $createnewtableSQL = "CREATE TABLE ".$this->quote($table_new)." ".preg_replace("/^\s*CREATE\s+TABLE\s+'?".str_replace("'","''",preg_quote($tmpname,"/"))."'?\s+(.*)$/is", '$1', $createtesttableSQL, 1); + + $newcolumns = ''; + $oldcolumns = ''; + reset($newcols); + foreach($newcols as $key => $val) + { + $newcolumns .= ($newcolumns?', ':'').$this->quote_id($val); + $oldcolumns .= ($oldcolumns?', ':'').$this->quote_id($key); + } + $copytonewsql = 'INSERT INTO '.$this->quote_id($table_new).'('.$newcolumns.') SELECT '.$oldcolumns.' FROM '.$this->quote_id($tmpname); + } + } + $alter_transaction = 'BEGIN; '; + $alter_transaction .= $createtemptableSQL.'; '; //create temp table + $alter_transaction .= $copytotempsql.'; '; //copy to table + $alter_transaction .= $dropoldsql.'; '; //drop old table + $alter_transaction .= $createnewtableSQL.'; '; //recreate original table + $alter_transaction .= $copytonewsql.'; '; //copy back to original table + $alter_transaction .= $droptempsql.'; '; //drop temp table + + $preg_index="/^\s*(CREATE\s+(?:UNIQUE\s+)?INDEX\s+(?:".$this->sqlite_surroundings_preg("+",false," '\"\[`")."\s*)*ON\s+)(".$this->sqlite_surroundings_preg($table).")(\s*\((?:".$this->sqlite_surroundings_preg("+",false," '\"\[`")."\s*)*\)\s*)\s*$/i"; + foreach($recreateQueries as $recreate_query) + { + if($recreate_query['type']=='index') + { + // this is an index. We need to make sure the index is not on a column that we drop. If it is, we drop the index as well. + $indexInfos = $this->selectArray('PRAGMA index_info('.$this->quote_id($recreate_query['name']).')'); + foreach($indexInfos as $indexInfo) + { + if(!isset($newcols[$indexInfo['name']])) + { + if($debug) $this->debugOutput .= 'Not recreating the following index:

'.htmlencode($recreate_query['sql']).'

'; + // Index on a column that was dropped. Skip recreation. + continue 2; + } + } + } + // TODO: In case we renamed a column on which there is an index, we need to recreate the index with the column name adjusted. + + // recreate triggers / indexes + if($table == $table_new) + { + // we had no RENAME TO, so we can recreate indexes/triggers just like the original ones + $alter_transaction .= $recreate_query['sql'].';'; + } else + { + // we had a RENAME TO, so we need to exchange the table-name in the CREATE-SQL of triggers & indexes + switch ($recreate_query['type']) + { + case 'index': + $recreate_queryIndex = preg_replace($preg_index, '$1'.$this->quote_id(strtr($table_new, array('\\' => '\\\\', '$' => '\$'))).'$3 ', $recreate_query['sql']); + if($recreate_queryIndex!=$recreate_query['sql'] && $recreate_queryIndex != NULL) + $alter_transaction .= $recreate_queryIndex.';'; + else + { + // the CREATE INDEX regex did not match. this normally should not happen + if($debug) $this->debugOutput .= 'ERROR: CREATE INDEX regex did not match!?

'; + // just try to recreate the index originally (will fail most likely) + $alter_transaction .= $recreate_query['sql'].';'; + } + break; + + case 'trigger': + // TODO: IMPLEMENT + $alter_transaction .= $recreate_query['sql'].';'; + break; + default: + if($debug) $this->debugOutput .= 'ERROR: Unknown type '.htmlencode($recreate_query['type']).'

'; + $alter_transaction .= $recreate_query['sql'].';'; + } + } + } + $alter_transaction .= 'COMMIT;'; + if($debug) $this->debugOutput .= $alter_transaction; + return $this->multiQuery($alter_transaction); + } + } + + //multiple query execution + //returns true on success, false otherwise. Use getError() to fetch the error. + public function multiQuery($query) + { + if($this->type=="PDO") + $success = $this->db->exec($query); + else if($this->type=="SQLite3") + $success = $this->db->exec($query); + else + $success = $this->db->queryExec($query, $error); + return $success; + } + + + // checks whether a table has a primary key + public function hasPrimaryKey($table) + { + $table_info = $this->getTableInfo($table); + foreach($table_info as $row_id => $row_data) + { + if($row_data['pk']) + { + return true; + } + + } + return false; + } + + // Returns an array of columns by which rows can be uniquely adressed. + // For tables with a rowid column, this is always array('rowid') + // for tables without rowid, this is an array of the primary key columns. + public function getPrimaryKey($table) + { + $primary_key = array(); + // check if this table has a rowid + $getRowID = $this->select('SELECT ROWID FROM '.$this->quote_id($table).' LIMIT 0,1'); + if(isset($getRowID[0])) + // it has, so we prefer addressing rows by rowid + return array('rowid'); + else + { + // the table is without rowid, so use the primary key + $table_info = $this->getTableInfo($table); + if(is_array($table_info)) + { + foreach($table_info as $row_id => $row_data) + { + if($row_data['pk']) + $primary_key[] = $row_data['name']; + } + } + } + return $primary_key; + } + + // selects a row by a given key $pk, which is an array of values + // for the columns by which a row can be adressed (rowid or primary key) + public function wherePK($table, $pk) + { + $where = ""; + $primary_key = $this->getPrimaryKey($table); + foreach($primary_key as $pk_index => $column) + { + if($where!="") + $where .= " AND "; + $where .= $this->quote_id($column) . ' = '; + if(is_int($pk[$pk_index]) || is_float($pk[$pk_index])) + $where .= $pk[$pk_index]; + else + $where .= $this->quote($pk[$pk_index]); + } + return $where; + } + + //get number of rows in table + public function numRows($table, $dontTakeLong = false) + { + // as Count(*) can be slow on huge tables without PK, + // if $dontTakeLong is set and the size is > 2MB only count() if there is a PK + if(!$dontTakeLong || $this->getSize() <= 2000 || $this->hasPrimaryKey($table)) + { + $result = $this->select("SELECT Count(*) FROM ".$this->quote_id($table)); + return $result[0]; + } else + { + return '?'; + } + } + + //correctly escape a string to be injected into an SQL query + public function quote($value) + { + if($this->type=="PDO") + { + // PDO quote() escapes and adds quotes + return $this->db->quote($value); + } + else if($this->type=="SQLite3") + { + return "'".$this->db->escapeString($value)."'"; + } + else + { + return "'".sqlite_escape_string($value)."'"; + } + } + + //correctly escape an identifier (column / table / trigger / index name) to be injected into an SQL query + public function quote_id($value) + { + // double-quotes need to be escaped by doubling them + $value = str_replace('"','""',$value); + return '"'.$value.'"'; + } + + + //import sql + //returns true on success, error message otherwise + public function import_sql($query) + { + $import = $this->multiQuery($query); + if(!$import) + return $this->getError(); + else + return true; + } + + public function prepareQuery($query) + { + if($this->type=='PDO' || $this->type=='SQLite3') + return $this->db->prepare($query); + else + { + // here we are in trouble, SQLiteDatabase cannot prepare statements. + // we need to emulate prepare as best as we can + # todo: implement this + return null; + } + } + + public function bindValue($handle, $parameter, $value, $type) + { + if($this->type=='SQLite3') + { + $types = array( + 'bool'=>SQLITE3_INTEGER, + 'int'=>SQLITE3_INTEGER, + 'float'=>SQLITE3_FLOAT, + 'text'=>SQLITE3_TEXT, + 'blob'=>SQLITE3_BLOB, + 'null'=>SQLITE3_NULL); + if(!isset($types[$type])) + $type = 'text'; + // there is no SQLITE_BOOL, so check value and make sure it is 0/1 + if($type=='bool') + { + if($value===1 || $value===true) + $value=1; + elseif($value===0 || $value===false) + $value=0; + else + return false; + } + return $handle->bindValue($parameter, $value, $types[$type]); + } + if($this->type=='PDO') + { + $types = array( + 'bool'=>PDO::PARAM_BOOL, + 'int'=>PDO::PARAM_INT, + 'float'=>PDO::PARAM_STR, + 'text'=>PDO::PARAM_STR, + 'blob'=>PDO::PARAM_LOB, + 'null'=>PDO::PARAM_NULL); + if(!isset($types[$type])) + $type = 'text'; + // there is no PDO::PARAM_FLOAT, so we check it ourself + if($type=='float') + { + if(is_numeric($value)) + $value = (float) $value; + else + return false; + } + return $handle->bindValue($parameter, $value, $types[$type]); + } + else + # todo: workaround + return false; + + } + + public function executePrepared($handle, $fetchResult=false) + { + if($this->type=='PDO') + { + $ok=$handle->execute(); + if($fetchResult && $ok) + { + $res = $handle->fetchAll(); + $handle->closeCursor(); + return $res; + } + else + { + if($ok) + $handle->closeCursor(); + return $ok; + } + } + elseif($this->type=='SQLite3') + { + $resultset=$handle->execute(); + if($fetchResult && $resultset!==false) + { + $res = $resultset->fetchArray(); + $resultset->finalize(); + return $res; + } + else + { + if($resultset!==false) + $resultset->finalize(); + if($resultset===false) + return false; + else + return true; + } + } + else + { + #todo. + return false; + } + } + + //import csv + //returns true on success, error message otherwise + public function import_csv($filename, $table, $field_terminate, $field_enclosed, $field_escaped, $null, $fields_in_first_row) + { + @set_time_limit(-1); + $csv_handle = fopen($filename,'r'); + $csv_insert = "BEGIN;\n"; + $csv_number_of_rows = 0; + // PHP requires enclosure defined, but has no problem if it was not used + if($field_enclosed=="") $field_enclosed='"'; + // PHP requires escaper defined + if($field_escaped=="") $field_escaped='\\'; + // support tab delimiters + if($field_terminate=='\t') $field_terminate = "\t"; + while($csv_handle!==false && !feof($csv_handle)) + { + $csv_data = fgetcsv($csv_handle, 0, $field_terminate, $field_enclosed, $field_escaped); + if(is_array($csv_data) && ($csv_data[0] != NULL || count($csv_data)>1)) + { + $csv_number_of_rows++; + if($csv_number_of_rows==1) + { + if($this->getTypeOfTable($table)!="table") + { + // First,Create a new table + $csv_insert .="CREATE TABLE ".$this->quote($table)." ("; + $number_of_cols = count($csv_data); + foreach($csv_data as $csv_col => $csv_cell) + { + if($fields_in_first_row) + $csv_insert .= $this->quote($csv_cell); + else + $csv_insert.= $this->quote("col{$csv_col}"); + if($csv_col < $number_of_cols-1) + $csv_insert .= ", "; + } + $csv_insert .=");"; + + } else { + $number_of_cols = count($this->getTableInfo($table)); + } + if($fields_in_first_row) + continue; + } + $csv_insert .= "INSERT INTO ".$this->quote_id($table)." VALUES ("; + for($csv_col = 0; $csv_col < $number_of_cols; $csv_col++) + { + if(isset($csv_data[$csv_col])) + $csv_cell = $csv_data[$csv_col]; + else + $csv_cell = $null; + if($csv_cell == $null) + $csv_insert .= "NULL"; + else + $csv_insert.= $this->quote($csv_cell); + if($csv_col < $number_of_cols-1) + $csv_insert .= ","; + } + $csv_insert .= ");\n"; + + if($csv_number_of_rows % 5000 == 0) + { + $csv_insert .= "COMMIT;\nBEGIN;\n"; + } + } + } + if($csv_handle === false) + return "Error reading CSV file"; + else + { + $csv_insert .= "COMMIT;"; + fclose($csv_handle); + $import = $this->multiQuery($csv_insert); + if(!$import) + return $this->getError(); + else + return true; + } + } + + //export csv + public function export_csv($tables, $field_terminate, $field_enclosed, $field_escaped, $null, $crlf, $fields_in_first_row) + { + @set_time_limit(-1); + // we use \r\n if the _client_ OS is windows (as the exported file is downloaded to the client), \n otherwise + $crlf = (isset($_SERVER['HTTP_USER_AGENT']) && strpos($_SERVER['HTTP_USER_AGENT'], 'Win')!==false ? "\r\n" : "\n"); + + $query = "SELECT * FROM sqlite_master WHERE type='table' or type='view' ORDER BY type DESC"; + $result = $this->selectArray($query); + for($i=0; $igetTableInfo($result[$i]['tbl_name']); + $cols = array(); + for($z=0; $zquote_id($result[$i]['tbl_name']); + $table_result = $this->query($query); + $firstRow=true; + while($row = $this->fetch($table_result, "assoc")) + { + if(!$firstRow) + echo $crlf; + else + $firstRow=false; + + for($y=0; $ygetPath().$crlf; + echo "----".$crlf; + } + $query = "SELECT * FROM sqlite_master WHERE type='table' OR type='index' OR type='view' OR type='trigger' ORDER BY type='trigger', type='index', type='view', type='table'"; + $result = $this->selectArray($query); + + if($transaction) + echo "BEGIN TRANSACTION;".$crlf; + + //iterate through each table + for($i=0; $iquote_id($result[$i]['name']).";".$crlf; + } + if($structure) + { + if($comments) + { + echo "\r\n----".$crlf; + if($result[$i]['type']=="table" || $result[$i]['type']=="view") + echo "-- ".ucfirst($result[$i]['type'])." ".$lang['struct_for']." ".$result[$i]['tbl_name'].$crlf; + else // index or trigger + echo "-- ".$lang['struct_for']." ".$result[$i]['type']." ".$result[$i]['name']." ".$lang['on_tbl']." ".$result[$i]['tbl_name'].$crlf; + echo "----".$crlf; + } + echo $result[$i]['sql'].";".$crlf; + } + if($data && $result[$i]['type']=="table") + { + $query = "SELECT * FROM ".$this->quote_id($result[$i]['tbl_name']); + $table_result = $this->query($query, "assoc"); + + if($comments) + { + $numRows = $this->numRows($result[$i]['tbl_name']); + echo "\r\n----".$crlf; + echo "-- ".$lang['data_dump']." ".$result[$i]['tbl_name'].", ".sprintf($lang['total_rows'], $numRows).$crlf; + echo "----".$crlf; + } + $temp = $this->getTableInfo($result[$i]['tbl_name']); + $cols = array(); + $cols_quoted = array(); + for($z=0; $zquote_id($temp[$z][1]); + } + while($row = $this->fetch($table_result)) + { + $vals = array(); + for($y=0; $yquote($row[$cols[$y]]); + } + echo "INSERT INTO ".$this->quote_id($result[$i]['tbl_name'])." (".implode(",", $cols_quoted).") VALUES (".implode(",", $vals).");".$crlf; + } + } + } + } + if($transaction) + echo "COMMIT;".$crlf; + + if(!$echo) { + $o = ob_get_contents(); + ob_end_clean(); + return $o; + } + + } +} +class GetParameters +{ + private $_fields; + + public function __construct(array $defaults = array()) + { + $this->_fields = $defaults; + } + + public function __set($key, $value) + { + $this->_fields[$key] = $value; + } + + public function __isset($key) + { + return isset($this->_fields[$key]); + } + + public function __unset($key) + { + unset($this->_fields[$key]); + } + + public function __get($key) + { + return $this->_fields[$key]; + } + + public function getURL(array $assoc = array(), $html = true, $prefix='?') + { + $arg_sep = ($html?'&':'&'); + return $prefix . http_build_query(array_merge($this->_fields, $assoc), '', $arg_sep); + } + + public function getLink(array $assoc = array(), $content = '[ link ]', $class = '', $title = '', $target='') + { + return '' . $content . ''; + } + + public function getForm(array $assoc = array(), $method = 'post', $upload = false, $name = '', $csrf = true) + { + $hidden = ''; + if($method == 'get') + { + $url = ''; + foreach(array_merge($this->_fields, $assoc) as $key => $value) + { + if(!is_null($value)) + $hidden .= ' '; + } + } + else + $url = $this->getURL($assoc); + + if($csrf && $method == 'post') + $hidden .= ''; + + return "
" . + $hidden; + } + + public function redirect(array $assoc = array(), $message="") + { + if($message!="") + { + $_SESSION[COOKIENAME.'messages'][md5($message)] = $message; + $url = $this->getURL(array_merge($assoc, array('message'=>md5($message))), false); + } + else + $url = $this->getURL($assoc, false); + + $protocol = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off' ? 'https' : 'http'); + + header("Location: ".$protocol."://".$_SERVER['HTTP_HOST'].$_SERVER['PHP_SELF'].$url, true, 302); + exit; + } +}// class MicroTimer (issue #146) +// wraps calls to microtime(), calculating the elapsed time and rounding output +// +class MicroTimer { + + private $startTime, $stopTime; + + // creates and starts a timer + function __construct() + { + $this->startTime = microtime(true); + } + + // stops a timer + public function stop() + { + $this->stopTime = microtime(true); + } + + // returns the number of seconds from the timer's creation, or elapsed + // between creation and call to ->stop() + public function elapsed() + { + if ($this->stopTime) + return round($this->stopTime - $this->startTime, 4); + + return round(microtime(true) - $this->startTime, 4); + } + + // called when using a MicroTimer object as a string + public function __toString() + { + return (string) $this->elapsed(); + } + +} +// class Resources (issue #157) +// outputs secondary files, such as css and javascript +// data is stored gzipped (gzencode) and encoded (base64_encode) +// +class Resources { + + // set this to the file containing getInternalResource; + // currently unused in split mode; set to __FILE__ for built PLA. + public static $embedding_file = __FILE__; + + private static $_resources = array( + 'css' => array( + 'mime' => 'text/css', + 'data' => 'resources/phpliteadmin.css', + ), + 'javascript' => array( + 'mime' => 'text/javascript', + 'data' => 'resources/phpliteadmin.js', + ), + 'favicon' => array( + 'mime' => 'image/x-icon', + 'data' => 'resources/favicon.ico', + 'base64' => 'true', + ), + ); + + // outputs the specified resource, if defined in this class. + // the main script should do no further output after calling this function. + public static function output($resource) + { + if (isset(self::$_resources[$resource])) { + $res =& self::$_resources[$resource]; + + if (function_exists('getInternalResource') && $data = getInternalResource($res['data'])) { + $filename = self::$embedding_file; + } else { + $filename = $res['data']; + } + + // use last-modified time as etag; etag must be quoted + $etag = '"' . filemtime($filename) . '"'; + + // check headers for matching etag; if etag hasn't changed, use the cached version + if (isset($_SERVER['HTTP_IF_NONE_MATCH']) && $_SERVER['HTTP_IF_NONE_MATCH'] == $etag) { + header('HTTP/1.0 304 Not Modified'); + return; + } + + header('Etag: ' . $etag); + + // cache file for at most 30 days + header('Cache-control: max-age=2592000'); + + // output resource + header('Content-type: ' . $res['mime']); + + if (isset($data)) { + if (isset($res['base64'])) { + echo base64_decode($data); + } else { + echo $data; + } + } else { + readfile($filename); + } + } + } + +} + +if (version_compare(phpversion(), '5.2.4', '<')) { + die('Your PHP version is PHP '.phpversion().', which is too old. You need at least PHP 5.2.4.'); +} + +if(strpos(ini_get('variables_order'),'G') === false || strpos(ini_get('variables_order'),'P') === false || + strpos(ini_get('variables_order'),'C') === false || strpos(ini_get('variables_order'),'S') === false) { + die('The php configuration variables_order needs to include G, P, C and S. The current value is "'.ini_get('variables_order').'". Please check the php configuration (php.ini).'); +} + +//- Initialization + +// load optional configuration file +$config_filename = './phpliteadmin.config.php'; +if (is_readable($config_filename)) +{ + include_once $config_filename; +} + +//constants 1 +define("PROJECT", "phpLiteAdmin"); +define("VERSION", "1.9.9-dev"); +define("FORCETYPE", false); //force the extension that will be used (set to false in almost all circumstances except debugging, possible values: false, "PDO", "SQLite3", "SQLiteDatabase") +define("SYSTEMPASSWORD", $password); // Makes things easier. +define('PROJECT_URL','https://www.phpliteadmin.org/'); +define('DONATE_URL','https://www.phpliteadmin.org/donate/'); +define('VERSION_CHECK_URL','https://www.phpliteadmin.org/current_version.php'); +define('PROJECT_BUGTRACKER_LINK','https://bitbucket.org/phpliteadmin/public/issues?status=new&status=open'); +define('PROJECT_INSTALL_LINK','https://bitbucket.org/phpliteadmin/public/wiki/Installation'); + +// up here, we don't output anything. debug output might appear here which is catched by ob and thrown later +ob_start(); + +// Resource output (css and javascript files) +// we get out of the main code as soon as possible, without inizializing the session +if (isset($_GET['resource'])) +{ + Resources::output($_GET['resource']); + exit(); +} + +// don't mess with this - required for the login session +ini_set('session.cookie_httponly', '1'); +if(!session_start()) +{ + die("Could not start a new session. Check your php setup regarding sessions."); +} + +// version-number added so after updating, old session-data is not used anylonger +// cookies names cannot contain symbols, except underscores +define("COOKIENAME", preg_replace('/[^a-zA-Z0-9_]/', '_', $cookie_name . '_' . VERSION) ); + +$params = new GetParameters(); + +if($debug==true) +{ + ini_set("display_errors", 1); + error_reporting(E_STRICT | E_ALL); +} else +{ + @ini_set("display_errors", 0); +} + +// start the timer to record page load time +$pageTimer = new MicroTimer(); + +// load language file +if($language != 'en') { + $temp_lang=$lang; + if(is_file('languages/lang_'.$language.'.php')) + include('languages/lang_'.$language.'.php'); + elseif(is_file('lang_'.$language.'.php')) + include('lang_'.$language.'.php'); + $lang = array_merge($temp_lang, $lang); + unset($temp_lang); +} + +// stripslashes if MAGIC QUOTES is turned on +// This is only a workaround. Please better turn off magic quotes! +// This code is from http://php.net/manual/en/security.magicquotes.disabling.php +if (is_callable('get_magic_quotes_gpc') && get_magic_quotes_gpc()) { + $process = array(&$_GET, &$_POST, &$_COOKIE, &$_REQUEST); + foreach($process as $key => $val) { + foreach ($val as $k => $v) { + unset($process[$key][$k]); + if (is_array($v)) { + $process[$key][stripslashes($k)] = $v; + $process[] = &$process[$key][stripslashes($k)]; + } else { + $process[$key][stripslashes($k)] = stripslashes($v); + } + } + } + unset($process); +} + + +//data types array +$sqlite_datatypes = array("INTEGER", "REAL", "TEXT", "BLOB","NUMERIC","BOOLEAN","DATETIME"); + +//available SQLite functions array (don't add anything here or there will be problems) +$sqlite_functions = array("abs", "hex", "length", "lower", "ltrim", "random", "round", "rtrim", "trim", "typeof", "upper"); + +//- Support functions + +// for php < 5.6.0 +if(!function_exists('hash_equals')) +{ + function hash_equals($str1, $str2) + { + if(strlen($str1) != strlen($str2)) + return false; + else { + $res = $str1 ^ $str2; + $ret = 0; + for($i = strlen($res) - 1; $i >= 0; $i--) + $ret |= ord($res[$i]); + return !$ret; + } + } +} + +// workaround if mbsting extension is missing. Sure this means no multibyte support. +if(!function_exists('mb_strlen')) +{ + function mb_strlen($s) + { + return strlen($s); + } +} +if(!function_exists('mb_substr')) +{ + function mb_substr($s, $start, $length=null, $encoding=null) + { + return substr($s, $start, null === $length ? 2147483647 : $length); + } +} +// no other mbstring functions used so far + +//function that allows SQL delimiter to be ignored inside comments or strings +function explode_sql($delimiter, $sql) +{ + $ign = array('"' => '"', "'" => "'", "/*" => "*/", "--" => "\n"); // Ignore sequences. + $out = array(); + $last = 0; + $slen = strlen($sql); + $dlen = strlen($delimiter); + $i = 0; + while($i < $slen) + { + // Split on delimiter + if($slen - $i >= $dlen && substr($sql, $i, $dlen) == $delimiter) + { + array_push($out, substr($sql, $last, $i - $last)); + $last = $i + $dlen; + $i += $dlen; + continue; + } + // Eat comments and string literals + foreach($ign as $start => $end) + { + $ilen = strlen($start); + if($slen - $i >= $ilen && substr($sql, $i, $ilen) == $start) + { + $i+=strlen($start); + $elen = strlen($end); + while($i < $slen) + { + if($slen - $i >= $elen && substr($sql, $i, $elen) == $end) + { + // SQL comment characters can be escaped by doubling the character. This recognizes and skips those. + if($start == $end && $slen - $i >= $elen*2 && substr($sql, $i, $elen*2) == $end.$end) + { + $i += $elen * 2; + continue; + } + else + { + $i += $elen; + continue 3; + } + } + $i++; + } + continue 2; + } + } + $i++; + } + if($last < $slen) + array_push($out, substr($sql, $last, $slen - $last)); + return $out; +} + +//function to scan entire directory tree and subdirectories +function dir_tree($dir) +{ + $path = array(); + $stack = array($dir); + while($stack) + { + $thisdir = array_pop($stack); + if($dircont = scandir($thisdir)) + { + $i=0; + while(isset($dircont[$i])) + { + if($dircont[$i] !== '.' && $dircont[$i] !== '..') + { + $current_file = $thisdir.DIRECTORY_SEPARATOR.$dircont[$i]; + if(is_file($current_file)) + { + $path[] = $thisdir.DIRECTORY_SEPARATOR.$dircont[$i]; + } + elseif (is_dir($current_file)) + { + $path[] = $thisdir.DIRECTORY_SEPARATOR.$dircont[$i]; + $stack[] = $current_file; + } + } + $i++; + } + } + } + return $path; +} + +//the function echo the help [?] links to the documentation +function helpLink($name) +{ + global $lang; + return "[?]"; +} + +// function to encode value into HTML just like htmlentities, but with adjusted default settings +function htmlencode($value, $flags=ENT_QUOTES, $encoding ="UTF-8") +{ + return htmlentities($value, $flags, $encoding); +} + +// reduce string chars +function subString($str) +{ + global $charsNum, $params; + if($charsNum > 10 && (!isset($params->fulltexts) || !$params->fulltexts) && mb_strlen($str)>$charsNum) + { + $str = mb_substr($str, 0, $charsNum).'...'; + } + return $str; +} + +// marks searchwords and htmlencodes correctly +function markSearchWords($input, $field, $search) +{ + $output = htmlencode($input); + if(isset($search['values'][$field]) && is_array($search['values'][$field])) + { + // build one regex that matches (all) search words + $regex = '/'; + $vali=0; + foreach($search['values'][$field] as $searchValue) + { + if($search['operators'][$field] =='LIKE' || $search['operators'][$field] == 'LIKE%') + $regex .= '(?:'.($searchValue[0]=='%'?'':'^'); // does the searchvalue have to occur at the start? + $regex .= preg_quote(trim($searchValue,'%'),'/'); // the search value + if($search['operators'][$field] =='LIKE' || $search['operators'][$field] == 'LIKE%') + $regex .= (substr($searchValue,-1)=='%'?'':'$').')'; // does the searchvalue have to occur at the end? + if($vali++ $betweenPart) + { + $output .= htmlencode($betweenPart); // part that does not match (might be empty) + if(isset($fldFoundParts[0][$index])) + $output .= ''.htmlencode($fldFoundParts[0][$index]).''; // the part that matched + } + } + return $output; +} + +// checks the (new) name of a database file +function checkDbName($name) +{ + global $allowed_extensions; + $info = pathinfo($name); + if(isset($info['extension']) && !in_array($info['extension'], $allowed_extensions)) + { + return false; + } else + { + return (!is_file($name) && !is_dir($name)); + } + +} + +// check whether a path is a db managed by this tool +// requires that $databases is already filled! +// returns the key of the db if managed, false otherwise. +function isManagedDB($path) +{ + global $databases; + foreach($databases as $db_key => $database) + { + if($path === $database['path']) + { + // a db we manage. Thats okay. + // return the key. + return $db_key; + } + } + // not a db we manage! + return false; +} + +// from a typename of a colun, get the type of the column's affinty +// see https://www.sqlite.org/datatype3.html section 2.1 for rules +function get_type_affinity($type) +{ + if (preg_match("/INT/i", $type)) + return "INTEGER"; + else if (preg_match("/(?:CHAR|CLOB|TEXT)/i", $type)) + return "TEXT"; + else if (preg_match("/BLOB/i", $type) || $type=="") + return "NONE"; + else if (preg_match("/(?:REAL|FLOA|DOUB)/i", $type)) + return "REAL"; + else + return "NUMERIC"; +} + + +// Returns a file size limit in bytes based on the PHP upload_max_filesize +// post_max_size and memory_limit. Returns -1 in case of no limit. +function fileUploadMaxSize() +{ + $max1 = parseSize(ini_get('post_max_size')); + $max2 = parseSize(ini_get('upload_max_filesize')); + $max3 = parseSize(ini_get('memory_limit')); + if($max1>0 && ($max1<=$max2 || $max2==0) && ($max1<=$max3 || $max3==-1)) + return $max1; + elseif($max2>0 && ($max2<=$max1 || $max1==0) && ($max2<=$max3 || $max3==-1)) + return $max2; + elseif($max3>-1 && ($max3<=$max1 || $max1==0) && ($max3<=$max2 || $max2==0)) + return $max3; + else + return -1; // no limit +} + +// Parses given size string like "12M" into number of bytes +// based on https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Component%21Utility%21Bytes.php/function/Bytes%3A%3AtoInt/8.2.x +function parseSize($size) +{ + // Remove the non-unit characters from the size. + $unit = preg_replace('/[^bkmgtpezy]/i', '', $size); + // Remove the non-numeric characters from the size. + $size = preg_replace('/[^0-9\.]/', '', $size); + if ($unit) + { + // Find the position of the unit in the ordered string which is the power + // of magnitude to multiply a kilobyte by. + return round($size * pow(1024, stripos('bkmgtpezy', $unit[0]))); + } + else { + return round($size); + } +} + + +//- Check user authentication, login and logout +$auth = new Authorization(); //create authorization object + +// check if user has attempted to log out +if (isset($_GET['logout'])) + $auth->revoke(); +// check if user has attempted to log in +else if (isset($_POST['login']) && isset($_POST['password'])) +{ + $attempt = $auth->attemptGrant($_POST['password'], isset($_POST['remember'])); + $params->redirect( $attempt ? array():array('failed'=>'1') ); +} + +//- Actions on database files and bulk data +if ($auth->isAuthorized()) +{ + + //- Create a new database + if(isset($_POST['new_dbname'])) + { + if($_POST['new_dbname']=='') + $params->redirect(array('table'=>null), $lang['err'].': '.$lang['db_blank']); + else + { + $str = preg_replace('@[^\w\-.]@u','', $_POST['new_dbname']); + $dbname = $str; + $dbpath = $str; + if(checkDbName($dbname)) + { + $tdata = array(); + $tdata['name'] = $dbname; + $tdata['path'] = $directory.DIRECTORY_SEPARATOR.$dbpath; + if(isset($_POST['new_dbtype'])) + $tdata['type'] = $_POST['new_dbtype']; + else + $tdata['type'] = 3; + $td = new Database($tdata); + $td->query("VACUUM"); + } else + { + if(is_file($dbname) || is_dir($dbname)) + $params->redirect(array('view'=>'structure'),$lang['err'].': '.sprintf($lang['db_exists'], htmlencode($dbname))); + else + $params->redirect(array('view'=>'structure'),$lang['extension_not_allowed'].': '.implode(', ', array_map('htmlencode', $allowed_extensions)).'
'.$lang['add_allowed_extension']); + } + } + } + + //- Scan a directory for databases + if($directory!==false) + { + if($directory[strlen($directory)-1]==DIRECTORY_SEPARATOR) //if user has a trailing slash in the directory, remove it + $directory = substr($directory, 0, strlen($directory)-1); + + if(is_dir($directory)) //make sure the directory is valid + { + if($subdirectories===true) + $arr = dir_tree($directory); + else + $arr = scandir($directory); + $databases = array(); + $j = 0; + for($i=0; $i $database) + { + if($database['path'] === $tdata['path']) + { + $currentDB = $database; + $params->database = $database['path']; + break; + } + } + } + } + else //the directory is not valid - display error and exit + { + echo "
".$lang['not_dir']."
"; + exit(); + } + } + else + { + for($i=0; $idatabase = $databases[$db_key]['path']; + } + } + + //- Delete an existing database + if(isset($_GET['database_delete'])) + { + $dbpath = $_POST['database_delete']; + // check whether $dbpath really is a db we manage + $checkDB = isManagedDB($dbpath); + if($checkDB !== false) + { + unlink($dbpath); + unset($params->database); + unset($currentDB); + unset($databases[$checkDB]); + } else die($lang['err'].': '.$lang['delete_only_managed']); + } + + //- Rename an existing database + if(isset($_GET['database_rename'])) + { + $oldpath = $_POST['oldname']; + $newpath = $_POST['newname']; + $oldpath_parts = pathinfo($oldpath); + $newpath_parts = pathinfo($newpath); + // only rename? + $newpath = $oldpath_parts['dirname'].DIRECTORY_SEPARATOR.basename($_POST['newname']); + if($newpath != $_POST['newname'] && $subdirectories) + { + // it seems that the file should not only be renamed but additionally moved. + // we need to make sure it stays within $directory... + $new_realpath = realpath($newpath_parts['dirname']).DIRECTORY_SEPARATOR; + $directory_realpath = realpath($directory).DIRECTORY_SEPARATOR; + if(strpos($new_realpath, $directory_realpath)===0) + { + // its okay, the new directory is within $directory + $newpath = $_POST['newname']; + } + else $params->redirect(array('view'=>'rename'), $lang['err'].': '.$lang['db_moved_outside']); + } + + if(checkDbName($newpath)) + { + $checkDB = isManagedDB($oldpath); + if($checkDB !==false ) + { + rename($oldpath, $newpath); + $databases[$checkDB]['path'] = $newpath; + $databases[$checkDB]['name'] = basename($newpath); + $currentDB = $databases[$checkDB]; + $params->database = $databases[$checkDB]['path']; + $params->redirect(array('view'=>'rename'), sprintf($lang['db_renamed'], htmlencode($oldpath))." '".htmlencode($newpath)."'."); + } + else $params->redirect(array('view'=>'rename'), $lang['err'].': '.$lang['rename_only_managed']); + } + else + { + if(is_file($newpath) || is_dir($newpath)) + $params->redirect(array('view'=>'rename'), $lang['err'].": " . sprintf($lang['db_exists'], htmlencode($newpath))); + else + $params->redirect(array('view'=>'rename'), $lang['err'].": " . $lang['extension_not_allowed'].': '.implode(', ', array_map('htmlencode', $allowed_extensions)).'
'.$lang['add_allowed_extension']); + } + } + + + //- Export (download a dump) an existing database + if(isset($_POST['export'])) + { + ob_end_clean(); + $export_filename = str_replace(array("\r", "\n"), '',$_POST['filename']); // against http header injection (php < 5.1.2 only) + if($_POST['export_type']=="sql") + { + header('Content-Type: text/sql'); + header('Content-Disposition: attachment; filename="'.$export_filename.'.'.$_POST['export_type'].'";'); + if(isset($_POST['tables'])) + $tables = $_POST['tables']; + else + { + $tables = array(); + $tables[0] = $_POST['single_table']; + } + $drop = isset($_POST['drop']); + $structure = isset($_POST['structure']); + $data = isset($_POST['data']); + $transaction = isset($_POST['transaction']); + $comments = isset($_POST['comments']); + $db = new Database($currentDB); + $db->export_sql($tables, $drop, $structure, $data, $transaction, $comments); + } + else if($_POST['export_type']=="csv") + { + header("Content-type: application/csv"); + header('Content-Disposition: attachment; filename="'.$export_filename.'.'.$_POST['export_type'].'";'); + header("Pragma: no-cache"); + header("Expires: 0"); + if(isset($_POST['tables'])) + $tables = $_POST['tables']; + else + { + $tables = array(); + $tables[0] = $_POST['single_table']; + } + $field_terminate = $_POST['export_csv_fieldsterminated']; + $field_enclosed = $_POST['export_csv_fieldsenclosed']; + $field_escaped = $_POST['export_csv_fieldsescaped']; + $null = $_POST['export_csv_replacenull']; + $crlf = isset($_POST['export_csv_crlf']); + $fields_in_first_row = isset($_POST['export_csv_fieldnames']); + $db = new Database($currentDB); + $db->export_csv($tables, $field_terminate, $field_enclosed, $field_escaped, $null, $crlf, $fields_in_first_row); + } + exit(); + } + + //- Import a file into an existing database + if(isset($_POST['import'])) + { + $db = new Database($currentDB); + $db->registerUserFunction($custom_functions); + if($_POST['import_type']=="sql") + { + $data = file_get_contents($_FILES["file"]["tmp_name"]); + $importSuccess = $db->import_sql($data); + } + else + { + $field_terminate = $_POST['import_csv_fieldsterminated']; + $field_enclosed = $_POST['import_csv_fieldsenclosed']; + $field_escaped = $_POST['import_csv_fieldsescaped']; + $null = $_POST['import_csv_replacenull']; + $fields_in_first_row = isset($_POST['import_csv_fieldnames']); + if(isset($_POST['single_table']) && $_POST['single_table']!='') + $table = $_POST['single_table']; + else + { + $table = basename($_FILES["file"]["name"],".csv"); + $i=""; + while($db->getTypeOfTable($table.$i)!="") + { + if($i=="") + $i=2; + else + $i++; + } + $table = $table.$i; + } + $importSuccess = $db->import_csv($_FILES["file"]["tmp_name"], $table, $field_terminate, $field_enclosed, $field_escaped, $null, $fields_in_first_row); + } + } + //- Download (backup) a database file (as SQLite file, not as dump) + if(isset($_GET['download']) && isManagedDB($_GET['download'])!==false) + { + ob_end_clean(); + header("Content-type: application/octet-stream"); + header('Content-Disposition: attachment; filename="'.basename($_GET['download']).'";'); + header("Pragma: no-cache"); + header("Expires: 0"); + readfile($_GET['download']); + exit; + } + + //- Select database (from session or first available) + if(!isset($currentDB) && count($databases)>0) + { + //set the current database to the first existing one in the array (default) + $currentDB = reset($databases); + $params->database = $currentDB['path']; + } + + if(isset($currentDB)) + { + //- Open database (creates a Database object) + $db = new Database($currentDB); //create the Database object + $db->registerUserFunction($custom_functions); + } + + // collect parameters early, just once + $target_table = isset($_GET['table']) ? $_GET['table'] : null; + // are we working on a view? let's check once here + $target_table_type = !is_null($target_table) ? $db->getTypeOfTable($target_table) : null; + if(is_null($target_table_type) && !is_null($target_table)) + $params->redirect(array('table'=>null), $lang['err'].': '.sprintf($lang['tbl_inexistent'], htmlencode($target_table))); + $params->table = $target_table; + + // initialize / change fulltexts and numrows parameter + if(isset($_GET['fulltexts'])) + $params->fulltexts = ($_GET['fulltexts'] ? 1 : 0); + else + $params->fulltexts = 0; + + if(isset($_GET['numRows']) && intval($_GET['numRows'])>0) + $params->numRows = intval($_GET['numRows']); + else + $params->numRows = $rowsNum; + + //- Switch on $_GET['action'] for operations without output + if(isset($_GET['action']) && isset($_GET['confirm'])) + { + switch($_GET['action']) + { + //- Table actions + + //- Create table (=table_create) + case "table_create": + $num = intval($_POST['rows']); + $name = $_POST['tablename']; + $primary_keys = array(); + for($i=0; $i<$num; $i++) + { + if($_POST[$i.'_field']!="" && isset($_POST[$i.'_primarykey'])) + { + $primary_keys[] = $_POST[$i.'_field']; + } + } + $query = "CREATE TABLE ".$db->quote($name)." ("; + for($i=0; $i<$num; $i++) + { + if($_POST[$i.'_field']!="") + { + $query .= $db->quote($_POST[$i.'_field'])." "; + $query .= $_POST[$i.'_type']." "; + if(isset($_POST[$i.'_primarykey'])) + { + if(count($primary_keys)==1) + { + $query .= "PRIMARY KEY "; + if(isset($_POST[$i.'_autoincrement']) && $db->getType() != "SQLiteDatabase") + $query .= "AUTOINCREMENT "; + } + $query .= "NOT NULL "; + } + if(!isset($_POST[$i.'_primarykey']) && isset($_POST[$i.'_notnull'])) + $query .= "NOT NULL "; + if($_POST[$i.'_defaultoption']!='defined' && $_POST[$i.'_defaultoption']!='none' && $_POST[$i.'_defaultoption']!='expr') + $query .= "DEFAULT ".$_POST[$i.'_defaultoption']." "; + elseif($_POST[$i.'_defaultoption']=='expr') + $query .= "DEFAULT (".$_POST[$i.'_defaultvalue'].") "; + elseif(isset($_POST[$i.'_defaultvalue']) && $_POST[$i.'_defaultoption']=='defined') + { + $typeAffinity = get_type_affinity($_POST[$i.'_type']); + if(($typeAffinity=="INTEGER" || $typeAffinity=="REAL" || $typeAffinity=="NUMERIC") && is_numeric($_POST[$i.'_defaultvalue'])) + $query .= "DEFAULT ".$_POST[$i.'_defaultvalue']." "; + else + $query .= "DEFAULT ".$db->quote($_POST[$i.'_defaultvalue'])." "; + } + $query = substr($query, 0, -1); + $query .= ", "; + } + } + if (count($primary_keys)>1) + { + $compound_key = ""; + foreach ($primary_keys as $primary_key) + { + $compound_key .= ($compound_key=="" ? "" : ", ") . $db->quote($primary_key); + } + $query .= "PRIMARY KEY (".$compound_key."), "; + } + $query = substr($query, 0, -2); + $query .= ")"; + $result = $db->query($query); + if($result === false) + $completed = $db->getError(true); + else + $completed = $lang['tbl']." '".htmlencode($_POST['tablename'])."' ".$lang['created'].".
".htmlencode($query).""; + $params->redirect(($result===false ? array() : array('action'=>'column_view', 'table'=>$name) ), $completed); + break; + + //- Empty table (=table_empty) + case "table_empty": + if(isset($_GET['pk'])) + $tables = json_decode($_GET['pk']); + else + $tables=array($_GET['table']); + $query1 = "BEGIN; "; + foreach($tables as $table) + { + if($db->getTypeOfTable($table)=='table') + $query1 .= "DELETE FROM ".$db->quote_id($table)."; "; + } + $query1 .= "COMMIT; "; + $result1=$db->multiQuery($query1); + if($result1 === false) + $completed = $db->getError(true); + if(isset($_POST['vacuum']) && $_POST['vacuum']) + { + $query2 = "VACUUM;"; + $result2 = $db->query($query2); + } + else + $query2 = ""; + if($result1 !== false) + $completed = $lang['tbl']." '".htmlencode(implode(', ',$tables))."' ".$lang['emptied'].".
".htmlencode($query1)."
".htmlencode($query2)."
"; + if(count($tables)==1) + $action = array('action'=>'row_view'); + else + $action = array(); + $params->redirect(($result1===false ? array() : $action ), $completed); + break; + + //- Create view (=view_create) + case "view_create": + $query = "CREATE VIEW ".$db->quote($_POST['viewname'])." AS ".$_POST['select']; + $result = $db->query($query); + if($result === false) + $completed = $db->getError(true); + else + $completed = $lang['view']." '".htmlencode($_POST['viewname'])."' ".$lang['created'].".
".htmlencode($query).""; + $params->redirect(($result===false ? array() : array('action'=>'column_view', 'table'=>$_POST['viewname']) ), $completed); + break; + + //- Drop table (or view) (=table_drop) + case "table_drop": + if(isset($_GET['pk'])) + $tables = json_decode($_GET['pk']); + else + $tables=array($_GET['table']); + $query1 = "BEGIN; "; + foreach($tables as $table) + { + if($db->getTypeOfTable($table)=='table') + $query1 .= "DROP TABLE ".$db->quote_id($table)."; "; + else + $query1 .= "DROP VIEW ".$db->quote_id($table)."; "; + } + $query1 .= "COMMIT; "; + $result1=$db->multiQuery($query1); + if($result1 === false) + $completed = $db->getError(true); + if(isset($_POST['vacuum']) && $_POST['vacuum']) + { + $query2 = "VACUUM;"; + $result2 = $db->query($query2); + } + else + $query2 = ""; + if($result1 !== false) + { + $target_table = null; + $completed = $lang['tbl'].' / '.$lang['view']." '".htmlencode(implode(', ',$tables))."' ".$lang['dropped'].".
".htmlencode($query1)."
".htmlencode($query2)."
";; + } + $params->redirect(array('table'=>null), $completed); + break; + + //- Rename table (=table_rename) + case "table_rename": + $query = "ALTER TABLE ".$db->quote_id($_GET['table'])." RENAME TO ".$db->quote($_POST['newname']); + $type = $db->getTypeOfTable($_GET['table']); + if($db->getVersion()==3 && $type=='table' // SQLite 3 can rename tables, not views + // In SQL(ite) table names are case-insensitve, so changing is not supported by SQLite. + // But table names are stored and displayed case sensitive, so we use the workaround for case sensitive renaming. + && !($_GET['table'] !== $_POST['newname'] && strtolower($_GET['table']) === strtolower($_POST['newname'])) + ) + $result = $db->query($query, true); + else + // Workaround can rename tables of sqlite2 and views of both sqlite versions. Can also do case sensitive renames. + $result = $db->query($query, false); + if($result === false) + $completed = $db->getError(true); + else + { + $completed = $lang['tbl']." '".htmlencode($_GET['table'])."' ".$lang['renamed']." '".htmlencode($_POST['newname'])."'.
".htmlencode($query).""; + $target_table = $_POST['newname']; + } + $params->redirect(array('action'=>'row_view', 'table'=>$_POST['newname']), $completed); + break; + + //- Search table (=table_search) + case "table_search": + $searchValues = array(); + $searchOperators = array(); + + $tableInfo = $db->getTableInfo($target_table); + $j = 0; + $whereExpr = array(); + for($i=0; $iquote_id($field)." ".$operator; + else{ + if($operator == "LIKE%"){ + $operator = "LIKE"; + if(!preg_match('/(^%)|(%$)/', $value)) $value = '%'.$value.'%'; + $searchValues[$field] = array($value); + $valueQuoted = $db->quote($value); + } + elseif($operator == 'IN' || $operator == 'NOT IN') + { + $value = trim($value, '() '); + $values = explode(',',$value); + $values = array_map('trim', $values, array_fill(0,count($values),' \'"')); + if($operator == 'IN') + $searchValues[$field] = $values; + $values = array_map(array($db, 'quote'), $values); + $valueQuoted = '(' .implode(', ', $values) . ')'; + } + else + { + $searchValues[$field] = array($value); + $valueQuoted = $db->quote($value); + } + $whereExpr[$j] = $db->quote_id($field)." ".$operator." ".$valueQuoted; + } + $j++; + } + } + $searchWhere = ''; + if(sizeof($whereExpr)>0) + { + $searchWhere .= " WHERE ".$whereExpr[0]; + for($i=1; $i $searchWhere, + 'values' => $searchValues, + 'operators' => $searchOperators + ); + $params->redirect(array('action'=>'table_search','search'=>$searchID)); + break; + + //- Row actions + + //- Create row (=row_create) + case "row_create": + $completed = ""; + $num = $_POST['newRows']; + $z = 0; + $error = false; + + $tableInfo = $db->getTableInfo($target_table); + + for($i=0; $i<$num; $i++) + { + if(!isset($_POST[$i.":ignore"])) + { + $query_cols = ""; + $query_vals = ""; + $all_default = true; + for($j=0; $jquote_id($tableInfo[$j]['name']).","; + + $function = $_POST["function_".$j][$i]; + if($function!="") + $query_vals .= $function."("; + if(preg_match('/^BLOB/', $type) && !$hexblobs) + $query_vals .= ':blobval'.$j; + elseif(preg_match('/^BLOB/', $type) && $hexblobs) + $query_vals .= 'X'.$db->quote($value); + elseif(($typeAffinity=="TEXT" || $typeAffinity=="NONE") && !$null) + $query_vals .= $db->quote($value); + elseif(($typeAffinity=="INTEGER" || $typeAffinity=="REAL"|| $typeAffinity=="NUMERIC") && $value=="") + $query_vals .= "NULL"; + elseif($null) + $query_vals .= "NULL"; + else + $query_vals .= $db->quote($value); + if($function!="") + $query_vals .= ")"; + $query_vals .= ","; + } + $query = "INSERT INTO ".$db->quote_id($target_table); + if(!$all_default) + { + $query_cols = substr($query_cols, 0, strlen($query_cols)-1); + $query_vals = substr($query_vals, 0, strlen($query_vals)-1); + + $query.=" (". $query_cols . ") VALUES (". $query_vals. ")"; + } else { + $query .= " DEFAULT VALUES"; + } + if(isset($blobFiles)) + { + // blob files need to be done using a prepared statement because the query size would be too large + $handle = $db->prepareQuery($query); + foreach($blobFiles as $j=>$filename) + $db->bindValue($handle, ':blobval'.$j, file_get_contents($filename), 'blob'); + + $result1 = $db->executePrepared($handle, false); + } + else + $result1 = $db->query($query); + if($result1===false) + $error = true; + $completed .= "".htmlencode($query)."
"; + $z++; + } + } + if($error) + $completed = $db->getError(true) . $completed; + else + $completed = $z." ".$lang['rows']." ".$lang['inserted'].".

".$completed; + $params->redirect(array('action'=>'row_view'), $completed); + break; + + //- Delete row (=row_delete) + case "row_delete": + $pks = json_decode($_GET['pk']); + + $query = "DELETE FROM ".$db->quote_id($target_table)." WHERE (".$db->wherePK($target_table,json_decode($pks[0])).")"; + for($i=1; $iwherePK($target_table,json_decode($pks[$i])).")"; + } + $result = $db->query($query); + if($result === false) + $completed = $db->getError(true); + else + $completed = sizeof($pks)." ".$lang['rows']." ".$lang['deleted'].".
".htmlencode($query).""; + $params->redirect(array('action'=>'row_view'), $completed); + break; + + //- Edit row (=row_edit) + case "row_edit": + $pks = json_decode($_GET['pk']); + $z = 0; + + $tableInfo = $db->getTableInfo($target_table); + + if(isset($_POST['new_row'])) + $completed = ""; + else + $completed = sizeof($pks)." ".$lang['rows']." ".$lang['affected'].".

"; + + for($i=0; $iquote_id($tableInfo[$j]['name']).' AS \'blob\' FROM '.$db->quote_id($target_table).' WHERE '.$db->wherePK($target_table, json_decode($pks[$i])); + $bl = $db->select($select); + $blobFiles[$j] = $bl['blob']; + unset($bl); + } + else + { + if($_FILES[$i.":".$j]["error"] == UPLOAD_ERR_OK && is_file($_FILES[$i.":".$j]["tmp_name"])) + $blobFiles[$j] = file_get_contents($_FILES[$i.":".$j]["tmp_name"]); + else + $blobFiles[$j] = null; + } + } + else + $value = $_POST[$j][$i]; + } + else + $value = ""; + if(!preg_match('/^BLOB/', $type) && $value===$tableInfo[$j]['dflt_value']) + { + // if the value is the default value, skip it + continue; + } + $all_default = false; + $query_cols .= $db->quote_id($tableInfo[$j]['name']).","; + + $function = $_POST["function_".$j][$i]; + if($function!="") + $query_vals .= $function."("; + if(preg_match('/^BLOB/', $type) && !$hexblobs) + $query_vals .= ':blobval'.$j; + elseif(preg_match('/^BLOB/', $type) && $hexblobs) + $query_vals .= 'X'.$db->quote($value); + elseif(($typeAffinity=="TEXT" || $typeAffinity=="NONE") && !$null) + $query_vals .= $db->quote($value); + elseif(($typeAffinity=="INTEGER" || $typeAffinity=="REAL"|| $typeAffinity=="NUMERIC") && $value=="") + $query_vals .= "NULL"; + elseif($null) + $query_vals .= "NULL"; + else + $query_vals .= $db->quote($value); + if($function!="") + $query_vals .= ")"; + $query_vals .= ","; + } + $query = "INSERT INTO ".$db->quote_id($target_table); + if(!$all_default) + { + $query_cols = substr($query_cols, 0, strlen($query_cols)-1); + $query_vals = substr($query_vals, 0, strlen($query_vals)-1); + + $query.=" (". $query_cols . ") VALUES (". $query_vals. ")"; + } else { + $query .= " DEFAULT VALUES"; + } + + if(isset($blobFiles)) + { + // blob files need to be done using a prepared statement because the query size would be too large + $handle = $db->prepareQuery($query); + foreach($blobFiles as $j=>$blobval) + $db->bindValue($handle, ':blobval'.$j, $blobval, 'blob'); + + $result1 = $db->executePrepared($handle, false); + } + else + $result1 = $db->query($query); + if($result1===false) + $error = true; + $z++; + } + else + { + $query = "UPDATE ".$db->quote_id($target_table)." SET "; + for($j=0; $jquote_id($tableInfo[$j]['name'])."="; + if($function!="") + $query .= $function."("; + if($null) + $query .= "NULL"; + else + { + if(preg_match('/^BLOB/', $type) && !$hexblobs) + $query .= ':blobval'.$j; + elseif(preg_match('/^BLOB/', $type) && $hexblobs) + $query .= 'X'.$db->quote($_POST[$j][$i]); + else + $query .= $db->quote($_POST[$j][$i]); + } + if($function!="") + $query .= ")"; + $query .= ", "; + } + $query = substr($query, 0, -2); + $query .= " WHERE ".$db->wherePK($target_table, json_decode($pks[$i])); + if(isset($blobFiles)) + { + // blob files need to be done using a prepared statement because the query size would be too large + $handle = $db->prepareQuery($query); + foreach($blobFiles as $j=>$filename) + $db->bindValue($handle, ':blobval'.$j, file_get_contents($filename), 'blob'); + + $result1 = $db->executePrepared($handle, false); + } + else + $result1 = $db->query($query); + if($result1===false) + { + $error = true; + } + } + $completed .= "".htmlencode($query)."
"; + } + if($error) + $completed = $db->getError(true) . $completed; + elseif(isset($_POST['new_row'])) + $completed = $z." ".$lang['rows']." ".$lang['inserted'].".

".$completed; + $params->redirect(array('action'=>'row_view'), $completed); + break; + + + case "row_get_blob": + $blobVal = $db->select("SELECT ".$db->quote_id($_GET['column'])." AS 'blob' FROM ".$db->quote_id($target_table)." WHERE ".$db->wherePK($target_table, json_decode($_GET['pk']))); + $filename = 'download'; + if(function_exists('getimagesizefromstring')) // introduced in PHP 5.4.0 + $imagesize = getimagesizefromstring($blobVal['blob']); + if(isset($imagesize) && $imagesize!==false && isset($imagesize['mime'])) + $mimetype = $imagesize['mime']; + elseif(class_exists('finfo')) // included since php 5.3.0, but might be disabled on Windows + { + $finfo = new finfo(FILEINFO_MIME); + $mimetype = $finfo->buffer($blobVal['blob']); + } + else + $mimetype = "application/octet-stream"; + + if(isset($imagesize) && $imagesize!==false && isset($imagesize[2])) + $extension = image_type_to_extension($imagesize[2]); + else + $extension = '.blob'; + ob_end_clean(); + header('Content-Length: '.strlen($blobVal['blob'])); + header("Content-type: ".$mimetype); + if(isset($_GET['download_blob']) && $_GET['download_blob']) + header('Content-Disposition: attachment; filename="'.$filename.$extension.'";'); + header("Pragma: no-cache"); + header("Expires: 0"); + echo $blobVal['blob']; + exit; + break; + + + //- Column actions + + //- Create column (=column_create) + case "column_create": + $num = intval($_POST['rows']); + for($i=0; $i<$num; $i++) + { + if($_POST[$i.'_field']!="") + { + $query = "ALTER TABLE ".$db->quote_id($target_table)." ADD ".$db->quote($_POST[$i.'_field'])." "; + $query .= $_POST[$i.'_type']." "; + if(isset($_POST[$i.'_primarykey'])) + $query .= "PRIMARY KEY "; + if(isset($_POST[$i.'_notnull'])) + $query .= "NOT NULL "; + if($_POST[$i.'_defaultoption']!='defined' && $_POST[$i.'_defaultoption']!='none' && $_POST[$i.'_defaultoption']!='expr') + $query .= "DEFAULT ".$_POST[$i.'_defaultoption']." "; + elseif($_POST[$i.'_defaultoption']=='expr') + $query .= "DEFAULT (".$_POST[$i.'_defaultvalue'].") "; + elseif(isset($_POST[$i.'_defaultvalue']) && $_POST[$i.'_defaultoption']=='defined') + { + $typeAffinity = get_type_affinity($_POST[$i.'_type']); + if(($typeAffinity=="INTEGER" || $typeAffinity=="REAL" || $typeAffinity=="NUMERIC") && is_numeric($_POST[$i.'_defaultvalue'])) + $query .= "DEFAULT ".$_POST[$i.'_defaultvalue']." "; + else + $query .= "DEFAULT ".$db->quote($_POST[$i.'_defaultvalue'])." "; + } + if($db->getVersion()==3 && + ($_POST[$i.'_defaultoption']=='defined' || $_POST[$i.'_defaultoption']=='none' || $_POST[$i.'_defaultoption']=='NULL') + // Sqlite3 cannot add columns with default values that are not constant + && !isset($_POST[$i.'_primarykey']) + // sqlite3 cannot add primary key columns + && (!isset($_POST[$i.'_notnull']) || $_POST[$i.'_defaultoption']!='none') + // SQLite3 cannot add NOT NULL columns without DEFAULT even if the table is empty + ) + // use SQLITE3 ALTER TABLE ADD COLUMN + $result = $db->query($query, true); + else + // use ALTER TABLE workaround + $result = $db->query($query, false); + if($result===false) + $error = true; + } + } + if($error) + $completed = $db->getError(true); + else + $completed = $lang['tbl']." '".htmlencode($target_table)."' ".$lang['altered']."."; + $params->redirect(array('action'=>'column_view'), $completed); + break; + + //- Delete column (=column_delete) + case "column_delete": + $pks = explode(":", $_GET['pk']); + $query = "ALTER TABLE ".$db->quote_id($target_table).' DROP '.$db->quote_id($pks[0]); + for($i=1; $iquote_id($pks[$i]); + } + $result = $db->query($query); + if($result === false) + $completed = $db->getError(true); + else + $completed = $lang['tbl']." '".htmlencode($target_table)."' ".$lang['altered']."."; + $params->redirect(array('action'=>'column_view'), $completed); + break; + + //- Add a primary key (=primarykey_add) + case "primarykey_add": + $pks = explode(":", $_GET['pk']); + $query = "ALTER TABLE ".$db->quote_id($target_table).' ADD PRIMARY KEY ('.$db->quote_id($pks[0]); + for($i=1; $iquote_id($pks[$i]); + } + $query .= ")"; + $result = $db->query($query); + if($result === false) + $completed = $db->getError(true); + else + $completed = $lang['tbl']." '".htmlencode($target_table)."' ".$lang['altered']."."; + $params->redirect(array('action'=>'column_view'), $completed); + break; + + //- Edit column (=column_edit) + case "column_edit": + $query = "ALTER TABLE ".$db->quote_id($target_table).' CHANGE '.$db->quote_id($_POST['oldvalue'])." ".$db->quote($_POST['0_field'])." ".$_POST['0_type']; + $result = $db->query($query); + if($result === false) + $completed = $db->getError(true); + else + $completed = $lang['tbl']." '".htmlencode($target_table)."' ".$lang['altered']."."; + $params->redirect(array('action'=>'column_view'), $completed); + break; + + //- Delete trigger (=trigger_delete) + case "trigger_delete": + $query = "DROP TRIGGER ".$db->quote_id($_GET['pk']); + $result = $db->query($query); + if($result === false) + $completed = $db->getError(true); + else + $completed = $lang['trigger']." '".htmlencode($_GET['pk'])."' ".$lang['deleted'].".
".htmlencode($query).""; + $params->redirect(array('action'=>'column_view'), $completed); + break; + + //- Delete index (=index_delete) + case "index_delete": + $query = "DROP INDEX ".$db->quote_id($_GET['pk']); + $result = $db->query($query); + if($result === false) + $completed = $db->getError(true); + else + $completed = $lang['index']." '".htmlencode($_GET['pk'])."' ".$lang['deleted'].".
".htmlencode($query).""; + $params->redirect(array('action'=>'column_view'), $completed); + break; + + //- Create trigger (=trigger_create) + case "trigger_create": + $str = "CREATE TRIGGER ".$db->quote($_POST['trigger_name']); + if($_POST['beforeafter']!="") + $str .= " ".$_POST['beforeafter']; + $str .= " ".$_POST['event']." ON ".$db->quote_id($target_table); + if(isset($_POST['foreachrow'])) + $str .= " FOR EACH ROW"; + if($_POST['whenexpression']!="") + $str .= " WHEN ".$_POST['whenexpression']; + $str .= " BEGIN"; + $str .= " ".$_POST['triggersteps']; + $str .= " END"; + $query = $str; + $result = $db->query($query); + if($result === false) + $completed = $db->getError(true); + else + $completed = $lang['trigger']." ".$lang['created'].".
".htmlencode($query).""; + $params->redirect(array('action'=>'column_view'), $completed); + break; + + //- Create index (=index_create) + case "index_create": + $num = $_POST['num']; + if($_POST['name']=="") + { + $completed = $lang['blank_index']; + } + else if($_POST['0_field']=="") + { + $completed = $lang['one_index']; + } + else + { + $str = "CREATE "; + if($_POST['duplicate']=="no") + $str .= "UNIQUE "; + $str .= "INDEX ".$db->quote($_POST['name'])." ON ".$db->quote_id($target_table)." ("; + $str .= $db->quote_id($_POST['0_field']).$_POST['0_order']; + for($i=1; $i<$num; $i++) + { + if($_POST[$i.'_field']!="") + $str .= ", ".$db->quote_id($_POST[$i.'_field']).$_POST[$i.'_order']; + } + $str .= ")"; + if(isset($_POST['where']) && $_POST['where']!='') + $str.=" WHERE ".$_POST['where']; + $query = $str; + $result = $db->query($query); + if($result === false) + $completed = $db->getError(true); + else + $completed = $lang['index']." ".$lang['created'].".
".htmlencode($query).""; + } + $params->redirect(array('action'=>'column_view'), $completed); + break; + } + } +} + +// if not in debug mode, destroy all output until here +if($debug) + $bufferedOutput = ob_get_contents(); +ob_end_clean(); + +//- HTML: output starts here +header('Content-Type: text/html; charset=utf-8'); +?> + + + + + + +<?php echo PROJECT ?> + +", PHP_EOL; +else + // only use the default stylesheet if an external one does not exist + echo "", PHP_EOL; + +// HTML: output help text, then exit +if(isset($_GET['help'])) +{ + //help section array + $help = array($lang['help1'] => sprintf($lang['help1_x'], PROJECT, PROJECT, PROJECT)); + for($i=2; isset($lang['help'.$i]); $i++) + $help[$lang['help'.$i]]=$lang['help'.$i.'_x']; + ?> + + +
+ "; + echo "".PROJECT." v".VERSION." ".$lang['help_doc']."

"; + foreach((array)$help as $key => $val) + { + echo "".$key."
"; + } + echo "
"; + echo "

"; + foreach((array)$help as $key => $val) + { + echo "
"; + echo "".$key.""; + echo "
"; + echo $val; + echo "
"; + echo "".$lang['back_top'].""; + echo "
"; + } + ?> + + + + isAuthorized()) +{ + //- Javascript include + ?> + + + + + + + + + + + + + + +".$lang['bad_php_directive'].""; + echo ""; + exit(); +} + +//- HTML: login screen if not authorized, exit +if(!$auth->isAuthorized()) +{ + echo "
"; + echo "

v".VERSION."

"; + echo "
"; + if (isset($_GET['failed'])) + echo "".$lang['passwd_incorrect']."

"; + echo $params->getForm(); + echo $lang['passwd'].":
"; + echo "

"; + echo ""; + echo ""; + echo ""; + echo "
"; + echo "
"; + echo "
"; + echo "
"; + echo "".$lang['powered']." ".PROJECT." | "; + printf($lang['page_gen'], $pageTimer); + echo "
"; + echo ""; + exit(); +} + +//- User is authorized, display the main application + +if(count($databases)==0) // the database array is empty, offer to create a new database +{ + //- HTML: form to create a new database, exit + if($directory!==false && is_writable($directory) && (is_executable($directory) || DIRECTORY_SEPARATOR === '\\')) + { + echo "
"; + printf($lang['no_db'], PROJECT, PROJECT); + echo "
"; + //if the user has performed some action, show the resulting message + if(isset($_GET['message']) && isset($_SESSION[COOKIENAME.'messages'][$_GET['message']])) + { + echo "
"; + echo $_SESSION[COOKIENAME.'messages'][$_GET['message']]; + echo "

"; + unset($_SESSION[COOKIENAME.'messages'][$_GET['message']]); + } + echo "
".$lang['db_create'].""; + echo $params->getForm(array('table'=>null), 'post', false, 'create_database'); + echo " "; + if(class_exists('SQLiteDatabase') && (class_exists('SQLite3') || class_exists('PDO'))) + { + echo ""; + } + echo ""; + echo ""; + echo "
"; + } + elseif($directory!==false && !is_executable($directory) && DIRECTORY_SEPARATOR === '/') + { + echo "
"; + echo $lang['err'].": ".sprintf($lang['dir_not_executable'], PROJECT, $directory); + echo "

"; + } + else + { + echo "
"; + echo $lang['err'].": ".sprintf($lang['no_db2'], PROJECT); + echo "

"; + } + exit(); +} + +//- HTML: sidebar +echo '
'; +echo "
"; +echo "

"; +echo " v".VERSION.""; +echo "

"; +echo ""; + +//- HTML: database list +$db->print_db_list(); +echo "
"; +echo "null))."'"; +if (!$target_table) + echo " class='active_table'"; +$name = $currentDB['name']; +if(strlen($name)>25) + $name = "...".substr($name, strlen($name)-22, 22); +echo ">".htmlencode($name).""; +echo ""; + +//- HTML: table list +$tables = $db->getTables(true, false); +foreach($tables as $tableName => $tableType) +{ + echo ""; + echo $params->getLink(array('action'=>'column_view', 'table'=>$tableName), "[".$lang[$tableType=='table'?'tbl':'view']."]"); + echo " "; + echo $params->getLink(array('action'=>'row_view', 'table'=>$tableName), htmlencode($tableName), + ($target_table == $tableName ? 'active_table' : '') ); + echo "
"; +} +if(count($tables)==0) + echo $lang['no_tbl']; +echo "
"; + +//- HTML: form to create a new database +if($directory!==false && is_writable($directory)) +{ + echo "
".$lang['db_create']." ".helpLink($lang['help2']).""; + echo $params->getForm(array('table'=>null), 'post', false, 'create_database'); + echo ""; + if(class_exists('SQLiteDatabase') && (class_exists('SQLite3') || class_exists('PDO'))) + { + echo ""; + } + echo ""; + echo ""; + echo "
"; +} + +echo "
"; +echo $params->getForm(array(),'get'); +echo ""; +echo ""; +echo "
"; +echo "
"; +echo '
'; + +//- HTML: breadcrumb navigation +echo $params->getLink(array('table'=>null), htmlencode($currentDB['name'])); +if ($target_table) + echo " → ".$params->getLink(array('action'=>'row_view'), htmlencode($target_table)); +echo "

"; + +//- Show the various tab views for a table +if($target_table) +{ + //- HTML: tabs + echo $params->getLink(array('action'=>'row_view'), $lang['browse'], + (in_array($_GET['action'], array('row_view', 'row_editordelete') ) ? 'tab_pressed' : 'tab')); + + echo $params->getLink(array('action'=>'column_view'), $lang['struct'], + (in_array($_GET['action'], array('column_view', 'column_edit', 'column_confirm', 'primarykey_add', 'column_create', 'index_create', 'index_delete', 'trigger_create', 'trigger_delete') ) ? 'tab_pressed' : 'tab')); + + echo $params->getLink(array('action'=>'table_sql'), $lang['sql'], + ($_GET['action']=="table_sql" ? 'tab_pressed' : 'tab')); + + echo $params->getLink(array( + 'action' => 'table_search', + 'oldSearch' => (isset($_GET['search'])?$_GET['search']:null) + ), $lang['srch'], ($_GET['action']=="table_search" ? 'tab_pressed' : 'tab')); + + if($target_table_type == 'table' && $db->isWritable() && $db->isDirWritable()) + echo $params->getLink(array('action'=>'row_create'), $lang['insert'], + ($_GET['action']=="row_create" ? 'tab_pressed' : 'tab')); + + echo $params->getLink(array('action'=>'table_export'), $lang['export'], + ($_GET['action']=="table_export" ? 'tab_pressed' : 'tab')); + + if($target_table_type == 'table' && $db->isWritable() && $db->isDirWritable()) + echo $params->getLink(array('action'=>'table_import'), $lang['import'], + ($_GET['action']=="table_import" ? 'tab_pressed' : 'tab')); + + if($db->isWritable() && $db->isDirWritable()) + echo $params->getLink(array('action'=>'table_rename'), $lang['rename'], + ($_GET['action']=="table_rename" ? 'tab_pressed' : 'tab')); + + if($target_table_type == 'table' && $db->isWritable() && $db->isDirWritable()) + { + echo $params->getLink(array('action'=>'table_confirm','action2'=>'table_empty'), $lang['empty'], + (isset($_GET['action2']) && $_GET['action2']=="table_empty" ? 'tab_pressed empty' : 'tab empty')); + + echo $params->getLink(array('action'=>'table_confirm','action2'=>'table_drop'), $lang['drop'], + (isset($_GET['action2']) && $_GET['action2']=="table_drop" ? 'tab_pressed drop' : 'tab drop')); + } elseif($db->isWritable() && $db->isDirWritable()) { + echo $params->getLink(array('action'=>'table_confirm','action2'=>'table_drop'), $lang['drop'], + (isset($_GET['action2']) && $_GET['action2']=="table_drop" ? 'tab_pressed drop' : 'tab drop')); + } +} +else +//- Show the various tab views for a database +{ + $view = isset($_GET['view']) ? $_GET['view'] : 'structure'; + + echo $params->getLink(array('view'=>'structure'), $lang['struct'], ($view=="structure" ? 'tab_pressed': 'tab') ); + + echo $params->getLink(array('view'=>'sql'), $lang['sql'], ($view=="sql" ? 'tab_pressed': 'tab') ); + + echo $params->getLink(array('view'=>'export'), $lang['export'], ($view=="export" ? 'tab_pressed': 'tab') ); + + if($db->isWritable() && $db->isDirWritable()) + echo $params->getLink(array('view'=>'import'), $lang['import'], ($view=="import" ? 'tab_pressed': 'tab') ); + + if($db->isWritable() && $db->isDirWritable()) + echo $params->getLink(array('view'=>'vacuum'), $lang['vac'], ($view=="vacuum" ? 'tab_pressed': 'tab') ); + + if($directory!==false && is_writable($directory)) + { + + echo $params->getLink(array('view'=>'rename'), $lang['db_rename'], ($view=="rename" ? 'tab_pressed': 'tab') ); + + echo $params->getLink(array('view'=>'delete'), "".$lang['db_del']."", ($view=="delete" ? 'tab_pressed delete_db': 'tab delete_db') ); + } +} + +echo "
"; +echo "
"; + +//- HTML: confirmation panel +//if the user has performed some action, show the resulting message +if(isset($_GET['message']) && isset($_SESSION[COOKIENAME.'messages'][$_GET['message']])) +{ + echo "
"; + echo $_SESSION[COOKIENAME.'messages'][$_GET['message']]; + echo "

"; + unset($_SESSION[COOKIENAME.'messages'][$_GET['message']]); +} + + +//- Switch on $_GET['action'] for operations with output +if(isset($_GET['action']) && !isset($_GET['confirm'])) +{ + switch($_GET['action']) + { + //- Table actions + + //- Confirm table action (=table_confirm) + case "table_confirm": + if(isset($_GET['check'])) + $pks = $_GET['check']; + elseif(isset($_GET['table'])) + $pks = array($_GET['table']); + else $pks = array(); + + if(sizeof($pks)==0) //nothing was selected so show an error + { + echo "
"; + echo $lang['err'].": ".$lang['no_sel']; + echo "
"; + echo "

"; + echo $params->getLink(array(), $lang['return']); + } + else + { + echo $params->getForm(array('action'=>$_GET['action2'], 'confirm'=>'1', 'pk'=>json_encode($pks))); + echo "
"; + printf($lang['ques_'.$_GET['action2']], htmlencode(implode(', ',$pks)), htmlencode($target_table)); + echo "

"; + echo " ".$lang['vac_on_empty']."

"; + echo " "; + if(count($pks)==1) + $action = array('action'=>'row_view'); + else + $action = array('table'=>null); + echo $params->getLink($action, $lang['cancel']); + echo "
"; + } + break; + + //- Create table (=table_create) + case "table_create": + $query = "SELECT name FROM sqlite_master WHERE type='table' AND name=".$db->quote($_GET['tablename']); + $results = $db->selectArray($query); + if(sizeof($results)>0) + $exists = true; + else + $exists = false; + echo "

".$lang['create_tbl'].": '".htmlencode($_GET['tablename'])."'

"; + if($_GET['tablefields']=="" || intval($_GET['tablefields'])<=0) + echo $lang['specify_fields']; + else if($_GET['tablename']=="") + echo $lang['specify_tbl']; + else if($exists) + echo $lang['tbl_exists']; + else + { + $num = intval($_GET['tablefields']); + $name = $_GET['tablename']; + echo $params->getForm(array('action'=>'table_create', 'confirm'=>'1')); + echo ""; + echo ""; + echo ""; + echo ""; + $headings = array($lang['fld'], $lang['type'], $lang['prim_key']); + if($db->getType() != "SQLiteDatabase") $headings[] = $lang['autoincrement']; + $headings[] = $lang['not_null']; + $headings[] = $lang['def_val']; + for($k=0; $k" . $headings[$k] . ""; + echo ""; + + for($i=0; $i<$num; $i++) + { + $tdWithClass = ""; + echo $tdWithClass; + echo ""; + echo ""; + echo $tdWithClass; + echo ""; + echo ""; + echo $tdWithClass; + echo ""; + echo ""; + if($db->getType() != "SQLiteDatabase") + { + echo $tdWithClass; + echo ""; + echo ""; + } + echo $tdWithClass; + echo ""; + echo ""; + echo $tdWithClass; + echo ""; + echo ""; + echo ""; + echo ""; + } + echo ""; + echo ""; + echo ""; + echo "
"; + echo "
"; + echo " "; + echo $params->getLink(array(), $lang['cancel']); + echo "
"; + echo ""; + if($db->getType() != "SQLiteDatabase") echo ""; + } + break; + + //- Perform SQL query on table (=table_sql) + case "table_sql": + if(isset($_POST['query']) && $_POST['query']!="") + { + $delimiter = $_POST['delimiter']; + $queryStr = $_POST['queryval']; + //save the queries in history if necessary + if($maxSavedQueries!=0 && $maxSavedQueries!=false) + { + if(!isset($_SESSION[COOKIENAME.'query_history'])) + $_SESSION[COOKIENAME.'query_history'] = array(); + $_SESSION[COOKIENAME.'query_history'][md5(strtolower($queryStr))] = $queryStr; + if(sizeof($_SESSION[COOKIENAME.'query_history']) > $maxSavedQueries) + array_shift($_SESSION[COOKIENAME.'query_history']); + } + $query = explode_sql($delimiter, $queryStr); //explode the query string into individual queries based on the delimiter + + for($i=0; $iquery($query[$i]); + + echo "
"; + echo "".htmlencode($query[$i]).""; + if($table_result === NULL || $table_result === false) + { + echo "
".$lang['err'].": ".htmlencode($db->getError())."
"; + } + echo "

"; + if($row = $db->fetch($table_result, 'num')) + { + for($j=0; $jgetColumnName($table_result,$j); + echo ""; + echo ""; + for($j=0; $j"; + echo htmlencode($headers[$j]); + echo ""; + } + echo ""; + $rowCount = 0; + for(; $rowCount==0 || $row = $db->fetch($table_result, 'num'); $rowCount++) + { + $tdWithClass = ""; + for($z=0; $zNULL"; + else + echo htmlencode(subString($row[$z])); + echo ""; + } + echo ""; + } + $queryTimer->stop(); + echo "
"; + echo "


"; + + + if($table_result !== NULL && $table_result !== false) + { + echo "
"; + if($rowCount>0 || $db->getAffectedRows()==0) + { + printf($lang['show_rows'], $rowCount); + } + if($db->getAffectedRows()>0 || $rowCount==0) + { + echo $db->getAffectedRows()." ".$lang['rows_aff']." "; + } + printf($lang['query_time'], $queryTimer); + echo "
"; + } + + + } + } + } + } + else + { + $delimiter = ";"; + $queryStr = "SELECT * FROM ".$db->quote_id($target_table)." WHERE 1"; + } + + echo "
"; + echo "".sprintf($lang['run_sql'],htmlencode($db->getName())).""; + echo $params->getForm(array('action'=>'table_sql')); + if(isset($_SESSION[COOKIENAME.'query_history']) && sizeof($_SESSION[COOKIENAME.'query_history'])>0) + { + echo "".$lang['recent_queries']."
    "; + foreach($_SESSION[COOKIENAME.'query_history'] as $key => $value) + echo "
  • ".htmlencode($value)."
  • "; + echo "


"; + } + echo "
"; + echo ""; + echo ""; + echo "
"; + echo "
"; + echo $lang['fields']."
"; + echo ""; + echo ""; + echo "
"; + echo "
"; + echo $lang['delimit']." "; + echo ""; + echo ""; + echo "
"; + break; + + //- Export table (=table_export) + case "table_export": + echo $params->getForm(); + echo "
".$lang['export'].""; + echo ""; + echo ""; + echo "
"; + echo "
"; + + echo "
".$lang['options'].""; + echo " ".helpLink($lang['help5'])."
"; + echo " ".helpLink($lang['help6'])."
"; + echo " ".helpLink($lang['help7'])."
"; + echo " ".helpLink($lang['help8'])."
"; + echo " ".helpLink($lang['help9'])."
"; + echo "
"; + + echo ""; + + echo "
"; + echo "

"; + echo "
".$lang['save_as'].""; + $file = pathinfo($db->getPath()); + $name = $file['filename']; + echo " "; + echo "
"; + echo ""; + echo "
".sprintf($lang['backup_hint'], + $params->getLink(array('download' => $currentDB['path'], 'token' => $_SESSION[COOKIENAME.'token']), $lang["backup_hint_linktext"], '', $lang['backup']))."
"; + break; + + //- Import table (=table_import) + case "table_import": + if(isset($_POST['import'])) + { + echo "
"; + if($importSuccess===true) + echo $lang['import_suc']; + else + echo $lang['err'].': '.htmlencode($importSuccess); + echo "

"; + } + echo $params->getForm(array('action' => 'table_import'), 'post', true); + echo "
".$lang['import_into']." ".htmlencode($target_table).""; + echo ""; + echo "
"; + echo "
"; + + echo "
".$lang['options'].""; + echo $lang['no_opt']; + echo "
"; + + echo ""; + + echo "
"; + echo "

"; + + echo "
".$lang['import_f'].""; + echo "".$lang['max_file_size'].": ".number_format(fileUploadMaxSize()/1024/1024)." MiB ".helpLink($lang['help11'])."
"; + echo ""; + echo ""; + echo "
"; + break; + + //- Rename table (=table_rename) + case "table_rename": + echo $params->getForm(array('action'=>'table_rename', 'confirm'=>'1')); + printf($lang['rename_tbl'], htmlencode($target_table)); + echo " "; + echo ""; + break; + + //- Search table (=table_search) + case "table_search": + if(!isset($_GET['search'])) + { + $tableInfo = $db->getTableInfo($target_table); + + echo $params->getForm(array('action'=>'table_search', 'confirm'=>'1')); + + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + + for($i=0; $i"; + $tdWithClassLeft = ""; + echo $tdWithClassLeft; + echo htmlencode($field); + echo ""; + echo $tdWithClassLeft; + echo htmlencode($type); + echo ""; + echo $tdWithClassLeft; + echo ""; + echo ""; + echo $tdWithClassLeft; + if($typeAffinity=="INTEGER" || $typeAffinity=="REAL" || $typeAffinity=="NUMERIC") + echo ""; + else + echo ""; + echo ""; + echo ""; + } + echo ""; + echo ""; + echo ""; + echo "
".$lang['fld']."".$lang['type']."".$lang['operator']."".$lang['val']."
"; + if(isset($_GET['oldSearch']) && isset($_SESSION[COOKIENAME.'search'][$_GET['oldSearch']]['values'][$field])) + $value = implode($_SESSION[COOKIENAME.'search'][$_GET['oldSearch']]['values'][$field], ","); + else + $value = ''; + if(isset($_GET['oldSearch']) && isset($_SESSION[COOKIENAME.'search'][$_GET['oldSearch']]['operators'][$field])) + $operator = $_SESSION[COOKIENAME.'search'][$_GET['oldSearch']]['operators'][$field]; + elseif($typeAffinity=="TEXT" || $typeAffinity=="NONE") + $operator = 'LIKE'; + else + $operator = '='; + + echo "
"; + echo ""; + echo "
"; + echo ""; + + break; + } + elseif(isset($_SESSION[COOKIENAME.'search'][$_GET['search']])) + { + $params->search = $_GET['search']; + $search = $_SESSION[COOKIENAME.'search'][$_GET['search']]; + // NOTICE: we do not break here!! we just do the same now like row_view-action does + } + + //- Row actions + + //- View row (=row_view) + case "row_view": + if(!isset($_GET['startRow'])) + $_GET['startRow'] = 0; + + if(isset($_SESSION[COOKIENAME.'currentTable']) && $_SESSION[COOKIENAME.'currentTable']!=$target_table) + { + unset($_SESSION[COOKIENAME.'sortRows']); + unset($_SESSION[COOKIENAME.'orderRows']); + } + if(isset($_GET['viewtype'])) + { + $_SESSION[COOKIENAME.'viewtype'] = $_GET['viewtype']; + } + + //- Query execution + if(!isset($_GET['sort'])) + $_GET['sort'] = NULL; + if(!isset($_GET['order'])) + $_GET['order'] = NULL; + + $numRows = $params->numRows; + $startRow = $_GET['startRow']; + if(isset($_GET['sort'])) + { + $_SESSION[COOKIENAME.'sortRows'] = $_GET['sort']; + $_SESSION[COOKIENAME.'currentTable'] = $target_table; + } + if(isset($_GET['order'])) + { + $_SESSION[COOKIENAME.'orderRows'] = $_GET['order']; + $_SESSION[COOKIENAME.'currentTable'] = $target_table; + } + $query = "SELECT * "; + // select the primary key column(s) last (ROWID if there is no PK). + // this will be used to identify rows, e.g. when editing/deleting rows + $primary_key = $db->getPrimaryKey($target_table); + foreach($primary_key as $pk) + { + $query.= ', '.$db->quote_id($pk); + $query.= ', typeof('.$db->quote_id($pk).')'; + } + $query .= " FROM ".$db->quote_id($target_table); + $queryDisp = "SELECT * FROM ".$db->quote_id($target_table); + $queryCount = "SELECT COUNT(*) AS count FROM ".$db->quote_id($target_table); + $queryAdd = ""; + if(isset($search) && isset($search['where'])) + { + $queryAdd = $search['where']; + $queryCount .= $search['where']; + } + if(isset($_SESSION[COOKIENAME.'sortRows'])) + $queryAdd .= " ORDER BY ".$db->quote_id($_SESSION[COOKIENAME.'sortRows']); + if(isset($_SESSION[COOKIENAME.'orderRows'])) + $queryAdd .= " ".$_SESSION[COOKIENAME.'orderRows']; + $queryAdd .= " LIMIT ".$startRow.", ".$numRows; + $query .= $queryAdd; + $queryDisp .= $queryAdd; + + $resultRows = $db->select($queryCount); + $totalRows = $resultRows['count']; + $shownRows = min($resultRows['count']-$startRow, $numRows); + + //- HTML: pagination buttons + $lastPage = intval($totalRows / $params->numRows); + $remainder = intval($totalRows % $params->numRows); + if($remainder==0) + $remainder = $params->numRows; + + echo "
"; + //previous button + if($_GET['startRow']>0) + { + echo "
"; + echo $params->getForm(array('action'=>$_GET['action']),'get'); + echo ""; + echo " "; + echo ""; + echo "
"; + echo "
"; + echo $params->getForm(array('action'=>$_GET['action']),'get'); + echo "numRows))."'/>"; + echo " "; + echo ""; + echo "
"; + } + + //show certain number buttons + echo "
"; + echo $params->getForm(array('action'=>$_GET['action'], 'numRows'=>null),'get'); + echo " "; + echo " "; + echo $lang['rows_records']; + + if(intval($_GET['startRow']+$params->numRows) < $totalRows) + echo "numRows)."'/>"; + else + echo " "; + echo $lang['as_a']; + echo " "; + echo ""; + echo "
"; + + //next button + if(intval($_GET['startRow']+$params->numRows)<$totalRows) + { + echo "
"; + echo $params->getForm(array('action'=>$_GET['action']),'get'); + echo "numRows)."'/>"; + echo " "; + echo ""; + echo "
"; + echo "
"; + echo $params->getForm(array('action'=>$_GET['action']),'get'); + echo ""; + echo " "; + echo ""; + echo "
"; + } + echo "
"; + echo "
"; + + + //- Show results + if($shownRows>0) + { + $queryTimer = new MicroTimer(); + $table_result = $db->query($query); + $queryTimer->stop(); + + + echo "
"; + echo "".$lang['showing_rows']." ".$startRow." - ".($startRow + $shownRows-1).", ".$lang['total'].": ".$totalRows." "; + printf($lang['query_time'], $queryTimer); + echo "
"; + echo "".htmlencode($queryDisp).""; + echo "

"; + + if($target_table_type == 'view') + { + echo sprintf($lang['readonly_tbl'], htmlencode($target_table))." https://en.wikipedia.org/wiki/View_(SQL)"; + echo "

"; + } + + $tableInfo = $db->getTableInfo($target_table); + $pkFirstCol = sizeof($tableInfo)+1; + //- Table view + if(!isset($_SESSION[COOKIENAME.'viewtype']) || $_SESSION[COOKIENAME.'viewtype']=="table") + { + echo $params->getForm(array('action'=>'row_editordelete'), 'post', false, 'checkForm'); + echo ""; + echo ""; + echo ""; + + for($i=0; $i"; + if(isset($_SESSION[COOKIENAME.'sortRows'])) + $orderTag = ($_SESSION[COOKIENAME.'sortRows']==$tableInfo[$i]['name'] && $_SESSION[COOKIENAME.'orderRows']=="ASC") ? "DESC" : "ASC"; + else + $orderTag = "ASC"; + echo $params->getLink(array('action'=>$_GET['action'], 'sort'=>$tableInfo[$i]['name'], 'order'=>$orderTag ), htmlencode($tableInfo[$i]['name'])); + if(isset($_SESSION[COOKIENAME.'sortRows']) && $_SESSION[COOKIENAME.'sortRows']==$tableInfo[$i]['name']) + echo (($_SESSION[COOKIENAME.'orderRows']=="ASC") ? " " : " "); + echo ""; + } + echo ""; + + for($i=0; $row = $db->fetch($table_result, 'num'); $i++) + { + // -g-> $pk will always be the last columns in each row of the array because we are doing "SELECT *, PK_1, typeof(PK_1), PK2, typeof(PK_2), ... FROM ..." + $pk_arr = array(); + for($col = $pkFirstCol; array_key_exists($col, $row); $col=$col+2) + { + // in $col we have the type and in $col-1 the value + if($row[$col]=='integer' || $row[$col]=='real') + // json encode as int or float, not string + $pk_arr[] = $row[$col-1]+0; + else + // encode as json string + $pk_arr[] = $row[$col-1]; + } + $pk = json_encode($pk_arr); + $tdWithClass = ""; + if($target_table_type == 'table' && $db->isWritable() && $db->isDirWritable()) + { + echo $tdWithClass; + echo ""; + echo ""; + echo $tdWithClass; + // -g-> Here, we need to put the PK in as the link for both the edit and delete. + echo $params->getLink(array('action'=>'row_editordelete', 'pk'=>$pk, 'type'=>'edit'),"".$lang['edit']."",'edit', $lang['edit']); + echo ""; + echo $tdWithClass; + echo $params->getLink(array('action'=>'row_editordelete', 'pk'=>$pk, 'type'=>'delete'),"".$lang['del']."",'delete', $lang['del']); + echo ""; + } else { + echo ""; + } + for($j=0; $jNULL"; + elseif(preg_match('/^BLOB/i', $tableInfo[$j]['type']) && !$hexblobs) + { + echo "
"; + echo $params->getLink(array('action'=>'row_get_blob', 'confirm'=>1, 'pk'=>$pk, 'column'=>$tableInfo[$j]['name'], 'download_blob'=>1),$lang["download"]).' | '; + echo $params->getLink(array('action'=>'row_get_blob', 'confirm'=>1, 'pk'=>$pk, 'column'=>$tableInfo[$j]['name'], 'download_blob'=>0),$lang["open_in_browser"],'','','_blank'); + echo "
"; + echo 'Size: '.number_format(strlen($row[$j])).' Bytes'; + echo "
"; + } + elseif(preg_match('/^BLOB/i', $tableInfo[$j]['type']) && $hexblobs) + { + echo htmlencode(subString(bin2hex($row[$j]))); + } + elseif(isset($search)) + echo markSearchWords(subString($row[$j]),$tableInfo[$j]['name'], $search); + else + echo htmlencode(subString($row[$j])); + echo ""; + } + echo "
"; + } + echo "
"; + echo "$_GET['action'], 'fulltexts'=>($params->fulltexts?0:1) ))."' title='".$lang[($params->fulltexts?'no_full_texts':'full_texts')]."'>"; + echo "&".($params->fulltexts?'r':'l')."arr; T &".($params->fulltexts?'l':'r')."arr;"; + echo "
"; + $tdWithClassLeft = ""; + echo "
"; + if($target_table_type == 'table' && $db->isWritable() && $db->isDirWritable()) + { + echo "".$lang['chk_all']." / ".$lang['unchk_all']." ".$lang['with_sel'].": "; + echo " "; + echo ""; + } + echo ""; + } + else + //- Chart view + { + if(!isset($_SESSION[COOKIENAME.$target_table.'chartlabels'])) + { + // No label-column set. Try to pick a text-column as label-column. + for($i=0; $i + + +
+ Chart Settings"; + echo $params->getForm(array('action'=>$_GET['action'])); + echo $lang['chart_type'].": "; + echo "

"; + echo $lang['lbl'].": "; + echo "

"; + echo $lang['val'].": "; + echo "

"; + echo ""; + echo ""; + echo ""; + echo "
"; + //end chart view + } + } + else //no rows - do nothing + { + echo "
"; + if(isset($search) || $totalRows>0) + echo $lang['no_rows']."

"; + elseif($target_table_type == 'table') + echo $lang['empty_tbl']." ".$params->getLink(array('action'=>'row_create'), $lang['click']) ." ".$lang['insert_rows'].'

'; + echo "".htmlencode($queryDisp).""; + echo "

"; + } + + if(isset($search)) + echo "

".$params->getLink(array('action'=>'table_search','search'=>null,'oldSearch' => (isset($_GET['search'])?$_GET['search']:null)), $lang['srch_again']); + + break; + + //- Create new row (=row_create) + case "row_create": + echo $params->getForm(array('action'=>'row_create'), 'get'); + echo $lang['restart_insert']; + echo " "; + echo $lang['rows']; + echo " "; + echo ""; + echo "
"; + echo $params->getForm(array('action'=>'row_create','confirm'=>'1'), 'post', true); + $tableInfo = $db->getTableInfo($target_table); + if(isset($_GET['newRows'])) + $num = $_GET['newRows']; + else + $num = 1; + echo ""; + for($j=0; $j<$num; $j++) + { + if($j>0) + echo "
"; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + + for($i=0; $i"; + echo ""; + echo $tdWithClassLeft; + echo htmlencode($field); + echo ""; + echo $tdWithClassLeft; + echo htmlencode($type); + echo ""; + echo $tdWithClassLeft; + echo ""; + echo ""; + echo $tdWithClassLeft; + if($tableInfo[$i]['notnull']==0) + { + if($value===NULL) + echo ""; + else + echo ""; + } + echo ""; + echo $tdWithClassLeft; + + if($typeAffinity=="INTEGER" || $typeAffinity=="REAL" || $typeAffinity=="NUMERIC") + echo ""; + elseif(preg_match('/^BLOB/', $type) && !$hexblobs) + echo ""; + else + echo ""; + echo ""; + echo ""; + } + echo ""; + echo ""; + echo ""; + echo "
".$lang['fld']."".$lang['type']."".$lang['func']."Null".$lang['val']."
"; + echo ""; + echo "

"; + } + echo ""; + break; + + //- Edit or delete row (=row_editordelete) + case "row_editordelete": + if(isset($_POST['check'])) + $pks = $_POST['check']; + else if(isset($_GET['pk'])) + $pks = array($_GET['pk']); + else $pks[0] = ""; + $str = implode(', ', $pks); + if($str=="") //nothing was selected so show an error + { + echo "
"; + echo $lang['err'].": ".$lang['no_sel']; + echo "
"; + echo "

".$params->getLink(array('action'=>'row_view'),$lang['return']); + } + else + { + if((isset($_POST['type']) && $_POST['type']=="edit") || (isset($_GET['type']) && $_GET['type']=="edit")) //edit + { + echo $params->getForm(array('action'=>'row_edit', 'confirm'=>'1', 'pk'=>json_encode($pks)),'post',true); + $tableInfo = $db->getTableInfo($target_table); + $primary_key = $db->getPrimaryKey($target_table); + + for($j=0; $jquote_id($target_table)." WHERE " . $db->wherePK($target_table, json_decode($pks[$j])); + $result1 = $db->select($query, 'num'); + + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + + for($i=0; $i"; + echo ""; + echo $tdWithClassLeft; + echo htmlencode($field); + echo ""; + echo $tdWithClassLeft; + echo htmlencode($type); + echo ""; + echo $tdWithClassLeft; + echo ""; + echo ""; + echo $tdWithClassLeft; + if($tableInfo[$i]['notnull']==0) + { + if($value===NULL) + echo ""; + else + echo ""; + } + echo ""; + echo $tdWithClassLeft; + if($typeAffinity=="INTEGER" || $typeAffinity=="REAL" || $typeAffinity=="NUMERIC") + echo ""; + elseif(preg_match('/^BLOB/', $type) && !$hexblobs) + { + if($value!==NULL) + { + echo ""; + echo $params->getLink(array('action'=>'row_get_blob', 'confirm'=>1, 'pk'=>$pks[$j], 'column'=>$field, 'download_blob'=>1),$lang["download"]).' | '; + echo $params->getLink(array('action'=>'row_get_blob', 'confirm'=>1, 'pk'=>$pks[$j], 'column'=>$field, 'download_blob'=>0),$lang["open_in_browser"],'','','_blank').'
'; + echo ""; + } + echo ""; + } + else + echo ""; + echo ""; + echo ""; + } + echo ""; + echo ""; + echo ""; + echo "
".$lang['fld']."".$lang['type']."".$lang['func']."Null".$lang['val']."
"; + // Note: the 'Save changes' button must be first in the code so it is the one used when submitting the form with the Enter key (issue #215) + echo " "; + echo " "; + echo $params->getLink(array('action'=>'row_view'), $lang['cancel']); + echo "
"; + echo "
"; + } + echo ""; + } + else //delete + { + echo $params->getForm(array('action'=>'row_delete', 'confirm'=>'1', 'pk'=>json_encode($pks))); + echo "
"; + printf($lang['ques_row_delete'], htmlencode($str), htmlencode($target_table)); + echo "

"; + echo " "; + echo $params->getLink(array('action'=>'row_view'), $lang['cancel']); + echo "
"; + } + } + break; + + //- Column actions + + //- View table structure (=column_view) + case "column_view": + $tableInfo = $db->getTableInfo($target_table); + + echo $params->getForm(array('action'=>'column_confirm'), 'get', false, 'checkForm'); + echo ""; + echo ""; + if($target_table_type == 'table' && $db->isWritable() && $db->isDirWritable()) + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + + $noPrimaryKey = true; + + for($i=0; $i"; + $tdWithClassLeft = ""; + if($target_table_type == 'table' && $db->isWritable() && $db->isDirWritable()) + { + echo $tdWithClass; + echo ""; + echo ""; + echo $tdWithClass; + echo $params->getLink(array('action'=>'column_edit', 'pk'=>$fieldVal),"".$lang['edit']."",'edit', $lang['edit']); + echo ""; + echo $tdWithClass; + echo $params->getLink(array('action'=>'column_confirm', 'action2'=>'column_delete', 'pk'=>$fieldVal),"".$lang['del']."",'delete', $lang['del']); + echo ""; + } + echo $tdWithClass; + echo htmlencode($colVal); + echo ""; + echo $tdWithClassLeft; + echo htmlencode($fieldVal); + echo ""; + echo $tdWithClassLeft; + echo htmlencode($typeVal); + echo ""; + echo $tdWithClassLeft; + echo htmlencode($notnullVal); + echo ""; + echo $tdWithClassLeft; + if($defaultVal===NULL) + echo "".$lang['none'].""; + elseif($defaultVal==="NULL") + echo "NULL"; + else + echo htmlencode($defaultVal); + echo ""; + echo $tdWithClassLeft; + echo htmlencode($primarykeyVal); + echo ""; + echo ""; + } + + echo "
".$lang['col']." #".$lang['fld']."".$lang['type']."".$lang['not_null']."".$lang['def_val']."".$lang['prim_key']."
"; + echo "
"; + if($target_table_type == 'table' && $db->isWritable() && $db->isDirWritable()) + { + echo "".$lang['chk_all']." / ".$lang['unchk_all']." ".$lang['with_sel'].": "; + echo " "; + echo ""; + } + echo ""; + if($target_table_type == 'table' && $db->isWritable() && $db->isDirWritable()) + { + echo "
"; + echo $params->getForm(array('action'=>'column_create'), 'get'); + echo $lang['add']." ".$lang['tbl_end']." "; + echo ""; + } + + echo "
"; + echo "
"; + echo "
"; + echo "".$lang['query_used_'.$target_table_type]."
"; + echo ""; + echo nl2br(htmlencode($db->export_sql(array($target_table),false,true,false,false,false,false))); + echo ""; + echo "
"; + echo "
"; + if($target_table_type != 'view') + { + echo "


"; + $query = "PRAGMA index_list(".$db->quote_id($target_table).")"; + $result = $db->selectArray($query); + if(sizeof($result)>0) + { + echo "

".$lang['indexes'].":

"; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + for($i=0; $iquote_id($result[$i]['name']).")"; + $info = $db->selectArray($query); + $span = sizeof($info); + + $tdWithClass = ""; + echo $tdWithClassSpan; + echo $params->getLink(array('action'=>'index_delete', 'pk'=>$result[$i]['name']), "".$lang['del']."", 'delete', $lang['del']); + echo ""; + echo $tdWithClassLeftSpan; + echo $result[$i]['name']; + echo ""; + echo $tdWithClassLeftSpan; + echo $unique; + echo ""; + for($j=0; $j<$span; $j++) + { + if($j!=0) + echo ""; + echo $tdWithClassLeft; + echo htmlencode($info[$j]['seqno']); + echo ""; + echo $tdWithClassLeft; + echo htmlencode($info[$j]['cid']); + echo ""; + echo $tdWithClassLeft; + echo htmlencode($info[$j]['name']); + echo ""; + echo ""; + } + } + echo "
"; + echo "".$lang['name']."".$lang['unique']."".$lang['seq_no']."".$lang['col']." #".$lang['fld']."
"; + $tdWithClassLeft = ""; + $tdWithClassSpan = ""; + $tdWithClassLeftSpan = ""; + echo "


"; + } + + $query = "SELECT * FROM sqlite_master WHERE type='trigger' AND tbl_name=".$db->quote($target_table)." ORDER BY name"; + $result = $db->selectArray($query); + //print_r($result); + if(sizeof($result)>0) + { + echo "

".$lang['triggers'].":

"; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + for($i=0; $i"; + echo ""; + echo $tdWithClass; + echo $params->getLink(array('action'=>'trigger_delete', 'pk'=>$result[$i]['name']), "".$lang['del']."", 'delete', $lang['del']); + echo ""; + echo $tdWithClass; + echo htmlencode($result[$i]['name']); + echo ""; + echo $tdWithClass; + echo htmlencode($result[$i]['sql']); + echo ""; + } + echo "
"; + echo "".$lang['name']."".$lang['sql']."


"; + } + + if($db->isWritable() && $db->isDirWritable()) + { + echo $params->getForm(array('action'=>'index_create'),'get'); + echo "
"; + echo $lang['create_index2']." ".$lang['cols']." "; + echo "
"; + echo ""; + + echo $params->getForm(array('action'=>'trigger_create'),'get'); + echo "
"; + echo $lang['create_trigger2']." "; + echo "
"; + echo ""; + } + } + break; + + //- Create column (=column_create) + case "column_create": + echo "

".sprintf($lang['new_fld'],htmlencode($_GET['table']))."

"; + if($_GET['tablefields']=="" || intval($_GET['tablefields'])<=0) + echo $lang['specify_fields']; + else if($_GET['table']=="") + echo $lang['specify_tbl']; + else + { + $num = intval($_GET['tablefields']); + $name = $_GET['table']; + echo $params->getForm(array('action'=>'column_create', 'confirm'=>'1')); + echo ""; + echo ""; + echo ""; + $headings = array($lang["fld"], $lang["type"], $lang["prim_key"]); + if($db->getType() != "SQLiteDatabase") $headings[] = $lang["autoincrement"]; + $headings[] = $lang["not_null"]; + $headings[] = $lang["def_val"]; + + for($k=0; $k" . $headings[$k] . ""; + echo ""; + + for($i=0; $i<$num; $i++) + { + $tdWithClass = ""; + echo $tdWithClass; + echo ""; + echo ""; + echo $tdWithClass; + echo ""; + echo ""; + echo $tdWithClass; + echo ""; + echo ""; + if($db->getType() != "SQLiteDatabase") + { + echo $tdWithClass; + echo ""; + echo ""; + } + echo $tdWithClass; + echo ""; + echo ""; + echo $tdWithClass; + echo ""; + echo ""; + echo ""; + echo ""; + } + echo ""; + echo ""; + echo ""; + echo "
"; + echo "
"; + echo " "; + echo $params->getLink(array('action'=>'column_view'), $lang['cancel']); + echo "
"; + echo ""; + } + break; + + //- Confirm column action (=column_confirm) + case "column_confirm": + if(isset($_GET['check'])) + $pks = $_GET['check']; + elseif(isset($_GET['pk'])) + $pks = array($_GET['pk']); + else $pks = array(); + + if(sizeof($pks)==0) //nothing was selected so show an error + { + echo "
"; + echo $lang['err'].": ".$lang['no_sel']; + echo "
"; + echo "

"; + echo $params->getLink(array('action'=>'column_view'), $lang['return']); + } + else + { + $str = $pks[0]; + $pkVal = $pks[0]; + for($i=1; $igetForm(array('action'=>$_GET['action2'], 'confirm'=>'1', 'pk'=>$pkVal)); + echo "
"; + printf($lang['ques_'.$_GET['action2']], htmlencode($str), htmlencode($target_table)); + echo "

"; + echo " "; + echo $params->getLink(array('action'=>'column_view'), $lang['cancel']); + echo "
"; + } + break; + + //- Edit column (=column_edit) + case "column_edit": + echo "

".sprintf($lang['edit_col'], htmlencode($_GET['pk']))." ".$lang['on_tbl']." '".htmlencode($target_table)."'

"; + echo $lang['sqlite_limit']."

"; + if(!isset($_GET['pk'])) + echo $lang['specify_col']; + else if (!$target_table) + echo $lang['specify_tbl']; + else + { + $tableInfo = $db->getTableInfo($target_table); + + for($i=0; $i".$lang['err'].": ".sprintf($lang['col_inexistent'], htmlencode($_GET['pk'])).""; + } + else + { + $name = $target_table; + echo $params->getForm(array('action'=>'column_edit', 'confirm'=>'1')); + echo ""; + echo ""; + echo ""; + //$headings = array("Field", "Type", "Primary Key", "Autoincrement", "Not NULL", "Default Value"); + $headings = array($lang["fld"], $lang["type"]); + for($k=0; $k".$headings[$k].""; + echo ""; + + $i = 0; + $tdWithClass = ""; + echo $tdWithClass; + echo ""; + echo ""; + echo $tdWithClass; + echo ""; + echo ""; + /* + echo $tdWithClass; + if($primarykeyVal) + echo " Yes"; + else + echo " Yes"; + echo ""; + echo $tdWithClass; + if(1==2) + echo " Yes"; + else + echo " Yes"; + echo ""; + echo $tdWithClass; + if($notnullVal) + echo " Yes"; + else + echo " Yes"; + echo ""; + echo $tdWithClass; + echo ""; + echo ""; + */ + echo ""; + + echo ""; + echo ""; + echo ""; + echo "
"; + echo "
"; + echo " "; + echo $params->getLink(array('action'=>'column_view'), $lang['cancel']); + echo "
"; + echo ""; + } + } + break; + + //- Delete index (=index_delete) + case "index_delete": + echo $params->getForm(array('action'=>'index_delete', 'pk'=>$_GET['pk'], 'confirm'=>'1')); + echo "
"; + echo sprintf($lang['ques_index_delete'], htmlencode($_GET['pk']))."

"; + echo " "; + echo $params->getLink(array('action'=>'column_view'), $lang['cancel']); + echo "
"; + echo ""; + break; + + //- Delete trigger (=trigger_delete) + case "trigger_delete": + echo $params->getForm(array('action'=>'trigger_delete', 'pk'=>$_GET['pk'], 'confirm'=>'1')); + echo "
"; + echo sprintf($lang['ques_trigger_delete'], htmlencode($_GET['pk']))."

"; + echo " "; + echo $params->getLink(array('action'=>'column_view'), $lang['cancel']); + echo "
"; + echo ""; + break; + + //- Create trigger (=trigger_create) + case "trigger_create": + echo "

".$lang['create_trigger']." '".htmlencode($_GET['table'])."'

"; + if($_GET['table']=="") + echo $lang['specify_tbl']; + else + { + echo $params->getForm(array('action'=>'trigger_create', 'confirm'=>'1')); + echo $lang['trigger_name'].":

"; + echo "
".$lang['db_event'].""; + echo $lang['before']."/".$lang['after'].": "; + echo ""; + echo "

"; + echo $lang['event'].": "; + echo ""; + echo "


"; + echo "
".$lang['trigger_act'].""; + echo "

"; + echo $lang['when_exp'].":
"; + echo ""; + echo "

"; + echo $lang['trigger_step'].":
"; + echo ""; + echo "


"; + echo " "; + echo $params->getLink(array('action'=>'column_view'), $lang['cancel']); + echo ""; + } + break; + + //- Create index (=index_create) + case "index_create": + echo "

".$lang['create_index']." '".htmlencode($_GET['table'])."'

"; + if($_GET['numcolumns']=="" || intval($_GET['numcolumns'])<=0) + echo $lang['specify_fields']; + else if($_GET['table']=="") + echo $lang['specify_tbl']; + else + { + echo $params->getForm(array('action'=>'index_create', 'confirm'=>'1')); + $num = intval($_GET['numcolumns']); + $tableInfo = $db->getTableInfo($_GET['table']); + echo "
".$lang['define_index'].""; + echo "
"; + echo ""; + echo "
"; + if(version_compare($db->getSQLiteVersion(),'3.8.0')>=0) + echo " ".helpLink($lang['help10']); + echo "
"; + echo "
"; + echo "
".$lang['define_in_col'].""; + for($i=0; $i<$num; $i++) + { + echo " "; + echo "
"; + } + echo "
"; + echo "

"; + echo ""; + echo " "; + echo $params->getLink(array('action'=>'column_view'), $lang['cancel']); + echo ""; + } + break; + } + echo ""; +} + +//- HMTL: views for databases +if(!$target_table && !isset($_GET['confirm']) && (!isset($_GET['action']) || (isset($_GET['action']) && $_GET['action']!="table_create" && $_GET['action']!="table_confirm"))) //the absence of these fields means we are viewing the database homepage +{ + //- Switch on $view (actually a series of if-else) + + if($view=="structure") + { + //- Database structure, shows all the tables (=structure) + + if($db->isWritable() && !$db->isDirWritable()) + { + echo "
"; + echo $lang['attention'].': '.$lang['directory_not_writable']; + echo "

"; + } + elseif(!$db->isWritable()) + { + echo "
"; + echo $lang['attention'].': '.$lang['database_not_writable']; + echo "

"; + } + + if ($auth->isPasswordDefault()) + { + echo "
"; + echo sprintf($lang['warn_passwd'],(is_readable('phpliteadmin.config.php')?'phpliteadmin.config.php':basename(__FILE__)))."
".$lang['warn0']; + echo "
"; + } + + if (!extension_loaded('mbstring')) + { + echo "
"; + echo $lang['warn_mbstring']; + echo "
"; + } + echo "".$lang['db_name'].": ".htmlencode($db->getName())."
"; + echo "".$lang['db_path'].": ".htmlencode($db->getPath())."
"; + echo "".$lang['db_size'].": ".number_format($db->getSize())." KiB
"; + echo "".$lang['db_mod'].": ".$db->getDate()."
"; + echo "".$lang['sqlite_v'].": ".$db->getSQLiteVersion()."
"; + echo "".$lang['sqlite_ext']." ".helpLink($lang['help1']).": ".$db->getType()."
"; + echo "".$lang['php_v'].": ".phpversion()."
"; + echo "".PROJECT." ".$lang["ver"].": ".VERSION; + echo "

"; + echo ""; + + if(isset($_GET['sort']) && ($_GET['sort']=='type' || $_GET['sort']=='name')) + $_SESSION[COOKIENAME.'sortTables'] = $_GET['sort']; + if(isset($_GET['order']) && ($_GET['order']=='ASC' || $_GET['order']=='DESC')) + $_SESSION[COOKIENAME.'orderTables'] = $_GET['order']; + + if(!isset($_SESSION[COOKIENAME.'sortTables'])) + $_SESSION[COOKIENAME.'sortTables'] = 'name'; + + if(!isset($_SESSION[COOKIENAME.'orderTables'])) + $_SESSION[COOKIENAME.'orderTables'] = 'ASC'; + + $tables = $db->getTables(true, false, $_SESSION[COOKIENAME.'sortTables'], $_SESSION[COOKIENAME.'orderTables']); + + if(sizeof($tables)==0) + echo $lang['no_tbl']."

"; + else + { + echo $params->getForm(array('action'=>'table_confirm',), 'get', false, 'checkForm'); + echo ""; + echo ""; + + echo ""; + + echo ""; + + echo ""; + echo ""; + echo ""; + + $totalRecords = 0; + $skippedTables = false; + $tableId = 0; + foreach($tables as $tableName => $tableType) + { + $records = $db->numRows($tableName, (!isset($_GET['forceCount']))); + if($records == '?') + { + $skippedTables = true; + $records = $params->getLink(array('forceCount'=>'1'), '?'); + } + else + $totalRecords += $records; + $tdWithClass = ""; + echo $tdWithClass; + echo ""; + echo ""; + echo $tdWithClassLeft; + echo $params->getLink(array('table'=>$tableName, 'action'=>'row_view'), htmlencode($tableName)); + echo ""; + echo $tdWithClassLeft; + echo ($tableType=="table"? $lang['tbl'] : $lang['view']); + echo ""; + echo $tdWithClass; + echo $params->getLink(array('table'=>$tableName, 'action'=>'row_view'), $lang['browse']); + echo ""; + echo $tdWithClass; + echo $params->getLink(array('table'=>$tableName, 'action'=>'column_view'), $lang['struct']); + echo ""; + echo $tdWithClass; + echo $params->getLink(array('table'=>$tableName, 'action'=>'table_sql'), $lang['sql']); + echo ""; + echo $tdWithClass; + echo $params->getLink(array('table'=>$tableName, 'action'=>'table_search'), $lang['srch']); + echo ""; + echo $tdWithClass; + if($tableType=="table" && $db->isWritable() && $db->isDirWritable()) + echo $params->getLink(array('table'=>$tableName, 'action'=>'row_create'), $lang['insert']); + else + echo $lang['insert']; + echo ""; + echo $tdWithClass; + echo $params->getLink(array('table'=>$tableName, 'action'=>'table_export'), $lang['export']); + echo ""; + echo $tdWithClass; + if($tableType=="table" && $db->isWritable() && $db->isDirWritable()) + echo $params->getLink(array('table'=>$tableName, 'action'=>'table_import'), $lang['import']); + else + echo $lang['import']; + echo ""; + echo $tdWithClass; + if($db->isWritable() && $db->isDirWritable()) + echo $params->getLink(array('table'=>$tableName, 'action'=>'table_rename'), $lang['rename']); + else + echo $lang['rename']; + echo ""; + echo $tdWithClass; + if($tableType=="table" && $db->isWritable() && $db->isDirWritable()) + echo $params->getLink(array('table'=>$tableName, 'action'=>'table_confirm', 'action2'=>'table_empty'), $lang['empty'], 'empty'); + else + echo $lang['empty']; + echo ""; + echo $tdWithClass; + if($db->isWritable() && $db->isDirWritable()) + echo $params->getLink(array('table'=>$tableName,'action'=>'table_confirm', 'action2'=>'table_drop'), $lang['drop'], 'drop'); + else + echo $lang['drop']; + echo ""; + echo $tdWithClass; + echo $records; + echo ""; + echo ""; + $tableId++; + } + echo ""; + echo ""; + echo ""; + echo ""; + echo "
"; + if(isset($_SESSION[COOKIENAME.'sortTables'])) + $orderTag = ($_SESSION[COOKIENAME.'sortTables']=="name" && $_SESSION[COOKIENAME.'orderTables']=="ASC") ? "DESC" : "ASC"; + else + $orderTag = "ASC"; + echo $params->getLink(array('sort'=>'name', 'order'=>$orderTag), $lang['name']); + if(isset($_SESSION[COOKIENAME.'sortTables']) && $_SESSION[COOKIENAME.'sortTables']=="name") + echo (($_SESSION[COOKIENAME.'orderTables']=="ASC") ? " " : " "); + echo ""; + if(isset($_SESSION[COOKIENAME.'sortTables'])) + $orderTag = ($_SESSION[COOKIENAME.'sortTables']=="type" && $_SESSION[COOKIENAME.'orderTables']=="ASC") ? "DESC" : "ASC"; + else + $orderTag = "ASC"; + echo $params->getLink(array('sort'=>'type', 'order'=>$orderTag), $lang['type']); + echo helpLink($lang['help3']); + if(isset($_SESSION[COOKIENAME.'sortTables']) && $_SESSION[COOKIENAME.'sortTables']=="type") + echo (($_SESSION[COOKIENAME.'orderTables']=="ASC") ? " " : " "); + echo "".$lang['act']."".$lang['rec']."
"; + $tdWithClassLeft = ""; + + echo "
".sizeof($tables)." ".$lang['total']."".$totalRecords.($skippedTables?" ".$params->getLink(array('forceCount'=>'1'),'+ ?'):"")."
"; + echo "".$lang['chk_all']." / ".$lang['unchk_all']." ".$lang['with_sel'].": "; + echo " "; + echo ""; + echo ""; + echo "
"; + if($skippedTables) + echo "
".sprintf($lang["counting_skipped"],"'1'))."'>","")."
"; + } + if($db->isWritable() && $db->isDirWritable()) + { + echo "
"; + echo "".$lang['create_tbl_db']." '".htmlencode($db->getName())."'"; + echo $params->getForm(array('action'=>'table_create'), 'get'); + echo $lang['name'].": "; + echo $lang['fld_num'].": "; + echo ""; + echo ""; + echo "
"; + echo "
"; + echo "
"; + echo "".$lang['create_view']." '".htmlencode($db->getName())."'"; + echo $params->getForm(array('action'=>'view_create', 'confirm'=>'1')); + echo $lang['name'].": "; + echo $lang['sel_state']." ".helpLink($lang['help4']).": "; + echo ""; + echo ""; + echo "
"; + } + } + else if($view=="sql") + { + //- Database SQL editor (=sql) + if(isset($_POST['query']) && $_POST['query']!="") + { + $delimiter = $_POST['delimiter']; + $queryStr = $_POST['queryval']; + //save the queries in history if necessary + if($maxSavedQueries!=0 && $maxSavedQueries!=false) + { + if(!isset($_SESSION[COOKIENAME.'query_history'])) + $_SESSION[COOKIENAME.'query_history'] = array(); + $_SESSION[COOKIENAME.'query_history'][md5(strtolower($queryStr))] = $queryStr; + if(sizeof($_SESSION[COOKIENAME.'query_history']) > $maxSavedQueries) + array_shift($_SESSION[COOKIENAME.'query_history']); + } + $query = explode_sql($delimiter, $queryStr); //explode the query string into individual queries based on the delimiter + + for($i=0; $iquery($query[$i]); + + echo "
"; + echo "".htmlencode($query[$i]).""; + if($table_result === NULL || $table_result === false) + { + echo "
".$lang['err'].": ".htmlencode($db->getError())."
"; + } + echo "
"; + if($row = $db->fetch($table_result, 'num')) + { + for($j=0; $jgetColumnName($table_result,$j); + echo ""; + echo ""; + for($j=0; $j"; + echo htmlencode($headers[$j]); + echo ""; + } + echo ""; + $rowCount = 0; + for(; $rowCount==0 || $row = $db->fetch($table_result, 'num'); $rowCount++) + { + $tdWithClass = ""; + for($z=0; $zNULL"; + else + echo htmlencode(subString($row[$z])); + echo ""; + } + echo ""; + } + $queryTimer->stop(); + echo "
"; + echo "


"; + + + if($table_result !== NULL && $table_result !== false) + { + echo "
"; + if($rowCount>0 || $db->getAffectedRows()==0) + { + printf($lang['show_rows'], $rowCount); + } + if($db->getAffectedRows()>0 || $rowCount==0) + { + echo $db->getAffectedRows()." ".$lang['rows_aff']." "; + } + printf($lang['query_time'], $queryTimer); + echo "
"; + } + + + } + } + } + } + else + { + $delimiter = ";"; + $queryStr = ""; + } + + echo "
"; + echo "".sprintf($lang['run_sql'],htmlencode($db->getName())).""; + echo $params->getForm(array('view'=>'sql')); + if(isset($_SESSION[COOKIENAME.'query_history']) && sizeof($_SESSION[COOKIENAME.'query_history'])>0) + { + echo "".$lang['recent_queries']."
    "; + foreach($_SESSION[COOKIENAME.'query_history'] as $key => $value) + { + echo "
  • ".htmlencode($value)."
  • "; + } + echo "


"; + } + echo ""; + echo ""; + echo $lang['delimit']." "; + echo ""; + echo ""; + echo "
"; + } + else if($view=="vacuum") + { + //- Vacuum database confirmation (=vacuum) + if(isset($_POST['vacuum'])) + { + $query = "VACUUM"; + $db->query($query); + echo "
"; + printf($lang['db_vac'], htmlencode($db->getName())); + echo "

"; + } + echo $params->getForm(array('view'=>'vacuum')); + printf($lang['vac_desc'],htmlencode($db->getName())); + echo "

"; + echo ""; + echo ""; + } + else if($view=="export") + { + //- Export view (=export) + echo $params->getForm(array('view'=>'export')); + echo "
".$lang['export'].""; + echo ""; + echo "

"; + echo ""; + echo "
"; + echo "
"; + + echo "
".$lang['options'].""; + echo " ".helpLink($lang['help5'])."
"; + echo " ".helpLink($lang['help6'])."
"; + echo " ".helpLink($lang['help7'])."
"; + echo " ".helpLink($lang['help8'])."
"; + echo " ".helpLink($lang['help9'])."
"; + echo "
"; + + echo ""; + + echo "
"; + echo "

"; + echo "
".$lang['save_as'].""; + $file = pathinfo($db->getPath()); + $name = $file['filename']; + echo " "; + echo "
"; + echo ""; + echo "
".sprintf($lang['backup_hint'], + $params->getLink(array('download'=>$currentDB['path'], 'token'=>$_SESSION[COOKIENAME.'token']), $lang["backup_hint_linktext"], '', $lang['backup']) + )."
"; + } + else if($view=="import") + { + //- Import view (=import) + if(isset($_POST['import'])) + { + echo "
"; + if($importSuccess===true) + echo $lang['import_suc']; + else + echo $importSuccess; + echo "

"; + } + + echo $params->getForm(array('view'=>'import'), 'post', true); + echo "
".$lang['import'].""; + echo ""; + echo "
"; + echo "
"; + + echo "
".$lang['options'].""; + echo $lang['no_opt']; + echo "
"; + + echo ""; + + echo "
"; + echo "

"; + + echo "
".$lang['import_f'].""; + echo "".$lang['max_file_size'].": ".number_format(fileUploadMaxSize()/1024/1024)." MiB ".helpLink($lang['help11'])."
"; + echo ""; + echo ""; + echo "
"; + } + else if($view=="rename") + { + //- Rename database confirmation (=rename) + echo $params->getForm(array('view'=>'rename', 'database_rename'=>'1')); + echo ""; + echo $lang['db_rename']." '".htmlencode($db->getPath())."' ".$lang['to']." "; + echo ""; + } + else if($view=="delete") + { + //- Delete database confirmation (=delete) + echo $params->getForm(array('database_delete'=>'1')); + echo "
"; + echo sprintf($lang['ques_database_delete'],htmlencode($db->getPath()))."

"; + echo ""; + echo " "; + echo $params->getLink(array(), $lang['cancel']); + echo "
"; + echo ""; + } + + echo ""; +} +echo ""; + +//- HTML: page footer +echo "
"; +echo "".$lang['powered']." ".PROJECT." | "; +echo $lang['free_software']." ".$lang['please_donate']." | "; +printf($lang['page_gen'], $pageTimer); +echo ""; +echo "
"; +$db->close(); //close the database +echo ""; +echo ""; + +//- End of main code + + +// returns data from internal resources, available in single-file mode +function getInternalResource($res) { + $resources = array('resources/phpliteadmin.css'=>array(0=>0,1=>4059,),'resources/phpliteadmin.js'=>array(0=>4059,1=>4454,),'resources/favicon.ico'=>array(0=>8513,1=>1448,),); + + if (isset($resources[$res]) && $f = fopen(__FILE__, 'r')) { + fseek($f, __COMPILER_HALT_OFFSET__ + $resources[$res][0]); + $data = fread($f, $resources[$res][1]); + fclose($f); + return $data; + } + return false; +} + +// resources embedded below, do not edit! +__halt_compiler() ?>body{margin:0px;padding:0px;font-family:Arial,Helvetica,sans-serif;font-size:14px;color:#000;background-color:#e0ebf6;overflow:auto}.body_tbl td{padding:9px 2px 9px 9px}.left_td{width:100px}a{color:#03F;text-decoration:none;cursor:pointer}a:hover{color:#06F}hr{height:1px;border:0;color:#bbb;background-color:#bbb;width:100%}h1{margin:0px;padding:5px;font-size:24px;background-color:#f3cece;text-align:center;color:#000;border-top-left-radius:5px;border-top-right-radius:5px;-moz-border-radius-topleft:5px;-moz-border-radius-topright:5px}#headerlinks{text-align:center;margin-bottom:10px;padding:5px 15px;border-color:#03F;border-width:1px;border-style:solid;border-left-style:none;border-right-style:none;font-size:12px;background-color:#e0ebf6;font-weight:bold}h1 #version{color:#000;font-size:16px}h1 #logo{color:#000}h2{margin:0px;padding:0px;font-size:14px;margin-bottom:20px}input,select,textarea,.CodeMirror{font-family:Arial,Helvetica,sans-serif;background-color:#eaeaea;color:#03F;border-color:#03F;border-style:solid;border-width:1px;margin:5px;border-radius:5px;-moz-border-radius:5px;padding:3px}input.btn{cursor:pointer}input.btn:hover{background-color:#ccc}fieldset label{min-width:200px;display:block;float:left}fieldset{padding:15px;border-color:#03F;border-width:1px;border-style:solid;border-radius:5px;-moz-border-radius:5px;background-color:#f9f9f9}#container{padding:10px}#leftNav{min-width:250px;padding:0px;border-color:#03F;border-width:1px;border-style:solid;background-color:#FFF;padding-bottom:15px;border-radius:5px;-moz-border-radius:5px}.databaseList select{max-width:200px}.viewTable tr td{padding:1px}#loginBox{width:500px;margin-left:auto;margin-right:auto;margin-top:50px;border-color:#03F;border-width:1px;border-style:solid;background-color:#FFF;border-radius:5px;-moz-border-radius:5px}#main{border-color:#03F;border-width:1px;border-style:solid;padding:15px;background-color:#FFF;border-bottom-left-radius:5px;border-bottom-right-radius:5px;border-top-right-radius:5px;-moz-border-radius-bottomleft:5px;-moz-border-radius-bottomright:5px;-moz-border-radius-topright:5px}.td1{background-color:#f9e3e3;text-align:right;font-size:12px;padding-left:10px;padding-right:10px}.td2{background-color:#f3cece;text-align:right;font-size:12px;padding-left:10px;padding-right:10px}.tdheader{border-color:#03F;border-width:1px;border-style:solid;font-weight:bold;font-size:12px;padding-left:10px;padding-right:10px;background-color:#e0ebf6;border-radius:5px;-moz-border-radius:5px}.confirm{border-color:#03F;border-width:1px;border-style:dashed;padding:15px;background-color:#e0ebf6}.tab{display:block;padding:5px;padding-right:8px;padding-left:8px;border-color:#03F;border-width:1px;border-style:solid;margin-right:5px;float:left;border-bottom-style:none;position:relative;top:1px;padding-bottom:4px;background-color:#eaeaea;border-top-left-radius:5px;border-top-right-radius:5px;-moz-border-radius-topleft:5px;-moz-border-radius-topright:5px}.tab_pressed{display:block;padding:5px;padding-right:8px;padding-left:8px;border-color:#03F;border-width:1px;border-style:solid;margin-right:5px;float:left;border-bottom-style:none;position:relative;top:1px;background-color:#FFF;cursor:default;border-top-left-radius:5px;border-top-right-radius:5px;-moz-border-radius-topleft:5px;-moz-border-radius-topright:5px}.helpq{font-size:11px;font-weight:normal}#help_container{padding:0px;font-size:12px;margin-left:auto;margin-right:auto;background-color:#fff}.help_outer{background-color:#FFF;padding:0px;height:300px;position:relative}.help_list{padding:10px;height:auto}.headd{font-size:14px;font-weight:bold;display:block;padding:10px;background-color:#e0ebf6;border-color:#03F;border-width:1px;border-style:solid;border-left-style:none;border-right-style:none}.help_inner{padding:10px}.help_top{display:block;position:absolute;right:10px;bottom:10px}.warning,.delete,.empty,.drop,.delete_db{color:red}.sidebar_table{font-size:11px}.active_table,.active_db{text-decoration:underline}.null{color:#888}.found{background:#FF0;text-decoration:none}function initAutoincrement(){var i=0;while(document.getElementById('i'+i+'_autoincrement')!=undefined){document.getElementById('i'+i+'_autoincrement').disabled=true;i++;}} +function toggleAutoincrement(i){var type=document.getElementById('i'+i+'_type');var primarykey=document.getElementById('i'+i+'_primarykey');var autoincrement=document.getElementById('i'+i+'_autoincrement');if(!autoincrement)return false;if(type.value=='INTEGER'&&primarykey.checked) +autoincrement.disabled=false;else{autoincrement.disabled=true;autoincrement.checked=false;}} +function toggleNull(i){var pk=document.getElementById('i'+i+'_primarykey');var notnull=document.getElementById('i'+i+'_notnull');if(pk.checked){notnull.disabled=true;notnull.checked=true;} +else{notnull.disabled=false;}} +function checkAll(){var i=0;while(document.getElementById('check_'+i)!=undefined){document.getElementById('check_'+i).checked=true;i++;}} +function uncheckAll(){var i=0;while(document.getElementById('check_'+i)!=undefined){document.getElementById('check_'+i).checked=false;i++;}} +function changeIgnore(area,e,u){if(area.value!=""){if(document.getElementById(e)!=undefined) +document.getElementById(e).checked=false;if(document.getElementById(u)!=undefined) +document.getElementById(u).checked=false;}} +function moveFields(){var fields=document.getElementById("fieldcontainer");var selected=[];for(var i=0;i0){CodeMirror.commands.autocomplete(instance);}} +function checkFileSize(input){if(input.files&&input.files.length==1){if(input.files[0].size>fileUploadMaxSize){alert(fileUploadMaxSizeErrorMsg+": "+(fileUploadMaxSize/1024/1024)+" MiB");return false;}} +return true;}AAABAAEAEBAAAAEAIAAoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwoKZQAAABMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABEMDJMAAAAdAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASDg7BAAAASAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAETDw9CCQkJ1QUFBb4AAABjAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgsLVgAAAO8YExP/AAAA7QAAALEAAAARAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQNDUsAAADJGBMT/xgTE/8AAAD/AAAAuAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZERE8DQoKwhgTE/8QDQ2sGBMT/xgTE/8AAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwICHkAAAD/EQwMzQAAAMIAAAD/AAAA7gAAAGEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOCwtWAAAA8RgTE/8IBQW1AAAA/wAAAP8AAADlAAAAHQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0KCsEAAAD/EQ8PzAAAAMkAAAD/AAAA/wAAAHoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABDAAAA/xgTE/8TDw/FAAAA8gAAAP8AAADqAAAAJgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAALUAAAD/GBMT/wAAALkAAAD/AAAA/wAAAIkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfAAAA4wAAAP8GBgbFAAAA2QAAAP8AAADGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAF4AAAD5AAAA/RgTE/8AAAD/AAAA0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQQAAAOIAAAD/AAAA/wAAAHYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjAAAArgAAAG4AAAAAAAAAAAAAAAAAAAAA \ No newline at end of file diff --git a/backend-php/admin_api.php b/backend-php/admin_api.php new file mode 100644 index 0000000..67834aa --- /dev/null +++ b/backend-php/admin_api.php @@ -0,0 +1,282 @@ +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + return $pdo; +}; + + +function relativePathToUrl($rel) +{ + $parts = explode('/', dirname($_SERVER['SCRIPT_NAME']) . '/' . $rel); + $norm = []; + foreach ($parts as $p) { + if ($p === '..' && $norm) array_pop($norm); + elseif ($p && $p !== '.') $norm[] = $p; + } + $proto = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http'; + return $proto . '://' . $_SERVER['HTTP_HOST'] . '/' . implode('/', $norm); +} + + +function FastDBReq(PDO $db, string $sql, array $params = []): array +{ + try { + $stmt = $db->prepare($sql); + $stmt->execute($params); + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } catch (PDOException $e) { + throw $e; + } +} + +function Login($req) +{ + return isset($req->data['pass']) && $req->data['pass'] == ADMIN_PASS; +} + +CreateApi(function ($api) { + $api->on('/', 'GET', function ($req) { + return 'hi'; + }); + $api->on('/phpliteadmin', 'GET', function ($req) { + header('Location: ./adm/phpliteadmin.php'); + return ""; + }); + $api->on('/login', 'POST', function ($req) { + if (!isset($req->data['pass'])) { + $req->res->code(401); + return 'bad data'; + } + if ($req->data['pass'] == ADMIN_PASS) { + return true; + } + return false; + }); + $api->on('/class', 'POST', function ($req) { + if (!Login($req)) { + $req->res->code(401); + return ['ok' => "auth_fail"]; + } + $db = db(); + if (isset($req->data['classId'])) { + $classId = $req->data['classId']; + if (isset($req->data['action'])) { + switch ($req->data['action']) { + // actions + case "update_name": + try { + if (!isset($req->data['value'])) { + $req->res->code(400); + return "bad data"; + } + $value = trim($req->data['value']); + if (strlen($value) == 0) { + $req->res->code(400); + return "bad data"; + } + FastDBReq($db, "UPDATE classes SET userclassname = ? WHERE classid=?", [$value, $classId]); + return true; + } catch (PDOException $e) { + error_log($e); + return "егор"; + } + break; + case 'create_admin': + if (!isset($req->data['value'])) { + $req->res->code(400); + return "bad data"; + } + $value = trim($req->data['value']); + if (strlen($value) == 0 || !str_contains($value, ';')) { + $req->res->code(400); + return "bad data"; + } + $value = explode(';', $value); + if (sizeof($value) != 3) { + $req->res->code(400); + return 'bad data'; + } + $n_adminId = $value[0]; + $n_adminName = base64_decode($value[1]); + $n_adminLevel = $value[2]; + + try { + FastDBReq($db, "INSERT INTO admins (adminId,adminName,adminClass,adminPrems) VALUES (?,?,?,?)", [$n_adminId, $n_adminName, $classId, $n_adminLevel]); + return true; + } catch (PDOException) { + $req->res->code(500); + return "ошибка"; + } + case "create": + if (!isset($req->data['value'])) { + $req->res->code(400); + return "bad data"; + } + $classname = trim($req->data['value']); + if (strlen($classname) == 0) { + $req->res->code(400); + return "bad data"; + } + FastDBReq($db, "INSERT INTO classes (classid, userclassname) VALUES (?,?)", [$classId, $classname]); + return true; + break; + case "DELETE": + // DANGER ZONE:: + FastDBReq($db, "DELETE FROM classes WHERE classid=?", [$classId]); + FastDBReq($db, "DELETE FROM admins WHERE adminClass=?", [$classId]); + return true; + break; + default: + $db = null; + return "?"; + } + } else { + // display class page + + $results = FastDBReq($db, "SELECT * FROM classes WHERE classId=?", [$classId]); + if (count($results) == 0) { + $db = null; + $req->res->code(400); + return "idi naxodi"; + } + $classData = $results[0]; + $adminsData = FastDBReq($db, "SELECT * FROM admins WHERE adminClass=?", [$classId]); + $db = null; + + return ["ok" => 'da', "class" => $classData, "admins" => $adminsData]; + } + } else { + // diplay all classes + $result = FastDBReq($db, "SELECT * FROM classes"); + return ['ok' => 'da', "data" => $result]; + } + }); + + $api->on('/admin', "POST", function ($req) { + if (!Login($req)) { + $req->res->code(401); + return "auth_fail"; + } + $db = db(); + if (isset($req->data['adminId'])) { + if (isset($req->data['action'])) { + switch ($req->data['action']) { + case "update_name": + if (!$req->hasData(['value', 'adminId'])) { + $req->res->code(400); + return "bad data"; + } + $value = trim($req->data['value']); + $adminId = $req->data['adminId']; + if (strlen($value) == 0) { + $req->res->code(400); + return "bad data"; + } + try { + FastDBReq($db, 'UPDATE admins SET adminName = ? WHERE adminId = ?', [$value, $adminId]); + } catch (PDOException) { + $db = null; + return "egor"; + } + $db = null; + return true; + case "DELETE": + //scary + if (!isset($req->data['adminId'])) { + $req->res->code(400); + return "bad data"; + } + $adminId = $req->data['adminId']; + FastDBReq($db, "DELETE FROM admins WHERE adminId=?", [$adminId]); + return true; + case "update_level": + if (!$req->hasData(['value', 'adminId'])) { + $req->res->code(400); + return "bad data"; + } + $value = trim($req->data['value']); + $adminId = $req->data['adminId']; + if (strlen($value) == 0) { + $req->res->code(400); + return "bad data"; + } + try { + FastDBReq($db, 'UPDATE admins SET adminPrems = ? WHERE adminId = ?', [$value, $adminId]); + } catch (PDOException) { + $db = null; + return "egor"; + } + $db = null; + return true; + } + } + } + }); + + $api->on('/server', "POST", function ($req) { + if (!Login($req)) { + $req->res->code(401); + return "auth_fail"; + } + if (isset($req->data['action'])) { + switch ($req->data['action']) { + case 'enable': + // unblock + if (file_exists(API_PATH . 'api.lock')) { + rename(API_PATH . 'api.lock', API_PATH . '_api.lock'); + } + break; + case 'disable': + // block + if (file_exists(API_PATH . 'api.lock')) return true; + if (file_exists(API_PATH . '_api.lock')) { + rename(API_PATH . '_api.lock', API_PATH . 'api.lock'); + } else { + $f = fopen(API_PATH . 'api.lock', 'w'); + fclose($f); + } + break; + case 'update_block_message': + if (!isset($req->data['value'])) { + $req->res->code(400); + return false; + } + if (file_exists(API_PATH . 'api.lock')) { + // write to existing + $f = fopen(API_PATH . 'api.lock', 'w'); + } else { + $f = fopen(API_PATH . '_api.lock', 'w'); + } + fwrite($f, $req->data['value']); + fclose($f); + break; + + default: + return false; + } + return true; + } else { + $capi = relativePathToUrl(API_PATH . 'api.php'); + $status = !file_exists(API_PATH . 'api.lock'); + $apiblockmsg = ""; + if ($status) { + // server enabled (lock disabled) + if (file_exists(API_PATH . '_api.lock')) { + $apiblockmsg = file_get_contents(API_PATH . '_api.lock'); + } + } else { + $apiblockmsg = file_get_contents(API_PATH . 'api.lock'); + } + return ["CLIENT_API" => $capi, "STATUS" => $status, "BLOCKMSG" => $apiblockmsg]; + } + }); +}); diff --git a/backend-php/api.php b/backend-php/api.php index 8c27be8..a06672a 100644 --- a/backend-php/api.php +++ b/backend-php/api.php @@ -10,6 +10,7 @@ function db() function findClosestDateSort($array, $targetDate) { + if (!preg_match('/\d{1,2}-\d{1,2}-(\d{2}|\d{4})$/', $targetDate)) return; $targetTimestamp = DateTime::createFromFormat('d-m-y', $targetDate)->getTimestamp(); usort($array, function ($a, $b) use ($targetTimestamp) { $aTimestamp = DateTime::createFromFormat('d-m-y', $a['fromDate'])->getTimestamp(); @@ -60,16 +61,31 @@ function joinArraysByKey(array $primaryArray, array $secondaryArray, bool $doNot function FastDBReq(PDO $db, string $sql, array $params = []): array { - // ivan - // pickme love - // <3 <3 <3 <3 - $stmt = $db->prepare($sql); - $stmt->execute($params); - return $stmt->fetchAll(PDO::FETCH_ASSOC); + try { + $stmt = $db->prepare($sql); + $stmt->execute($params); + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } catch (PDOException $e) { + throw $e; + } } CreateApi(function ($api) { + + if (file_exists('./api.lock')) { + http_response_code(500); + $f = fopen('./api.lock', 'r') or die("server eгor"); + $fs = filesize('./api.lock'); + if ($fs == 0) { + $cnt = ""; + } else { + $cnt = fread($f, $fs); + } + fclose($f); + die($cnt); + } + $api->on('/', 'GET', function () { return 'dengi'; }); @@ -125,6 +141,23 @@ CreateApi(function ($api) { return; } }); + $api->on('/get_admin_level', 'POST', function ($req) { + if (!isset($req->data['adminId'])) { + $req->res->code(400); + return ""; + } + $adminId = $req->data['adminId']; + $db = db(); + $results = FastDBReq($db, "SELECT adminPrems FROM admins where adminid = ?", [$adminId]); + $db = null; + + if (count($results) != 0) { + return $results[0]; + } else { + $req->res->code(400); + return; + } + }); $api->on('/admin_login', 'POST', function ($req) { if (!isset($req->data['adminId'])) { $req->res->code(400); @@ -152,6 +185,7 @@ CreateApi(function ($api) { } $classId = substr(strtoupper($req->data['classId']), 0, 10); $day = $req->data['day']; + if (!preg_match('/\d{1,2}-\d{1,2}-(\d{2}|\d{4})$/', $day)) return "wrong date format"; $db = db(); $results = FastDBReq($db, "SELECT * FROM changes WHERE classId = ? AND forDay = ?", [$classId, $day]); @@ -362,9 +396,201 @@ CreateApi(function ($api) { FastDBReq($db, 'DELETE FROM rings WHERE classId=? AND fromDate=?', [$classId, $day]); break; default: + $db = null; return "you saved your live because you didn't know what tabs are here"; } $db = null; return true; }); + $api->on('/update_admin', "POST", function ($req) { + if (!$req->hasData(['adminId', 'action'])) { + $req->res->code(400); + return "bad data"; + } + $adminId = $req->data['adminId']; + $withSelf = true; + $action = $req->data['action']; + if (isset($req->data['person'])) { + $withSelf = false; + } + $adminId = $req->data['adminId']; + $db = db(); + $results = FastDBReq($db, "SELECT * FROM admins WHERE adminId=?", [$adminId]); + if (count($results) == 0) { + $db = null; + $req->res->code(400); + return "админ не найден"; + } + if (!$withSelf && !in_array($results[0]['adminPrems'], [-1, 2])) { + $db = null; + $req->res->code(400); + return 'нету прав'; + } + if ($withSelf) { + switch ($action) { + case "change_name": + if (!isset($req->data['value'])) { + $req->res->code(400); + return "bad data"; + } + $value = trim($req->data['value']); + if (strlen($value) == 0) { + $req->res->code(400); + return "bad data"; + } + try { + FastDBReq($db, 'UPDATE admins SET adminName = ? WHERE adminId = ?', [$value, $adminId]); + } catch (PDOException) { + $db = null; + return "egor"; + } + break; + } + } else { + $person = $req->data['person']; + switch ($action) { + case "change_name": + if (!isset($req->data['value'])) { + $req->res->code(400); + return "bad data"; + } + $value = trim($req->data['value']); + if (strlen($value) == 0) { + $req->res->code(400); + return "bad data"; + } + try { + FastDBReq($db, 'UPDATE admins SET adminName = ? WHERE adminId = ?', [$value, $person]); + } catch (PDOException) { + $db = null; + return "egor"; + } + break; + default: + $db = null; + return "?"; + } + } + $db = null; + return true; + }); + + $api->on('/classprofile_view', "POST", function ($req) { + if (!$req->hasData(['adminId'])) { + $req->res->code(400); + return "bad data"; + } + $adminId = $req->data['adminId']; + $db = db(); + $results = FastDBReq($db, "SELECT * FROM admins WHERE adminId=?", [$adminId]); + if (count($results) == 0) { + $db = null; + $req->res->code(400); + return "админ не найден"; + } + if (!in_array($results[0]['adminPrems'], [-1, 2])) { + $db = null; + $req->res->code(400); + return "недостаточно прав"; + } + + $classId = $results[0]['adminClass']; + + $results = FastDBReq($db, "SELECT * FROM classes WHERE classId=?", [$classId]); + if (count($results) == 0) { + $db = null; + $req->res->code(400); + return "класса не существует"; + } + $classData = $results[0]; + $adminsData = FastDBReq($db, "SELECT * FROM admins WHERE adminClass=?", [$classId]); + $db = null; + + return ["status" => 1, "class" => $classData, "admins" => $adminsData]; + }); + + $api->on('/classprofile_edit', "POST", function ($req) { + if (!$req->hasData(['adminId', 'action'])) { + $req->res->code(400); + return "bad data"; + } + $adminId = $req->data['adminId']; + $db = db(); + $results = FastDBReq($db, "SELECT * FROM admins WHERE adminId=?", [$adminId]); + if (count($results) == 0) { + $db = null; + $req->res->code(400); + return "админ не найден"; + } + if (!in_array($results[0]['adminPrems'], [-1, 2])) { + $db = null; + $req->res->code(400); + return "недостаточно прав"; + } + + $classId = $results[0]['adminClass']; + + $action = $req->data['action']; + + switch ($action) { + case 'update_name': + try { + if (!isset($req->data['value'])) { + $req->res->code(400); + return "bad data"; + } + $value = trim($req->data['value']); + if (strlen($value) == 0) { + $req->res->code(400); + return "bad data"; + } + FastDBReq($db, "UPDATE classes SET userclassname = ? WHERE classid=?", [$value, $classId]); + return true; + } catch (PDOException $e) { + error_log($e); + return "егор"; + } + break; + case 'create_admin': + if (!isset($req->data['value'])) { + $req->res->code(400); + return "bad data"; + } + $value = trim($req->data['value']); + if (strlen($value) == 0 || !str_contains($value, ';')) { + $req->res->code(400); + return "bad data"; + } + $value = explode(';', $value); + if (sizeof($value) != 2) { + $req->res->code(400); + return 'bad data'; + } + $n_adminId = $value[0]; + $n_adminName = base64_decode($value[1]); + + try { + FastDBReq($db, "INSERT INTO admins (adminId,adminName,adminClass,adminPrems) VALUES (?,?,?,?)", [$n_adminId, $n_adminName, $classId, 1]); + return true; + } catch (PDOException) { + $req->res->code(500); + return "ошибка"; + } + case "delete_admin": + if (!isset($req->data['value'])) { + $req->res->code(400); + return "bad data"; + } + $person = trim($req->data['value']); + if (strlen($person) == 0) { + $req->res->code(400); + return "bad data"; + } + FastDBReq($db, "DELETE FROM admins WHERE adminId=?", [$person]); + return true; + default: + $db = null; + return "?"; + } + }); }); diff --git a/backend-php/k-api.php b/backend-php/k-api.php index 8839e17..36d024c 100644 --- a/backend-php/k-api.php +++ b/backend-php/k-api.php @@ -1,38 +1,54 @@ dev_mode = $devmode; $apiFun($api); $api->end(); } class _K_Api { private $has_exec = false; + public $dev_mode = false; /** @param callable(_K_ApiRequest): string|array|int|float|bool $fun */ public function on(string $route, string $method, callable $fun) { $request_url = (count($_GET) > 0 ? array_keys($_GET)[0] : ''); $r = str_starts_with($route, '/') ? substr($route, 1) : $route; + if ($this->has_exec) return; if ($r != $request_url) { return; } else { $this->has_exec = true; - if ($_SERVER['REQUEST_METHOD'] != $method) { + + $req_method = $_SERVER['REQUEST_METHOD']; + if ($req_method != $method) { http_response_code(405); - die("ERROR: unsupported method ($method available)"); + $_a = ""; + if ($this->dev_mode) + $_a = " - using $req_method, available $method"; + die("ERROR: Method Not Allowed" . $_a); return; } + $data = []; if ($method == 'GET' && count($_GET) > 1) { $data = array_slice(array_keys($_GET), 1); } else if ($method == 'POST' && count($_POST) > 0) { $data = $_POST; } + $fun = $fun(new _K_ApiRequest($data)); if (is_string($fun) || is_numeric($fun)) { if (json_validate($fun) && !is_numeric($fun)) { @@ -43,8 +59,17 @@ class _K_Api Header('Content-Type: application/json'); echo json_encode($fun); } else { - die("ERROR: unknown return type"); + http_response_code(500); + $_a = "Internal Server Error"; + if ($this->dev_mode) { + $_type = gettype($fun); + $_a = "unknown return type ($_type), (available bool, array, numeric, string)"; + } + die("ERROR: " . $_a); + return; } + + return; } } @@ -52,7 +77,7 @@ class _K_Api { if (!$this->has_exec) { http_response_code(404); - echo '404'; + echo 'ERROR: Not Found'; } } } diff --git a/backend-php/phpinfo.php b/backend-php/phpinfo.php deleted file mode 100644 index e974c40..0000000 --- a/backend-php/phpinfo.php +++ /dev/null @@ -1 +0,0 @@ -=16.8.0" } }, "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw=="], @@ -494,6 +496,8 @@ "babel-plugin-polyfill-regenerator": ["babel-plugin-polyfill-regenerator@0.6.5", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.5" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg=="], + "babel-plugin-react-compiler": ["babel-plugin-react-compiler@1.0.0", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw=="], + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "baseline-browser-mapping": ["baseline-browser-mapping@2.8.25", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-2NovHVesVF5TXefsGX1yzx1xgr7+m9JQenvz6FQY3qd+YXkKkYiv+vTCc7OriP9mcDZpTC5mAOYN4ocd29+erA=="], @@ -1212,15 +1216,19 @@ "@apideck/better-ajv-errors/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], - "@babel/helper-annotate-as-pure/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + "@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="], + + "@babel/core/@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="], + + "@babel/generator/@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="], "@babel/helper-create-class-features-plugin/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], "@babel/helper-member-expression-to-functions/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], - "@babel/helper-member-expression-to-functions/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + "@babel/helper-module-imports/@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="], - "@babel/helper-optimise-call-expression/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + "@babel/helper-module-transforms/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="], "@babel/helper-remap-async-to-generator/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], @@ -1228,11 +1236,11 @@ "@babel/helper-skip-transparent-expression-wrappers/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], - "@babel/helper-skip-transparent-expression-wrappers/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], - "@babel/helper-wrap-function/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], - "@babel/helper-wrap-function/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + "@babel/helpers/@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="], + + "@babel/parser/@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="], "@babel/plugin-bugfix-firefox-class-in-computed-class-key/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], @@ -1244,14 +1252,18 @@ "@babel/plugin-transform-function-name/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], - "@babel/plugin-transform-modules-systemjs/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], - "@babel/plugin-transform-modules-systemjs/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], "@babel/plugin-transform-object-rest-spread/@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], "@babel/preset-env/@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="], + "@babel/preset-modules/@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="], + + "@babel/template/@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="], + + "@babel/traverse/@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="], + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], @@ -1286,6 +1298,14 @@ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@types/babel__core/@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="], + + "@types/babel__generator/@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="], + + "@types/babel__template/@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="], + + "@types/babel__traverse/@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="], + "@vitejs/plugin-react-swc/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.46", "", {}, "sha512-xMNwJo/pHkEP/mhNVnW+zUiJDle6/hxrwO0mfSJuEVRbBfgrJFuUSRoZx/nYUw5pCjrysl9OkNXCkAdih8GCnA=="], "accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], @@ -1322,90 +1342,84 @@ "@apideck/better-ajv-errors/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - "@babel/helper-annotate-as-pure/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + "@babel/core/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="], + + "@babel/generator/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="], "@babel/helper-create-class-features-plugin/@babel/traverse/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], "@babel/helper-create-class-features-plugin/@babel/traverse/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], - "@babel/helper-create-class-features-plugin/@babel/traverse/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], - "@babel/helper-member-expression-to-functions/@babel/traverse/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], "@babel/helper-member-expression-to-functions/@babel/traverse/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], - "@babel/helper-member-expression-to-functions/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], - - "@babel/helper-optimise-call-expression/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + "@babel/helper-module-imports/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="], "@babel/helper-remap-async-to-generator/@babel/traverse/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], "@babel/helper-remap-async-to-generator/@babel/traverse/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], - "@babel/helper-remap-async-to-generator/@babel/traverse/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], - "@babel/helper-replace-supers/@babel/traverse/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], "@babel/helper-replace-supers/@babel/traverse/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], - "@babel/helper-replace-supers/@babel/traverse/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], - "@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], "@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], - "@babel/helper-skip-transparent-expression-wrappers/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], - "@babel/helper-wrap-function/@babel/traverse/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], "@babel/helper-wrap-function/@babel/traverse/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], - "@babel/helper-wrap-function/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + "@babel/helpers/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="], + + "@babel/parser/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="], "@babel/plugin-bugfix-firefox-class-in-computed-class-key/@babel/traverse/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], "@babel/plugin-bugfix-firefox-class-in-computed-class-key/@babel/traverse/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], - "@babel/plugin-bugfix-firefox-class-in-computed-class-key/@babel/traverse/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/@babel/traverse/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/@babel/traverse/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/@babel/traverse/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], - "@babel/plugin-transform-classes/@babel/traverse/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], "@babel/plugin-transform-classes/@babel/traverse/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], - "@babel/plugin-transform-classes/@babel/traverse/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], - "@babel/plugin-transform-destructuring/@babel/traverse/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], "@babel/plugin-transform-destructuring/@babel/traverse/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], - "@babel/plugin-transform-destructuring/@babel/traverse/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], - "@babel/plugin-transform-function-name/@babel/traverse/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], "@babel/plugin-transform-function-name/@babel/traverse/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], - "@babel/plugin-transform-function-name/@babel/traverse/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], - "@babel/plugin-transform-modules-systemjs/@babel/traverse/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], "@babel/plugin-transform-modules-systemjs/@babel/traverse/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], - "@babel/plugin-transform-modules-systemjs/@babel/traverse/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], - "@babel/plugin-transform-object-rest-spread/@babel/traverse/@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], "@babel/plugin-transform-object-rest-spread/@babel/traverse/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], - "@babel/plugin-transform-object-rest-spread/@babel/traverse/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + "@babel/preset-modules/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="], + + "@babel/template/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="], + + "@babel/traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="], "@rollup/plugin-node-resolve/@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + "@types/babel__core/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="], + + "@types/babel__generator/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="], + + "@types/babel__template/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="], + + "@types/babel__traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="], + "body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "compression/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], @@ -1427,23 +1441,5 @@ "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "workbox-build/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - - "@babel/helper-create-class-features-plugin/@babel/traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], - - "@babel/helper-remap-async-to-generator/@babel/traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], - - "@babel/helper-replace-supers/@babel/traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], - - "@babel/plugin-bugfix-firefox-class-in-computed-class-key/@babel/traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], - - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/@babel/traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], - - "@babel/plugin-transform-classes/@babel/traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], - - "@babel/plugin-transform-destructuring/@babel/traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], - - "@babel/plugin-transform-function-name/@babel/traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], - - "@babel/plugin-transform-object-rest-spread/@babel/traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], } } diff --git a/dengi.ps1 b/dengi.ps1 new file mode 100644 index 0000000..4839716 --- /dev/null +++ b/dengi.ps1 @@ -0,0 +1,23 @@ +# save as Count-Word.ps1 +param( + [string]$Root = ".", + [string]$Word = "dengi" +) + +$pattern = "\b" + [regex]::Escape($Word) + "\b" +$count = 0 + +Get-ChildItem -Path $Root -Recurse -File -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -notmatch "\\dist(\\|$)" } | + ForEach-Object { + try { + # ; + $text = Get-Content -LiteralPath $_.FullName -ErrorAction Stop -Raw -Encoding UTF8 + } catch { + try { $text = Get-Content -LiteralPath $_.FullName -ErrorAction Stop -Raw -Encoding Default } catch { return } + } + $matches = [regex]::Matches($text, $pattern, "IgnoreCase") + $count += $matches.Count + } + +Write-Output $count diff --git a/dx4dashboard/.gitignore b/dx4dashboard/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/dx4dashboard/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/dx4dashboard/README.md b/dx4dashboard/README.md new file mode 100644 index 0000000..86b2b11 --- /dev/null +++ b/dx4dashboard/README.md @@ -0,0 +1,75 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## React Compiler + +The React Compiler is enabled on this template. See [this documentation](https://react.dev/learn/react-compiler) for more information. + +Note: This will impact Vite dev & build performances. + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/dx4dashboard/bun.lock b/dx4dashboard/bun.lock new file mode 100644 index 0000000..4b815c9 --- /dev/null +++ b/dx4dashboard/bun.lock @@ -0,0 +1,569 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "dx4dashboard", + "dependencies": { + "@tailwindcss/vite": "^4.1.18", + "jotai": "^2.16.1", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-icons": "^5.5.0", + "tailwindcss": "^4.1.18", + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "babel-plugin-react-compiler": "^1.0.0", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "^7.2.4", + }, + }, + }, + "packages": { + "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + + "@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="], + + "@babel/core": ["@babel/core@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw=="], + + "@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="], + + "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="], + + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], + + "@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="], + + "@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], + + "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="], + + "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], + + "@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], + + "@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], + + "@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.2", "", { "os": "android", "cpu": "arm64" }, "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.27.2", "", { "os": "android", "cpu": "x64" }, "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.2", "", { "os": "linux", "cpu": "arm" }, "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.2", "", { "os": "none", "cpu": "x64" }, "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="], + + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], + + "@eslint/config-array": ["@eslint/config-array@0.21.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA=="], + + "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="], + + "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], + + "@eslint/eslintrc": ["@eslint/eslintrc@3.3.3", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ=="], + + "@eslint/js": ["@eslint/js@9.39.2", "", {}, "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA=="], + + "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], + + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], + + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], + + "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.53", "", {}, "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.55.1", "", { "os": "android", "cpu": "arm" }, "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.55.1", "", { "os": "android", "cpu": "arm64" }, "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.55.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.55.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.55.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.55.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.55.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.55.1", "", { "os": "linux", "cpu": "arm" }, "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.55.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.55.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.55.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.55.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.55.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.55.1", "", { "os": "linux", "cpu": "x64" }, "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.55.1", "", { "os": "linux", "cpu": "x64" }, "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.55.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.55.1", "", { "os": "none", "cpu": "arm64" }, "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.55.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.55.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.55.1", "", { "os": "win32", "cpu": "x64" }, "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.55.1", "", { "os": "win32", "cpu": "x64" }, "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw=="], + + "@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="], + + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.18", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.18", "@tailwindcss/oxide-darwin-arm64": "4.1.18", "@tailwindcss/oxide-darwin-x64": "4.1.18", "@tailwindcss/oxide-freebsd-x64": "4.1.18", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", "@tailwindcss/oxide-linux-x64-musl": "4.1.18", "@tailwindcss/oxide-wasm32-wasi": "4.1.18", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" } }, "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A=="], + + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.18", "", { "os": "android", "cpu": "arm64" }, "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q=="], + + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.18", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A=="], + + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.18", "", { "os": "darwin", "cpu": "x64" }, "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw=="], + + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.18", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA=="], + + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18", "", { "os": "linux", "cpu": "arm" }, "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA=="], + + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw=="], + + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg=="], + + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g=="], + + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ=="], + + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.18", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.0", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA=="], + + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.18", "", { "os": "win32", "cpu": "arm64" }, "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA=="], + + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.18", "", { "os": "win32", "cpu": "x64" }, "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q=="], + + "@tailwindcss/vite": ["@tailwindcss/vite@4.1.18", "", { "dependencies": { "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "tailwindcss": "4.1.18" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA=="], + + "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], + + "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], + + "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], + + "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@types/node": ["@types/node@24.10.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg=="], + + "@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="], + + "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.52.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.52.0", "@typescript-eslint/type-utils": "8.52.0", "@typescript-eslint/utils": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.52.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-okqtOgqu2qmZJ5iN4TWlgfF171dZmx2FzdOv2K/ixL2LZWDStL8+JgQerI2sa8eAEfoydG9+0V96m7V+P8yE1Q=="], + + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.52.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.52.0", "@typescript-eslint/types": "8.52.0", "@typescript-eslint/typescript-estree": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-iIACsx8pxRnguSYhHiMn2PvhvfpopO9FXHyn1mG5txZIsAaB6F0KwbFnUQN3KCiG3Jcuad/Cao2FAs1Wp7vAyg=="], + + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.52.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.52.0", "@typescript-eslint/types": "^8.52.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-xD0MfdSdEmeFa3OmVqonHi+Cciab96ls1UhIF/qX/O/gPu5KXD0bY9lu33jj04fjzrXHcuvjBcBC+D3SNSadaw=="], + + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.52.0", "", { "dependencies": { "@typescript-eslint/types": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0" } }, "sha512-ixxqmmCcc1Nf8S0mS0TkJ/3LKcC8mruYJPOU6Ia2F/zUUR4pApW7LzrpU3JmtePbRUTes9bEqRc1Gg4iyRnDzA=="], + + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.52.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-jl+8fzr/SdzdxWJznq5nvoI7qn2tNYV/ZBAEcaFMVXf+K6jmXvAFrgo/+5rxgnL152f//pDEAYAhhBAZGrVfwg=="], + + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.52.0", "", { "dependencies": { "@typescript-eslint/types": "8.52.0", "@typescript-eslint/typescript-estree": "8.52.0", "@typescript-eslint/utils": "8.52.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-JD3wKBRWglYRQkAtsyGz1AewDu3mTc7NtRjR/ceTyGoPqmdS5oCdx/oZMWD5Zuqmo6/MpsYs0wp6axNt88/2EQ=="], + + "@typescript-eslint/types": ["@typescript-eslint/types@8.52.0", "", {}, "sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg=="], + + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.52.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.52.0", "@typescript-eslint/tsconfig-utils": "8.52.0", "@typescript-eslint/types": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0", "debug": "^4.4.3", "minimatch": "^9.0.5", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-XP3LClsCc0FsTK5/frGjolyADTh3QmsLp6nKd476xNI9CsSsLnmn4f0jrzNoAulmxlmNIpeXuHYeEQv61Q6qeQ=="], + + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.52.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.52.0", "@typescript-eslint/types": "8.52.0", "@typescript-eslint/typescript-estree": "8.52.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-wYndVMWkweqHpEpwPhwqE2lnD2DxC6WVLupU/DOt/0/v+/+iQbbzO3jOHjmBMnhu0DgLULvOaU4h4pwHYi2oRQ=="], + + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.52.0", "", { "dependencies": { "@typescript-eslint/types": "8.52.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-ink3/Zofus34nmBsPjow63FP5M7IGff0RKAgqR6+CFpdk22M7aLwC9gOcLGYqr7MczLPzZVERW9hRog3O4n1sQ=="], + + "@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.2", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.53", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ=="], + + "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "babel-plugin-react-compiler": ["babel-plugin-react-compiler@1.0.0", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.9.11", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ=="], + + "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + + "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], + + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001762", "", {}, "sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw=="], + + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.267", "", {}, "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw=="], + + "enhanced-resolve": ["enhanced-resolve@5.18.4", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q=="], + + "esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "eslint": ["eslint@9.39.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.2", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw=="], + + "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.0.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA=="], + + "eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.26", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ=="], + + "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], + + "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], + + "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], + + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], + + "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="], + + "hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="], + + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + + "jotai": ["jotai@2.16.1", "", { "peerDependencies": { "@babel/core": ">=7.0.0", "@babel/template": ">=7.0.0", "@types/react": ">=17.0.0", "react": ">=17.0.0" }, "optionalPeers": ["@babel/core", "@babel/template", "@types/react", "react"] }, "sha512-vrHcAbo3P7Br37C8Bv6JshMtlKMPqqmx0DDREtTjT4nf3QChDrYdbH+4ik/9V0cXA57dK28RkJ5dctYvavcIlg=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + + "lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.2", "", { "os": "linux", "cpu": "arm" }, "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="], + + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + + "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], + + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="], + + "react-dom": ["react-dom@19.2.3", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.3" } }, "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg=="], + + "react-icons": ["react-icons@5.5.0", "", { "peerDependencies": { "react": "*" } }, "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw=="], + + "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], + + "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + + "rollup": ["rollup@4.55.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.55.1", "@rollup/rollup-android-arm64": "4.55.1", "@rollup/rollup-darwin-arm64": "4.55.1", "@rollup/rollup-darwin-x64": "4.55.1", "@rollup/rollup-freebsd-arm64": "4.55.1", "@rollup/rollup-freebsd-x64": "4.55.1", "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", "@rollup/rollup-linux-arm-musleabihf": "4.55.1", "@rollup/rollup-linux-arm64-gnu": "4.55.1", "@rollup/rollup-linux-arm64-musl": "4.55.1", "@rollup/rollup-linux-loong64-gnu": "4.55.1", "@rollup/rollup-linux-loong64-musl": "4.55.1", "@rollup/rollup-linux-ppc64-gnu": "4.55.1", "@rollup/rollup-linux-ppc64-musl": "4.55.1", "@rollup/rollup-linux-riscv64-gnu": "4.55.1", "@rollup/rollup-linux-riscv64-musl": "4.55.1", "@rollup/rollup-linux-s390x-gnu": "4.55.1", "@rollup/rollup-linux-x64-gnu": "4.55.1", "@rollup/rollup-linux-x64-musl": "4.55.1", "@rollup/rollup-openbsd-x64": "4.55.1", "@rollup/rollup-openharmony-arm64": "4.55.1", "@rollup/rollup-win32-arm64-msvc": "4.55.1", "@rollup/rollup-win32-ia32-msvc": "4.55.1", "@rollup/rollup-win32-x64-gnu": "4.55.1", "@rollup/rollup-win32-x64-msvc": "4.55.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A=="], + + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + + "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="], + + "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], + + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + + "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="], + + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "typescript-eslint": ["typescript-eslint@8.52.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.52.0", "@typescript-eslint/parser": "8.52.0", "@typescript-eslint/typescript-estree": "8.52.0", "@typescript-eslint/utils": "8.52.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-atlQQJ2YkO4pfTVQmQ+wvYQwexPDOIgo+RaVcD7gHgzy/IQA+XTyuxNM9M9TVXvttkF7koBHmcwisKdOAf2EcA=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + + "vite": ["vite@7.3.0", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + + "zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="], + + "zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="], + + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], + + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], + + "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + + "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + } +} diff --git a/dx4dashboard/eslint.config.js b/dx4dashboard/eslint.config.js new file mode 100644 index 0000000..90e58d5 --- /dev/null +++ b/dx4dashboard/eslint.config.js @@ -0,0 +1,31 @@ +import js from "@eslint/js"; +import globals from "globals"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import tseslint from "typescript-eslint"; +import { defineConfig, globalIgnores } from "eslint/config"; + +export default defineConfig([ + globalIgnores(["dist"]), + { + files: ["**/*.{ts,tsx}"], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + rules: { + "no-unused-vars": ["warn", { varsIgnorePattern: "^[A-Z_]|^motion$" }], + "@typescript-eslint/no-unused-vars": [ + "warn", + { varsIgnorePattern: "^[A-Z_]|^motion$" }, + ], + "react-hooks/exhaustive-deps": ["off"], + }, + }, +]); diff --git a/dx4dashboard/index.html b/dx4dashboard/index.html new file mode 100644 index 0000000..dba9fa4 --- /dev/null +++ b/dx4dashboard/index.html @@ -0,0 +1,13 @@ + + + + + + + dx4dashboard + + +
+ + + diff --git a/dx4dashboard/package.json b/dx4dashboard/package.json new file mode 100644 index 0000000..8d3f007 --- /dev/null +++ b/dx4dashboard/package.json @@ -0,0 +1,35 @@ +{ + "name": "dx4dashboard", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@tailwindcss/vite": "^4.1.18", + "jotai": "^2.16.1", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-icons": "^5.5.0", + "tailwindcss": "^4.1.18" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "babel-plugin-react-compiler": "^1.0.0", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "^7.2.4" + } +} diff --git a/dx4dashboard/public/vite.svg b/dx4dashboard/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/dx4dashboard/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dx4dashboard/src/App.tsx b/dx4dashboard/src/App.tsx new file mode 100644 index 0000000..55a0ca4 --- /dev/null +++ b/dx4dashboard/src/App.tsx @@ -0,0 +1,104 @@ +// CHANGE TO YOUR API DESTINATION +export const API_BASE_URL = "http://localhost:8321/admin_api.php"; +// export const API_BASE_URL = "http://192.168.3.10:8321/admin_api.php"; +// export const API_BASE_URL = "http://x90620bx.beget.tech/dx4back/admin_api.php" + +// TYPE: url/dx4/ +export const CLIENT_FRONTEND_URL = "http://localhost:5173/dx4/"; +// export const CLIENT_FRONTEND_URL = "http://x90620bx.beget.tech/dx4/"; + +import { atom, useAtom } from "jotai"; +import { atomWithStorage } from "jotai/utils"; +import { useEffect } from "react"; +import LoginPage from "./pages/LoginPage"; +import DashPage from "./pages/DashPage"; +import ClassProfilePage from "./pages/ClassProfile"; +const passAtom = atomWithStorage("DX4DASH_pass", ""); + +// KATAMAZ DENGI ROUTER 4 PRO MAX ULTIMATE (from dx4, yeah, oh now in typescript) +// 💀💀 +// PROS - vrode raboatat, +// too simple, lightweight, stupidly fast +// (beacause you basicly download entire site, lol) +// CONS - shit +function firstPathSegment(path: string) { + const m = path.match(/^\/?([^/]+)/); + return m ? m[1] : ""; +} +const secondPathSegment = (p: string) => + p.replace(/^\/?[^/]+/, "").replace(/^\/+/, ""); + +const routeAtom = atom("/"); +const basePageProps = { + // atoms + passAtom, + routeAtom, + + // fucntions + setsetRoute, + secondPathSegment, + + // constants + API_BASE_URL, +}; + +const pages = { + "/": , + "/dash": , + "/class": , +}; + +let changeAnchor = true; +const defaultAnchor = document.location.hash.substring(1); + +// lol 💀 +function setsetRoute(setRoute: (s: string) => void, path: string) { + changeAnchor = true; + setRoute(path); + document.location.hash = path; +} + +export default function DX4() { + const [route, _setRoute] = useAtom(routeAtom); + useEffect(() => { + if ( + document.location.hash && + document.location.hash.substring(1) !== route + ) { + _setRoute(document.location.hash.substring(1)); + } + window.addEventListener("load", () => { + if (defaultAnchor == "" || defaultAnchor == "/") { + document.location.hash = defaultAnchor; + } + }); + }, [route, _setRoute]); + window.addEventListener("hashchange", () => { + if (document.location.hash == "") { + document.location.hash = "/"; + } + if (changeAnchor) { + changeAnchor = false; + return; + } + _setRoute(document.location.hash.substring(1)); + }); + const page = Object.entries(pages).find( + ([key]) => firstPathSegment(key) == firstPathSegment(route) + ); + return ( + (page ? page[1] : false) || ||
some shit happened
+ ); +} + +export function ERROR404() { + const route = useAtom(routeAtom)[0]; + return ( +
+
+ 404(((( +
+
{route}
+
+ ); +} diff --git a/dx4dashboard/src/assets/logo.svg b/dx4dashboard/src/assets/logo.svg new file mode 100644 index 0000000..aaa98d5 --- /dev/null +++ b/dx4dashboard/src/assets/logo.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dx4dashboard/src/assets/react.svg b/dx4dashboard/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/dx4dashboard/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dx4dashboard/src/index.css b/dx4dashboard/src/index.css new file mode 100644 index 0000000..e38b072 --- /dev/null +++ b/dx4dashboard/src/index.css @@ -0,0 +1,31 @@ +@import "tailwindcss"; + +:root { + font-family: "Jost","Segoe UI", sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: dark; + color: #ffffffde; + background-color: #141414; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* ensure selectable elements still selectable */ +.selectable, +.selectable::after, +.selectable::before, +.selectable *, +.selectable *::before, +.selectable *::after, +input { + user-select: text; +} + +.bg-grain { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100'%3E%3Cfilter id='noise' x='0' y='0'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.8 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)' opacity='0.45'/%3E%3C/svg%3E"); +} diff --git a/dx4dashboard/src/main.tsx b/dx4dashboard/src/main.tsx new file mode 100644 index 0000000..bef5202 --- /dev/null +++ b/dx4dashboard/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.tsx' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/dx4dashboard/src/pages/ClassProfile.tsx b/dx4dashboard/src/pages/ClassProfile.tsx new file mode 100644 index 0000000..fdc9c0c --- /dev/null +++ b/dx4dashboard/src/pages/ClassProfile.tsx @@ -0,0 +1,556 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { useAtom } from "jotai"; +import type { ssroutetype } from "../types"; +import { CoolBg, CopyClassLink, Header } from "./DashPage"; +import { + AvailableAdminSymbols, + copyDXLink, + randomString, + secondPathSegment, +} from "../stilltrash/trash"; +import { useEffect, useRef, useState } from "react"; +import { + DengiButton, + DengiEditable, + DengiYesOrNoDialog, + LoadingSpinner, + TextPreloader, +} from "../stilltrash/dengi_components"; +import { + AdminCreateReq, + AdminRemoveReq, + AdminUpdateReq, + ClassReq, + ClassUpdateNameReq, + DELETEClassRequest, +} from "../stilltrash/req_mgr"; +import { + BiCheckCircle, + BiLink, + BiPlus, + BiPlusCircle, + BiTrash, + BiX, +} from "react-icons/bi"; + +export default function ClassProfilePage({ + passAtom, + routeAtom, + setsetRoute, +}: { + passAtom: any; + routeAtom: any; + setsetRoute: ssroutetype; +}) { + const [route, setRoute] = useAtom(routeAtom); + const [pass, _setPass] = useAtom(passAtom); + const classId = secondPathSegment(route as string); + if (classId == "") setRoute("/dash/"); + return ( + <> +
+
+
+
+ setsetRoute(setRoute, r)} + /> +
+
+
+ + + ); +} + +function ClassProfileEditor({ + pass, + classId, + setRoute, +}: { + pass: string; + classId: string; + setRoute: (r: string) => void; +}) { + const [classData, setClassData] = useState | false>( + false + ); + const [adminsData, setAdminsData] = useState< + Record[] | false + >(false); + const [dataI, setDataI] = useState(0); + useEffect(() => { + ClassReq(pass, classId).then((i) => { + if (i[0]) { + setClassData(i[1]["class"]); + setAdminsData(i[1]["admins"]); + } + }); + }, [classId, dataI]); + + return ( + <> +

+ управление классом {classId}{" "} + {classData ? ({classData.userclassname}) : } +

+ + + + + + + + + + + +
classId + {classId} +
название + { + idle(true); + const r = await ClassUpdateNameReq( + pass, + classId, + v as string + ); + if (r[1] === true) { + setClassData((prev) => ({ + ...prev, + userclassname: v as string, + })); + } + idle(false); + }} + /> +
+
+ + {classData && ( + + )} +
+ + админы + + + + + + + + + + + {adminsData && + adminsData.map((i) => ( + { + setDataI(dataI + 1); + }, + setAdminFrId: (v: string) => { + setAdminsData( + (prev) => + prev && + prev.map((a: any) => + a.adminId === i.adminId ? { ...a, adminName: v } : a + ) + ); + }, + }} + /> + ))} + {!adminsData && ( + + + + + + + )} + +
имяidуров.
+ + + + + + + + + + + + + +
+ {adminsData && ( + { + setDataI(dataI + 1); + }, + classId, + pass, + }} + /> + )} + + ); +} + +function AdminProfileRow({ + adminId, + pass, + adminName, + classId, + setAdminFrId, + adminLevel, + update, +}: { + adminId: string; + pass: string; + adminName: string; + classId: string; + setAdminFrId: (v: string) => void; + adminLevel: string; + update: () => void; +}) { + return ( + + + { + i(true); + const r = await AdminUpdateReq(pass, adminId, "name", v as string); + if (r[1] === true) { + setAdminFrId(v as string); + } + i(false); + }} + /> + + + + + + + + + + + + + ); +} + +const AdminLevelPicker = ({ + adminId, + pass, + value, +}: { + adminId: string; + pass: string; + value: string; +}) => { + const [val, setVal] = useState(value); + const [idle, setIdle] = useState(false); + return ( +
+ {!idle ? ( + + ) : ( + + )} + + ); +}; + +const RemoveAdminButton = ({ + adminId, + pass, + adminName, + update, +}: { + adminId: string; + pass: string; + adminName: string; + update: () => void; +}) => { + const [showDiag, setShowDiag] = useState(false); + const [deleting, setDeleting] = useState(false); + + return ( + <> + + вы точно хотите удалить админа {adminName}? +

+ ), + yesBtn: "да", + noBtn: "нет", + yesIcon: , + noIcon: , + bgDissmis: true, + yesBtnStyle: "bg-red-500! text-white! md:hover:bg-red-600!", + }} + onAnswer={async (answ: boolean) => { + setShowDiag(false); + if (answ) { + setDeleting(true); + const r = await AdminRemoveReq(pass, adminId); + if (r[1] === true) { + update(); + } + } + }} + /> + { + if (deleting) return; + setShowDiag(true); + }} + > + {deleting ? : } + + + ); +}; + +const RemoveClassButton = ({ + pass, + classId, + setRoute, + className, +}: { + pass: string; + classId: string; + setRoute: (r: string) => void; + className: string; +}) => { + const [showDiag, setShowDiag] = useState(false); + const [deleting, setDeleting] = useState(false); + + return ( + <> + + вы точно хотите удалить класс{" "} + + {classId} ({className}) + + ? +

+ ), + yesBtn: "да", + noBtn: "нет", + yesIcon: , + noIcon: , + bgDissmis: true, + yesBtnStyle: "bg-red-500! text-white! md:hover:bg-red-600!", + }} + onAnswer={async (answ: boolean) => { + setShowDiag(false); + if (answ) { + setDeleting(true); + const r = await DELETEClassRequest(pass, classId); + if (r[1] === true) { + setRoute("/dash/class"); + } + setDeleting(false); + } + }} + /> + { + if (deleting) return; + setShowDiag(true); + }} + > + {deleting ? : } + + + ); +}; + +const CopyAdminLink = ({ + classId, + adminId, +}: { + classId: string; + adminId: string; +}) => { + const [linkCopied, setLinkCopied] = useState(false); + const copy = () => { + copyDXLink(classId, adminId).then((a) => { + if (a) { + setLinkCopied(true); + setTimeout(() => { + setLinkCopied(false); + }, 2000); + } + }); + }; + return ( + <> + copy()}> + {linkCopied ? : } + + + ); +}; + +function CreateAdmin({ + pass, + classId, + update, +}: { + pass: string; + update: () => void; + classId: string; +}) { + const [creating, setCreating] = useState(false); + const [adminId, setAdminId] = useState(""); + const [adminName, setAdminName] = useState(""); + const [levelVal, setLevelVal] = useState("1"); + const [locked, setLocked] = useState(false); + const form = useRef(null); + return ( + <> +
+ + {creating && ( +
{ + e.preventDefault(); + if (locked || adminId.length < 5) return; + setLocked(true); + const c = await AdminCreateReq( + pass, + classId, + adminId, + adminName, + levelVal + ); + if (c[1] == true) { + update(); + setCreating(false); + } + setLocked(false); + }} + > + + setAdminName((e.target as HTMLInputElement).value.trimStart()) + } + /> + { + let val = (e.target as HTMLInputElement).value; + let nval = ""; + val.split("").forEach((i) => { + if (AvailableAdminSymbols.includes(i)) { + nval = nval + i; + } + }); + (e.target as HTMLInputElement).value = nval; + setAdminId(nval); + }} + /> + + +
+ )} + { + setCreating(!creating); + setAdminName(""); + setAdminId(randomString(8, AvailableAdminSymbols)); + }} + > + + + + +
+ + ); +} diff --git a/dx4dashboard/src/pages/DashPage.tsx b/dx4dashboard/src/pages/DashPage.tsx new file mode 100644 index 0000000..fc0ad13 --- /dev/null +++ b/dx4dashboard/src/pages/DashPage.tsx @@ -0,0 +1,548 @@ +import { useAtom } from "jotai"; +import { + BiCheckCircle, + BiEdit, + BiExit, + BiLeftArrow, + BiLink, + BiPlus, + BiPlusCircle, +} from "react-icons/bi"; +import Logo from "../assets/logo.svg"; +import type { ssroutetype } from "../types"; +import { useEffect, useRef, useState } from "react"; +import { + AvailableSymbols, + copyDXLink, + randomString, + secondPathSegment, +} from "../stilltrash/trash"; +import { API_BASE_URL, CLIENT_FRONTEND_URL } from "../App"; +import { + DengiButton, + DengiEditable, + Link, + LoadingSpinner, + Switch, + TextPreloader, +} from "../stilltrash/dengi_components"; +import { + ClassesReq, + CreateClassReq, + ServerBlockReq, + ServerMsgBlockReq, + ServerReq, +} from "../stilltrash/req_mgr"; + +export default function DashPage({ + passAtom, + routeAtom, + setsetRoute, +}: { + passAtom: any; + routeAtom: any; + setsetRoute: ssroutetype; +}) { + const [route, setRoute] = useAtom(routeAtom); + const [pass, _setPass] = useAtom(passAtom); + + const tabs: Record = { + class: "классы", + admin: "админы", + server: "сервер", + }; + + let tab = secondPathSegment(route as string); + if (!tab || tab == "") tab = Object.keys(tabs)[0]; + + return ( + <> +
+
+
+
+ {Object.keys(tabs).map((i, idx) => ( + setsetRoute(setRoute, "/dash/" + i)} + > + {tabs[i]} + + ))} +
+ {/* {tab} */} +
+ {(() => { + switch (tab) { + case "class": + return ( + setsetRoute(setRoute, p)} + /> + ); + case "admin": + return ; + case "server": + return ; + } + })()} +
+
+
+ + + ); +} + +function ClassesMainEditor({ + pass, + setRoute, +}: { + pass: string; + setRoute: (s: string) => void; +}) { + const [classesData, setClassesData] = useState< + Record[] | false + >(false); + const [updateId, setUpdateId] = useState(0); + useEffect(() => { + ClassesReq(pass).then((i) => { + if (i[1]["ok"] == "da") { + setClassesData(i[1]["data"]); + } + }); + }, [updateId]); + return ( +
+ {classesData ? ( + <> + {classesData.map((i, idx) => ( +
+ {i.classid} + + {i.userclassname} + + + + setRoute("/class/" + i.classid)}> + + + +
+ ))} +
+ setUpdateId(updateId + 1)} /> +
+ + ) : ( + <> + + + )} +
+ ); +} +export const CopyClassLink = ({ + classId, + compact = false, +}: { + classId: string; + compact?: boolean; +}) => { + const [linkCopied, setLinkCopied] = useState(false); + + return ( + { + copyDXLink(classId).then((a) => { + if (a) { + setLinkCopied(true); + setTimeout(() => { + setLinkCopied(false); + }, 2000); + } + }); + }} + > + {compact ? ( + <>{linkCopied ? : } + ) : ( + <> + + {linkCopied ? "скопировано" : "коп ссылку"} + + )} + + ); +}; +function AdminsMainEditor({ pass }: { pass: string }) { + return ( +
+ e.preventDefault()} /> + {/* e.preventDefault()}/> */} +
+ ); +} +function ServerEditor({ pass }: { pass: string }) { + const [serverData, setServerData] = useState | false>( + false + ); + const [idle, setIdle] = useState(false); + const [showL, setShowL] = useState(false); + const [blockChecked, setBlockChecked] = useState(false); + let update = 0; + useEffect(() => { + ServerReq(pass).then(async (i) => { + if (i[0]) { + setServerData(i[1]); + setBlockChecked(!i[1]["STATUS"] as boolean); + } + }); + }, [update]); + return ( + <> +

настройки api сервера

+ + + + + + + + + + + + + + + +
admin_api_path + + {API_BASE_URL} + +
client_api_path + {serverData ? ( + + {serverData["CLIENT_API"]} + + ) : ( + + )} +
client_frontend + + {CLIENT_FRONTEND_URL} + +
+ + + + open phpliteadmin + + +
+

блокировка api

+
+ {serverData && ( + <> + { + console.log(v); + s(true); + setIdle(true); + const r = await ServerMsgBlockReq(pass, v as string); + if (r[1] !== true) { + alert(r[1]); + } else { + setServerData((prev) => ({ + ...prev, + BLOCKMSG: v as string, + })); + } + s(false); + setIdle(false); + }} + /> + { + setBlockChecked(e); + setIdle(true); + setShowL(true); + const r = await ServerBlockReq(pass, e); + if (r[1] !== true) { + alert(r[1]); + setBlockChecked(!e); + } + setIdle(false); + setShowL(false); + }} + /> + + )} + {(showL || !serverData) && } +
+
+ + ); +} + +function CreateClass({ pass, update }: { pass: string; update: () => void }) { + const [creating, setCreating] = useState(false); + const [classId, setClassId] = useState(""); + const [className, setClassName] = useState(""); + const [locked, setLocked] = useState(false); + const form = useRef(null); + return ( + <> +
+ + {creating && ( +
{ + e.preventDefault(); + if (locked || classId.length < 5) return; + setLocked(true); + const c = await CreateClassReq(pass, classId, className); + if (c[1] == true) { + update(); + setCreating(false); + } + setLocked(false); + }} + > + + setClassName((e.target as HTMLInputElement).value.trimStart()) + } + /> + { + let val = (e.target as HTMLInputElement).value.toUpperCase(); + let nval = ""; + val.split("").forEach((i) => { + if (AvailableSymbols.includes(i)) { + nval = nval + i; + } + }); + (e.target as HTMLInputElement).value = nval; + setClassId(nval); + }} + /> + +
+ )} + { + setCreating(!creating); + setClassName(""); + setClassId(randomString(8, AvailableSymbols).toUpperCase()); + }} + > + + + + +
+ + ); +} + +// export function AdminProfileEditor({ +// adminFrId, +// setAdminFrId, +// adminId, +// classId, +// pickmeMode = false, +// }) { +// return ( +//
+// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +//
имя +// { +// e.setIdle(true); +// const r = await AdminUpdateRequest(API_BASE_URL, adminId, { +// action: "change_name", +// value: e.value.trim(), +// }); +// if (r[1] == true) setAdminFrId(e.value); +// e.setIdle(false); +// }} +// /> +//
id +// +//
class +// {classId ? classId : } +//
+// +//
+// ); +// } + +// const CopyLink = ({ classId, adminId }) => { +// const [showDiag, setShowDiag] = useState(false); +// const [seenDiag, setSeenDiag] = useState(false); +// const [linkCopied, setLinkCopied] = useState(false); +// const copy = () => { +// copyDXLink(classId, adminId).then((a) => { +// if (a) { +// setLinkCopied(true); +// setTimeout(() => { +// setLinkCopied(false); +// }, 2000); +// } +// }); +// }; +// return ( +//
+// +// любой, кто получит ссылку,{" "} +// будет иметь доступ к админке.
вы желаете продолжить? +//

+// ), +// yesBtn: "да", +// noBtn: "нет", +// yesIcon: , +// noIcon: , +// bgDissmis: true, +// yesBtnStyle: "bg-orange-500! text-white! md:hover:bg-orange-600!", +// }} +// onAnswer={(answ) => { +// if (answ) { +// setSeenDiag(true); +// copy(); +// } +// setShowDiag(false); +// }} +// /> +// (seenDiag ? copy() : setShowDiag(true))}> +// +// {linkCopied ? "скопировано" : "коп ссылку на админку"} +// +//
+// ); +// }; + +export const CoolBg = ({}) => { + return ( + <> +
+ + ); +}; + +export const Header = ({ + setsetRoute, + setRoute, + back = false, +}: { + setsetRoute: ssroutetype; + setRoute: any; + back?: boolean; +}) => { + return ( +
+ + {back && ( + { + setsetRoute(setRoute, "/dash/"); + }} + className={ + "rounded-full " + + "lg:hover:bg-white lg:hover:fill-black" + + " lg:hover:scale-105 transition cursor-pointer" + } + /> + )} + logo + + { + setsetRoute(setRoute, "/"); + }} + onContextMenu={(e) => { + e.preventDefault(); + }} + className={ + "rounded-full " + + "lg:hover:bg-white lg:hover:fill-black" + + " lg:hover:scale-105 transition cursor-pointer" + } + /> +
+ ); +}; diff --git a/dx4dashboard/src/pages/LoginPage.tsx b/dx4dashboard/src/pages/LoginPage.tsx new file mode 100644 index 0000000..bfcc83f --- /dev/null +++ b/dx4dashboard/src/pages/LoginPage.tsx @@ -0,0 +1,113 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { useRef, useState } from "react"; +import Logo from "../assets/logo.svg"; + +import { BiHide, BiShow } from "react-icons/bi"; + +import { LoginReq } from "../stilltrash/req_mgr"; +import { useAtom } from "jotai"; +import type { ssroutetype } from "../types"; +export default function LoginPage({ + passAtom, + routeAtom, + setsetRoute, +}: { + passAtom: any; + routeAtom: any; + setsetRoute: ssroutetype; +}) { + const [show, setShow] = useState(false); + const [atomPass, setAtomPass] = useAtom(passAtom); + const [_route, setRoute] = useAtom(routeAtom); + const [loading, setLoading] = useState(false); + const [pass, setPass] = useState(""); + return ( +
+
+ {atomPass != "" && ( +
+ + go to dash + + +
+ )} + logo dengi +

dashboard

+

вход

+
{ + e.preventDefault(); + setLoading(true); + const r = await LoginReq(pass); + if (r[1]) { + setAtomPass(pass); + setsetRoute(setRoute, "/dash"); + } + setLoading(false); + }} + > + + setPass((e.target as HTMLInputElement).value)} + disabled={loading} + /> + { + setShow(!show); + }} + > + {!show ? ( + + ) : ( + + )} + + + + + +
+
+ ); +} + +function CoolLoginButton({ disabled }: { disabled: boolean }) { + const logbuttonRef = useRef(null); + + return ( + + ); +} diff --git a/dx4dashboard/src/stilltrash/dengi_components.tsx b/dx4dashboard/src/stilltrash/dengi_components.tsx new file mode 100644 index 0000000..a7c10c7 --- /dev/null +++ b/dx4dashboard/src/stilltrash/dengi_components.tsx @@ -0,0 +1,312 @@ +import { useEffect, useState, type AnchorHTMLAttributes } from "react"; +import { BiCheck, BiHide, BiShow, BiX } from "react-icons/bi"; + +export function Link(props: AnchorHTMLAttributes) { + let link = props.href; + if (!link) { + link = props.children as string; + } + return ( + + {link} + + ); +} + +export function Switch({ + checked, + defaultChecked = false, + onChange, + disabled = false, + ariaLabel, + className = "", + size = "md", // "sm" | "md" | "lg" +}: { + checked?: boolean; + defaultChecked?: boolean; + onChange?: (newState: boolean) => void; + disabled?: boolean; + ariaLabel?: string; + className?: string; + size?: "sm" | "md" | "lg"; +}) { + const isControlled = checked !== undefined; + const [internalChecked, setInternalChecked] = useState(defaultChecked); + const current = isControlled ? checked : internalChecked; + + useEffect(() => { + if (!isControlled) return; + // eslint-disable-next-line react-hooks/set-state-in-effect + setInternalChecked(checked); + }, [checked, isControlled]); + + const sizes = { + sm: { track: "w-9 h-5", thumb: "w-3 h-3", translate: "translate-x-4" }, + md: { track: "w-11 h-6", thumb: "w-4 h-4", translate: "translate-x-5" }, + lg: { track: "w-14 h-8", thumb: "w-6 h-6", translate: "translate-x-6" }, + }; + const s = sizes[size] || sizes.md; + + const toggle = (next?: boolean) => { + if (disabled) return; + const newState = typeof next === "boolean" ? next : !current; + if (!isControlled) setInternalChecked(newState); + onChange?.(newState); + }; + + return ( + + ); +} + +export function LoadingSpinner({ size }: { size: number }) { + return ( + + ); +} +export function DengiEditable({ + value, + password = false, + disWHidden = true, + disabled = false, + showLoading = true, + minLenght = 1, + placeholder, + onSubmit = () => {}, + onInput = () => {}, +}: { + value?: string | false; + password?: boolean; + disWHidden?: boolean; + disabled?: boolean; + placeholder?: string; + showLoading?: boolean; + minLenght?: number; + onSubmit?: ( + value: string | false | undefined, + setIdle: (v: boolean) => void + ) => void; + onInput?: ( + value: string | false | undefined, + setIdle: (v: boolean) => void + ) => void; +}) { + const [val, setVal] = useState(value); + const [hid, setHid] = useState(true); + const [idle, setIdle] = useState(false); + useEffect(() => { + setVal(value); + }, [value]); + return ( +
{ + if (idle) return; + e.preventDefault(); + onSubmit(val, setIdle); + }} + > + {value !== false || !showLoading ? ( + { + const v = (e.target as HTMLInputElement).value; + setVal(v); + onInput(v, setIdle); + }} + /> + ) : ( + + + + )} + + {password && ( + + )} + + {val !== value && ( + + )} +
+ ); +} +import trashcss from "./trash.module.css"; +export function TextPreloader({ width = "5rem" }) { + return ( + } + > + + + ); +} + +export function DengiButton({ + onClick, + children, + disabled = false, + className = "", +}: { + onClick?: () => void; + children?: any; + disabled?: boolean; + className?: string; +}) { + return ( + + {children} + + ); +} + +const defaultDialogContent = { + message: "?", + yesBtn: "da", + noBtn: "net", + bgDissmis: true, + yesBlocked: false, + yesIcon: , + noIcon: , + noBtnStyle: "", + yesBtnStyle: "", +}; +export function DengiYesOrNoDialog({ + content = defaultDialogContent, + show = false, + //@ts-ignore + onAnswer, +}) { + return ( + <> + {show && ( +
{ + if (e.target === e.currentTarget && content.bgDissmis) + onAnswer(false); + }} + > +
+ {content.message} +
+ onAnswer(true)} + disabled={content.yesBlocked} + > + {content.yesIcon} + {content.yesBtn} + + onAnswer(false)} + > + {content.noIcon} + {content.noBtn} + +
+
+
+ )} + + ); +} diff --git a/dx4dashboard/src/stilltrash/req_mgr.ts b/dx4dashboard/src/stilltrash/req_mgr.ts new file mode 100644 index 0000000..efae6d7 --- /dev/null +++ b/dx4dashboard/src/stilltrash/req_mgr.ts @@ -0,0 +1,128 @@ +import { API_BASE_URL } from "../App"; +import { b64e } from "./trash"; + +export async function makeRequest( + url: string, + params?: + | string + | Record + | string[][] + | URLSearchParams + | undefined +) { + try { + const req = await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams(params).toString(), + }); + + return req.ok && + req.headers.get("Content-Type")?.includes("application/json") + ? [1, await req.json()] + : [req.status, await req.text()]; + } catch (err) { + return [-1, err]; + } +} + +export function LoginReq(pass: string) { + return makeRequest(API_BASE_URL + "?login", { pass }); +} + +export function ServerReq(pass: string) { + return makeRequest(API_BASE_URL + "?server", { pass }); +} + +export function ServerBlockReq(pass: string, block: boolean) { + return makeRequest(API_BASE_URL + "?server", { + pass, + action: block ? "disable" : "enable", + }); +} +export function ServerMsgBlockReq(pass: string, value: string) { + return makeRequest(API_BASE_URL + "?server", { + pass, + action: "update_block_message", + value, + }); +} + +export function ClassesReq(pass: string) { + return makeRequest(API_BASE_URL + "?class", { pass }); +} + +export function ClassReq(pass: string, classId: string) { + return makeRequest(API_BASE_URL + "?class", { pass, classId }); +} + +export function ClassUpdateNameReq( + pass: string, + classId: string, + value: string +) { + return makeRequest(API_BASE_URL + "?class", { + pass, + classId, + action: "update_name", + value, + }); +} + +export function CreateClassReq( + pass: string, + classId: string, + className: string +) { + return makeRequest(API_BASE_URL + "?class", { + pass, + classId, + action: "create", + value: className, + }); +} + +export function DELETEClassRequest(pass: string, classId: string) { + return makeRequest(API_BASE_URL + "?class", { + pass, + classId, + action: "DELETE", + }); +} + +export function AdminUpdateReq( + pass: string, + adminId: string, + action: "level" | "name", + value: string +) { + return makeRequest(API_BASE_URL + "?admin", { + pass, + adminId, + action: "update_" + action, + value, + }); +} + +export function AdminRemoveReq(pass: string, adminId: string) { + return makeRequest(API_BASE_URL + "?admin", { + pass, + adminId, + action: "DELETE", + }); +} + +export function AdminCreateReq( + pass: string, + classId: string, + adminId: string, + adminName: string, + adminPrems: string +) { + return makeRequest(API_BASE_URL + "?class", { + pass, + classId, + action: "create_admin", + value: `${adminId};${b64e(adminName)};${adminPrems}`, + }); +} diff --git a/dx4dashboard/src/stilltrash/trash.module.css b/dx4dashboard/src/stilltrash/trash.module.css new file mode 100644 index 0000000..b213587 --- /dev/null +++ b/dx4dashboard/src/stilltrash/trash.module.css @@ -0,0 +1,11 @@ +.anim_preloader { + animation: anim-preloader 1s infinite linear; +} +@keyframes anim-preloader { + from { + transform: translateX(-50px) rotate(20deg); + } + to { + transform: translateX(calc((var(--w, 50px)) + (50px))) rotate(20deg); + } +} diff --git a/dx4dashboard/src/stilltrash/trash.ts b/dx4dashboard/src/stilltrash/trash.ts new file mode 100644 index 0000000..1e38227 --- /dev/null +++ b/dx4dashboard/src/stilltrash/trash.ts @@ -0,0 +1,91 @@ +import { CLIENT_FRONTEND_URL } from "../App"; + +export const secondPathSegment = (p: string) => + p.replace(/^\/?[^/]+/, "").replace(/^\/+/, ""); + +export function b64e(str: string) { + return btoa( + encodeURIComponent(str).replace( + /%([0-9A-F]{2})/g, + function toSolidBytes(_match, p1) { + // @ts-ignore + return String.fromCharCode("0x" + p1); + } + ) + ); +} + +export function b64d(str: string) { + return decodeURIComponent( + atob(str) + .split("") + .map(function (c) { + return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2); + }) + .join("") + ); +} + +/** by claude + * @param {string} text - The text to copy to clipboard + * @returns {Promise} - Returns true if successful, false otherwise + */ +export async function copyToClipboard(text: string) { + // Method 1: Modern Clipboard API (preferred) + if (navigator.clipboard && window.isSecureContext) { + try { + await navigator.clipboard.writeText(text); + return true; + } catch (err) { + console.warn("Clipboard API failed, trying fallback:", err); + } + } + // Method 2: Fallback for older browsers or non-secure contexts + try { + const textarea = document.createElement("textarea"); + textarea.value = text; + textarea.style.position = "fixed"; + textarea.style.left = "-999999px"; + textarea.style.top = "-999999px"; + textarea.style.opacity = "0"; + textarea.setAttribute("readonly", ""); + document.body.appendChild(textarea); + textarea.select(); + textarea.setSelectionRange(0, text.length); + const success = document.execCommand("copy"); + document.body.removeChild(textarea); + return success; + } catch (err) { + console.error("All copy methods failed:", err); + return false; + } +} + +// dx4 specific +export async function copyDXLink( + classId: string, + adminId: string | undefined = undefined +) { + return copyToClipboard( + CLIENT_FRONTEND_URL + "#//" + classId + (adminId ? ";" + b64e(adminId) : "") + ); +} + +export const randomString = (length: number, symbols: string[]) => { + const len = symbols.length; + return Array.from( + { length }, + () => symbols[Math.floor(Math.random() * len)] + ).join(""); +}; + + +export const AvailableSymbols = (() => { + const a = "abcdefghijklmnopqrstuvwxyz"; // alphabet + const A = a.toUpperCase(); // ALPHABET + const n = "1234567890"; // numbers + const s = "_-"; // specials + return (a + n + A + s).split(""); +})(); + +export const AvailableAdminSymbols = [...AvailableSymbols, ..."$()*^%@!+=.".split("")]; diff --git a/dx4dashboard/src/types.ts b/dx4dashboard/src/types.ts new file mode 100644 index 0000000..0b7b398 --- /dev/null +++ b/dx4dashboard/src/types.ts @@ -0,0 +1 @@ +export type ssroutetype = (setRoute: (e: string) => void, r: string) => void; diff --git a/dx4dashboard/tsconfig.app.json b/dx4dashboard/tsconfig.app.json new file mode 100644 index 0000000..efe3838 --- /dev/null +++ b/dx4dashboard/tsconfig.app.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": false, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/dx4dashboard/tsconfig.json b/dx4dashboard/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/dx4dashboard/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/dx4dashboard/tsconfig.node.json b/dx4dashboard/tsconfig.node.json new file mode 100644 index 0000000..8a67f62 --- /dev/null +++ b/dx4dashboard/tsconfig.node.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/dx4dashboard/vite.config.ts b/dx4dashboard/vite.config.ts new file mode 100644 index 0000000..4e77979 --- /dev/null +++ b/dx4dashboard/vite.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import tailwindcss from "@tailwindcss/vite"; +// https://vite.dev/config/ +export default defineConfig({ + plugins: [ + react({ + babel: { + plugins: [["babel-plugin-react-compiler"]], + }, + }), + tailwindcss(), + ], + base: "/dx4dashboard", +}); diff --git a/index.html b/index.html index 7cf409d..2e4c04c 100644 --- a/index.html +++ b/index.html @@ -23,6 +23,21 @@ content="/icons/windows11/Square150x150Logo.scale-100.png" /> + + + + dx 4 @@ -64,15 +79,6 @@ div[data-ktkz-loading] img { animation: ktkz-preloader-pulse 3s var(--animate-ease) infinite; } - div[data-ktkz-loading] p { - font-family: "Segoe UI", sans-serif; - color: #eef; - opacity: 0; - animation: ktkz-preloader-show 2s linear 2s forwards; - } - div[data-ktkz-loading] p a { - color: #aaf; - } @keyframes ktkz-preloader-spin { to { @@ -104,12 +110,6 @@
-

- если не работает попробуйте - перезагрузить -

diff --git a/package.json b/package.json index dfe2df0..c3e986a 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@types/react-dom": "^19.1.7", "@vitejs/plugin-react": "^5.0.0", "@vitejs/plugin-react-swc": "^4.2.1", + "babel-plugin-react-compiler": "^1.0.0", "eslint": "^9.33.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", diff --git a/src/App.jsx b/src/App.jsx index 1219e6c..816bb66 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,7 +1,7 @@ // CHANGE TO YOUR API DESTINATION +export const API_BASE_URL = "http://localhost:8321/api.php"; // export const API_BASE_URL = "http://192.168.3.10:8321/api.php"; -// export const API_BASE_URL = "http://localhost:8321/api.php" -export const API_BASE_URL = "http://x90620bx.beget.tech/dx4back/dengidengi.php" +// export const API_BASE_URL = "http://x90620bx.beget.tech/dx4back/api.php" import { atom, useAtom } from "jotai"; import { atomWithStorage } from "jotai/utils"; @@ -19,6 +19,8 @@ const pickmeModeAtom = atomWithStorage("pickmeMode", false); const EditorPage = lazy(() => import("./pages/editor")); const TestPage = lazy(() => import("./pages/test")); +const AdminProfilePage = lazy(() => import("./pages/AdminProfile")); +const ClassProfilePage = lazy(() => import("./pages/ClassProfile")); const dengi_cash = new DengiCaching(); @@ -61,6 +63,8 @@ const pages = { "/exit": , "/editor": , "/test": , + "/adminprofile": , + "/classprofile": , }; let changeAnchor = true; diff --git a/src/index.css b/src/index.css index 048fca8..af5c9fb 100644 --- a/src/index.css +++ b/src/index.css @@ -33,3 +33,7 @@ input { .bg-grain { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100'%3E%3Cfilter id='noise' x='0' y='0'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.8 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)' opacity='0.45'/%3E%3C/svg%3E"); } + +body > *:not(#root):not(vite-error-overlay) { + display: none !important; +} diff --git a/src/pages/AdminProfile.jsx b/src/pages/AdminProfile.jsx new file mode 100644 index 0000000..9d39d39 --- /dev/null +++ b/src/pages/AdminProfile.jsx @@ -0,0 +1,212 @@ +import { useAtom } from "jotai"; +import { motion } from "motion/react"; +import { BiCopy, BiError, BiLeftArrowAlt, BiLink, BiX } from "react-icons/bi"; +import Logo from "../assets/logo.svg"; +import { AdminReq, AdminUpdateRequest } from "../trash/req_mgr"; +import { API_BASE_URL } from "../App"; +import { useEffect, useState } from "react"; +import { + DengiButton, + DengiEditable, + DengiYesOrNoDialog, + TextPreloader, +} from "../trash/trash-components"; +import { copyDXLink } from "../trash/helpers"; + +export default function AdminProfilePage({ + adminAtom, + setsetRoute, + routeAtom, + pickmeModeAtom, + dengi_cash, + classIDAtom, +}) { + const [pickmeMode, _] = useAtom(pickmeModeAtom); + const [_route, setRoute] = useAtom(routeAtom); + const [adminId, _setAdminId] = useAtom(adminAtom); + const [classId, _setClassId] = useAtom(classIDAtom); + const [adminFrId, setAdminFrId] = useState(false); + const [adminData, setAdminData] = useState(false); + if (classId === "") { + setsetRoute(setRoute, "/"); + } + useEffect(() => { + let cancelled = false; + + (async () => { + const a = (await AdminReq(API_BASE_URL, adminId)) || false; + if (!cancelled && a[0] === 1) { + setAdminFrId(a[1].adminName); + setAdminData({ class: a[1].adminClass, adminPerms: a[1].adminPerms }); + } + })(); + return () => { + cancelled = true; + }; + }, [adminId]); + return ( + <> + + +
+ профиль админа + { + setAdminFrId(v); + dengi_cash.delete(`adminfr-${adminId}`); + }, + }} + /> + + + + + ); +} + +export function AdminProfileEditor({ + adminFrId, + setAdminFrId, + adminId, + classId, + pickmeMode = false, +}) { + return ( +
+ + + + + + + + + + + + + + + +
имя + { + e.setIdle(true); + const r = await AdminUpdateRequest(API_BASE_URL, adminId, { + action: "change_name", + value: e.value.trim(), + }); + if (r[1] == true) setAdminFrId(e.value); + e.setIdle(false); + }} + /> +
id + +
class + {classId ? classId : } +
+ +
+ ); +} + +const CopyLink = ({ classId, adminId }) => { + const [showDiag, setShowDiag] = useState(false); + const [seenDiag, setSeenDiag] = useState(false); + const [linkCopied, setLinkCopied] = useState(false); + const copy = () => { + copyDXLink(classId, adminId).then((a) => { + if (a) { + setLinkCopied(true); + setTimeout(() => { + setLinkCopied(false); + }, 2000); + } + }); + }; + return ( +
+ + любой, кто получит ссылку,{" "} + будет иметь доступ к админке.
вы желаете продолжить? +

+ ), + yesBtn: "да", + noBtn: "нет", + yesIcon: , + noIcon: , + bgDissmis: true, + yesBtnStyle: "bg-orange-500! text-white! md:hover:bg-orange-600!", + }} + onAnswer={(answ) => { + if (answ) { + setSeenDiag(true); + copy(); + } + setShowDiag(false); + }} + /> + (seenDiag ? copy() : setShowDiag(true))}> + + {linkCopied ? "скопировано" : "коп ссылку на админку"} + +
+ ); +}; + +const CoolBg = ({ pickmeMode }) => { + return ( + <> +
+ + ); +}; + +const Header = ({ setsetRoute, setRoute, pickmeMode }) => { + return ( +
+ + { + setsetRoute(setRoute, "/app"); + }} + onContextMenu={(e) => { + e.preventDefault(); + }} + className={ + "rounded-full " + + (pickmeMode + ? "lg:hover:bg-pink-400" + : "lg:hover:bg-white lg:hover:fill-black") + + " lg:hover:scale-105 transition cursor-pointer" + } + /> + logo + +
+ ); +}; diff --git a/src/pages/ClassProfile.jsx b/src/pages/ClassProfile.jsx new file mode 100644 index 0000000..fc9bf54 --- /dev/null +++ b/src/pages/ClassProfile.jsx @@ -0,0 +1,521 @@ +import { useAtom } from "jotai"; +import { AnimatePresence, motion } from "motion/react"; +import { + BiCheck, + BiCheckCircle, + BiCopy, + BiLeftArrowAlt, + BiLink, + BiPlus, + BiPlusCircle, + BiTrash, + BiX, + BiXCircle, +} from "react-icons/bi"; +import Logo from "../assets/logo.svg"; +import { + AdminReq, + AdminUpdateRequest, + ClassUpdateRequest, + getClassProfile, +} from "../trash/req_mgr"; +import { API_BASE_URL } from "../App"; +import { useEffect, useRef, useState } from "react"; +import { + DengiButton, + DengiEditable, + DengiYesOrNoDialog, + LoadingSpinner, + TextPreloader, +} from "../trash/trash-components"; +import { b64e, copyDXLink, randomString } from "../trash/helpers"; +import { AvailableAdminSymbols } from "../trash/dengi"; + +export default function AdminProfilePage({ + classIDAtom, + adminAtom, + setsetRoute, + routeAtom, + pickmeModeAtom, +}) { + const [pickmeMode, _] = useAtom(pickmeModeAtom); + const [_route, setRoute] = useAtom(routeAtom); + const [adminId, _setAdminId] = useAtom(adminAtom); + const [classId, _setClassId] = useAtom(classIDAtom); + const [error, setError] = useState(""); + const [classData, setClassData] = useState(false); + const [adminsData, setAdminsData] = useState(false); + const [dataId, setDataId] = useState(0); + if (classId === "") { + setsetRoute(setRoute, "/"); + } + useEffect(() => { + let cancelled = false; + + (async () => { + const a = (await getClassProfile(API_BASE_URL, adminId)) || false; + if (!cancelled && a[0] === 1) { + if (a[1]["class"]) { + setClassData(a[1]["class"]); + setAdminsData(a[1]["admins"]); + } else { + setError(a[1]); + } + } else { + setError(a[1]); + } + })(); + return () => { + cancelled = true; + }; + }, [adminId, dataId]); + return ( + <> + + +
+ + управление классом + + {error && typeof error == "string" && ( + {error} + )} + { + setDataId(dataId + 1); + }, + setAdminsData, + setClassFr: (v) => { + setClassData((prev) => ({ ...prev, userclassname: v })); + }, + }} + /> + + + + + ); +} + +export function ClassProfileEditor({ + classId, + classFr, + pickmeMode, + loaded, + adminId, + adminsData, + setClassFr, + update, + setAdminsData, +}) { + return ( +
+ + + + + + + + + + + +
classId + {classId} +
название + { + e.setIdle(true); + let r = await ClassUpdateRequest(API_BASE_URL, adminId, { + action: "update_name", + value: e.value.trim(), + }); + if (r[1] == true) setClassFr(e.value.trim()); + e.setIdle(false); + }} + /> +
+
+ +
+ + админы + + + + + + + + + + + {adminsData && + adminsData.map((i) => ( + { + setAdminsData((prev) => + prev.map((a) => + a.adminId === i.adminId ? { ...a, adminName: v } : a + ) + ); + }, + }} + /> + ))} + {!loaded && ( + + + + + + + )} + +
имяidуров.
+ + + + + + + + + + + + + +
+ {loaded && } +
+ ); +} + +function CreateAdmin({ editorAdminId, update }) { + const [creating, setCreating] = useState(false); + const [adminId, setAdminId] = useState(""); + const [adminName, setAdminName] = useState(""); + const [locked, setLocked] = useState(false); + const form = useRef(null); + return ( + <> +
+ + + {creating && ( + { + e.preventDefault(); + if (locked || adminId.length < 5) return; + setLocked(true); + const c = await ClassUpdateRequest( + API_BASE_URL, + editorAdminId, + { + action: "create_admin", + value: `${adminId};${b64e(adminName)}`, + } + ); + if (c[1] == true) { + update(); + setCreating(false); + } + setLocked(false); + }} + > + setAdminName(e.target.value.trimStart())} + /> + { + let val = e.target.value; + let nval = ""; + val.split("").forEach((i) => { + if (AvailableAdminSymbols.includes(i)) { + nval = nval + i; + } + }); + e.target.value = nval; + setAdminId(nval); + }} + /> + + + )} + + { + setCreating(!creating); + setAdminName(""); + setAdminId(randomString(8, AvailableAdminSymbols)); + }} + > + + + + +
+ + ); +} + +function AdminProfileRow({ + adminId, + editorAdminId, + adminName, + classId, + pickmeMode, + setAdminFrId, + adminLevel, + update, +}) { + return ( + + + { + e.setIdle(true); + const r = await AdminUpdateRequest(API_BASE_URL, editorAdminId, { + action: "change_name", + value: e.value.trim(), + person: adminId, + }); + if (r[1] == true) setAdminFrId(e.value); + e.setIdle(false); + }} + /> + + + + + {adminLevel} + + + + + + ); +} + +const RemoveAdminButton = ({ + adminId, + editorAdminId, + adminName, + update, + disabled, +}) => { + const [showDiag, setShowDiag] = useState(false); + const [deleting, setDeleting] = useState(false); + + return ( + <> + + вы точно хотите удалить админа {adminName}? +

+ ), + yesBtn: "да", + noBtn: "нет", + yesIcon: , + noIcon: , + bgDissmis: true, + yesBtnStyle: "bg-red-500! text-white! md:hover:bg-red-600!", + }} + onAnswer={async (answ) => { + setShowDiag(false); + if (answ) { + setDeleting(true); + const c = await ClassUpdateRequest(API_BASE_URL, editorAdminId, { + action: "delete_admin", + value: adminId, + }); + if (c[1] == true) { + update(); + setDeleting(false); + } + } + }} + /> + { + if (deleting) return; + setShowDiag(true); + }} + > + {deleting ? : } + + + ); +}; + +const CopyClassLink = ({ classId }) => { + const [linkCopied, setLinkCopied] = useState(false); + + return ( + { + copyDXLink(classId).then((a) => { + if (a) { + setLinkCopied(true); + setTimeout(() => { + setLinkCopied(false); + }, 2000); + } + }); + }} + > + + {linkCopied ? "скопировано" : "коп ссылку"} + + ); +}; +const CopyAdminLink = ({ classId, adminId }) => { + const [showDiag, setShowDiag] = useState(false); + const [seenDiag, setSeenDiag] = useState(false); + const [linkCopied, setLinkCopied] = useState(false); + const copy = () => { + copyDXLink(classId, adminId).then((a) => { + if (a) { + setLinkCopied(true); + setTimeout(() => { + setLinkCopied(false); + }, 2000); + } + }); + }; + return ( + <> + + любой, кто получит ссылку,{" "} + будет иметь доступ к админке.
вы желаете продолжить? +

+ ), + yesBtn: "да", + noBtn: "нет", + yesIcon: , + noIcon: , + bgDissmis: true, + yesBtnStyle: "bg-orange-500! text-white! md:hover:bg-orange-600!", + }} + onAnswer={(answ) => { + if (answ) { + setSeenDiag(true); + copy(); + } + setShowDiag(false); + }} + /> + (seenDiag ? copy() : setShowDiag(true))}> + {linkCopied ? : } + + + ); +}; + +const CoolBg = ({ pickmeMode }) => { + return ( + <> +
+ + ); +}; + +const Header = ({ setsetRoute, setRoute, pickmeMode }) => { + return ( +
+ + { + setsetRoute(setRoute, "/app"); + }} + onContextMenu={(e) => { + e.preventDefault(); + }} + className={ + "rounded-full " + + (pickmeMode + ? "lg:hover:bg-pink-400" + : "lg:hover:bg-white lg:hover:fill-black") + + " lg:hover:scale-105 transition cursor-pointer" + } + /> + logo + +
+ ); +}; diff --git a/src/pages/ControlPanel.jsx b/src/pages/ControlPanel.jsx deleted file mode 100644 index 7ed544e..0000000 --- a/src/pages/ControlPanel.jsx +++ /dev/null @@ -1,9 +0,0 @@ -export default function Panel() { - return <> -
-
- -
-
- ; -} diff --git a/src/pages/DX4AppPage.jsx b/src/pages/DX4AppPage.jsx index b256ef0..a31cd69 100644 --- a/src/pages/DX4AppPage.jsx +++ b/src/pages/DX4AppPage.jsx @@ -1,20 +1,22 @@ -import React, { useEffect, useRef, useState } from "react"; +import { Fragment, memo, useEffect, useRef, useState } from "react"; import { BiDotsHorizontalRounded, BiExit, BiSolidUser, - BiStar, BiPencil, BiLink, + BiWrench, } from "react-icons/bi"; import Logo from "../assets/logo.svg"; import { Switch, LoadingSpinner, DatePicker, + TextPreloader, // CoolMouseBg, } from "../trash/trash-components"; import { + AdminReq, AdminUserFRequest, classUserFRequest, RaspGet, @@ -24,7 +26,7 @@ import { AnimatePresence, LayoutGroup, motion } from "motion/react"; import { apiFormatDate, isTimeBeforeNow } from "../trash/dengi"; import { useAtom } from "jotai"; import { API_BASE_URL } from "../App"; -import { copyToClipboard, ifstyle } from "../trash/helpers"; +import { copyDXLink, ifstyle } from "../trash/helpers"; export function DX4App({ classIDAtom, @@ -41,6 +43,7 @@ export function DX4App({ const [pickmeMode, setPickmeMode] = useAtom(pickmeModeAtom); const [rasp, setRasp] = useState([false, {}, 0]); const [selectedDate, setSelectedDate] = useState(new Date()); + const [managing, setManaging] = useState(false); useEffect(() => { let cancelled = false; @@ -49,14 +52,14 @@ export function DX4App({ if (!dengi_cash.wasCached(cacheName)) { RaspGet(API_BASE_URL, classId, selectedDate).then( (d) => { - if (cancelled) return; if (d[0] == 1) { try { - setRasp([ - true, - d[1]["data"] && d[1]["data"][0] ? d[1]["data"] : -322, - d[1]["type"] ? d[1]["type"] : -1, - ]); + if (!cancelled) + setRasp([ + true, + d[1]["data"] && d[1]["data"][0] ? d[1]["data"] : -322, + d[1]["type"] ? d[1]["type"] : -1, + ]); //console.log(d, d[1] ? d[1] : false); dengi_cash.cache(cacheName, [ d[1]["data"] && d[1]["data"][0] ? d[1]["data"] : -322, @@ -64,18 +67,20 @@ export function DX4App({ ]); } catch (exception) { //console.error(exception); - setRasp([true, exception.toString()]); + if (!cancelled) setRasp([true, exception.toString()]); } } else { - setRasp([true, "ошибка: " + d[1].toString()]); + if (!cancelled) setRasp([true, "ошибка: " + d[1].toString()]); //console.log(d[1]); } }, (err) => { + if (cancelled) return; setRasp([true, "ошибка: " + err]); } ); } else { + if (cancelled) return; setRasp([true, ...dengi_cash.get(cacheName)]); } @@ -133,8 +138,8 @@ export function DX4App({ else promises.push(Promise.resolve([0])); if (adminId !== "") promises.push( - dengi_cash.withCacheAsync(`adminfr-${adminId}`, async () => - AdminUserFRequest(API_BASE_URL, adminId) + dengi_cash.withCacheAsync(`admindata-${adminId}`, async () => + AdminReq(API_BASE_URL, adminId) ) ); else promises.push(Promise.resolve([0])); @@ -143,6 +148,8 @@ export function DX4App({ const classF = classRes[0] === 1 ? classRes[1].userclassname : false; const adminF = adminRes[0] === 1 ? adminRes[1].adminName : false; setUserFrNames({ admin: adminF, class: classF }); + if (adminRes[0] === 1) + setManaging([-1, 2].includes(adminRes[1].adminPrems)); }); return () => { cancelled = true; @@ -259,28 +266,65 @@ export function DX4App({ + {adminId != "" && managing && ( + + { + // setsetRoute(setRoute, `/editor/${apiFormatDate(selectedDate)}`); + // }} + href={`#/classprofile`} + className={`flex items-center justify-center border p-1 gap-2 px-2 cursor-pointer rounded-xl transition-colors ${ + pickmeMode + ? "border-pink-400/30 text-pink-300/80 lg:hover:text-white lg:hover:border-pink-500" + : "border-zinc-700 text-zinc-400 lg:hover:text-white lg:hover:border-white" + } `} + > + управление + + + )} - {adminId != "" && ( - +
+ + {adminId != "" && ( { // setsetRoute(setRoute, `/editor/${apiFormatDate(selectedDate)}`); // }} - href={`#/editor/${apiFormatDate(selectedDate)}`} - className="flex items-center justify-center border p-1 gap-2 px-2 lg:hover:border-white cursor-pointer rounded-xl transition-colors border-zinc-700 text-zinc-400 lg:hover:text-white" + href={ + userFrNames.admin + ? `#/editor/${apiFormatDate(selectedDate)}` + : undefined + } + className={`flex items-center justify-center p-1.5 cursor-pointer border transition *:transition-colors rounded-full ${ + pickmeMode + ? "border-pink-400/30 lg:hover:text-white hover:lg:bg-pink-500" + : "border-zinc-700 text-white lg:hover:text-black lg:hover:bg-white" + } `} > - открыть в редакторе + {userFrNames.admin ? ( + + ) : ( + + )} - - )} - + )} +
@@ -306,7 +350,7 @@ export function DX4App({ ); } -const CoolMainBg = React.memo(({ pickmeMode }) => { +const CoolMainBg = memo(({ pickmeMode }) => { return (
{ ); }); -export const RaspView = React.memo(({ rasp, pickmeMode, selectedDate }) => { +export const RaspView = memo(({ rasp, pickmeMode, selectedDate }) => { return ( {rasp[0] && typeof rasp[1] == "object" && ( @@ -324,10 +368,14 @@ export const RaspView = React.memo(({ rasp, pickmeMode, selectedDate }) => { //layout="position" key={selectedDate} className={ - "w-full border-collapse border-spacing-x-0 border-spacing-y-[10px] bg-gradient-to-br backdrop-blur-xl mb-10 " + + "w-full border-collapse border-spacing-x-0 border-spacing-y-[10px] bg-gradient-to-br mb-10 " + (pickmeMode ? "from-pink-400/30" : "from-zinc-800/40") + " to-transparent rounded-2xl selectable" } + onContextMenu={(ev) => { + ev.preventDefault(); + itemRedo.action(); + }} variants={{ hid: { opacity: 0 }, show: { opacity: 1 }, @@ -372,61 +420,7 @@ export const RaspView = React.memo(({ rasp, pickmeMode, selectedDate }) => { {rasp[1].map((d, i) => ( - - - {d["num"]} - - - {d.subject - .toString() - .split("\n") - .map((line, i, array) => ( - - {line} - {i < array.length - 1 &&
} -
- )) || d.subject} - - - {d.cabinet - .toString() - .split("\n") - .map((line, i, array) => ( - - {line} - {i < array.length - 1 &&
} -
- )) || d.cabinet} - - -
-
- - {d["t-begin"]}  - - - {d["t-end"]} -
-
- - + ))} @@ -457,7 +451,7 @@ export const RaspView = React.memo(({ rasp, pickmeMode, selectedDate }) => { ); }); -const UserSettings = React.memo( +const UserSettings = memo( ({ openUserSettings, userFrNames, @@ -476,8 +470,8 @@ const UserSettings = React.memo( , - `админ ${ - userFrNames["admin"] ? userFrNames["admin"] : adminId - }`, - () => {}, + <> + {`админ ${userFrNames["admin"] ? userFrNames["admin"] : ""}`} + {!userFrNames["admin"] && } + , + () => { + setsetRoute(setRoute, "/adminprofile"); + }, adminId != "", ], [ , - classLinkCopied ? "скопированно" : "коп. ссылку", + classLinkCopied ? "скопировано" : "коп. ссылку", () => { - copyToClipboard( - location.href.substring(0, location.href.search("#")) + - "#//" + - classId - ).then((a) => { + copyDXLink(classId).then((a) => { if (a) { setClassLinkCopied(true); setTimeout(() => { @@ -556,3 +549,88 @@ const UserSettings = React.memo( ); } ); + +class GlobalListener { + constructor() { + this.listeners = []; + } + listen(fn, once = false) { + const wrapper = (...args) => { + try { + fn(...args); + } finally { + if (once) this.listeners = this.listeners.filter((l) => l !== wrapper); + } + }; + this.listeners.push(wrapper); + return () => { + this.listeners = this.listeners.filter((l) => l !== wrapper); + }; + } + action(...args) { + this.listeners.slice().forEach((fn) => { + try { + fn(...args); + } catch (err) { + console.error(err); + } + }); + } +} + +const itemRedo = new GlobalListener(); + +const TableItem = memo(({ d }) => { + const [sel, setSel] = useState(false); + useEffect(() => { + itemRedo.listen(() => { + setSel(false); + }); + }, []); + return ( + { + setSel(!sel); + }} + > + {d["num"]} + + {d.subject + .toString() + .split("\n") + .map((line, i, array) => ( + + {line} + {i < array.length - 1 &&
} +
+ )) || d.subject} + + + {d.cabinet + .toString() + .split("\n") + .map((line, i, array) => ( + + {line} + {i < array.length - 1 &&
} +
+ )) || d.cabinet} + + +
+
+ {d["t-begin"]}  - + {d["t-end"]} +
+
+ + + ); +}); diff --git a/src/pages/LoginPage.jsx b/src/pages/LoginPage.jsx index 25f4b37..307ca50 100644 --- a/src/pages/LoginPage.jsx +++ b/src/pages/LoginPage.jsx @@ -7,15 +7,7 @@ import { BiHide, BiShow, BiSolidErrorCircle, BiBlock } from "react-icons/bi"; import { LoginReq, AdminReq } from "../trash/req_mgr"; import { API_BASE_URL } from "../App"; import { b64d } from "../trash/helpers"; -const AvailableSymbols = (() => { - const a = "abcdefghijklmnopqrstuvwxyz"; // alphabet - const A = a.toUpperCase(); // ALPHABET - const n = "1234567890"; // numbers - const s = "_-"; // specials - return (a + n + A + s).split(""); -})(); - -const AvailableAdminSymbols = [...AvailableSymbols, ..."$()*^%@!+=.".split("")]; +import { AvailableAdminSymbols, AvailableSymbols } from "../trash/dengi"; export function LoginPage({ classIDAtom, @@ -51,7 +43,7 @@ export function LoginPage({ setsetRoute(setRoute, "/app"); } else { const al = await AdminReq(API_BASE_URL, aid); - console.log(al); + // console.log(al); if (al[0] === 1) { // yes, I know that check must go on backend, but im stupid lazy piece of shit 💀💀💀 // TODO: FIX LATER @@ -61,8 +53,8 @@ export function LoginPage({ setsetRoute(setRoute, "/app"); } else { // you are not from here - console.log(al); - setErrorText("админ не пренадлежит этому классу"); + // console.log(al); + setErrorText("админ не найден"); } } else { setErrorText(al[1]); @@ -108,9 +100,9 @@ export function LoginPage({ {errorText.length > 0 && ( @@ -128,24 +120,23 @@ export function LoginPage({ )} - - добро пожаловать. - вход + logo dengi +

добро пожаловать.

+

вход

- - + setIsAdmin(e)} @@ -178,7 +169,7 @@ export function LoginPage({ name="" /> админка - + @@ -200,8 +191,8 @@ export function LoginPage({ {isLogining && ( @@ -224,7 +215,7 @@ function CoolLoginButton({ display, isLogining }) { initial={{ y: 10, opacity: 0 }} animate={{ y: 0, opacity: 1 }} ref={logbuttonRef} - exit={{ y: 40, opacity: 0, rotate: 20, filter: "blur(3px)" }} + exit={{ y: 40, opacity: 0, rotate: 20 }} onMouseMove={(e) => { const rect = e.target.getBoundingClientRect(); e.target.style.setProperty( @@ -257,10 +248,10 @@ function CoolInputWithHide({ exit={{ y: 10, opacity: 0 }} className="flex *:outline-none *:border *:border-transparent *:focus:border-white" > - { @@ -78,7 +75,7 @@ export default function EditorPage(props) { }, [classId, adminId]); // on rerenderr useEffect(() => { - console.log("editor cache clean"); + // console.log("editor cache clean"); props.dengi_cash.delete( "editor_schedule", "editor_changes", @@ -163,9 +160,17 @@ export default function EditorPage(props) { {userFrNames["class"] ? userFrNames["class"] : classId} - + - {userFrNames["admin"] ? userFrNames["admin"] : adminId} + {userFrNames["admin"] ? ( + userFrNames["admin"] + ) : ( + + )} - + {formatDate(date)}
-
+
{tabs.map((item, idx) => (
)}
@@ -246,6 +268,7 @@ export default function EditorPage(props) { setChangesWereMade, date, adminId, + pickmeMode, }} dengiCash={props.dengi_cash} /> @@ -255,16 +278,16 @@ export default function EditorPage(props) { {/* */}
- + ); } -function DefaultEditorBG() { +function DefaultEditorBG({ pickmeMode }) { return (
); } diff --git a/src/pages/test.jsx b/src/pages/test.jsx index 052ff1c..225c602 100644 --- a/src/pages/test.jsx +++ b/src/pages/test.jsx @@ -1,212 +1,11 @@ -import React, { useState } from 'react'; -import { DndContext, closestCenter, PointerSensor, TouchSensor, useSensor, useSensors } from '@dnd-kit/core'; -import { arrayMove, SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable'; -import { CSS } from '@dnd-kit/utilities'; +import { TextPreloader } from "../trash/trash-components"; -// Configuration -const COLUMNS = [ - { key: 'position', label: 'Pos', width: 'w-16', placeholder: '#' }, - { key: 'subject', label: 'Subject', width: 'flex-1', placeholder: 'Enter subject' }, - { key: 'time', label: 'Time', width: 'w-24', placeholder: '00:00' }, - { key: 'room', label: 'Room', width: 'w-20', placeholder: 'A-1' }, -]; - -const INITIAL_DATA = [ - { id: '1', position: '1', subject: 'Mathematics', time: '09:00', room: 'A-1' }, - { id: '2', position: '2', subject: 'Physics', time: '10:00', room: 'B-2' }, - { id: '3', position: '3', subject: 'Chemistry', time: '11:00', room: 'A-3' }, - { id: '4', position: '4', subject: 'Biology', time: '13:00', room: 'C-1' }, -]; - -function SortableRow({ item, columns, onUpdate, onDelete }) { - const { - attributes, - listeners, - setNodeRef, - transform, - transition, - isDragging, - } = useSortable({ id: item.id }); - - const style = { - transform: CSS.Transform.toString(transform), - transition, - opacity: isDragging ? 0.5 : 1, - }; +export default function TestPage() { return ( -
-
- {columns.map((col) => ( - onUpdate(item.id, col.key, e.target.value)} - className={`${col.width} px-3 py-3 bg-transparent text-white border-r border-gray-700 last:border-r-0 focus:outline-none focus:bg-gray-700/50 placeholder-gray-500`} - placeholder={col.placeholder} - /> - ))} -
- ⋮ -
- + <> +
+
-
+ ); } - -export default function ScheduleEditor({ - initialData = INITIAL_DATA, - columns = COLUMNS, - onChange, -}) { - const [items, setItems] = useState(initialData); - - const sensors = useSensors( - useSensor(PointerSensor, { - activationConstraint: { - distance: 8, - }, - }), - useSensor(TouchSensor, { - activationConstraint: { - delay: 200, - tolerance: 8, - }, - }) - ); - - const updateItems = (newItems) => { - setItems(newItems); - onChange?.(newItems); - }; - - const handleDragEnd = (event) => { - const { active, over } = event; - - if (over && active.id !== over.id) { - const oldIndex = items.findIndex((item) => item.id === active.id); - const newIndex = items.findIndex((item) => item.id === over.id); - - const newItems = arrayMove(items, oldIndex, newIndex); - - // Update positions based on new order - const positions = newItems.map(item => item.position); - const reorderedItems = newItems.map((item, idx) => ({ - ...item, - position: positions[idx] - })); - - updateItems(reorderedItems); - } - }; - - const handleUpdate = (id, field, value) => { - const newItems = items.map(item => - item.id === id ? { ...item, [field]: value } : item - ); - updateItems(newItems); - }; - - const handleDelete = (id) => { - const newItems = items.filter(item => item.id !== id); - updateItems(newItems); - }; - - const handleAddRow = () => { - const newId = String(Date.now()); - const lastPosition = items.length > 0 - ? String(parseInt(items[items.length - 1].position || '0') + 1) - : '1'; - - const newItem = { id: newId, position: lastPosition }; - columns.forEach(col => { - if (col.key !== 'position') { - newItem[col.key] = ''; - } - }); - - updateItems([...items, newItem]); - }; - - const handleClear = () => { - updateItems([]); - }; - - return ( -
-
-

- Schedule Editor -

- - - item.id)} - strategy={verticalListSortingStrategy} - > -
- {items.map((item) => ( - - ))} -
-
-
- - {items.length === 0 && ( -
- No items. Add a row to get started. -
- )} - -
- - -
- -
-

Instructions:

-
    -
  • • Drag rows by the handle (⋮) on the right to reorder
  • -
  • • Positions move with their rows when dragged
  • -
  • • Click any field to edit
  • -
  • • Click × to delete a row
  • -
-
-
-
- ); -} \ No newline at end of file diff --git a/src/pages/test_arch0.jsx b/src/pages/test_arch0.jsx new file mode 100644 index 0000000..052ff1c --- /dev/null +++ b/src/pages/test_arch0.jsx @@ -0,0 +1,212 @@ +import React, { useState } from 'react'; +import { DndContext, closestCenter, PointerSensor, TouchSensor, useSensor, useSensors } from '@dnd-kit/core'; +import { arrayMove, SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; + +// Configuration +const COLUMNS = [ + { key: 'position', label: 'Pos', width: 'w-16', placeholder: '#' }, + { key: 'subject', label: 'Subject', width: 'flex-1', placeholder: 'Enter subject' }, + { key: 'time', label: 'Time', width: 'w-24', placeholder: '00:00' }, + { key: 'room', label: 'Room', width: 'w-20', placeholder: 'A-1' }, +]; + +const INITIAL_DATA = [ + { id: '1', position: '1', subject: 'Mathematics', time: '09:00', room: 'A-1' }, + { id: '2', position: '2', subject: 'Physics', time: '10:00', room: 'B-2' }, + { id: '3', position: '3', subject: 'Chemistry', time: '11:00', room: 'A-3' }, + { id: '4', position: '4', subject: 'Biology', time: '13:00', room: 'C-1' }, +]; + +function SortableRow({ item, columns, onUpdate, onDelete }) { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id: item.id }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.5 : 1, + }; + return ( +
+
+ {columns.map((col) => ( + onUpdate(item.id, col.key, e.target.value)} + className={`${col.width} px-3 py-3 bg-transparent text-white border-r border-gray-700 last:border-r-0 focus:outline-none focus:bg-gray-700/50 placeholder-gray-500`} + placeholder={col.placeholder} + /> + ))} +
+ ⋮ +
+ +
+
+ ); +} + +export default function ScheduleEditor({ + initialData = INITIAL_DATA, + columns = COLUMNS, + onChange, +}) { + const [items, setItems] = useState(initialData); + + const sensors = useSensors( + useSensor(PointerSensor, { + activationConstraint: { + distance: 8, + }, + }), + useSensor(TouchSensor, { + activationConstraint: { + delay: 200, + tolerance: 8, + }, + }) + ); + + const updateItems = (newItems) => { + setItems(newItems); + onChange?.(newItems); + }; + + const handleDragEnd = (event) => { + const { active, over } = event; + + if (over && active.id !== over.id) { + const oldIndex = items.findIndex((item) => item.id === active.id); + const newIndex = items.findIndex((item) => item.id === over.id); + + const newItems = arrayMove(items, oldIndex, newIndex); + + // Update positions based on new order + const positions = newItems.map(item => item.position); + const reorderedItems = newItems.map((item, idx) => ({ + ...item, + position: positions[idx] + })); + + updateItems(reorderedItems); + } + }; + + const handleUpdate = (id, field, value) => { + const newItems = items.map(item => + item.id === id ? { ...item, [field]: value } : item + ); + updateItems(newItems); + }; + + const handleDelete = (id) => { + const newItems = items.filter(item => item.id !== id); + updateItems(newItems); + }; + + const handleAddRow = () => { + const newId = String(Date.now()); + const lastPosition = items.length > 0 + ? String(parseInt(items[items.length - 1].position || '0') + 1) + : '1'; + + const newItem = { id: newId, position: lastPosition }; + columns.forEach(col => { + if (col.key !== 'position') { + newItem[col.key] = ''; + } + }); + + updateItems([...items, newItem]); + }; + + const handleClear = () => { + updateItems([]); + }; + + return ( +
+
+

+ Schedule Editor +

+ + + item.id)} + strategy={verticalListSortingStrategy} + > +
+ {items.map((item) => ( + + ))} +
+
+
+ + {items.length === 0 && ( +
+ No items. Add a row to get started. +
+ )} + +
+ + +
+ +
+

Instructions:

+
    +
  • • Drag rows by the handle (⋮) on the right to reorder
  • +
  • • Positions move with their rows when dragged
  • +
  • • Click any field to edit
  • +
  • • Click × to delete a row
  • +
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/trash/dengi.js b/src/trash/dengi.js index 79269a1..7f60814 100644 --- a/src/trash/dengi.js +++ b/src/trash/dengi.js @@ -53,7 +53,11 @@ export function usToRussianDay(usDay) { export function formatDate(date) { return `${ dayOfWeeksNames[usToRussianDay(date.getDay())] - }, ${date.getDate()} ${monthNames[date.getMonth()]}`; + }, ${date.getDate()} ${monthNames[date.getMonth()]}${ + date.getFullYear() != new Date().getFullYear() + ? " " + date.getFullYear() + : "" + }`; } export function apiFormatDate(date) { return ( @@ -128,9 +132,33 @@ export function convertEditorToApi(arr) { } export function isTimeBeforeNow(timeStr) { - const [h, m] = timeStr.split(':').map(Number); + const [h, m] = timeStr.split(":").map(Number); if (!Number.isFinite(h) || !Number.isFinite(m)) return false; const now = new Date(); - const t = new Date(now.getFullYear(), now.getMonth(), now.getDate(), h, m, 0, 0); + const t = new Date( + now.getFullYear(), + now.getMonth(), + now.getDate(), + h, + m, + 0, + 0 + ); return t.getTime() < now.getTime(); -} \ No newline at end of file +} +export const premsMeanings = { + "-1": "неограниченый", + // 0: "reserved" + 1: "обычный", + 2: "управляющий", +}; + +export const AvailableSymbols = (() => { + const a = "abcdefghijklmnopqrstuvwxyz"; // alphabet + const A = a.toUpperCase(); // ALPHABET + const n = "1234567890"; // numbers + const s = "_-"; // specials + return (a + n + A + s).split(""); +})(); + +export const AvailableAdminSymbols = [...AvailableSymbols, ..."$()*^%@!+=.".split("")]; diff --git a/src/trash/editorTable.jsx b/src/trash/editorTable.jsx index 83cb10c..a9686db 100644 --- a/src/trash/editorTable.jsx +++ b/src/trash/editorTable.jsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { DndContext, closestCenter, @@ -155,6 +155,7 @@ function SortableRow({ onDuplicate, setFocusEdit, setHoverEdit, + pickmeMode, }) { const { attributes, @@ -175,7 +176,9 @@ function SortableRow({
@@ -188,7 +191,11 @@ function SortableRow({ wrap="hard" rows={1} onChange={(e) => onUpdate(item.id, col.key, e.target.value)} - className={`px-1 py-3 text-center bg-transparent text-white border-r border-zinc-700 last:border-r-0 last:rounded-tr-lg focus:outline-none focus:bg-gray-700/50 placeholder-gray-500 transition-colors hover:bg-gray-700/20 first:rounded-tl-lg max-h-min resize-none block min-w-0 grow ${col.width}`} + className={`px-1 py-3 text-center bg-transparent text-white border-r ${ + pickmeMode ? "border-pink-400/50" : "border-zinc-700" + } last:border-r-0 last:rounded-tr-lg focus:outline-none focus:bg-gray-700/50 placeholder-gray-500 transition-colors hover:bg-gray-700/20 first:rounded-tl-lg max-h-min resize-none block min-w-0 grow ${ + col.width + }`} placeholder={col.placeholder} onFocus={() => setFocusEdit(col.key)} onBlur={() => setFocusEdit(null)} @@ -244,6 +251,7 @@ export function TableEditor({ adminId, date, dengiCash, + pickmeMode = false, }) { const [items, setItems] = useState(initialData); const [undoItems, setUndoItems] = useState([]); @@ -553,7 +561,9 @@ export function TableEditor({ ))} @@ -585,6 +595,7 @@ export function TableEditor({ onDuplicate={handleDuplicate} setFocusEdit={setFocusEdit} setHoverEdit={setHoverEdit} + pickmeMode={pickmeMode} /> ))} @@ -600,7 +611,9 @@ export function TableEditor({ opacity: 1, scale: 1, }} - className="border-2 border-dashed border-gray-700 rounded-lg p-12 text-center text-gray-500" + className={`border-2 border-dashed ${ + pickmeMode ? "border-pink-700" : "border-gray-700" + } rounded-lg p-12 text-center text-gray-500`} > пусто diff --git a/src/trash/editor_req_mgr.js b/src/trash/editor_req_mgr.js index 180e1dc..7e00206 100644 --- a/src/trash/editor_req_mgr.js +++ b/src/trash/editor_req_mgr.js @@ -1,80 +1,26 @@ +import { makeRequest } from "./req_mgr"; + export async function EditorPrefillRequest(host, tab, adminid, date) { - try { - const req = await fetch(host + "?editor_data", { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: new URLSearchParams({ - adminId: adminid, - tab: tab, - day: date, - }).toString(), - }); - - if ( - req.ok && - req.headers.get("Content-Type")?.includes("application/json") - ) { - return [1, await req.json()]; - } else { - return [req.status, await req.text()]; - } - } catch (err) { - return [-1, err]; - } + return makeRequest(host + "?editor_data", { + adminId: adminid, + tab, + day: date, + }); } -export async function EditorPublishRequest(host, tab, adminid, date, newData) { - try { - const req = await fetch(host + "?editor_publish", { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: new URLSearchParams({ - adminId: adminid, - tab: tab, - day: date, - newData: newData, - }).toString(), - }); - if ( - req.ok && - req.headers.get("Content-Type")?.includes("application/json") - ) { - return [1, await req.json()]; - } else { - return [req.status, await req.text()]; - } - } catch (err) { - return [-1, err]; - } +export async function EditorPublishRequest(host, tab, adminid, date, newData) { + return makeRequest(host + "?editor_publish", { + adminId: adminid, + tab, + day: date, + newData, + }); } export async function EditorRemoveRequest(host, tab, adminid, date) { - try { - const req = await fetch(host + "?editor_fullitems_del", { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: new URLSearchParams({ - adminId: adminid, - tab: tab, - day: date, - }).toString(), - }); - - if ( - req.ok && - req.headers.get("Content-Type")?.includes("application/json") - ) { - return [1, await req.json()]; - } else { - return [req.status, await req.text()]; - } - } catch (err) { - return [-1, err]; - } + return makeRequest(host + "?editor_fullitems_del", { + adminId: adminid, + tab, + day: date, + }); } diff --git a/src/trash/helpers.js b/src/trash/helpers.js index aaca77c..cc3081d 100644 --- a/src/trash/helpers.js +++ b/src/trash/helpers.js @@ -23,10 +23,7 @@ export function b64d(str) { ); } - /** by claude - * Universal copy-to-clipboard function - * Works with modern Clipboard API and falls back to legacy methods * @param {string} text - The text to copy to clipboard * @returns {Promise} - Returns true if successful, false otherwise */ @@ -37,34 +34,44 @@ export async function copyToClipboard(text) { await navigator.clipboard.writeText(text); return true; } catch (err) { - console.warn('Clipboard API failed, trying fallback:', err); + console.warn("Clipboard API failed, trying fallback:", err); } } - // Method 2: Fallback for older browsers or non-secure contexts try { - const textarea = document.createElement('textarea'); + const textarea = document.createElement("textarea"); textarea.value = text; - - // Make it invisible and non-intrusive - textarea.style.position = 'fixed'; - textarea.style.left = '-999999px'; - textarea.style.top = '-999999px'; - textarea.style.opacity = '0'; - textarea.setAttribute('readonly', ''); - + textarea.style.position = "fixed"; + textarea.style.left = "-999999px"; + textarea.style.top = "-999999px"; + textarea.style.opacity = "0"; + textarea.setAttribute("readonly", ""); document.body.appendChild(textarea); - - // Select and copy textarea.select(); textarea.setSelectionRange(0, text.length); - - const success = document.execCommand('copy'); + const success = document.execCommand("copy"); document.body.removeChild(textarea); - return success; } catch (err) { - console.error('All copy methods failed:', err); + console.error("All copy methods failed:", err); return false; } -} \ No newline at end of file +} + +// dx4 specific +export async function copyDXLink(classId, adminId = undefined) { + return copyToClipboard( + location.href.substring(0, location.href.search("#")) + + "#//" + + classId + + (adminId ? ";" + b64e(adminId) : "") + ); +} + +export const randomString = (length, symbols) => { + const len = symbols.length; + return Array.from( + { length }, + () => symbols[Math.floor(Math.random() * len)] + ).join(""); +}; diff --git a/src/trash/md5.js b/src/trash/md5.js deleted file mode 100644 index 6ab493e..0000000 --- a/src/trash/md5.js +++ /dev/null @@ -1,167 +0,0 @@ -/** - * MD5 hash implementation - * Based on: http://www.myersdaily.org/joseph/javascript/md5-text.html - */ - -const HEX_CHARS = '0123456789abcdef'; - -// Pre-compute add32 function based on environment -const add32 = (() => { - const test = (a, b) => (a + b) & 0xFFFFFFFF; - // Test if simple addition works correctly - if (test(0x80000000, 0x80000000) === 0) { - return test; - } - // Fallback for environments with non-standard 32-bit arithmetic - return (x, y) => { - const lsw = (x & 0xFFFF) + (y & 0xFFFF); - const msw = (x >> 16) + (y >> 16) + (lsw >> 16); - return (msw << 16) | (lsw & 0xFFFF); - }; -})(); - -const cmn = (q, a, b, x, s, t) => { - a = add32(add32(a, q), add32(x, t)); - return add32((a << s) | (a >>> (32 - s)), b); -}; - -const ff = (a, b, c, d, x, s, t) => cmn((b & c) | ((~b) & d), a, b, x, s, t); -const gg = (a, b, c, d, x, s, t) => cmn((b & d) | (c & (~d)), a, b, x, s, t); -const hh = (a, b, c, d, x, s, t) => cmn(b ^ c ^ d, a, b, x, s, t); -const ii = (a, b, c, d, x, s, t) => cmn(c ^ (b | (~d)), a, b, x, s, t); - -const md5blk = (s) => { - const blks = []; - for (let i = 0; i < 64; i += 4) { - blks[i >> 2] = s.charCodeAt(i) + - (s.charCodeAt(i + 1) << 8) + - (s.charCodeAt(i + 2) << 16) + - (s.charCodeAt(i + 3) << 24); - } - return blks; -}; - -const md5cycle = (x, k) => { - let a = x[0], b = x[1], c = x[2], d = x[3]; - - a = ff(a, b, c, d, k[0], 7, -680876936); - d = ff(d, a, b, c, k[1], 12, -389564586); - c = ff(c, d, a, b, k[2], 17, 606105819); - b = ff(b, c, d, a, k[3], 22, -1044525330); - a = ff(a, b, c, d, k[4], 7, -176418897); - d = ff(d, a, b, c, k[5], 12, 1200080426); - c = ff(c, d, a, b, k[6], 17, -1473231341); - b = ff(b, c, d, a, k[7], 22, -45705983); - a = ff(a, b, c, d, k[8], 7, 1770035416); - d = ff(d, a, b, c, k[9], 12, -1958414417); - c = ff(c, d, a, b, k[10], 17, -42063); - b = ff(b, c, d, a, k[11], 22, -1990404162); - a = ff(a, b, c, d, k[12], 7, 1804603682); - d = ff(d, a, b, c, k[13], 12, -40341101); - c = ff(c, d, a, b, k[14], 17, -1502002290); - b = ff(b, c, d, a, k[15], 22, 1236535329); - - a = gg(a, b, c, d, k[1], 5, -165796510); - d = gg(d, a, b, c, k[6], 9, -1069501632); - c = gg(c, d, a, b, k[11], 14, 643717713); - b = gg(b, c, d, a, k[0], 20, -373897302); - a = gg(a, b, c, d, k[5], 5, -701558691); - d = gg(d, a, b, c, k[10], 9, 38016083); - c = gg(c, d, a, b, k[15], 14, -660478335); - b = gg(b, c, d, a, k[4], 20, -405537848); - a = gg(a, b, c, d, k[9], 5, 568446438); - d = gg(d, a, b, c, k[14], 9, -1019803690); - c = gg(c, d, a, b, k[3], 14, -187363961); - b = gg(b, c, d, a, k[8], 20, 1163531501); - a = gg(a, b, c, d, k[13], 5, -1444681467); - d = gg(d, a, b, c, k[2], 9, -51403784); - c = gg(c, d, a, b, k[7], 14, 1735328473); - b = gg(b, c, d, a, k[12], 20, -1926607734); - - a = hh(a, b, c, d, k[5], 4, -378558); - d = hh(d, a, b, c, k[8], 11, -2022574463); - c = hh(c, d, a, b, k[11], 16, 1839030562); - b = hh(b, c, d, a, k[14], 23, -35309556); - a = hh(a, b, c, d, k[1], 4, -1530992060); - d = hh(d, a, b, c, k[4], 11, 1272893353); - c = hh(c, d, a, b, k[7], 16, -155497632); - b = hh(b, c, d, a, k[10], 23, -1094730640); - a = hh(a, b, c, d, k[13], 4, 681279174); - d = hh(d, a, b, c, k[0], 11, -358537222); - c = hh(c, d, a, b, k[3], 16, -722521979); - b = hh(b, c, d, a, k[6], 23, 76029189); - a = hh(a, b, c, d, k[9], 4, -640364487); - d = hh(d, a, b, c, k[12], 11, -421815835); - c = hh(c, d, a, b, k[15], 16, 530742520); - b = hh(b, c, d, a, k[2], 23, -995338651); - - a = ii(a, b, c, d, k[0], 6, -198630844); - d = ii(d, a, b, c, k[7], 10, 1126891415); - c = ii(c, d, a, b, k[14], 15, -1416354905); - b = ii(b, c, d, a, k[5], 21, -57434055); - a = ii(a, b, c, d, k[12], 6, 1700485571); - d = ii(d, a, b, c, k[3], 10, -1894986606); - c = ii(c, d, a, b, k[10], 15, -1051523); - b = ii(b, c, d, a, k[1], 21, -2054922799); - a = ii(a, b, c, d, k[8], 6, 1873313359); - d = ii(d, a, b, c, k[15], 10, -30611744); - c = ii(c, d, a, b, k[6], 15, -1560198380); - b = ii(b, c, d, a, k[13], 21, 1309151649); - a = ii(a, b, c, d, k[4], 6, -145523070); - d = ii(d, a, b, c, k[11], 10, -1120210379); - c = ii(c, d, a, b, k[2], 15, 718787259); - b = ii(b, c, d, a, k[9], 21, -343485551); - - x[0] = add32(a, x[0]); - x[1] = add32(b, x[1]); - x[2] = add32(c, x[2]); - x[3] = add32(d, x[3]); -}; - -const rhex = (n) => { - let s = ''; - for (let j = 0; j < 4; j++) { - s += HEX_CHARS[(n >> (j * 8 + 4)) & 0x0F] + HEX_CHARS[(n >> (j * 8)) & 0x0F]; - } - return s; -}; - -const hex = (x) => x.map(rhex).join(''); - -const md51 = (s) => { - const n = s.length; - const state = [1732584193, -271733879, -1732584194, 271733878]; - - let i; - for (i = 64; i <= n; i += 64) { - md5cycle(state, md5blk(s.substring(i - 64, i))); - } - - s = s.substring(i - 64); - const tail = new Array(16).fill(0); - - for (i = 0; i < s.length; i++) { - tail[i >> 2] |= s.charCodeAt(i) << ((i % 4) << 3); - } - - tail[i >> 2] |= 0x80 << ((i % 4) << 3); - - if (i > 55) { - md5cycle(state, tail); - tail.fill(0); - } - - tail[14] = n * 8; - md5cycle(state, tail); - - return state; -}; - -/** - * Calculate MD5 hash of a string - * @param {string} s - Input string - * @returns {string} MD5 hash in hexadecimal format - */ -export function md5(s) { - return hex(md51(s)); -} \ No newline at end of file diff --git a/src/trash/req_mgr.js b/src/trash/req_mgr.js index 7f58b6f..376dc5e 100644 --- a/src/trash/req_mgr.js +++ b/src/trash/req_mgr.js @@ -1,127 +1,53 @@ import { apiFormatDate } from "./dengi"; +export async function makeRequest(url, params) { + try { + const req = await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams(params).toString(), + }); + + return req.ok && + req.headers.get("Content-Type")?.includes("application/json") + ? [1, await req.json()] + : [req.status, await req.text()]; + } catch (err) { + return [-1, err]; + } +} + export async function LoginReq(host, username) { - try { - const req = await fetch(host + "?login", { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", // 💀 важный заголовок - }, - body: new URLSearchParams({ - classId: username, - }).toString(), - }); - - if ( - req.ok && - req.headers.get("Content-Type")?.includes("application/json") - ) { - return [1, await req.json()]; - } else { - return [req.status, await req.text()]; - } - } catch (err) { - return [-1, err]; - } + return makeRequest(host + "?login", { classId: username }); } + export async function RaspGet(host, classId, day) { - try { - const req = await fetch(host + "?req_client_schedule", { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", // 💀 важный заголовок - }, - body: new URLSearchParams({ - classId: classId, - day: apiFormatDate(day), - }).toString(), - }); - - if ( - req.ok && - req.headers.get("Content-Type")?.includes("application/json") - ) { - return [1, await req.json()]; - } else { - return [req.status, await req.text()]; - } - } catch (err) { - return [-1, err]; - } -} -export async function AdminReq(host, adminid) { - try { - const req = await fetch(host + "?admin_login", { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", // 💀 dengi - }, - body: new URLSearchParams({ - adminId: adminid, - }).toString(), - }); - - if ( - req.ok && - req.headers.get("Content-Type")?.includes("application/json") - ) { - return [1, await req.json()]; - } else { - return [req.status, await req.text()]; - } - } catch (err) { - return [-1, err]; - } + return makeRequest(host + "?req_client_schedule", { + classId, + day: apiFormatDate(day), + }); } -export async function AdminUserFRequest(host, adminid) { - try { - const req = await fetch(host + "?get_admin_user_f", { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: new URLSearchParams({ - adminId: adminid, - }).toString(), - }); - - if ( - req.ok && - req.headers.get("Content-Type")?.includes("application/json") - ) { - return [1, await req.json()]; - } else { - return [req.status, await req.text()]; - } - } catch (err) { - return [-1, err]; - } +export async function AdminReq(host, adminId) { + return makeRequest(host + "?admin_login", { adminId }); } -export async function classUserFRequest(host, classid) { - try { - const req = await fetch(host + "?get_class_user_f", { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: new URLSearchParams({ - classId: classid, - }).toString(), - }); - - if ( - req.ok && - req.headers.get("Content-Type")?.includes("application/json") - ) { - return [1, await req.json()]; - } else { - return [req.status, await req.text()]; - } - } catch (err) { - return [-1, err]; - } +export async function AdminUserFRequest(host, adminId) { + return makeRequest(host + "?get_admin_user_f", { adminId }); } +export async function classUserFRequest(host, classId) { + return makeRequest(host + "?get_class_user_f", { classId }); +} +/**@param {{action:string,person?:string,value?}} data */ +export async function AdminUpdateRequest(host, adminId, data) { + return makeRequest(host + "?update_admin", { adminId, ...data }); +} +/**@param {{action:string,value?}} data */ +export async function ClassUpdateRequest(host, adminId, data) { + return makeRequest(host + "?classprofile_edit", { adminId, ...data }); +} +export async function getClassProfile(host, adminId) { + return makeRequest(host + "?classprofile_view", { adminId }); +} diff --git a/src/trash/trash-components.jsx b/src/trash/trash-components.jsx index 5741118..de6d326 100644 --- a/src/trash/trash-components.jsx +++ b/src/trash/trash-components.jsx @@ -1,8 +1,15 @@ -import React, { useEffect, useState } from "react"; -import { BiCheck, BiLeftArrowAlt, BiRightArrowAlt, BiX } from "react-icons/bi"; +import { useEffect, useState } from "react"; +import { + BiCheck, + BiHide, + BiLeftArrowAlt, + BiRightArrowAlt, + BiShow, + BiX, +} from "react-icons/bi"; import { formatDate, getDateTags } from "./dengi.js"; import { AnimatePresence, motion, LayoutGroup } from "motion/react"; - +import trashcss from "./trash.module.css"; export function Switch({ checked, defaultChecked = false, @@ -67,7 +74,7 @@ export function Switch({ ); } -export function LoadingSpinner(props) { +export function LoadingSpinner({ size }) { return (
- +
+ { className={ "block min-w-2 min-h-2 rounded-full mx-1 transition-colors" + (props.indicator - ? " bg-zinc-300 animate-pulse" + ? props.pink + ? " bg-pink-500 animate-pulse" + : " bg-zinc-300 animate-pulse" : " bg-transparent") } layoutId="datepicker_indicator" @@ -151,7 +176,11 @@ export const DatePicker = React.memo((props) => { layoutId="datepicker_date_div" layout="position" > - + { )} - +
); -}); +}; export function DengiButton({ onClick, @@ -235,23 +269,24 @@ export function DengiButton({ // ); // } +const defaultDialogContent = { + message: "?", + yesBtn: "da", + noBtn: "net", + bgDissmis: true, + yesBlocked: false, + yesIcon: , + noIcon: , + noBtnStyle: "", + yesBtnStyle: "", +}; export function DengiYesOrNoDialog({ - content = { - message: "?", - yesBtn: "da", - noBtn: "net", - bgDissmis: true, - yesIcon: , - noIcon: , - noBtnStyle: "", - yesBtnStyle: "", - }, + content = defaultDialogContent, show, onAnswer, }) { return ( <> - {/* Background */} {show && ( onAnswer(true)} + disabled={content.yesBlocked} > {content.yesIcon} {content.yesBtn} @@ -299,3 +335,119 @@ export function DengiYesOrNoDialog({ ); } + +export function DengiEditable({ + value, + password = false, + disWHidden = true, + disabled = false, + showLoading = true, + minLenght = 1, + // eslint-disable-next-line no-unused-vars + onSubmit = ({ value, setIdle }) => {}, + // eslint-disable-next-line no-unused-vars + onInput = ({ value, setIdle }) => {}, + pink = false, +}) { + const [val, setVal] = useState(value); + const [hid, setHid] = useState(true); + const [idle, setIdle] = useState(false); + useEffect(() => { + setVal(value); + }, [value]); + return ( + { + if (idle) return; + e.preventDefault(); + onSubmit({ value: val, setIdle }); + }} + > + {value !== false || !showLoading ? ( + { + setVal(e.target.value); + onInput({ value: e.target.value, setIdle }); + }} + /> + ) : ( + + + + )} + + {password && ( + + )} + + + {val !== value && ( + { + e.preventDefault(); + setVal(value); + }} + > + {idle ? : } + + )} + + + ); +} + +export function TextPreloader({ width = "5rem" }) { + return ( + + + + ); +} diff --git a/src/trash/trash.module.css b/src/trash/trash.module.css new file mode 100644 index 0000000..b213587 --- /dev/null +++ b/src/trash/trash.module.css @@ -0,0 +1,11 @@ +.anim_preloader { + animation: anim-preloader 1s infinite linear; +} +@keyframes anim-preloader { + from { + transform: translateX(-50px) rotate(20deg); + } + to { + transform: translateX(calc((var(--w, 50px)) + (50px))) rotate(20deg); + } +} diff --git a/vite.config.js b/vite.config.js index 36d3fc0..54f1d1c 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,5 +1,5 @@ import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react-swc"; +import react from "@vitejs/plugin-react"; import tailwindcss from "@tailwindcss/vite"; import { VitePWA } from "vite-plugin-pwa"; @@ -7,7 +7,11 @@ import { VitePWA } from "vite-plugin-pwa"; export default defineConfig({ base: "/dx4/", plugins: [ - react(), + react({ + babel: { + plugins: [["babel-plugin-react-compiler"]], + }, + }), tailwindcss(), VitePWA({ registerType: "autoUpdate",