diff --git a/build/DDL/00-recommended_table_order.txt b/build/DDL/00-recommended_table_order.txt index e945b56c..843e4561 100644 --- a/build/DDL/00-recommended_table_order.txt +++ b/build/DDL/00-recommended_table_order.txt @@ -1 +1 @@ -ban__ips ban__mails ban__names bic__acc_type bic__pzn bic__rclose bic__reg bic__rstr bic__settings bic__srvcs cron__settings cron__tasks ffxiv__achievement ffxiv__city ffxiv__clan ffxiv__count_filter ffxiv__enemy ffxiv__grandcompany ffxiv__guardian ffxiv__jobs ffxiv__linkshell_rank ffxiv__nameday ffxiv__orderby ffxiv__pvpteam_rank ffxiv__server ffxiv__timeactive pma__bookmark pma__central_columns pma__column_info pma__designer_settings pma__export_templates pma__favorite pma__history pma__navigationhiding pma__pdf_pages pma__recent pma__relation pma__savedsearches pma__table_coords pma__table_info pma__table_uiprefs pma__tracking pma__userconfig pma__usergroups pma__users seo__pageviews seo__visitors sys__languages sys__log_types sys__settings talks__alt_link_types talks__tags uc__groups uc__permissions uc__users bic__list bic__swift cron__log cron__schedule ffxiv__estate ffxiv__freecompany ffxiv__freecompany_names ffxiv__freecompany_rank ffxiv__freecompany_ranking ffxiv__grandcompany_rank ffxiv__linkshell ffxiv__linkshell_names ffxiv__pvpteam ffxiv__pvpteam_names sys__files sys__logs talks__types uc__cookies uc__emails uc__group_to_permission uc__sessions uc__user_to_group uc__user_to_permission bic__accounts bic__acc_rstr bic__bic_rstr ffxiv__character ffxiv__character_achievement ffxiv__character_clans ffxiv__character_jobs ffxiv__character_names ffxiv__character_servers ffxiv__freecompany_character ffxiv__linkshell_character ffxiv__pvpteam_character talks__sections talks__threads talks__thread_to_tags uc__avatars uc__user_to_section talks__alt_links talks__posts talks__posts_history talks__attachments talks__likes talks__old_music \ No newline at end of file +ban__ips ban__mails ban__names bic__acc_type bic__pzn bic__rclose bic__reg bic__rstr bic__settings bic__srvcs cron__event_types cron__settings cron__tasks ffxiv__achievement ffxiv__city ffxiv__clan ffxiv__count_filter ffxiv__enemy ffxiv__grandcompany ffxiv__guardian ffxiv__jobs ffxiv__linkshell_rank ffxiv__nameday ffxiv__orderby ffxiv__pvpteam_rank ffxiv__server ffxiv__timeactive pma__bookmark pma__central_columns pma__column_info pma__designer_settings pma__export_templates pma__favorite pma__history pma__navigationhiding pma__pdf_pages pma__recent pma__relation pma__savedsearches pma__table_coords pma__table_info pma__table_uiprefs pma__tracking pma__userconfig pma__usergroups pma__users seo__pageviews seo__visitors sys__languages sys__log_types sys__settings talks__alt_link_types talks__tags uc__groups uc__permissions uc__users bic__list bic__swift cron__log cron__schedule ffxiv__estate ffxiv__freecompany ffxiv__freecompany_names ffxiv__freecompany_rank ffxiv__freecompany_ranking ffxiv__grandcompany_rank ffxiv__linkshell ffxiv__linkshell_names ffxiv__pvpteam ffxiv__pvpteam_names sys__files sys__logs talks__types uc__cookies uc__emails uc__group_to_permission uc__sessions uc__user_to_group uc__user_to_permission bic__accounts bic__acc_rstr bic__bic_rstr ffxiv__character ffxiv__character_achievement ffxiv__character_clans ffxiv__character_jobs ffxiv__character_names ffxiv__character_servers ffxiv__freecompany_character ffxiv__linkshell_character ffxiv__pvpteam_character talks__sections talks__threads talks__thread_to_tags uc__avatars uc__user_to_section talks__alt_links talks__posts talks__posts_history talks__attachments talks__likes talks__old_music \ No newline at end of file diff --git a/build/DDL/Data/Cron.sql b/build/DDL/Data/Cron.sql index 1c549193..53be8ab6 100644 --- a/build/DDL/Data/Cron.sql +++ b/build/DDL/Data/Cron.sql @@ -76,4 +76,6 @@ INSERT INTO `cron__schedule` (`task`, `arguments`, `instance`, `system`, `freque INSERT INTO `cron__schedule` (`task`, `arguments`, `instance`, `system`, `frequency`, `dayofmonth`, `dayofweek`, `priority`, `message`) VALUES ('ffUpdateOld', '[50, \"$cronInstance\"]', '5', '1', '60', NULL, NULL, '0', 'Updating old FFXIV entities'); INSERT INTO `cron__schedule` (`task`, `arguments`, `instance`, `system`, `frequency`, `dayofmonth`, `dayofweek`, `priority`, `message`) VALUES ('ffUpdateOld', '[50, \"$cronInstance\"]', '6', '1', '60', NULL, NULL, '0', 'Updating old FFXIV entities'); -UPDATE `cron__settings` SET `value` = '7' WHERE `cron__settings`.`setting` = 'maxThreads'; \ No newline at end of file +UPDATE `cron__settings` SET `value` = '7' WHERE `cron__settings`.`setting` = 'maxThreads'; + +UPDATE `cron__settings` SET `value` = '10' WHERE `cron__settings`.`setting` = 'maxThreads'; \ No newline at end of file diff --git a/build/DDL/Data/FFTracker.sql b/build/DDL/Data/FFTracker.sql index c7ba2d4a..f3b6330c 100644 --- a/build/DDL/Data/FFTracker.sql +++ b/build/DDL/Data/FFTracker.sql @@ -8149,3 +8149,5 @@ INSERT IGNORE INTO `ffxiv__server` (`serverid`, `server`, `datacenter`) VALUES (89, 'Rafflesia', 'Dynamis'); INSERT INTO `ffxiv__jobs`(`name`) VALUES ('Alchemist'), ('Armorer'), ('Astrologian'), ('Bard'), ('Black Mage'), ('Blacksmith'), ('Blue Mage'), ('Botanist'), ('Carpenter'), ('Culinarian'), ('Dancer'), ('Dark Knight'), ('Dragoon'), ('Fisher'), ('Goldsmith'), ('Gunbreaker'), ('Leatherworker'), ('Machinist'), ('Miner'), ('Monk'), ('Ninja'), ('Paladin'), ('Pictomancer'), ('Reaper'), ('Red Mage'), ('Sage'), ('Samurai'), ('Scholar'), ('Summoner'), ('Viper'), ('Warrior'), ('Weaver'), ('White Mage'); + +UPDATE `ffxiv__achievement` SET `earnedby`=(SELECT COUNT(*) FROM `ffxiv__character_achievement` WHERE `ffxiv__character_achievement`.`achievementid`=`ffxiv__achievement`.`achievementid`); \ No newline at end of file diff --git a/build/DDL/cron__event_types.sql b/build/DDL/cron__event_types.sql new file mode 100644 index 00000000..57c2888b --- /dev/null +++ b/build/DDL/cron__event_types.sql @@ -0,0 +1,5 @@ +CREATE TABLE `cron__event_types` ( + `type` varchar(30) NOT NULL COMMENT 'Type of the event', + `description` varchar(100) NOT NULL COMMENT 'Description of the event', + PRIMARY KEY (`type`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_nopad_ci COMMENT='Different event types for logging and SSE output' `PAGE_COMPRESSED`='ON' ROW_FORMAT=Dynamic; \ No newline at end of file diff --git a/build/DDL/cron__log.sql b/build/DDL/cron__log.sql index 29255887..b898aa06 100644 --- a/build/DDL/cron__log.sql +++ b/build/DDL/cron__log.sql @@ -6,11 +6,12 @@ CREATE TABLE `cron__log` ( `task` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_nopad_ci DEFAULT NULL COMMENT 'Optional task ID', `arguments` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_nopad_ci DEFAULT NULL COMMENT 'Optional task arguments', `instance` int(10) unsigned DEFAULT NULL COMMENT 'Instance number of the task', - `message` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_nopad_ci NOT NULL COMMENT 'Error for the text', + `message` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_nopad_ci NOT NULL COMMENT 'Message provided by the event', KEY `time` (`time`), KEY `time_desc` (`time` DESC) USING BTREE, KEY `type` (`type`) USING BTREE, KEY `runby` (`runby`) USING BTREE, KEY `task` (`task`) USING BTREE, - CONSTRAINT `errors_to_tasks` FOREIGN KEY (`task`) REFERENCES `cron__tasks` (`task`) ON DELETE CASCADE ON UPDATE CASCADE + CONSTRAINT `cron_log_to_event_type` FOREIGN KEY (`type`) REFERENCES `cron__event_types` (`type`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `cron_log_to_tasks` FOREIGN KEY (`task`) REFERENCES `cron__tasks` (`task`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC `PAGE_COMPRESSED`='ON'; \ No newline at end of file diff --git a/build/DDL/cron__schedule.sql b/build/DDL/cron__schedule.sql index 3f0680e0..793e59cf 100644 --- a/build/DDL/cron__schedule.sql +++ b/build/DDL/cron__schedule.sql @@ -2,6 +2,7 @@ CREATE TABLE `cron__schedule` ( `task` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_nopad_ci NOT NULL COMMENT 'Task ID', `arguments` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_nopad_ci NOT NULL COMMENT 'Optional arguments in JSON string', `instance` int(10) unsigned NOT NULL DEFAULT 1 COMMENT 'Instance number of the task', + `enabled` tinyint(1) unsigned NOT NULL DEFAULT 1 COMMENT 'Whether a task instance is enabled and should be run as per schedule', `system` tinyint(1) unsigned NOT NULL DEFAULT 0 COMMENT 'Flag indicating whether a task instance is system one and can''t be deleted from Cron\\Schedule class', `frequency` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'Frequency to run a task in seconds', `dayofmonth` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_nopad_ci DEFAULT NULL COMMENT 'Optional limit to run only on specific days of the month. Expects array of integers in JSON string.', @@ -24,5 +25,6 @@ CREATE TABLE `cron__schedule` ( KEY `runby` (`runby`), KEY `lastrun` (`lastrun`), KEY `arguments` (`arguments`), + KEY `enabled` (`enabled`), CONSTRAINT `schedule_to_task` FOREIGN KEY (`task`) REFERENCES `cron__tasks` (`task`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC `PAGE_COMPRESSED`='ON'; \ No newline at end of file diff --git a/build/DDL/cron__tasks.sql b/build/DDL/cron__tasks.sql index 9d29902e..6b54e37a 100644 --- a/build/DDL/cron__tasks.sql +++ b/build/DDL/cron__tasks.sql @@ -5,7 +5,11 @@ CREATE TABLE `cron__tasks` ( `parameters` varchar(5000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_nopad_ci DEFAULT NULL COMMENT 'Optional parameters used on initial object creation in JSON string', `allowedreturns` varchar(5000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_nopad_ci DEFAULT NULL COMMENT 'Optional allowed return values to be treated as ''true'' by Cron processor in JSON string', `maxTime` int(10) unsigned NOT NULL DEFAULT 3600 COMMENT 'Maximum time allowed for the task to run. If exceeded, it will be terminated by PHP.', + `minFrequency` int(10) unsigned NOT NULL DEFAULT 60 COMMENT 'Minimal allowed frequency (in seconds) at which a task instance can run. Does not apply to one-time jobs.', + `retry` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'Custom number of seconds to reschedule a failed task instance for. 0 disables the functionality.', + `enabled` tinyint(1) unsigned NOT NULL DEFAULT 1 COMMENT 'Whether a task (and thus all its instances) is enabled and should be run as per schedule', `system` tinyint(1) unsigned NOT NULL DEFAULT 0 COMMENT 'Flag indicating that task is system and can''t be deleted from Cron\\Task class', `description` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_nopad_ci DEFAULT NULL COMMENT 'Description of the task', - PRIMARY KEY (`task`) + PRIMARY KEY (`task`), + KEY `enabled` (`enabled`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC `PAGE_COMPRESSED`='ON'; \ No newline at end of file diff --git a/build/DDL/ffxiv__achievement.sql b/build/DDL/ffxiv__achievement.sql index ad648c86..d62e3ee9 100644 --- a/build/DDL/ffxiv__achievement.sql +++ b/build/DDL/ffxiv__achievement.sql @@ -6,6 +6,7 @@ CREATE TABLE `ffxiv__achievement` ( `category` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_uca1400_nopad_as_ci DEFAULT NULL COMMENT 'Category of the achievement', `subcategory` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_uca1400_nopad_as_ci DEFAULT NULL COMMENT 'Subcategory of the achievement', `icon` varchar(150) CHARACTER SET utf8mb4 COLLATE utf8mb4_uca1400_nopad_as_ci DEFAULT NULL COMMENT 'Achievement icon without base URL (https://img.finalfantasyxiv.com/lds/pc/global/images/itemicon/)', + `earnedby` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'Number of characters that has earned the achievement so far', `howto` text CHARACTER SET utf8mb4 COLLATE utf8mb4_uca1400_nopad_ai_ci DEFAULT NULL COMMENT 'Instructions on getting achievements taken from Lodestone', `points` tinyint(3) unsigned DEFAULT NULL COMMENT 'Amount of points assigned to character for getting the achievement', `title` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_uca1400_nopad_ai_ci DEFAULT NULL COMMENT 'Optional title rewarded to character', diff --git a/config/frankenphp/cron/php.cron b/config/frankenphp/cron/php.cron index 92a32a8a..cf4afcef 100644 --- a/config/frankenphp/cron/php.cron +++ b/config/frankenphp/cron/php.cron @@ -1,4 +1,4 @@ -*/5 * * * * /usr/local/php/config/cron/php-cron.sh -#* * * * * sleep 20 && /usr/local/php/config/cron/php-cron.sh -#* * * * * sleep 40 && /usr/local/php/config/cron/php-cron.sh +* * * * * /usr/local/php/config/cron/php-cron.sh +* * * * * sleep 20 && /usr/local/php/config/cron/php-cron.sh +* * * * * sleep 40 && /usr/local/php/config/cron/php-cron.sh 0 0 * * * /usr/local/php/config/cron/log-rotate.sh diff --git a/lib/Website/HomePage.php b/lib/Website/HomePage.php index 45cdec49..b68e582f 100644 --- a/lib/Website/HomePage.php +++ b/lib/Website/HomePage.php @@ -1,5 +1,6 @@ init(); } - - #Initial routing logic + + /** + * Initial routing logic + * @return void + */ private function init(): void { #If not CLI - do redirects and other HTTP-related stuff @@ -67,13 +74,13 @@ private function init(): void if (self::$CLI) { #Process Cron $this->dbConnect(); - $healthCheck = (new Maintenance); + $healthCheck = new Maintenance(); #Check if DB is down $healthCheck->dbDown(); #Check space availability $healthCheck->noSpace(); #Run cron - (new Cron\Agent())->process(200); + (new Cron\Agent())->process(50); #Ensure we exit no matter what happens with CRON exit; } @@ -81,8 +88,8 @@ private function init(): void self::$method = $_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'] ?? $_SERVER['REQUEST_METHOD'] ?? null; #Parse multipart/form-data for PUT/DELETE/PATCH methods (if any) Headers::multiPartFormParse(); - if (in_array(self::$method, ['PUT', 'DELETE', 'PATCH'])) { - $_POST = array_change_key_case(self::$headers::$_PUT ?: self::$headers::$_DELETE ?: self::$headers::$_PATCH ?: []); + if (\in_array(self::$method, ['PUT', 'DELETE', 'PATCH'])) { + $_POST = array_change_key_case(Headers::$_PUT ?: Headers::$_DELETE ?: Headers::$_PATCH ?: []); Sanitization::carefulArraySanitization($_POST); } #Set canonical URL @@ -105,14 +112,14 @@ private function init(): void #Show error page if DB is down if (!self::$dbup) { self::$http_error = ['http_error' => 503, 'reason' => 'Failed to connect to database']; - #Show error page if maintenance is running } elseif (self::$dbUpdate) { + #Show error page if maintenance is running self::$http_error = ['http_error' => 503, 'reason' => 'Site is under maintenance and temporary unavailable']; } if ($uri[0] !== 'api') { Website\Config::$links = array_merge(Website\Config::$links, [ - ['rel' => 'stylesheet preload', 'href' => '/assets/styles/' . filemtime(Website\Config::$cssDir.'/app.css') . '.css', 'as' => 'style'], - ['rel' => 'preload', 'href' => '/assets/app.' . filemtime(Website\Config::$jsDir.'/app.js') . '.js', 'as' => 'script'], + ['rel' => 'stylesheet preload', 'href' => '/assets/styles/'.filemtime(Website\Config::$cssDir.'/app.css').'.css', 'as' => 'style'], + ['rel' => 'preload', 'href' => '/assets/app.'.filemtime(Website\Config::$jsDir.'/app.js').'.js', 'as' => 'script'], ]); } Links::links(Website\Config::$links); @@ -124,7 +131,7 @@ private function init(): void } #Try to start session if it's not started yet and DB is up if (self::$dbup && !self::$staleReturn && session_status() === PHP_SESSION_NONE) { - session_set_save_handler(new Session, true); + session_set_save_handler(new Session(), true); session_start(); #Update CSRF token if ($uri[0] !== 'api') { @@ -156,7 +163,7 @@ private function init(): void } #Do not do processing if we already encountered a problem if (empty(self::$http_error)) { - $vars = (new MainRouter)->route($uri); + $vars = new MainRouter()->route($uri); } else { $vars = self::$http_error; } @@ -173,7 +180,11 @@ private function init(): void Errors::error_log($e); } } - + + /** + * Generate canonical link + * @return void + */ public function canonical(): void { #May be client is using HTTP1.0 and there is not much to worry about, but maybe there is. @@ -194,10 +205,10 @@ public function canonical(): void } #And also return page or search query, if present self::$canonical .= '?'.http_build_query([ - #Do not add 1st page as query (since it is excessive) - 'page' => empty($_GET['page']) || $_GET['page'] === '1' ? null : $_GET['page'], - 'search' => $_GET['search'] ?? null, - ], encoding_type: PHP_QUERY_RFC3986); + #Do not add 1st page as query (since it is excessive) + 'page' => empty($_GET['page']) || $_GET['page'] === '1' ? null : $_GET['page'], + 'search' => $_GET['search'] ?? null, + ], encoding_type: PHP_QUERY_RFC3986); #Trim the excessive question mark, in case no query was attached self::$canonical = rtrim(self::$canonical, '?'); #Trim trailing slashes if any @@ -209,32 +220,41 @@ public function canonical(): void ['rel' => 'canonical', 'href' => self::$canonical], ]); } - - #Function to process some special files - + + /** + * Function to process some special files + * @param string $request + * + * @return int + */ public function filesRequests(string $request): int { #Remove query string, if present (that is everything after ?) $request = preg_replace('/^(.*)(\?.*)?$/', '$1', $request); if (preg_match('/^\.well-known\/security\.txt$/i', $request) === 1) { #Send headers, that will identify this as actual file - @header('Content-Type: text/plain; charset=utf-8'); - @header('Content-Disposition: inline; filename="security.txt"'); + if (headers_sent() === false) { + header('Content-Type: text/plain; charset=utf-8'); + header('Content-Disposition: inline; filename="security.txt"'); + } $this->twigProc(['template_override' => 'about/security.txt.twig', 'expires' => date(DateTimeInterface::RFC3339_EXTENDED, strtotime('last monday of next month midnight'))]); return 200; } #Return 0, since we did not hit anything return 0; } - - #Database connection + + /** + * Database connection + * @return bool + */ public function dbConnect(): bool { #Check in case we accidentally call this for 2nd time if (self::$dbup === false) { try { Pool::openConnection( - (new Config) + new Config() ->setHost($_ENV['DATABASE_HOST'], (int)$_ENV['MARIADB_PORT']) ->setUser($_ENV['DATABASE_USER']) ->setPassword($_ENV['DATABASE_PASSWORD']) @@ -254,7 +274,7 @@ public function dbConnect(): bool ->setOption(\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT, true), maxTries: 5); self::$dbup = true; #Cache controller - self::$dbController = (new Controller); + self::$dbController = new Controller(); #Check for maintenance self::$dbUpdate = (bool)self::$dbController->selectValue('SELECT `value` FROM `sys__settings` WHERE `setting`=\'maintenance\''); self::$dbController->query('SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;'); @@ -271,8 +291,14 @@ public function dbConnect(): bool } return true; } - - #Twig processing of the generated page + + /** + * Twig processing of the generated page + * @param array $twigVars List of Twig variables + * @param bool $cache Indicates if this is a cache pass + * + * @return bool + */ final public function twigProc(array $twigVars = [], bool $cache = false): bool { if ($cache) { @@ -290,7 +316,9 @@ final public function twigProc(array $twigVars = [], bool $cache = false): bool $output = EnvironmentGenerator::getTwig()->render($twigVars['template_override'] ?? 'index.twig', $twigVars); #Output data Common::zEcho($output, $twigVars['cacheStrat'] ?? 'hour', false); + /** @noinspection PhpUsageOfSilenceOperatorInspection */ @ob_end_flush(); + /** @noinspection PhpUsageOfSilenceOperatorInspection */ @ob_flush(); flush(); if (!empty($twigVars['cache_expires_at']) && ($twigVars['cache_expires_at'] - time()) > 0) { @@ -326,6 +354,7 @@ final public function twigProc(array $twigVars = [], bool $cache = false): bool self::$dataCache->write($twigVars, age: (int)$twigVars['cacheAge']); } if (self::$staleReturn === true) { + /** @noinspection PhpUsageOfSilenceOperatorInspection */ @ob_end_clean(); } else { #Output data diff --git a/lib/Website/fftracker/Cron.php b/lib/Website/fftracker/Cron.php index 836150f7..75e8698f 100644 --- a/lib/Website/fftracker/Cron.php +++ b/lib/Website/fftracker/Cron.php @@ -61,7 +61,9 @@ public function UpdateEntity(string|int $id, #[ExpectedValues(['character', 'fre /** * Function to update old entities - * @param int $limit How many entities to process + * + * @param int $limit How many entities to process + * @param int $instance Instance number, that called the function * * @return bool|string */ diff --git a/lib/Website/fftracker/Entities/Achievement.php b/lib/Website/fftracker/Entities/Achievement.php index 266e13f8..d0a7eb59 100644 --- a/lib/Website/fftracker/Entities/Achievement.php +++ b/lib/Website/fftracker/Entities/Achievement.php @@ -1,5 +1,6 @@ selectRow('SELECT *, (SELECT COUNT(*) as `count` FROM `ffxiv__character_achievement` WHERE `ffxiv__character_achievement`.`achievementid` = `ffxiv__achievement`.`achievementid`) as `count` FROM `ffxiv__achievement` WHERE `ffxiv__achievement`.`achievementid` = :id', [':id'=>$this->id]); + $data = HomePage::$dbController->selectRow('SELECT * FROM `ffxiv__achievement` WHERE `ffxiv__achievement`.`achievementid` = :id', [':id' => $this->id]); #Return empty, if nothing was found if (empty($data)) { return []; } #Get last characters with this achievement - $data['characters'] = HomePage::$dbController->selectAll('SELECT * FROM (SELECT \'character\' AS `type`, `ffxiv__character`.`characterid` AS `id`, `ffxiv__character`.`name`, `ffxiv__character`.`avatar` AS `icon` FROM `ffxiv__character_achievement` LEFT JOIN `ffxiv__character` ON `ffxiv__character`.`characterid` = `ffxiv__character_achievement`.`characterid` WHERE `ffxiv__character_achievement`.`achievementid` = :id ORDER BY `ffxiv__character_achievement`.`time` DESC LIMIT 50) t ORDER BY `name`', [':id'=>$this->id]); + $data['characters'] = HomePage::$dbController->selectAll('SELECT * FROM (SELECT \'character\' AS `type`, `ffxiv__character`.`characterid` AS `id`, `ffxiv__character`.`name`, `ffxiv__character`.`avatar` AS `icon` FROM `ffxiv__character_achievement` LEFT JOIN `ffxiv__character` ON `ffxiv__character`.`characterid` = `ffxiv__character_achievement`.`characterid` WHERE `ffxiv__character_achievement`.`achievementid` = :id ORDER BY `ffxiv__character_achievement`.`time` DESC LIMIT 50) t ORDER BY `name`', [':id' => $this->id]); #Register for an update if old enough or category or howto or dbid are empty. Also check that this is not a bot. if (empty($_SESSION['UA']['bot']) && !empty($data['characters']) && (empty($data['category']) || empty($data['subcategory']) || empty($data['howto']) || empty($data['dbid']) || (time() - strtotime($data['updated'])) >= 31536000)) { - #(new TaskInstance())->settingsFromArray(['task' => 'ffUpdateEntity', 'arguments' => [(string)$this->id, 'achievement'], 'message' => 'Updating achievement with ID '.$this->id, 'priority' => 2])->add(); + (new TaskInstance())->settingsFromArray(['task' => 'ffUpdateEntity', 'arguments' => [(string)$this->id, 'achievement'], 'message' => 'Updating achievement with ID '.$this->id, 'priority' => 2])->add(); } return $data; } - + /** - * @throws \Exception + * Get data from Lodestone + * @param bool $allowSleep Whether to wait in case Lodestone throttles the request (that is throttle on our side) + * + * @return string|array */ public function getFromLodestone(bool $allowSleep = false): string|array { #Cache objects - $Lodestone = (new Lodestone); + $Lodestone = new Lodestone(); #Get characters $altChars = HomePage::$dbController->selectColumn( 'SELECT `characterid` FROM `ffxiv__character_achievement` WHERE `achievementid`=:ach ORDER BY `time` DESC LIMIT 50;', @@ -72,7 +79,7 @@ public function getFromLodestone(bool $allowSleep = false): string|array } return 'Request throttled by Lodestone'; } - if (!empty($data['characters'][$char]['achievements'][$this->id]) && is_array($data['characters'][$char]['achievements'][$this->id])) { + if (!empty($data['characters'][$char]['achievements'][$this->id]) && \is_array($data['characters'][$char]['achievements'][$this->id])) { #Update character ID #Try to get achievement ID as seen in Lodestone database (play guide) $data = $Lodestone->searchDatabase('achievement', 0, 0, $data['characters'][$char]['achievements'][$this->id]['name'])->getResult(); @@ -97,8 +104,13 @@ public function getFromLodestone(bool $allowSleep = false): string|array } return []; } - - #Function to do processing + + /** + * Function to do processing of DB data + * @param array $fromDB + * + * @return void + */ protected function process(array $fromDB): void { $this->name = $fromDB['name']; @@ -119,77 +131,75 @@ protected function process(array $fromDB): void ], ]; $this->characters = [ - 'total' => (int)$fromDB['count'], + 'total' => (int)$fromDB['earnedby'], 'last' => $fromDB['characters'], ]; } - - #Function to update the entity + + /** + * Function to update the entity in DB + * @return bool + */ protected function updateDB(): bool { - try { - #Prepare bindings for actual update - $bindings = []; - $bindings[':achievementid'] = $this->id; - $bindings[':name'] = $this->lodestone['name']; - $bindings[':icon'] = self::removeLodestoneDomain($this->lodestone['icon']); + + #Prepare bindings for actual update + $bindings = []; + $bindings[':achievementid'] = $this->id; + $bindings[':name'] = $this->lodestone['name']; + $bindings[':icon'] = self::removeLodestoneDomain($this->lodestone['icon']); + #Download icon + $webp = Images::download($this->lodestone['icon'], Config::$icons.$bindings[':icon']); + if ($webp) { + $bindings[':icon'] = str_replace('.png', '.webp', $bindings[':icon']); + } + $bindings[':points'] = $this->lodestone['points']; + $bindings[':category'] = $this->lodestone['category']; + $bindings[':subcategory'] = $this->lodestone['subcategory']; + if (empty($this->lodestone['howto'])) { + $bindings[':howto'] = [NULL, 'null']; + } else { + $bindings[':howto'] = $this->lodestone['howto']; + } + if (empty($this->lodestone['title'])) { + $bindings[':title'] = [NULL, 'null']; + } else { + $bindings[':title'] = $this->lodestone['title']; + } + if (empty($this->lodestone['item']['name'])) { + $bindings[':item'] = [NULL, 'null']; + } else { + $bindings[':item'] = $this->lodestone['item']['name']; + } + if (empty($this->lodestone['item']['icon'])) { + $bindings[':itemicon'] = [NULL, 'null']; + } else { + $bindings[':itemicon'] = self::removeLodestoneDomain($this->lodestone['item']['icon']); #Download icon - $webp = Images::download($this->lodestone['icon'], Config::$icons.$bindings[':icon']); + $webp = Images::download($this->lodestone['item']['icon'], Config::$icons.$bindings[':itemicon']); if ($webp) { - $bindings[':icon'] = str_replace('.png', '.webp', $bindings[':icon']); - } - $bindings[':points'] = $this->lodestone['points']; - $bindings[':category'] = $this->lodestone['category']; - $bindings[':subcategory'] = $this->lodestone['subcategory']; - if (empty($this->lodestone['howto'])) { - $bindings[':howto'] = [NULL, 'null']; - } else { - $bindings[':howto'] = $this->lodestone['howto']; - } - if (empty($this->lodestone['title'])) { - $bindings[':title'] = [NULL, 'null']; - } else { - $bindings[':title'] = $this->lodestone['title']; - } - if (empty($this->lodestone['item']['name'])) { - $bindings[':item'] = [NULL, 'null']; - } else { - $bindings[':item'] = $this->lodestone['item']['name']; - } - if (empty($this->lodestone['item']['icon'])) { - $bindings[':itemicon'] = [NULL, 'null']; - } else { - $bindings[':itemicon'] = self::removeLodestoneDomain($this->lodestone['item']['icon']); - #Download icon - $webp = Images::download($this->lodestone['item']['icon'], Config::$icons.$bindings[':itemicon']); - if ($webp) { - $bindings[':itemicon'] = str_replace('.png', '.webp', $bindings[':itemicon']); - } - } - if (empty($this->lodestone['item']['id'])) { - $bindings[':itemid'] = [NULL, 'null']; - } else { - $bindings[':itemid'] = $this->lodestone['item']['id']; - } - #Eggstreme Hunting is a duplicate name for Legacy achievement (ID 500) and for current one (ID 903). - #But current seasonal achievements do not have viewable page in Lodestone Database for some reason. - #Yet DBID is found for current achievement due to... Duplicate name. Which results in unique key violation. - #Since it's supposed to be "invisible" we enforce DBID to be null for it. - if (empty($this->lodestone['dbid']) || $this->id === '903') { - $bindings[':dbid'] = [NULL, 'null']; - } else { - $bindings[':dbid'] = $this->lodestone['dbid']; + $bindings[':itemicon'] = str_replace('.png', '.webp', $bindings[':itemicon']); } + } + if (empty($this->lodestone['item']['id'])) { + $bindings[':itemid'] = [NULL, 'null']; + } else { + $bindings[':itemid'] = $this->lodestone['item']['id']; + } + #Eggstreme Hunting is a duplicate name for Legacy achievement (ID 500) and for current one (ID 903). + #But current seasonal achievements do not have viewable page in Lodestone Database for some reason. + #Yet DBID is found for current achievement due to... Duplicate name. Which results in unique key violation. + #Since it's supposed to be "invisible" we enforce DBID to be null for it. + if (empty($this->lodestone['dbid']) || $this->id === '903') { + $bindings[':dbid'] = [NULL, 'null']; + } else { + $bindings[':dbid'] = $this->lodestone['dbid']; + } + try { return HomePage::$dbController->query('INSERT INTO `ffxiv__achievement` SET `achievementid`=:achievementid, `name`=:name, `icon`=:icon, `points`=:points, `category`=:category, `subcategory`=:subcategory, `howto`=:howto, `title`=:title, `item`=:item, `itemicon`=:itemicon, `itemid`=:itemid, `dbid`=:dbid ON DUPLICATE KEY UPDATE `achievementid`=:achievementid, `name`=:name, `icon`=:icon, `points`=:points, `category`=:category, `subcategory`=:subcategory, `howto`=:howto, `title`=:title, `item`=:item, `itemicon`=:itemicon, `itemid`=:itemid, `dbid`=:dbid, `updated`=CURRENT_TIMESTAMP()', $bindings); - } catch(\Exception $e) { + } catch (\Exception $e) { Errors::error_log($e, 'achievementid: '.$this->id); return false; } } - - #To be called from API to allow update only for owned character - public function updateFromApi(): bool|string - { - return $this->update(); - } } \ No newline at end of file diff --git a/lib/Website/fftracker/Entities/Character.php b/lib/Website/fftracker/Entities/Character.php index 108d0a48..619976ae 100644 --- a/lib/Website/fftracker/Entities/Character.php +++ b/lib/Website/fftracker/Entities/Character.php @@ -1,5 +1,6 @@ selectRow('SELECT *, `ffxiv__achievement`.`icon` AS `titleIcon`, `ffxiv__character`.`name`, `ffxiv__character`.`registered`, `ffxiv__character`.`updated`, `ffxiv__enemy`.`name` AS `killedby` FROM `ffxiv__character` LEFT JOIN `ffxiv__clan` ON `ffxiv__character`.`clanid` = `ffxiv__clan`.`clanid` LEFT JOIN `ffxiv__guardian` ON `ffxiv__character`.`guardianid` = `ffxiv__guardian`.`guardianid` LEFT JOIN `ffxiv__nameday` ON `ffxiv__character`.`namedayid` = `ffxiv__nameday`.`namedayid` LEFT JOIN `ffxiv__city` ON `ffxiv__character`.`cityid` = `ffxiv__city`.`cityid` LEFT JOIN `ffxiv__server` ON `ffxiv__character`.`serverid` = `ffxiv__server`.`serverid` LEFT JOIN `ffxiv__grandcompany_rank` ON `ffxiv__character`.`gcrankid` = `ffxiv__grandcompany_rank`.`gcrankid` LEFT JOIN `ffxiv__grandcompany` ON `ffxiv__grandcompany_rank`.`gcId` = `ffxiv__grandcompany`.`gcId` LEFT JOIN `ffxiv__achievement` ON `ffxiv__character`.`titleid` = `ffxiv__achievement`.`achievementid` LEFT JOIN `ffxiv__enemy` ON `ffxiv__character`.`enemyid` = `ffxiv__enemy`.`enemyid` WHERE `ffxiv__character`.`characterid` = :id;', [':id'=>$this->id]); + $data = HomePage::$dbController->selectRow('SELECT *, `ffxiv__achievement`.`icon` AS `titleIcon`, `ffxiv__character`.`name`, `ffxiv__character`.`registered`, `ffxiv__character`.`updated`, `ffxiv__enemy`.`name` AS `killedby` FROM `ffxiv__character` LEFT JOIN `ffxiv__clan` ON `ffxiv__character`.`clanid` = `ffxiv__clan`.`clanid` LEFT JOIN `ffxiv__guardian` ON `ffxiv__character`.`guardianid` = `ffxiv__guardian`.`guardianid` LEFT JOIN `ffxiv__nameday` ON `ffxiv__character`.`namedayid` = `ffxiv__nameday`.`namedayid` LEFT JOIN `ffxiv__city` ON `ffxiv__character`.`cityid` = `ffxiv__city`.`cityid` LEFT JOIN `ffxiv__server` ON `ffxiv__character`.`serverid` = `ffxiv__server`.`serverid` LEFT JOIN `ffxiv__grandcompany_rank` ON `ffxiv__character`.`gcrankid` = `ffxiv__grandcompany_rank`.`gcrankid` LEFT JOIN `ffxiv__grandcompany` ON `ffxiv__grandcompany_rank`.`gcId` = `ffxiv__grandcompany`.`gcId` LEFT JOIN `ffxiv__achievement` ON `ffxiv__character`.`titleid` = `ffxiv__achievement`.`achievementid` LEFT JOIN `ffxiv__enemy` ON `ffxiv__character`.`enemyid` = `ffxiv__enemy`.`enemyid` WHERE `ffxiv__character`.`characterid` = :id;', [':id' => $this->id]); if (!empty($data['privated'])) { foreach ($data as $key => $value) { - if (!in_array($key, ['avatar', 'registered', 'updated', 'deleted', 'privated', 'name'])) { + if (!\in_array($key, ['avatar', 'registered', 'updated', 'deleted', 'privated', 'name'])) { unset($data[$key]); } } @@ -57,15 +63,15 @@ protected function getFromDB(): array $data['username'] = null; } #Get jobs - $data['jobs'] = HomePage::$dbController->selectAll('SELECT `name`, `level`, `last_change` FROM `ffxiv__character_jobs` LEFT JOIN `ffxiv__jobs` ON `ffxiv__character_jobs`.`jobid`=`ffxiv__jobs`.`jobid` WHERE `ffxiv__character_jobs`.`characterid`=:id ORDER BY `name`;', [':id'=>$this->id]); + $data['jobs'] = HomePage::$dbController->selectAll('SELECT `name`, `level`, `last_change` FROM `ffxiv__character_jobs` LEFT JOIN `ffxiv__jobs` ON `ffxiv__character_jobs`.`jobid`=`ffxiv__jobs`.`jobid` WHERE `ffxiv__character_jobs`.`characterid`=:id ORDER BY `name`;', [':id' => $this->id]); #Get old names. For now returning only the count due to cases of bullying, when the old names are learnt. They are still being collected, though for statistical purposes. - $data['oldNames'] = HomePage::$dbController->selectColumn('SELECT `name` FROM `ffxiv__character_names` WHERE `characterid`=:id AND `name`!=:name', [':id'=>$this->id, ':name'=>$data['name']]); + $data['oldNames'] = HomePage::$dbController->selectColumn('SELECT `name` FROM `ffxiv__character_names` WHERE `characterid`=:id AND `name`!=:name', [':id' => $this->id, ':name' => $data['name']]); #Get previous known incarnations (combination of gender and race/clan) - $data['incarnations'] = HomePage::$dbController->selectAll('SELECT `genderid`, `ffxiv__clan`.`race`, `ffxiv__clan`.`clan` FROM `ffxiv__character_clans` LEFT JOIN `ffxiv__clan` ON `ffxiv__character_clans`.`clanid` = `ffxiv__clan`.`clanid` WHERE `ffxiv__character_clans`.`characterid`=:id AND (`ffxiv__character_clans`.`clanid`!=:clanid AND `ffxiv__character_clans`.`genderid`!=:genderid) ORDER BY `genderid` , `race` , `clan` ', [':id'=>$this->id, ':clanid'=>$data['clanid'], ':genderid'=>$data['genderid']]); + $data['incarnations'] = HomePage::$dbController->selectAll('SELECT `genderid`, `ffxiv__clan`.`race`, `ffxiv__clan`.`clan` FROM `ffxiv__character_clans` LEFT JOIN `ffxiv__clan` ON `ffxiv__character_clans`.`clanid` = `ffxiv__clan`.`clanid` WHERE `ffxiv__character_clans`.`characterid`=:id AND (`ffxiv__character_clans`.`clanid`!=:clanid AND `ffxiv__character_clans`.`genderid`!=:genderid) ORDER BY `genderid` , `race` , `clan` ', [':id' => $this->id, ':clanid' => $data['clanid'], ':genderid' => $data['genderid']]); #Get old servers - $data['servers'] = HomePage::$dbController->selectAll('SELECT `ffxiv__server`.`datacenter`, `ffxiv__server`.`server` FROM `ffxiv__character_servers` LEFT JOIN `ffxiv__server` ON `ffxiv__server`.`serverid`=`ffxiv__character_servers`.`serverid` WHERE `ffxiv__character_servers`.`characterid`=:id AND `ffxiv__character_servers`.`serverid` != :serverid ORDER BY `datacenter` , `server` ', [':id'=>$this->id, ':serverid'=>$data['serverid']]); + $data['servers'] = HomePage::$dbController->selectAll('SELECT `ffxiv__server`.`datacenter`, `ffxiv__server`.`server` FROM `ffxiv__character_servers` LEFT JOIN `ffxiv__server` ON `ffxiv__server`.`serverid`=`ffxiv__character_servers`.`serverid` WHERE `ffxiv__character_servers`.`characterid`=:id AND `ffxiv__character_servers`.`serverid` != :serverid ORDER BY `datacenter` , `server` ', [':id' => $this->id, ':serverid' => $data['serverid']]); #Get achievements - $data['achievements'] = HomePage::$dbController->selectAll('SELECT \'achievement\' AS `type`, `ffxiv__achievement`.`achievementid` AS `id`, `ffxiv__achievement`.`category`, `ffxiv__achievement`.`subcategory`, `ffxiv__achievement`.`name`, `time`, `icon` FROM `ffxiv__character_achievement` LEFT JOIN `ffxiv__achievement` ON `ffxiv__character_achievement`.`achievementid`=`ffxiv__achievement`.`achievementid` WHERE `ffxiv__character_achievement`.`characterid` = :id AND `ffxiv__achievement`.`category` IS NOT NULL AND `ffxiv__achievement`.`achievementid` IS NOT NULL ORDER BY `time` DESC, `name` LIMIT 10', [':id'=>$this->id]); + $data['achievements'] = HomePage::$dbController->selectAll('SELECT \'achievement\' AS `type`, `ffxiv__achievement`.`achievementid` AS `id`, `ffxiv__achievement`.`category`, `ffxiv__achievement`.`subcategory`, `ffxiv__achievement`.`name`, `time`, `icon` FROM `ffxiv__character_achievement` LEFT JOIN `ffxiv__achievement` ON `ffxiv__character_achievement`.`achievementid`=`ffxiv__achievement`.`achievementid` WHERE `ffxiv__character_achievement`.`characterid` = :id AND `ffxiv__achievement`.`category` IS NOT NULL AND `ffxiv__achievement`.`achievementid` IS NOT NULL ORDER BY `time` DESC, `name` LIMIT 10', [':id' => $this->id]); #Get affiliated groups' details $data['groups'] = Entity::cleanCrestResults(HomePage::$dbController->selectAll( '(SELECT \'freecompany\' AS `type`, 0 AS `crossworld`, `ffxiv__freecompany_character`.`freecompanyid` AS `id`, `ffxiv__freecompany`.`name` as `name`, `current`, `ffxiv__freecompany_character`.`rankid`, `ffxiv__freecompany_rank`.`rankname` AS `rank`, `crest_part_1`, `crest_part_2`, `crest_part_3`, `grandcompanyid` FROM `ffxiv__freecompany_character` LEFT JOIN `ffxiv__freecompany` ON `ffxiv__freecompany_character`.`freecompanyid`=`ffxiv__freecompany`.`freecompanyid` LEFT JOIN `ffxiv__freecompany_rank` ON `ffxiv__freecompany_rank`.`freecompanyid`=`ffxiv__freecompany`.`freecompanyid` AND `ffxiv__freecompany_character`.`rankid`=`ffxiv__freecompany_rank`.`rankid` WHERE `characterid`=:id) @@ -74,17 +80,23 @@ protected function getFromDB(): array UNION ALL (SELECT \'pvpteam\' AS `type`, 1 AS `crossworld`, `ffxiv__pvpteam_character`.`pvpteamid` AS `id`, `ffxiv__pvpteam`.`name` as `name`, `current`, `ffxiv__pvpteam_character`.`rankid`, `ffxiv__pvpteam_rank`.`rank` AS `rank`, `crest_part_1`, `crest_part_2`, `crest_part_3`, null as `grandcompanyid` FROM `ffxiv__pvpteam_character` LEFT JOIN `ffxiv__pvpteam` ON `ffxiv__pvpteam_character`.`pvpteamid`=`ffxiv__pvpteam`.`pvpteamid` LEFT JOIN `ffxiv__pvpteam_rank` ON `ffxiv__pvpteam_character`.`rankid`=`ffxiv__pvpteam_rank`.`pvprankid` WHERE `characterid`=:id) ORDER BY `current` DESC, `name` ASC;', - [':id'=>$this->id] + [':id' => $this->id] )); #Clean up the data from unnecessary (technical) clutter unset($data['manual'], $data['clanid'], $data['namedayid'], $data['achievementid'], $data['category'], $data['subcategory'], $data['howto'], $data['points'], $data['icon'], $data['item'], $data['itemicon'], $data['itemid'], $data['serverid']); #In case the entry is old enough (at least 1 day old) and register it for update. Also check that this is not a bot. if (empty($_SESSION['UA']['bot']) && (time() - strtotime($data['updated'])) >= 86400) { - #(new TaskInstance())->settingsFromArray(['task' => 'ffUpdateEntity', 'arguments' => [(string)$this->id, 'character'], 'message' => 'Updating character with ID '.$this->id, 'priority' => 1])->add(); + (new TaskInstance())->settingsFromArray(['task' => 'ffUpdateEntity', 'arguments' => [(string)$this->id, 'character'], 'message' => 'Updating character with ID '.$this->id, 'priority' => 1])->add(); } return $data; } - + + /** + * Get character data from Lodestone + * @param bool $allowSleep Whether to wait in case Lodestone throttles the request (that is throttle on our side) + * + * @return string|array + */ public function getFromLodestone(bool $allowSleep = false): string|array { $Lodestone = (new Lodestone()); @@ -119,8 +131,13 @@ public function getFromLodestone(bool $allowSleep = false): string|array $data['404'] = false; return $data; } - - #Function to do processing + + /** + * Function to process data from DB + * @param array $fromDB + * + * @return void + */ protected function process(array $fromDB): void { $this->name = $fromDB['name']; @@ -178,11 +195,18 @@ protected function process(array $fromDB): void $this->achievementPoints = $fromDB['achievement_points'] ?? 0; $this->jobs = $fromDB['jobs'] ?? []; } - - #Function to update the entity + + /** + * Function to update the entity + * @param bool $manual Flag indicating whether character is being added manually + * + * @return bool + */ protected function updateDB(bool $manual = false): bool { try { + #Get time of last update for the character if it exists on tracker + $updated = HomePage::$dbController->selectValue('SELECT `updated` FROM `ffxiv__character` WHERE `characterid`=:characterid', [':characterid' => $this->id]); #If character on Lodestone is not registered in Free Company or PvP Team, add their IDs as NULL for consistency if (empty($this->lodestone['freeCompany']['id'])) { $this->lodestone['freeCompany']['id'] = NULL; @@ -203,7 +227,7 @@ protected function updateDB(bool $manual = false): bool [ ':fcId' => $this->lodestone['freeCompany']['id'], ':fcName' => $this->lodestone['freeCompany']['name'], - ':server'=>$this->lodestone['server'], + ':server' => $this->lodestone['server'], ], ]; } @@ -213,7 +237,7 @@ protected function updateDB(bool $manual = false): bool [ ':pvpId' => $this->lodestone['pvp']['id'], ':pvpName' => $this->lodestone['pvp']['name'], - ':server'=>$this->lodestone['server'], + ':server' => $this->lodestone['server'], ], ]; } @@ -240,22 +264,22 @@ protected function updateDB(bool $manual = false): bool ON DUPLICATE KEY UPDATE `serverid`=(SELECT `serverid` FROM `ffxiv__server` WHERE `server`=:server), `name`=:name, `updated`=CURRENT_TIMESTAMP(), `privated`=NULL, `deleted`=NULL, `enemyid`=NULL, `biography`=:biography, `titleid`=(SELECT `achievementid` as `titleid` FROM `ffxiv__achievement` WHERE `title` IS NOT NULL AND `title`=:title LIMIT 1), `avatar`=:avatar, `clanid`=(SELECT `clanid` FROM `ffxiv__clan` WHERE `clan`=:clan), `genderid`=:genderid, `namedayid`=(SELECT `namedayid` FROM `ffxiv__nameday` WHERE `nameday`=:nameday), `guardianid`=(SELECT `guardianid` FROM `ffxiv__guardian` WHERE `guardian`=:guardian), `cityid`=(SELECT `cityid` FROM `ffxiv__city` WHERE `city`=:city), `gcrankid`=(SELECT `gcrankid` FROM `ffxiv__grandcompany_rank` WHERE `gc_rank` IS NOT NULL AND `gc_rank`=:gcRank ORDER BY `gcrankid` LIMIT 1), `achievement_points`=:achievementPoints;', [ - ':characterid'=>$this->id, - ':server'=>$this->lodestone['server'], - ':name'=>$this->lodestone['name'], - ':manual'=>[$manual, 'bool'], - ':avatar'=>str_replace(['https://img2.finalfantasyxiv.com/f/', 'c0_96x96.jpg', 'c0.jpg'], '', $this->lodestone['avatar']), - ':biography'=>[ + ':characterid' => $this->id, + ':server' => $this->lodestone['server'], + ':name' => $this->lodestone['name'], + ':manual' => [$manual, 'bool'], + ':avatar' => str_replace(['https://img2.finalfantasyxiv.com/f/', 'c0_96x96.jpg', 'c0.jpg'], '', $this->lodestone['avatar']), + ':biography' => [ (empty($this->lodestone['bio']) ? NULL : $this->lodestone['bio']), (empty($this->lodestone['bio']) ? 'null' : 'string'), ], - ':title'=>(empty($this->lodestone['title']) ? '' : $this->lodestone['title']), - ':clan'=>$this->lodestone['clan'], - ':genderid'=>($this->lodestone['gender']==='male' ? '1' : '0'), - ':nameday'=>$this->lodestone['nameday'], - ':guardian'=>$this->lodestone['guardian']['name'], - ':city'=>$this->lodestone['city']['name'], - ':gcRank'=>(empty($this->lodestone['grandCompany']['rank']) ? '' : $this->lodestone['grandCompany']['rank']), + ':title' => (empty($this->lodestone['title']) ? '' : $this->lodestone['title']), + ':clan' => $this->lodestone['clan'], + ':genderid' => ($this->lodestone['gender'] === 'male' ? '1' : '0'), + ':nameday' => $this->lodestone['nameday'], + ':guardian' => $this->lodestone['guardian']['name'], + ':city' => $this->lodestone['city']['name'], + ':gcRank' => (empty($this->lodestone['grandCompany']['rank']) ? '' : $this->lodestone['grandCompany']['rank']), ':achievementPoints' => [$achievementPoints, 'int'] ], ]; @@ -277,32 +301,15 @@ protected function updateDB(bool $manual = false): bool } } } - #Insert server, if it has not been inserted yet. If server is registered at all. - if (HomePage::$dbController->check('SELECT `serverid` FROM `ffxiv__server` WHERE `server`=:server;', [':server' => $this->lodestone['server']]) === true) { - $queries[] = [ - 'INSERT IGNORE INTO `ffxiv__character_servers`(`characterid`, `serverid`) VALUES (:characterid, (SELECT `serverid` FROM `ffxiv__server` WHERE `server`=:server));', - [ - ':characterid' => $this->id, - ':server' => $this->lodestone['server'], - ], - ]; - } - #Insert name, if it has not been inserted yet - $queries[] = [ - 'INSERT IGNORE INTO `ffxiv__character_names`(`characterid`, `name`) VALUES (:characterid, :name);', - [ - ':characterid'=>$this->id, - ':name'=>$this->lodestone['name'], - ], - ]; + $this->insertServerAndName($queries); #Insert race, clan and sex combination, if it has not been inserted yet if (!empty($this->lodestone['clan'])) { $queries[] = [ 'INSERT IGNORE INTO `ffxiv__character_clans`(`characterid`, `genderid`, `clanid`) VALUES (:characterid, :genderid, (SELECT `clanid` FROM `ffxiv__clan` WHERE `clan`=:clan));', [ - ':characterid'=>$this->id, - ':genderid'=>($this->lodestone['gender']==='male' ? '1' : '0'), - ':clan'=>$this->lodestone['clan'], + ':characterid' => $this->id, + ':genderid' => ($this->lodestone['gender'] === 'male' ? '1' : '0'), + ':clan' => $this->lodestone['clan'], ], ]; } @@ -310,8 +317,8 @@ protected function updateDB(bool $manual = false): bool $queries[] = [ 'UPDATE `ffxiv__freecompany_character` SET `current`=0 WHERE `characterid`=:characterid AND `freecompanyid` '.(empty($this->lodestone['freeCompany']['id']) ? 'IS NOT ' : '!= ').' :fcId;', [ - ':characterid'=>$this->id, - ':fcId'=>[ + ':characterid' => $this->id, + ':fcId' => [ $this->lodestone['freeCompany']['id'], (empty($this->lodestone['freeCompany']['id']) ? 'null' : 'string'), ], @@ -321,8 +328,8 @@ protected function updateDB(bool $manual = false): bool $queries[] = [ 'UPDATE `ffxiv__pvpteam_character` SET `current`=0 WHERE `characterid`=:characterid AND `pvpteamid` '.(empty($this->lodestone['pvp']['id']) ? 'IS NOT ' : '!= ').' :pvpId;', [ - ':characterid'=>$this->id, - ':pvpId'=>[ + ':characterid' => $this->id, + ':pvpId' => [ $this->lodestone['pvp']['id'], (empty($this->lodestone['pvp']['id']) ? 'null' : 'string'), ], @@ -330,7 +337,7 @@ protected function updateDB(bool $manual = false): bool ]; #Achievements if (!empty($this->lodestone['achievements']) && is_array($this->lodestone['achievements'])) { - foreach ($this->lodestone['achievements'] as $achievementid=>$item) { + foreach ($this->lodestone['achievements'] as $achievementid => $item) { $icon = self::removeLodestoneDomain($item['icon']); #Download icon, if it's not already present if (is_file(str_replace('.png', '.webp', Config::$icons.$icon))) { @@ -349,26 +356,38 @@ protected function updateDB(bool $manual = false): bool ':points' => $item['points'], ], ]; - $queries[] = [ - 'INSERT INTO `ffxiv__character_achievement` SET `characterid`=:characterid, `achievementid`=:achievementid, `time`=:time ON DUPLICATE KEY UPDATE `time`=:time;', - [ - ':characterid'=>$this->id, - ':achievementid'=>$achievementid, - ':time'=>[$item['time'], 'datetime'], - ], - ]; + #If the achievement is new since the last check, or if this is the first time the character is being processed, add and count the achievement + if (!empty($updated)) { + $queries[] = [ + 'INSERT INTO `ffxiv__character_achievement` SET `characterid`=:characterid, `achievementid`=:achievementid, `time`=:time ON DUPLICATE KEY UPDATE `time`=:time;', + [ + ':characterid' => $this->id, + ':achievementid' => $achievementid, + ':time' => [$item['time'], 'datetime'], + ], + ]; + $queries[] = [ + 'UPDATE `ffxiv__achievement` SET `earnedby`=`earnedby`+1 WHERE `achievementid`=:achievementid;', + [ + ':achievementid' => $achievementid, + ], + ]; + } } } } - #HomePage::$dbController->debug = true; HomePage::$dbController->query($queries); + #Clean achievements, unless this is a new character + if (!empty($updated)) { + $this->cleanAchievements(); + } #Register Free Company update if change was detected if (!empty($this->lodestone['freeCompany']['id']) && HomePage::$dbController->check('SELECT `characterid` FROM `ffxiv__freecompany_character` WHERE `characterid`=:characterid AND `freecompanyid`=:fcID;', [':characterid' => $this->id, ':fcID' => $this->lodestone['freeCompany']['id']]) === false && (new FreeCompany($this->lodestone['freeCompany']['id']))->update() !== true) { - #(new TaskInstance())->settingsFromArray(['task' => 'ffUpdateEntity', 'arguments' => [(string)$this->lodestone['freeCompany']['id'], 'freecompany'], 'message' => 'Updating free company with ID '.$this->lodestone['freeCompany']['id'], 'priority' => 1])->add(); + (new TaskInstance())->settingsFromArray(['task' => 'ffUpdateEntity', 'arguments' => [(string)$this->lodestone['freeCompany']['id'], 'freecompany'], 'message' => 'Updating free company with ID '.$this->lodestone['freeCompany']['id'], 'priority' => 1])->add(); } #Register PvP Team update if change was detected if (!empty($this->lodestone['pvp']['id']) && HomePage::$dbController->check('SELECT `characterid` FROM `ffxiv__pvpteam_character` WHERE `characterid`=:characterid AND `pvpteamid`=:pvpID;', [':characterid' => $this->id, ':pvpID' => $this->lodestone['pvp']['id']]) === false && (new PvPTeam($this->lodestone['pvp']['id']))->update() !== true) { - #(new TaskInstance())->settingsFromArray(['task' => 'ffUpdateEntity', 'arguments' => [(string)$this->lodestone['pvp']['id'], 'pvpteam'], 'message' => 'Updating PvP team with ID '.$this->lodestone['pvp']['id'], 'priority' => 1])->add(); + (new TaskInstance())->settingsFromArray(['task' => 'ffUpdateEntity', 'arguments' => [(string)$this->lodestone['pvp']['id'], 'pvpteam'], 'message' => 'Updating PvP team with ID '.$this->lodestone['pvp']['id'], 'priority' => 1])->add(); } #Check if character is linked to a user $character = HomePage::$dbController->selectRow('SELECT `characterid`, `userid` FROM `ffxiv__character` WHERE `characterid`=:id;', [':id' => $this->id]); @@ -377,13 +396,16 @@ protected function updateDB(bool $manual = false): bool (new User($character['userid']))->addAvatar(false, $this->lodestone['avatar'], (int)$this->id); } return true; - } catch(\Exception $e) { + } catch (\Exception $e) { Errors::error_log($e, 'characterid: '.$this->id); return false; } } - #Function to mark character as private + /** + * Function to mark character as private + * @return bool + */ protected function markPrivate(): bool { try { @@ -399,24 +421,7 @@ protected function markPrivate(): bool ':avatar' => str_replace(['https://img2.finalfantasyxiv.com/f/', 'c0_96x96.jpg', 'c0.jpg'], '', $this->lodestone['avatar']), ], ]; - #Insert server, if it has not been inserted yet. If server is registered at all. - if (HomePage::$dbController->check('SELECT `serverid` FROM `ffxiv__server` WHERE `server`=:server;', [':server' => $this->lodestone['server']]) === true) { - $queries[] = [ - 'INSERT IGNORE INTO `ffxiv__character_servers`(`characterid`, `serverid`) VALUES (:characterid, (SELECT `serverid` FROM `ffxiv__server` WHERE `server`=:server));', - [ - ':characterid' => $this->id, - ':server' => $this->lodestone['server'], - ], - ]; - } - #Insert name, if it has not been inserted yet - $queries[] = [ - 'INSERT IGNORE INTO `ffxiv__character_names`(`characterid`, `name`) VALUES (:characterid, :name);', - [ - ':characterid' => $this->id, - ':name' => $this->lodestone['name'], - ], - ]; + $this->insertServerAndName($queries); return HomePage::$dbController->query($queries); } return HomePage::$dbController->query( @@ -429,7 +434,38 @@ protected function markPrivate(): bool } } - #Function to update the entity + /** + * Extracted function to update server and name of the character + * @param array $queries + * + * @return void + */ + private function insertServerAndName(array &$queries): void + { + #Insert server, if it has not been inserted yet. If server is registered at all. + if (HomePage::$dbController->check('SELECT `serverid` FROM `ffxiv__server` WHERE `server`=:server;', [':server' => $this->lodestone['server']]) === true) { + $queries[] = [ + 'INSERT IGNORE INTO `ffxiv__character_servers`(`characterid`, `serverid`) VALUES (:characterid, (SELECT `serverid` FROM `ffxiv__server` WHERE `server`=:server));', + [ + ':characterid' => $this->id, + ':server' => $this->lodestone['server'], + ], + ]; + } + #Insert name, if it has not been inserted yet + $queries[] = [ + 'INSERT IGNORE INTO `ffxiv__character_names`(`characterid`, `name`) VALUES (:characterid, :name);', + [ + ':characterid' => $this->id, + ':name' => $this->lodestone['name'], + ], + ]; + } + + /** + * Function to update the entity + * @return bool + */ protected function delete(): bool { try { @@ -438,27 +474,27 @@ protected function delete(): bool $queries[] = [ 'UPDATE `ffxiv__freecompany_character` SET `current`=0 WHERE `characterid`=:characterid;', [ - ':characterid'=>$this->id, + ':characterid' => $this->id, ], ]; #Remove from PvP Team $queries[] = [ 'UPDATE `ffxiv__pvpteam_character` SET `current`=0 WHERE `characterid`=:characterid;', [ - ':characterid'=>$this->id, + ':characterid' => $this->id, ], ]; #Remove from Linkshells $queries[] = [ 'UPDATE `ffxiv__linkshell_character` SET `current`=0 WHERE `characterid`=:characterid;', [ - ':characterid'=>$this->id, + ':characterid' => $this->id, ], ]; #Update character $queries[] = [ 'UPDATE `ffxiv__character` SET `deleted` = COALESCE(`deleted`, UTC_DATE()), `enemyid` = COALESCE(`enemyid`, (SELECT `enemyid` FROM `ffxiv__enemy` ORDER BY RAND() LIMIT 1)), `updated`=CURRENT_TIMESTAMP() WHERE `characterid` = :id', - [':id'=>$this->id], + [':id' => $this->id], ]; return HomePage::$dbController->query($queries); } catch (\Throwable $e) { @@ -466,8 +502,11 @@ protected function delete(): bool return false; } } - - #Link user to character + + /** + * Link user to character + * @return array + */ public function linkUser(): array { try { @@ -495,13 +534,13 @@ public function linkUser(): array return ['http_error' => 424, 'reason' => 'No tracker token found for character with id `'.$this->id.'`']; } #Check if ID of the current user is the same as the user who has this token - if (!HomePage::$dbController->check('SELECT `userid` FROM `uc__users` WHERE `userid`=:userid AND `ff_token`=:token;', [':userid'=>$_SESSION['userid'], ':token'=>$token])) { + if (!HomePage::$dbController->check('SELECT `userid` FROM `uc__users` WHERE `userid`=:userid AND `ff_token`=:token;', [':userid' => $_SESSION['userid'], ':token' => $token])) { return ['http_error' => 403, 'reason' => 'Wrong token or user provided']; } #Link character to user $result = HomePage::$dbController->query([ - 'UPDATE `ffxiv__character` SET `userid`=:userid WHERE `characterid`=:characterid;', [':userid'=>$_SESSION['userid'], ':characterid'=>$this->id], - 'INSERT IGNORE INTO `uc__user_to_group` (`userid`, `groupid`) VALUES (:userid, '.\Simbiat\Website\Config::groupsIDs['Linked to FF'].');', [':userid'=>$_SESSION['userid']], + 'UPDATE `ffxiv__character` SET `userid`=:userid WHERE `characterid`=:characterid;', [':userid' => $_SESSION['userid'], ':characterid' => $this->id], + 'INSERT IGNORE INTO `uc__user_to_group` (`userid`, `groupid`) VALUES (:userid, '.Config::groupsIDs['Linked to FF'].');', [':userid' => $_SESSION['userid']], ]); Security::log('User details change', 'Attempted to link FFXIV character', ['id' => $this->id, 'result' => $result]); #Download avatar @@ -512,24 +551,48 @@ public function linkUser(): array } } - #To be called from API to allow update only for owned character - public function updateFromApi(): bool|array|string + /** + * Remove unnecessary achievements + * @return bool + */ + public function cleanAchievements(): bool { - if ($_SESSION['userid'] === 1) { - return ['http_error' => 403, 'reason' => 'Authentication required']; - } - #Check if any character currently registered in a group is linked to the user try { - if (!in_array('refreshAllFF', $_SESSION['permissions'])) { - $check = HomePage::$dbController->check('SELECT `characterid` FROM `ffxiv__character` WHERE `characterid` = :id AND `userid`=:userid', [':id' => $this->id, ':userid' => $_SESSION['userid']]); - if (!$check) { - return ['http_error' => 403, 'reason' => 'Character not linked to user']; - } + #Get list of potential achievements to remove, which is all achievements that besides last 50 and have more than 50 other non-deleted and non-privated characters with it + $potential = HomePage::$dbController->selectColumn( + 'SELECT `achievementid` + FROM ( + SELECT * FROM `ffxiv__character_achievement` WHERE `characterid`=:characterid ORDER BY `time` DESC LIMIT 100000 OFFSET 50 + ) AS `lastAchievements` + WHERE ( + SELECT COUNT(*) AS `count` FROM `ffxiv__character_achievement` + INNER JOIN `ffxiv__character` ON `ffxiv__character_achievement`.`characterid`=`ffxiv__character`.`characterid` + WHERE `achievementid`=`lastAchievements`.`achievementid` AND `ffxiv__character`.`deleted` IS NULL AND `ffxiv__character`.`privated` IS NULL + )>50;', + [':characterid' => $this->id], + ); + #Iterrate over each achievement, and remove them if achievement current character is not one of the last 50 that has the achievement, and that there are still 50 owners of the achievement + foreach ($potential as $achievement) { + HomePage::$dbController->query( + 'DELETE FROM `ffxiv__character_achievement` + WHERE `achievementid`=:achievement AND `characterid`=:characterid AND + ( + SELECT `count` FROM ( + SELECT COUNT(*) AS `count`, GROUP_CONCAT(`characterid`) AS `ids` FROM ( + SELECT `ffxiv__character_achievement`.`characterid` FROM `ffxiv__character_achievement` + INNER JOIN `ffxiv__character` ON `ffxiv__character_achievement`.`characterid`=`ffxiv__character`.`characterid` + WHERE `achievementid`=:achievement AND `ffxiv__character`.`deleted` IS NULL AND `ffxiv__character`.`privated` IS NULL + ORDER BY `time` DESC LIMIT 50 + ) AS `latestCharacters` + ) AS `validation` + WHERE `count`=50 AND NOT FIND_IN_SET(:characterid, `ids`) + )=50;', + [':characterid' => $this->id, ':achievement' => $achievement], + ); } - return $this->update(); - } catch (\Throwable $e) { - Errors::error_log($e, debug: $this->debug); - return ['http_error' => 503, 'reason' => 'Failed to validate linkage']; + } catch (\Throwable $exception) { + Errors::error_log($exception); } + return false; } } \ No newline at end of file diff --git a/lib/Website/fftracker/Entities/FreeCompany.php b/lib/Website/fftracker/Entities/FreeCompany.php index 2d44c60b..78b7c296 100644 --- a/lib/Website/fftracker/Entities/FreeCompany.php +++ b/lib/Website/fftracker/Entities/FreeCompany.php @@ -1,5 +1,6 @@ selectRow('SELECT * FROM `ffxiv__freecompany` LEFT JOIN `ffxiv__server` ON `ffxiv__freecompany`.`serverid`=`ffxiv__server`.`serverid` LEFT JOIN `ffxiv__grandcompany` ON `ffxiv__freecompany`.`grandcompanyid`=`ffxiv__grandcompany`.`gcId` LEFT JOIN `ffxiv__timeactive` ON `ffxiv__freecompany`.`activeid`=`ffxiv__timeactive`.`activeid` LEFT JOIN `ffxiv__estate` ON `ffxiv__freecompany`.`estateid`=`ffxiv__estate`.`estateid` LEFT JOIN `ffxiv__city` ON `ffxiv__estate`.`cityid`=`ffxiv__city`.`cityid` WHERE `freecompanyid`=:id', [':id'=>$this->id]); + $data = HomePage::$dbController->selectRow('SELECT * FROM `ffxiv__freecompany` LEFT JOIN `ffxiv__server` ON `ffxiv__freecompany`.`serverid`=`ffxiv__server`.`serverid` LEFT JOIN `ffxiv__grandcompany` ON `ffxiv__freecompany`.`grandcompanyid`=`ffxiv__grandcompany`.`gcId` LEFT JOIN `ffxiv__timeactive` ON `ffxiv__freecompany`.`activeid`=`ffxiv__timeactive`.`activeid` LEFT JOIN `ffxiv__estate` ON `ffxiv__freecompany`.`estateid`=`ffxiv__estate`.`estateid` LEFT JOIN `ffxiv__city` ON `ffxiv__estate`.`cityid`=`ffxiv__city`.`cityid` WHERE `freecompanyid`=:id', [':id' => $this->id]); #Return empty, if nothing was found if (empty($data)) { return []; } - #Get old names - $data['oldnames'] = HomePage::$dbController->selectColumn('SELECT `name` FROM `ffxiv__freecompany_names` WHERE `freecompanyid`=:id AND `name`!=:name', [':id'=>$this->id, ':name'=>$data['name']]); + $data['oldnames'] = HomePage::$dbController->selectColumn('SELECT `name` FROM `ffxiv__freecompany_names` WHERE `freecompanyid`=:id AND `name`!=:name', [':id' => $this->id, ':name' => $data['name']]); #Get members - $data['members'] = HomePage::$dbController->selectAll('SELECT \'character\' AS `type`, `ffxiv__freecompany_character`.`characterid` AS `id`, `ffxiv__freecompany_rank`.`rankid`, `rankname` AS `rank`, `name`, `ffxiv__character`.`avatar` AS `icon`, `userid` FROM `ffxiv__freecompany_character` LEFT JOIN `ffxiv__freecompany_rank` ON `ffxiv__freecompany_rank`.`rankid`=`ffxiv__freecompany_character`.`rankid` AND `ffxiv__freecompany_rank`.`freecompanyid`=`ffxiv__freecompany_character`.`freecompanyid` LEFT JOIN `ffxiv__character` ON `ffxiv__character`.`characterid`=`ffxiv__freecompany_character`.`characterid` LEFT JOIN (SELECT `rankid`, COUNT(*) AS `total` FROM `ffxiv__freecompany_character` WHERE `ffxiv__freecompany_character`.`freecompanyid`=:id GROUP BY `rankid`) `ranklist` ON `ranklist`.`rankid` = `ffxiv__freecompany_character`.`rankid` WHERE `ffxiv__freecompany_character`.`freecompanyid`=:id AND `current`=1 ORDER BY `ranklist`.`total`, `ranklist`.`rankid` , `ffxiv__character`.`name`;', [':id'=>$this->id]); + $data['members'] = HomePage::$dbController->selectAll('SELECT \'character\' AS `type`, `ffxiv__freecompany_character`.`characterid` AS `id`, `ffxiv__freecompany_rank`.`rankid`, `rankname` AS `rank`, `name`, `ffxiv__character`.`avatar` AS `icon`, `userid` FROM `ffxiv__freecompany_character` LEFT JOIN `ffxiv__freecompany_rank` ON `ffxiv__freecompany_rank`.`rankid`=`ffxiv__freecompany_character`.`rankid` AND `ffxiv__freecompany_rank`.`freecompanyid`=`ffxiv__freecompany_character`.`freecompanyid` LEFT JOIN `ffxiv__character` ON `ffxiv__character`.`characterid`=`ffxiv__freecompany_character`.`characterid` LEFT JOIN (SELECT `rankid`, COUNT(*) AS `total` FROM `ffxiv__freecompany_character` WHERE `ffxiv__freecompany_character`.`freecompanyid`=:id GROUP BY `rankid`) `ranklist` ON `ranklist`.`rankid` = `ffxiv__freecompany_character`.`rankid` WHERE `ffxiv__freecompany_character`.`freecompanyid`=:id AND `current`=1 ORDER BY `ranklist`.`total`, `ranklist`.`rankid` , `ffxiv__character`.`name`;', [':id' => $this->id]); #History of ranks. Ensuring that we get only the freshest 100 entries sorted from latest to newest - $data['ranks_history'] = HomePage::$dbController->selectAll('SELECT `date`, `weekly`, `monthly`, `members` FROM `ffxiv__freecompany_ranking` WHERE `freecompanyid`=:id ORDER BY `date` DESC LIMIT 100;', [':id'=>$this->id]); + $data['ranks_history'] = HomePage::$dbController->selectAll('SELECT `date`, `weekly`, `monthly`, `members` FROM `ffxiv__freecompany_ranking` WHERE `freecompanyid`=:id ORDER BY `date` DESC LIMIT 100;', [':id' => $this->id]); #Clean up the data from unnecessary (technical) clutter unset($data['manual'], $data['gcId'], $data['estateid'], $data['gc_icon'], $data['activeid'], $data['cityid'], $data['left'], $data['top'], $data['cityicon']); #In case the entry is old enough (at least 1 day old) and register it for update. Also check that this is not a bot. if (empty($_SESSION['UA']['bot']) && (time() - strtotime($data['updated'])) >= 86400) { - #(new TaskInstance())->settingsFromArray(['task' => 'ffUpdateEntity', 'arguments' => [(string)$this->id, 'freecompany'], 'message' => 'Updating free company with ID '.$this->id, 'priority' => 1])->add(); + (new TaskInstance())->settingsFromArray(['task' => 'ffUpdateEntity', 'arguments' => [(string)$this->id, 'freecompany'], 'message' => 'Updating free company with ID '.$this->id, 'priority' => 1])->add(); } return $data; } - + + /** + * Get data from Lodestone + * @param bool $allowSleep Whether to wait in case Lodestone throttles the request (that is throttle on our side) + * + * @return string|array + */ public function getFromLodestone(bool $allowSleep = false): string|array { - $Lodestone = (new Lodestone); + $Lodestone = new Lodestone(); $data = $Lodestone->getFreeCompany($this->id)->getFreeCompanyMembers($this->id, 0)->getResult(); if (empty($data['freecompanies'][$this->id]['server']) || (empty($data['freecompanies'][$this->id]['members']) && (int)($data['freecompanies'][$this->id]['members_count'] ?? 0) > 0) || (!empty($data['freecompanies'][$this->id]['members']) && count($data['freecompanies'][$this->id]['members']) < (int)($data['freecompanies'][$this->id]['members_count'] ?? 0))) { if (!empty($data['freecompanies'][$this->id]) && (int)$data['freecompanies'][$this->id] === 404) { @@ -87,8 +98,13 @@ public function getFromLodestone(bool $allowSleep = false): string|array $data['404'] = false; return $data; } - - #Function to do processing + + /** + * Function to process data from DB + * @param array $fromDB + * + * @return void + */ protected function process(array $fromDB): void { $this->name = $fromDB['name']; @@ -145,7 +161,7 @@ protected function process(array $fromDB): void $this->oldNames = $fromDB['oldnames']; $this->ranking = $fromDB['ranks_history']; #Adjust types for ranking - foreach ($this->ranking as $key=>$rank) { + foreach ($this->ranking as $key => $rank) { $this->ranking[$key]['date'] = strtotime($rank['date']); $this->ranking[$key]['weekly'] = (int)$rank['weekly']; $this->ranking[$key]['monthly'] = (int)$rank['monthly']; @@ -153,8 +169,13 @@ protected function process(array $fromDB): void } $this->members = $fromDB['members']; } - - #Function to update the entity + + /** + * Function to update the free company + * @param bool $manual + * + * @return bool + */ protected function updateDB(bool $manual = false): bool { try { @@ -169,67 +190,67 @@ protected function updateDB(bool $manual = false): bool `freecompanyid`, `name`, `manual`, `serverid`, `formed`, `registered`, `updated`, `deleted`, `grandcompanyid`, `tag`, `crest_part_1`, `crest_part_2`, `crest_part_3`, `rank`, `slogan`, `activeid`, `recruitment`, `communityid`, `estate_zone`, `estateid`, `estate_message`, `Role-playing`, `Leveling`, `Casual`, `Hardcore`, `Dungeons`, `Guildhests`, `Trials`, `Raids`, `PvP`, `Tank`, `Healer`, `DPS`, `Crafter`, `Gatherer` ) VALUES ( - :freecompanyid, :name, :manual, (SELECT `serverid` FROM `ffxiv__server` WHERE `server`=:server), :formed, UTC_DATE(), CURRENT_TIMESTAMP(), NULL, (SELECT `gcId` FROM `ffxiv__grandcompany` WHERE `gcName`=:grandCompany), :tag, :crest_part_1, :crest_part_2, :crest_part_3, :rank, :slogan, (SELECT `activeid` FROM `ffxiv__timeactive` WHERE `active`=:active AND `active` IS NOT NULL LIMIT 1), :recruitment, :communityid, :estate_zone, (SELECT `estateid` FROM `ffxiv__estate` WHERE CONCAT(\'Plot \', `plot`, \', \', `ward`, \' Ward, \', `area`, \' (\', CASE WHEN `size` = 1 THEN \'Small\' WHEN `size` = 2 THEN \'Medium\' WHEN `size` = 3 THEN \'Large\' END, \')\')=:estate_address LIMIT 1), :estate_message, :rolePlaying, :leveling, :casual, :hardcore, :dungeons, :guildhests, :trials, :raids, :pvp, :tank, :healer, :dps, :crafter, :gatherer + :freecompanyid, :name, :manual, (SELECT `serverid` FROM `ffxiv__server` WHERE `server`=:server), :formed, UTC_DATE(), CURRENT_TIMESTAMP(), NULL, (SELECT `gcId` FROM `ffxiv__grandcompany` WHERE `gcName`=:grandCompany), :tag, :crest_part_1, :crest_part_2, :crest_part_3, :rank, :slogan, (SELECT `activeid` FROM `ffxiv__timeactive` WHERE `active`=:active LIMIT 1), :recruitment, :communityid, :estate_zone, (SELECT `estateid` FROM `ffxiv__estate` WHERE CONCAT(\'Plot \', `plot`, \', \', `ward`, \' Ward, \', `area`, \' (\', CASE WHEN `size` = 1 THEN \'Small\' WHEN `size` = 2 THEN \'Medium\' WHEN `size` = 3 THEN \'Large\' END, \')\')=:estate_address LIMIT 1), :estate_message, :rolePlaying, :leveling, :casual, :hardcore, :dungeons, :guildhests, :trials, :raids, :pvp, :tank, :healer, :dps, :crafter, :gatherer ) ON DUPLICATE KEY UPDATE `name`=:name, `serverid`=(SELECT `serverid` FROM `ffxiv__server` WHERE `server`=:server), `formed`=:formed, `updated`=CURRENT_TIMESTAMP(), `deleted`=NULL, `grandcompanyid`=(SELECT `gcId` FROM `ffxiv__grandcompany` WHERE `gcName`=:grandCompany), `tag`=:tag, `crest_part_1`=:crest_part_1, `crest_part_2`=:crest_part_2, `crest_part_3`=:crest_part_3, `rank`=:rank, `slogan`=:slogan, `activeid`=(SELECT `activeid` FROM `ffxiv__timeactive` WHERE `active`=:active AND `active` IS NOT NULL LIMIT 1), `recruitment`=:recruitment, `communityid`=:communityid, `estate_zone`=:estate_zone, `estateid`=(SELECT `estateid` FROM `ffxiv__estate` WHERE CONCAT(\'Plot \', `plot`, \', \', `ward`, \' Ward, \', `area`, \' (\', CASE WHEN `size` = 1 THEN \'Small\' WHEN `size` = 2 THEN \'Medium\' WHEN `size` = 3 THEN \'Large\' END, \')\')=:estate_address LIMIT 1), `estate_message`=:estate_message, `Role-playing`=:rolePlaying, `Leveling`=:leveling, `Casual`=:casual, `Hardcore`=:hardcore, `Dungeons`=:dungeons, `Guildhests`=:guildhests, `Trials`=:trials, `Raids`=:raids, `PvP`=:pvp, `Tank`=:tank, `Healer`=:healer, `DPS`=:dps, `Crafter`=:crafter, `Gatherer`=:gatherer;', [ - ':freecompanyid'=>$this->id, - ':name'=>$this->lodestone['name'], - ':manual'=>[$manual, 'bool'], - ':server'=>$this->lodestone['server'], - ':formed'=>[$this->lodestone['formed'], 'date'], - ':grandCompany'=>$this->lodestone['grandCompany'], - ':tag'=>$this->lodestone['tag'], - ':crest_part_1'=>[ + ':freecompanyid' => $this->id, + ':name' => $this->lodestone['name'], + ':manual' => [$manual, 'bool'], + ':server' => $this->lodestone['server'], + ':formed' => [$this->lodestone['formed'], 'date'], + ':grandCompany' => $this->lodestone['grandCompany'], + ':tag' => $this->lodestone['tag'], + ':crest_part_1' => [ (empty($this->lodestone['crest'][0]) ? NULL : $this->lodestone['crest'][0]), (empty($this->lodestone['crest'][0]) ? 'null' : 'string'), ], - ':crest_part_2'=>[ + ':crest_part_2' => [ (empty($this->lodestone['crest'][1]) ? NULL : $this->lodestone['crest'][1]), (empty($this->lodestone['crest'][1]) ? 'null' : 'string'), ], - ':crest_part_3'=>[ + ':crest_part_3' => [ (empty($this->lodestone['crest'][2]) ? NULL : $this->lodestone['crest'][2]), (empty($this->lodestone['crest'][2]) ? 'null' : 'string'), ], - ':rank'=>$this->lodestone['rank'], - ':slogan'=>[ + ':rank' => $this->lodestone['rank'], + ':slogan' => [ (empty($this->lodestone['slogan']) ? NULL : Sanitization::sanitizeHTML($this->lodestone['slogan'])), (empty($this->lodestone['slogan']) ? 'null' : 'string'), ], - ':active'=>[ + ':active' => [ (empty($this->lodestone['active']) ? NULL : $this->lodestone['active']), (empty($this->lodestone['active']) ? 'null' : 'string'), ], - ':recruitment'=>(strcasecmp($this->lodestone['recruitment'], 'Open') === 0 ? 1 : 0), - ':estate_zone'=>[ + ':recruitment' => (strcasecmp($this->lodestone['recruitment'], 'Open') === 0 ? 1 : 0), + ':estate_zone' => [ (empty($this->lodestone['estate']['name']) ? NULL : $this->lodestone['estate']['name']), (empty($this->lodestone['estate']['name']) ? 'null' : 'string'), ], - ':estate_address'=>[ + ':estate_address' => [ (empty($this->lodestone['estate']['address']) ? NULL : $this->lodestone['estate']['address']), (empty($this->lodestone['estate']['address']) ? 'null' : 'string'), ], - ':estate_message'=>[ + ':estate_message' => [ (empty($this->lodestone['estate']['greeting']) ? NULL : Sanitization::sanitizeHTML($this->lodestone['estate']['greeting'])), (empty($this->lodestone['estate']['greeting']) ? 'null' : 'string'), ], - ':rolePlaying'=>(empty($this->lodestone['focus']) ? 0 : $this->lodestone['focus'][ array_search('Role-playing', array_column($this->lodestone['focus'], 'name'), true) ]['enabled']), - ':leveling'=>(empty($this->lodestone['focus']) ? 0 : $this->lodestone['focus'][ array_search('Leveling', array_column($this->lodestone['focus'], 'name'), true) ]['enabled']), - ':casual'=>(empty($this->lodestone['focus']) ? 0 : $this->lodestone['focus'][ array_search('Casual', array_column($this->lodestone['focus'], 'name'), true) ]['enabled']), - ':hardcore'=>(empty($this->lodestone['focus']) ? 0 : $this->lodestone['focus'][ array_search('Hardcore', array_column($this->lodestone['focus'], 'name'), true) ]['enabled']), - ':dungeons'=>(empty($this->lodestone['focus']) ? 0 : $this->lodestone['focus'][ array_search('Dungeons', array_column($this->lodestone['focus'], 'name'), true) ]['enabled']), - ':guildhests'=>(empty($this->lodestone['focus']) ? 0 : $this->lodestone['focus'][ array_search('Guildhests', array_column($this->lodestone['focus'], 'name'), true) ]['enabled']), - ':trials'=>(empty($this->lodestone['focus']) ? 0 : $this->lodestone['focus'][ array_search('Trials', array_column($this->lodestone['focus'], 'name'), true) ]['enabled']), - ':raids'=>(empty($this->lodestone['focus']) ? 0 : $this->lodestone['focus'][ array_search('Raids', array_column($this->lodestone['focus'], 'name'), true) ]['enabled']), - ':pvp'=>(empty($this->lodestone['focus']) ? 0 : $this->lodestone['focus'][ array_search('PvP', array_column($this->lodestone['focus'], 'name'), true) ]['enabled']), - ':tank'=>(empty($this->lodestone['seeking']) ? 0 : $this->lodestone['seeking'][ array_search('Tank', array_column($this->lodestone['seeking'], 'name'), true) ]['enabled']), - ':healer'=>(empty($this->lodestone['seeking']) ? 0 : $this->lodestone['seeking'][ array_search('Healer', array_column($this->lodestone['seeking'], 'name'), true) ]['enabled']), - ':dps'=>(empty($this->lodestone['seeking']) ? 0 : $this->lodestone['seeking'][ array_search('DPS', array_column($this->lodestone['seeking'], 'name'), true) ]['enabled']), - ':crafter'=>(empty($this->lodestone['seeking']) ? 0 : $this->lodestone['seeking'][ array_search('Crafter', array_column($this->lodestone['seeking'], 'name'), true) ]['enabled']), - ':gatherer'=>(empty($this->lodestone['seeking']) ? 0 : $this->lodestone['seeking'][ array_search('Gatherer', array_column($this->lodestone['seeking'], 'name'), true) ]['enabled']), - ':communityid'=>[ + ':rolePlaying' => (empty($this->lodestone['focus']) ? 0 : $this->lodestone['focus'][array_search('Role-playing', array_column($this->lodestone['focus'], 'name'), true)]['enabled']), + ':leveling' => (empty($this->lodestone['focus']) ? 0 : $this->lodestone['focus'][array_search('Leveling', array_column($this->lodestone['focus'], 'name'), true)]['enabled']), + ':casual' => (empty($this->lodestone['focus']) ? 0 : $this->lodestone['focus'][array_search('Casual', array_column($this->lodestone['focus'], 'name'), true)]['enabled']), + ':hardcore' => (empty($this->lodestone['focus']) ? 0 : $this->lodestone['focus'][array_search('Hardcore', array_column($this->lodestone['focus'], 'name'), true)]['enabled']), + ':dungeons' => (empty($this->lodestone['focus']) ? 0 : $this->lodestone['focus'][array_search('Dungeons', array_column($this->lodestone['focus'], 'name'), true)]['enabled']), + ':guildhests' => (empty($this->lodestone['focus']) ? 0 : $this->lodestone['focus'][array_search('Guildhests', array_column($this->lodestone['focus'], 'name'), true)]['enabled']), + ':trials' => (empty($this->lodestone['focus']) ? 0 : $this->lodestone['focus'][array_search('Trials', array_column($this->lodestone['focus'], 'name'), true)]['enabled']), + ':raids' => (empty($this->lodestone['focus']) ? 0 : $this->lodestone['focus'][array_search('Raids', array_column($this->lodestone['focus'], 'name'), true)]['enabled']), + ':pvp' => (empty($this->lodestone['focus']) ? 0 : $this->lodestone['focus'][array_search('PvP', array_column($this->lodestone['focus'], 'name'), true)]['enabled']), + ':tank' => (empty($this->lodestone['seeking']) ? 0 : $this->lodestone['seeking'][array_search('Tank', array_column($this->lodestone['seeking'], 'name'), true)]['enabled']), + ':healer' => (empty($this->lodestone['seeking']) ? 0 : $this->lodestone['seeking'][array_search('Healer', array_column($this->lodestone['seeking'], 'name'), true)]['enabled']), + ':dps' => (empty($this->lodestone['seeking']) ? 0 : $this->lodestone['seeking'][array_search('DPS', array_column($this->lodestone['seeking'], 'name'), true)]['enabled']), + ':crafter' => (empty($this->lodestone['seeking']) ? 0 : $this->lodestone['seeking'][array_search('Crafter', array_column($this->lodestone['seeking'], 'name'), true)]['enabled']), + ':gatherer' => (empty($this->lodestone['seeking']) ? 0 : $this->lodestone['seeking'][array_search('Gatherer', array_column($this->lodestone['seeking'], 'name'), true)]['enabled']), + ':communityid' => [ (empty($this->lodestone['communityid']) ? NULL : $this->lodestone['communityid']), (empty($this->lodestone['communityid']) ? 'null' : 'string'), ], @@ -239,8 +260,8 @@ protected function updateDB(bool $manual = false): bool $queries[] = [ 'INSERT IGNORE INTO `ffxiv__freecompany_names`(`freecompanyid`, `name`) VALUES (:freecompanyid, :name);', [ - ':freecompanyid'=>$this->id, - ':name'=>$this->lodestone['name'], + ':freecompanyid' => $this->id, + ':name' => $this->lodestone['name'], ], ]; #Adding ranking @@ -258,7 +279,7 @@ protected function updateDB(bool $manual = false): bool ]; } #Get members as registered on tracker - $trackMembers = HomePage::$dbController->selectColumn('SELECT `characterid` FROM `ffxiv__freecompany_character` WHERE `freecompanyid`=:fcId AND `current`=1;', [':fcId'=>$this->id]); + $trackMembers = HomePage::$dbController->selectColumn('SELECT `characterid` FROM `ffxiv__freecompany_character` WHERE `freecompanyid`=:fcId AND `current`=1;', [':fcId' => $this->id]); #Process members, that left the company foreach ($trackMembers as $member) { #Check if member from tracker is present in Lodestone list @@ -267,26 +288,26 @@ protected function updateDB(bool $manual = false): bool $queries[] = [ 'UPDATE `ffxiv__freecompany_character` SET `current`=0 WHERE `freecompanyid`=:fcId AND `characterid`=:characterid;', [ - ':characterid'=>$member, - ':fcId'=>$this->id, + ':characterid' => $member, + ':fcId' => $this->id, ], ]; } } #Process Lodestone members if (!empty($this->lodestone['members'])) { - foreach ($this->lodestone['members'] as $member=>$details) { + foreach ($this->lodestone['members'] as $member => $details) { #Register or update rank name $queries[] = [ 'INSERT INTO `ffxiv__freecompany_rank` (`freecompanyid`, `rankid`, `rankname`) VALUE (:freecompanyid, :rankid, :rankName) ON DUPLICATE KEY UPDATE `rankname`=:rankName', [ - ':freecompanyid'=>$this->id, - ':rankid'=>$details['rankid'], - ':rankName'=>(empty($details['rank']) ? '' : $details['rank']), + ':freecompanyid' => $this->id, + ':rankid' => $details['rankid'], + ':rankName' => (empty($details['rank']) ? '' : $details['rank']), ], ]; #Check if member is registered on tracker, while saving the status for future use - $this->lodestone['members'][$member]['registered'] = HomePage::$dbController->check('SELECT `characterid` FROM `ffxiv__character` WHERE `characterid`=:characterid', [':characterid'=>$member]); + $this->lodestone['members'][$member]['registered'] = HomePage::$dbController->check('SELECT `characterid` FROM `ffxiv__character` WHERE `characterid`=:characterid', [':characterid' => $member]); if (!$this->lodestone['members'][$member]['registered']) { #Create basic entry of the character $queries[] = [ @@ -309,9 +330,9 @@ protected function updateDB(bool $manual = false): bool $queries[] = [ 'INSERT INTO `ffxiv__freecompany_character` (`freecompanyid`, `characterid`, `rankid`, `current`) VALUES (:fcId, :characterid, :rankid, 1) ON DUPLICATE KEY UPDATE `current`=1, `rankid`=:rankid;', [ - ':characterid'=>$member, - ':fcId'=>$this->id, - ':rankid'=>$details['rankid'], + ':characterid' => $member, + ':fcId' => $this->id, + ':rankid' => $details['rankid'], ], ]; } @@ -323,13 +344,15 @@ protected function updateDB(bool $manual = false): bool $this->charMassCron($this->lodestone['members']); } return true; - } catch(\Exception $e) { + } catch (\Exception $e) { Errors::error_log($e, 'freecompanyid: '.$this->id); return false; } } - - #Function to update the entity + + /** Delete free company + * @return bool + */ protected function delete(): bool { try { diff --git a/lib/Website/fftracker/Entities/Linkshell.php b/lib/Website/fftracker/Entities/Linkshell.php index 4a116e7e..e6906f52 100644 --- a/lib/Website/fftracker/Entities/Linkshell.php +++ b/lib/Website/fftracker/Entities/Linkshell.php @@ -9,6 +9,9 @@ use Simbiat\Website\HomePage; use Simbiat\Lodestone; +/** + * Class representing a FFXIV linkshell (chat group) + */ class Linkshell extends Entity { #Custom properties @@ -21,9 +24,7 @@ class Linkshell extends Entity public array $oldNames = []; public array $members = []; - #Function to get initial data from DB - - /** + /**Function to get initial data from DB * @throws \Exception */ protected function getFromDB(): array @@ -46,14 +47,20 @@ protected function getFromDB(): array #In case the entry is old enough (at least 1 day old) and register it for update. Also check that this is not a bot. if (empty($_SESSION['UA']['bot']) && (time() - strtotime($data['updated'])) >= 86400) { if ((int)$data['crossworld'] === 0) { - #(new TaskInstance())->settingsFromArray(['task' => 'ffUpdateEntity', 'arguments' => [(string)$this->id, 'linkshell'], 'message' => 'Updating linkshell with ID '.$this->id, 'priority' => 1])->add(); + (new TaskInstance())->settingsFromArray(['task' => 'ffUpdateEntity', 'arguments' => [(string)$this->id, 'linkshell'], 'message' => 'Updating linkshell with ID '.$this->id, 'priority' => 1])->add(); } else { - #(new TaskInstance())->settingsFromArray(['task' => 'ffUpdateEntity', 'arguments' => [(string)$this->id, 'crossworldlinkshell'], 'message' => 'Updating crossworld linkshell with ID '.$this->id, 'priority' => 1])->add(); + (new TaskInstance())->settingsFromArray(['task' => 'ffUpdateEntity', 'arguments' => [(string)$this->id, 'crossworldlinkshell'], 'message' => 'Updating crossworld linkshell with ID '.$this->id, 'priority' => 1])->add(); } } return $data; } + /** + * Get linkshell data from Lodestone + * @param bool $allowSleep Whether to wait in case Lodestone throttles the request (that is throttle on our side) + * + * @return string|array + */ public function getFromLodestone(bool $allowSleep = false): string|array { $Lodestone = (new Lodestone()); @@ -86,7 +93,12 @@ public function getFromLodestone(bool $allowSleep = false): string|array return $data; } - #Function to do processing + /** + * Function to process data from DB + * @param array $fromDB + * + * @return void + */ protected function process(array $fromDB): void { $this->name = $fromDB['name']; @@ -106,7 +118,12 @@ protected function process(array $fromDB): void } } - #Function to update the entity + /** + * Function to update the entity + * @param bool $manual Flag to indicate that linkshel has been added manually + * + * @return bool + */ protected function updateDB(bool $manual = false): bool { try { @@ -216,7 +233,10 @@ protected function updateDB(bool $manual = false): bool } } - #Function to update the entity + /** + * Delete linkshell + * @return bool + */ protected function delete(): bool { try { diff --git a/lib/Website/fftracker/Entities/PvPTeam.php b/lib/Website/fftracker/Entities/PvPTeam.php index b895071c..0c4b4e5c 100644 --- a/lib/Website/fftracker/Entities/PvPTeam.php +++ b/lib/Website/fftracker/Entities/PvPTeam.php @@ -1,14 +1,17 @@ selectRow('SELECT * FROM `ffxiv__pvpteam` LEFT JOIN `ffxiv__server` ON `ffxiv__pvpteam`.`datacenterid`=`ffxiv__server`.`serverid` WHERE `pvpteamid`=:id', [':id'=>$this->id]); + $data = HomePage::$dbController->selectRow('SELECT * FROM `ffxiv__pvpteam` LEFT JOIN `ffxiv__server` ON `ffxiv__pvpteam`.`datacenterid`=`ffxiv__server`.`serverid` WHERE `pvpteamid`=:id', [':id' => $this->id]); #Return empty, if nothing was found if (empty($data)) { return []; } #Get old names - $data['oldnames'] = HomePage::$dbController->selectColumn('SELECT `name` FROM `ffxiv__pvpteam_names` WHERE `pvpteamid`=:id AND `name`<>:name', [':id'=>$this->id, ':name'=>$data['name']]); + $data['oldnames'] = HomePage::$dbController->selectColumn('SELECT `name` FROM `ffxiv__pvpteam_names` WHERE `pvpteamid`=:id AND `name`<>:name', [':id' => $this->id, ':name' => $data['name']]); #Get members - $data['members'] = HomePage::$dbController->selectAll('SELECT \'character\' AS `type`, `ffxiv__pvpteam_character`.`characterid` AS `id`, `ffxiv__character`.`pvp_matches` AS `matches`, `ffxiv__character`.`name`, `ffxiv__character`.`avatar` AS `icon`, `ffxiv__pvpteam_rank`.`rank`, `ffxiv__pvpteam_rank`.`pvprankid`, `userid` FROM `ffxiv__pvpteam_character` LEFT JOIN `ffxiv__pvpteam_rank` ON `ffxiv__pvpteam_rank`.`pvprankid`=`ffxiv__pvpteam_character`.`rankid` LEFT JOIN `ffxiv__character` ON `ffxiv__pvpteam_character`.`characterid`=`ffxiv__character`.`characterid` WHERE `ffxiv__pvpteam_character`.`pvpteamid`=:id AND `current`=1 ORDER BY `ffxiv__pvpteam_character`.`rankid` , `ffxiv__character`.`name` ', [':id'=>$this->id]); + $data['members'] = HomePage::$dbController->selectAll('SELECT \'character\' AS `type`, `ffxiv__pvpteam_character`.`characterid` AS `id`, `ffxiv__character`.`pvp_matches` AS `matches`, `ffxiv__character`.`name`, `ffxiv__character`.`avatar` AS `icon`, `ffxiv__pvpteam_rank`.`rank`, `ffxiv__pvpteam_rank`.`pvprankid`, `userid` FROM `ffxiv__pvpteam_character` LEFT JOIN `ffxiv__pvpteam_rank` ON `ffxiv__pvpteam_rank`.`pvprankid`=`ffxiv__pvpteam_character`.`rankid` LEFT JOIN `ffxiv__character` ON `ffxiv__pvpteam_character`.`characterid`=`ffxiv__character`.`characterid` WHERE `ffxiv__pvpteam_character`.`pvpteamid`=:id AND `current`=1 ORDER BY `ffxiv__pvpteam_character`.`rankid` , `ffxiv__character`.`name` ', [':id' => $this->id]); #Clean up the data from unnecessary (technical) clutter unset($data['manual'], $data['datacenterid'], $data['serverid'], $data['server']); #In case the entry is old enough (at least 1 day old) and register it for update. Also check that this is not a bot. if (empty($_SESSION['UA']['bot']) && (time() - strtotime($data['updated'])) >= 86400) { - #(new TaskInstance())->settingsFromArray(['task' => 'ffUpdateEntity', 'arguments' => [(string)$this->id, 'pvpteam'], 'message' => 'Updating PvP team with ID '.$this->id, 'priority' => 1])->add(); + (new TaskInstance())->settingsFromArray(['task' => 'ffUpdateEntity', 'arguments' => [(string)$this->id, 'pvpteam'], 'message' => 'Updating PvP team with ID '.$this->id, 'priority' => 1])->add(); } return $data; } - + + /** + * Get PvP team data from Lodestone + * @param bool $allowSleep Whether to wait in case Lodestone throttles the request (that is throttle on our side) + * + * @return string|array + */ public function getFromLodestone(bool $allowSleep = false): string|array { - $Lodestone = (new Lodestone); + $Lodestone = new Lodestone(); $data = $Lodestone->getPvPTeam($this->id)->getResult(); if (empty($data['pvpteams'][$this->id]['dataCenter']) || empty($data['pvpteams'][$this->id]['members'])) { if (!empty($data['pvpteams'][$this->id]['members']) && (int)$data['pvpteams'][$this->id]['members'] === 404) { @@ -77,8 +86,13 @@ public function getFromLodestone(bool $allowSleep = false): string|array unset($data['pageCurrent'], $data['pageTotal']); return $data; } - - #Function to do processing + + /** + * Process data from DB + * @param array $fromDB + * + * @return void + */ protected function process(array $fromDB): void { $this->name = $fromDB['name']; @@ -97,12 +111,17 @@ protected function process(array $fromDB): void $this->dataCenter = $fromDB['datacenter']; $this->oldNames = $fromDB['oldnames']; $this->members = $fromDB['members']; - foreach ($this->members as $key=>$member) { + foreach ($this->members as $key => $member) { $this->members[$key]['matches'] = (int)$member['matches']; } } - - #Function to update the entity + + /** + * Function to update the entity + * @param bool $manual Flag indicating that PvP team is being added manually + * + * @return bool + */ protected function updateDB(bool $manual = false): bool { try { @@ -112,24 +131,24 @@ protected function updateDB(bool $manual = false): bool $queries[] = [ 'INSERT INTO `ffxiv__pvpteam` (`pvpteamid`, `name`, `manual`, `formed`, `registered`, `updated`, `deleted`, `datacenterid`, `communityid`, `crest_part_1`, `crest_part_2`, `crest_part_3`) VALUES (:pvpteamid, :name, :manual, :formed, UTC_DATE(), CURRENT_TIMESTAMP(), NULL, (SELECT `serverid` FROM `ffxiv__server` WHERE `datacenter`=:datacenter ORDER BY `serverid` LIMIT 1), :communityid, :crest_part_1, :crest_part_2, :crest_part_3) ON DUPLICATE KEY UPDATE `name`=:name, `formed`=:formed, `updated`=CURRENT_TIMESTAMP(), `deleted`=NULL, `datacenterid`=(SELECT `serverid` FROM `ffxiv__server` WHERE `datacenter`=:datacenter ORDER BY `serverid` LIMIT 1), `communityid`=:communityid, `crest_part_1`=:crest_part_1, `crest_part_2`=:crest_part_2, `crest_part_3`=:crest_part_3;', [ - ':pvpteamid'=>$this->id, - ':datacenter'=>$this->lodestone['dataCenter'], - ':name'=>$this->lodestone['name'], - ':manual'=>[$manual, 'bool'], - ':formed'=>[$this->lodestone['formed'], 'date'], - ':communityid'=>[ + ':pvpteamid' => $this->id, + ':datacenter' => $this->lodestone['dataCenter'], + ':name' => $this->lodestone['name'], + ':manual' => [$manual, 'bool'], + ':formed' => [$this->lodestone['formed'], 'date'], + ':communityid' => [ (empty($this->lodestone['communityid']) ? NULL : $this->lodestone['communityid']), (empty($this->lodestone['communityid']) ? 'null' : 'string'), ], - ':crest_part_1'=>[ + ':crest_part_1' => [ (empty($this->lodestone['crest'][0]) ? NULL : $this->lodestone['crest'][0]), (empty($this->lodestone['crest'][0]) ? 'null' : 'string'), ], - ':crest_part_2'=>[ + ':crest_part_2' => [ (empty($this->lodestone['crest'][1]) ? NULL : $this->lodestone['crest'][1]), (empty($this->lodestone['crest'][1]) ? 'null' : 'string'), ], - ':crest_part_3'=>[ + ':crest_part_3' => [ (empty($this->lodestone['crest'][2]) ? NULL : $this->lodestone['crest'][2]), (empty($this->lodestone['crest'][2]) ? 'null' : 'string'), ], @@ -139,12 +158,12 @@ protected function updateDB(bool $manual = false): bool $queries[] = [ 'INSERT IGNORE INTO `ffxiv__pvpteam_names`(`pvpteamid`, `name`) VALUES (:pvpteamid, :name);', [ - ':pvpteamid'=>$this->id, - ':name'=>$this->lodestone['name'], + ':pvpteamid' => $this->id, + ':name' => $this->lodestone['name'], ], ]; #Get members as registered on tracker - $trackMembers = HomePage::$dbController->selectColumn('SELECT `characterid` FROM `ffxiv__pvpteam_character` WHERE `pvpteamid`=:pvpteamid AND `current`=1;', [':pvpteamid'=>$this->id]); + $trackMembers = HomePage::$dbController->selectColumn('SELECT `characterid` FROM `ffxiv__pvpteam_character` WHERE `pvpteamid`=:pvpteamid AND `current`=1;', [':pvpteamid' => $this->id]); #Process members, that left the team foreach ($trackMembers as $member) { #Check if member from tracker is present in Lodestone list @@ -153,17 +172,17 @@ protected function updateDB(bool $manual = false): bool $queries[] = [ 'UPDATE `ffxiv__pvpteam_character` SET `current`=0 WHERE `pvpteamid`=:pvpId AND `characterid`=:characterid;', [ - ':characterid'=>$member, - ':pvpId'=>$this->id, + ':characterid' => $member, + ':pvpId' => $this->id, ], ]; } } #Process Lodestone members if (!empty($this->lodestone['members'])) { - foreach ($this->lodestone['members'] as $member=>$details) { + foreach ($this->lodestone['members'] as $member => $details) { #Check if member is registered on tracker, while saving the status for future use - $this->lodestone['members'][$member]['registered'] = HomePage::$dbController->check('SELECT `characterid` FROM `ffxiv__character` WHERE `characterid`=:characterid', [':characterid'=>$member]); + $this->lodestone['members'][$member]['registered'] = HomePage::$dbController->check('SELECT `characterid` FROM `ffxiv__character` WHERE `characterid`=:characterid', [':characterid' => $member]); if (!$this->lodestone['members'][$member]['registered']) { #Create basic entry of the character $queries[] = [ @@ -174,12 +193,12 @@ protected function updateDB(bool $manual = false): bool :characterid, (SELECT `serverid` FROM `ffxiv__server` WHERE `server`=:server), :name, UTC_DATE(), TIMESTAMPADD(SECOND, -3600, CURRENT_TIMESTAMP()), :avatar, `gcrankid` = (SELECT `gcrankid` FROM `ffxiv__grandcompany_rank` WHERE `gc_rank`=:gcRank ORDER BY `gcrankid` LIMIT 1), :matches ) ON DUPLICATE KEY UPDATE `deleted`=NULL, `enemyid`=NULL;', [ - ':characterid'=>$member, - ':server'=>$details['server'], - ':name'=>$details['name'], - ':avatar'=>str_replace(['https://img2.finalfantasyxiv.com/f/', 'c0.jpg'], '', $details['avatar']), - ':gcRank'=>(empty($details['grandCompany']['rank']) ? '' : $details['grandCompany']['rank']), - ':matches'=>(empty($details['feasts']) ? 0 : $details['feasts']), + ':characterid' => $member, + ':server' => $details['server'], + ':name' => $details['name'], + ':avatar' => str_replace(['https://img2.finalfantasyxiv.com/f/', 'c0.jpg'], '', $details['avatar']), + ':gcRank' => (empty($details['grandCompany']['rank']) ? '' : $details['grandCompany']['rank']), + ':matches' => (empty($details['feasts']) ? 0 : $details['feasts']), ] ]; } @@ -187,9 +206,9 @@ protected function updateDB(bool $manual = false): bool $queries[] = [ 'INSERT INTO `ffxiv__pvpteam_character` (`pvpteamid`, `characterid`, `rankid`, `current`) VALUES (:pvpteamId, :characterid, (SELECT `pvprankid` FROM `ffxiv__pvpteam_rank` WHERE `rank`=:rank LIMIT 1), 1) ON DUPLICATE KEY UPDATE `current`=1, `rankid`=(SELECT `pvprankid` FROM `ffxiv__pvpteam_rank` WHERE `rank`=:rank AND `rank` IS NOT NULL LIMIT 1);', [ - ':characterid'=>$member, - ':pvpteamId'=>$this->id, - ':rank'=>$details['rank'] ?? 'Member', + ':characterid' => $member, + ':pvpteamId' => $this->id, + ':rank' => $details['rank'] ?? 'Member', ], ]; } @@ -201,13 +220,16 @@ protected function updateDB(bool $manual = false): bool $this->charMassCron($this->lodestone['members']); } return true; - } catch(\Exception $e) { + } catch (\Exception $e) { Errors::error_log($e, 'pvpteamid: '.$this->id); return false; } } - - #Function to update the entity + + /** + * Delete PvP Team + * @return bool + */ protected function delete(): bool { try { @@ -219,7 +241,7 @@ protected function delete(): bool ]; #Update PvP Team $queries[] = [ - 'UPDATE `ffxiv__pvpteam` SET `deleted` = COALESCE(`deleted`, UTC_DATE()), `updated`=CURRENT_TIMESTAMP() WHERE `pvpteamid` = :id', [':id'=>$this->id], + 'UPDATE `ffxiv__pvpteam` SET `deleted` = COALESCE(`deleted`, UTC_DATE()), `updated`=CURRENT_TIMESTAMP() WHERE `pvpteamid` = :id', [':id' => $this->id], ]; return HomePage::$dbController->query($queries); } catch (\Throwable $e) { diff --git a/lib/Website/fftracker/Entity.php b/lib/Website/fftracker/Entity.php index dca30d48..c8606140 100644 --- a/lib/Website/fftracker/Entity.php +++ b/lib/Website/fftracker/Entity.php @@ -11,6 +11,9 @@ use function is_array, sprintf, dirname; +/** + * Generic class for FFXIV entities + */ abstract class Entity extends \Simbiat\Website\Abstracts\Entity { protected const entityType = 'character'; @@ -25,16 +28,34 @@ abstract class Entity extends \Simbiat\Website\Abstracts\Entity */ abstract protected function getFromDB(): array; - #Get entity data from Lodestone + /** + * Get entity data from Lodestone + * @param bool $allowSleep Whether to wait in case Lodestone throttles the request (that is throttle on our side) + * + * @return string|array + */ abstract public function getFromLodestone(bool $allowSleep = false): string|array; - #Function to do processing + /** + * Function to do processing + * @param array $fromDB + * + * @return void + */ abstract protected function process(array $fromDB): void; - #Function to update the entity + /** + * Function to update the entity in DB + * @return bool + */ abstract protected function updateDB(): bool; - #Update the entity + /** + * Update the entity + * @param bool $allowSleep Flag indicating that entityi is being added manually + * + * @return string|bool + */ public function update(bool $allowSleep = false): string|bool { #Check if ID was set @@ -81,7 +102,10 @@ public function update(bool $allowSleep = false): string|bool return $this->updateDB(); } - #To be called from API to allow update only for owned character + /** + * To be called from API to allow entity updates + * @return bool|array|string + */ public function updateFromApi(): bool|array|string { if ($_SESSION['userid'] === 1) { @@ -90,11 +114,20 @@ public function updateFromApi(): bool|array|string if (empty(array_intersect(['refreshOwnedFF', 'refreshAllFF'], $_SESSION['permissions']))) { return ['http_error' => 403, 'reason' => 'No `'.implode('` or `', ['refreshOwnedFF', 'refreshAllFF']).'` permission']; } - #Check if any character currently registered in a group is linked to the user try { - $check = HomePage::$dbController->check('SELECT `'.$this::entityType.'id` FROM `ffxiv__'.$this::entityType.'_character` LEFT JOIN `ffxiv__character` ON `ffxiv__'.$this::entityType.'_character`.`characterid`=`ffxiv__character`.`characterid` WHERE `'.$this::entityType.'id` = :id AND `userid`=:userid', [':id' => $this->id, ':userid' => $_SESSION['userid']]); - if (!$check) { - return ['http_error' => 403, 'reason' => 'Group not linked to user']; + if ($this::entityType !== 'achievement') { + if ($this::entityType === 'character') { + $check = HomePage::$dbController->check('SELECT `characterid` FROM `ffxiv__character` WHERE `characterid` = :id AND `userid`=:userid', [':id' => $this->id, ':userid' => $_SESSION['userid']]); + if (!$check) { + return ['http_error' => 403, 'reason' => 'Character not linked to user']; + } + } else { + #Check if any character currently registered in a group is linked to the user + $check = HomePage::$dbController->check('SELECT `'.$this::entityType.'id` FROM `ffxiv__'.$this::entityType.'_character` LEFT JOIN `ffxiv__character` ON `ffxiv__'.$this::entityType.'_character`.`characterid`=`ffxiv__character`.`characterid` WHERE `'.$this::entityType.'id` = :id AND `userid`=:userid', [':id' => $this->id, ':userid' => $_SESSION['userid']]); + if (!$check) { + return ['http_error' => 403, 'reason' => 'Group not linked to user']; + } + } } return $this->update(); } catch (\Throwable $e) { @@ -103,7 +136,10 @@ public function updateFromApi(): bool|array|string } } - #Register the entity, if it has not been registered already + /** + * Register the entity, if it has not been registered already + * @return bool|int + */ public function register(): bool|int { #Check if ID was set @@ -147,7 +183,12 @@ public function register(): bool|int return $this->updateDB(true); } - #Helper function to add new characters to Cron en masse + /** + * Helper function to add new characters to Cron en masse + * @param array $members + * + * @return void + */ protected function charMassCron(array $members): void { #Cache CRON object @@ -157,7 +198,7 @@ protected function charMassCron(array $members): void if (!$details['registered']) { #Priority is higher, since they are missing a lot of data. try { - #$cron->settingsFromArray(['task' => 'ffUpdateEntity', 'arguments' => [(string)$member, 'character'], 'message' => 'Updating character with ID '.$member, 'priority' => 2])->add(); + $cron->settingsFromArray(['task' => 'ffUpdateEntity', 'arguments' => [(string)$member, 'character'], 'message' => 'Updating character with ID '.$member, 'priority' => 2])->add(); } catch (\Throwable) { #Do nothing, not considered critical } @@ -166,7 +207,12 @@ protected function charMassCron(array $members): void } } - #Function to remove Lodestone domain(s) from image links + /** + * Function to remove Lodestone domain(s) from image links + * @param string $url + * + * @return string + */ public static function removeLodestoneDomain(string $url): string { return str_replace([ @@ -175,7 +221,12 @@ public static function removeLodestoneDomain(string $url): string ], '', $url); } - #Function to download crest components from Lodestone + /** + * Function to download crest components from Lodestone + * @param array $images + * + * @return void + */ protected function downloadCrestComponents(array $images): void { foreach ($images as $key => $image) { @@ -225,6 +276,12 @@ protected function downloadCrestComponents(array $images): void } } + /** + * Function to turn a group crest into a favicon + * @param array $images + * + * @return string|null + */ public static function crestToFavicon(array $images): ?string { $images = self::sortComponents($images); @@ -243,7 +300,12 @@ public static function crestToFavicon(array $images): ?string return '/assets/images/fftracker/merged-crests/not_found.webp'; } - #Function converts image URL to local path + /** + * Function converts image URL to local path + * @param string $image + * + * @return string|null + */ protected static function crestToLocal(string $image): ?string { $filename = basename($image); @@ -263,6 +325,12 @@ protected static function crestToLocal(string $image): ?string return null; } + /** + * Sort crest components + * @param array $images + * + * @return array + */ protected static function sortComponents(array $images): array { $imagesToMerge = []; @@ -284,7 +352,14 @@ protected static function sortComponents(array $images): array return $imagesToMerge; } - #Function to merge 1 to 3 images making up a crest on Lodestone into 1 stored on tracker side + /** + * Function to merge 1 to 3 images making up a crest on Lodestone into 1 stored on tracker side + * @param array $images Array of crest components + * @param string $finalPath Where to save final file + * @param bool $debug Debug mode to log errors + * + * @return bool + */ protected static function CrestMerge(array $images, string $finalPath, bool $debug = false): bool { try { @@ -307,6 +382,12 @@ protected static function CrestMerge(array $images, string $finalPath, bool $deb } } + /** + * Clean component crests to have a proper image, even if they are empty + * @param array $results + * + * @return array + */ public static function cleanCrestResults(array $results): array { foreach ($results as $key => $result) { diff --git a/lib/Website/fftracker/Statistics.php b/lib/Website/fftracker/Statistics.php index eb464e60..9a9f5cd3 100644 --- a/lib/Website/fftracker/Statistics.php +++ b/lib/Website/fftracker/Statistics.php @@ -38,7 +38,7 @@ public function update(#[ExpectedValues(self::statisticsType)] string $type = 'o } #Create path if missing if (!is_dir(Config::$statistics) && !mkdir(Config::$statistics) && !is_dir(Config::$statistics)) { - throw new \RuntimeException(sprintf('Directory "%s" was not created', Config::$statistics)); + throw new \RuntimeException(\sprintf('Directory "%s" was not created', Config::$statistics)); } $cachePath = Config::$statistics.$type.'.json'; #Get Lodestone object for optimization @@ -220,7 +220,11 @@ private function getGroups(array &$data, Controller $dbCon): void private function getAchievements(array &$data, Controller $dbCon): void { #Get achievements statistics - $data['achievements'] = $dbCon->selectAll('SELECT \'achievement\' as `type`, `ffxiv__achievement`.`category`, `ffxiv__achievement`.`achievementid` AS `id`, `ffxiv__achievement`.`icon`, `ffxiv__achievement`.`name` AS `name`, `count` FROM (SELECT `ffxiv__character_achievement`.`achievementid`, count(`ffxiv__character_achievement`.`achievementid`) AS `count` from `ffxiv__character_achievement` GROUP BY `ffxiv__character_achievement`.`achievementid` ORDER BY `count`) `tempresult` INNER JOIN `ffxiv__achievement` ON `tempresult`.`achievementid`=`ffxiv__achievement`.`achievementid` WHERE `ffxiv__achievement`.`category` IS NOT NULL ORDER BY `count`'); + $data['achievements'] = $dbCon->selectAll( + 'SELECT \'achievement\' as `type`, `category`, `achievementid` AS `id`, `icon`, `name`, `earnedby` AS `count` + FROM `ffxiv__achievement` + WHERE `ffxiv__achievement`.`category` IS NOT NULL AND `earnedby`>0 ORDER BY `count`;' + ); #Split achievements by categories $data['achievements'] = ArrayHelpers::splitByKey($data['achievements'], 'category'); #Get only top 20 for each category @@ -416,16 +420,16 @@ private function scheduleBugs(array $data): void #These may be because of temporary issues on parser or Lodestone side, so schedule them for update $cron = new TaskInstance(); foreach ($data['bugs']['noClan'] as $character) { - #$cron->settingsFromArray(['task' => 'ffUpdateEntity', 'arguments' => [(string)$character['id'], 'character'], 'message' => 'Updating character with ID '.$character['id']])->add(); + $cron->settingsFromArray(['task' => 'ffUpdateEntity', 'arguments' => [(string)$character['id'], 'character'], 'message' => 'Updating character with ID '.$character['id']])->add(); } foreach ($data['bugs']['noMembers'] as $group) { - #$cron->settingsFromArray(['task' => 'ffUpdateEntity', 'arguments' => [(string)$group['id'], $group['type']], 'message' => 'Updating group with ID '.$group['id']])->add(); + $cron->settingsFromArray(['task' => 'ffUpdateEntity', 'arguments' => [(string)$group['id'], $group['type']], 'message' => 'Updating group with ID '.$group['id']])->add(); } foreach ($data['bugs']['duplicateNames'] as $servers) { foreach ($servers as $server) { foreach ($server as $names) { foreach ($names as $duplicate) { - #$cron->settingsFromArray(['task' => 'ffUpdateEntity', 'arguments' => [(string)$duplicate['id'], $duplicate['type']], 'message' => 'Updating entity with ID '.$duplicate['id']])->add(); + $cron->settingsFromArray(['task' => 'ffUpdateEntity', 'arguments' => [(string)$duplicate['id'], $duplicate['type']], 'message' => 'Updating entity with ID '.$duplicate['id']])->add(); } } } diff --git a/templates/fftracker/character.twig b/templates/fftracker/character.twig index b9b0eca9..91d11748 100644 --- a/templates/fftracker/character.twig +++ b/templates/fftracker/character.twig @@ -1,4 +1,3 @@ -{% if 1 == 2 %}
{% if not character.avatarID %} @@ -103,5 +102,4 @@
{% endif %} -
-{% endif %} \ No newline at end of file + \ No newline at end of file diff --git a/templates/fftracker/fftracker.twig b/templates/fftracker/fftracker.twig index a1831b72..fee0e6f0 100644 --- a/templates/fftracker/fftracker.twig +++ b/templates/fftracker/fftracker.twig @@ -21,9 +21,6 @@ {% endif %} -
-

Due to data growing so much, that it started affecting the server overall up to a state of not really running (details on GitHub here), data updates have been temporarily stopped. Character pages will not be displayed at all, since it will not be possible to make them private, so treating them all as private temporarily.

-
{% if subServiceName == 'search' %} {{ include('common/elements/search.twig') }} {% endif %}