From be35c6ba17f3b8de4a924d4c9af014ae020f4a53 Mon Sep 17 00:00:00 2001 From: "Justin M. Wray" Date: Tue, 21 Feb 2017 04:31:59 -0500 Subject: [PATCH] Attachments and Links Import/Export, Database Restore, and Control Cleanup (#451) * Attachments and Links Import/Export, Database Restore, and Control Cleanup * Attachments can now be exported and imported. On export, attachments are downloaded into a Tar Gzip and securely extracted on import. * Links and Attachments data is now provided within the Levels export. Users must import both the Level data and the Attachment files to restore the levels with attachments. * A database restore option has been added which utilizes the backed up database content. This overwrites all data in the database. * The Control page has been reorganized to align the various functionality better. * Memcached flushing has been added to all relevant data imports. * Error handling has been added to the various import functions. * * Removed getter function for the Attachment constant. * Switched double quotes with single quotes. --- src/Db.php | 14 +++ src/controllers/AdminController.php | 90 ++++++++++++++----- src/controllers/ajax/AdminAjaxController.php | 20 ++++- .../importers/BinaryImporterController.php | 12 +++ .../modals/ActionModalController.php | 26 +++++- src/models/Attachment.php | 22 +++++ src/models/Control.php | 62 ++++++++++++- src/models/Level.php | 34 ++++++- src/static/js/admin.js | 44 ++++++++- 9 files changed, 297 insertions(+), 27 deletions(-) create mode 100644 src/controllers/importers/BinaryImporterController.php diff --git a/src/Db.php b/src/Db.php index 9fe0e33a..c1a3bf28 100644 --- a/src/Db.php +++ b/src/Db.php @@ -39,6 +39,20 @@ public function getBackupCmd(): string { return $backup_cmd; } + public function getRestoreCmd(): string { + $usr = must_have_idx($this->config, 'DB_USERNAME'); + $pwd = must_have_idx($this->config, 'DB_PASSWORD'); + $db = must_have_idx($this->config, 'DB_NAME'); + $restore_cmd = + 'mysql -u '. + escapeshellarg($usr). + ' --password='. + escapeshellarg($pwd). + ' '. + escapeshellarg($db); + return $restore_cmd; + } + public async function genConnection(): Awaitable { await $this->genConnect(); invariant($this->conn !== null, 'Connection cant be null.'); diff --git a/src/controllers/AdminController.php b/src/controllers/AdminController.php index 2d465561..8f21e726 100644 --- a/src/controllers/AdminController.php +++ b/src/controllers/AdminController.php @@ -1010,10 +1010,16 @@ public function renderControlsContent(): :xhp {
+
@@ -1028,23 +1034,6 @@ class="fb-cta cta--yellow" -
-
-
- - -
-
-
@@ -1072,6 +1061,32 @@ class="fb-cta cta--yellow" +
+
+
+ + +
+
+
+
+
+
+ +
+
+
@@ -1170,6 +1185,41 @@ class="fb-cta cta--yellow" +
+
+
+ + +
+
+
+
+
+
+ +
+
+
+ +
+
+
+

{tr('Categories')}

+
+
diff --git a/src/controllers/ajax/AdminAjaxController.php b/src/controllers/ajax/AdminAjaxController.php index f9461961..d2fea5af 100644 --- a/src/controllers/ajax/AdminAjaxController.php +++ b/src/controllers/ajax/AdminAjaxController.php @@ -124,17 +124,20 @@ protected function getActions(): array { 'pause_game', 'unpause_game', 'reset_game', + 'export_attachments', 'backup_db', 'export_game', 'export_teams', 'export_logos', 'export_levels', 'export_categories', + 'restore_db', 'import_game', 'import_teams', 'import_logos', 'import_levels', 'import_categories', + 'import_attachments', 'flush_memcached', 'reset_database', ); @@ -432,8 +435,11 @@ protected function getActions(): array { case 'unpause_game': await Control::genUnpause(); return Utils::ok_response('Success', 'admin'); + case 'export_attachments': + await Control::exportAttachments(); + return Utils::ok_response('Success', 'admin'); case 'backup_db': - Control::backupDb(); + await Control::backupDb(); return Utils::ok_response('Success', 'admin'); case 'export_game': await Control::exportGame(); @@ -450,6 +456,12 @@ protected function getActions(): array { case 'export_categories': await Control::exportCategories(); return Utils::ok_response('Success', 'admin'); + case 'restore_db': + $result = await Control::restoreDb(); + if ($result) { + return Utils::ok_response('Success', 'admin'); + } + return Utils::error_response('Error importing', 'admin'); case 'import_game': $result = await Control::importGame(); if ($result) { @@ -480,6 +492,12 @@ protected function getActions(): array { return Utils::ok_response('Success', 'admin'); } return Utils::error_response('Error importing', 'admin'); + case 'import_attachments': + $result = await Control::importAttachments(); + if ($result) { + return Utils::ok_response('Success', 'admin'); + } + return Utils::error_response('Error importing', 'admin'); case 'flush_memcached': $result = await Control::genFlushMemcached(); if ($result) { diff --git a/src/controllers/importers/BinaryImporterController.php b/src/controllers/importers/BinaryImporterController.php new file mode 100644 index 00000000..9a87c300 --- /dev/null +++ b/src/controllers/importers/BinaryImporterController.php @@ -0,0 +1,12 @@ +contains($file_name)) { + $input_filename = $file[$file_name]['tmp_name']; + return $input_filename; + } + return false; + } +} diff --git a/src/controllers/modals/ActionModalController.php b/src/controllers/modals/ActionModalController.php index d2caac18..92cbf43b 100644 --- a/src/controllers/modals/ActionModalController.php +++ b/src/controllers/modals/ActionModalController.php @@ -189,14 +189,34 @@ class="fb-cta cta--yellow js-close-modal js-confirm-save">

{tr('Items have been imported successfully')}

; return tuple($title, $content); + case 'restore-database': + $title = +

+ {tr('restore_')}{tr('Database')} +

; + $content = +
+

+ {tr( + 'Are you sure you want to restore the database? This will overwrite ALL existing data!', + )} +

+ +
; + return tuple($title, $content); case 'reset-database': $title =

diff --git a/src/models/Attachment.php b/src/models/Attachment.php index da43d3c5..6bdddcfd 100644 --- a/src/models/Attachment.php +++ b/src/models/Attachment.php @@ -17,6 +17,7 @@ private function __construct( private int $id, private int $levelId, private string $filename, + private string $type, ) {} public function getId(): int { @@ -27,6 +28,10 @@ public function getFilename(): string { return $this->filename; } + public function getType(): string { + return $this->type; + } + public function getLevelId(): int { return $this->levelId; } @@ -265,6 +270,22 @@ public function getLevelId(): int { } } + public static async function genImportAttachments( + int $level_id, + string $filename, + string $type, + ): Awaitable { + $db = await self::genDb(); + await $db->queryf( + 'INSERT INTO attachments (filename, type, level_id, created_ts) VALUES (%s, %s, %d, NOW())', + $filename, + (string) $type, + $level_id, + ); + + return true; + } + private static function attachmentFromRow( Map $row, ): Attachment { @@ -272,6 +293,7 @@ private static function attachmentFromRow( intval(must_have_idx($row, 'id')), intval(must_have_idx($row, 'level_id')), must_have_idx($row, 'filename'), + must_have_idx($row, 'type'), ); } } diff --git a/src/models/Control.php b/src/models/Control.php index c5ae30f6..e6e259d3 100644 --- a/src/models/Control.php +++ b/src/models/Control.php @@ -340,6 +340,7 @@ class Control extends Model { if (!$levels_result) { return false; } + await self::genFlushMemcached(); return true; } return false; @@ -349,6 +350,7 @@ class Control extends Model { $data_teams = JSONImporterController::readJSON('teams_file'); if (is_array($data_teams)) { $teams = must_have_idx($data_teams, 'teams'); + await self::genFlushMemcached(); return await Team::importAll($teams); } return false; @@ -358,6 +360,7 @@ class Control extends Model { $data_logos = JSONImporterController::readJSON('logos_file'); if (is_array($data_logos)) { $logos = must_have_idx($data_logos, 'logos'); + await self::genFlushMemcached(); return await Logo::importAll($logos); } return false; @@ -367,6 +370,7 @@ class Control extends Model { $data_levels = JSONImporterController::readJSON('levels_file'); if (is_array($data_levels)) { $levels = must_have_idx($data_levels, 'levels'); + await self::genFlushMemcached(); return await Level::importAll($levels); } return false; @@ -376,11 +380,55 @@ class Control extends Model { $data_categories = JSONImporterController::readJSON('categories_file'); if (is_array($data_categories)) { $categories = must_have_idx($data_categories, 'categories'); + await self::genFlushMemcached(); return await Category::importAll($categories); } return false; } + public static async function importAttachments(): Awaitable { + $output = array(); + $status = 0; + $filename = + strval(BinaryImporterController::getFilename('attachments_file')); + $document_root = must_have_string(Utils::getSERVER(), 'DOCUMENT_ROOT'); + $directory = $document_root.Attachment::attachmentsDir; + $cmd = "tar -zx -C $directory -f $filename"; + exec($cmd, $output, $status); + if (intval($status) !== 0) { + return false; + } + $directory_files = scandir($directory); + foreach ($directory_files as $file) { + $chmod = chmod($directory.$file, 0600); + invariant( + $chmod === true, + 'Failed to set attachment file permissions to 0600', + ); + } + await self::genFlushMemcached(); + return true; + } + + public static async function restoreDb(): Awaitable { + $output = array(); + $status = 0; + $filename = + strval(BinaryImporterController::getFilename('database_file')); + $cmd = "cat $filename | gunzip - "; + exec($cmd, $output, $status); + if (intval($status) !== 0) { + return false; + } + $cmd = "cat $filename | gunzip - | ".Db::getInstance()->getRestoreCmd(); + exec($cmd, $output, $status); + if (intval($status) !== 0) { + return false; + } + await self::genFlushMemcached(); + return true; + } + public static async function exportGame(): Awaitable { $game = array(); $logos = await Logo::exportAll(); @@ -424,12 +472,24 @@ class Control extends Model { exit(); } - public static function backupDb(): void { + public static async function exportAttachments(): Awaitable { + $filename = 'fbctf-attachments-'.date("d-m-Y").'.tgz'; + header('Content-Type: application/x-tgz'); + header('Content-Disposition: attachment; filename="'.$filename.'"'); + $document_root = must_have_string(Utils::getSERVER(), 'DOCUMENT_ROOT'); + $directory = $document_root.Attachment::attachmentsDir; + $cmd = "tar -cz -C $directory . "; + passthru($cmd); + exit(); + } + + public static async function backupDb(): Awaitable { $filename = 'fbctf-backup-'.date("d-m-Y").'.sql.gz'; header('Content-Type: application/x-gzip'); header('Content-Disposition: attachment; filename="'.$filename.'"'); $cmd = Db::getInstance()->getBackupCmd().' | gzip --best'; passthru($cmd); + exit(); } public static async function genAllActivity( diff --git a/src/models/Level.php b/src/models/Level.php index 994e023e..c86d32dd 100644 --- a/src/models/Level.php +++ b/src/models/Level.php @@ -158,7 +158,7 @@ private static function levelFromRow(Map $row): Level { if (!$exist && $entity_exist && $category_exist) { $entity = await Country::genCountry($entity_iso_code); $category = await Category::genSingleCategoryByName($c); - await self::genCreate( + $level_id = await self::genCreate( $type, $title, must_have_string($level, 'description'), @@ -172,6 +172,23 @@ private static function levelFromRow(Map $row): Level { must_have_string($level, 'hint'), must_have_int($level, 'penalty'), ); + $links = must_have_idx($level, 'links'); + invariant(is_array($links), 'links must be of type array'); + foreach ($links as $link) { + await Link::genCreate($link, $level_id); + } + $attachments = must_have_idx($level, 'attachments'); + invariant( + is_array($attachments), + 'attachments must be of type array', + ); + foreach ($attachments as $attachment) { + await Attachment::genImportAttachments( + $level_id, + $attachment['filename'], + $attachment['type'], + ); + } } } return true; @@ -186,6 +203,19 @@ private static function levelFromRow(Map $row): Level { foreach ($all_levels as $level) { $entity = await Country::gen($level->getEntityId()); $category = await Category::genSingleCategory($level->getCategoryId()); + $links = await Link::genAllLinks($level->getId()); + $link_array = array(); + foreach ($links as $link) { + $link_array[] = $link->getLink(); + } + $attachments = await Attachment::genAllAttachments($level->getId()); + $attachment_array = array(); + foreach ($attachments as $attachment) { + $attachment_array[] = [ + 'filename' => $attachment->getFilename(), + 'type' => $attachment->getType(), + ]; + } $one_level = array( 'type' => $level->getType(), 'title' => $level->getTitle(), @@ -200,6 +230,8 @@ private static function levelFromRow(Map $row): Level { 'flag' => $level->getFlag(), 'hint' => $level->getHint(), 'penalty' => $level->getPenalty(), + 'links' => $link_array, + 'attachments' => $attachment_array, ); array_push($all_levels_data, $one_level); } diff --git a/src/static/js/admin.js b/src/static/js/admin.js index 8570b449..fd68f1dd 100644 --- a/src/static/js/admin.js +++ b/src/static/js/admin.js @@ -408,6 +408,14 @@ function createAnnouncement(section) { } } +//Create and download attachments backup +function attachmentsExport() { + var csrf_token = $('input[name=csrf_token]')[0].value; + var action = 'export_attachments'; + var url = 'index.php?p=admin&ajax=true&action=' + action + '&csrf_token=' + csrf_token; + window.location.href = url; +} + // Create and download database backup function databaseBackup() { var csrf_token = $('input[name=csrf_token]')[0].value; @@ -444,16 +452,31 @@ function submitImport(type_file, action_file) { var responseData = JSON.parse(data); if (responseData.result == 'OK') { console.log('OK'); - Modal.loadPopup('p=action&modal=import-done', 'action-import'); + Modal.loadPopup('p=action&modal=import-done', 'action-import', function() { + var ok_button = $("a[class='fb-cta cta--yellow js-close-modal']"); + ok_button.attr('href', '?p=admin&page=controls'); + ok_button.removeClass('js-close-modal'); + }); } else { console.log('Failed'); Modal.loadPopup('p=action&modal=error', 'action-error', function() { $('.error-text').html('

Sorry there was a problem importing the items. Please try again.

'); + var ok_button = $("a[class='fb-cta cta--yellow js-close-modal']"); + ok_button.attr('href', '?p=admin&page=controls'); + ok_button.removeClass('js-close-modal'); }); } }); } +//Restore and replace database +function databaseRestore() { + $('#restore-database_file').trigger('click'); + $('#restore-database_file').change(function() { + submitImport('database_file', 'restore_db'); + }); +} + // Import and replace whole game function importGame() { $('#import-game_file').trigger('click'); @@ -494,6 +517,14 @@ function importLevels() { }); } +//Import and replace current attachments +function importAttachments() { + $('#import-attachments_file').trigger('click'); + $('#import-attachments_file').change(function() { + submitImport('attachments_file', 'import_attachments'); + }); +} + // Export and download current teams function exportCurrentTeams() { var csrf_token = $('input[name=csrf_token]')[0].value; @@ -1023,6 +1054,8 @@ module.exports = { } } else if (action === 'create-announcement') { createAnnouncement($section); + } else if (action === 'export-attachments') { + attachmentsExport(); } else if (action === 'backup-db') { databaseBackup(); } else if (action === 'import-game') { @@ -1043,6 +1076,8 @@ module.exports = { exportCurrentLogos(); } else if (action === 'import-levels') { importLevels(); + } else if (action === 'import-attachments') { + importAttachments(); } else if (action === 'export-levels') { exportCurrentLevels(); } else if (action === 'import-categories') { @@ -1389,5 +1424,12 @@ module.exports = { }); }); + // prompt restore database + $('.js-restore-database').on('click', function(event) { + event.preventDefault(); + Modal.loadPopup('p=action&modal=restore-database', 'action-restore-database', function() { + $('#restore_database').click(databaseRestore); + }); + }); } };