From a7ea9227cf332c4e1b5c3ad663257cf823d46289 Mon Sep 17 00:00:00 2001
From: KyMAN <0kyman0@gmail.com>
Date: Wed, 31 Jan 2024 11:24:53 +0100
Subject: [PATCH] fix: Migrating project to the new Gitea Host.
---
.gitignore | 6 +
HTML/base.kstats.html | 183 ++++++++++
JSON/html.files.json | 1 +
MySQL/KStats.my.sql | 447 ++++++++++++++++++++++++
PHP/KStats.php | 302 ++++++++++++++++
PHP/include.php | 8 +
Public/.htaccess | 7 +
Public/api/index.php | 5 +
Public/ecma/KStats.ecma.js | 247 +++++++++++++
Public/git_update.php | 4 +
Public/images/min.png | Bin 0 -> 546 bytes
Public/tests/cookies.html | 33 ++
Public/tests/sessions.php | 23 ++
Public/tests/test.html | 29 ++
Public/wmd.php | 6 +
Public/wmd_scripts.php | 21 ++
WMD/dev/ECMAScript/index.w.md | 24 ++
WMD/dev/PHP/index.w.md | 36 ++
WMD/dev/Public/api/index.php.w.md | 23 ++
WMD/dev/Public/ecma/KStats.ecma.js.w.md | 268 ++++++++++++++
WMD/dev/Public/git_update.php.w.md | 23 ++
WMD/dev/Public/tests/sessions.php.w.md | 23 ++
WMD/dev/Public/wmd.php.w.md | 23 ++
WMD/dev/Public/wmd_scripts.php.w.md | 23 ++
WMD/dev/index.w.md | 0
WMD/es/bugs.w.md | 52 +++
WMD/es/index.w.md | 37 ++
WMD/es/project.w.md | 24 ++
WMD/es/projects.w.md | 54 +++
WMD/es/targets.w.md | 45 +++
WMD/es/work.w.md | 195 +++++++++++
WMD/index.w.md | 40 +++
32 files changed, 2212 insertions(+)
create mode 100755 .gitignore
create mode 100755 HTML/base.kstats.html
create mode 100755 JSON/html.files.json
create mode 100755 MySQL/KStats.my.sql
create mode 100755 PHP/KStats.php
create mode 100755 PHP/include.php
create mode 100755 Public/.htaccess
create mode 100755 Public/api/index.php
create mode 100755 Public/ecma/KStats.ecma.js
create mode 100755 Public/git_update.php
create mode 100755 Public/images/min.png
create mode 100755 Public/tests/cookies.html
create mode 100755 Public/tests/sessions.php
create mode 100755 Public/tests/test.html
create mode 100755 Public/wmd.php
create mode 100755 Public/wmd_scripts.php
create mode 100755 WMD/dev/ECMAScript/index.w.md
create mode 100755 WMD/dev/PHP/index.w.md
create mode 100755 WMD/dev/Public/api/index.php.w.md
create mode 100755 WMD/dev/Public/ecma/KStats.ecma.js.w.md
create mode 100755 WMD/dev/Public/git_update.php.w.md
create mode 100755 WMD/dev/Public/tests/sessions.php.w.md
create mode 100755 WMD/dev/Public/wmd.php.w.md
create mode 100755 WMD/dev/Public/wmd_scripts.php.w.md
create mode 100755 WMD/dev/index.w.md
create mode 100755 WMD/es/bugs.w.md
create mode 100755 WMD/es/index.w.md
create mode 100755 WMD/es/project.w.md
create mode 100755 WMD/es/projects.w.md
create mode 100755 WMD/es/targets.w.md
create mode 100755 WMD/es/work.w.md
create mode 100755 WMD/index.w.md
diff --git a/.gitignore b/.gitignore
new file mode 100755
index 0000000..d4e01c9
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+PHP/KStats.Secrets.php
+KStats.apache2.conf
+Public/index.html
+Public/es
+Public/dev
+Data
\ No newline at end of file
diff --git a/HTML/base.kstats.html b/HTML/base.kstats.html
new file mode 100755
index 0000000..20834f0
--- /dev/null
+++ b/HTML/base.kstats.html
@@ -0,0 +1,183 @@
+
+
+
+ {title}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/JSON/html.files.json b/JSON/html.files.json
new file mode 100755
index 0000000..25dd2a9
--- /dev/null
+++ b/JSON/html.files.json
@@ -0,0 +1 @@
+{"files":["\/mnt\/d\/git\/KStats\/Public\/dev\/ECMAScript\/index.html","\/mnt\/d\/git\/KStats\/Public\/dev\/PHP\/index.html","\/mnt\/d\/git\/KStats\/Public\/dev\/Public\/api\/index.php.html","\/mnt\/d\/git\/KStats\/Public\/dev\/Public\/ecma\/KStats.ecma.js.html","\/mnt\/d\/git\/KStats\/Public\/dev\/Public\/git_update.php.html","\/mnt\/d\/git\/KStats\/Public\/dev\/Public\/tests\/sessions.php.html","\/mnt\/d\/git\/KStats\/Public\/dev\/Public\/wmd.php.html","\/mnt\/d\/git\/KStats\/Public\/dev\/Public\/wmd_scripts.php.html","\/mnt\/d\/git\/KStats\/Public\/dev\/index.html","\/mnt\/d\/git\/KStats\/Public\/es\/bugs.html","\/mnt\/d\/git\/KStats\/Public\/es\/index.html","\/mnt\/d\/git\/KStats\/Public\/es\/project.html","\/mnt\/d\/git\/KStats\/Public\/es\/projects.html","\/mnt\/d\/git\/KStats\/Public\/es\/targets.html","\/mnt\/d\/git\/KStats\/Public\/es\/work.html","\/mnt\/d\/git\/KStats\/Public\/index.html"],"directories":["\/dev","\/dev\/ECMAScript","\/dev\/PHP","\/dev\/Public","\/dev\/Public\/api","\/dev\/Public\/ecma","\/dev\/Public\/tests","\/es"]}
\ No newline at end of file
diff --git a/MySQL/KStats.my.sql b/MySQL/KStats.my.sql
new file mode 100755
index 0000000..cfd4596
--- /dev/null
+++ b/MySQL/KStats.my.sql
@@ -0,0 +1,447 @@
+create database if not exists KStats character set utf8mb4 collate utf8mb4_general_ci;
+use KStats;
+
+delimiter ;^
+
+ drop procedure if exists tables_remove;^
+ create procedure tables_remove() begin
+
+ -- Level 2.
+ drop table if exists SessionsUrls;
+
+ -- Level 1.
+ drop table if exists Sessions;
+ drop table if exists IpsData;
+
+ -- Level 0.
+ drop table if exists Urls;
+ drop table if exists Ips;
+ drop table if exists Tokens;
+ drop table if exists Settings;
+
+ end;^
+
+ drop procedure if exists tables_create;^
+ create procedure tables_create() begin
+
+ -- Level 0.
+ create table if not exists Ips(
+ id integer not null auto_increment,
+ address varchar(39) not null,
+ date_in datetime not null default now(),
+ date_out datetime,
+ constraint ips_id primary key(id)
+ );
+
+ create table if not exists Urls(
+ id integer not null auto_increment,
+ url varchar(2048) not null,
+ date_in datetime not null default now(),
+ date_out datetime,
+ constraint urls_id primary key(id)
+ );
+
+ create table if not exists Tokens(
+ id integer not null auto_increment,
+ token varchar(256) not null,
+ enabled boolean not null default true,
+ pattern varchar(2048),
+ public boolean not null,
+ remarks varchar(2048),
+ date_in datetime not null default now(),
+ date_out datetime,
+ constraint tokens_id primary key(id)
+ );
+
+ create table if not exists Settings(
+ id integer not null auto_increment,
+ name varchar(32) not null,
+ value varchar(256),
+ date_in datetime not null default now(),
+ date_out datetime,
+ constraint settings_id primary key(id)
+ );
+
+ create table if not exists Countries(
+ id integer not null auto_increment,
+ name varchar(128),
+ code varchar(16),
+ date_in datetime not null default now(),
+ date_out datetime,
+ constraint countries_id primary key(id)
+ );
+
+ create table if not exists Isps(
+ id integer not null auto_increment,
+ name varchar(256),
+ date_in datetime not null default now(),
+ date_out datetime,
+ constraint countries_id primary key(id)
+ );
+
+ -- Level 1.
+ create table if not exists Sessions(
+ id integer not null auto_increment,
+ ip integer not null,
+ date_in datetime not null default now(),
+ date_last datetime not null default now(),
+ date_out datetime,
+ constraint sessions_id primary key(id),
+ constraint sessions_ip foreign key(ip) references Ips(id)
+ );
+
+ create table if not exists IpsData(
+ id integer not null auto_increment,
+ ip integer not null,
+ country integer not null,
+ isp integer,
+ latitude decimal(9, 6),
+ longitude decimal(9, 6),
+ `data` text not null,
+ date_in datetime not null default now(),
+ date_out datetime,
+ constraint ips_data_id primary key(id),
+ constraint ips_data_ip foreign key(ip) references Ips(id),
+ constraint ips_data_country foreign key(country) references Countries(id),
+ constraint ips_data_isp foreign key(isp) references Isps(id)
+ );
+
+ -- Level 2.
+ create table if not exists SessionsUrls(
+ id integer not null auto_increment,
+ `session` integer not null,
+ token integer not null,
+ url integer not null,
+ date_in datetime not null default now(),
+ date_last datetime not null default now(),
+ date_out datetime,
+ constraint sessions_urls_id primary key(id),
+ constraint sessions_urls_session foreign key(`session`) references Sessions(id),
+ constraint sessions_urls_token foreign key(token) references Tokens(id),
+ constraint sessions_urls_url foreign key(url) references Urls(id)
+ );
+
+ end;^
+
+ drop procedure if exists tables_update;^
+ create procedure tables_update() begin
+
+ if (select 1 from information_schema.columns where table_schema = 'KStats' && table_name = 'Tokens' && column_name = 'enabled' limit 1) is null then
+ alter table Tokens add column enabled boolean not null default true;
+ end if;
+ if (select 1 from information_schema.columns where table_schema = 'KStats' && table_name = 'Tokens' && column_name = 'pattern' limit 1) is null then
+ alter table Tokens add column pattern varchar(2048);
+ end if;
+ if (select 1 from information_schema.columns where table_schema = 'KStats' && table_name = 'Tokens' && column_name = 'public' limit 1) is null then
+ alter table Tokens add column public boolean not null;
+ end if;
+ if (select 1 from information_schema.columns where table_schema = 'KStats' && table_name = 'Tokens' && column_name = 'remarks' limit 1) is null then
+ alter table Tokens add column remarks varchar(2048);
+ end if;
+ if (select 1 from information_schema.columns where table_schema = 'KStats' && table_name = 'SessionsUrls' && column_name = 'date_last' limit 1) is null then
+ alter table SessionsUrls add column date_last datetime not null default now();
+ end if;
+
+ end;^
+
+ drop procedure if exists tables_fill;^
+ create procedure tables_fill() begin
+
+ drop temporary table if exists KStatsTTSettings;
+ create temporary table KStatsTTSettings(
+ id integer not null auto_increment,
+ name varchar(32) not null,
+ value varchar(256),
+ date_in datetime not null default now(),
+ primary key(id)
+ );
+
+ insert into KStatsTTSettings(name, value) values
+ ('token_alphabet', '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'),
+ ('token_length', 57),
+ ('default_value', null),
+ ('session_timeout', 900);
+
+ insert into Settings(name, value)
+ select name, value from KStatsTTSettings where name not in (
+ select name from Settings where date_out is null
+ );
+
+ drop temporary table KStatsTTSettings;
+
+ end;^
+
+ -- call tables_remove();^
+ call tables_create();^
+ call tables_update();^
+ call tables_fill();^
+
+ drop function if exists settings_get;^
+ create function settings_get(
+ $name varchar(32)
+ ) returns varchar(256) begin
+ return ifnull(
+ (select value from Settings where date_out is null && name = $name limit 1),
+ (select value from Settings where date_out is null && name in ('default_value', 'value') limit 1)
+ );
+ end;^
+
+ drop function if exists settings_get_integer;^
+ create function settings_get_integer(
+ $name varchar(32)
+ ) returns integer begin
+ return convert(settings_get($name), integer);
+ end;^
+
+ drop procedure if exists token_create;^
+ create procedure token_create(
+ in $public boolean,
+ in $pattern varchar(2048),
+ in $remarks varchar(2048),
+ out $error bigint,
+ out $id integer,
+ out $token varchar(256)
+ ) begin
+
+ set $id := 0;
+ set $error := (
+ (case
+ when $public is null then 1 << 1
+ else 0 end) |
+ /*(case
+ when $pattern is null then 1 << 2
+ when $pattern = '' then 1 << 3
+ else 0 end) |
+ (case
+ when $remarks is null then 1 << 4
+ when $remarks = '' then 1 << 5
+ else 0 end) |*/
+ 0
+ );
+
+ if !$error then begin
+
+ declare $alphabet varchar(256) default settings_get('token_alphabet');
+ declare `$length` integer default settings_get_integer('token_length');
+ declare $l integer default length($alphabet);
+
+ while $token is null || (select 1 from Tokens where date_out is null && token = $token limit 1) is not null do
+ set $token := '';
+ while length($token) < `$length` do
+ set $token := concat($token, substring($alphabet, floor(rand() * $l) + 1, 1));
+ end while;
+ end while;
+
+ insert into Tokens(token, public, pattern, remarks) values($token, $public, $pattern, $remarks);
+ set $id := last_insert_id();
+
+ end;end if;
+
+ end;^
+
+ drop procedure if exists ips_get;^
+ create procedure ips_get(
+ in $ip varchar(39),
+ out $error bigint,
+ out $id integer
+ ) begin
+
+ set $error := (case
+ when $ip is null then 1 << 1
+ when $ip = '' then 1 << 2
+ -- when $ip not regexp '^[0-9]{1,3}(\.[0-9]{1,3}){0,3}$' then 1 << 3
+ else 0 end);
+
+ if !$error then begin
+
+ set $id := (select id from Ips where date_out is null && address = $ip limit 1);
+
+ if $id is null then begin
+ insert into Ips(address) values($ip);
+ set $id := last_insert_id();
+ end;end if;
+
+ end;end if;
+
+ end;^
+
+ drop procedure if exists urls_get;^
+ create procedure urls_get(
+ in $url varchar(2048),
+ out $error bigint,
+ out $id integer
+ ) begin
+
+ set $error := (case
+ when $url is null then 1 << 1
+ when $url = '' then 1 << 2
+ else 0 end);
+
+ if !$error then begin
+
+ set $id := (select id from Urls where date_out is null && url = $url limit 1);
+
+ if $id is null then begin
+ insert into Urls(url) values($url);
+ set $id := last_insert_id();
+ end;end if;
+
+ end;end if;
+
+ end;^
+
+ drop function if exists token_get;^
+ create function token_get(
+ $token varchar(256)
+ ) returns boolean begin
+ return (select id from Tokens where date_out is null && token = $token);
+ end;^
+
+ /*drop function if exists token_validate;^
+ create function token_validate(
+ $token varchar(256),
+ $url varchar(2048)
+ ) returns bigint begin
+ return (case
+ when $token is null then 1 << 0
+ when $token = '' then 1 << 1'Token for YoutubeSoundsGame project.'
+ when $token regexp '^[0-9]+$' && convert(integer, $token) < 1 then 1 << 2
+ else ifnull((
+ select (
+ if(enabled, 0, 1 << 4) |
+ if(pattern regexp (select url from Urls where date_out is null && (concat('', id) = $url || url = $url) limit 1), 0, 1 << 5) |
+ 0
+ ) from Tokens where date_out is null && (concat('', id) = $token || token = $token) limit 1
+ ), 1 << 3) end);
+ end;^*/
+
+ drop function if exists token_validate;^
+ create function token_validate(
+ $token integer,
+ $url varchar(2048)
+ ) returns bigint begin
+ return (case
+ when $token is null then 1 << 0
+ when $token < 1 then 1 << 2
+ else ifnull((
+ select (
+ if(enabled, 0, 1 << 4) |
+ if(pattern is null || $url regexp pattern, 0, 1 << 5) |
+ if(public, 0, 1 << 6) |
+ 0
+ ) from Tokens where date_out is null && id = $token limit 1
+ ), 1 << 3) end);
+ end;^
+
+ drop function if exists session_url_validate;^
+ create function session_url_validate(
+ `$session` integer,
+ $id integer
+ ) returns bigint begin
+ return if(`$session` is null, 1 << 0, (case
+ when $id is null then 1 << 1
+ when $id < 1 then 1 << 2
+ else ifnull((
+ select (
+ if(date_out is null, 0, 1 << 4) |
+ if(`session` = `$session`, 0, 1 << 5) |
+ 0
+ ) from SessionsUrls where id = $id limit 1
+ ), 1 << 3) end));
+ end;^
+
+ drop procedure if exists register;^
+ create procedure register(
+ in `$session` integer,
+ in `$from` integer,
+ in $token varchar(256),
+ in $ip varchar(39),
+ in $url varchar(2048),
+ out $error bigint,
+ out $current_session integer,
+ out $id integer
+ ) begin
+
+ declare $ip_id integer;
+ declare $ip_error bigint;
+ declare $url_id integer;
+ declare $url_error bigint;
+ declare $token_id integer default token_get($token);
+
+ call ips_get($ip, $ip_error, $ip_id);
+ call urls_get($url, $url_error, $url_id);
+
+ set $error := (
+ ($ip_error << 1) |
+ ($url_error << 5) |
+ (token_validate($token_id, $url) << 8) |
+ 0
+ );
+
+ if !$error then begin
+
+ if `$session` is null || (select 1 from Sessions where id = `$session` && date_out is null && ip = $ip_id && timestampdiff(second, now(), date_last) > settings_get_integer('session_timeout')) then begin
+ insert into Sessions(ip) values($ip_id);
+ set `$session` := last_insert_id();
+ end;else
+ update Sessions set date_last := now() where id = `$session`;
+ end if;
+ set $current_session := `$session`;
+
+ if `$from` is not null then begin
+ set $error := (
+ (session_url_validate(`$session`, `$from`) << 15) |
+ 0
+ );
+ if !$error then begin
+ set $id := `$from`;
+ update SessionsUrls set date_last := now() where id = $id;
+ end;end if;
+ end;else
+ insert into SessionsUrls(`session`, token, url) values(`$session`, $token_id, $url_id);
+ set $id := last_insert_id();
+ end if;
+
+ end;end if;
+
+ end;^
+
+ drop view if exists SessionsView;^
+ create view SessionsView as select
+ sessions.id as id,
+ sessions.ip as ip_id,
+ ips.address as ip,
+ sessions.date_in as date_in,
+ sessions.date_last as date_last,
+ sessions.date_out as date_out,
+ timestampdiff(second, sessions.date_in, ifnull(sessions.date_out, sessions.date_last)) as `time`,
+ ips.date_in as ip_date_in
+ from Sessions sessions
+ join Ips ips on sessions.ip = ips.id
+ where ips.date_out is null;^
+
+ drop view if exists SessionsUrlsView;^
+ create view SessionsUrlsView as select
+ sessions_urls.id as id,
+ sessions.id as session_id,
+ sessions.ip_id as ip_id,
+ sessions.ip as ip,
+ urls.id as url_id,
+ urls.url as url,
+ sessions_urls.date_in as date_in,
+ sessions_urls.date_last as date_last,
+ timestampdiff(second, sessions_urls.date_in, sessions_urls.date_last) as `time`,
+ sessions.date_in as session_date_in,
+ sessions.date_last as session_date_last,
+ sessions.date_out as session_date_out,
+ sessions.ip_date_in as ip_date_in,
+ sessions.`time` as session_time,
+ urls.date_in as url_date_in
+ from SessionsUrls sessions_urls
+ join SessionsView sessions on sessions_urls.`session` = sessions.id
+ join Urls urls on sessions_urls.url = urls.id
+ where
+ sessions_urls.date_out is null &&
+ urls.date_out is null;^
+
+delimiter ;
\ No newline at end of file
diff --git a/PHP/KStats.php b/PHP/KStats.php
new file mode 100755
index 0000000..c38a7ae
--- /dev/null
+++ b/PHP/KStats.php
@@ -0,0 +1,302 @@
+ "mysql",
+ "database" => "KStats",
+ "host" => "localhost",
+ "port" => 3306,
+ "charset" => "utf8",
+ "user" => "root",
+ "password" => "",
+ "connection_string" => "{engine}:dbname={database};host={host};port={port};charset={charset}",
+ "allow_all" => false,
+ "data_key" => "kstats_data",
+ "ip_keys_ordered" => ["HTTP_CF_CONNECTING_IP", "HTTP_X_FORWARDED_FOR", "HTTP_X_REAL_IP", "HTTP_CLIENT_IP", "REMOTE_ADDR"]
+ ];
+ private $input = [];
+ private $connection = null;
+ private $token = null;
+ private $type = null;
+ private $method = null;
+ private $mode = null;
+ private $id = null;
+ private $query = null;
+ private $session = null;
+ private $request_data = null;
+
+ public static function is_dictionary($value){
+ return is_array($value) && array_values($value) != $value;
+ }
+
+ public static function is_secure(){
+ return (isset($_SERVER["HTTPS"]) && $_SERVER["HTTPS"] == "on") || $_SERVER["SERVER_PORT"] == 443;
+ }
+
+ private static function get_url(){
+ return self::$url ? self::$url : (self::$url = (
+ isset($_SERVER["HTTP_REFERER"]) ?
+ $_SERVER["HTTP_REFERER"] :
+ "http" . (self::is_secure() ? "s" : "") . "://" . $_SERVER["HTTP_HOST"] . $_SERVER["REQUEST_URI"]
+ ));
+ }
+
+ public function default_value($default = null, $nulls = null){
+ return $default !== null || (is_bool($nulls) ? $nulls : $this->settings("nulls", null, false, false)) ? $default : $this->settings(["default_value", "default"], null, null, true);
+ }
+
+ private function settings($names = null, $inputs = null, $default = null, $nulls = null){
+ if(!$names)
+ return $this->default_value($default, $nulls);
+
+ !is_bool($nulls) && ($nulls = $this->settings("nulls", null, false, false));
+ !is_array($names) && ($names = [$names]);
+
+ foreach(array_merge(is_array($inputs) ? (self::is_dictionary($inputs) ? [$input] : $input) : [], [$this->input, self::$default_settings]) as $input)
+ if(self::is_dictionary($input))
+ foreach($names as $name)
+ if($name && isset($input[$name]) && ($nulls || $input[$name] !== null))
+ return $input[$name];
+ return $this->default_value($default, $nulls);
+ }
+
+ public function get_ip(){
+ foreach($this->settings("ip_keys_ordered") as $key){
+ if(!empty($_SERVER[$key]))
+ return explode(",", $_SERVER[$key])[0];
+ if($ips = getenv($key))
+ return explode(",", $ips)[0];
+ };
+ return null;
+ }
+
+ private function get_token(){
+ return preg_replace('/^\/api\/([^\/]+)(\/.*)?$/', "$1", $_SERVER["REQUEST_URI"]);
+ }
+
+ public static function string_variables($string, $variables = null, $default = null){
+
+ if(!is_array($variables))
+ $variables = [];
+ elseif(self::is_dictionary($variables))
+ $variables = [$variables];
+
+ return preg_replace_callback('/\{([^\{\}]+)\}/', function($values) use($variables){
+ foreach($variables as $set)
+ if(isset($set[$values[1]]))
+ return $set[$values[1]];
+ return $values[0];
+ }, $string);
+ }
+
+ private function query($query, $variables){
+
+ $used = [];
+ $results = [
+ "tables" => [],
+ "variables" => []
+ ];
+
+ if(!is_array($variables))
+ $variables = [];
+ elseif(self::is_dictionary($variables))
+ $variables = [$variables];
+
+ preg_replace_callback('/\@([a-zA-Z0-9_]+)/', function($values) use(&$used){
+ !in_array($values[1], $used) && ($used[] = $values[1]);
+ }, is_array($variables) ? ($query = preg_replace_callback('/\{([^\{\}]+)\}/', function($values) use($variables){
+ foreach($variables as $set)
+ if(isset($set[$values[1]]))
+ return (
+ $set[$values[1]] === null ? "null" : (
+ is_bool($set[$values[1]]) ? ($set[$values[1]] ? "true" : "false") : (
+ is_string($set[$values[1]]) ? "'" . preg_replace('/([\\\\\'])/', "\\\\$1", $set[$values[1]]) . "'" : (
+ $set[$values[1]]
+ ))));
+ return "null";
+ }, $query)) : $query);
+
+ if(!empty($used)){
+ $subquery = "";
+ foreach($used as $key)
+ $subquery .= ($subquery ? "," : "") . " @" . $key . " as '" . $key . "'";
+ $query .= (preg_match('/;$/', $query) ? "" : ";") . "select" . $subquery . ";";
+ }
+
+ if(!$this->connection){
+ $this->connection = new \PDO(self::string_variables($this->settings(["connection_string", "string_connection"]), [
+ "engine" => $this->settings(["connection_engine", "engine"]),
+ "database" => $this->settings(["connection_database", "database"]),
+ "host" => $this->settings(["connection_host", "host"]),
+ "port" => $this->settings(["connection_port", "port"]),
+ "charset" => $this->settings(["connection_charset", "charset"])
+ ]), $this->settings(["connection_user", "user"]), $this->settings(["connection_password", "password"]));
+ $this->connection->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
+ $this->connection->beginTransaction();
+ };
+
+ $this->query = $query;
+
+ ($statement = $this->connection->prepare($query))->execute();
+
+ do{
+ try{
+ $table = [];
+ foreach($statement->rowCount() == 0 ? [] : $statement->fetchAll(\PDO::FETCH_ASSOC) as $new_row){
+ $row = [];
+ foreach($new_row as $key => $value){
+ if($value && is_string($value) && in_array($value[0], ["[", "{"])){
+ try{
+ $row[$key] = json_decode(utf8_encode($value), true);
+ }catch(\Exception $exception){}
+ !$row[$key] && ($row[$key] = utf8_encode($value));
+ }else
+ $row[$key] = $value;
+ };
+ $table[] = $row;
+ };
+ $results["tables"][] = $table;
+ }catch(\Exception $exception){};
+ }while($statement->nextRowset());
+
+ if(preg_match('/(,\s+?|\()\@/', $query)){
+ $l = count($results["tables"]) - 1;
+ foreach($results["tables"][$l][0] as $key => $value)
+ $results["variables"][$key] = $value;
+ unset($results["tables"][$l]);
+ };
+
+ $this->connection->commit();
+
+ return $results;
+ }
+
+ private function save(){
+
+ $results = null;
+
+ if(!in_array($this->type, ["js", "ecma"]) || $this->settings("allow_all")){
+
+ // isset($_SERVER["HTTP_REFERER"]) &&
+ $results = $this->query("call register({session}, {from}, {token}, {ip}, {url}, @error, @current_session, @id);", [
+ "session" => $this->session ? $this->session : null,
+ "from" => $this->id ? $this->id : null,
+ "token" => $this->get_token(),
+ "ip" => $this->get_ip(),
+ "url" => $this->request_data && isset($this->request_data["url"]) ? $this->request_data["url"] : self::get_url()
+ ]);
+
+ $results &&
+ ($_SESSION["kstats_id"] = $results["variables"]["current_session"]);
+
+ };
+
+ return $results;
+ }
+
+ private function print($data){
+
+ switch($this->type){
+ case "js":
+ header("content-type: text/javascript");
+ echo file_get_contents(__DIR__ . "/../Public/js/KStats.js");
+ break;
+ case "ecma":
+ header("content-type: text/javascript");
+ echo file_get_contents(__DIR__ . "/../Public/ecma/KStats.ecma.js");
+ break;
+ case "image":
+ case "img":
+ header("content-type: image/png");
+ echo file_get_contents(__DIR__ . "/../Public/images/min.png");
+ break;
+ case "css":
+ header("content-type: text/css");
+ echo "kstats-tag-link::after{content : '" . $data . "';}";
+ break;
+ case "json":
+ header("content-type: application/json");
+ echo json_encode([
+ "ok" => true,
+ "code" => 200,
+ "data" => $data
+ ]);
+ break;
+ case "test":
+ header("content-type: text/javascript");
+ echo "console.log(" . json_encode([
+ "session" => $_SESSION
+ ]) . ");console.log(document.cookie.split(';'));";
+ break;
+ default:
+ header("content-type: text/plain");
+ echo "";
+ break;
+ };
+
+ exit(0);
+
+ }
+
+ public function __construct($input = null){
+
+ is_array($input) && ($this->input = $input);
+
+ if(preg_match('/^\/api\/([^\/]+)\/([^\/]+)\/([^\/]+)\/([^\/\?]+)(\/([^\?]+))?/', $_SERVER["REQUEST_URI"], $matches)){
+
+ $data_key = $this->settings("data_key");
+
+ $this->token = $matches[1];
+ $this->session = intval($matches[2]);
+ $this->type = $matches[3];
+
+ foreach([$_POST, $_GET, $_COOKIE] as $set)
+ if(isset($set[$data_key])){
+ $this->request_data = json_decode(base64_decode(urldecode($set[$data_key])), true);
+ break;
+ };
+
+ // print_r(["request_data", $this->request_data, "get" => $_GET]);
+
+ switch($this->method = $matches[4]){
+ case "set":
+ isset($matches[6]) && $matches[6] && ($this->id = intval($matches[6]));
+ $response = $this->save();
+ $this->print(array_merge(is_array($response) ? $response : [], [
+ "url" => self::get_url(),
+ "query" => $this->query
+ ]));
+ };
+
+ };
+
+ $this->print(false);
+
+ }
+
+ public function close(){
+
+ if($this->connection){
+ try{
+ $this->connection->commit();
+ }catch(\Exception $exception){
+ try{
+ $this->connection->rollback();
+ }catch(\Exception $subexception){};
+ };
+ };
+ $this->connection = null;
+
+ }
+
+ public function __destruct(){
+
+ $this->close();
+
+ }
+
+ };
diff --git a/PHP/include.php b/PHP/include.php
new file mode 100755
index 0000000..2402bb6
--- /dev/null
+++ b/PHP/include.php
@@ -0,0 +1,8 @@
+ _default !== undefined && (_default !== null || (typeof nulls == "boolean" ? nulls : settings("nulls", null, false, false))) ? _default : settings(["default_value", "default"], null, null, true);
+
+ const settings = this.settings = (names, inputs, _default, nulls) => {
+ if(!names)
+ return default_value(_default, nulls);
+
+ const l = (names.push ? names : names = [names]).length,
+ m = (inputs = (inputs ? inputs.push ? inputs : [inputs] : []).concat([input, custom, default_settings])).length;
+
+ typeof nulls != "boolean" && (nulls = settings("nulls", null, false, false));
+
+ for(let j = 0; j < m; j ++)
+ if(typeof inputs[j] == "object")
+ for(let i = 0; i < l; i ++)
+ if(names[i] && inputs[j][names[i]] !== undefined && (nulls || inputs[j][names[i]] !== null))
+ return inputs[j][names[i]];
+ return default_value(_default, nulls);
+ };
+
+ const load = this.load = (url, callback) => {
+
+ let ended = false;
+ const ajax = new XMLHttpRequest(),
+ timeout = settings(["ajax_timeout", "load_timeout", "timeout"]),
+ date = Date.now(),
+ end = message => {
+ if(ended)
+ return;
+ ended = true;
+ typeof callback == "function" && callback(ajax.responseText, ajax.status, ajax.readyState, message == "OK", message);
+ };
+
+ ajax.open("get", url, true);
+ ajax.timeout = timeout;
+ ajax.onreadystatechange = () => {
+ if(ended)
+ return;
+ if(ajax.readyState == 4)
+ end((ajax.status >= 200 && ajax.status < 300) || [301, 302, 304].includes(ajax.status) ? "OK" : "HTTP_ERROR");
+ else if(Date.now() - date > timeout)
+ end("FORCED_TIMEOUT");
+ };
+ ajax.send(null);
+
+ ajax.onabort = () => end("ABORTED"),
+ ajax.onerror = () => end("ERROR");
+ ajax.ontimeout = () => end("TIMEOUT");
+
+ return ajax;
+ };
+
+ const settings_add_i = (items, overwrite, callback, i) => {
+
+ if(i >= items.length){
+ typeof callback == "function" && callback();
+ return;
+ };
+
+ if(items[i]){
+ if(typeof items[i] == "string"){
+
+ let json;
+
+ try{
+ json = JSON.parse(items[i]);
+ }catch(exception){};
+
+ if(json)
+ settings_add_i(json.push ? json : [json], overwrite, end, 0);
+ else
+ load(json, response => {
+ try{
+ json = JSON.parse(response);
+ }catch(exception){};
+ if(json)
+ settings_add_i(json.push ? json : [json], overwrite, end, 0);
+ else
+ end();
+ });
+
+ return;
+ };
+ if(typeof items[i] == "object"){
+ if(items[i].push){
+ settings_add_i(items[i], overwrite, end, 0);
+ return;
+ };
+ for(const key in items[i])
+ (overwrite || custom[key] === undefined) && (custom[key] = items[i][key]);
+ };
+ };
+
+ end();
+
+ };
+
+ const settings_add = this.settings_add = (items, overwrite, callback) => settings_add_i(items ? items.push ? items : [items] : [], typeof overwrite == "boolean" ? overwrite : settings(["settings_overwrite", "overwrite"]), callback, 0);
+
+ const threads_method = () => threads.forEach(thread => thread && thread());
+
+ const threads_start = this.threads_start = () => thread === null && (thread = setInterval(threads_method, 1000 / settings(["frames_per_second", "fps"])));
+
+ this.threads_stop = () => {
+ if(thread === null)
+ return;
+
+ clearInterval(thread);
+ thread = null;
+
+ };
+
+ const threads_add = this.threads_add = method => {
+ if(typeof method != "function")
+ return null;
+
+ let i = 0;
+ const l = threads.length;
+
+ for(; i < l; i ++)
+ if(!threads[i])
+ break;
+
+ threads[i] = method;
+
+ return i;
+ };
+
+ const threads_remove = this.threads_remove = i => !isNaN(i) && threads[i] && (threads[i] = null);
+
+ const string_variables = this.string_variables = (string, variables, _default) => {
+
+ if(!variables)
+ variables = [];
+ else if(!variables.push)
+ variables = [variables];
+
+ for(let i = variables.length - 1; i >= 0; i --)
+ typeof variables[i] != "object" && (variables = variables.splice(i, 1));
+
+ const l = variables.length;
+
+ return string.replace(/\{([^\{\}]+)\}/g, (...arguments) => {
+ for(let i = 0; i < l; i ++)
+ if(variables[i][arguments[1]] !== undefined)
+ return variables[i][arguments[1]];
+ return _default !== undefined ? _default : arguments[0];
+ });
+ };
+
+ const update = () => load(tmp = string_variables(url || (url = settings(["kstats_url", "url"])), {
+ session : session
+ }) + (id ? "/" + id : "") + "?" + (data_key || (data_key = settings("data_key"))) + "=" + btoa(JSON.stringify({
+ url : window.location.href.replace(/^([^#]+)(\#.*)?$/, "$1")
+ })), (...arguments) => {
+ if(!arguments[0])
+ return;
+
+ const data = JSON.parse(arguments[0]),
+ current_session = Number(data.data.variables.current_session);
+
+ session != current_session && (document.cookie = settings("session_cookie_name") + "=" + (session = current_session) + ";expires=" + new Date(Date.now() + settings("session_timeout")).toUTCString() + ";path=/;SameSite=Lax");
+
+ if(id === null){
+ id = data.data.variables.id;
+ addEventListener("beforeunload", update);
+ };
+
+ });
+
+ const register_event = () => {
+
+ const date = Date.now();
+
+ if(date - last_connection > milliseconds_per_connection){
+ last_connection = date;
+ update();
+ };
+
+ };
+
+ this.start = () => {
+
+ if(started)
+ return;
+ started = true;
+
+ settings_add(settings("settings_files"), true, () => {
+
+ const session_pattern = new RegExp("^" + settings("session_cookie_name") + "=(.+)$");
+
+ milliseconds_per_connection = settings("milliseconds_per_connection");
+
+ !decodeURIComponent(document.cookie).split(";").some(cookie => {
+
+ const matches = cookie.trim().match(session_pattern);
+
+ if(matches){
+ session = Number(matches[1]);
+ return true;
+ };
+
+ }) && (session = 0);
+
+ threads_start();
+ register_thread = threads_add(register_event);
+
+ });
+
+ };
+
+ const construct = () => {
+
+ settings("autostart") && self.start();
+
+ };
+
+ construct();
+
+};
diff --git a/Public/git_update.php b/Public/git_update.php
new file mode 100755
index 0000000..0879a28
--- /dev/null
+++ b/Public/git_update.php
@@ -0,0 +1,4 @@
+&1");
diff --git a/Public/images/min.png b/Public/images/min.png
new file mode 100755
index 0000000000000000000000000000000000000000..1cdb99c59dbd1142192a1e58f88a715bc283c81a
GIT binary patch
literal 546
zcmV+-0^R+IP)EX>4Tx04R}tkv&MmKpe$iQ$>-8pdCaUGE^rEqN0vc#UfZJZG~1HOfLO`CJjl7
zi=*ILaPVWX>fqw6tAnc`2!4P#Iyou2NQwVT3N2zhIPS;0dyl(!fKV?p&FUBjG~G6n
zv8a^Eu1a062%;Y%W+2MUGUBPlGCuBDCtHSV+=-)W<*M`V-_*$W;O(
z#{w$QAiI9>Klt6Pm7kpOlEf&``QkVqBS6
+
+
+ ContactBook
+
+
+
+
+
+
+
+
+
diff --git a/Public/tests/sessions.php b/Public/tests/sessions.php
new file mode 100755
index 0000000..f28bd1e
--- /dev/null
+++ b/Public/tests/sessions.php
@@ -0,0 +1,23 @@
+ time() + (86400 * 30),
+ "path" => "/",
+ "domain" => $_SERVER['SERVER_NAME'],
+ "secure" => false,
+ "httponly" => true,
+ "samesite" => "Strict"
+ ]);
+ echo "PASA
\n";
+ print_r(array_keys($_COOKIE));
+ };
+ echo (
+ (isset($_SESSION["id"]) ? $_SESSION["id"] : $_SESSION["id"] = random_int(0, 99999999)) . "
\n" .
+ $_COOKIE["id"]
+ );
diff --git a/Public/tests/test.html b/Public/tests/test.html
new file mode 100755
index 0000000..6cd7309
--- /dev/null
+++ b/Public/tests/test.html
@@ -0,0 +1,29 @@
+
+
+
+ ContactBook
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Test
+
+
diff --git a/Public/wmd.php b/Public/wmd.php
new file mode 100755
index 0000000..305c9ab
--- /dev/null
+++ b/Public/wmd.php
@@ -0,0 +1,6 @@
+ "update_scripts",
+ "author" => "KyMAN",
+ "project" => "KStats",
+ "class" => "KStats",
+ "object" => "kstats",
+ "url" => "https://kstats.k3y.pw",
+ "project_author" => "KyMAN",
+ "key_words" => "contact,book,contact book,kyman,secrets,notes",
+ "logo" => "/images/KStats.png",
+ "language" => "es",
+ "wmd_file" => "/../WMarkDown/HTML/script.w.md",
+ "wmd_file_empty" => "/../WMarkDown/HTML/file.w.md",
+ "ignore_script_paths" => [],
+ "only" => "/Public"
+ ], KStats\Secrets::wmarkdown));
diff --git a/WMD/dev/ECMAScript/index.w.md b/WMD/dev/ECMAScript/index.w.md
new file mode 100755
index 0000000..13f339f
--- /dev/null
+++ b/WMD/dev/ECMAScript/index.w.md
@@ -0,0 +1,24 @@
+[[post_data {
+ "author" : "KyMAN",
+ "since" : 20220320,
+ "version" : 20220320
+}]]
+# ECMAScript
+
+[[header_level 0]]
+[[include /WMD/dev/Public/ecma/KStats.ecma.js.w.md]]
+
+
+
+[[html_data {
+ "title" : "ECMAScript - KStats",
+ "url" : "https://kstats.k3y.pw/dev/ECMAScript/index.html",
+ "author" : "KyMAN",
+ "since" : 20220320,
+ "version" : 20220320,
+ "key_words" : "contact,book,contact book,kyman,secrets,notes,developt,desarrollo,programación,ecmascript",
+ "description" : "Parte ECMAScript del KStats.",
+ "project" : "KStats",
+ "logo" : "/images/KStats.png",
+ "language" : "es"
+}]]
diff --git a/WMD/dev/PHP/index.w.md b/WMD/dev/PHP/index.w.md
new file mode 100755
index 0000000..33a32b0
--- /dev/null
+++ b/WMD/dev/PHP/index.w.md
@@ -0,0 +1,36 @@
+[[post_data {
+ "author" : "KyMAN",
+ "since" : 20220320,
+ "version" : 20220320
+}]]
+# PHP
+
+[[header_level 0]]
+[[include /WMD/dev/Public/api/index.php.w.md]]
+
+[[header_level 0]]
+[[include /WMD/dev/Public/git_update.php.w.md]]
+
+[[header_level 0]]
+[[include /WMD/dev/Public/tests/sessions.php.w.md]]
+
+[[header_level 0]]
+[[include /WMD/dev/Public/wmd.php.w.md]]
+
+[[header_level 0]]
+[[include /WMD/dev/Public/wmd_scripts.php.w.md]]
+
+
+
+[[html_data {
+ "title" : "PHP - KStats",
+ "url" : "https://kstats.k3y.pw/dev/PHP/index.html",
+ "author" : "KyMAN",
+ "since" : 20220320,
+ "version" : 20220320,
+ "key_words" : "contact,book,contact book,kyman,secrets,notes,developt,desarrollo,programación,php",
+ "description" : "Parte PHP del KStats.",
+ "project" : "KStats",
+ "logo" : "/images/KStats.png",
+ "language" : "es"
+}]]
diff --git a/WMD/dev/Public/api/index.php.w.md b/WMD/dev/Public/api/index.php.w.md
new file mode 100755
index 0000000..ae0685c
--- /dev/null
+++ b/WMD/dev/Public/api/index.php.w.md
@@ -0,0 +1,23 @@
+[[post_data {
+ "author" : "KyMAN",
+ "since" : 20220320,
+ "version" : 20220320
+}]]
+# index.php
+
+```txt
+/Public/api/index.php
+```
+
+[[html_data {
+ "title" : "index.php - KStats",
+ "url" : "https://kstats.k3y.pw/Public/api/index.php.html",
+ "author" : "KyMAN",
+ "since" : 20220320,
+ "version" : 20220320,
+ "key_words" : "contact,book,contact book,kyman,secrets,notes,developt,desarrollo,programación,main",
+ "description" : "index.php del KStats.",
+ "project" : "KStats",
+ "logo" : "/images/KStats.png",
+ "language" : "es"
+}]]
diff --git a/WMD/dev/Public/ecma/KStats.ecma.js.w.md b/WMD/dev/Public/ecma/KStats.ecma.js.w.md
new file mode 100755
index 0000000..ac69aa0
--- /dev/null
+++ b/WMD/dev/Public/ecma/KStats.ecma.js.w.md
@@ -0,0 +1,268 @@
+[[post_data {
+ "author" : "KyMAN",
+ "since" : 20220320,
+ "version" : 20220320
+}]]
+# KStats.ecma.js
+
+```txt
+/Public/ecma/KStats.ecma.js
+```
+
+## [[plain KStats.default_value]]
+
+[[wdoc
+Método object.
+@name KStats.default_value
+@lang ECMAScript
+@author KyMAN
+@since 20220320
+@version 20220320
+@access public
+@hash a1841f42c4352bb7f9f7bb87f89ff6fb
+#_default - optional Parámetro _default
+#nulls - optional Parámetro nulls
+#return - - Retorno.
+]]
+
+## [[plain KStats.settings]]
+
+[[wdoc
+Método object.
+@name KStats.settings
+@see KStats.default_value
+@see KStats.settings
+@lang ECMAScript
+@author KyMAN
+@since 20220320
+@version 20220320
+@access public
+@hash 97e5deffc108b21fd26d5aa45248e1e0
+#names - optional Parámetro names
+#inputs - optional Parámetro inputs
+#_default - optional Parámetro _default
+#nulls - optional Parámetro nulls
+#return - - Retorno.
+]]
+
+## [[plain KStats.load]]
+
+[[wdoc
+Método object.
+@name KStats.load
+@see KStats.settings
+@lang ECMAScript
+@author KyMAN
+@since 20220320
+@version 20220320
+@access public
+@hash 5d29594ed33dec2bfa759d0f4dc71d16
+#url - optional Parámetro url
+#callback - optional Parámetro callback
+#return - - Retorno.
+]]
+
+## [[plain KStats.settings_add_i]]
+
+[[wdoc
+Método object.
+@name KStats.settings_add_i
+@see KStats.settings_add_i
+@see KStats.load
+@lang ECMAScript
+@author KyMAN
+@since 20220320
+@version 20220320
+@access private
+@hash f91128cfea61bf0ad16c2d04ba1a1822
+#items - optional Parámetro items
+#overwrite - optional Parámetro overwrite
+#callback - optional Parámetro callback
+#i - optional Parámetro i
+]]
+
+## [[plain KStats.settings_add]]
+
+[[wdoc
+Método object.
+@name KStats.settings_add
+@see KStats.settings_add_i
+@see KStats.settings
+@lang ECMAScript
+@author KyMAN
+@since 20220320
+@version 20220320
+@access public
+@hash e6787641e9592c9293090db05ce49585
+#items - optional Parámetro items
+#overwrite - optional Parámetro overwrite
+#callback - optional Parámetro callback
+#return - - Retorno.
+]]
+
+## [[plain KStats.threads_method]]
+
+[[wdoc
+Método object.
+@name KStats.threads_method
+@lang ECMAScript
+@author KyMAN
+@since 20220320
+@version 20220320
+@access private
+@hash ab9bbde1ed980d410067bd829ca8d957
+#return - - Retorno.
+]]
+
+## [[plain KStats.threads_start]]
+
+[[wdoc
+Método object.
+@name KStats.threads_start
+@see KStats.settings
+@see KStats.threads_method
+@lang ECMAScript
+@author KyMAN
+@since 20220320
+@version 20220320
+@access public
+@hash 1090cef55ff368e19f68fb0f13262eb0
+#return - - Retorno.
+]]
+
+## [[plain KStats.threads_stop]]
+
+[[wdoc
+Método object.
+@name KStats.threads_stop
+@lang ECMAScript
+@author KyMAN
+@since 20220320
+@version 20220320
+@access public
+@hash c1b6d79c278d28a090faf96255fa0c34
+]]
+
+## [[plain KStats.threads_add]]
+
+[[wdoc
+Método object.
+@name KStats.threads_add
+@lang ECMAScript
+@author KyMAN
+@since 20220320
+@version 20220320
+@access public
+@hash 7b36976fb1531ed998a2a76196d98172
+#method - optional Parámetro method
+#return - - Retorno.
+]]
+
+## [[plain KStats.threads_remove]]
+
+[[wdoc
+Método object.
+@name KStats.threads_remove
+@lang ECMAScript
+@author KyMAN
+@since 20220320
+@version 20220320
+@access public
+@hash e005119bc808e1eefdbfec228fac6441
+#i - optional Parámetro i
+#return - - Retorno.
+]]
+
+## [[plain KStats.string_variables]]
+
+[[wdoc
+Método object.
+@name KStats.string_variables
+@lang ECMAScript
+@author KyMAN
+@since 20220320
+@version 20220320
+@access public
+@hash e372ccebccaf8f8d06c24f79d629e7a6
+#string - optional Parámetro string
+#variables - optional Parámetro variables
+#_default - optional Parámetro _default
+]]
+
+## [[plain KStats.update]]
+
+[[wdoc
+Método object.
+@name KStats.update
+@see KStats.load
+@see KStats.string_variables
+@see KStats.settings
+@see KStats.update
+@lang ECMAScript
+@author KyMAN
+@since 20220320
+@version 20220320
+@access private
+@hash 735287af679b35ef121b59e52ba3dbab
+#return - - Retorno.
+]]
+
+## [[plain KStats.register_event]]
+
+[[wdoc
+Método object.
+@name KStats.register_event
+@see KStats.update
+@lang ECMAScript
+@author KyMAN
+@since 20220320
+@version 20220320
+@access private
+@hash b6094c4945ffa697ddde38d0158e4f7e
+]]
+
+## [[plain KStats.start]]
+
+[[wdoc
+Método object.
+@name KStats.start
+@see KStats.settings_add
+@see KStats.settings
+@see KStats.threads_start
+@see KStats.threads_add
+@see KStats.register_event
+@lang ECMAScript
+@author KyMAN
+@since 20220320
+@version 20220320
+@access public
+@hash 0414214855b51d79636becf6f5b3bdc1
+]]
+
+## [[plain KStats.construct]]
+
+[[wdoc
+Método object.
+@name KStats.construct
+@see KStats.settings
+@see KStats.start
+@lang ECMAScript
+@author KyMAN
+@since 20220320
+@version 20220320
+@access private
+@hash 88422b1f098ab1ff14b3b73c5b3f48f5
+]]
+
+[[html_data {
+ "title" : "KStats.ecma.js - KStats",
+ "url" : "https://kstats.k3y.pw/Public/ecma/KStats.ecma.js.html",
+ "author" : "KyMAN",
+ "since" : 20220320,
+ "version" : 20220320,
+ "key_words" : "contact,book,contact book,kyman,secrets,notes,developt,desarrollo,programación,cma",
+ "description" : "KStats.ecma.js del KStats.",
+ "project" : "KStats",
+ "logo" : "/images/KStats.png",
+ "language" : "es"
+}]]
diff --git a/WMD/dev/Public/git_update.php.w.md b/WMD/dev/Public/git_update.php.w.md
new file mode 100755
index 0000000..632d35a
--- /dev/null
+++ b/WMD/dev/Public/git_update.php.w.md
@@ -0,0 +1,23 @@
+[[post_data {
+ "author" : "KyMAN",
+ "since" : 20220320,
+ "version" : 20220320
+}]]
+# git_update.php
+
+```txt
+/Public/git_update.php
+```
+
+[[html_data {
+ "title" : "git_update.php - KStats",
+ "url" : "https://kstats.k3y.pw/Public/git_update.php.html",
+ "author" : "KyMAN",
+ "since" : 20220320,
+ "version" : 20220320,
+ "key_words" : "contact,book,contact book,kyman,secrets,notes,developt,desarrollo,programación,main",
+ "description" : "git_update.php del KStats.",
+ "project" : "KStats",
+ "logo" : "/images/KStats.png",
+ "language" : "es"
+}]]
diff --git a/WMD/dev/Public/tests/sessions.php.w.md b/WMD/dev/Public/tests/sessions.php.w.md
new file mode 100755
index 0000000..afa5593
--- /dev/null
+++ b/WMD/dev/Public/tests/sessions.php.w.md
@@ -0,0 +1,23 @@
+[[post_data {
+ "author" : "KyMAN",
+ "since" : 20220320,
+ "version" : 20220320
+}]]
+# sessions.php
+
+```txt
+/Public/tests/sessions.php
+```
+
+[[html_data {
+ "title" : "sessions.php - KStats",
+ "url" : "https://kstats.k3y.pw/Public/tests/sessions.php.html",
+ "author" : "KyMAN",
+ "since" : 20220320,
+ "version" : 20220320,
+ "key_words" : "contact,book,contact book,kyman,secrets,notes,developt,desarrollo,programación,main",
+ "description" : "sessions.php del KStats.",
+ "project" : "KStats",
+ "logo" : "/images/KStats.png",
+ "language" : "es"
+}]]
diff --git a/WMD/dev/Public/wmd.php.w.md b/WMD/dev/Public/wmd.php.w.md
new file mode 100755
index 0000000..3a766ba
--- /dev/null
+++ b/WMD/dev/Public/wmd.php.w.md
@@ -0,0 +1,23 @@
+[[post_data {
+ "author" : "KyMAN",
+ "since" : 20220320,
+ "version" : 20220320
+}]]
+# wmd.php
+
+```txt
+/Public/wmd.php
+```
+
+[[html_data {
+ "title" : "wmd.php - KStats",
+ "url" : "https://kstats.k3y.pw/Public/wmd.php.html",
+ "author" : "KyMAN",
+ "since" : 20220320,
+ "version" : 20220320,
+ "key_words" : "contact,book,contact book,kyman,secrets,notes,developt,desarrollo,programación,main",
+ "description" : "wmd.php del KStats.",
+ "project" : "KStats",
+ "logo" : "/images/KStats.png",
+ "language" : "es"
+}]]
diff --git a/WMD/dev/Public/wmd_scripts.php.w.md b/WMD/dev/Public/wmd_scripts.php.w.md
new file mode 100755
index 0000000..8c1c9c3
--- /dev/null
+++ b/WMD/dev/Public/wmd_scripts.php.w.md
@@ -0,0 +1,23 @@
+[[post_data {
+ "author" : "KyMAN",
+ "since" : 20220320,
+ "version" : 20220320
+}]]
+# wmd_scripts.php
+
+```txt
+/Public/wmd_scripts.php
+```
+
+[[html_data {
+ "title" : "wmd_scripts.php - KStats",
+ "url" : "https://kstats.k3y.pw/Public/wmd_scripts.php.html",
+ "author" : "KyMAN",
+ "since" : 20220320,
+ "version" : 20220320,
+ "key_words" : "contact,book,contact book,kyman,secrets,notes,developt,desarrollo,programación,main",
+ "description" : "wmd_scripts.php del KStats.",
+ "project" : "KStats",
+ "logo" : "/images/KStats.png",
+ "language" : "es"
+}]]
diff --git a/WMD/dev/index.w.md b/WMD/dev/index.w.md
new file mode 100755
index 0000000..e69de29
diff --git a/WMD/es/bugs.w.md b/WMD/es/bugs.w.md
new file mode 100755
index 0000000..5b7af95
--- /dev/null
+++ b/WMD/es/bugs.w.md
@@ -0,0 +1,52 @@
+[[post_data {
+ "author" : "KyMAN",
+ "since" : 20220402,
+ "version" : 20220402
+}]]
+# Bugs y errores
+
+En esta sección de la Web se detallarán los errores y Bugs encontrados durante el desarrollo y uso
+de la herramienta.
+
+## 2022040200 - Redirección IP con Cloudflare y Nginx
+
+A la hora de capturar la dirección IP del usuario sobre Cloudflare nos encontramos con un problema
+el cual, el paquete de datos HTTP altera la cabecera al paso por Cloudflare, siendo el cliente
+Cloudflare y no el cliente real al que hacemos referencia. Las variables de los datos META también
+son alterados, incluyendo el REMOTE_ADDR.
+
+[X] Arreglar el problema con la configuración actual sobre Nginx contra Cloudflare.
+
+> [[! note NOTA]]: Cloudflare agrega un nuevo valor en los META del paquete HTTP llamado
+HTTP_CF_CONNECTING_IP, el cual puede ser recogido directamente por PHP mediante $_SERVER o cargado
+indirectamente con los datos de cabecera. Se pueden alterar los datos de entrada del REMOTE_ADDR
+desde Nginx poniendo como valor dicha cabecera pero al ser accesible desde PHP de esta forma,
+simplemente se alterará la cadena de llaves de acceso a las IPs.
+
+```php
+
+$ip = null;
+
+foreach(["HTTP_CF_CONNECTING_IP", "HTTP_X_FORWARDED_FOR", "HTTP_X_REAL_IP", "HTTP_CLIENT_IP", "REMOTE_ADDR"] as $key){
+ if(!empty($_SERVER[$key]))
+ $ip = explode(",", $_SERVER[$key])[0];
+ if($ips = getenv($key))
+ $ip = explode(",", $ips)[0];
+};
+
+echo $ip;
+
+```
+
+[[html_data {
+ "title" : "KStats - Bugs",
+ "url" : "https://kstats.k3y.pw/es/bugs.html",
+ "author" : "KyMAN",
+ "since" : 20220402,
+ "version" : 20220402,
+ "key_words" : "kstats,stats,statistics,kyman,wmd,wmarkdown,documentación,bugs,fallos,errores,arreglos,problemas,fix",
+ "description" : "Bugs y errores del proyecto KStats.",
+ "project" : "KStats",
+ "logo" : "https://kstats.k3y.pw/images/KStats.png",
+ "language" : "es"
+}]]
diff --git a/WMD/es/index.w.md b/WMD/es/index.w.md
new file mode 100755
index 0000000..acefaee
--- /dev/null
+++ b/WMD/es/index.w.md
@@ -0,0 +1,37 @@
+[[post_data {
+ "author" : "KyMAN",
+ "since" : 20220316,
+ "version" : 20220320
+}]]
+# KStats
+
+Documentación del proyecto KStats, proyecto que hace registros masivos de movimientos de los
+usuarios dentro de las Webs registradas por los Tokens.
+
+[[header_level 0]]
+[[include /WMD/es/project.w.md]]
+
+[[header_level 0]]
+[[include /WMD/es/work.w.md]]
+
+[[header_level 0]]
+[[include /WMD/es/projects.w.md]]
+
+[[header_level 0]]
+[[include /WMD/es/bugs.w.md]]
+
+[[header_level 0]]
+[[include /WMD/es/targets.w.md]]
+
+[[html_data {
+ "title" : "KStats - Documentación",
+ "url" : "https://kstats.k3y.pw/es/",
+ "author" : "KyMAN",
+ "since" : 20220316,
+ "version" : 20220320,
+ "key_words" : "kstats,stats,statistics,kyman,wmd,wmarkdown,documentación",
+ "description" : "Documentación del proyecto KStats.",
+ "project" : "KStats",
+ "logo" : "https://kstats.k3y.pw/images/KStats.png",
+ "language" : "es"
+}]]
diff --git a/WMD/es/project.w.md b/WMD/es/project.w.md
new file mode 100755
index 0000000..d98a4be
--- /dev/null
+++ b/WMD/es/project.w.md
@@ -0,0 +1,24 @@
+[[post_data {
+ "author" : "KyMAN",
+ "since" : 20220316,
+ "version" : 20220320
+}]]
+# KStats
+
+El proyecto KStats no es más que un proyecto de gestión estadística de proceso paralelo a la
+ejecución de la Web el cual centraliza los datos en una base de datos común, lo que permite que un
+proyecto KStats levantado gestione más de un sitio Web. Requiere del CORS deshabilitado en el
+entorno API.
+
+[[html_data {
+ "title" : "KStats - Idioma",
+ "url" : "https://kstats.k3y.pw/es/projects.html",
+ "author" : "KyMAN",
+ "since" : 20220316,
+ "version" : 20220320,
+ "key_words" : "kstats,stats,statistics,kyman,wmd,wmarkdown,documentación",
+ "description" : "Documentación del proyecto KStats.",
+ "project" : "KStats",
+ "logo" : "https://kstats.k3y.pw/images/KStats.png",
+ "language" : "es"
+}]]
diff --git a/WMD/es/projects.w.md b/WMD/es/projects.w.md
new file mode 100755
index 0000000..85fb26e
--- /dev/null
+++ b/WMD/es/projects.w.md
@@ -0,0 +1,54 @@
+[[post_data {
+ "author" : "KyMAN",
+ "since" : 20220320,
+ "version" : 20220320
+}]]
+# Proyectos
+
+En esta sección se mostrarán los proyectos dependientes ya sea del KStats, como el KStats de otros
+proyectos. Para empezar dicha lista, empezaremos con los proyectos de los cuales depende el KStats,
+los cuales son los siguientes:
+
+[[links_group [{
+ "images" : ["https://wmarkdown.k3y.pw/images/wmarkdown.png"],
+ "link" : "https://wmarkdown.k3y.pw/",
+ "text" : "WMarkDown"
+}, {
+ "images" : ["https://wdictionaries.k3y.pw/images/wdictionaries.png"],
+ "link" : "https://wdictionaries.k3y.pw/",
+ "text" : "WDictionaries"
+}] ]]
+
+Los siguientes proyectos usan este proyecto para llevar una gestión estadística de uso por páginas
+Web.
+
+[[links_group [{
+ "images" : ["https://wmarkdown.k3y.pw/images/wmarkdown.png"],
+ "link" : "https://wmarkdown.k3y.pw/",
+ "text" : "WMarkDown"
+}, {
+ "images" : ["https://wdictionaries.k3y.pw/images/wdictionaries.png"],
+ "link" : "https://wdictionaries.k3y.pw/",
+ "text" : "WDictionaries"
+}, {
+ "images" : ["https://kyman.k3y.pw/images/KyMAN.png"],
+ "link" : "https://kyman.k3y.pw/",
+ "text" : "KyMAN"
+}, {
+ "images" : ["https://anp.k3y.pw/images/AnP.png"],
+ "link" : "https://anp.k3y.pw/",
+ "text" : "AnP"
+}] ]]
+
+[[html_data {
+ "title" : "KStats - Proyectos",
+ "url" : "https://kstats.k3y.pw/es/",
+ "author" : "KyMAN",
+ "since" : 20220320,
+ "version" : 20220320,
+ "key_words" : "kstats,stats,statistics,kyman,wmd,wmarkdown,documentación,projects,proyectos",
+ "description" : "Proyectos con el KStats.",
+ "project" : "KStats",
+ "logo" : "https://kstats.k3y.pw/images/KStats.png",
+ "language" : "es"
+}]]
diff --git a/WMD/es/targets.w.md b/WMD/es/targets.w.md
new file mode 100755
index 0000000..ddc26b0
--- /dev/null
+++ b/WMD/es/targets.w.md
@@ -0,0 +1,45 @@
+[[post_data {
+ "author" : "KyMAN",
+ "since" : 20220320,
+ "version" : 20220402
+}]]
+# Objetivos
+
+En esta sección se mostrarán los objetivos a llevar a cabo dentro de este proyecto. Todos ellos
+serán realizados por KyMAN.
+
+- [X] Crear proyecto y base del mismo.
+- [X] Crear base de datos con el procedimiento almacenado de inserción de registro.
+- [X] Crear entorno servidor en PHP. Intentar capturar la URL desde el propio PHP y gestionar
+sesión en entorno servidor.
+- [X] Añadir campo de habilitado o deshabilitado a los Tokens.
+- [X] Añadir campo de comprobación de URLs para evitar que cojan el Token para registrar sitios
+no esperados.
+- [X] Probar entorno CORS contra CSS.
+- [X] Crear lado privado de los Tokens para coger los datos de los Tokens públicos.
+ - *Un Token privado puede tener más de un Token público.*
+ - *Un Token público no tiene porqué tener un Token privado.*
+- [X] Añadir campo Observaciones a los Tokens.
+- [X] Cambiar método de registro mediante ECMA/JS vía CORS.
+- [-] Programar automatismo de Geolocalización de IPs.
+ - https://api.iplocation.net/
+ - [-] Analizar los datos de las URLs de geolocalización añadidas a
+ https://kyman.k3y.pw/wlog/info.html#Geolocation.
+- [X] Publicar versión funcional del proyecto.
+- [-] Documentar.
+- [-] Adaptar la documentación a que se genere en el servidor y mantener estadísticas de desarrollo
+en el GitLab.
+- [X] Arreglar Bug 2022040200.
+
+[[html_data {
+ "title" : "KStats - Objetivos",
+ "url" : "https://kstats.k3y.pw/es/targets.html",
+ "author" : "KyMAN",
+ "since" : 20220320,
+ "version" : 20220402,
+ "key_words" : "kstats,stats,statistics,kyman,wmd,wmarkdown,documentación,targets,objetivos",
+ "description" : "Objetivos del proyecto KStats.",
+ "project" : "KStats",
+ "logo" : "https://kstats.k3y.pw/images/KStats.png",
+ "language" : "es"
+}]]
diff --git a/WMD/es/work.w.md b/WMD/es/work.w.md
new file mode 100755
index 0000000..2f2cb18
--- /dev/null
+++ b/WMD/es/work.w.md
@@ -0,0 +1,195 @@
+[[post_data {
+ "author" : "KyMAN",
+ "since" : 20220320,
+ "version" : 20220320
+}]]
+# Funcionamiento
+
+El funcionamiento del KStats está dividido en dos partes: cliente y servidor. Para empezar por la
+parte más común, nos vamos a ir por la parte del lado cliente. Pero antes de empezar, es importante
+mencionar que el entorno cliente es un entorno ECMAScript el cual ha de ser ejecutado para que
+funcione sobre el lado servidor, y el lado servidor ha de estar activo.
+
+## Cliente
+
+El lado cliente se basa dos partes diferenciadas: en la creación del Token; y la instalación cliente
+del mismo.
+
+> [[! important IMPORTANTE]]: El KStats funciona de dos formas diferentes cara el cliente:
+registrando un histórico completo mediante ECMA/JS; y registrando accesos mediante petición URL.
+
+### Creación del Token
+
+Empezando por la creación del Token, nos encontramos en la base de datos, en caso de no tener acceso
+por favor, pónganse en contacto con el administrador del sitio, y hay que ejecutar un procedimiento
+almacenado llamado "token_create" el cual tiene los siguientes parámetros:
+
+- **$public (_entrada_)**: Parámetro de entrada que indica si el Token es público o no. Al estar
+sobre el lado cliente éste ha de ser siempre "true".
+- **$pattern (_entrada_)**: Parámetro de entrada que permite restringir los registros de URLs a un
+patrón regular, el cual puede tener más de un radical, como por ejemplo el caso de la Web de KyMAN.
+- **$remarks (_entrada_)**: Parámetro de entrada de texto libre con límite en 2048 caracteres que
+sirve para describir el Token, finalidad, etc. Así como establecer notas, observaciones o cualquier
+otro elemento textual que pertenezca al mismo.
+- **$error (_salida_)**: Parámetro de salida que mostrará un código de error en formato numérico
+entero donde cada bit representa lo siguiente según posición:
+ 0. Excepción SQL.
+ 1. El valor '$public' es nulo.
+ 2. El valor '$pattern' es nulo.
+ 3. El valor '$pattern' está vacío.
+ 4. El valor '$remarks' es nulo.
+ 5. El valor '$remarks' está vacío.
+- **$id (_salida_)**: Parámetro de salida que retorna el ID del nuevo Token creado.
+- **$token (_salida_)**: Parámetro de salida que retorna el nuevo Token creado.
+
+Ejemplos:
+
+```sql
+
+-- Para crear el Token de KSTats.
+call token_create(true, '^https?\\:\\/{2}kstats\\.k3y\\.pw\\/?', 'Token for KStats project.', @error, @id, @token);
+select @error, @id, @token;
+
+-- Para crear el Token de KyMAN y MiguelBST.
+call token_create(true, '^https?\\:\\/{2}(kyman|m(iguel)?bst)\\.k3y\\.pw\\/?', 'Token for KyMAN|MBST project.', @error, @id, @token);
+select @error, @id, @token;
+
+```
+
+### Adjuntar KStats para ECMA/JS
+
+Una vez tenemos los Tokens creados, podemos ir al entorno cliente propiamente dicho donde hemos de
+agregar en cada página Web donde queramos registrar los Stats el siguiente fragment HTML y ECMA.
+
+```html
+
+
+
+
+
+```
+
+La primera etiqueta adjunta a nuestra página Web el Script que gestiona el Script ECMA que nos hace
+falta para poder ejecutar el KStats; mientras que el segundo crea el objeto KStats con la URL
+concreta que identifica tu sitio Web dentro del KStats con su Token concreto. Es importante saber la
+estructura de la URL de petición.
+
+Los parámetros de entrada que se le pueden dar a ese diccionario donde metemos la URL son los
+siguientes:
+
+- **autostart**: Valor que determina si el objeto se inicia de forma automática (true) o se hace de
+forma manual (false). Por defecto es true.
+- **nulls**: Valor Booleano que determina si se admiten retornos nulos o no. Por defecto es false.
+- **default_value**: Valor por defecto a retornar en caso de no tener opciones de valor. Por defecto
+es null.
+- **ajax_timeout**: Tiempo límite de ejecución de una petición asícrona AJAX (XMLHttpRequest) en
+miliseguncos. Por defecto son 2000 milisegundos.
+- **settings_overwrite**: Valor Booleano que determina si se sobreescriben los valores de la
+configuración a la hora de añadirlos (true), en base a sus llaves, o no (false). Por defecto es false.
+- **kstats_url**: URL por defecto a la cual atacar. Por defecto es
+"https://kstats.k3y.pw/api/uCDY3brWxEJrJywm2sFcKo1d8oaUdmxTTrv3VGuhpyRDpPYXyKeHWeknh/{session}/ecma/set".
+- **frames_per_second** o **fps**: Fotogramas por segundo o tasa de refrescos por segundo cara los
+hilos. Por defecto son 1 fotograma por segundo.
+- **milliseconds_per_connection**: Tiempo de espera entre una conexión y otra para actualizar el
+estado actual del usuario en milisegundos. Por defecto son 2000.
+- **session_cookie_name**: Nombre de llave de la Cookie que almacena la sesión del servidor de forma
+cruzada. Por defecto es "kstats_session_id".
+- **session_timeout**: Tiempo límite de inactividad de la sesión en milisegundos. Por defecto es de
+60000. *Este valor no tiene efecto inicialmente*.
+- **data_key**: Nombre de llave de variable por donde se enviarán los datos al servidor. Por defecto
+es "kstats_data". **Tiene que ser el mismo que la del servidor.**
+
+### Registro por petición URL
+
+Este método permite hacer registro a partir de una llamada, tanto manual como automática, al
+servidor. A continuación se presentarán ejemplos de llamadas automáticas:
+
+```html
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+Y a continuación como se haría de forma manual desde HTML.
+
+```html
+
+
+KStats
+
+
+
+
+```
+
+### Estructura de la URL
+
+La estructura de la URL se compone de varias variables las cuales las encapsulamos a continuación entre llaves:
+
+```txt
+https://{domain}/api/{token}/{session}/{response}/{action}/{id}
+```
+
+Cada una de estas variables indica lo siguiente:
+
+- **domain**: Parámetro obligatorio que indica el dominio donde se encuentra el sitio Web del
+servidor KStats al que estamos atacando.
+- **token**: Parámetro obligatorio que indica el Token a usar contra el servidor KStats.
+- **session**: Parámetro obligatorio pero automático donde sólo hemos de indicar la variable, el
+cual contendrá el ID de la sesión actual a partir de una Cookie local evitando que éstas se crucen
+entre distintos dominios.
+- **response**: Modo de respuesta tras la acción de registrar la petición. Estos modos son los
+siguientes:
+ - **[[ignore js]]**: Retorna el Script KStats en formato JavaScript 1.8.5.
+ - **[[ignore ecma]]**: Retorna el Script KStats en formato ECMA 2015.
+ - **img** o **image**: Retorna una simple imagen de 1x1 de forma simbólica para no dar error
+ contra una etiqueta HTML IMG.
+ - **[[ignore css]]**: Retorna un contenido CSS para poder usar la etiqueta HTML LINK.
+ - **[[ignore json]]**: Retorna el resultado del proceso en formato JSON.
+ - **test**: Hace un retornos sobre un entorno de pruebas.
+- **action**: Método o acción a ejecutar en el servidor. Dichos métodos son los siguientes:
+ - **set**: Establece un nuevo registro de conexión o actualiza uno ya existente.
+- **id**: ID de registro actual o existente que se usará para determinar el tiempo de conexión en
+esa página Web concreta. *No analizar tiempo de inactividad*.
+
+> [[! note NOTA]]: En caso de no tener ninguno de estos tipos de valor en la variable 'response',
+éste retornará siempre un texto plano vacío.
+
+> [[! importante]]: El servidor ha de tener firma SSL para poder realizar la operación asíncrona
+segura por defecto.
+
+[[html_data {
+ "title" : "KStats - Funcionamiento",
+ "url" : "https://kstats.k3y.pw/es/",
+ "author" : "KyMAN",
+ "since" : 20220320,
+ "version" : 20220320,
+ "key_words" : "kstats,stats,statistics,kyman,wmd,wmarkdown,documentación,funcionamiento",
+ "description" : "Funcionamiento del proyecto KStats.",
+ "project" : "KStats",
+ "logo" : "https://kstats.k3y.pw/images/KStats.png",
+ "language" : "es"
+}]]
diff --git a/WMD/index.w.md b/WMD/index.w.md
new file mode 100755
index 0000000..2ecd55b
--- /dev/null
+++ b/WMD/index.w.md
@@ -0,0 +1,40 @@
+[[post_data {
+ "author" : "KyMAN",
+ "since" : 20220316,
+ "version" : 20220320
+}]]
+# KStats
+
+Seleccione un idioma para acceder a la documentación del proyecto.
+
+[[links_group [{
+ "images" : ["https://i.imgur.com/im1o0gc.png"],
+ "link" : "/es/",
+ "text" : "Español",
+ "self" : true
+}] ]]
+
+[[header_level 0]]
+[[include /WMD/es/project.w.md]]
+
+[[header_level 0]]
+[[include /WMD/es/projects.w.md]]
+
+[[header_level 0]]
+[[include /WMD/es/bugs.w.md]]
+
+[[header_level 0]]
+[[include /WMD/es/targets.w.md]]
+
+[[html_data {
+ "title" : "KStats - Idioma",
+ "url" : "https://kstats.k3y.pw/",
+ "author" : "KyMAN",
+ "since" : 20220316,
+ "version" : 20220320,
+ "key_words" : "kstats,stats,statistics,kyman,wmd,wmarkdown,documentación",
+ "description" : "Documentación del proyecto KStats.",
+ "project" : "KStats",
+ "logo" : "https://kstats.k3y.pw/images/KStats.png",
+ "language" : "es"
+}]]