From f9f69a7c41f8f341664d71b70d3d9b6b0ea19ffd Mon Sep 17 00:00:00 2001 From: Akhil09 Date: Fri, 14 Oct 2016 15:23:10 +0530 Subject: [PATCH 01/18] add hindi translation --- src/language/lang_hi.php | 658 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 658 insertions(+) create mode 100644 src/language/lang_hi.php diff --git a/src/language/lang_hi.php b/src/language/lang_hi.php new file mode 100644 index 00000000..b563040d --- /dev/null +++ b/src/language/lang_hi.php @@ -0,0 +1,658 @@ + + 'H:i:s D d/m/Y', //used by date() function + //Translations for IndexController + 'Facebook CTF' => + 'Facebook CTF', + 'Conquer the world' => + 'Conquereix el món', + 'Play' => + 'Jugar', + 'Welcome to the Facebook Capture the Flag Competition. By clicking "Play," you will be entered into the official CTF challenge. Good luck in your conquest.' => + 'Benvingut al "Capture The Flag" de Facebook. Premeu el botó de "a jugar" per entrar al CTF. Bona sort en la conquesta.', + 'Get ready for the CTF to start and access the gameboard now!' => + 'Prepara\'t a que comenci el CTF i accedeix al taulell de joc ara!', + 'Gameboard' => + 'Taulell de joc', + 'Register Team' => + 'Registrar Equip', + 'Get ready for the CTF to start and register your team now!' => + 'Prepara\'t per començar el CTF i registra el teu equip ara!', + 'Login' => + 'Iniciar Sessió', + 'Soon' => + 'Properament', + 'Upcoming Game' => + 'Proper Joc', + '_days' => + '_dies', + '_hours' => + '_hores', + '_minutes' => + '_minuts', + '_seconds' => + '_segons', + 'Official CTF Rules' => + 'Normativa Oficial del CTF', + 'Following actions are prohibited, unless explicitly told otherwise by event Admins.' => + 'Les següents accions no estan permeses, a no ser que sigui contradit explicitament pels administradors de l\'event.', + 'Rule' => + 'Norma', + 'Cooperation' => + 'Cooperació', + 'No cooperation between teams with independent accounts. Sharing of keys or providing revealing hints to other teams is cheating, don’t do it.' => + 'Sense cooperació entre els equips amb comptes independents. Compartir claus o revelar pistes a altres equips es considera trampa, no ho facis.', + 'Attacking Scoreboard' => + 'Taulell de Puntuacions d\'Atac', + 'No attacking the competition infrastructure. If bugs or vulns are found, please alert the competition organizers immediately.' => + 'No ataquis la infrastructura de la competició. En case de trobar cap vulnerabilitat, siusplay avisa als organitzadors inmediatament', + 'Sabotage' => + 'Sabotatge', + 'Absolutely no sabotaging of other competing teams, or in any way hindering their independent progress.' => + 'No sabotegis altres equips, or de destorbar el seu progrés de forma individual.', + 'Bruteforcing' => + 'Força bruta', + 'No brute forcing of challenge flag/ keys against the scoring site.' => + 'No realitzis força bruta per atacar als serveis', + 'Denial Of Service' => + 'Denegació de Servei', + 'DoSing the CTF platform or any of the challenges is forbidden.' => + 'Realitzar un atac de denegació de servei a la plataforma o a qualsevol de les proves està prohibit', + 'Legal' => + 'Legal', + 'Disclaimer' => + 'Disclaimer', + 'By participating in the contest, you agree to release Facebook and its employees, and the hosting organization from any and all liability, claims or actions of any kind whatsoever for injuries, damages or losses to persons and property which may be sustained in connection with the contest. You acknowledge and agree that Facebook et al is not responsible for technical, hardware or software failures, or other errors or problems which may occur in connection with the contest.' => + 'By participating in the contest, you agree to release Facebook and its employees, and the hosting organization from any and all liability, claims or actions of any kind whatsoever for injuries, damages or losses to persons and property which may be sustained in connection with the contest. You acknowledge and agree that Facebook et al is not responsible for technical, hardware or software failures, or other errors or problems which may occur in connection with the contest.', + 'If you have any questions about what is or is not allowed, please ask an organizer.' => + 'If you have any questions about what is or is not allowed, please ask an organizer.', + 'Have fun!' => + 'Passa-ho be!', + 'Name' => + 'Nom', + 'Email' => + 'Email', + 'Token' => + 'Token', + 'Team Registration' => + 'Registre d\'Equip', + 'Team Name' => + 'Nom de l\'equip', + 'Password' => + 'Contrassenya', + 'Choose an Emblem' => + 'Escollir un emblema', + 'Sign Up' => + 'Donar-se d\'alta', + 'Register to play Capture The Flag here. Once you have registered, you will be logged in.' => + 'Dona\'t d\'alta per a jugar al Capture The Flag aqui. Quan estiguis registrat accediràs a la sessió.', + 'Not Available' => + 'No Disponible', + 'Team Registration will be open soon, stay tuned!' => + 'L\'equip es registrarà aviat, estigueu atents!', + 'Try Again' => + 'Tornar a intentar-ho', + 'Select' => + 'Selecciona', + 'Team Login' => + 'Login de l\'equip', + 'Please login here. If you have not registered, you may do so by clicking "Sign Up" below. ' => + 'Si us plau entreu les vostres credencials aqui. Si no esteu registrats, podeu fer-ho clickant a "Donar-vos d\'alta". ', + 'Team Login will be open soon, stay tuned!' => + 'Team Login will be open soon, stay tuned!', + 'ERROR' => + 'ERROR', + 'Start Over' => + 'Començar de Nou', + 'Window is too small' => + 'La Finestra és massa petita', + 'For the best CTF experience, please make window size bigger.' => + 'Per a una millor experiència es recomenable fer el tamany de la finestra més gran', + 'Thank you.' => + 'Gràcies.', + 'Logout' => + 'Tanca la Sessió', + 'Registration' => + 'Registre', + 'Play CTF' => + 'Jugar al CTF', + 'Rules' => + 'Normes', + //Translations for GameboardController + 'Admin' => + 'Admin', + 'ADMIN' => + 'ADMIN', + 'Navigation' => + 'Navigació', + 'View Mode' => + 'Manera De Visualitzar', + 'View mode' => + 'Mode de Visuatlització', + 'Tutorial' => + 'Tutorial', + 'Scoreboard' => + 'Taulell De Puntuació', + 'You' => + 'Tu', + 'Others' => + 'Altri', + 'All' => + 'All', + 'Leaderboard' => + 'Taulell de Lideratges', + 'Announcements' => + 'Anuncis', + 'Teams' => + 'Equips', + 'Filter' => + 'Filtrar', + 'Activity' => + 'Activitat', + 'Game Clock' => + 'Rellotge del Joc', + //Translations for AdminController + 'Auto' => + 'Automàtic', + 'All Categories' => + 'Totes les Categories', + 'Open' => + 'Obrir', + 'Tokenized' => + 'Tokenized', + 'Hour' => + 'Hora', + 'Hours' => + 'Hores', + 'Used by' => + 'Utilitzat per', + 'Used By' => + 'Fet servir', + 'Available' => + 'Disponible', + 'Registration Tokens' => + 'Registrar Tokens', + 'Create More' => + 'Crear Més', + 'Export Available' => + 'Exportar Disponibles', + 'Not started yet' => + 'Encara no ha començat', + 'Configuration' => + 'Configuració', + 'Tokens' => + 'Tokens', + 'Game Configuration' => + 'Configuració del Joc', + 'OK' => + 'OK', + 'status_' => + 'status_', + 'On' => + 'On', + 'Off' => + 'Off', + 'Player Names' => + 'Noms dels Jugadors', + 'Players Per Team' => + 'Jugadors per Equip', + 'Registration Type' => + 'Tipus de Registre', + 'Strong Passwords' => + 'Contrassenyes Fortes', + 'Team Selection' => + 'Selectió d\'Equip', + 'Game' => + 'Joc', + 'Scoring' => + 'Puntuacions', + 'Progressive Cycle (s)' => + 'Cicles Progressius (s)', + 'Refresh Gameboard' => + 'Actualitza el taulell de joc', + 'Default Bonus' => + 'Bonus per Defecte', + 'Bases Cycle (s)' => + 'Cicle de les Bases (s)', + 'Default Bonus Dec' => + 'Default Bonus Dec', + 'Timer' => + 'Temporitzador', + 'Server Time' => + 'Hora del Servidor', + 'Game Duration' => + 'Duració del Joc', + 'Begin Time' => + 'Hora de començar!', + 'Expected End Time' => + 'S\'esperava la fi del Temps', + 'Language' => + 'Idioma', + 'DELETE' => + 'ESBORRAR', + 'Delete' => + 'Esborrar', + 'No Announcements' => + 'No Hi Ha Anuncis', + 'Game Controls' => + 'Controls del Joc', + 'Write New Announcement here' => + 'Escriu un nou Anunci aqui', + 'Create' => + 'Crear', + 'General' => + 'General', + 'Back Up Database' => + 'Realitzar copia de seguretat eBack Up Database', + 'Export Full Game' => + 'Exportar el Joc Sencer', + 'Import Full Game' => + 'Importar el Joc Sencer', + 'Import Teams' => + 'Importar Equips', + 'Export Teams' => + 'Exportar Equips', + 'Import Logos' => + 'Importar Logos', + 'Export Logos' => + 'Exportar Logos', + 'Import Levels' => + 'Importar Nivells', + 'Export Levels' => + 'Exportar Nivells', + 'Import Categories' => + 'Importar Categories', + 'Export Categories' => + 'Exportar Categories', + 'Levels' => + 'Nivells', + 'New Quiz Level' => + 'Nou nivell del Quiz', + 'Title' => + 'Títol', + 'Question' => + 'Pregunta', + 'Level title' => + 'Nivell_Titol', + 'Quiz question' => + 'Pregunta de Quiz', + 'Country' => + 'Païs', + 'Answer' => + 'Resposta', + 'Points' => + 'Punts', + 'Hint' => + 'Pista', + 'Hint Penalty' => + 'Penalització per Pista', + 'EDIT' => + 'EDITAR', + 'All Quiz Levels' => + 'Tots els Nivells de Quiz', + 'Filter By:' => + 'Filtrar Per:', + 'All Status' => + 'Tots els Estats', + 'Enabled' => + 'Habilitat', + 'Disabled' => + 'Deshabilitat', + 'Quiz Level' => + 'Nivell del Quiz', + 'Show Answer' => + 'Mostrar Resposta', + 'Bonus' => + 'Bonus', + '-Dec' => + '-Dec', + 'Save' => + 'Desar', + 'Quiz Management' => + 'Gestió del Quiz', + 'Add Quiz Level' => + 'Afegir Nivell de Quiz', + 'New Flag Level' => + 'Nova Flag pel Nivell', + 'Description' => + 'Descripció', + 'Level description' => + 'Descripció del Nivell', + 'Category' => + 'Categoria', + 'Flag' => + 'Flag', + 'flag' => + 'flag', + 'All Flag Levels' => + 'Flags de Tots els Nivells', + 'New Attachment:' => + 'Nou Adjunt:', + 'Attachment' => + 'Adjunt', + 'Link' => + 'Enllaç', + 'New Link:' => + 'Nou Enllaç:', + 'Flag Level' => + 'Nivell de Flag', + 'Categories' => + 'Categories', + '+ Attachment' => + '+ Adjunt', + '+ Link' => + '+ Enllaç', + 'Flags Management' => + 'Gestió de Flags', + 'Add Flag Level' => + 'Afegir flags i nivell', + 'New Base Level' => + 'Nou Nivell Base', + 'Keep Points' => + 'Conservar Punts', + 'Capture points' => + 'Capturar punts', + 'All Base Levels' => + 'Tots els nivells Base', + 'Base Level' => + 'Nivell Base', + 'Bases Management' => + 'Bases Management', + 'Add Base Level' => + 'Add Base Level', + 'New Category' => + 'Nova Categoria', + 'Category: ' => + 'Category: ', + 'Categories Management' => + 'Gestió de Categories', + 'Add Category' => + 'Afegir Categoria', + 'All Countries' => + 'Tots els Païssos', + 'In Use' => + 'En us', + 'In use' => + 'En us', + 'Not Used' => + 'No esta Utilitzat', + 'Yes' => + 'Si', + 'No' => + 'No', + 'ISO Code' => + 'Codi ISO', + 'Countries Management' => + 'Gestió de Païssos', + 'No Team Names' => + 'No hi ha noms per l\'equip', + 'time' => + 'time', + 'type' => + 'tipus', + 'pts' => + 'pts', + 'Level' => + 'Nivell', + 'level' => + 'nivell', + 'No Scores' => + 'No hi ha Puntuacions', + 'Attempt' => + 'Intent', + 'No Failures' => + 'No hi ha fracassos', + 'Team' => + 'Team', + 'team' => + 'team', + 'Names' => + 'Noms', + 'Scores' => + 'Puntuacions', + 'Failures' => + 'Fracassos', + 'New Team' => + 'Nou Equip', + 'Team Logo' => + 'Logo de l\'Equip', + 'Selected Logo:' => + 'Logo escollit:', + 'Select Logo' => + 'Seleccionar Logo', + 'All Teams' => + 'Tots els Equips', + 'Protected' => + 'Protegit', + 'Score' => + 'Puntiació', + 'Change Password' => + 'Canviar contrassenya', + 'Admin Level' => + 'Nivell d\'administrador', + 'Visibility' => + 'Visibilitat', + 'Team Management' => + 'Gestió d\'equip', + 'Add Team' => + 'Afegir Equip', + 'None' => + 'Cap', + 'Logo Name' => + 'Nom del Logo', + 'Logo Management' => + 'Gestor de Logos', + 'Session' => + 'Session', + 'Cookie' => + 'Cookie', + 'Creation Time' => + 'Creation Time', + 'Last Access' => + 'Últim Acces', + 'Data' => + 'Dades', + 'Sessions' => + 'Sessions', + 'entry' => + 'entry', + 'No Entries' => + 'No hi ha entrades', + 'Game Logs' => + 'Registres del Joc', + 'Game Logs Timeline' => + 'Linea de temps dels registres del Joc', + 'End Game' => + 'Finalitzar Joc', + 'Begin Game' => + 'Begin Game', + 'Game Admin' => + 'Administrador del Joc', + 'Controls' => + 'Controls', + 'Quiz' => + 'Preguntes i Respostes', + 'Flags' => + 'Flags', + 'Bases' => + 'Bases', + 'Countries' => + 'Països', + 'Logos' => + 'Logos', + //Translations for inc/* and inc/gameboard/* + 'captured' => + 'captured', + 'Status' => + 'Estat', + 'Completed' => + 'Completat', + 'Remaining' => + 'Restant', + 'Start' => + 'Començar', + 'End' => + 'Acabar', + 'Rank' => + 'Rang', + 'pts' => + 'pts', //points + 'Your Rank' => + 'El teu Rang', + 'Your Score' => + 'La teva puntuació', + 'Everyone' => + 'Tothom', + 'Your Team' => + 'El Teu Equip', + 'Captured' => + 'Catpurats', + 'Initiating' => + 'Iniciant', + 'run : > boot_sequence' => + 'run : > boot_sequence', + 'Extracting' => + 'Extraient', + //Translations for Utils.php's time_ago() function + 'just now' => + 'Ara mateix', + 'd' => + 'd', //day + 'hr' => + 'hr', //hour + 'min' => + 'min', //minute + 'sec' => + 'sec', //second + 'ds' => + 'd', //days + 'hrs' => + 'hrs', //hours + 'mins' => + 'mins', //minutes + 'secs' => + 'secs', //seconds + 'ago' => + 'ago', + //Translations for ModalControllers + 'begin_' => + 'begin_', + 'Are you sure you want to kick off the game? Logs will be cleared and progressive scoreboard will start' => + 'Estas segur que vols començar el joc? Els registres s\'sesborraran i el taulell de puntuacions apareixera progressivament', + 'end_' => + 'end_', + 'Are you sure you want to finish the current game?' => + 'Estas segur que vols finalitzar el joc actual?', + 'Are you sure you want to logout from the game?' => + 'Estas segur que vols tancar la sessió actual del joc?', + 'Saved' => + 'Desat', + 'All changes have been successfully saved.' => + 'Tots els canvis s\'shan gravat de forma satisfactoria', + 'Error' => + 'Error', + 'Sorry your form was not saved. Please correct the all errors and save again.' => + 'Ho sentim, el formulari no s\'ha desat. Si us plau corregiu els errors i premeu "Desar" de nou', + 'cancel_' => + 'cancel_', + 'Are you sure you want to cancel? You have unsaved changes that will be reverted.' => + 'Are you sure you want to cancel? You have unsaved changes that will be reverted.', + 'choose_logo' => + 'choose_logo', + 'captured_' => + 'captured_', + 'flag_owner_' => + 'flag_owner_', + 'INACTIVE' => + 'INACTIVE', + 'PTS' => + 'PTS', + 'category' => + 'category', + 'capture_' => + 'capture_', + 'Insert your answer' => + 'Insert your answer', + 'Request Hint' => + 'Request Hint', + 'Submit' => + 'Submit', + 'hint_' => + 'hint_', + 'first_capture' => + 'first_capture', + 'completed_by' => + 'completed_by', + 'scoreboard_' => + 'scoreboard_', + 'filter_' => + 'filter_', + 'rank_' => + 'rank_', + 'team_name_' => + 'team_name_', + 'quiz_pts_' => + 'quiz_pts_', + 'flag_pts_' => + 'flag_pts_', + 'base_pts_' => + 'base_pts_', + 'total_pts_' => + 'total_pts_', + 'team_' => + 'team_', + 'team_members' => + 'team_members', + 'base_pts' => + 'base_pts', + 'quiz_pts' => + 'quiz_pts', + 'flag_pts' => + 'flag_pts', + 'total_pts' => + 'total_pts', + 'Tool bars are located on all edges of the gameboard. Tap a category to expand and close each tool bar.' => + 'Tool bars are located on all edges of the gameboard. Tap a category to expand and close each tool bar.', + 'Tool_Bars' => + 'Tool_Bars', + 'Tap the "Game Clock" to keep track of time during gameplay. Don’t let time get the best of you.' => + 'Tap the "Game Clock" to keep track of time during gameplay. Don’t let time get the best of you.', + 'Game_Clock' => + 'Game_Clock', + 'Countries marked with an ' => + 'Countries marked with an ', + 'are captured by you.' => + 'are captured by you.', + ' are owned by others.' => + ' are owned by others.', + 'Captures' => + 'Captures', + 'Tap Plus[+] to Zoom In. Tap Minus[-] to Zoom Out.' => + 'Prem Mes[+] per fer apropar el Zoom o Menys[-] per allunyar.', + 'Click and Drag to move left, right, up and down.' => + 'Clicka i arrossega per moure esquerra, dreta, amunt i avall', + 'Zoom' => + 'Zoom', + 'Tap Forward Slash [/] to activate computer commands. A list of commands can be found under "Rules".' => + 'Tap Forward Slash [/] to activate computer commands. A list of commands can be found under "Rules".', + 'Command_Line' => + 'Linea_De_Comandes', + 'Click "Nav" to access main navigation links like Rules of Play, Registration, Blog, Jobs & more.' => + 'Prem "Nav" per accedir als enllaços principals de navegació (Normes del Joc, Registre, Blog, Feines && ..)', + 'Track your competition by clicking "scorboard" to access real-time game statistics and graphs.' => + 'Track your competition by clicking "scorboard" to access real-time game statistics and graphs.', + 'Have fun, be the best and conquer the world.' => + 'Diverteix-te, sigues el millor i conquereix el món.', + 'Game_On' => + 'Game_On', + 'tutorial_' => + 'tutorial_', + 'Next' => + 'Següent', + 'Skip to play' => + 'Salta per començar a jugar', + 'Powered By Facebook' => + 'Powered By Facebook', +); From c5a3eb26a1102e50f4ad857b3a4fc4ad4f17b3a0 Mon Sep 17 00:00:00 2001 From: Akhil09 Date: Fri, 14 Oct 2016 16:55:22 +0530 Subject: [PATCH 02/18] added hindi translation --- src/language/lang_hi.php | 612 +++++++++++++++++++-------------------- 1 file changed, 306 insertions(+), 306 deletions(-) diff --git a/src/language/lang_hi.php b/src/language/lang_hi.php index b563040d..e0dbee5c 100644 --- a/src/language/lang_hi.php +++ b/src/language/lang_hi.php @@ -5,594 +5,594 @@ 'H:i:s D d/m/Y', //used by date() function //Translations for IndexController 'Facebook CTF' => - 'Facebook CTF', + 'फेसबुक CTF', 'Conquer the world' => - 'Conquereix el món', + 'दुनिया जीत लो', 'Play' => - 'Jugar', + 'प्ले', 'Welcome to the Facebook Capture the Flag Competition. By clicking "Play," you will be entered into the official CTF challenge. Good luck in your conquest.' => - 'Benvingut al "Capture The Flag" de Facebook. Premeu el botó de "a jugar" per entrar al CTF. Bona sort en la conquesta.', + 'फेसबुक पर आपका स्वागत है ध्वज प्रतियोगिता पर कब्जा। क्लिक करके "खेलते हैं," आप सरकारी CTF चुनौती में दर्ज किया जाएगा। अपनी विजय में गुड लक।', 'Get ready for the CTF to start and access the gameboard now!' => - 'Prepara\'t a que comenci el CTF i accedeix al taulell de joc ara!', + 'CTF शुरू किया और अब gameboard का उपयोग करने के लिए तैयार हो जाओ!', 'Gameboard' => - 'Taulell de joc', + 'खेल का बोर्ड', 'Register Team' => - 'Registrar Equip', + 'रजिस्टर टीम', 'Get ready for the CTF to start and register your team now!' => - 'Prepara\'t per començar el CTF i registra el teu equip ara!', + 'CTF शुरू करते हैं और अब अपनी टीम को पंजीकृत करने के लिए तैयार हो जाओ!', 'Login' => - 'Iniciar Sessió', + 'लॉग इन करें', 'Soon' => - 'Properament', + 'शीघ्र', 'Upcoming Game' => - 'Proper Joc', + 'आगामी खेल', '_days' => - '_dies', + '_days', '_hours' => - '_hores', + '_घंटे', '_minutes' => - '_minuts', + '_minutes', '_seconds' => - '_segons', + '_seconds', 'Official CTF Rules' => - 'Normativa Oficial del CTF', + 'सरकारी CTF नियम', 'Following actions are prohibited, unless explicitly told otherwise by event Admins.' => - 'Les següents accions no estan permeses, a no ser que sigui contradit explicitament pels administradors de l\'event.', + 'निम्न क्रियाओं में निषिद्ध कर रहे हैं, जब तक कि स्पष्ट रूप से घटना व्यवस्थापक द्वारा अन्यथा बताया।', 'Rule' => - 'Norma', + 'नियम', 'Cooperation' => - 'Cooperació', + 'सहयोग', 'No cooperation between teams with independent accounts. Sharing of keys or providing revealing hints to other teams is cheating, don’t do it.' => - 'Sense cooperació entre els equips amb comptes independents. Compartir claus o revelar pistes a altres equips es considera trampa, no ho facis.', + 'स्वतंत्र खातों के साथ टीमों के बीच कोई सहयोग। चाबियों का साझा करने या अन्य टीमों को खुलासा संकेत उपलब्ध कराने धोखा दे रही है, यह मत करो। ', 'Attacking Scoreboard' => - 'Taulell de Puntuacions d\'Atac', + 'हमला स्कोरबोर्ड', 'No attacking the competition infrastructure. If bugs or vulns are found, please alert the competition organizers immediately.' => - 'No ataquis la infrastructura de la competició. En case de trobar cap vulnerabilitat, siusplay avisa als organitzadors inmediatament', + 'कोई प्रतियोगिता के बुनियादी ढांचे पर हमला। अगर कीड़े या vulns पाए जाते हैं, प्रतियोगिता के आयोजकों को तुरंत सचेत करें।', 'Sabotage' => - 'Sabotatge', + 'तोड़फोड़', 'Absolutely no sabotaging of other competing teams, or in any way hindering their independent progress.' => - 'No sabotegis altres equips, or de destorbar el seu progrés de forma individual.', + 'बिल्कुल अन्य प्रतिस्पर्धी टीमों का कोई sabotaging, या किसी भी तरह से उनके स्वतंत्र प्रगति में बाधा उत्पन्न में।', 'Bruteforcing' => - 'Força bruta', + 'Bruteforcing', 'No brute forcing of challenge flag/ keys against the scoring site.' => - 'No realitzis força bruta per atacar als serveis', + 'कोई जानवर चुनौती ध्वज के लिए मजबूर / स्कोरिंग साइट के खिलाफ। कुंजी', 'Denial Of Service' => - 'Denegació de Servei', + 'सेवा की मनाई', 'DoSing the CTF platform or any of the challenges is forbidden.' => - 'Realitzar un atac de denegació de servei a la plataforma o a qualsevol de les proves està prohibit', + 'CTF मंच या चुनौतियों में से किसी के लिए मना किया है खुराक।', 'Legal' => - 'Legal', + 'कानूनी', 'Disclaimer' => - 'Disclaimer', + 'अस्वीकरण', 'By participating in the contest, you agree to release Facebook and its employees, and the hosting organization from any and all liability, claims or actions of any kind whatsoever for injuries, damages or losses to persons and property which may be sustained in connection with the contest. You acknowledge and agree that Facebook et al is not responsible for technical, hardware or software failures, or other errors or problems which may occur in connection with the contest.' => - 'By participating in the contest, you agree to release Facebook and its employees, and the hosting organization from any and all liability, claims or actions of any kind whatsoever for injuries, damages or losses to persons and property which may be sustained in connection with the contest. You acknowledge and agree that Facebook et al is not responsible for technical, hardware or software failures, or other errors or problems which may occur in connection with the contest.', + 'प्रतियोगिता में भाग लेने से, आप किसी भी और सभी दायित्व, दावा या चोट, क्षति या व्यक्तियों और संपत्ति को नुकसान के साथ जो संबंध में निरंतर किया जा सकता है के लिए किसी भी तरह की कार्रवाई से फेसबुक और अपने कर्मचारियों, और होस्टिंग संगठन को रिहा करने के लिए सहमत प्रतियोगिता। आप स्वीकार करते हैं और मानते हैं कि फेसबुक एट अल तकनीकी, हार्डवेयर या सॉफ्टवेयर विफलताओं, या अन्य त्रुटियों या समस्याओं जो प्रतियोगिता के संबंध में हो सकता है के लिए जिम्मेदार नहीं है।', 'If you have any questions about what is or is not allowed, please ask an organizer.' => - 'If you have any questions about what is or is not allowed, please ask an organizer.', + 'तुम क्या है या अनुमति नहीं है के बारे में किसी भी सवाल है, तो एक आयोजक कहें।', 'Have fun!' => - 'Passa-ho be!', + 'मज़े करो!', 'Name' => - 'Nom', + 'नाम', 'Email' => - 'Email', + 'ईमेल', 'Token' => - 'Token', + 'टोकन', 'Team Registration' => - 'Registre d\'Equip', + 'टीम पंजीकरण', 'Team Name' => - 'Nom de l\'equip', + 'टीम का नाम', 'Password' => - 'Contrassenya', + 'पासवर्ड', 'Choose an Emblem' => - 'Escollir un emblema', + 'एक प्रतीक चुनें', 'Sign Up' => - 'Donar-se d\'alta', + 'साइन अप करें', 'Register to play Capture The Flag here. Once you have registered, you will be logged in.' => - 'Dona\'t d\'alta per a jugar al Capture The Flag aqui. Quan estiguis registrat accediràs a la sessió.', + 'यहाँ पर कब्जा झंडा खेलने के लिए रजिस्टर। एक बार जब आप पंजीकृत है, तो आप में लॉग इन किया जाएगा।', 'Not Available' => - 'No Disponible', + 'उपलब्ध नहीं है', 'Team Registration will be open soon, stay tuned!' => - 'L\'equip es registrarà aviat, estigueu atents!', + 'टीम पंजीकरण जल्द ही खोल दिया जाएगा, देखते रहें!', 'Try Again' => - 'Tornar a intentar-ho', + 'पुनः प्रयास करें', 'Select' => - 'Selecciona', + 'चुनते हैं', 'Team Login' => - 'Login de l\'equip', + 'टीम लॉग इन', 'Please login here. If you have not registered, you may do so by clicking "Sign Up" below. ' => - 'Si us plau entreu les vostres credencials aqui. Si no esteu registrats, podeu fer-ho clickant a "Donar-vos d\'alta". ', + 'कृपया यहां लॉगिन करें। आप पंजीकृत नहीं किया है, तो आप क्लिक करके नीचे "साइन अप" से ऐसा कर सकते हैं। ', 'Team Login will be open soon, stay tuned!' => - 'Team Login will be open soon, stay tuned!', + 'टीम लॉग इन जल्द ही खोल दिया जाएगा, देखते रहें!', 'ERROR' => - 'ERROR', + 'त्रुटि', 'Start Over' => - 'Començar de Nou', + 'फिर से शुरू करना', 'Window is too small' => - 'La Finestra és massa petita', + 'विंडो बहुत छोटा है', 'For the best CTF experience, please make window size bigger.' => - 'Per a una millor experiència es recomenable fer el tamany de la finestra més gran', + 'सर्वश्रेष्ठ CTF अनुभव के लिए, कृपया विंडो का आकार बड़ा बना।', 'Thank you.' => - 'Gràcies.', + 'धन्यवाद।', 'Logout' => - 'Tanca la Sessió', + 'लोग आउट', 'Registration' => - 'Registre', + 'पंजीकरण', 'Play CTF' => - 'Jugar al CTF', + 'प्ले CTF', 'Rules' => - 'Normes', + 'नियम', //Translations for GameboardController 'Admin' => - 'Admin', + 'व्यवस्थापक', 'ADMIN' => - 'ADMIN', + 'व्यवस्थापक', 'Navigation' => - 'Navigació', + 'पथ प्रदर्शन', 'View Mode' => - 'Manera De Visualitzar', + 'दृश्य मोड', 'View mode' => - 'Mode de Visuatlització', + 'दृश्य मोड', 'Tutorial' => - 'Tutorial', + 'ट्यूटोरियल', 'Scoreboard' => - 'Taulell De Puntuació', + 'स्कोरबोर्ड', 'You' => - 'Tu', + 'आप', 'Others' => - 'Altri', + 'दूसरों', 'All' => - 'All', + 'सब', 'Leaderboard' => - 'Taulell de Lideratges', + 'लीडरबोर्ड', 'Announcements' => - 'Anuncis', + 'घोषणाएँ', 'Teams' => - 'Equips', + ' टीमों के', 'Filter' => - 'Filtrar', + 'फिल्टर', 'Activity' => - 'Activitat', + 'गतिविधि', 'Game Clock' => - 'Rellotge del Joc', + 'खेल घड़ी', //Translations for AdminController 'Auto' => - 'Automàtic', + 'ऑटो', 'All Categories' => - 'Totes les Categories', + 'सब वर्ग', 'Open' => - 'Obrir', + 'खुला', 'Tokenized' => 'Tokenized', 'Hour' => - 'Hora', + 'काल', 'Hours' => - 'Hores', + 'घंटे', 'Used by' => - 'Utilitzat per', + 'के द्वारा उपयोग', 'Used By' => - 'Fet servir', + 'के द्वारा उपयोग', 'Available' => - 'Disponible', + 'उपलब्ध', 'Registration Tokens' => - 'Registrar Tokens', + 'पंजीकरण टोकन', 'Create More' => - 'Crear Més', + 'बनाएं अधिक', 'Export Available' => - 'Exportar Disponibles', + 'निर्यात उपलब्ध', 'Not started yet' => - 'Encara no ha començat', + 'अभी तक शुरू नहीं हुआ', 'Configuration' => - 'Configuració', + ' विन्यास', 'Tokens' => - 'Tokens', + 'टोकन', 'Game Configuration' => - 'Configuració del Joc', + 'खेल विन्यास', 'OK' => - 'OK', + 'ठीक', 'status_' => - 'status_', + 'स्थिति_', 'On' => - 'On', + 'पर', 'Off' => - 'Off', + 'बंद', 'Player Names' => - 'Noms dels Jugadors', + 'खिलाड़ी के नाम', 'Players Per Team' => - 'Jugadors per Equip', + 'प्रति टीम के खिलाड़ियों को', 'Registration Type' => - 'Tipus de Registre', + 'पंजीकरण प्रकार', 'Strong Passwords' => - 'Contrassenyes Fortes', + 'मजबूत पासवर्ड', 'Team Selection' => - 'Selectió d\'Equip', + 'टीम के चयन', 'Game' => - 'Joc', + 'खेल', 'Scoring' => - 'Puntuacions', + 'स्कोरिंग', 'Progressive Cycle (s)' => - 'Cicles Progressius (s)', + 'प्रगतिशील चक्र (s)', 'Refresh Gameboard' => - 'Actualitza el taulell de joc', + 'ताज़ा gameboard', 'Default Bonus' => - 'Bonus per Defecte', + 'मूलभूत बोनस', 'Bases Cycle (s)' => - 'Cicle de les Bases (s)', + 'आधारों चक्र (s)', 'Default Bonus Dec' => - 'Default Bonus Dec', + 'मूलभूत बोनस दिसम्बर', 'Timer' => - 'Temporitzador', + 'टाइमर', 'Server Time' => - 'Hora del Servidor', + 'सर्वर समय', 'Game Duration' => - 'Duració del Joc', + 'खेल अवधि', 'Begin Time' => - 'Hora de començar!', + 'शुरू टाइम', 'Expected End Time' => - 'S\'esperava la fi del Temps', + 'उम्मीद अंत समय', 'Language' => - 'Idioma', + 'भाषा', 'DELETE' => - 'ESBORRAR', + 'हटाएँ', 'Delete' => - 'Esborrar', + 'हटाएँ', 'No Announcements' => - 'No Hi Ha Anuncis', + 'नहीं घोषणाएँ', 'Game Controls' => - 'Controls del Joc', + 'खेल नियंत्रण', 'Write New Announcement here' => - 'Escriu un nou Anunci aqui', + 'नई घोषणा लिखें यहां', 'Create' => - 'Crear', + 'सर्जन करना', 'General' => - 'General', + 'सामान्य', 'Back Up Database' => - 'Realitzar copia de seguretat eBack Up Database', + 'डेटाबेस वापस ऊपर', 'Export Full Game' => - 'Exportar el Joc Sencer', + 'निर्यात पूरा खेल', 'Import Full Game' => - 'Importar el Joc Sencer', + 'आयात पूरा खेल', 'Import Teams' => - 'Importar Equips', + 'आयात टीमों के', 'Export Teams' => - 'Exportar Equips', + 'निर्यात टीमों के', 'Import Logos' => - 'Importar Logos', + 'आयात लोगो', 'Export Logos' => - 'Exportar Logos', + 'निर्यात लोगो', 'Import Levels' => - 'Importar Nivells', + 'आयात के स्तर', 'Export Levels' => - 'Exportar Nivells', + 'निर्यात का स्तर', 'Import Categories' => - 'Importar Categories', + 'आयात श्रेणियाँ', 'Export Categories' => - 'Exportar Categories', + 'निर्यात श्रेणियाँ', 'Levels' => - 'Nivells', + 'स्तर', 'New Quiz Level' => - 'Nou nivell del Quiz', + 'नई प्रश्नोत्तरी स्तर', 'Title' => - 'Títol', + 'शीर्षक', 'Question' => - 'Pregunta', + 'सवाल', 'Level title' => - 'Nivell_Titol', + 'स्तर का शीर्षक', 'Quiz question' => - 'Pregunta de Quiz', + 'क्विज सवाल', 'Country' => - 'Païs', + 'देश', 'Answer' => - 'Resposta', + 'उत्तर', 'Points' => - 'Punts', + 'अंक', 'Hint' => - 'Pista', + 'सुझाव', 'Hint Penalty' => - 'Penalització per Pista', + 'सुझाव जुर्माना', 'EDIT' => - 'EDITAR', + 'संपादन', 'All Quiz Levels' => - 'Tots els Nivells de Quiz', + 'सभी प्रश्नोत्तरी स्तर', 'Filter By:' => - 'Filtrar Per:', + 'के द्वारा छनित:', 'All Status' => - 'Tots els Estats', + 'सभी स्थिति', 'Enabled' => - 'Habilitat', + 'सक्षम', 'Disabled' => - 'Deshabilitat', + 'अक्षम', 'Quiz Level' => - 'Nivell del Quiz', + 'क्विज स्तर', 'Show Answer' => - 'Mostrar Resposta', + 'उत्तर दिखाओ', 'Bonus' => - 'Bonus', + 'बोनस', '-Dec' => '-Dec', 'Save' => - 'Desar', + 'बचाना', 'Quiz Management' => - 'Gestió del Quiz', + 'क्विज प्रबंधन', 'Add Quiz Level' => - 'Afegir Nivell de Quiz', + 'जोड़ें प्रश्नोत्तरी स्तर', 'New Flag Level' => - 'Nova Flag pel Nivell', + 'नया झंडा स्तर', 'Description' => - 'Descripció', + 'विवरण', 'Level description' => - 'Descripció del Nivell', + 'स्तर विवरण', 'Category' => - 'Categoria', + 'वर्ग', 'Flag' => - 'Flag', + 'झंडा', 'flag' => - 'flag', + 'झंडा', 'All Flag Levels' => - 'Flags de Tots els Nivells', + 'सभी झंडा स्तर', 'New Attachment:' => - 'Nou Adjunt:', + 'नई कुर्की:', 'Attachment' => - 'Adjunt', + 'अनुलग्नक', 'Link' => - 'Enllaç', + 'संपर्क', 'New Link:' => - 'Nou Enllaç:', + 'नई लिंक:', 'Flag Level' => - 'Nivell de Flag', + 'ध्वज स्तर', 'Categories' => - 'Categories', + 'श्रेणियाँ', '+ Attachment' => - '+ Adjunt', + '+ अनुलग्नक', '+ Link' => - '+ Enllaç', + '+ लिंक', 'Flags Management' => - 'Gestió de Flags', + 'झंडे प्रबंधन', 'Add Flag Level' => - 'Afegir flags i nivell', + 'जोड़ें ध्वज स्तर', 'New Base Level' => - 'Nou Nivell Base', + 'न्यू आधार स्तर', 'Keep Points' => - 'Conservar Punts', + 'अंक रखना', 'Capture points' => - 'Capturar punts', + 'कैद अंक', 'All Base Levels' => - 'Tots els nivells Base', + 'सभी आधार स्तर', 'Base Level' => - 'Nivell Base', + 'आधार स्तर', 'Bases Management' => - 'Bases Management', + 'आधारों प्रबंधन', 'Add Base Level' => - 'Add Base Level', + 'जोड़ें आधार स्तर', 'New Category' => - 'Nova Categoria', + 'नई श्रेणी', 'Category: ' => - 'Category: ', + 'वर्ग: ', 'Categories Management' => - 'Gestió de Categories', + 'श्रेणियाँ प्रबंधन', 'Add Category' => - 'Afegir Categoria', + 'श्रेणी जोड़ना', 'All Countries' => - 'Tots els Païssos', + 'सभी देश', 'In Use' => - 'En us', + 'उपयोग में', 'In use' => - 'En us', + 'उपयोग में', 'Not Used' => - 'No esta Utilitzat', + 'उपयोग नहीं किया', 'Yes' => - 'Si', + 'हाँ', 'No' => - 'No', + 'नहीं', 'ISO Code' => - 'Codi ISO', + 'आईएसओ कोड', 'Countries Management' => - 'Gestió de Païssos', + 'देशों प्रबंधन', 'No Team Names' => - 'No hi ha noms per l\'equip', + 'नहीं टीम के नाम', 'time' => - 'time', + 'पहर', 'type' => - 'tipus', + 'प्रकार', 'pts' => - 'pts', + 'अंक', 'Level' => - 'Nivell', + 'स्तर', 'level' => - 'nivell', + 'स्तर', 'No Scores' => - 'No hi ha Puntuacions', + 'नहीं स्कोर', 'Attempt' => - 'Intent', + 'प्रयास', 'No Failures' => - 'No hi ha fracassos', + 'कोई विफलताओं', 'Team' => - 'Team', + 'टीम', 'team' => - 'team', + 'टीम', 'Names' => - 'Noms', + 'नाम', 'Scores' => - 'Puntuacions', + 'स्कोर', 'Failures' => - 'Fracassos', + 'विफलताओं', 'New Team' => - 'Nou Equip', + 'नई टीम', 'Team Logo' => - 'Logo de l\'Equip', + 'टीम लोगो', 'Selected Logo:' => - 'Logo escollit:', + 'चुने लोगो:', 'Select Logo' => - 'Seleccionar Logo', + 'लोगो चुनें', 'All Teams' => - 'Tots els Equips', + 'सभी टीमों के', 'Protected' => - 'Protegit', + 'संरक्षित', 'Score' => - 'Puntiació', + 'स्कोर', 'Change Password' => - 'Canviar contrassenya', + 'पासवर्ड बदलें', 'Admin Level' => - 'Nivell d\'administrador', + 'व्यवस्थापक स्तर', 'Visibility' => - 'Visibilitat', + 'दृश्यता', 'Team Management' => - 'Gestió d\'equip', + 'टीम प्रबंधन', 'Add Team' => - 'Afegir Equip', + 'टीम जोड़ें', 'None' => - 'Cap', + 'कोई नहीं', 'Logo Name' => - 'Nom del Logo', + 'लोगो का नाम', 'Logo Management' => - 'Gestor de Logos', + 'लोगो प्रबंधन', 'Session' => - 'Session', + 'सत्र', 'Cookie' => - 'Cookie', + 'कुकी', 'Creation Time' => - 'Creation Time', + 'रचना समय', 'Last Access' => - 'Últim Acces', + 'अंतिम पहुंच', 'Data' => - 'Dades', + 'जानकारी', 'Sessions' => - 'Sessions', + 'सत्र', 'entry' => - 'entry', + 'प्रवेश', 'No Entries' => - 'No hi ha entrades', + 'कोई प्रविष्टि नहीं', 'Game Logs' => - 'Registres del Joc', + 'खेल लॉग', 'Game Logs Timeline' => - 'Linea de temps dels registres del Joc', + 'खेल लॉग टाइमलाइन', 'End Game' => - 'Finalitzar Joc', + 'End खेल', 'Begin Game' => - 'Begin Game', + 'शुरू खेल', 'Game Admin' => - 'Administrador del Joc', + 'खेल व्यवस्थापक', 'Controls' => - 'Controls', + 'नियंत्रण', 'Quiz' => - 'Preguntes i Respostes', + 'क्विज', 'Flags' => - 'Flags', + 'झंडे', 'Bases' => - 'Bases', + 'कुर्सियां', 'Countries' => - 'Països', + 'देश', 'Logos' => - 'Logos', +  'लोगो', //Translations for inc/* and inc/gameboard/* 'captured' => - 'captured', + 'पकड़े', 'Status' => - 'Estat', + 'स्थिति', 'Completed' => - 'Completat', + 'पूरा कर लिया है', 'Remaining' => - 'Restant', + 'शेष', 'Start' => - 'Començar', + 'प्रारंभ', 'End' => - 'Acabar', + 'समाप्त', 'Rank' => - 'Rang', + 'श्रेणी', 'pts' => - 'pts', //points + 'अंक', // अंक 'Your Rank' => - 'El teu Rang', + 'आपका रैंक', 'Your Score' => - 'La teva puntuació', + 'अपने स्कोर', 'Everyone' => - 'Tothom', + 'हर कोई', 'Your Team' => - 'El Teu Equip', + 'आपकी टीम', 'Captured' => - 'Catpurats', + 'पकड़े', 'Initiating' => - 'Iniciant', + 'आरंभ', 'run : > boot_sequence' => - 'run : > boot_sequence', + 'चलाने: > boot_sequence', 'Extracting' => - 'Extraient', + 'निकालने', //Translations for Utils.php's time_ago() function 'just now' => - 'Ara mateix', + 'अभी', 'd' => - 'd', //day + 'डी', //day 'hr' => - 'hr', //hour + 'घंटे', //घंटे 'min' => - 'min', //minute + 'मिनट', //मिनट 'sec' => - 'sec', //second + 'सेकंड', //सेकंड 'ds' => - 'd', //days + 'डी', //दिन 'hrs' => - 'hrs', //hours + 'बजे', //घंटे 'mins' => - 'mins', //minutes + 'मिनट', //मिनट 'secs' => - 'secs', //seconds + 'सेकेंड', //सेकेंड 'ago' => - 'ago', + 'पहले', //Translations for ModalControllers 'begin_' => - 'begin_', + 'शुरू_', 'Are you sure you want to kick off the game? Logs will be cleared and progressive scoreboard will start' => - 'Estas segur que vols començar el joc? Els registres s\'sesborraran i el taulell de puntuacions apareixera progressivament', + 'क्या आप वाकई खेल बंद किक करने के लिए चाहते हैं? Logs साफ हो जाएगा और प्रगतिशील स्कोरबोर्ड शुरू कर देंगे', 'end_' => - 'end_', + 'समाप्त_', 'Are you sure you want to finish the current game?' => - 'Estas segur que vols finalitzar el joc actual?', + 'क्या आप मौजूदा खेल खत्म करना चाहते हैं?', 'Are you sure you want to logout from the game?' => - 'Estas segur que vols tancar la sessió actual del joc?', + 'क्या आप वाकई इस खेल से लॉगआउट करना चाहते हैं?', 'Saved' => - 'Desat', + 'बचाया', 'All changes have been successfully saved.' => - 'Tots els canvis s\'shan gravat de forma satisfactoria', + 'सभी परिवर्तन सफलतापूर्वक बचा लिया गया है।', 'Error' => - 'Error', + 'त्रुटि', 'Sorry your form was not saved. Please correct the all errors and save again.' => - 'Ho sentim, el formulari no s\'ha desat. Si us plau corregiu els errors i premeu "Desar" de nou', + 'माफ करना अपने फार्म सहेजा नहीं गया था। कृपया सभी त्रुटियों को ठीक करें और फिर से बचाने के लिए।', 'cancel_' => - 'cancel_', + 'रद्द करना_', 'Are you sure you want to cancel? You have unsaved changes that will be reverted.' => - 'Are you sure you want to cancel? You have unsaved changes that will be reverted.', + 'पक्का आप रद्द करना चाहते हैं? आप बिना सहेजे गए परिवर्तन है कि वापस सौंप दिया जाएगा।', 'choose_logo' => - 'choose_logo', + 'Choose_logo', 'captured_' => - 'captured_', + 'पकड़े_', 'flag_owner_' => 'flag_owner_', 'INACTIVE' => - 'INACTIVE', + 'निष्क्रिय', 'PTS' => - 'PTS', + 'पीटीएस', 'category' => - 'category', + 'वर्ग', 'capture_' => - 'capture_', + 'कब्जा_', 'Insert your answer' => - 'Insert your answer', + 'आपका जवाब डालें', 'Request Hint' => - 'Request Hint', + 'अनुरोध सुझाव', 'Submit' => - 'Submit', + 'जमा करें', 'hint_' => - 'hint_', + 'Hint_', 'first_capture' => 'first_capture', 'completed_by' => - 'completed_by', + 'द्वारा पूरा किया गया', 'scoreboard_' => 'scoreboard_', 'filter_' => 'filter_', 'rank_' => - 'rank_', + 'श्रेणी_', 'team_name_' => - 'team_name_', + 'टीम का नाम_', 'quiz_pts_' => 'quiz_pts_', 'flag_pts_' => @@ -604,7 +604,7 @@ 'team_' => 'team_', 'team_members' => - 'team_members', + 'टीम का सदस्या', 'base_pts' => 'base_pts', 'quiz_pts' => @@ -614,45 +614,45 @@ 'total_pts' => 'total_pts', 'Tool bars are located on all edges of the gameboard. Tap a category to expand and close each tool bar.' => - 'Tool bars are located on all edges of the gameboard. Tap a category to expand and close each tool bar.', + 'उपकरण सलाखों gameboard के सभी किनारों पर स्थित हैं। विस्तार और प्रत्येक उपकरण पट्टी बंद करने के लिए एक वर्ग टैप करें।', 'Tool_Bars' => 'Tool_Bars', 'Tap the "Game Clock" to keep track of time during gameplay. Don’t let time get the best of you.' => - 'Tap the "Game Clock" to keep track of time during gameplay. Don’t let time get the best of you.', + 'ठोकर "खेल घड़ी" gameplay के दौरान समय का ट्रैक रखने के लिए। बार जब आप का सबसे अच्छा मिल मत करो।', 'Game_Clock' => 'Game_Clock', 'Countries marked with an ' => - 'Countries marked with an ', + 'देशों के साथ चिह्नित ', 'are captured by you.' => - 'are captured by you.', + 'आप के द्वारा कब्जा कर रहे हैं।', ' are owned by others.' => - ' are owned by others.', + 'दूसरों के स्वामित्व में हैं।', 'Captures' => - 'Captures', + 'कब्जा', 'Tap Plus[+] to Zoom In. Tap Minus[-] to Zoom Out.' => - 'Prem Mes[+] per fer apropar el Zoom o Menys[-] per allunyar.', + 'नल प्लस [+] ज़ूम करने के लिए। ठोकर ऋण [-] ज़ूम आउट करने के लिए ।', 'Click and Drag to move left, right, up and down.' => - 'Clicka i arrossega per moure esquerra, dreta, amunt i avall', + 'पर क्लिक करें और, ठीक है, ऊपर और नीचे करने के लिए छोड़ दिया और कदम खींचें।', 'Zoom' => - 'Zoom', + 'ज़ूम', 'Tap Forward Slash [/] to activate computer commands. A list of commands can be found under "Rules".' => - 'Tap Forward Slash [/] to activate computer commands. A list of commands can be found under "Rules".', + 'आगे स्लेश टैप [/] कंप्यूटर आदेशों को सक्रिय करें। आदेशों की एक सूची "नियम" के नीचे पाया जा सकता है।', 'Command_Line' => - 'Linea_De_Comandes', + 'कमांड लाइन', 'Click "Nav" to access main navigation links like Rules of Play, Registration, Blog, Jobs & more.' => - 'Prem "Nav" per accedir als enllaços principals de navegació (Normes del Joc, Registre, Blog, Feines && ..)', + '"नव" प्ले, पंजीकरण, ब्लॉग, नौकरियों के नियम और अधिक की तरह मुख्य नेविगेशन लिंक का उपयोग करने के लिए क्लिक करें।', 'Track your competition by clicking "scorboard" to access real-time game statistics and graphs.' => - 'Track your competition by clicking "scorboard" to access real-time game statistics and graphs.', + '"स्कोरबोर्ड" वास्तविक समय खेल सांख्यिकी और रेखांकन का उपयोग करने के लिए क्लिक करके अपने प्रतियोगिता ट्रैक।', 'Have fun, be the best and conquer the world.' => - 'Diverteix-te, sigues el millor i conquereix el món.', + 'मजेदार है, सबसे अच्छा हो सकता है और दुनिया को जीत है।', 'Game_On' => - 'Game_On', + 'खेल शुरू', 'tutorial_' => - 'tutorial_', + 'Tutorial_', 'Next' => - 'Següent', + 'अगला', 'Skip to play' => - 'Salta per començar a jugar', + 'खेलने के लिए जाएं', 'Powered By Facebook' => - 'Powered By Facebook', + 'फेसबुक द्वारा संचालित', ); From 6bf831726b70119aea0acbc76394ce337cc612e8 Mon Sep 17 00:00:00 2001 From: Akhil09 Date: Wed, 15 Feb 2017 14:48:28 +0530 Subject: [PATCH 03/18] Update lang_hi.php --- src/language/lang_hi.php | 284 +++++++++++++++++++-------------------- 1 file changed, 142 insertions(+), 142 deletions(-) diff --git a/src/language/lang_hi.php b/src/language/lang_hi.php index e0dbee5c..ee51a584 100644 --- a/src/language/lang_hi.php +++ b/src/language/lang_hi.php @@ -5,21 +5,21 @@ 'H:i:s D d/m/Y', //used by date() function //Translations for IndexController 'Facebook CTF' => - 'फेसबुक CTF', + ' Facebook CTF', 'Conquer the world' => 'दुनिया जीत लो', 'Play' => 'प्ले', 'Welcome to the Facebook Capture the Flag Competition. By clicking "Play," you will be entered into the official CTF challenge. Good luck in your conquest.' => - 'फेसबुक पर आपका स्वागत है ध्वज प्रतियोगिता पर कब्जा। क्लिक करके "खेलते हैं," आप सरकारी CTF चुनौती में दर्ज किया जाएगा। अपनी विजय में गुड लक।', - 'Get ready for the CTF to start and access the gameboard now!' => - 'CTF शुरू किया और अब gameboard का उपयोग करने के लिए तैयार हो जाओ!', + 'Facebook पर आपका स्वागत है फ़लैग प्रतियोगिता को कैप्चर करें. "खेलें" क्लिक करके आप औपचारिक रूप से CTF चुनौती में शामिल हो जाएँगे. आपकी जीत के लिए शुभ कामनाएँ.', + 'Get ready for the CTF to start and access the board now!' => + 'अब CTF के शुरू होने और गेमबोर्ड एक्सेस करने के लिए तैयार रहें!!', 'Gameboard' => - 'खेल का बोर्ड', + ‘गेमबोर्ड', 'Register Team' => - 'रजिस्टर टीम', + 'टीम पंजीकृत करें', 'Get ready for the CTF to start and register your team now!' => - 'CTF शुरू करते हैं और अब अपनी टीम को पंजीकृत करने के लिए तैयार हो जाओ!', + 'CTF के शुरू होने के लिए तैयार रहें और अपनी टीम को अभी पंजीकृत करें!', 'Login' => 'लॉग इन करें', 'Soon' => @@ -27,47 +27,47 @@ 'Upcoming Game' => 'आगामी खेल', '_days' => - '_days', + '_दिन', '_hours' => '_घंटे', '_minutes' => - '_minutes', + '_मिनट', '_seconds' => - '_seconds', + '_सेकंड', 'Official CTF Rules' => - 'सरकारी CTF नियम', + 'आधिकारिक CTF नियम', 'Following actions are prohibited, unless explicitly told otherwise by event Admins.' => - 'निम्न क्रियाओं में निषिद्ध कर रहे हैं, जब तक कि स्पष्ट रूप से घटना व्यवस्थापक द्वारा अन्यथा बताया।', + 'निम्न क्रियाएँ निषिद्ध हैं, जब तक कि ईवेंट व्यवस्थापक द्वारा स्पष्ट रूप से अन्यथा न बताया गया हो', 'Rule' => 'नियम', 'Cooperation' => 'सहयोग', 'No cooperation between teams with independent accounts. Sharing of keys or providing revealing hints to other teams is cheating, don’t do it.' => - 'स्वतंत्र खातों के साथ टीमों के बीच कोई सहयोग। चाबियों का साझा करने या अन्य टीमों को खुलासा संकेत उपलब्ध कराने धोखा दे रही है, यह मत करो। ', + 'स्वतंत्र खातों वाली टीमों के बीच कोई सहयोग नहीं. चाबियों को साझा करना या अन्य टीमों को खुलासे के संकेत उपलब्ध कराना धोखा है, ऐसा न करें', 'Attacking Scoreboard' => - 'हमला स्कोरबोर्ड', + 'आक्रमण स्कोरबोर्ड', 'No attacking the competition infrastructure. If bugs or vulns are found, please alert the competition organizers immediately.' => - 'कोई प्रतियोगिता के बुनियादी ढांचे पर हमला। अगर कीड़े या vulns पाए जाते हैं, प्रतियोगिता के आयोजकों को तुरंत सचेत करें।', + ' प्रतिस्पर्धी के बुनियादी ढांचे पर कोई आक्रमण नहीं. अगर बग या vulns पाए जाते हैं, तो कृपया प्रतियोगिता के आयोजकों को तुरंत सचेत करें.', 'Sabotage' => - 'तोड़फोड़', + 'नुकसान ', 'Absolutely no sabotaging of other competing teams, or in any way hindering their independent progress.' => - 'बिल्कुल अन्य प्रतिस्पर्धी टीमों का कोई sabotaging, या किसी भी तरह से उनके स्वतंत्र प्रगति में बाधा उत्पन्न में।', + 'अन्य प्रतिस्पर्धी टीमों को नुकसान, या किसी भी तरह से उनकी स्वतंत्र प्रगति में बाधा बिलकुल उत्पन्न नहीं करना', 'Bruteforcing' => - 'Bruteforcing', + 'ब्रूटफ़ोर्सिंग', 'No brute forcing of challenge flag/ keys against the scoring site.' => - 'कोई जानवर चुनौती ध्वज के लिए मजबूर / स्कोरिंग साइट के खिलाफ। कुंजी', + 'स्कोर साइट के विरूद्ध चुनौती फ़्लैग/ कुंजियों की ब्रूटफ़ोर्सिंग नहीं', 'Denial Of Service' => - 'सेवा की मनाई', + 'सेवा की मनाही', 'DoSing the CTF platform or any of the challenges is forbidden.' => - 'CTF मंच या चुनौतियों में से किसी के लिए मना किया है खुराक।', + 'CTF प्लेटफ़ॉर्म या अन्य किसी भी चुनौती की डॉसिंग निषिद्ध है', 'Legal' => 'कानूनी', 'Disclaimer' => 'अस्वीकरण', 'By participating in the contest, you agree to release Facebook and its employees, and the hosting organization from any and all liability, claims or actions of any kind whatsoever for injuries, damages or losses to persons and property which may be sustained in connection with the contest. You acknowledge and agree that Facebook et al is not responsible for technical, hardware or software failures, or other errors or problems which may occur in connection with the contest.' => - 'प्रतियोगिता में भाग लेने से, आप किसी भी और सभी दायित्व, दावा या चोट, क्षति या व्यक्तियों और संपत्ति को नुकसान के साथ जो संबंध में निरंतर किया जा सकता है के लिए किसी भी तरह की कार्रवाई से फेसबुक और अपने कर्मचारियों, और होस्टिंग संगठन को रिहा करने के लिए सहमत प्रतियोगिता। आप स्वीकार करते हैं और मानते हैं कि फेसबुक एट अल तकनीकी, हार्डवेयर या सॉफ्टवेयर विफलताओं, या अन्य त्रुटियों या समस्याओं जो प्रतियोगिता के संबंध में हो सकता है के लिए जिम्मेदार नहीं है।', + 'प्रतियोगिता में भाग लेने से, आप किसी भी और सभी दायित्व, दावे या चोट, क्षति या व्यक्तियों और संपत्ति को नुकसान के लिए किसी भी कार्यवाही, जो प्रतिस्पर्धा के संबंध में हो, के लिए Facebook और उसके कर्मचारियों, और होस्टिंग संगठनों को मुक्त करने के लिए सहमत होते हैं. आप स्वीकार करते हैं और मानते हैं कि Facebook और संबंधित सभी, प्रतिस्पर्धा के संबंध में होने वाली तकनीकी, हार्डवेयर या सॉफ्टवेयर विफलताओं, या अन्य त्रुटियों या समस्याओं के लिए जिम्मेदार नहीं है.', 'If you have any questions about what is or is not allowed, please ask an organizer.' => - 'तुम क्या है या अनुमति नहीं है के बारे में किसी भी सवाल है, तो एक आयोजक कहें।', + 'अगर आपके पास क्या अनुमत है और क्या नहीं, इस बारे में कोई सवाल हैं, तो किसी आयोजक से पूछें.', 'Have fun!' => 'मज़े करो!', 'Name' => @@ -87,7 +87,7 @@ 'Sign Up' => 'साइन अप करें', 'Register to play Capture The Flag here. Once you have registered, you will be logged in.' => - 'यहाँ पर कब्जा झंडा खेलने के लिए रजिस्टर। एक बार जब आप पंजीकृत है, तो आप में लॉग इन किया जाएगा।', + 'फ़्लैग कैप्चर करें खेलने के लिए यहाँ रजिस्टर करें. एक बार जब आप पंजीकरण कर लेंगे, तो आपको लॉग इन किया जाएगा.', 'Not Available' => 'उपलब्ध नहीं है', 'Team Registration will be open soon, stay tuned!' => @@ -95,29 +95,29 @@ 'Try Again' => 'पुनः प्रयास करें', 'Select' => - 'चुनते हैं', + 'चुनें ', 'Team Login' => 'टीम लॉग इन', 'Please login here. If you have not registered, you may do so by clicking "Sign Up" below. ' => - 'कृपया यहां लॉगिन करें। आप पंजीकृत नहीं किया है, तो आप क्लिक करके नीचे "साइन अप" से ऐसा कर सकते हैं। ', + 'कृपया यहाँ लॉगिन करें. अगर आपने पंजीकरण नहीं किया है, तो आप नीचे "साइन अप करें" क्लिक करके ऐसा कर सकते हैं. ', 'Team Login will be open soon, stay tuned!' => 'टीम लॉग इन जल्द ही खोल दिया जाएगा, देखते रहें!', 'ERROR' => - 'त्रुटि', + 'गूलती', 'Start Over' => - 'फिर से शुरू करना', + 'फिर से शुरू करें', 'Window is too small' => 'विंडो बहुत छोटा है', 'For the best CTF experience, please make window size bigger.' => - 'सर्वश्रेष्ठ CTF अनुभव के लिए, कृपया विंडो का आकार बड़ा बना।', + 'सर्वश्रेष्ठ CTF अनुभव के लिए, कृपया विंडो का आकार बड़ा करें.', 'Thank you.' => - 'धन्यवाद।', + 'धन्यवाद. ', 'Logout' => 'लोग आउट', 'Registration' => 'पंजीकरण', 'Play CTF' => - 'प्ले CTF', + 'CTF खेलें', 'Rules' => 'नियम', //Translations for GameboardController @@ -126,7 +126,7 @@ 'ADMIN' => 'व्यवस्थापक', 'Navigation' => - 'पथ प्रदर्शन', + 'नेविगेशन', 'View Mode' => 'दृश्य मोड', 'View mode' => @@ -138,7 +138,7 @@ 'You' => 'आप', 'Others' => - 'दूसरों', + 'अन्य', 'All' => 'सब', 'Leaderboard' => @@ -146,7 +146,7 @@ 'Announcements' => 'घोषणाएँ', 'Teams' => - ' टीमों के', + ' टीम', 'Filter' => 'फिल्टर', 'Activity' => @@ -157,13 +157,13 @@ 'Auto' => 'ऑटो', 'All Categories' => - 'सब वर्ग', + 'सभी श्रेणियाँ', 'Open' => 'खुला', 'Tokenized' => - 'Tokenized', + 'टोकन किया गया', 'Hour' => - 'काल', + 'घंटा', 'Hours' => 'घंटे', 'Used by' => @@ -175,7 +175,7 @@ 'Registration Tokens' => 'पंजीकरण टोकन', 'Create More' => - 'बनाएं अधिक', + 'अधिक बनाएँ', 'Export Available' => 'निर्यात उपलब्ध', 'Not started yet' => @@ -191,19 +191,19 @@ 'status_' => 'स्थिति_', 'On' => - 'पर', + 'चालू', 'Off' => 'बंद', 'Player Names' => 'खिलाड़ी के नाम', 'Players Per Team' => - 'प्रति टीम के खिलाड़ियों को', + 'प्रति टीम खिलाड़ी', 'Registration Type' => 'पंजीकरण प्रकार', 'Strong Passwords' => 'मजबूत पासवर्ड', 'Team Selection' => - 'टीम के चयन', + 'टीम का चयन', 'Game' => 'खेल', 'Scoring' => @@ -211,13 +211,13 @@ 'Progressive Cycle (s)' => 'प्रगतिशील चक्र (s)', 'Refresh Gameboard' => - 'ताज़ा gameboard', + 'gameboard रीफ़्रेश करें', 'Default Bonus' => - 'मूलभूत बोनस', + 'डिफ़ॉल्ट बोनस', 'Bases Cycle (s)' => - 'आधारों चक्र (s)', + 'बेस चक्र ', 'Default Bonus Dec' => - 'मूलभूत बोनस दिसम्बर', + ' डिफ़ॉल्ट बोनस दिसम्बर', 'Timer' => 'टाइमर', 'Server Time' => @@ -225,9 +225,9 @@ 'Game Duration' => 'खेल अवधि', 'Begin Time' => - 'शुरू टाइम', + 'शुरू होने का समय', 'Expected End Time' => - 'उम्मीद अंत समय', + 'अपेक्षित समाप्ति समय', 'Language' => 'भाषा', 'DELETE' => @@ -235,41 +235,41 @@ 'Delete' => 'हटाएँ', 'No Announcements' => - 'नहीं घोषणाएँ', + 'कोई घोषणाएँ नहीं', 'Game Controls' => 'खेल नियंत्रण', 'Write New Announcement here' => - 'नई घोषणा लिखें यहां', + 'नई घोषणा यहाँ लिखें', 'Create' => - 'सर्जन करना', + 'बनाएँ', 'General' => 'सामान्य', 'Back Up Database' => - 'डेटाबेस वापस ऊपर', + 'डेटाबेस बैक अप करें', 'Export Full Game' => - 'निर्यात पूरा खेल', + 'पूरा खेल एक्सपोर्ट करें', 'Import Full Game' => - 'आयात पूरा खेल', + 'पूरा खेल इम्पोर्ट करें', 'Import Teams' => - 'आयात टीमों के', + 'टीम इम्पोर्ट करें', 'Export Teams' => - 'निर्यात टीमों के', + 'टीम एक्सपोर्ट करें ', 'Import Logos' => - 'आयात लोगो', + 'लोगो इम्पोर्ट करें', 'Export Logos' => - 'निर्यात लोगो', + 'लोगो एक्सपोर्ट करें', 'Import Levels' => - 'आयात के स्तर', + 'स्तर इम्पोर्ट करें', 'Export Levels' => - 'निर्यात का स्तर', + 'स्तर एक्सपोर्ट करें', 'Import Categories' => - 'आयात श्रेणियाँ', + 'श्रेणियाँ इम्पोर्ट करें', 'Export Categories' => - 'निर्यात श्रेणियाँ', + 'श्रेणियाँ एक्सपोर्ट करें', 'Levels' => 'स्तर', 'New Quiz Level' => - 'नई प्रश्नोत्तरी स्तर', + 'नया क्विज़ स्तर', 'Title' => 'शीर्षक', 'Question' => @@ -277,7 +277,7 @@ 'Level title' => 'स्तर का शीर्षक', 'Quiz question' => - 'क्विज सवाल', + 'क्विज़ सवाल', 'Country' => 'देश', 'Answer' => @@ -291,9 +291,9 @@ 'EDIT' => 'संपादन', 'All Quiz Levels' => - 'सभी प्रश्नोत्तरी स्तर', + 'सभी क्विज़ स्तर', 'Filter By:' => - 'के द्वारा छनित:', + 'के द्वारा फ़िल्टर:', 'All Status' => 'सभी स्थिति', 'Enabled' => @@ -307,69 +307,69 @@ 'Bonus' => 'बोनस', '-Dec' => - '-Dec', + '-दिसं', 'Save' => - 'बचाना', + 'सहेजें', 'Quiz Management' => 'क्विज प्रबंधन', 'Add Quiz Level' => - 'जोड़ें प्रश्नोत्तरी स्तर', + 'क्विज़ स्तर जोड़ें', 'New Flag Level' => - 'नया झंडा स्तर', + 'नया फ़्लैग स्तर', 'Description' => 'विवरण', 'Level description' => 'स्तर विवरण', 'Category' => - 'वर्ग', + 'श्रेणी', 'Flag' => - 'झंडा', + 'फ़्लैग', 'flag' => - 'झंडा', + 'फ़्लैग', 'All Flag Levels' => - 'सभी झंडा स्तर', + 'सभी फ़्लैग स्तर', 'New Attachment:' => - 'नई कुर्की:', + 'नया अटैचमेंट:', 'Attachment' => - 'अनुलग्नक', + ' अटैचमेंट', 'Link' => 'संपर्क', 'New Link:' => 'नई लिंक:', 'Flag Level' => - 'ध्वज स्तर', + 'फ़्लैग स्तर', 'Categories' => 'श्रेणियाँ', '+ Attachment' => - '+ अनुलग्नक', + '+ अटैचमेंट ', '+ Link' => '+ लिंक', 'Flags Management' => - 'झंडे प्रबंधन', + ' फ़्लैग प्रबंधन', 'Add Flag Level' => - 'जोड़ें ध्वज स्तर', + 'फ़्लैग स्तर जोड़ें', 'New Base Level' => - 'न्यू आधार स्तर', + 'नया बेस स्तर', 'Keep Points' => - 'अंक रखना', + 'अंक रखें', 'Capture points' => - 'कैद अंक', + 'अंक कैप्चर करें', 'All Base Levels' => - 'सभी आधार स्तर', + 'सभी बेस स्तर', 'Base Level' => - 'आधार स्तर', + 'बेस स्तर', 'Bases Management' => - 'आधारों प्रबंधन', + 'बेस प्रबंधन', 'Add Base Level' => - 'जोड़ें आधार स्तर', + 'बेस स्तर जोड़ें', 'New Category' => 'नई श्रेणी', 'Category: ' => - 'वर्ग: ', + ' श्रेणी: ', 'Categories Management' => - 'श्रेणियाँ प्रबंधन', + 'श्रेणी प्रबंधन', 'Add Category' => - 'श्रेणी जोड़ना', + 'श्रेणी जोड़ें', 'All Countries' => 'सभी देश', 'In Use' => @@ -385,11 +385,11 @@ 'ISO Code' => 'आईएसओ कोड', 'Countries Management' => - 'देशों प्रबंधन', + 'देश प्रबंधन', 'No Team Names' => - 'नहीं टीम के नाम', + 'कोई टीम नाम नहीं', 'time' => - 'पहर', + 'समय', 'type' => 'प्रकार', 'pts' => @@ -399,11 +399,11 @@ 'level' => 'स्तर', 'No Scores' => - 'नहीं स्कोर', + 'कोई स्कोर नहीं', 'Attempt' => 'प्रयास', 'No Failures' => - 'कोई विफलताओं', + 'कोई विफलता नहीं', 'Team' => 'टीम', 'team' => @@ -413,7 +413,7 @@ 'Scores' => 'स्कोर', 'Failures' => - 'विफलताओं', + 'विफलताएँ', 'New Team' => 'नई टीम', 'Team Logo' => @@ -423,7 +423,7 @@ 'Select Logo' => 'लोगो चुनें', 'All Teams' => - 'सभी टीमों के', + 'सभी टीम', 'Protected' => 'संरक्षित', 'Score' => @@ -449,15 +449,15 @@ 'Cookie' => 'कुकी', 'Creation Time' => - 'रचना समय', + 'बनाने का समय', 'Last Access' => - 'अंतिम पहुंच', + 'अंतिम एक्सेस', 'Data' => - 'जानकारी', + 'डेटा', 'Sessions' => 'सत्र', 'entry' => - 'प्रवेश', + 'प्रविष्टि', 'No Entries' => 'कोई प्रविष्टि नहीं', 'Game Logs' => @@ -465,9 +465,9 @@ 'Game Logs Timeline' => 'खेल लॉग टाइमलाइन', 'End Game' => - 'End खेल', + 'खेल समाप्त करें', 'Begin Game' => - 'शुरू खेल', + 'खेल शुरू करें', 'Game Admin' => 'खेल व्यवस्थापक', 'Controls' => @@ -475,16 +475,16 @@ 'Quiz' => 'क्विज', 'Flags' => - 'झंडे', + 'फ़्लैग', 'Bases' => - 'कुर्सियां', + 'बेस', 'Countries' => 'देश', 'Logos' =>  'लोगो', //Translations for inc/* and inc/gameboard/* 'captured' => - 'पकड़े', + 'कैप्चर किया गया', 'Status' => 'स्थिति', 'Completed' => @@ -496,30 +496,30 @@ 'End' => 'समाप्त', 'Rank' => - 'श्रेणी', + 'रैंक', 'pts' => 'अंक', // अंक 'Your Rank' => 'आपका रैंक', 'Your Score' => - 'अपने स्कोर', + 'आपका स्कोर', 'Everyone' => - 'हर कोई', + 'सभी', 'Your Team' => 'आपकी टीम', 'Captured' => - 'पकड़े', + 'कैप्चर किया गया', 'Initiating' => - 'आरंभ', + 'शुरू किया जा रहा है', 'run : > boot_sequence' => - 'चलाने: > boot_sequence', + 'चलाएँ: > boot_sequence', 'Extracting' => - 'निकालने', + 'एक्सट्रैक कर रहा है', //Translations for Utils.php's time_ago() function 'just now' => 'अभी', 'd' => - 'डी', //day + 'दि', //दिन 'hr' => 'घंटे', //घंटे 'min' => @@ -527,7 +527,7 @@ 'sec' => 'सेकंड', //सेकंड 'ds' => - 'डी', //दिन + 'दि', //दिन 'hrs' => 'बजे', //घंटे 'mins' => @@ -538,9 +538,9 @@ 'पहले', //Translations for ModalControllers 'begin_' => - 'शुरू_', + 'शुरू करें_', 'Are you sure you want to kick off the game? Logs will be cleared and progressive scoreboard will start' => - 'क्या आप वाकई खेल बंद किक करने के लिए चाहते हैं? Logs साफ हो जाएगा और प्रगतिशील स्कोरबोर्ड शुरू कर देंगे', + 'क्या आप वाकई खेल बंद करना चाहते हैं? लॉग साफ हो जाएगा और प्रगतिशील स्कोरबोर्ड शुरू हो जाएगा', 'end_' => 'समाप्त_', 'Are you sure you want to finish the current game?' => @@ -548,21 +548,21 @@ 'Are you sure you want to logout from the game?' => 'क्या आप वाकई इस खेल से लॉगआउट करना चाहते हैं?', 'Saved' => - 'बचाया', + 'सहेजा गया', 'All changes have been successfully saved.' => - 'सभी परिवर्तन सफलतापूर्वक बचा लिया गया है।', + 'सभी परिवर्तन सफलतापूर्वक सहेजे गए.', 'Error' => - 'त्रुटि', + 'गलती', 'Sorry your form was not saved. Please correct the all errors and save again.' => - 'माफ करना अपने फार्म सहेजा नहीं गया था। कृपया सभी त्रुटियों को ठीक करें और फिर से बचाने के लिए।', + 'माफ करें, आपका फार्म नहीं सहेजा गया. कृपया सभी गलतियों को ठीक करें और फिर से सहेजें.', 'cancel_' => - 'रद्द करना_', + 'रद्द करें_', 'Are you sure you want to cancel? You have unsaved changes that will be reverted.' => - 'पक्का आप रद्द करना चाहते हैं? आप बिना सहेजे गए परिवर्तन है कि वापस सौंप दिया जाएगा।', + 'क्या आप वाकई रद्द करना चाहते हैं? आपके पास बिना सहेजे गए परिवर्तन हैं जो वापस कर दिए जाएँगे.', 'choose_logo' => 'Choose_logo', 'captured_' => - 'पकड़े_', + 'कैप्चर किया गया_', 'flag_owner_' => 'flag_owner_', 'INACTIVE' => @@ -570,13 +570,13 @@ 'PTS' => 'पीटीएस', 'category' => - 'वर्ग', + 'श्रेणी', 'capture_' => - 'कब्जा_', + 'कैप्चर_', 'Insert your answer' => 'आपका जवाब डालें', 'Request Hint' => - 'अनुरोध सुझाव', + 'सुझाव का अनुरोध करें', 'Submit' => 'जमा करें', 'hint_' => @@ -590,7 +590,7 @@ 'filter_' => 'filter_', 'rank_' => - 'श्रेणी_', + 'रैंक_', 'team_name_' => 'टीम का नाम_', 'quiz_pts_' => @@ -604,7 +604,7 @@ 'team_' => 'team_', 'team_members' => - 'टीम का सदस्या', + 'टीम के सदस्य', 'base_pts' => 'base_pts', 'quiz_pts' => @@ -614,37 +614,37 @@ 'total_pts' => 'total_pts', 'Tool bars are located on all edges of the gameboard. Tap a category to expand and close each tool bar.' => - 'उपकरण सलाखों gameboard के सभी किनारों पर स्थित हैं। विस्तार और प्रत्येक उपकरण पट्टी बंद करने के लिए एक वर्ग टैप करें।', + 'टूल बार गेमबोर्ड के सभी किनारों पर स्थित हैं. हर टूल बार को विस्तृत और बंद करने के लिए किसी श्रेणी पर टैप करें.', 'Tool_Bars' => 'Tool_Bars', 'Tap the "Game Clock" to keep track of time during gameplay. Don’t let time get the best of you.' => - 'ठोकर "खेल घड़ी" gameplay के दौरान समय का ट्रैक रखने के लिए। बार जब आप का सबसे अच्छा मिल मत करो।', + 'गेमप्ले के दौरान समय का ट्रैक रखने के लिए "खेल घड़ी" टैप करें. इस समय के कारण आप अपना अच्छा प्रदर्शन न खोएँ.', 'Game_Clock' => 'Game_Clock', 'Countries marked with an ' => - 'देशों के साथ चिह्नित ', + ' इसके साथ चिह्नित देश ', 'are captured by you.' => - 'आप के द्वारा कब्जा कर रहे हैं।', + 'आप के द्वारा कैप्चर किए गए हैं.', ' are owned by others.' => - 'दूसरों के स्वामित्व में हैं।', + 'दूसरों के स्वामित्व में हैं.', 'Captures' => - 'कब्जा', + 'कैप्चर', 'Tap Plus[+] to Zoom In. Tap Minus[-] to Zoom Out.' => - 'नल प्लस [+] ज़ूम करने के लिए। ठोकर ऋण [-] ज़ूम आउट करने के लिए ।', + 'ज़ूम इन करने के लिए प्लस [+] टैप करें. ज़ूम आउट करने के लिए माइनस[-] टैप करें.', 'Click and Drag to move left, right, up and down.' => - 'पर क्लिक करें और, ठीक है, ऊपर और नीचे करने के लिए छोड़ दिया और कदम खींचें।', + 'बाएँ, दाएँ, ऊपर और नीचे ले जाने के लिए क्लिक करें और खींचें', 'Zoom' => 'ज़ूम', 'Tap Forward Slash [/] to activate computer commands. A list of commands can be found under "Rules".' => - 'आगे स्लेश टैप [/] कंप्यूटर आदेशों को सक्रिय करें। आदेशों की एक सूची "नियम" के नीचे पाया जा सकता है।', + 'कंप्यूटर कमांड सक्रिय करने के लिए आगे का स्लेश [/] टैप करें. "नियम" के अंतर्गत कमांड की सूची प्राप्त की जा सकती है.', 'Command_Line' => 'कमांड लाइन', 'Click "Nav" to access main navigation links like Rules of Play, Registration, Blog, Jobs & more.' => - '"नव" प्ले, पंजीकरण, ब्लॉग, नौकरियों के नियम और अधिक की तरह मुख्य नेविगेशन लिंक का उपयोग करने के लिए क्लिक करें।', + ' खेल के नियम, पंजीकरण, ब्लॉग, नौकरी और अधिक जैसे मुख्य नेविगेशन एक्सेस करने के लिए "नेव" क्लिक करें.', 'Track your competition by clicking "scorboard" to access real-time game statistics and graphs.' => - '"स्कोरबोर्ड" वास्तविक समय खेल सांख्यिकी और रेखांकन का उपयोग करने के लिए क्लिक करके अपने प्रतियोगिता ट्रैक।', + ' रीयल टाइम खेल सांख्यिकी और ग्राफ़ एक्सेस करने के लिए "स्कोरबोर्ड" क्लिक करके अपने प्रतिस्पर्धी का ट्रैक रखें.', 'Have fun, be the best and conquer the world.' => - 'मजेदार है, सबसे अच्छा हो सकता है और दुनिया को जीत है।', + 'मज़े करें, सर्वश्रेष्ठ बनें और दुनिया जीतें', 'Game_On' => 'खेल शुरू', 'tutorial_' => @@ -652,7 +652,7 @@ 'Next' => 'अगला', 'Skip to play' => - 'खेलने के लिए जाएं', + 'खेलने के लिए स्किप करें ', 'Powered By Facebook' => - 'फेसबुक द्वारा संचालित', + ' Facebook द्वारा संचालित', ); From de72b287306954726b5f1daee4993a54e0013ceb Mon Sep 17 00:00:00 2001 From: "Justin M. Wray" Date: Thu, 16 Feb 2017 18:15:54 -0500 Subject: [PATCH 04/18] Error Checking During Build Tests (#452) * Error Checking During Build Tests * Execute hh_client during build tests. * Currently the PHP built-in getimagesizefromstring function is not in the HHVM upstream hhi, and therefore generates an error. In the future, once getimagesizefromstring is added upstream, we should use the hh_client exit status. * * Readded execute permissions to the script. --- extra/run_tests.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/extra/run_tests.sh b/extra/run_tests.sh index 3da6845a..0144aee7 100755 --- a/extra/run_tests.sh +++ b/extra/run_tests.sh @@ -29,3 +29,11 @@ hhvm vendor/phpunit/phpunit/phpunit tests echo "[+] Deleting test database" mysql -u "$DB_USER" --password="$DB_PWD" -e "DROP DATABASE IF EXISTS $DB;" mysql -u "$DB_USER" --password="$DB_PWD" -e "FLUSH PRIVILEGES;" + +# In the future, we should use the hh_client exit status. +# Current there are some PHP built-ins not found in the hhi files upstream in HHVM. +echo "[+] Verifying HHVM Strict Compliance and Error Checking" +if [[ $(hh_client $CODE_PATH | grep -vP "Unbound" | wc -l) != 0 ]]; then + hh_client $CODE_PATH + exit 1 +fi From 8e4151e52d28a3c521cc4287074c093809f34cf4 Mon Sep 17 00:00:00 2001 From: "Justin M. Wray" Date: Thu, 16 Feb 2017 18:16:13 -0500 Subject: [PATCH 05/18] HHVM/Hack Typing Error Fixes (#450) * HHVM/Hack Typing Error Fixes * Fixed a few HHVM/Hack typing and strict compliance issues. * This is necessary before hh_client can run and be enforced during the build process. (See comments on #435) * * Updated formatting. --- src/controllers/AdminController.php | 3 ++- src/controllers/Controller.php | 6 ++++-- src/controllers/ajax/IndexAjaxController.php | 10 +++++++--- src/models/Category.php | 3 ++- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/controllers/AdminController.php b/src/controllers/AdminController.php index 3ffc0abc..f7ca3dde 100644 --- a/src/controllers/AdminController.php +++ b/src/controllers/AdminController.php @@ -2852,7 +2852,8 @@ class={$highlighted_color} if (count($failures) > 0) { $failures_tbody = ; foreach ($failures as $failure) { - if (!Level::genCheckStatus($failure->getLevelId())) { + $check_status = await Level::genCheckStatus($failure->getLevelId()); + if (!$check_status) { continue; } $level = await Level::gen($failure->getLevelId()); diff --git a/src/controllers/Controller.php b/src/controllers/Controller.php index 53ae6084..8a9375d4 100644 --- a/src/controllers/Controller.php +++ b/src/controllers/Controller.php @@ -18,8 +18,10 @@ abstract protected function genRenderBody(string $page): Awaitable<:xhp>; // TODO: Potential LFI - Review how to do internationalization better $document_root = must_have_string(Utils::getSERVER(), 'DOCUMENT_ROOT'); $language_style = ''; - if (file_exists($document_root. '/static/css/locals/' .$language. '/style.css')) { - $language_style = 'static/css/locals/' .$language. '/style.css'; + if (file_exists( + $document_root.'/static/css/locals/'.$language.'/style.css', + )) { + $language_style = 'static/css/locals/'.$language.'/style.css'; } return diff --git a/src/controllers/ajax/IndexAjaxController.php b/src/controllers/ajax/IndexAjaxController.php index 3843b957..e087d8f2 100644 --- a/src/controllers/ajax/IndexAjaxController.php +++ b/src/controllers/ajax/IndexAjaxController.php @@ -109,6 +109,8 @@ protected function getActions(): array { array $names, array $emails, ): Awaitable { + $ldap_password = $password; + // Check if registration is enabled $registration = await Configuration::gen('registration'); if ($registration->getValue() === '0') { @@ -144,7 +146,6 @@ protected function getActions(): array { // Use randomly generated password for local account for LDAP users // This will help avoid leaking users ldap passwords if the server's database // is compromised. - $ldap_password = $password; $password = gmp_strval( gmp_init(bin2hex(openssl_random_pseudo_bytes(16)), 16), 62, @@ -189,6 +190,7 @@ protected function getActions(): array { // Verify that this team name is not created yet $team_exists = await Team::genTeamExist($shortname); if (!$team_exists) { + invariant(is_string($password), "Expected password to be a string"); $password_hash = Team::generateHash($password); $team_id = await Team::genCreate($shortname, $password_hash, $logo_name); @@ -205,9 +207,11 @@ protected function getActions(): array { await Token::genUse($token, $team_id); } // Login the team - if ($ldap->getValue() === '1') - return await $this->genLoginTeam($team_id, $ldap_password); else + if ($ldap->getValue() === '1') { + return await $this->genLoginTeam($team_id, $ldap_password); + } else { return await $this->genLoginTeam($team_id, $password); + } } else { return Utils::error_response('Registration failed', 'registration'); } diff --git a/src/models/Category.php b/src/models/Category.php index 67a0138f..f101cf6a 100644 --- a/src/models/Category.php +++ b/src/models/Category.php @@ -83,7 +83,8 @@ private static function categoryFromRow(Map $row): Category { if (!$mc_result || count($mc_result) === 0 || $refresh) { $db = await self::genDb(); $categories = array(); - $result = await $db->queryf('SELECT * FROM categories ORDER BY category ASC'); + $result = + await $db->queryf('SELECT * FROM categories ORDER BY category ASC'); foreach ($result->mapRows() as $row) { $categories[] = self::categoryFromRow($row); } From c2957d7c0b7b289f3160e66a17fa6fd1359aaa65 Mon Sep 17 00:00:00 2001 From: "Justin M. Wray" Date: Fri, 17 Feb 2017 15:15:11 -0500 Subject: [PATCH 06/18] Require bxslider version 4.2.6 (Fixes #455) (#458) * This resolves a current build error #455. * bxslider was updated from 4.2.6 to 4.2.7 on February 14th. Previously FBCTF allowed for a near match to 4.2.6. However, FBCTF fails to build with 4.2.7. During the installation, process grunt failed to build the browserify javascript. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9635fddc..1391ddde 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,6 @@ "jquery": "^2.2.3", "keycode": "^2.1.1", "typed.js": "^1.1.1", - "bxslider": "~4.2.6" + "bxslider": "4.2.6" } } From 899ab2beda13eb4fba09596ce5ddbe6b4475d976 Mon Sep 17 00:00:00 2001 From: "Justin M. Wray" Date: Fri, 17 Feb 2017 15:15:21 -0500 Subject: [PATCH 07/18] Fixed Syntax Errors in Hindi Language (Fixes Build Errors) (#460) * Fixed minor syntax error due to character encoding. * This will ensure the project builds (no Hack errors). --- src/language/lang_hi.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/language/lang_hi.php b/src/language/lang_hi.php index ee51a584..8cecda62 100644 --- a/src/language/lang_hi.php +++ b/src/language/lang_hi.php @@ -15,7 +15,7 @@ 'Get ready for the CTF to start and access the board now!' => 'अब CTF के शुरू होने और गेमबोर्ड एक्सेस करने के लिए तैयार रहें!!', 'Gameboard' => - ‘गेमबोर्ड', + 'गेमबोर्ड', 'Register Team' => 'टीम पंजीकृत करें', 'Get ready for the CTF to start and register your team now!' => @@ -481,7 +481,7 @@ 'Countries' => 'देश', 'Logos' => -  'लोगो', + 'लोगो', //Translations for inc/* and inc/gameboard/* 'captured' => 'कैप्चर किया गया', From ca4009121be3e105c3c2877c451424cebba838a5 Mon Sep 17 00:00:00 2001 From: "Justin M. Wray" Date: Tue, 21 Feb 2017 04:31:05 -0500 Subject: [PATCH 08/18] Automated Game Start and Stop (#449) * Automated Game Start and Stop * Games will automatically start and stop at their scheduled times. Administrators can still manually start or stop a game regardless of the configured schedule. * Both Control::genAutoBegin() and Control::genAutoEnd() were added to check the current time against the scheduled start or stop time and perform the relevant action (Control::genBegin or Control::getEnd). * Control::genAutoRun() checks the current game status and determine if the game should be starting or ending, calling the appropriate function (Control::genAutoBegin or Control::getAutoEnd) and is exclusively used in the new autorun.php script. * Control::genRunAutoRunScript() runs the new autorun.php script, ensuring the script is not already running before starting a new copy. * The Router class was updated to include a call to Control::genRunAutoRunScript(), this ensures the script is always running. This script status check, and execution when needed, only takes place on a full page load. * The autorun.php script runs Control::genAutoRun() and sleeps up to 30 seconds. * If the game is scheduled to start or stop within 30 seconds, the script will sleep for the necessary amount of time. * Games will always start with at most a 29-second difference from the scheduled time. This discrepancy can only take place if the schedule is changed within 30 seconds of the previously scheduled time. Otherwise, the execution will happen at the scheduled time. * This automation is self-contained and requires no additional dependencies or external services (like cron, etc.). * * Allow administrators to define the cycle time (in seconds) for the autorun process. This time will be used for the sliding sleep. * * Added sanitization to the autorun script path/file. --- database/schema.sql | 1 + database/test_schema.sql | 1 + src/Router.php | 1 + src/controllers/AdminController.php | 10 ++++ src/models/Control.php | 89 +++++++++++++++++++++++++++++ src/scripts/autorun.php | 48 ++++++++++++++++ 6 files changed, 150 insertions(+) create mode 100644 src/scripts/autorun.php diff --git a/database/schema.sql b/database/schema.sql index b076c614..b12a299c 100644 --- a/database/schema.sql +++ b/database/schema.sql @@ -196,6 +196,7 @@ INSERT INTO `configuration` (field, value, description) VALUES("scoring", "0", " INSERT INTO `configuration` (field, value, description) VALUES("gameboard", "1", "(Boolean) Refresh all data in the gameboard"); INSERT INTO `configuration` (field, value, description) VALUES("progressive_cycle", "300", "(Integer) Frequency to take progressive scoreboard in seconds"); INSERT INTO `configuration` (field, value, description) VALUES("bases_cycle", "5", "(Integer) Frequency to score base levels in seconds"); +INSERT INTO `configuration` (field, value, description) VALUES("autorun_cycle", "30", "(Integer) Frequency to cycle autorun in seconds"); INSERT INTO `configuration` (field, value, description) VALUES("registration", "0", "(Boolean) Ability to register teams"); INSERT INTO `configuration` (field, value, description) VALUES("registration_names", "0", "(Boolean) Registration will ask for names"); INSERT INTO `configuration` (field, value, description) VALUES("registration_type", "1", "(Integer) Type of registration: 1 - Open; 2 - Tokenized;"); diff --git a/database/test_schema.sql b/database/test_schema.sql index 8ed35e26..99dc922d 100644 --- a/database/test_schema.sql +++ b/database/test_schema.sql @@ -196,6 +196,7 @@ INSERT INTO `configuration` (field, value, description) VALUES("scoring", "0", " INSERT INTO `configuration` (field, value, description) VALUES("gameboard", "1", "(Boolean) Refresh all data in the gameboard"); INSERT INTO `configuration` (field, value, description) VALUES("progressive_cycle", "300", "(Integer) Frequency to take progressive scoreboard in seconds"); INSERT INTO `configuration` (field, value, description) VALUES("bases_cycle", "5", "(Integer) Frequency to score base levels in seconds"); +INSERT INTO `configuration` (field, value, description) VALUES("autorun_cycle", "30", "(Integer) Frequency to cycle autorun in seconds"); INSERT INTO `configuration` (field, value, description) VALUES("registration", "0", "(Boolean) Ability to register teams"); INSERT INTO `configuration` (field, value, description) VALUES("registration_names", "0", "(Boolean) Registration will ask for names"); INSERT INTO `configuration` (field, value, description) VALUES("registration_type", "1", "(Integer) Type of registration: 1 - Open; 2 - Tokenized;"); diff --git a/src/Router.php b/src/Router.php index 2d5336c2..22fad642 100644 --- a/src/Router.php +++ b/src/Router.php @@ -16,6 +16,7 @@ class Router { $xhp = await self::genRouteModal($page, strval($modal)); return strval($xhp); } else { + await Control::genRunAutoRunScript(); $response = await self::genRouteNormal($page); return strval($response); } diff --git a/src/controllers/AdminController.php b/src/controllers/AdminController.php index f7ca3dde..2d465561 100644 --- a/src/controllers/AdminController.php +++ b/src/controllers/AdminController.php @@ -290,6 +290,7 @@ class="fb-cta cta--yellow" 'default_bonus' => Configuration::gen('default_bonus'), 'default_bonusdec' => Configuration::gen('default_bonusdec'), 'bases_cycle' => Configuration::gen('bases_cycle'), + 'autorun_cycle' => Configuration::gen('autorun_cycle'), 'start_ts' => Configuration::gen('start_ts'), 'end_ts' => Configuration::gen('end_ts'), }; @@ -314,6 +315,7 @@ class="fb-cta cta--yellow" $default_bonus = $results['default_bonus']; $default_bonusdec = $results['default_bonusdec']; $bases_cycle = $results['bases_cycle']; + $autorun_cycle = $results['autorun_cycle']; $start_ts = $results['start_ts']; $end_ts = $results['end_ts']; @@ -714,6 +716,14 @@ class="fb-cta cta--yellow"
+
+ + getValue()} + name="fb--conf--autorun_cycle" + /> +
diff --git a/src/models/Control.php b/src/models/Control.php index 57c604fd..c5ae30f6 100644 --- a/src/models/Control.php +++ b/src/models/Control.php @@ -216,6 +216,95 @@ class Control extends Model { await Level::genBaseScoring(); } + public static async function genAutoBegin(): Awaitable { + // Get start time + $config_start_ts = await Configuration::gen('start_ts'); + $start_ts = intval($config_start_ts->getValue()); + + // Get end time + $config_end_ts = await Configuration::gen('end_ts'); + $end_ts = intval($config_end_ts->getValue()); + + // Get paused status + $config_game_paused = await Configuration::gen('game_paused'); + $game_paused = intval($config_game_paused->getValue()); + + if (($game_paused === 0) && ($start_ts <= time()) && ($end_ts > time())) { + // Start the game + await Control::genBegin(); + } + } + + public static async function genAutoEnd(): Awaitable { + // Get start time + $config_start_ts = await Configuration::gen('start_ts'); + $start_ts = intval($config_start_ts->getValue()); + + // Get end time + $config_end_ts = await Configuration::gen('end_ts'); + $end_ts = intval($config_end_ts->getValue()); + + // Get paused status + $config_game_paused = await Configuration::gen('game_paused'); + $game_paused = intval($config_game_paused->getValue()); + + if (($game_paused === 0) && ($end_ts <= time())) { + // Start the game + await Control::genEnd(); + } + } + + public static async function genAutoRun(): Awaitable { + // Get start time + $config_game = await Configuration::gen('game'); + $game = intval($config_game->getValue()); + + if ($game === 0) { + // Check and start the game + await Control::genAutoBegin(); + } else { + // Check and stop the game + await Control::genAutoEnd(); + } + } + + public static async function genRunAutoRunScript(): Awaitable { + $autorun_status = await Control::checkScriptRunning('autorun'); + if ($autorun_status === false) { + $autorun_location = escapeshellarg( + must_have_string(Utils::getSERVER(), 'DOCUMENT_ROOT'). + '/scripts/autorun.php', + ); + $cmd = + 'hhvm -vRepo.Central.Path=/var/run/hhvm/.hhvm.hhbc_autorun '. + $autorun_location. + ' > /dev/null 2>&1 & echo $!'; + $pid = shell_exec($cmd); + await Control::genStartScriptLog(intval($pid), 'autorun', $cmd); + } + } + + public static async function checkScriptRunning( + string $name, + ): Awaitable { + $db = await self::genDb(); + $result = await $db->queryf( + 'SELECT pid FROM scripts WHERE name = %s AND status = 1 LIMIT 1', + $name, + ); + if ($result->numRows() >= 1) { + $pid = intval(must_have_idx($result->mapRows()[0], 'pid')); + $status = file_exists("/proc/$pid"); + if ($status === false) { + await Control::genStopScriptLog($pid); + await Control::genClearScriptLog(); + } + return $status; + } else { + return false; + } + } + public static async function importGame(): Awaitable { $data_game = JSONImporterController::readJSON('game_file'); if (is_array($data_game)) { diff --git a/src/scripts/autorun.php b/src/scripts/autorun.php new file mode 100644 index 00000000..9f45bffc --- /dev/null +++ b/src/scripts/autorun.php @@ -0,0 +1,48 @@ +getValue()); + $sleep = $conf_sleep_secs; + $conf_game = \HH\Asio\join(Configuration::gen('game')); + $config_start_ts = \HH\Asio\join(Configuration::gen('start_ts')); + $start_ts = intval($config_start_ts->getValue()); + $config_end_ts = \HH\Asio\join(Configuration::gen('end_ts')); + $end_ts = intval($config_end_ts->getValue()); + + if (($conf_game->getValue() === '1') && + (($end_ts - time()) < $conf_sleep_secs)) { + $sleep = $end_ts - time(); + } else if (($conf_game->getValue() === '0') && + (($start_ts - time()) < $conf_sleep_secs)) { + $sleep = $start_ts - time(); + } + + if ($sleep < 0) { + $sleep = $conf_sleep_secs; + } + + sleep($sleep); +} From be35c6ba17f3b8de4a924d4c9af014ae020f4a53 Mon Sep 17 00:00:00 2001 From: "Justin M. Wray" Date: Tue, 21 Feb 2017 04:31:59 -0500 Subject: [PATCH 09/18] 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); + }); + }); } }; From 8d1db8c41e6868d82cec6fa90fb2c67fc8195ef6 Mon Sep 17 00:00:00 2001 From: juliannagler Date: Tue, 14 Mar 2017 16:00:42 -0700 Subject: [PATCH 10/18] Update README.md --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 30a2c5dc..328a3451 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,6 @@ The Facebook CTF is a platform to host Jeopardy and “King of the Hill” style * If running an open competition: * Point participants towards the registration page * Enjoy! -* Note: We're hiring! If you're interested in the remote eng lead position, apply [here](http://pro.applytojob.com/apply/Qe1YmW/CTF-Engineering-Lead)! # Installation From 8f16141798c7d266775cb48396ecbaca8dd99fc0 Mon Sep 17 00:00:00 2001 From: juliannagler Date: Fri, 17 Mar 2017 13:21:07 -0700 Subject: [PATCH 11/18] Update README.md --- README.md | 152 ++---------------------------------------------------- 1 file changed, 5 insertions(+), 147 deletions(-) diff --git a/README.md b/README.md index 328a3451..f3a48c81 100644 --- a/README.md +++ b/README.md @@ -18,157 +18,15 @@ The Facebook CTF is a platform to host Jeopardy and “King of the Hill” style * Point participants towards the registration page * Enjoy! -# Installation - -The Facebook CTF platform can be provisioned in development or production environments. Note that the *only* supported system is 64 bit Ubuntu 14.04. Ubuntu 16.04 is not supported at this time, nor is 32 bit. We will accept PRs to support other platforms, but we will not officially support those platforms if there are any issues. - -### Development - -While it is possible to do development on a physical Ubuntu machine (and possibly other Linux distros as well), we highly recommend doing all development on a Vagrant VM. First, install [VirtualBox](https://www.virtualbox.org/wiki/Downloads) and [Vagrant](https://www.vagrantup.com/downloads.html). Then run: - -```bash -git clone https://github.com/facebook/fbctf -cd fbctf -vagrant up -``` - -This will create a local virtual machine with Ubuntu 14.04 using Vagrant and VirtualBox as the provider. The provisioning script will install all necessary software to the platform locally, using self-signed certificates. The platform will be available on [https://10.10.10.5](https://10.10.10.5) by default. You can find any error logs in `/var/log/hhvm/error.log`. If you would like to change this IP address, you can find the configuration for it in the `Vagrantfile`. - -Once the VM has started, go to the URL/IP of the server (10.10.10.5 in the default case). Click the "Login" link at the top right, enter the 'admin' as the team name and 'password' as the password (without quotes). You will be redirected to the administration page. At the bottom of the navigation bar on the left, there will be a link to go to the gameboard. - -To login to the Vagrant VM, navigate to the fbctf folder and run: - -``` -vagrant ssh -``` - -If you are using a non-English locale on the host system, you will run into problems during the installation. The easiest solution is to run vagrant with a default English locale: - -```bash -LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 vagrant up -``` - -Note that if you don't want to use the Vagrant VM (not recommended), you can provision in dev mode manually. To do so, run the following commands: - -``` -sudo apt-get update -sudo apt-get install git -git clone https://github.com/facebook/fbctf -cd fbctf -./extra/provision.sh -m dev -s $PWD -``` - -### Production -Please note that your machine needs to have at least 2GB of RAM, otherwise the composer part of the installation will fail. -The target system needs to be 64 bit Ubuntu 14.04. Run the following commands: - -```bash -sudo apt-get update -sudo apt-get install git -git clone https://github.com/facebook/fbctf -cd fbctf -./extra/provision.sh -m prod -s $PWD -``` - -*Note*: Because this is a production environment, the password will be randomly generated when the provision script finishes. This ensures that you can't forget to change the default password after provisioning. Make sure to watch the very end of the provision script, as the password will be printed out. It will not be stored elsewhere, so either keep track of it or change it. In order to change the password, run the following command: - -``` -source ./extra/lib.sh -set_password new_password ctf ctf fbctf $PWD -``` - -This will set the password to 'new_password', assuming the database user/password is ctf/ctf and the database name is fbctf (these are the defaults). - -By default, the provision script will place the code in the `/var/www/fbctf` directory, install all dependencies, and start the server. In order to run in production mode, we require that you use SSL. You can choose between generating new self-signed, using your own or generate valid SSL certificates using [Let's Encrypt](https://letsencrypt.org/). The provisioning script uses [certbot](https://certbot.eff.org/) to assist with the generation of valid SSL certificates. - -#### Production Certificates - -As mentioned above, there are three different type of certificates that the provision script will use: - -1. Self-signed certificate (```-c self```): - It is the same type of certificate that the development mode provisioning. Both the CRT and the key files will be generated and the command could be: - - ``` - ./extra/provision.sh -m prod -c self -s $PWD - ``` - -2. Use of own certificate (```-c own```): - If we already have a valid SSL certificate for our domain and want to use it. If the path for both CRT and key files is not provided, it will be prompted. Example command: - - ``` - ./extra/provision.sh -m prod -c own -k /path/to/my.key -C /path/to/cert.crt -s $PWD - ``` - -3. Generate new certificate using Let's Encrypt (```-c certbot```): - A new valid SSL certificate will be generated using [certbot](https://certbot.eff.org/). There are few needed parameters that if not provided, will be prompted: - - ``` - ./extra/provision.sh -m prod -c certbot -D test.mydomain.com -e myemail@mydomain.com -s $PWD - ``` - Note that certbot will run a challenge-response to validate the server so it will need Internet access. +For more information, see the [Admin Guide](https://github.com/facebook/fbctf/wiki/Admin-Guide) -Once you've provisioned the VM, go to the URL/IP of the server. Click the "Login" link at the top right, enter the admin credentials, and you'll be redirected to the admin page. Enter the credentials you received at the end of the provision script to log in. - - -#### Optional installation - -If you are going to be modifying files outside of the Vagrant VM, you will need to synchronize the files using [Unison](https://www.cis.upenn.edu/~bcpierce/unison/download.html) (bi-directional file sync over SSH). Once Unison is installed, you can sync your local repo with the VM with the following command from the root of the repository (outside the VM): - -`./extra/unison.sh $PWD` - -Note that the unison script will not sync NPM dependencies, so if you ever need to run `npm install`, you should always run it on the VM itself. - -This step is not necessary if all development is done on the VM. - - -#### Keep code updated - -If you are already running the fbctf platform and want to keep the code updated, there is an easy way to do that with the provision script. -For example, the following command will run in a production environment and it will pull master from Github and get it ready to run, from the folder ```/var/www/fbctf```: - -``` -./extra/provision.sh -m prod -U -s $PWD -d /var/www/fbctf -``` - - -# Using Docker - -[Dockerfile](Dockerfile) is provided, you can use docker to deploy fbctf to both development and production. - -### Development - -This build command will provision fbctf in dev mode in docker: - -`docker build --build-arg MODE=dev -t="fbctf_in_dev" .` (don't forget the dot at the end) - - -Run command: - -`docker run -p 80:80 -p 443:443 fbctf_in_dev` - -Once you've started the container, go to the URL/IP of the server. Click the "Login" link at the top right, enter the default admin credentials, and you'll be redirected to the admin page. - -### Production -Please note that your machine needs to have at least 2GB of RAM, otherwise the composer part of the installation will fail. -This build command will provision fbctf in prod mode in docker. Once you've started the container, Let's Encrypt will take YOUR_DOMAIN to obtain a trusted certificate at zero cost: - -`docker build --build-arg MODE=prod --build-arg DOMAIN=test.mydomain.com --build-arg EMAIL=myemail@mydomain.com --build-arg TYPE=certbot -t="fbctf_in_prod" .` (don't forget the dot at the end) - -Run command: - -`docker run -p 80:80 -p 443:443 -v /etc/letsencrypt:/etc/letsencrypt fbctf_in_prod` - -*Note 1: Because this is a production environment, the password will be randomly generated when the provision script finishes. Make sure to watch the very end of the provision script while the image is building, as the password will be printed out. It will not be stored elsewhere, so either keep track of it or change it. In order to change the password, run the following command in container:* - -``` -set_password new_password ctf ctf fbctf /root -``` +# Installation -*This will set the password to 'new_password', assuming the database user/password is ctf/ctf and the database name is fbctf (these are the defaults).* +The FBCTF platform was designed with flexibility in mind, allowing for different types of installations depending on the needs of the end user. The FBCTF platform can be installed either in Development Mode, or Production Mode. Development is for development, and Production is intended for live events utilizing the FBCTF platform. -*Note 2: You'll have to mount /etc/letsencrypt as a volume to persist the certs. ex: `docker run -v /etc/letsencrypt:/etc/letsencrypt ...`* +[Development Installation Guide](https://github.com/facebook/fbctf/wiki/Installation-Guide,-Development) -You can go to the URL/IP of the server, click the "Login" link at the top right, enter the admin credentials, and you'll be redirected to the admin page. +[Production Installation Guide](https://github.com/facebook/fbctf/wiki/Installation-Guide,-Production) ## Reporting an Issue From 53a6fa7b9d571b7a91ab9eb08048c09892c8332f Mon Sep 17 00:00:00 2001 From: juliannagler Date: Fri, 17 Mar 2017 13:21:52 -0700 Subject: [PATCH 12/18] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f3a48c81..3544c8bf 100644 --- a/README.md +++ b/README.md @@ -36,13 +36,13 @@ If you have issues installing the platform, please provide the entire output of ## Contribute -You’ve used it, now you want to make it better? Awesome! Pull requests are welcome! Click [here] (https://github.com/facebook/fbctf/blob/master/CONTRIBUTING.md) to find out how to contribute. +You’ve used it, now you want to make it better? Awesome! Pull requests are welcome! Click [here](https://github.com/facebook/fbctf/blob/master/CONTRIBUTING.md) to find out how to contribute. -Facebook also has [bug bounty program] (https://www.facebook.com/whitehat/) that includes FBCTF. If you find a security vulnerability in the platform, please submit it via the process outlined on that page and do not file a public issue. +Facebook also has [bug bounty program](https://www.facebook.com/whitehat/) that includes FBCTF. If you find a security vulnerability in the platform, please submit it via the process outlined on that page and do not file a public issue. ## Have more questions? -Check out the [wiki pages] (https://github.com/facebook/fbctf/wiki) attached to this repo. +Check out the [wiki pages](https://github.com/facebook/fbctf/wiki) attached to this repo. ## License From 300091c70f56d39278e3ffb113cd19b15b77896e Mon Sep 17 00:00:00 2001 From: juliannagler Date: Fri, 17 Mar 2017 13:26:06 -0700 Subject: [PATCH 13/18] Update README.md --- README.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/README.md b/README.md index 3544c8bf..57a73505 100644 --- a/README.md +++ b/README.md @@ -12,10 +12,6 @@ The Facebook CTF is a platform to host Jeopardy and “King of the Hill” style * Follow setup instructions below to spin up platform infrastructure. * Enter challenges into admin page * Have participants register as teams - * If running a closed competition: - * In the admin page, generate and export tokens to be shared with approved teams, then point participants towards the registration page - * If running an open competition: - * Point participants towards the registration page * Enjoy! For more information, see the [Admin Guide](https://github.com/facebook/fbctf/wiki/Admin-Guide) @@ -46,4 +42,4 @@ Check out the [wiki pages](https://github.com/facebook/fbctf/wiki) attached to t ## License -This source code is licensed under the Creative Commons Attribution-NonCommercial 4.0 International license found in the LICENSE file in the root directory of this source tree. +This source code is licensed under the Creative Commons Attribution-NonCommercial 4.0 International license. View the license [here](https://github.com/facebook/fbctf/blob/master/LICENSE). From 0b7ca09adea8e70bb71bb8e922cffa0d418ac8d3 Mon Sep 17 00:00:00 2001 From: "Justin M. Wray" Date: Tue, 21 Mar 2017 19:03:57 -0400 Subject: [PATCH 14/18] Live Sync API (#459) * Live Sync API * Introducing the Live Sync API. * The Live Sync API allows administrators to import game activity in near-real-time. Users can link their accounts on one or more FBCTF platform instances and their scores will be automatically imported into the systems that have been linked. * The primary use-case revolves around event aggregation across multiple FBCTF instances. Event organizers can now separate FBCTF instances and combine scores into one global scoreboard. * The Live Sync API will import Levels, Categories, Scoring Events, and Hint Usage. Scores are automatically calculated, and bonuses are updated to ensure accurate scoring across linked FBCTF instances. * Administrators determine which systems, if any, are linked. * Users must link their account in order for their activity to be synced. * The UI/UX of FBCTF has been updated to include a mechanism for users to configure their Live Sync credentials. * Users cannot obtain hints or capture levels on the importing system. * The API is JSON based and the schema is generalized so that it can leveraged by other platforms or external processes. So data can be synced from non-FBCTF platforms. * The importing script will automatically handle country conflicts (if two systems have the same country selected for a level). * USER GUIDE (Documentation): * Users must first have an account on all FBCTF instances they wish to link. * The user must then login and access the game board. * From the navigation menu, the user should select "Account." * The user must then set a Live Sync username and password. The Live Sync username and password must not be their login credentials. In fact, users are prohibited from using their account password as their Live Sync password. * The user would repeat the above steps for each FBCTF instance they wish to link. The Live Sync credentials must be the same on each FBCTF instance or their accounts will not be linked. * ADMIN GUIDE (Documentation): * The admin is free to sync as many platforms as their desire. Additionally the admin may import from as many API sources as their desire. * The admin will need to launch the "live import" script, on any importing systems, from the command-line: * `hhvm -vRepo.Central.Path=/var/run/hhvm/.hhvm.hhbc_liveimport /var/www/fbctf/src/scripts/liveimport.php ` * Disabling of the SSL Verification and Debugging are both optional. The URL(s) and Sleep time are required arguments. * EXAMPLE: `hhvm -vRepo.Central.Path=/var/run/hhvm/.hhvm.hhbc_liveimport /var/www/fbctf/src/scripts/liveimport.php "https://10.10.10.101/data/livesync.php https://10.10.10.102/data/livesync.php https://10.10.10.103/other/platform/api" 300 true true` * API SCHEMA (JSON): * JSON: [{"active":true,"type":"flag","title":"Example Level 1","description":"This is the first example level.","entity_iso_code":"TJ","category":"None","points":100,"bonus":30,"bonus_dec":10,"penalty":10,"teams":{"fbctf:user1:$2y$12$a1T4KyqqxADi3YIJ7M2sf.VoSHz6qMBx.zrxAIvZnD8de95EsLeny":{"timestamp":"2017-02-17 02:20:22","capture":true,"hint":false}}}] * Explained (Formatted output for readability - the actually data must be in valid JSON format): [0] => Array ( [active] => 1 // Level Status (Enabled/Disabled) [type] => flag // Level Type (Flag or Quiz) [title] => Example Level 1 // Level Name [description] => This is the first example level. // Level Description [entity_iso_code] => US // Country Code (Mapping) [category] => None // Level Category [points] => 100 // Points [bonus] => 30 // Bonus Points [bonus_dec] => 10 // Bonus Point Decrement [penalty] => 0 // Hint Cost [teams] => Array ( [fbctf:user3:$2y$12$GIR7V0Q2OMDv8cTTOnzKVpGYgR4.pWTsPRHtZ3yenKZ9JxOabx4m2] => Array // Live Sync Type, Live Sync Username, Live Sync Key (Hash) ( [timestamp] => 2017-02-17 01:09:24 // Activity Timestamp [capture] => 1 // Capture Status [hint] => // Hint Used ) ) ) * Example (Formatted output for readability - the actually data must be in valid JSON format): Array ( [0] => Array ( [active] => 1 [type] => flag [title] => Example Level 1 [description] => This is the first example level. [entity_iso_code] => US [category] => None [points] => 100 [bonus] => 30 [bonus_dec] => 10 [penalty] => 0 [teams] => Array ( ) ) [1] => Array ( [active] => 1 [type] => flag [title] => Example Level 2 [description] => This is the second example level. [entity_iso_code] => OM [category] => None [points] => 100 [bonus] => 30 [bonus_dec] => 10 [penalty] => 0 [teams] => Array ( [fbctf:user1:$2y$12$n.VmlNNwxmZ/OkGGuhVhFeX0VExAgjoaYzyetLCIemSXN/yxWXLyO] => Array ( [timestamp] => 2017-02-17 01:01:49 [capture] => 1 [hint] => 1 ) [fbctf:user2:$2y$12$GIDv8cR7V0nzKVpQ2OMTTOGYgR4.pWTxOPRH9abtsJZ3yenKZx4m2] => Array ( [timestamp] => 2017-02-17 01:21:13 [capture] => 1 [hint] => 1 ) ) ) [2] => Array ( [active] => 1 [type] => flag [title] => Example Level 3 [description] => This is the third example level. [entity_iso_code] => MA [category] => None [points] => 100 [bonus] => 30 [bonus_dec] => 10 [penalty] => 0 [teams] => Array ( [fbctf:user2:$2y$12$GIDv8cR7VpQ2OM0nzKVTTOGYgR4.pWTxOabtsPRH9JZ3yenKZx4m2] => Array ( [timestamp] => 2017-02-17 01:18:45 [capture] => 1 [hint] => ) [fbctf:user1:$2y$12$n.VmlNNwxmZ/OkGGuhVhFeXYzExAg0VoajyetLCIemSXN/yxWXLyO] => Array ( [timestamp] => 2017-02-17 01:01:41 [capture] => 1 [hint] => ) ) ) [3] => Array ( [active] => 1 [type] => flag [title] => Example Level 4 [description] => This is the second example level. [entity_iso_code] => RO [category] => None [points] => 100 [bonus] => 30 [bonus_dec] => 10 [penalty] => 0 [teams] => Array ( [fbctf:user3:$2y$12$GIDv8cR7V02OnzKVpQMTTOGYgR4.pWTsPOabtZRH9Jx3yenKZx4m2] => Array ( [timestamp] => 2017-02-17 01:09:24 [capture] => 1 [hint] => ) ) ) ) * TO DO (Enhancements): * Implemented alternative Live Sync key/authentication mechanisms, such as: Facebook Login, OAuth, etc. * Improve the processing of Bases/Progressive scoring. * Integrate password strength enforcement for the Live Sync credentials. * * Added unit tests for Live Sync to TeamTest * * Updated unit tests for the Live Sync API. * Added Google OAuth to Live Sync API * Google OAuth can now be used with the Live Sync when the exporting system provides the "google_oauth" type and provides the email address of the user in base64 encoded form. * Added Google OAuth UI/UX. If enabled, this allows a user to link their Google account to their FBCTF account using Google OAuth. The user simply navigates to the account page and clicks the "Link Your Google Account" button and completes the sign-in/authorization process. * Administrators must enable Google OAuth. When disabled the option does not appear for the users. To enable Google OAuth the administrator must first create a Google API account and then place the API secrets file on the system (in a non-web directory). The administrator would then set the full path to the API secrets file in the settings.ini file, within the GOOGLE_OAUTH_FILE field. * The Live Sync API has been updated to handle the "google_oauth" type case. * The liveimport.php script has been updated to set default values for some of the API fields. The following fields are mandatory: * title * description * points * teams * The live import code has also been updated to ensure duplicate levels, when using a combination of non-defined and defined countries, are not generated. * The project now requires google/apiclient ^2.0 from composer. Updated composer.json and composer.lock to define the new dependencies. * Minor formatting updates. * * Ensure mandatory fields are set, gracefully skip when they are not. * Refined Live Import CLI Options and Updated Google OAuth Data * The live sync import script (livesync.php) now utilizes `getopts()` to provide more user-friendly option input to the command-line script. The script will provide a help message upon usage without the required field(s). Here is the help message text: ``` Usage: hhvm -vRepo.Central.Path=/var/run/hhvm/.hhvm.hhbc_liveimport /var/www/fbctf/src/scripts/liveimport.php --url [Switched allowed multiple times. Optionally provide custom HTTP headers after URL, pipe delimited] --sleep

; return tuple($title, $content); + case 'account': + $title = +

+ {tr('account_')}{tr('Settings')} +

; + if (Configuration::genGoogleOAuthFileExists() === true) { + $google_oauth_content = +
; + } else { + $google_oauth_content = ''; + } + $content = +
+ {$google_oauth_content} +
+

+ {tr( + 'Setup your FBCTF Live Sync credentials. These credentials must be the SAME on all other FBCTF instances that you are linking. DO NOT use your account password.', + )} +

+ + +
; + return tuple($title, $content); default: invariant(false, "Invalid modal name $modal"); } diff --git a/src/data/google_oauth.php b/src/data/google_oauth.php new file mode 100644 index 00000000..8a559e4f --- /dev/null +++ b/src/data/google_oauth.php @@ -0,0 +1,99 @@ +setAuthConfig($google_oauth_file); + $client->setAccessType('offline'); + $client->setScopes(['profile email']); + $client->setRedirectUri( + 'https://'.$_SERVER['HTTP_HOST'].'/data/google_oauth.php', + ); + + if (isset($_GET['code'])) { + $client->authenticate($_GET['code']); + $access_token = $client->getAccessToken(); + $oauth_client = new Google_Service_Oauth2($client); + $profile = $oauth_client->userinfo->get(); + $livesync_password_update = \HH\Asio\join( + Team::genSetLiveSyncPassword( + SessionUtils::sessionTeam(), + "google_oauth", + $profile->email, + $profile->id, + ), + ); + if ($livesync_password_update === true) { + $message = + tr('Your FBCTF account was successfully linked with Google.'); + $javascript_status = + 'window.opener.document.getElementsByClassName("google-link-response")[0].innerHTML = "'. + tr('Your FBCTF account was successfully linked with Google.'). + '"'; + } else { + $message = + tr( + 'There was an error connecting your account to Google, please try again later.', + ); + $javascript_status = + 'window.opener.document.getElementsByClassName("google-link-response")[0].innerHTML = "'. + tr( + 'There was an error connecting your account to Google, please try again later.', + ). + '"'; + } + $javascript_close = "window.open('', '_self', ''); window.close();"; + } else if (isset($_GET['error'])) { + $message = + tr( + 'There was an error connecting your account to Google, please try again later.', + ); + $javascript_status = + 'window.opener.document.getElementsByClassName("google-link-response")[0].innerHTML = "'. + tr( + 'There was an error connecting your account to Google, please try again later.', + ). + '"'; + $javascript_close = "window.open('', '_self', ''); window.close();"; + } else { + $auth_url = $client->createAuthUrl(); + header('Location: '.filter_var($auth_url, FILTER_SANITIZE_URL)); + exit; + } +} else { + $message = tr('Google OAuth is disabled.'); + $javascript_status = + 'window.opener.document.getElementsByClassName("google-link-response")[0].innerHTML = "'. + tr('Google OAuth is disabled.'). + '"'; + $javascript_close = "window.open('', '_self', ''); window.close();"; +} + +$output = +
+ + + + {$message} +
+ + + +
; + +print $output; diff --git a/src/data/livesync.php b/src/data/livesync.php new file mode 100644 index 00000000..9f564d01 --- /dev/null +++ b/src/data/livesync.php @@ -0,0 +1,93 @@ + { + $data = array(); + + $teams_array = array(); + $all_teams = await Team::genAllTeams(); + foreach ($all_teams as $team) { + $team_livesync_exists = + await Team::genLiveSyncExists($team->getId(), "fbctf"); + if ($team_livesync_exists === true) { + $team_livesync_key = + await Team::genGetLiveSyncKey($team->getId(), "fbctf"); + $teams_array[$team->getId()] = strval($team_livesync_key); + } + } + + $scores_array = array(); + $scored_teams = array(); + $all_scores = await ScoreLog::genAllScores(); + foreach ($all_scores as $score) { + if (in_array($score->getTeamId(), array_keys($teams_array)) === false) { + continue; + } + $scores_array[$score->getLevelId()][$teams_array[$score->getTeamId()]]['timestamp'] = + $score->getTs(); + $scores_array[$score->getLevelId()][$teams_array[$score->getTeamId()]]['capture'] = + true; + $scores_array[$score->getLevelId()][$teams_array[$score->getTeamId()]]['hint'] = + false; + $scored_teams[$score->getLevelId()][] = $score->getTeamId(); + } + $all_hints = await HintLog::genAllHints(); + foreach ($all_hints as $hint) { + if ($hint->getPenalty()) { + if (in_array($hint->getTeamId(), array_keys($teams_array)) === + false) { + continue; + } + $scores_array[$hint->getLevelId()][$teams_array[$hint->getTeamId()]]['hint'] = + true; + if (in_array( + $hint->getTeamId(), + $scored_teams[$hint->getLevelId()], + ) === + false) { + $scores_array[$hint->getLevelId()][$teams_array[$hint->getTeamId()]]['capture'] = + false; + $scores_array[$hint->getLevelId()][$teams_array[$hint->getTeamId()]]['timestamp'] = + $hint->getTs(); + } + } + } + + $levels_array = array(); + $all_levels = await Level::genAllLevels(); + foreach ($all_levels as $level) { + $entity = await Country::gen($level->getEntityId()); + $category = await Category::genSingleCategory($level->getCategoryId()); + if (array_key_exists($level->getId(), $scores_array)) { + $score_level_array = $scores_array[$level->getId()]; + } else { + $score_level_array = array(); + } + $one_level = array( + 'active' => $level->getActive(), + 'type' => $level->getType(), + 'title' => $level->getTitle(), + 'description' => $level->getDescription(), + 'entity_iso_code' => $entity->getIsoCode(), + 'category' => $category->getCategory(), + 'points' => $level->getPoints(), + 'bonus' => $level->getBonusFix(), + 'bonus_dec' => $level->getBonusDec(), + 'penalty' => $level->getPenalty(), + 'teams' => $score_level_array, + ); + $levels_array[] = $one_level; + } + + $data = $levels_array; + $this->jsonSend($data); + } + +} + +/* HH_IGNORE_ERROR[1002] */ +$syncData = new LiveSyncDataController(); +\HH\Asio\join($syncData->genGenerateData()); diff --git a/src/models/Configuration.php b/src/models/Configuration.php index 5af97b4a..7d1355db 100644 --- a/src/models/Configuration.php +++ b/src/models/Configuration.php @@ -102,4 +102,27 @@ private static function configurationFromRow( must_have_idx($row, 'description'), ); } + + public static function genGoogleOAuthFileExists(): bool { + $settings_file = '../../settings.ini'; + $config = parse_ini_file($settings_file); + + if ((array_key_exists('GOOGLE_OAUTH_FILE', $config) === true) && + (file_exists($config['GOOGLE_OAUTH_FILE']) === true)) { + return true; + } + return false; + } + + public static function genGoogleOAuthFile(): string { + $settings_file = '../../settings.ini'; + $config = parse_ini_file($settings_file); + + if ((array_key_exists('GOOGLE_OAUTH_FILE', $config) === true) && + (file_exists($config['GOOGLE_OAUTH_FILE']) === true)) { + return strval($config['GOOGLE_OAUTH_FILE']); + } + + return ''; + } } diff --git a/src/models/Level.php b/src/models/Level.php index c86d32dd..67176dba 100644 --- a/src/models/Level.php +++ b/src/models/Level.php @@ -1207,4 +1207,87 @@ public static function getBasesResponses( return false; } } + + public static async function getLevelIdByTypeTitleCountry( + string $type, + string $title, + string $entity_iso_code, + ): Awaitable { + $db = await self::genDb(); + + $result = + await $db->queryf( + 'SELECT id FROM levels WHERE type = %s AND title = %s AND entity_id IN (SELECT id FROM countries WHERE iso_code = %s)', + $type, + $title, + $entity_iso_code, + ); + + invariant($result->numRows() === 1, 'Expected exactly one result'); + return intval(must_have_idx($result->mapRows()[0], 'id')); + } + + public static async function genAlreadyExistUnknownCountry( + string $type, + string $title, + string $description, + int $points, + ): Awaitable { + $db = await self::genDb(); + + $result = + await $db->queryf( + 'SELECT COUNT(*) FROM levels WHERE type = %s AND title = %s AND description = %s AND points = %d', + $type, + $title, + $description, + $points, + ); + + if ($result->numRows() > 0) { + invariant($result->numRows() === 1, 'Expected exactly one result'); + return (intval(idx($result->mapRows()[0], 'COUNT(*)')) > 0); + } else { + return false; + } + } + + public static async function genLevelIdUnknownCountry( + string $type, + string $title, + string $description, + int $points, + ): Awaitable { + $db = await self::genDb(); + + $result = + await $db->queryf( + 'SELECT id FROM levels WHERE type = %s AND title = %s AND description = %s AND points = %d', + $type, + $title, + $description, + $points, + ); + + invariant($result->numRows() === 1, 'Expected exactly one result'); + return intval(must_have_idx($result->mapRows()[0], 'id')); + } + + public static async function genLevelUnknownCountry( + string $type, + string $title, + string $description, + int $points, + ): Awaitable { + + $level_id = await self::genLevelIdUnknownCountry( + $type, + $title, + $description, + $points, + ); + + $level = await self::gen($level_id); + return $level; + } } diff --git a/src/models/ScoreLog.php b/src/models/ScoreLog.php index 7c1cc7f4..2a68699f 100644 --- a/src/models/ScoreLog.php +++ b/src/models/ScoreLog.php @@ -225,4 +225,83 @@ private static function scorelogFromRow(Map $row): ScoreLog { MultiTeam::invalidateMCRecords('TEAMS_BY_LEVEL'); // Invalidate Memcached MultiTeam data. MultiTeam::invalidateMCRecords('TEAMS_FIRST_CAP'); // Invalidate Memcached MultiTeam data. } + + public static async function genScoreLogUpdate( + int $level_id, + int $team_id, + int $points, + string $type, + string $timestamp, + ): Awaitable { + $db = await self::genDb(); + await $db->queryf( + 'UPDATE scores_log SET ts = %s, level_id = %d, team_id = %d, points = %d, type = %s WHERE level_id = %d AND team_id = %d', + $timestamp, + $level_id, + $team_id, + $points, + $type, + $level_id, + $team_id, + ); + self::invalidateMCRecords(); // Invalidate Memcached ScoreLog data. + Control::invalidateMCRecords('ALL_ACTIVITY'); // Invalidate Memcached Control data. + MultiTeam::invalidateMCRecords('ALL_TEAMS'); // Invalidate Memcached MultiTeam data. + MultiTeam::invalidateMCRecords('POINTS_BY_TYPE'); // Invalidate Memcached MultiTeam data. + MultiTeam::invalidateMCRecords('LEADERBOARD'); // Invalidate Memcached MultiTeam data. + MultiTeam::invalidateMCRecords('TEAMS_BY_LEVEL'); // Invalidate Memcached MultiTeam data. + MultiTeam::invalidateMCRecords('TEAMS_FIRST_CAP'); // Invalidate Memcached MultiTeam data. + } + + public static async function genUpdateScoreLogBonus( + int $level_id, + int $team_id, + int $points, + ): Awaitable { + $db = await self::genDb(); + await $db->queryf( + 'UPDATE scores_log SET ts = ts, points = %d WHERE level_id = %d AND team_id = %d', + $points, + $level_id, + $team_id, + ); + self::invalidateMCRecords(); // Invalidate Memcached ScoreLog data. + Control::invalidateMCRecords('ALL_ACTIVITY'); // Invalidate Memcached Control data. + MultiTeam::invalidateMCRecords('ALL_TEAMS'); // Invalidate Memcached MultiTeam data. + MultiTeam::invalidateMCRecords('POINTS_BY_TYPE'); // Invalidate Memcached MultiTeam data. + MultiTeam::invalidateMCRecords('LEADERBOARD'); // Invalidate Memcached MultiTeam data. + MultiTeam::invalidateMCRecords('TEAMS_BY_LEVEL'); // Invalidate Memcached MultiTeam data. + MultiTeam::invalidateMCRecords('TEAMS_FIRST_CAP'); // Invalidate Memcached MultiTeam data. + } + + public static async function genLevelScores( + int $level_id, + ): Awaitable> { + $db = await self::genDb(); + $result = await $db->queryf( + 'SELECT * FROM scores_log WHERE level_id = %d ORDER BY ts ASC', + $level_id, + ); + + $scores = array(); + foreach ($result->mapRows() as $row) { + $scores[] = self::scorelogFromRow($row); + } + + return $scores; + } + + public static async function genLevelScoreByTeam( + int $team_id, + int $level_id, + ): Awaitable { + $db = await self::genDb(); + $result = await $db->queryf( + 'SELECT * FROM scores_log WHERE team_id = %d AND level_id = %d', + $team_id, + $level_id, + ); + + return self::scorelogFromRow($result->mapRows()[0]); + } } diff --git a/src/models/Team.php b/src/models/Team.php index bdf1ad74..696ad92d 100644 --- a/src/models/Team.php +++ b/src/models/Team.php @@ -665,4 +665,253 @@ public static function regenerateHash(string $password_hash): bool { return $rank; } + + public static async function genTeamUpdatePoints( + int $team_id, + int $points, + ): Awaitable { + $db = await self::genDb(); + await $db->queryf( + 'UPDATE teams SET last_score = last_score, points = %d WHERE id = %d', + $points, + $team_id, + ); + MultiTeam::invalidateMCRecords(); // Invalidate Memcached MultiTeam data. + Control::invalidateMCRecords('ALL_ACTIVITY'); // Invalidate Memcached Control data. + } + + public static async function genGetLiveSyncKey( + int $team_id, + string $type, + ): Awaitable { + $db = await self::genDb(); + $result = await $db->queryf( + 'SELECT * FROM livesync WHERE team_id = %d AND type = %s', + $team_id, + $type, + ); + invariant($result->numRows() === 1, 'Expected exactly one result'); + + $username = strval(must_have_idx($result->mapRows()[0], 'username')); + $key_from_db = strval(must_have_idx($result->mapRows()[0], 'sync_key')); + + switch ($type) { + case 'fbctf': + $key = self::generateHash($key_from_db); + break; + // FALLTHROUGH + default: + $key = $key_from_db; + break; + } + + return strval($type.":".$username.":".$key); + } + + public static async function genLiveSyncExists( + int $team_id, + string $type, + ): Awaitable { + $db = await self::genDb(); + $result = await $db->queryf( + 'SELECT id FROM livesync WHERE team_id = %d AND type = %s', + $team_id, + $type, + ); + if ($result->numRows() === 1) { + return true; + } + return false; + } + + public static async function genSetLiveSyncPassword( + int $team_id, + string $type, + string $username, + string $password, + ): Awaitable { + $db = await self::genDb(); + + if (($username === '') || ($password === '')) { + return false; + } + + switch ($type) { + case 'fbctf': + $key = hash("sha256", $password); + $team = await self::genTeam($team_id); + if (password_verify($password, $team->getPasswordHash())) { + return false; + } + break; + // FALLTHROUGH + default: + $key = $password; + break; + } + + $username_result = + await $db->queryf( + 'SELECT id FROM livesync WHERE username = %s AND type = %s AND team_id != %d', + $username, + $type, + $team_id, + ); + if ($username_result->numRows() > 0) { + return false; + } + + $current_id_result = await $db->queryf( + 'SELECT id FROM livesync WHERE team_id = %d AND type = %s', + $team_id, + $type, + ); + if ($current_id_result->numRows() === 1) { + $result = await $db->queryf( + 'UPDATE livesync SET username = %s, sync_key = %s WHERE id = %d', + $username, + $key, + intval(must_have_idx($current_id_result->mapRows()[0], 'id')), + ); + if ($result) { + return true; + } + } else { + $result = + await $db->queryf( + 'INSERT INTO livesync (type, team_id, username, sync_key) VALUES (%s, %d, %s, %s)', + $type, + $team_id, + $username, + $key, + ); + if ($result) { + return true; + } + } + return false; + } + + public static async function genLiveSyncKeyExists( + string $key, + ): Awaitable { + $db = await self::genDb(); + + if (strpos($key, ':') === false) { + return false; + } + list($type, $username, $key) = explode(':', $key); + + switch ($type) { + case 'fbctf': + $result = await $db->queryf( + 'SELECT * FROM livesync WHERE username = %s AND type = %s', + $username, + $type, + ); + break; + case 'google_oauth': + $result = await $db->queryf( + 'SELECT * FROM livesync WHERE sync_key = %s AND type = %s', + $key, + $type, + ); + break; + // FALLTHROUGH + default: + $result = await $db->queryf( + 'SELECT * FROM livesync WHERE sync_key = %s', + $key, + ); + break; + } + + if ($result->numRows() > 0) { + $team_id = 0; + foreach ($result->mapRows() as $row) { + $type = strval(must_have_idx($row, 'type')); + $username = strval(must_have_idx($row, 'username')); + $key_from_db = strval(must_have_idx($row, 'sync_key')); + + switch ($type) { + case 'fbctf': + if (password_verify($key_from_db, $key)) { + return true; + } + break; + // FALLTHROUGH + default: + if (strval($key) === strval($key_from_db)) { + return true; + } + break; + } + } + } + return false; + } + + public static async function genTeamFromLiveSyncKey( + string $key, + ): Awaitable { + $db = await self::genDb(); + + invariant(strpos($key, ':'), "Invalid live sync key"); + list($type, $username, $key) = explode(':', $key); + + switch ($type) { + case 'fbctf': + $result = await $db->queryf( + 'SELECT * FROM livesync WHERE username = %s AND type = %s', + $username, + $type, + ); + invariant($result->numRows() > 0, 'Expected at least one result'); + break; + case 'google_oauth': + $result = await $db->queryf( + 'SELECT * FROM livesync WHERE sync_key = %s AND type = %s', + $key, + $type, + ); + break; + // FALLTHROUGH + default: + $result = await $db->queryf( + 'SELECT * FROM livesync WHERE sync_key = %s', + $key, + ); + invariant($result->numRows() > 0, 'Expected at least one result'); + break; + } + + $team_id = 0; + foreach ($result->mapRows() as $row) { + $type = strval(must_have_idx($row, 'type')); + $username = strval(must_have_idx($row, 'username')); + $key_from_db = strval(must_have_idx($row, 'sync_key')); + + switch ($type) { + case 'fbctf': + if (password_verify($key_from_db, $key)) { + $team_id = intval(must_have_idx($row, 'team_id')); + $team = await self::genTeam($team_id); + return $team; + } + break; + // FALLTHROUGH + default: + if (strval($key) === strval($key_from_db)) { + $team_id = intval(must_have_idx($row, 'team_id')); + $team = await self::genTeam($team_id); + return $team; + } + break; + } + } + invariant($team_id !== 0, 'team_id not found'); + $team = await self::genTeam($team_id); + return $team; + } + } diff --git a/src/scripts/liveimport.php b/src/scripts/liveimport.php new file mode 100644 index 00000000..bf47d7ca --- /dev/null +++ b/src/scripts/liveimport.php @@ -0,0 +1,591 @@ + [Switched allowed multiple times. Optionally provide custom HTTP headers after URL, pipe delimited] \n". + " --sleep
+
+
+

{tr('LiveSync')}

+
+ + + + +
+
+
+
+
+ + getValue()} + name="fb--conf--livesync_auth_key" + /> +
+
+
+

{tr('Language')}

diff --git a/src/data/livesync.php b/src/data/livesync.php index 9f564d01..433fcef4 100644 --- a/src/data/livesync.php +++ b/src/data/livesync.php @@ -6,83 +6,188 @@ class LiveSyncDataController extends DataController { public async function genGenerateData(): Awaitable { $data = array(); + await tr_start(); + $input_auth_key = idx(Utils::getGET(), 'auth', ''); + $livesync_awaits = Map { + 'livesync_enabled' => Configuration::gen('livesync'), + 'livesync_auth_key' => Configuration::gen('livesync_auth_key'), + }; + $livesync_awaits_results = await \HH\Asio\m($livesync_awaits); + $livesync_enabled = $livesync_awaits_results['livesync_enabled']; + $livesync_auth_key = $livesync_awaits_results['livesync_auth_key']; - $teams_array = array(); - $all_teams = await Team::genAllTeams(); - foreach ($all_teams as $team) { - $team_livesync_exists = - await Team::genLiveSyncExists($team->getId(), "fbctf"); - if ($team_livesync_exists === true) { - $team_livesync_key = - await Team::genGetLiveSyncKey($team->getId(), "fbctf"); - $teams_array[$team->getId()] = strval($team_livesync_key); - } - } + if ($livesync_enabled->getValue() === '1' && + hash_equals( + strval($livesync_auth_key->getValue()), + strval($input_auth_key), + )) { + + $livesync_enabled_awaits = Map { + 'all_teams' => Team::genAllTeams(), + 'all_scores' => ScoreLog::genAllScores(), + 'all_hints' => HintLog::genAllHints(), + 'all_levels' => Level::genAllLevels(), + }; + $livesync_enabled_awaits_results = + await \HH\Asio\m($livesync_enabled_awaits); + $all_teams = $livesync_enabled_awaits_results['all_teams']; + invariant( + is_array($all_teams), + 'all_teams should be an array and not null', + ); + + $all_scores = $livesync_enabled_awaits_results['all_scores']; + invariant( + is_array($all_scores), + 'all_scores should be an array and not null', + ); + + $all_hints = $livesync_enabled_awaits_results['all_hints']; + invariant( + is_array($all_hints), + 'all_hints should be an array and not null', + ); - $scores_array = array(); - $scored_teams = array(); - $all_scores = await ScoreLog::genAllScores(); - foreach ($all_scores as $score) { - if (in_array($score->getTeamId(), array_keys($teams_array)) === false) { - continue; + $all_levels = $livesync_enabled_awaits_results['all_levels']; + invariant( + is_array($all_levels), + 'all_levels should be an array and not null', + ); + + $data = array(); + $teams_array = array(); + $team_livesync_exists = Map {}; + $team_livesync_key = Map {}; + foreach ($all_teams as $team) { + $team_id = $team->getId(); + $team_livesync_exists->add( + Pair {$team_id, Team::genLiveSyncExists($team_id, "fbctf")}, + ); } - $scores_array[$score->getLevelId()][$teams_array[$score->getTeamId()]]['timestamp'] = - $score->getTs(); - $scores_array[$score->getLevelId()][$teams_array[$score->getTeamId()]]['capture'] = - true; - $scores_array[$score->getLevelId()][$teams_array[$score->getTeamId()]]['hint'] = - false; - $scored_teams[$score->getLevelId()][] = $score->getTeamId(); - } - $all_hints = await HintLog::genAllHints(); - foreach ($all_hints as $hint) { - if ($hint->getPenalty()) { - if (in_array($hint->getTeamId(), array_keys($teams_array)) === + $team_livesync_exists_results = await \HH\Asio\m($team_livesync_exists); + foreach ($team_livesync_exists_results as $team_id => $livesync_exists) { + if ($livesync_exists === true) { + $team_livesync_key->add( + Pair {$team_id, Team::genGetLiveSyncKey($team_id, "fbctf")}, + ); + } + } + $team_livesync_key_results = await \HH\Asio\m($team_livesync_key); + $teams_array = $team_livesync_key_results->toArray(); + + $scores_array = array(); + $scored_teams = array(); + + foreach ($all_scores as $score) { + if (in_array($score->getTeamId(), array_keys($teams_array)) === false) { continue; } - $scores_array[$hint->getLevelId()][$teams_array[$hint->getTeamId()]]['hint'] = + $scores_array[$score->getLevelId()][$teams_array[$score->getTeamId()]]['timestamp'] = + $score->getTs(); + $scores_array[$score->getLevelId()][$teams_array[$score->getTeamId()]]['capture'] = true; - if (in_array( - $hint->getTeamId(), - $scored_teams[$hint->getLevelId()], - ) === - false) { - $scores_array[$hint->getLevelId()][$teams_array[$hint->getTeamId()]]['capture'] = - false; - $scores_array[$hint->getLevelId()][$teams_array[$hint->getTeamId()]]['timestamp'] = - $hint->getTs(); + $scores_array[$score->getLevelId()][$teams_array[$score->getTeamId()]]['hint'] = + false; + $scored_teams[$score->getLevelId()][] = $score->getTeamId(); + } + foreach ($all_hints as $hint) { + if ($hint->getPenalty()) { + if (in_array($hint->getTeamId(), array_keys($teams_array)) === + false) { + continue; + } + $scores_array[$hint->getLevelId()][$teams_array[$hint->getTeamId()]]['hint'] = + true; + if (in_array( + $hint->getTeamId(), + $scored_teams[$hint->getLevelId()], + ) === + false) { + $scores_array[$hint->getLevelId()][$teams_array[$hint->getTeamId()]]['capture'] = + false; + $scores_array[$hint->getLevelId()][$teams_array[$hint->getTeamId()]]['timestamp'] = + $hint->getTs(); + } } } - } - $levels_array = array(); - $all_levels = await Level::genAllLevels(); - foreach ($all_levels as $level) { - $entity = await Country::gen($level->getEntityId()); - $category = await Category::genSingleCategory($level->getCategoryId()); - if (array_key_exists($level->getId(), $scores_array)) { - $score_level_array = $scores_array[$level->getId()]; - } else { - $score_level_array = array(); + $levels_array = array(); + $entities = Map {}; + $categories = Map {}; + foreach ($all_levels as $level) { + $level_id = $level->getId(); + $entities->add(Pair {$level_id, Country::gen($level->getEntityId())}); + $categories->add( + Pair { + $level_id, + Category::genSingleCategory($level->getCategoryId()), + }, + ); } - $one_level = array( - 'active' => $level->getActive(), - 'type' => $level->getType(), - 'title' => $level->getTitle(), - 'description' => $level->getDescription(), - 'entity_iso_code' => $entity->getIsoCode(), - 'category' => $category->getCategory(), - 'points' => $level->getPoints(), - 'bonus' => $level->getBonusFix(), - 'bonus_dec' => $level->getBonusDec(), - 'penalty' => $level->getPenalty(), - 'teams' => $score_level_array, + $entities_results = await \HH\Asio\m($entities); + invariant( + $entities_results instanceof Map, + 'entities_results should of type Map and not null', ); - $levels_array[] = $one_level; - } - $data = $levels_array; + $categories_results = await \HH\Asio\m($categories); + invariant( + $categories_results instanceof Map, + 'categories_results should of type Map and not null', + ); + + foreach ($all_levels as $level) { + $level_id = $level->getId(); + $entity = $entities_results->get($level_id); + invariant( + $entity instanceof Country, + 'entity should of type Country and not null', + ); + + $category = $categories_results->get($level_id); + invariant( + $category instanceof Category, + 'category should of type Category and not null', + ); + + if (array_key_exists($level->getId(), $scores_array)) { + $score_level_array = $scores_array[$level_id]; + } else { + $score_level_array = array(); + } + $one_level = array( + 'active' => $level->getActive(), + 'type' => $level->getType(), + 'title' => $level->getTitle(), + 'description' => $level->getDescription(), + 'entity_iso_code' => $entity->getIsoCode(), + 'category' => $category->getCategory(), + 'points' => $level->getPoints(), + 'bonus' => $level->getBonusFix(), + 'bonus_dec' => $level->getBonusDec(), + 'penalty' => $level->getPenalty(), + 'teams' => $score_level_array, + ); + $levels_array[] = $one_level; + } + + $data = $levels_array; + } else if ($livesync_enabled->getValue() === '0') { + $data['error'] = tr( + 'LiveSync is disabled, please contact the administrator for access.', + ); + } else if (strval($input_auth_key) !== + strval($livesync_auth_key->getValue())) { + $data['error'] = + tr( + 'LiveSync auth key is invalid, please contact the administrator for access.', + ); + } else { + $data['error'] = tr( + 'LiveSync failed, please contact the administrator for assistance.', + ); + } $this->jsonSend($data); } diff --git a/src/scripts/liveimport.php b/src/scripts/liveimport.php index bf47d7ca..c5ea2c50 100644 --- a/src/scripts/liveimport.php +++ b/src/scripts/liveimport.php @@ -60,6 +60,10 @@ class LiveSyncImport { $json = await self::genDownloadData($url, $check_certificates); $data = json_decode($json); if (empty($data) === false) { + if ((!is_array($data)) && (property_exists($data, 'error'))) { + self::debug(true, $url, '!!!', strval($data->error)); + continue; + } foreach ($data as $level) { $mandatories_set = await self::genMandatoriesSet($level); if ($mandatories_set === false) { From dd3c8742f8939b64a2611cd9b1101c906ba5ba9e Mon Sep 17 00:00:00 2001 From: Eric Shulze Date: Thu, 18 May 2017 15:38:03 -0500 Subject: [PATCH 17/18] Temp fix for issue 499 & 500. Forcing Grunt to continue as it is not correctly detecting node_modules in the folder (#502) --- extra/lib.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extra/lib.sh b/extra/lib.sh index 237ddf56..c6bff3d6 100755 --- a/extra/lib.sh +++ b/extra/lib.sh @@ -80,7 +80,7 @@ function run_grunt() { local __mode=$2 cd "$__path" - grunt + grunt --force # grunt watch on the VM will make sure your js files are # properly updated when developing 'remotely' with unison. From 51e06a7094df9d9e66249b40cae387d2150a52fd Mon Sep 17 00:00:00 2001 From: "Justin M. Wray" Date: Fri, 19 May 2017 19:13:40 -0400 Subject: [PATCH 18/18] Merge Deconflict of /dev and /master (#503) * Registration enforcing strong passwords (#442) * Password types in admin * Fully functional password complexity enforcement for registration * lowercase word in text * Adding test for password types regex and fixing all errors for hh_client * Updating outdated schema for tests * Custom branding for icon and text (#448) * Custom branding for icon and text * Replace async calls branding xhp by attributes * Use genRenderBranding in genRenderMobilePage and combine awaitables --- database/schema.sql | 14 +- database/test_schema.sql | 17 +- src/controllers/AdminController.php | 168 ++++++++++++++++--- src/controllers/Controller.php | 25 +++ src/controllers/GameboardController.php | 14 +- src/controllers/IndexController.php | 35 ++-- src/controllers/ViewModeController.php | 14 +- src/controllers/ajax/AdminAjaxController.php | 13 ++ src/controllers/ajax/IndexAjaxController.php | 15 +- src/language/lang_en.php | 16 ++ src/language/lang_es.php | 12 +- src/models/Configuration.php | 29 ++++ src/models/Logo.php | 10 +- src/static/css/scss/_admin.scss | 4 + src/static/css/scss/_icons.scss | 4 + src/static/js/admin.js | 47 +++++- src/static/js/fb-ctf.js | 1 - src/static/js/index.js | 15 +- src/xhp/Custombranding.php | 19 +++ src/xhp/Fbbranding.php | 4 +- tests/models/PasswordTypesTest.php | 21 +++ 21 files changed, 421 insertions(+), 76 deletions(-) create mode 100644 src/xhp/Custombranding.php create mode 100644 tests/models/PasswordTypesTest.php diff --git a/database/schema.sql b/database/schema.sql index 5114666c..da145d61 100644 --- a/database/schema.sql +++ b/database/schema.sql @@ -225,12 +225,15 @@ INSERT INTO `configuration` (field, value, description) VALUES("ldap_domain_suff INSERT INTO `configuration` (field, value, description) VALUES("login", "1", "(Boolean) Ability to login"); INSERT INTO `configuration` (field, value, description) VALUES("login_select", "0", "(Boolean) Login selecting the team"); INSERT INTO `configuration` (field, value, description) VALUES("login_strongpasswords", "0", "(Boolean) Enforce using strong passwords"); -INSERT INTO `configuration` (field, value, description) VALUES("password_type", "1", "(Integer) Type of passwords: See password_types"); +INSERT INTO `configuration` (field, value, description) VALUES("password_type", "1", "(Integer) Type of passwords: See table password_types"); INSERT INTO `configuration` (field, value, description) VALUES("default_bonus", "30", "(Integer) Default value for bonus in levels"); INSERT INTO `configuration` (field, value, description) VALUES("default_bonusdec", "10", "(Integer) Default bonus decrement in levels"); INSERT INTO `configuration` (field, value, description) VALUES("language", "en", "(String) Language of the system"); INSERT INTO `configuration` (field, value, description) VALUES("livesync", "0", "(Boolean) LiveSync functionality"); INSERT INTO `configuration` (field, value, description) VALUES("livesync_auth_key", "", "(String) Optional LiveSync Auth Key"); +INSERT INTO `configuration` (field, value, description) VALUES("custom_logo", "0", "(Boolean) Custom branding logo"); +INSERT INTO `configuration` (field, value, description) VALUES("custom_text", "Powered By Facebook", "(String) Custom branding text"); +INSERT INTO `configuration` (field, value, description) VALUES("custom_logo_image", "static/img/favicon.png", "(String) Custom logo image file"); UNLOCK TABLES; -- @@ -243,17 +246,18 @@ DROP TABLE IF EXISTS `password_types`; CREATE TABLE `password_types` ( `id` int(11) NOT NULL AUTO_INCREMENT, `field` varchar(100) NOT NULL, + `value` text NOT NULL, `description` text NOT NULL, - `regex` text NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `field` (`field`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1; /*!40101 SET character_set_client = @saved_cs_client */; LOCK TABLES `password_types` WRITE; -INSERT INTO `password_types` (field, regex, description) VALUES("1", "/.*^(?=.{8,})(?=.*[a-z])(?=.*[0-9]).*$/", "Length > 8, [a-z] and [0-9]"); -INSERT INTO `password_types` (field, regex, description) VALUES("2", "/.*^(?=.{8,})(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9]).*$/", "Length > 8, [a-z], [A-Z] and [0-9]"); -INSERT INTO `password_types` (field, regex, description) VALUES("3", "/.*^(?=.{8,})(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*\W).*$/", "Length > 8, [a-z], [A-Z], [0-9] and Special chars"); +INSERT INTO `password_types` (field, value, description) VALUES("1", "/.+/", "Length > 0"); +INSERT INTO `password_types` (field, value, description) VALUES("2", "/.*^(?=.{8,})(?=.*[a-z])(?=.*[0-9]).*$/", "Length > 8, [a-z] and [0-9]"); +INSERT INTO `password_types` (field, value, description) VALUES("3", "/.*^(?=.{8,})(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9]).*$/", "Length > 8, [a-z], [A-Z] and [0-9]"); +INSERT INTO `password_types` (field, value, description) VALUES("4", "/.*^(?=.{8,})(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[\\W]+).*$/", "Length > 8, [a-z], [A-Z], [0-9] and Special chars"); UNLOCK TABLES; diff --git a/database/test_schema.sql b/database/test_schema.sql index 8b376076..122c9ce4 100644 --- a/database/test_schema.sql +++ b/database/test_schema.sql @@ -118,11 +118,11 @@ CREATE TABLE `teams` ( `active` tinyint(1) NOT NULL DEFAULT 1, `name` text NOT NULL, `password_hash` text NOT NULL, - `points` int(11) NOT NULL, + `points` int(11) NOT NULL DEFAULT 0, `last_score` timestamp NOT NULL, `logo` text NOT NULL, - `admin` tinyint(1) NOT NULL, - `protected` tinyint(1) NOT NULL, + `admin` tinyint(1) NOT NULL DEFAULT 0, + `protected` tinyint(1) NOT NULL DEFAULT 0, `visible` tinyint(1) NOT NULL DEFAULT 1, `created_ts` timestamp NOT NULL DEFAULT 0, PRIMARY KEY (`id`) @@ -225,7 +225,7 @@ INSERT INTO `configuration` (field, value, description) VALUES("ldap_domain_suff INSERT INTO `configuration` (field, value, description) VALUES("login", "1", "(Boolean) Ability to login"); INSERT INTO `configuration` (field, value, description) VALUES("login_select", "0", "(Boolean) Login selecting the team"); INSERT INTO `configuration` (field, value, description) VALUES("login_strongpasswords", "0", "(Boolean) Enforce using strong passwords"); -INSERT INTO `configuration` (field, value, description) VALUES("password_type", "1", "(Integer) Type of passwords: See password_types"); +INSERT INTO `configuration` (field, value, description) VALUES("password_type", "1", "(Integer) Type of passwords: See table password_types"); INSERT INTO `configuration` (field, value, description) VALUES("default_bonus", "30", "(Integer) Default value for bonus in levels"); INSERT INTO `configuration` (field, value, description) VALUES("default_bonusdec", "10", "(Integer) Default bonus decrement in levels"); INSERT INTO `configuration` (field, value, description) VALUES("language", "en", "(String) Language of the system"); @@ -243,17 +243,18 @@ DROP TABLE IF EXISTS `password_types`; CREATE TABLE `password_types` ( `id` int(11) NOT NULL AUTO_INCREMENT, `field` varchar(100) NOT NULL, + `value` text NOT NULL, `description` text NOT NULL, - `regex` text NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `field` (`field`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1; /*!40101 SET character_set_client = @saved_cs_client */; LOCK TABLES `password_types` WRITE; -INSERT INTO `password_types` (field, regex, description) VALUES("1", "/.*^(?=.{8,})(?=.*[a-z])(?=.*[0-9]).*$/", "Length > 8, [a-z] and [0-9]"); -INSERT INTO `password_types` (field, regex, description) VALUES("2", "/.*^(?=.{8,})(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9]).*$/", "Length > 8, [a-z], [A-Z] and [0-9]"); -INSERT INTO `password_types` (field, regex, description) VALUES("3", "/.*^(?=.{8,})(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*\W).*$/", "Length > 8, [a-z], [A-Z], [0-9] and Special chars"); +INSERT INTO `password_types` (field, value, description) VALUES("1", "/.+/", "Length > 0"); +INSERT INTO `password_types` (field, value, description) VALUES("2", "/.*^(?=.{8,})(?=.*[a-z])(?=.*[0-9]).*$/", "Length > 8, [a-z] and [0-9]"); +INSERT INTO `password_types` (field, value, description) VALUES("3", "/.*^(?=.{8,})(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9]).*$/", "Length > 8, [a-z], [A-Z] and [0-9]"); +INSERT INTO `password_types` (field, value, description) VALUES("4", "/.*^(?=.{8,})(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[\\W]+).*$/", "Length > 8, [a-z], [A-Z], [0-9] and Special chars"); UNLOCK TABLES; diff --git a/src/controllers/AdminController.php b/src/controllers/AdminController.php index f4708025..621a221f 100644 --- a/src/controllers/AdminController.php +++ b/src/controllers/AdminController.php @@ -149,6 +149,25 @@ class="fb--conf--registration_type" return $select; } + // TODO: Translate password types + private async function genStrongPasswordsSelect(): Awaitable<:xhp> { + $types = await Configuration::genAllPasswordTypes(); + $config = await Configuration::genCurrentPasswordType(); + $select = ; + foreach ($types as $type) { + $select->appendChild( + + ); + } + + return $select; + } + private async function genConfigurationDurationSelect(): Awaitable<:xhp> { $config = await Configuration::gen('game_duration_unit'); $duration_unit = $config->getValue(); @@ -295,6 +314,9 @@ class="fb-cta cta--yellow" 'end_ts' => Configuration::gen('end_ts'), 'livesync' => Configuration::gen('livesync'), 'livesync_auth_key' => Configuration::gen('livesync_auth_key'), + 'custom_logo' => Configuration::gen('custom_logo'), + 'custom_text' => Configuration::gen('custom_text'), + 'custom_logo_image' => Configuration::gen('custom_logo_image'), }; $results = await \HH\Asio\m($awaitables); @@ -322,6 +344,9 @@ class="fb-cta cta--yellow" $end_ts = $results['end_ts']; $livesync = $results['livesync']; $livesync_auth_key = $results['livesync_auth_key']; + $custom_logo = $results['custom_logo']; + $custom_text = $results['custom_text']; + $custom_logo_image = $results['custom_logo_image']; $registration_on = $registration->getValue() === '1'; $registration_off = $registration->getValue() === '0'; @@ -343,6 +368,8 @@ class="fb-cta cta--yellow" $timer_off = $timer->getValue() === '0'; $livesync_on = $livesync->getValue() === '1'; $livesync_off = $livesync->getValue() === '0'; + $custom_logo_on = $custom_logo->getValue() === '1'; + $custom_logo_off = $custom_logo->getValue() === '0'; $game_start_array = array(); if ($start_ts->getValue() !== '0' && $start_ts->getValue() !== 'NaN') { @@ -416,6 +443,7 @@ class="fb-cta cta--yellow" 'configuration_duration_select' => $this->genConfigurationDurationSelect(), 'language_select' => $this->genLanguageSelect(), + 'password_types_select' => $this->genStrongPasswordsSelect(), }; $results = await \HH\Asio\m($awaitables); @@ -423,6 +451,44 @@ class="fb-cta cta--yellow" $configuration_duration_select = $results['configuration_duration_select']; $language_select = $results['language_select']; + $password_types_select = $results['password_types_select']; + + if ($login_strongpasswords->getValue() === '0') { // Strong passwords are not enforced + $strong_passwords =
; + } else { + $strong_passwords = +
+ + {$password_types_select} +
; + } + + if ($custom_logo->getValue() === '0') { // Custom branding is not enabled + $custom_logo_xhp =
; + } else { + $custom_logo_xhp = +
+ + getValue()} + /> +
+
+ + {tr('Change')} + +
+ +
; + } return
@@ -527,56 +593,59 @@ class="fb-cta cta--yellow"
-
+
- +
-
-
+
- +
-
+
+ {$strong_passwords} +
@@ -932,11 +1001,59 @@ class="fb-cta cta--yellow"
-

{tr('Language')}

+

{tr('Internationalization')}

+
+
+
+
+ + {$language_select} +
+
+
+
+
+
+

{tr('Branding')}

-
-
- {$language_select} +
+
+
+ +
+ + + + +
+
+
+
+ {$custom_logo_xhp} +
+
+
+ + getValue()} + /> +
@@ -3786,6 +3903,7 @@ public function renderMainContent(): :xhp { {tr('Begin Game')} ; } + $branding_xhp = await $this->genRenderBranding(); return
@@ -3858,7 +3976,7 @@ public function renderMainContent(): :xhp { {tr('Gameboard')} {tr('Logout')} - + {$branding_xhp}
; } diff --git a/src/controllers/Controller.php b/src/controllers/Controller.php index 8a9375d4..2c950ee7 100644 --- a/src/controllers/Controller.php +++ b/src/controllers/Controller.php @@ -7,6 +7,31 @@ abstract protected function getPages(): array; abstract protected function genRenderBody(string $page): Awaitable<:xhp>; + public async function genRenderBranding(): Awaitable<:xhp> { + $awaitables = Map { + 'custom_logo' => Configuration::gen('custom_logo'), + 'custom_text' => Configuration::gen('custom_text'), + 'custom_logo_image' => Configuration::gen('custom_logo_image'), + }; + $results = await \HH\Asio\m($awaitables); + $branding = $results['custom_logo']; + $custom_text = $results['custom_text']; + if ($branding->getValue() === '0') { + $branding_xhp = + getValue()))} + />; + } else { + $custom_logo_image = $results['custom_logo_image']; + $branding_xhp = + getValue())} + brandingLogo={strval($custom_logo_image->getValue())} + />; + } + return $branding_xhp; + } + public async function genRender(): Awaitable<:xhp> { $page = $this->processRequest(); $body = await $this->genRenderBody($page); diff --git a/src/controllers/GameboardController.php b/src/controllers/GameboardController.php index 5f047a57..bd84ad92 100644 --- a/src/controllers/GameboardController.php +++ b/src/controllers/GameboardController.php @@ -23,12 +23,13 @@ protected function getPages(): array { return array('main', 'viewmode'); } - public function renderMainContent(): :xhp { + public async function genRenderMainContent(): Awaitable<:xhp> { if (SessionUtils::sessionAdmin()) { $admin_link =
  • {tr('Admin')}
  • ; } else { $admin_link = null; } + $branding_gen = await $this->genRenderBranding(); return
    @@ -61,7 +62,7 @@ public function renderMainContent(): :xhp { @@ -140,19 +141,20 @@ class="module--outer-right"
    ; } - public function renderPage(string $page): :xhp { + public async function genRenderPage(string $page): Awaitable<:xhp> { switch ($page) { case 'main': - return $this->renderMainContent(); + return await $this->genRenderMainContent(); break; default: - return $this->renderMainContent(); + return await $this->genRenderMainContent(); break; } } <<__Override>> public async function genRenderBody(string $page): Awaitable<:xhp> { + $rendered_page = await $this->genRenderPage($page); return
    - {$this->renderPage($page)} + {$rendered_page}
    ; diff --git a/src/controllers/IndexController.php b/src/controllers/IndexController.php index 0982724d..d7f1920f 100644 --- a/src/controllers/IndexController.php +++ b/src/controllers/IndexController.php @@ -345,6 +345,7 @@ class="fb-main page--team-registration full-height fb-scroll"> name="teamname" type="text" maxlength={20} + autofocus={true} /> {$ldap_domain_suffix}
    @@ -451,6 +452,7 @@ class="fb-main page--registration full-height fb-scroll"> name="teamname" type="text" maxlength={20} + autofocus={true} /> {$ldap_domain_suffix} @@ -458,6 +460,10 @@ class="fb-main page--registration full-height fb-scroll"> +
    + +
    {tr('Password is too simple')}
    +
    {$token_field}
    @@ -726,7 +732,8 @@ public function renderErrorPage(): :xhp { ; } - public function renderMobilePage(): :xhp { + public async function genRenderMobilePage(): Awaitable<:xhp> { + $branding_xhp = await $this->genRenderBranding(); return
    - + {$branding_xhp}
    ; } - public function renderMainNav(): :xhp { + public async function genRenderMainNav(): Awaitable<:xhp> { if (SessionUtils::sessionActive()) { $right_nav = ; + $branding_gen = await $this->genRenderBranding(); + $branding = + ; return ; } @@ -820,7 +830,7 @@ public function renderMainNav(): :xhp { case 'error': return $this->renderErrorPage(); case 'mobile': - return $this->renderMobilePage(); + return await $this->genRenderMobilePage(); case 'login': return await $this->genRenderLoginContent(); case 'registration': @@ -841,11 +851,12 @@ public function renderMainNav(): :xhp { <<__Override>> public async function genRenderBody(string $page): Awaitable<:xhp> { $rendered_page = await $this->genRenderPage($page); + $rendered_nav = await $this->genRenderMainNav(); return
    -
    {$this->renderMainNav()}
    +
    {$rendered_nav}
    {$rendered_page}
    diff --git a/src/controllers/ViewModeController.php b/src/controllers/ViewModeController.php index d2459000..172e92ac 100644 --- a/src/controllers/ViewModeController.php +++ b/src/controllers/ViewModeController.php @@ -23,7 +23,8 @@ protected function getPages(): array { return array('main'); } - public function renderMainContent(): :xhp { + public async function genRenderMainContent(): Awaitable<:xhp> { + $branding_gen = await $this->genRenderBranding(); return
    @@ -31,7 +32,7 @@ public function renderMainContent(): :xhp { @@ -58,24 +59,25 @@ class="module--outer-right active"
    ; } - public function renderPage(string $page): :xhp { + public async function genRenderPage(string $page): Awaitable<:xhp> { switch ($page) { case 'main': - return $this->renderMainContent(); + return await $this->genRenderMainContent(); break; default: - return $this->renderMainContent(); + return await $this->genRenderMainContent(); break; } } <<__Override>> public async function genRenderBody(string $page): Awaitable<:xhp> { + $rendered_page = await $this->genRenderPage($page); return
    - {$this->renderPage($page)} + {$rendered_page}
    ; diff --git a/src/controllers/ajax/AdminAjaxController.php b/src/controllers/ajax/AdminAjaxController.php index d2fea5af..832a6e27 100644 --- a/src/controllers/ajax/AdminAjaxController.php +++ b/src/controllers/ajax/AdminAjaxController.php @@ -29,6 +29,10 @@ protected function getFilters(): array { 'filter' => FILTER_VALIDATE_REGEXP, 'options' => array('regexp' => '/^[\w-]+$/'), ), + 'logo_b64' => array( + 'filter' => FILTER_VALIDATE_REGEXP, + 'options' => array('regexp' => '/^[\w+-\/]+={0,2}$/'), + ), 'entity_id' => FILTER_VALIDATE_INT, 'attachment_id' => FILTER_VALIDATE_INT, 'filename' => array( @@ -117,6 +121,7 @@ protected function getActions(): array { 'delete_link', 'begin_game', 'change_configuration', + 'change_custom_logo', 'create_announcement', 'delete_announcement', 'create_tokens', @@ -407,6 +412,14 @@ protected function getActions(): array { } else { return Utils::error_response('Invalid configuration', 'admin'); } + case 'change_custom_logo': + $logo = must_have_string($params, 'logo_b64'); + $custom_logo = await Logo::genCreateCustom($logo, true); + if ($custom_logo) { + return Utils::ok_response('Success', 'admin'); + } else { + return Utils::error_response('Error changing logo', 'admin'); + } case 'create_announcement': await Announcement::genCreate( must_have_string($params, 'announcement'), diff --git a/src/controllers/ajax/IndexAjaxController.php b/src/controllers/ajax/IndexAjaxController.php index e087d8f2..5e164e69 100644 --- a/src/controllers/ajax/IndexAjaxController.php +++ b/src/controllers/ajax/IndexAjaxController.php @@ -117,8 +117,18 @@ protected function getActions(): array { return Utils::error_response('Registration failed', 'registration'); } + // Check if strongs passwords are enforced + $login_strongpasswords = await Configuration::gen('login_strongpasswords'); + if ($login_strongpasswords->getValue() !== '0') { + $password_type = await Configuration::genCurrentPasswordType(); + if (!preg_match(strval($password_type->getValue()), $password)) { + return Utils::error_response('Password too simple', 'registration'); + } + } + // Check if ldap is enabled and verify credentials if successful $ldap = await Configuration::gen('ldap'); + $ldap_password = ''; if ($ldap->getValue() === '1') { // Get server information from configuration $ldap_server = await Configuration::gen('ldap_server'); @@ -146,10 +156,11 @@ protected function getActions(): array { // Use randomly generated password for local account for LDAP users // This will help avoid leaking users ldap passwords if the server's database // is compromised. - $password = gmp_strval( + $ldap_password = $password; + $password = strval(gmp_strval( gmp_init(bin2hex(openssl_random_pseudo_bytes(16)), 16), 62, - ); + )); } // Check if tokenized registration is enabled diff --git a/src/language/lang_en.php b/src/language/lang_en.php index 4a572053..77563863 100644 --- a/src/language/lang_en.php +++ b/src/language/lang_en.php @@ -85,6 +85,12 @@ 'Password', 'Choose an Emblem' => 'Choose an Emblem', + 'or upload your own' => + 'or upload your own', + 'Clear your custom emblem to use a default emblem.' => + 'Clear your custom emblem to use a default emblem.', + 'Password is too simple' => + 'Password is too simple', 'Sign Up' => 'Sign Up', 'Register to play Capture The Flag here. Once you have registered, you will be logged in.' => @@ -229,8 +235,18 @@ 'Begin Time', 'Expected End Time' => 'Expected End Time', + 'Internationalization' => + 'Internationalization', 'Language' => 'Language', + 'Branding' => + 'Branding', + 'Custom Logo' => + 'Custom Logo', + 'Logo' => + 'Logo', + 'Custom Text' => + 'Custom Text', 'DELETE' => 'DELETE', 'Delete' => diff --git a/src/language/lang_es.php b/src/language/lang_es.php index 9d6e19b0..5903087b 100644 --- a/src/language/lang_es.php +++ b/src/language/lang_es.php @@ -229,8 +229,18 @@ 'Tiempo de Inicio', 'Expected End Time' => 'Tiempo de Finalización Esperada', + 'Internationalization' => + 'Internacionalización', 'Language' => - 'Lenguaje', + 'Idioma', + 'Branding' => + 'Personalización', + 'Custom Logo' => + 'Logo Personalizado', + 'Logo' => + 'Logo', + 'Custom Text' => + 'Texto Personalizado', 'DELETE' => 'BORRAR', 'Delete' => diff --git a/src/models/Configuration.php b/src/models/Configuration.php index 7d1355db..dc344e40 100644 --- a/src/models/Configuration.php +++ b/src/models/Configuration.php @@ -78,6 +78,35 @@ public function getDescription(): string { return intval(idx(firstx($result->mapRows()), 'COUNT(*)')) > 0; } + // All the password types. + public static async function genAllPasswordTypes( + ): Awaitable> { + $db = await self::genDb(); + $result = await $db->queryf('SELECT * FROM password_types'); + + $types = array(); + foreach ($result->mapRows() as $row) { + $types[] = self::configurationFromRow($row->toArray()); + } + + return $types; + } + + // Current password type. + public static async function genCurrentPasswordType( + ): Awaitable { + $db = await self::genDb(); + $db_result = await $db->queryf( + 'SELECT * FROM password_types WHERE field = (SELECT value FROM configuration WHERE field = %s) LIMIT 1', + 'password_type' + ); + + invariant($db_result->numRows() === 1, 'Expected exactly one result'); + $result = firstx($db_result->mapRows())->toArray(); + + return self::configurationFromRow($result); + } + // All the configuration. public static async function genAllConfiguration( ): Awaitable> { diff --git a/src/models/Logo.php b/src/models/Logo.php index bd7bcc54..a4f69ec5 100644 --- a/src/models/Logo.php +++ b/src/models/Logo.php @@ -252,6 +252,7 @@ private static function logoFromRow(Map $row): Logo { // Create custom logo public static async function genCreateCustom( string $base64_data, + bool $branding = false, ): Awaitable { // Check image size $image_size_bytes = strlen($base64_data) * self::BASE64_BYTES_PER_CHAR; @@ -269,6 +270,8 @@ private static function logoFromRow(Map $row): Logo { // Get image properties and verify mimetype $base64_data = str_replace(' ', '+', $base64_data); $binary_data = base64_decode(str_replace(' ', '+', $base64_data)); + /* HH_IGNORE_ERROR[2049] */ + /* HH_IGNORE_ERROR[4107] */ $image_info = getimagesizefromstring($binary_data); $mimetype = $image_info[2]; @@ -295,8 +298,6 @@ private static function logoFromRow(Map $row): Logo { ); } - $db = await self::genDb(); - $used = true; $enabled = true; $protected = false; @@ -310,6 +311,11 @@ private static function logoFromRow(Map $row): Logo { $filepath, ); + // If created logo is for branding, set configuration value + if ($branding) { + await Configuration::genUpdate('custom_logo_image', $logo->getLogo()); + } + // Return newly created logo_id return $logo; } diff --git a/src/static/css/scss/_admin.scss b/src/static/css/scss/_admin.scss index 69f53f91..e887c034 100644 --- a/src/static/css/scss/_admin.scss +++ b/src/static/css/scss/_admin.scss @@ -139,6 +139,10 @@ General Admin } } + #custom-logo-input { + display: none; + } + input:not(:checked) + label { color: $teal-blue; } diff --git a/src/static/css/scss/_icons.scss b/src/static/css/scss/_icons.scss index 96cc5580..34f3300c 100644 --- a/src/static/css/scss/_icons.scss +++ b/src/static/css/scss/_icons.scss @@ -9,6 +9,10 @@ vertical-align: middle; } +.icon-text { + vertical-align: middle; +} + .has-icon { vertical-align: middle; } diff --git a/src/static/js/admin.js b/src/static/js/admin.js index fd68f1dd..4a811847 100644 --- a/src/static/js/admin.js +++ b/src/static/js/admin.js @@ -940,7 +940,10 @@ function toggleConfiguration(radio_id) { field: radio_action, value: action_value }; - if (radio_action) { + var refresh_fields = ['login_strongpasswords', 'custom_logo']; + if (refresh_fields.indexOf(radio_action) !== -1) { + sendAdminRequest(toggle_data, true); + } else { sendAdminRequest(toggle_data, false); } } @@ -951,9 +954,10 @@ function changeConfiguration(field, value) { field: field, value: value }; - if (field === 'registration_type' || field === 'language') { + var refresh_fields = ['registration_type', 'language']; + if (refresh_fields.indexOf(field) !== -1) { sendAdminRequest(conf_data, true); - }else { + } else { sendAdminRequest(conf_data, false); } } @@ -1431,5 +1435,42 @@ module.exports = { $('#restore_database').click(databaseRestore); }); }); + + // custom logo file selector + var $customLogoInput = $('#custom-logo-input'); + var $customLogoImage = $('#custom-logo-image'); + $('#custom-logo-link').on('click', function() { + $customLogoInput.trigger('click'); + }); + // on file input change, set image + $customLogoInput.change(function() { + var input = this; + if (input.files && input.files[0]) { + if (input.files[0].size > (1000*1024)) { + alert('Please upload an image less than 1000KB!'); + return; + } + + var reader = new FileReader(); + + reader.onload = function (e) { + $customLogoImage.attr('src', e.target.result); + var rawImageData = e.target.result; + var filetypeBeginIdx = rawImageData.indexOf('/') + 1; + var filetypeEndIdx = rawImageData.indexOf(';'); + var filetype = rawImageData.substring(filetypeBeginIdx, filetypeEndIdx); + var base64 = rawImageData.substring(rawImageData.indexOf(',') + 1); + var logo_data = { + action: 'change_custom_logo', + logoType: filetype, + logo_b64: base64 + }; + sendAdminRequest(logo_data, true); + }; + + reader.readAsDataURL(input.files[0]); + + } + }); } }; diff --git a/src/static/js/fb-ctf.js b/src/static/js/fb-ctf.js index 3d8e57e9..fce831ac 100644 --- a/src/static/js/fb-ctf.js +++ b/src/static/js/fb-ctf.js @@ -2571,7 +2571,6 @@ function setupInputListeners() { }); // on file input change, set image preview and emblem carousel notice $customEmblemInput.change(function() { -console.log('foo'); var input = this; if (input.files && input.files[0]) { if (input.files[0].size > (1000*1024)) { diff --git a/src/static/js/index.js b/src/static/js/index.js index 32e04954..289fa87f 100644 --- a/src/static/js/index.js +++ b/src/static/js/index.js @@ -7,10 +7,16 @@ function teamNameFormError() { }); } -function teamPasswordFormError() { +function teamPasswordFormError(toosimple) { $('.el--text')[1].classList.add('form-error'); + if (toosimple) { + $('#password_error')[0].classList.remove('completely-hidden'); + } $('.fb-form input[name="password"]').on('change', function() { $('.el--text')[1].classList.remove('form-error'); + if (toosimple) { + $('#password_error')[0].classList.add('completely-hidden'); + } }); } @@ -109,9 +115,10 @@ function sendIndexRequest(request_data) { goToPage(responseData.redirect); } else { // TODO: Make this a modal - console.log('Failed'); - teamNameFormError(); - teamPasswordFormError(); + verifyTeamName('register'); + if (responseData.message === 'Password too simple') { + teamPasswordFormError(true); + } teamTokenFormError(); } }); diff --git a/src/xhp/Custombranding.php b/src/xhp/Custombranding.php new file mode 100644 index 00000000..e693700e --- /dev/null +++ b/src/xhp/Custombranding.php @@ -0,0 +1,19 @@ + + :brandingLogo}/> +
    + {$this->:brandingText} + ; + } +} diff --git a/src/xhp/Fbbranding.php b/src/xhp/Fbbranding.php index 158e1c95..28f6020b 100644 --- a/src/xhp/Fbbranding.php +++ b/src/xhp/Fbbranding.php @@ -2,6 +2,8 @@ class :fbbranding extends :x:element { category %flow; + attribute + string brandingText; protected string $tagName = 'fbbranding'; @@ -11,7 +13,7 @@ protected function render(): XHPRoot { - {tr('Powered By Facebook')} + {' '}{$this->:brandingText} ; } } diff --git a/tests/models/PasswordTypesTest.php b/tests/models/PasswordTypesTest.php new file mode 100644 index 00000000..3680eefa --- /dev/null +++ b/tests/models/PasswordTypesTest.php @@ -0,0 +1,21 @@ +assertEquals(4, count($all)); + + $p = $all[0]; + $this->assertTrue((bool)preg_match($p->getValue(), 'a')); + + $p = $all[1]; + $this->assertTrue((bool)preg_match($p->getValue(), 'password1')); + + $p = $all[2]; + $this->assertTrue((bool)preg_match($p->getValue(), 'Password1')); + + $p = $all[3]; + $this->assertTrue((bool)preg_match($p->getValue(), 'Pas$word1')); + } +}