diff --git a/.github/update-composer-json.php b/.github/update-composer-json.php
new file mode 100644
index 000000000..2144c54fb
--- /dev/null
+++ b/.github/update-composer-json.php
@@ -0,0 +1,137 @@
+ 'package', // The type is at the root level of the repository entry
+ 'package' => [
+ 'name' => $packageName,
+ 'version' => $packageVersionRange,
+ 'type' => 'library', // Default type, you can adjust this if needed
+ 'dist' => [
+ 'url' => $repositoryUrl,
+ 'type' => $distType, // Use the dynamically derived dist type
+ 'reference' => $packageHash // Adding the hash (reference) if available
+ ]
+ ]
+ ];
+ // Track pinned package
+ $pinnedPackages[] = $packageName;
+ }
+}
+
+// Write the updated composer.json back to the file
+if (file_put_contents($composerJsonPath, json_encode($composerJson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . PHP_EOL)) {
+ echo "composer.json has been updated with the versions from composer.lock.\n";
+ if (count($pinnedPackages) > 0) {
+ echo "The following packages were pinned to repositories:\n";
+ foreach ($pinnedPackages as $packageName) {
+ echo " - $packageName\n";
+ }
+ }
+} else {
+ echo "Failed to update composer.json.\n";
+ exit(1);
+}
diff --git a/.github/workflows/auto-assign-pr.yml b/.github/workflows/auto-assign-pr.yml
new file mode 100644
index 000000000..fecb812bc
--- /dev/null
+++ b/.github/workflows/auto-assign-pr.yml
@@ -0,0 +1,24 @@
+name: Auto-Assign PR
+
+on:
+ pull_request_target:
+ types: [opened]
+
+permissions:
+ issues: write
+
+jobs:
+ assign:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Assign PR to repo owner
+ uses: actions/github-script@v8
+ with:
+ github-token: ${{ secrets.GH_ADMIN_TOKEN }}
+ script: |
+ await github.rest.issues.addAssignees({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ assignees: ["proditis"]
+ });
diff --git a/.markdownlint.yaml b/.markdownlint.yaml
index 7ace1f4be..6a7d02cde 100644
--- a/.markdownlint.yaml
+++ b/.markdownlint.yaml
@@ -3,4 +3,6 @@ MD012: false
MD013: false
# Disable required empty line after heading
MD022: false
-MD033: false
\ No newline at end of file
+MD033: false
+MD040: false
+MD043: false
\ No newline at end of file
diff --git a/ansible/Dockerfiles/sanitycheck/variables.yml b/ansible/Dockerfiles/sanitycheck/variables.yml
index 9766edd51..1762de0cb 100644
--- a/ansible/Dockerfiles/sanitycheck/variables.yml
+++ b/ansible/Dockerfiles/sanitycheck/variables.yml
@@ -12,6 +12,7 @@ writeup_allowed: 0
headshot_spin: 0
instance_allowed: 0
TargetOndemand: false
+dynamic_treasures: 0
container:
name: "{{hostname}}"
hostname: "{{fqdn}}"
diff --git a/ansible/files/pui.service.conf b/ansible/files/pui.service.conf
index fb1fa8ea0..9dd62ce24 100644
--- a/ansible/files/pui.service.conf
+++ b/ansible/files/pui.service.conf
@@ -1,6 +1,8 @@
# Allow users to connect to port 80/443
pass in quick on egress inet proto tcp from {
= Html::a('Create Player', ['create'], ['class' => 'btn btn-success']) ?>
= Html::a('Import Players', ['import'], ['class' => 'btn btn-info']) ?>
- = Html::a('Export Full Players', ['export'], ['class' => 'btn','style' => 'background: #4040bf; color: white;',]) ?>
+ = Html::a('Export Full Players', ['export'], ['class' => 'btn', 'style' => 'background: #4040bf; color: white;',]) ?>
= Html::a(Yii::t('app', 'Fail Validate'), ['fail-validation'], [
'class' => 'btn',
'style' => 'background: #4d246f; color: white;',
@@ -45,7 +45,7 @@
'rowOptions' => function ($model, $key, $index, $grid) {
// $model is the current data model being rendered
// check your condition in the if like `if($model->hasMedicalRecord())` which could be a method of model class which checks for medical records.
- $tmpObj = clone ($model);
+ $tmpObj = clone($model);
$tmpObj->scenario = 'validator';
if (!$tmpObj->validate()) {
unset($tmpObj);
@@ -112,9 +112,12 @@
[
'attribute' => 'approval',
'filter' => $searchModel::APPROVAL,
+ 'format' => 'html',
'visible' => Yii::$app->sys->player_require_approval === true,
'value' => function ($model) {
- return $model::APPROVAL[$model->approval];
+ if ($model->status === 10)
+ return "(" . $model::APPROVAL[$model->approval] . ")";
+ return "" . $model::APPROVAL[$model->approval] . "";
}
],
@@ -138,7 +141,7 @@
return false;
},
'disconnect-vpn' => function ($model) {
- if ($model->last->vpn_local_address !== null && $model->disconnectQueue===null) return true;
+ if ($model->last->vpn_local_address !== null && $model->disconnectQueue === null) return true;
return false;
},
'view' => function ($model) {
@@ -157,7 +160,7 @@
return false;
},
'mail' => function ($model) {
- if ($model->status == 10 || $model->approval == 0) return false;
+ if ($model->status == 10 || $model->status == 0 || $model->approval == 0 || ( $model->emailToken == null && $model->passwordResetToken == null)) return false;
return true;
},
'delete' => function ($model) {
diff --git a/backend/modules/gameplay/controllers/ChallengeController.php b/backend/modules/gameplay/controllers/ChallengeController.php
index 893a293c5..281693787 100644
--- a/backend/modules/gameplay/controllers/ChallengeController.php
+++ b/backend/modules/gameplay/controllers/ChallengeController.php
@@ -22,167 +22,171 @@ class ChallengeController extends \app\components\BaseController
/**
* {@inheritdoc}
*/
- public function behaviors()
- {
- return ArrayHelper::merge(parent::behaviors(),[]);
- }
+ public function behaviors()
+ {
+ return ArrayHelper::merge(parent::behaviors(), [
+ 'access' => [
+ 'class' => \yii\filters\AccessControl::class,
+ 'rules' => [
+ 'authActions' => [
+ 'allow' => true,
+ 'actions' => ['index', 'view'],
+ 'roles' => ['@'],
+ 'matchCallback' => function () {
+ return \Yii::$app->user->identity->isAdmin;
+ },
+ ],
+ ],
+ ],
+ ]);
+ }
- /**
- * Lists all Challenge models.
- * @return mixed
- */
- public function actionIndex()
- {
- $searchModel=new ChallengeSearch();
- $dataProvider=$searchModel->search(Yii::$app->request->queryParams);
-
- return $this->render('index', [
- 'searchModel' => $searchModel,
- 'dataProvider' => $dataProvider,
- ]);
- }
+ /**
+ * Lists all Challenge models.
+ * @return mixed
+ */
+ public function actionIndex()
+ {
+ $searchModel = new ChallengeSearch();
+ $dataProvider = $searchModel->search(Yii::$app->request->queryParams);
- /**
- * Displays a single Challenge model.
- * @param integer $id
- * @return mixed
- * @throws NotFoundHttpException if the model cannot be found
- */
- public function actionView($id)
- {
- $query=Question::find()->joinWith('challenge');
-
- $query->select('question.*,(SELECT COUNT(question_id) FROM player_question WHERE question.id=player_question.question_id) as answered');
- // add conditions that should always apply here
-
- $dataProvider=new ActiveDataProvider([
- 'query' => $query,
- ]);
- $query->andFilterWhere([
- 'question.challenge_id' => $id,
- ]);
-
- $dataProvider->setSort([
- 'defaultOrder' => ['weight' => SORT_ASC,'id'=>SORT_ASC],
- 'attributes' => array_merge(
- $dataProvider->getSort()->attributes,
- [
- 'challengename' => [
- 'asc' => ['challengename' => SORT_ASC],
- 'desc' => ['challengename' => SORT_DESC],
- ],
- 'answered' => [
- 'asc' => ['answered' => SORT_ASC],
- 'desc' => ['answered' => SORT_DESC],
- ],
- ]
- ),
- ]);
-
- return $this->render('view', [
- 'model' => $this->findModel($id),
- 'questionProvider'=>$dataProvider,
- ]);
- }
+ return $this->render('index', [
+ 'searchModel' => $searchModel,
+ 'dataProvider' => $dataProvider,
+ ]);
+ }
- /**
- * Creates a new Challenge model.
- * If creation is successful, the browser will be redirected to the 'view' page.
- * @return mixed
- */
- public function actionCreate()
- {
- $model=new Challenge();
-
- if($model->load(Yii::$app->request->post()) && $model->save())
- {
- $model->file=UploadedFile::getInstance($model, 'file');
- try
- {
- if($model->file)
- {
- if(trim($model->filename)==='')
- {
- $model->filename=$model->id;
- $model->updateAttributes(['filename'=>$model->id]);
- }
- $model->file->saveAs(Yii::getAlias(Yii::$app->sys->challenge_home).'/'.$model->filename);
- }
- Yii::$app->session->addFlash('success', Yii::t('app','Challenge [{name}] created',['name'=>Html::encode($model->name)]));
- Yii::$app->session->addFlash('warning', Yii::t('app','Don\'t forget to create a question for the challenge.'));
- }
- catch(\Exception $e)
- {
- Yii::$app->session->setFlash('error', Yii::t('app','Failed to create challenge [{name}]',['name'=>Html::encode($model->name)]));
- }
- return $this->redirect(['view', 'id' => $model->id]);
- }
+ /**
+ * Displays a single Challenge model.
+ * @param integer $id
+ * @return mixed
+ * @throws NotFoundHttpException if the model cannot be found
+ */
+ public function actionView($id)
+ {
+ $query = Question::find()->joinWith('challenge');
+
+ $query->select('question.*,(SELECT COUNT(question_id) FROM player_question WHERE question.id=player_question.question_id) as answered');
+ // add conditions that should always apply here
+
+ $dataProvider = new ActiveDataProvider([
+ 'query' => $query,
+ ]);
+ $query->andFilterWhere([
+ 'question.challenge_id' => $id,
+ ]);
+
+ $dataProvider->setSort([
+ 'defaultOrder' => ['weight' => SORT_ASC, 'id' => SORT_ASC],
+ 'attributes' => array_merge(
+ $dataProvider->getSort()->attributes,
+ [
+ 'challengename' => [
+ 'asc' => ['challengename' => SORT_ASC],
+ 'desc' => ['challengename' => SORT_DESC],
+ ],
+ 'answered' => [
+ 'asc' => ['answered' => SORT_ASC],
+ 'desc' => ['answered' => SORT_DESC],
+ ],
+ ]
+ ),
+ ]);
+
+ return $this->render('view', [
+ 'model' => $this->findModel($id),
+ 'questionProvider' => $dataProvider,
+ ]);
+ }
- return $this->render('create', [
- 'model' => $model,
- ]);
+ /**
+ * Creates a new Challenge model.
+ * If creation is successful, the browser will be redirected to the 'view' page.
+ * @return mixed
+ */
+ public function actionCreate()
+ {
+ $model = new Challenge();
+
+ if ($model->load(Yii::$app->request->post()) && $model->save()) {
+ $model->file = UploadedFile::getInstance($model, 'file');
+ try {
+ if ($model->file) {
+ if (trim($model->filename) === '') {
+ $model->filename = $model->id;
+ $model->updateAttributes(['filename' => $model->id]);
+ }
+ $model->file->saveAs(Yii::getAlias(Yii::$app->sys->challenge_home) . '/' . $model->filename);
+ }
+ Yii::$app->session->addFlash('success', Yii::t('app', 'Challenge [{name}] created', ['name' => Html::encode($model->name)]));
+ Yii::$app->session->addFlash('warning', Yii::t('app', 'Don\'t forget to create a question for the challenge.'));
+ } catch (\Exception $e) {
+ Yii::$app->session->setFlash('error', Yii::t('app', 'Failed to create challenge [{name}]', ['name' => Html::encode($model->name)]));
+ }
+ return $this->redirect(['view', 'id' => $model->id]);
}
- /**
- * Updates an existing Challenge model.
- * If update is successful, the browser will be redirected to the 'view' page.
- * @param integer $id
- * @return mixed
- * @throws NotFoundHttpException if the model cannot be found
- */
- public function actionUpdate($id)
- {
- $model=$this->findModel($id);
-
- if($model->load(Yii::$app->request->post()) && $model->save())
- {
- $model->file=UploadedFile::getInstance($model, 'file');
- if($model->file !== null)
- {
- if(trim($model->filename)==='')
- {
- $model->filename=$model->id;
- $model->updateAttributes(['filename'=>$model->id]);
- }
- $model->file->saveAs(Yii::getAlias(Yii::$app->sys->challenge_home).'/'.$model->filename);
- }
- Yii::$app->session->addFlash('success', Yii::t('app','Challenge [{name}] updated',['name'=>Html::encode($model->name)]));
- return $this->redirect(['view', 'id' => $model->id]);
- }
+ return $this->render('create', [
+ 'model' => $model,
+ ]);
+ }
- return $this->render('update', [
- 'model' => $model,
- ]);
+ /**
+ * Updates an existing Challenge model.
+ * If update is successful, the browser will be redirected to the 'view' page.
+ * @param integer $id
+ * @return mixed
+ * @throws NotFoundHttpException if the model cannot be found
+ */
+ public function actionUpdate($id)
+ {
+ $model = $this->findModel($id);
+
+ if ($model->load(Yii::$app->request->post()) && $model->save()) {
+ $model->file = UploadedFile::getInstance($model, 'file');
+ if ($model->file !== null) {
+ if (trim($model->filename) === '') {
+ $model->filename = $model->id;
+ $model->updateAttributes(['filename' => $model->id]);
+ }
+ $model->file->saveAs(Yii::getAlias(Yii::$app->sys->challenge_home) . '/' . $model->filename);
+ }
+ Yii::$app->session->addFlash('success', Yii::t('app', 'Challenge [{name}] updated', ['name' => Html::encode($model->name)]));
+ return $this->redirect(['view', 'id' => $model->id]);
}
- /**
- * Deletes an existing Challenge model.
- * If deletion is successful, the browser will be redirected to the 'index' page.
- * @param integer $id
- * @return mixed
- * @throws NotFoundHttpException if the model cannot be found
- */
- public function actionDelete($id)
- {
- $this->findModel($id)->delete();
-
- return $this->redirect(['index']);
- }
+ return $this->render('update', [
+ 'model' => $model,
+ ]);
+ }
- /**
- * Finds the Challenge model based on its primary key value.
- * If the model is not found, a 404 HTTP exception will be thrown.
- * @param integer $id
- * @return Challenge the loaded model
- * @throws NotFoundHttpException if the model cannot be found
- */
- protected function findModel($id)
- {
- if(($model=Challenge::findOne($id)) !== null)
- {
- return $model;
- }
+ /**
+ * Deletes an existing Challenge model.
+ * If deletion is successful, the browser will be redirected to the 'index' page.
+ * @param integer $id
+ * @return mixed
+ * @throws NotFoundHttpException if the model cannot be found
+ */
+ public function actionDelete($id)
+ {
+ $this->findModel($id)->delete();
+
+ return $this->redirect(['index']);
+ }
- throw new NotFoundHttpException(Yii::t('app','The requested page does not exist.'));
+ /**
+ * Finds the Challenge model based on its primary key value.
+ * If the model is not found, a 404 HTTP exception will be thrown.
+ * @param integer $id
+ * @return Challenge the loaded model
+ * @throws NotFoundHttpException if the model cannot be found
+ */
+ protected function findModel($id)
+ {
+ if (($model = Challenge::findOne($id)) !== null) {
+ return $model;
}
+
+ throw new NotFoundHttpException(Yii::t('app', 'The requested page does not exist.'));
+ }
}
diff --git a/backend/modules/gameplay/controllers/QuestionController.php b/backend/modules/gameplay/controllers/QuestionController.php
index d02804c92..83a49e414 100644
--- a/backend/modules/gameplay/controllers/QuestionController.php
+++ b/backend/modules/gameplay/controllers/QuestionController.php
@@ -17,113 +17,124 @@ class QuestionController extends \app\components\BaseController
/**
* {@inheritdoc}
*/
- public function behaviors()
- {
- return ArrayHelper::merge(parent::behaviors(),[]);
- }
+ public function behaviors()
+ {
+ return ArrayHelper::merge(parent::behaviors(), [
+ 'access' => [
+ 'class' => \yii\filters\AccessControl::class,
+ 'rules' => [
+ 'authActions' => [
+ 'allow' => true,
+ 'actions' => ['index', 'view'],
+ 'roles' => ['@'],
+ 'matchCallback' => function () {
+ return \Yii::$app->user->identity->isAdmin;
+ },
+ ],
+ ],
+ ],
+
+ ]);
+ }
- /**
- * Lists all Question models.
- * @return mixed
- */
- public function actionIndex()
- {
- $searchModel=new QuestionSearch();
- $dataProvider=$searchModel->search(Yii::$app->request->queryParams);
-
- return $this->render('index', [
- 'searchModel' => $searchModel,
- 'dataProvider' => $dataProvider,
- ]);
- }
+ /**
+ * Lists all Question models.
+ * @return mixed
+ */
+ public function actionIndex()
+ {
+ $searchModel = new QuestionSearch();
+ $dataProvider = $searchModel->search(Yii::$app->request->queryParams);
- /**
- * Displays a single Question model.
- * @param integer $id
- * @return mixed
- * @throws NotFoundHttpException if the model cannot be found
- */
- public function actionView($id)
- {
- return $this->render('view', [
- 'model' => $this->findModel($id),
- ]);
- }
+ return $this->render('index', [
+ 'searchModel' => $searchModel,
+ 'dataProvider' => $dataProvider,
+ ]);
+ }
- /**
- * Creates a new Question model.
- * If creation is successful, the browser will be redirected to the 'view' page.
- * @return mixed
- */
- public function actionCreate()
- {
- $model=new Question();
- if(\app\modules\gameplay\models\Challenge::find()->count() == 0)
- {
- // If there are no player redirect to create player page
- Yii::$app->session->setFlash('warning', Yii::t('app',"No Challenges found create one first."));
- return $this->redirect(['/frontend/challenge/create']);
- }
-
- if($model->load(Yii::$app->request->post()) && $model->save())
- {
- return $this->redirect(['view', 'id' => $model->id]);
- }
-
- return $this->render('create', [
- 'model' => $model,
- ]);
+ /**
+ * Displays a single Question model.
+ * @param integer $id
+ * @return mixed
+ * @throws NotFoundHttpException if the model cannot be found
+ */
+ public function actionView($id)
+ {
+ return $this->render('view', [
+ 'model' => $this->findModel($id),
+ ]);
+ }
+
+ /**
+ * Creates a new Question model.
+ * If creation is successful, the browser will be redirected to the 'view' page.
+ * @return mixed
+ */
+ public function actionCreate()
+ {
+ $model = new Question();
+ if (\app\modules\gameplay\models\Challenge::find()->count() == 0) {
+ // If there are no player redirect to create player page
+ Yii::$app->session->setFlash('warning', Yii::t('app', "No Challenges found create one first."));
+ return $this->redirect(['/frontend/challenge/create']);
}
- /**
- * Updates an existing Question model.
- * If update is successful, the browser will be redirected to the 'view' page.
- * @param integer $id
- * @return mixed
- * @throws NotFoundHttpException if the model cannot be found
- */
- public function actionUpdate($id)
- {
- $model=$this->findModel($id);
-
- if($model->load(Yii::$app->request->post()) && $model->save())
- {
- return $this->redirect(['view', 'id' => $model->id]);
- }
-
- return $this->render('update', [
- 'model' => $model,
- ]);
+ if ($model->load(Yii::$app->request->post()) && $model->save()) {
+ return $this->redirect(['view', 'id' => $model->id]);
}
- /**
- * Deletes an existing Question model.
- * If deletion is successful, the browser will be redirected to the 'index' page.
- * @param integer $id
- * @return mixed
- * @throws NotFoundHttpException if the model cannot be found
- */
- public function actionDelete($id)
- {
- $this->findModel($id)->delete();
-
- return $this->redirect(['index']);
+ return $this->render('create', [
+ 'model' => $model,
+ ]);
+ }
+
+ /**
+ * Updates an existing Question model.
+ * If update is successful, the browser will be redirected to the 'view' page.
+ * @param integer $id
+ * @return mixed
+ * @throws NotFoundHttpException if the model cannot be found
+ */
+ public function actionUpdate($id)
+ {
+ $model = $this->findModel($id);
+
+ if ($model->load(Yii::$app->request->post()) && $model->save()) {
+ return $this->redirect(['view', 'id' => $model->id]);
}
- /**
- * Finds the Question model based on its primary key value.
- * If the model is not found, a 404 HTTP exception will be thrown.
- * @param integer $id
- * @return Question the loaded model
- * @throws NotFoundHttpException if the model cannot be found
- */
- protected function findModel($id)
- {
- if(($model=Question::findOne($id)) !== null)
- {
- return $model;
- }
-
- throw new NotFoundHttpException(Yii::t('app','The requested page does not exist.'));
+ return $this->render('update', [
+ 'model' => $model,
+ ]);
+ }
+
+ /**
+ * Deletes an existing Question model.
+ * If deletion is successful, the browser will be redirected to the 'index' page.
+ * @param integer $id
+ * @return mixed
+ * @throws NotFoundHttpException if the model cannot be found
+ */
+ public function actionDelete($id)
+ {
+ $this->findModel($id)->delete();
+
+ return $this->redirect(['index']);
+ }
+
+ /**
+ * Finds the Question model based on its primary key value.
+ * If the model is not found, a 404 HTTP exception will be thrown.
+ * @param integer $id
+ * @return Question the loaded model
+ * @throws NotFoundHttpException if the model cannot be found
+ */
+ protected function findModel($id)
+ {
+ if (($model = Question::findOne($id)) !== null) {
+ return $model;
}
+
+ throw new NotFoundHttpException(Yii::t('app', 'The requested page does not exist.'));
+ }
}
diff --git a/backend/modules/gameplay/controllers/TreasureController.php b/backend/modules/gameplay/controllers/TreasureController.php
index 8b25e255a..9937f722e 100644
--- a/backend/modules/gameplay/controllers/TreasureController.php
+++ b/backend/modules/gameplay/controllers/TreasureController.php
@@ -19,7 +19,21 @@ class TreasureController extends \app\components\BaseController
*/
public function behaviors()
{
- return ArrayHelper::merge(parent::behaviors(), []);
+ return ArrayHelper::merge(parent::behaviors(), [
+ 'access' => [
+ 'class' => \yii\filters\AccessControl::class,
+ 'rules' => [
+ 'authActions' => [
+ 'allow' => true,
+ 'actions' => ['index', 'view'],
+ 'roles' => ['@'],
+ 'matchCallback' => function () {
+ return \Yii::$app->user->identity->isAdmin;
+ },
+ ],
+ ],
+ ],
+ ]);
}
/**
@@ -47,7 +61,7 @@ public function actionValidate()
if ($string !== "") {
$secretKey = Yii::$app->sys->treasure_secret_key;
$results = Yii::$app->db->createCommand("select treasure.id,player.id as player_id from treasure,player where md5(HEX(AES_ENCRYPT(CONCAT(code, player.id), :secretKey))) LIKE :code", [':secretKey' => $secretKey, ':code' => $string])->queryOne();
- if ($results === [] || $results===false) {
+ if ($results === [] || $results === false) {
Yii::$app->session->setFlash('warning', Yii::t('app', "Code not found."));
} else {
$player = \app\modules\frontend\models\Player::findOne($results['player_id']);
@@ -56,21 +70,18 @@ public function actionValidate()
'username' => $player->username,
'actions' => false
]);
- if($player->teamPlayer!==NULL)
- {
- $msg = sprintf('Code belongs to player [%s] from team [%s] for target %s and treasure %s', $profileLink,$player->teamPlayer->team->name, $treasure->target->name, $treasure->name);
- }
- else
- {
+ if ($player->teamPlayer !== NULL) {
+ $msg = sprintf('Code belongs to player [%s] from team [%s] for target %s and treasure %s', $profileLink, $player->teamPlayer->team->name, $treasure->target->name, $treasure->name);
+ } else {
$msg = sprintf('Code belongs to player [%s] for target %s and treasure %s', $profileLink, $treasure->target->name, $treasure->name);
}
Yii::$app->session->setFlash('success', $msg);
- $string='';
+ $string = '';
}
}
- return $this->render('validate',['code'=>$string]);
+ return $this->render('validate', ['code' => $string]);
}
/**
diff --git a/backend/modules/infrastructure/controllers/TargetMetadataController.php b/backend/modules/infrastructure/controllers/TargetMetadataController.php
index 25f062ab9..a7ee6c011 100644
--- a/backend/modules/infrastructure/controllers/TargetMetadataController.php
+++ b/backend/modules/infrastructure/controllers/TargetMetadataController.php
@@ -19,102 +19,116 @@ class TargetMetadataController extends \app\components\BaseController
*/
public function behaviors()
{
- return ArrayHelper::merge(parent::behaviors(), []);
+ return ArrayHelper::merge(parent::behaviors(), [
+ 'access' => [
+ 'class' => \yii\filters\AccessControl::class,
+ 'rules' => [
+ 'authActions' => [
+ 'allow' => true,
+ 'actions' => ['index', 'view'],
+ 'roles' => ['@'],
+ 'matchCallback' => function () {
+ return \Yii::$app->user->identity->isAdmin;
+ },
+ ],
+ ],
+ ],
+ ]);
}
- /**
- * Lists all TargetMetadata models.
- * @return mixed
- */
+ /**
+ * Lists all TargetMetadata models.
+ * @return mixed
+ */
public function actionIndex()
{
- $searchModel = new TargetMetadataSearch();
- $dataProvider = $searchModel->search(Yii::$app->request->queryParams);
+ $searchModel = new TargetMetadataSearch();
+ $dataProvider = $searchModel->search(Yii::$app->request->queryParams);
- return $this->render('index', [
- 'searchModel' => $searchModel,
- 'dataProvider' => $dataProvider,
- ]);
+ return $this->render('index', [
+ 'searchModel' => $searchModel,
+ 'dataProvider' => $dataProvider,
+ ]);
}
- /**
- * Displays a single TargetMetadata model.
- * @param integer $id
- * @return mixed
- * @throws NotFoundHttpException if the model cannot be found
- */
+ /**
+ * Displays a single TargetMetadata model.
+ * @param integer $id
+ * @return mixed
+ * @throws NotFoundHttpException if the model cannot be found
+ */
public function actionView($id)
{
- return $this->render('view', [
- 'model' => $this->findModel($id),
- ]);
+ return $this->render('view', [
+ 'model' => $this->findModel($id),
+ ]);
}
- /**
- * Creates a new TargetMetadata model.
- * If creation is successful, the browser will be redirected to the 'view' page.
- * @return mixed
- */
+ /**
+ * Creates a new TargetMetadata model.
+ * If creation is successful, the browser will be redirected to the 'view' page.
+ * @return mixed
+ */
public function actionCreate()
{
- $model = new TargetMetadata();
+ $model = new TargetMetadata();
if ($model->load(Yii::$app->request->post()) && $model->save()) {
- return $this->redirect(['view', 'id' => $model->target_id]);
+ return $this->redirect(['view', 'id' => $model->target_id]);
}
- return $this->render('create', [
- 'model' => $model,
- ]);
+ return $this->render('create', [
+ 'model' => $model,
+ ]);
}
- /**
- * Updates an existing TargetMetadata model.
- * If update is successful, the browser will be redirected to the 'view' page.
- * @param integer $id
- * @return mixed
- * @throws NotFoundHttpException if the model cannot be found
- */
+ /**
+ * Updates an existing TargetMetadata model.
+ * If update is successful, the browser will be redirected to the 'view' page.
+ * @param integer $id
+ * @return mixed
+ * @throws NotFoundHttpException if the model cannot be found
+ */
public function actionUpdate($id)
{
- $model = $this->findModel($id);
+ $model = $this->findModel($id);
if ($model->load(Yii::$app->request->post()) && $model->save()) {
- return $this->redirect(['view', 'id' => $model->target_id]);
+ return $this->redirect(['view', 'id' => $model->target_id]);
}
- return $this->render('update', [
- 'model' => $model,
- ]);
+ return $this->render('update', [
+ 'model' => $model,
+ ]);
}
- /**
- * Deletes an existing TargetMetadata model.
- * If deletion is successful, the browser will be redirected to the 'index' page.
- * @param integer $id
- * @return mixed
- * @throws NotFoundHttpException if the model cannot be found
- */
+ /**
+ * Deletes an existing TargetMetadata model.
+ * If deletion is successful, the browser will be redirected to the 'index' page.
+ * @param integer $id
+ * @return mixed
+ * @throws NotFoundHttpException if the model cannot be found
+ */
public function actionDelete($id)
{
- $this->findModel($id)->delete();
+ $this->findModel($id)->delete();
- return $this->redirect(['index']);
+ return $this->redirect(['index']);
}
- /**
- * Finds the TargetMetadata model based on its primary key value.
- * If the model is not found, a 404 HTTP exception will be thrown.
- * @param integer $id
- * @return TargetMetadata the loaded model
- * @throws NotFoundHttpException if the model cannot be found
- */
+ /**
+ * Finds the TargetMetadata model based on its primary key value.
+ * If the model is not found, a 404 HTTP exception will be thrown.
+ * @param integer $id
+ * @return TargetMetadata the loaded model
+ * @throws NotFoundHttpException if the model cannot be found
+ */
protected function findModel($id)
{
if (($model = TargetMetadata::findOne($id)) !== null) {
- return $model;
+ return $model;
}
- throw new NotFoundHttpException('The requested page does not exist.');
+ throw new NotFoundHttpException('The requested page does not exist.');
}
}
diff --git a/backend/modules/infrastructure/models/TargetInstanceQuery.php b/backend/modules/infrastructure/models/TargetInstanceQuery.php
index b2551b250..da1067901 100644
--- a/backend/modules/infrastructure/models/TargetInstanceQuery.php
+++ b/backend/modules/infrastructure/models/TargetInstanceQuery.php
@@ -9,14 +9,50 @@
*/
class TargetInstanceQuery extends \yii\db\ActiveQuery
{
+ /**
+ * Filters TargetInstances to those that are active
+ * and whose team has at least one approved member with vpn_local_address != 0
+ */
+ public function withApprovedMemberHeartbeat()
+ {
+ return $this
+ // join to the team_instance player with teamPlayer
+ ->innerJoin(['tp' => 'team_player'], 'target_instance.player_id = tp.player_id AND tp.approved = 1')
+ // join to the team
+ ->innerJoin(['t' => 'team'], 'tp.team_id = t.id')
+ // join to approved members of the team
+ ->innerJoin(['am' => 'team_player'], 'am.team_id = t.id AND am.approved = 1')
+ // join approved member's last
+ ->innerJoin(['al' => 'player_last'], 'al.id = am.player_id and al.vpn_local_address is not null')
+ // ensure the approved member has a vpn_local_address
+ ->distinct();
+ }
+
public function active()
{
- return $this->andWhere('[[ip]] IS NOT NULL')->andWhere('[[reboot]]!=2');
+ return $this->andWhere('target_instance.[[ip]] IS NOT NULL')->andWhere('target_instance.[[reboot]]!=2');
+ }
+
+ public function last_updated(int $seconds_ago = 1)
+ {
+ return $this->andWhere(['<', 'target_instance.[[updated_at]]', new \yii\db\Expression("NOW() - INTERVAL $seconds_ago SECOND")]);
}
- public function pending_action($minutes_ago = 60)
+ public function pending_action(int $seconds_ago = 1)
{
- return $this->andWhere('[[ip]] IS NULL')->orWhere(['>', 'reboot', 0])->orWhere(['<', 'updated_at', new \yii\db\Expression("NOW() - INTERVAL $minutes_ago MINUTE")]);
+ return $this->addSelect([
+ 'target_instance.*',
+ 'reboot' => new \yii\db\Expression(
+ "IF(target_instance.updated_at < (NOW() - INTERVAL :seconds SECOND), 2, target_instance.reboot)",
+ [':seconds' => $seconds_ago]
+ ),
+ ])
+ ->andWhere([
+ 'or',
+ ['target_instance.ip' => null],
+ ['>', 'target_instance.reboot', 0],
+ ['<', 'target_instance.updated_at', new \yii\db\Expression("NOW() - INTERVAL $seconds_ago SECOND")]
+ ]);
}
diff --git a/backend/modules/infrastructure/views/target-instance/view.php b/backend/modules/infrastructure/views/target-instance/view.php
index 1a6c5f3ae..92ebc9210 100644
--- a/backend/modules/infrastructure/views/target-instance/view.php
+++ b/backend/modules/infrastructure/views/target-instance/view.php
@@ -66,7 +66,7 @@
'team_allowed:boolean',
[
'label' => 'encrypted flags',
- 'visible'=>$model->target->dynamic_treasures,
+ 'visible'=>$model->target->dynamic_treasures && \Yii::$app->user->identity->isAdmin,
'format' => 'raw',
'value' => function ($model) {
$lines=[];
diff --git a/backend/modules/settings/models/ConfigureForm.php b/backend/modules/settings/models/ConfigureForm.php
index 8eb273406..58c6ea1f0 100644
--- a/backend/modules/settings/models/ConfigureForm.php
+++ b/backend/modules/settings/models/ConfigureForm.php
@@ -57,7 +57,7 @@ class ConfigureForm extends Model
public $leaderboard_show_zero;
public $time_zone;
public $target_days_new = 2;
- public $target_days_updated = 1;
+ public $target_days_updated = 0;
public $discord_news_webhook;
public $pf_state_limits;
public $stripe_apiKey;
@@ -472,7 +472,7 @@ public function rules()
[['online_timeout'], 'default', 'value' => 900],
[['spins_per_day'], 'default', 'value' => 2],
['target_days_new', 'default', 'value' => 1],
- ['target_days_updated', 'default', 'value' => 2],
+ ['target_days_updated', 'default', 'value' => 0],
[['event_start', 'event_end', 'registrations_start', 'registrations_end'], 'datetime', 'format' => 'php:Y-m-d H:i:s'],
[[
'dashboard_is_home',
diff --git a/backend/modules/settings/models/Sysconfig.php b/backend/modules/settings/models/Sysconfig.php
index 021aad19f..308121e49 100644
--- a/backend/modules/settings/models/Sysconfig.php
+++ b/backend/modules/settings/models/Sysconfig.php
@@ -56,7 +56,7 @@ public function afterFind()
if ($this->val == 0 || $this->val == "")
$this->val = "";
else
- $this->val = Yii::$app->formatter->asDatetime($this->val, 'php:Y-m-d H:i:s', 'UTC');
+ $this->val = date('Y-m-d H:i:s', $this->val);
break;
default:
break;
@@ -70,7 +70,7 @@ public function beforeSave($insert)
$Q = sprintf("DROP EVENT IF EXISTS event_end_notification");
\Yii::$app->db->createCommand($Q)->execute();
if (!empty($this->val)) {
- $Q = sprintf("CREATE EVENT event_end_notification ON SCHEDULE AT '%s' DO INSERT INTO `notification`(player_id,category,title,body,archived) SELECT id,'swal:info',memc_get('sysconfig:event_end_notification_title'),memc_get('sysconfig:event_end_notification_body'),0 FROM player WHERE status=10", $this->val);
+ $Q = sprintf("CREATE EVENT event_end_notification ON SCHEDULE AT '%s' DO BEGIN INSERT INTO `notification`(player_id,category,title,body,archived) SELECT id,'swal:info',memc_get('sysconfig:event_end_notification_title'),memc_get('sysconfig:event_end_notification_body'),0 FROM player WHERE status=10; DO memc_set('event_finished',1); SELECT 1 INTO OUTFILE '/tmp/event_finished';END", $this->val);
\Yii::$app->db->createCommand($Q)->execute();
$this->val = strtotime($this->val);
} else {
@@ -101,11 +101,9 @@ public function afterSave($insert, $changedAttributes)
if ($this->id === 'stripe_webhookLocalEndpoint' && array_key_exists('val', $changedAttributes)) {
$oldVal = $changedAttributes['val'];
$newVal = $this->val;
- if(($u=UrlRoute::findOne(['destination'=>'subscription/default/webhook']))!==NULL)
- {
- $u->updateAttributes(['source'=>$newVal]);
+ if (($u = UrlRoute::findOne(['destination' => 'subscription/default/webhook'])) !== NULL) {
+ $u->updateAttributes(['source' => $newVal]);
}
-
}
}
diff --git a/contrib/event_shutdown.sh b/contrib/event_shutdown.sh
new file mode 100644
index 000000000..6217f0f72
--- /dev/null
+++ b/contrib/event_shutdown.sh
@@ -0,0 +1,6 @@
+#!/bin/ksh
+PATH=/sbin:/usr/sbin:/bin:/usr/bin:/usr/X11R6/bin:/usr/local/sbin:/usr/local/bin
+rcctl stop openvpn findingsd heartbeatd inetd cron
+supervisorctl stop all
+backend target/destroy-instances
+ifconfig tun0 down
diff --git a/contrib/findingsd-federated.sql b/contrib/findingsd-federated.sql
index ac873b622..848499eae 100644
--- a/contrib/findingsd-federated.sql
+++ b/contrib/findingsd-federated.sql
@@ -116,6 +116,32 @@ CREATE TABLE `player_ssl` (
UNIQUE KEY `serial` (`serial`)
) ENGINE=FEDERATED DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci CONNECTION='mysql://{{db_user}}:{{db_pass}}@{{db_host}}:3306/{{db_name}}/player_ssl';
+DROP TABLE IF EXISTS `private_network`;
+CREATE TABLE `private_network` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `player_id` int(11) unsigned DEFAULT NULL,
+ `name` varchar(255) DEFAULT NULL,
+ `team_accessible` tinyint(1) DEFAULT NULL,
+ `created_at` datetime DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ KEY `idx-private_network-player_id` (`player_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci CONNECTION='mysql://{{db_user}}:{{db_pass}}@{{db_host}}:3306/{{db_name}}/private_network';
+
+DROP TABLE IF EXISTS `private_network_target`;
+CREATE TABLE `private_network_target` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `private_network_id` int(11) DEFAULT NULL,
+ `target_id` int(11) NOT NULL,
+ `ip` int(11) unsigned DEFAULT NULL,
+ `state` smallint(6) unsigned DEFAULT 0,
+ `server_id` int(11) DEFAULT NULL,
+ `ipoctet` varchar(15) GENERATED ALWAYS AS (inet_ntoa(`ip`)) VIRTUAL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `idx-unique-private_network_id-target_id` (`private_network_id`,`target_id`),
+ KEY `idx-private_network_target-private_network_id` (`private_network_id`),
+ KEY `idx-private_network_target-server_id` (`server_id`),
+ KEY `idx-private_network_target-target_id` (`target_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci CONNECTION='mysql://{{db_user}}:{{db_pass}}@{{db_host}}:3306/{{db_name}}/private_network_target';
DROP TABLE IF EXISTS `debuglogs`;
CREATE TABLE debuglogs (
@@ -241,3 +267,17 @@ BEGIN
END IF;
END
//
+
+
+DROP EVENT IF EXISTS `event_shutdown` //
+CREATE EVENT `event_shutdown` ON SCHEDULE EVERY 5 SECOND STARTS '2020-01-01 00:00:00' ON COMPLETION PRESERVE ENABLE DO
+BEGIN
+ IF (select memc_server_count()<1) THEN
+ select memc_servers_set('{{db_host}}:{{memc_port|default(11211)}}') INTO @memc_server_set_status;
+ END IF;
+
+ IF memc_get('event_finished') IS NOT NULL THEN
+ ALTER EVENT `event_shutdown` DISABLE;
+ SELECT 1 INTO OUTFILE '/tmp/event_finished';
+ END IF;
+END //
diff --git a/contrib/sample-migrations/m000000_000001_system_settings.php b/contrib/sample-migrations/m000000_000001_system_settings.php
index 5b76f0a90..ff7323611 100644
--- a/contrib/sample-migrations/m000000_000001_system_settings.php
+++ b/contrib/sample-migrations/m000000_000001_system_settings.php
@@ -31,6 +31,10 @@ class m000000_000001_system_settings extends Migration
['id' => "leaderboard_show_zero", 'val' => "0"],
['id' => "leaderboard_visible_after_event_end", 'val' => "1"],
['id' => "leaderboard_visible_before_event_start", 'val' => "0"],
+ ['id' => "country_rankings", 'val' => "0"],
+ ['id' => "player_point_rankings", 'val' => "0"],
+ ['id' => "player_monthly_rankings", 'val' => "0"],
+
['id' => 'frontpage_scenario', 'val' => 'Welcome to our lovely event... Edit from backend Content => Frontpage Scenario'],
['id' => "event_end_notification_title", 'val' => "🎉 Our awesome echoCTF finished 🎉"],
['id' => "event_end_notification_body", 'val' => "The awesome echoCTF is over 🎉🎉🎉 Congratulations to you and your team 👏👏👏 Thank you for participating!!!"],
@@ -59,23 +63,38 @@ class m000000_000001_system_settings extends Migration
['id' => "team_manage_members", 'val' => "1"],
['id' => "team_required", 'val' => "1"],
['id' => 'team_visible_instances', 'val' => "1"],
+ ['id' => 'team_only_leaderboards', 'val' => "1"],
+ ['id' => 'team_encrypted_claims_allowed', 'val' => "1"],
+
/**
* Player settings
*/
['id' => "approved_avatar", 'val' => "1"],
['id' => "player_profile", 'val' => "1"],
['id' => "profile_visibility", 'val' => "public"],
- ['id' => "require_activation", 'val' => "0"],
- ['id' => 'player_require_identification', 'val' => "0"],
+ ['id' => "require_activation", 'val' => "1"],
+ ['id' => 'player_require_identification', 'val' => "1"],
['id' => 'all_players_vip', 'val' => "1"],
- ['id' => 'player_require_approval', 'val' => "0"],
+ ['id' => 'player_require_approval', 'val' => "1"],
['id' => 'profile_discord', 'val' => "1"],
['id' => 'profile_echoctf', 'val' => "1"],
['id' => 'profile_github', 'val' => "1"],
['id' => 'profile_settings_fields', 'val' => 'avatar,bio,country,discord,echoctf,email,fullname,github,pending_progress,twitter,username,visibility'],
+ ['id' => 'avatar_robohash_set', 'val' => 'set3'],
+
/**
* Configuration settings
*/
+ ['id' => 'target_guest_view_deny', 'val' => '1'],
+ ['id' => 'disable_ondemand_operations', 'val' => '1'],
+ ['id' => 'module_smartcity_disabled', 'val' => '1'],
+ ['id' => 'module_speedprogramming_enabled', 'val' => '0'],
+ ['id' => 'dashboard_news_total_pages', 'val' => '10'],
+ ['id' => 'dashboard_news_records_per_page', 'val' => '3'],
+ ['id' => 'force_https_urls', 'val' => '1'],
+ ['id' => 'subscriptions_menu_show', 'val' => '0'],
+ ['id' => 'log_failed_claims', 'val' => '1'],
+
['id' => 'academic_grouping', 'val' => '0'],
['id' => "challenge_home", 'val' => "uploads/"],
['id' => "dashboard_is_home", 'val' => "1"],
@@ -126,8 +145,11 @@ class m000000_000001_system_settings extends Migration
*/
public function safeUp()
{
- foreach ($this->news as $entry)
+ foreach ($this->news as $entry) {
+ $entry['created_at']=new \yii\db\Expression('NOW()');
+ $entry['updated_at']=new \yii\db\Expression('NOW()');
$this->upsert('news', $entry, true);
+ }
// delete not needed url routes
foreach ($this->delete_url_routes as $route) {
diff --git a/contrib/watchdog-action.py b/contrib/watchdog-action.py
new file mode 100644
index 000000000..d5eaa9cf5
--- /dev/null
+++ b/contrib/watchdog-action.py
@@ -0,0 +1,33 @@
+#!/usr/local/bin/python3
+#
+# pip install watchdog
+import argparse
+import os
+from watchdog.observers import Observer
+from watchdog.events import FileSystemEventHandler
+
+# CLI arguments
+parser = argparse.ArgumentParser()
+parser.add_argument("--file_path", required=True, help="Full path to the file to monitor")
+parser.add_argument("--action", required=True, help="Full path to the file we will execute")
+args = parser.parse_args()
+
+FULL_PATH = args.file_path
+FOLDER = os.path.dirname(FULL_PATH)
+TARGET_FILE = os.path.basename(FULL_PATH)
+ACTION = args.action
+
+class Handler(FileSystemEventHandler):
+ def __init__(self, observer):
+ self.observer = observer
+
+ def on_created(self, event):
+ if not event.is_directory and event.src_path == FULL_PATH:
+ os.system(ACTION)
+ self.observer.stop()
+
+observer = Observer()
+handler = Handler(observer)
+observer.schedule(handler, FOLDER, recursive=False)
+observer.start()
+observer.join()
diff --git a/contrib/watchdoger.py b/contrib/watchdoger.py
new file mode 100644
index 000000000..38651de7a
--- /dev/null
+++ b/contrib/watchdoger.py
@@ -0,0 +1,46 @@
+#!/usr/local/bin/python3
+#
+# pip install watchdog requests
+import argparse
+import os
+import requests
+from watchdog.observers import Observer
+from watchdog.events import FileSystemEventHandler
+
+# CLI arguments
+parser = argparse.ArgumentParser()
+parser.add_argument("--file_path", required=True, help="Full path to the file to monitor")
+parser.add_argument("--url", required=True, help="HTTP endpoint URL to POST to")
+parser.add_argument("--token", required=True, help="Bearer token for authorization")
+args = parser.parse_args()
+
+FULL_PATH = args.file_path
+FOLDER = os.path.dirname(FULL_PATH)
+TARGET_FILE = os.path.basename(FULL_PATH)
+URL = args.url
+BEARER_TOKEN = args.token
+
+class Handler(FileSystemEventHandler):
+ def __init__(self, observer):
+ self.observer = observer
+
+ def on_created(self, event):
+ if not event.is_directory and event.src_path == FULL_PATH:
+ response = requests.post(
+ URL,
+ headers={
+ "Authorization": f"Bearer {BEARER_TOKEN}",
+ "Content-Type": "application/json"
+ },
+ json={
+ "event": "apiNotifications"
+ }
+ )
+ print(f"Posted {event.src_path}, status: {response.status_code}")
+ self.observer.stop() # exit after sending
+
+observer = Observer()
+handler = Handler(observer)
+observer.schedule(handler, FOLDER, recursive=False)
+observer.start()
+observer.join()
diff --git a/docs/Websockets.md b/docs/Websockets.md
new file mode 100644
index 000000000..3b46b4756
--- /dev/null
+++ b/docs/Websockets.md
@@ -0,0 +1,59 @@
+# Websockets service
+
+echoCTF.RED provides player updates to the live players through the use of [ws-server](https://github.com/echoCTF/ws-server).
+
+The services that want to communicate an update to the current live players submit their events through the HTTP service of ws-server.
+
+The system can send messages to a specific player or all connected players through the `/publish` and `/broadcast` endpoints respectively.
+
+Currently the following events are implemented:
+
+* `notification`: Sends a direct notification, Alert or Sweetalerts.
+* `apiNotifications`: Tell the clients to perform an update of their in-page notifications.
+* `target`: Update the target card if currently visible
+
+## Examples
+
+* Notify all users to perform an `apiNotifications()` js call. Effectively fetch the latest notifications through ajax.
+
+```shell
+curl -X POST "http://localhost:8888/broadcast" \
+ -H "Authorization: Bearer YOURTOKEN" \
+ -H "Content-Type: application/json" \
+ -d '{ "event": "apiNotifications" }'
+```
+
+* Send a SweetAlert (`"type": "swap:info"`) notification to player with id `1`
+
+```shell
+curl -X POST "http://localhost:8888/publish" \
+ -H "Authorization: Bearer server123token" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "player_id": "1",
+ "event": "notification",
+ "payload":
+ {
+ "title": "This is a notification",
+ "body": "This is the notification body",
+ "type": "swal:info"
+ }
+ }'
+```
+
+Note: Removing the `swal:` prefix from `type` sends a normal bootstrap alert notification.
+
+* Send an update for to player id `1` for updates on target id `2`
+
+```shell
+curl -X POST "http://localhost:8888/publish" \
+ -H "Authorization: Bearer server123token" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "player_id": "1",
+ "event": "target",
+ "payload": { "id": "2" }
+ }'
+```
+
+This will execute the js code `targetUpdates(2)`.
diff --git a/frontend/commands/TesterController.php b/frontend/commands/TesterController.php
deleted file mode 120000
index b00eb05cc..000000000
--- a/frontend/commands/TesterController.php
+++ /dev/null
@@ -1 +0,0 @@
-../../backend/commands/TesterController.php
\ No newline at end of file
diff --git a/frontend/commands/TesterController.php b/frontend/commands/TesterController.php
new file mode 100644
index 000000000..4dc1dbdf3
--- /dev/null
+++ b/frontend/commands/TesterController.php
@@ -0,0 +1,88 @@
+stdout("*** TESTER COMMAND ***\n");
+
+ echo Table::widget([
+ 'headers' => ['Action', 'Usage', 'Description'],
+ 'rows' => [
+ ['Action' => 'tester/mail', 'Usage' => 'tester/mail email@example.com', 'Description' => 'Send a test mail with the current settings'],
+ ['Action' => 'tester/ws-notify', 'Usage' => 'tester/ws-notify player', 'Description' => 'Send a test websocket notification to the given player by id'],
+ ],
+ ]);
+ }
+
+ /**
+ * Test the mailer configuration by sending a test email.
+ *
+ * Usage:
+ * backend tester/mail test@example.com
+ *
+ * @param string $to Recipient email
+ */
+ public function actionMail($to)
+ {
+ $mailer = Yii::$app->mailer;
+ try {
+
+ $this->stdout("*** SETTINGS *** \n");
+ if (\Yii::$app->sys->mail_useFileTransport) {
+ $this->stdout("mail_useFileTransport: Yes\n");
+ $this->stdout("mails folder: " . @\Yii::getAlias('@app/runtime/mail/') . "\n");
+ }
+ if (\Yii::$app->sys->dsn) $this->stdout("dsn: " . \Yii::$app->sys->dsn . "\n");
+ if (\Yii::$app->sys->mail_from) $this->stdout("mail_from: " . \Yii::$app->sys->mail_from . "\n");
+ if (\Yii::$app->sys->mail_fromName) $this->stdout("mail_fromName: " . \Yii::$app->sys->mail_fromName . "\n");
+ if (\Yii::$app->sys->mail_host) $this->stdout("mail_host: " . \Yii::$app->sys->mail_host . "\n");
+ if (\Yii::$app->sys->mail_port) $this->stdout("mail_port: " . \Yii::$app->sys->mail_port . "\n");
+ if (\Yii::$app->sys->mail_username) $this->stdout("mail_username: " . \Yii::$app->sys->mail_username . "\n");
+ if (\Yii::$app->sys->mail_password) $this->stdout("mail_password: **USED BUT HIDDEN**\n");
+
+ $result = $mailer->compose()
+ ->setFrom([\Yii::$app->sys->mail_from => \Yii::$app->sys->mail_fromName])
+ ->setTo($to)
+ ->setSubject('echoCTF Installation Mail Test')
+ ->setTextBody("This is a test email sent at " . date('Y-m-d H:i:s'))
+ ->send();
+ if ($result) {
+ $this->stdout("✅ Test email successfully sent to {$to}\n");
+ } else {
+ $this->stderr("❌ Failed to send test email to {$to}\n");
+ }
+ } catch (\Symfony\Component\Mailer\Exception\TransportExceptionInterface $e) {
+ $this->stderr("❌ Transport error: " . $e->getMessage() . "\n");
+ } catch (\Throwable $e) {
+ $this->stderr("❌ Error: " . $e->getMessage() . "\n");
+ }
+ }
+
+ public function actionWsNotify($player)
+ {
+ $player=\app\models\Player::findOne($player);
+ $type = "info";
+ $title="title";
+ $body="body";
+ $cc = true;
+ $archive = true;
+ $apiOnly = false;
+ $player->notify($type, $title, $body, $cc, $archive, $apiOnly);
+ }
+
+}
diff --git a/frontend/components/Img.php b/frontend/components/Img.php
index f4371ad72..75126b2e1 100644
--- a/frontend/components/Img.php
+++ b/frontend/components/Img.php
@@ -55,12 +55,22 @@ public static function profile($profile)
imagestring($image, 6, 200, $lineheight*$i++, sprintf(\Yii::t('app',"root@%s:/# ./userinfo --profile %d"),\Yii::$app->sys->offense_domain,$profile->id),$textcolor);
imagestring($image, 6, 200, $lineheight*$i++, sprintf(\Yii::t('app',"username.....: %s"),$profile->owner->username),$greencolor);
imagestring($image, 6, 200, $lineheight*$i++, sprintf(\Yii::t('app',"joined.......: %s"),date("d.m.Y", strtotime($profile->owner->created))),$greencolor);
- imagestring($image, 6, 200, $lineheight*$i++, sprintf(\Yii::t('app',"points.......: %s"),number_format($profile->owner->playerScore->points)),$greencolor);
- imagestring($image, 6, 200, $lineheight*$i++, sprintf(\Yii::t('app',"rank.........: %s"),$profile->owner->playerScore->points == 0 ? "-":$profile->rank->ordinalPlace),$greencolor);
- imagestring($image, 6, 200, $lineheight*$i++, sprintf(\Yii::t('app',"level........: %d / %s"),$profile->experience->id, $profile->experience->name),$greencolor);
- imagestring($image, 6, 200, $lineheight*$i++, sprintf(\Yii::t('app',"flags........: %d"), $profile->totalTreasures),$greencolor);
- imagestring($image, 6, 200, $lineheight*$i++, sprintf(\Yii::t('app',"challenges...: %d / %d first"),$profile->challengesSolverCount, $profile->firstChallengeSolversCount),$greencolor);
- imagestring($image, 6, 200, $lineheight*$i++, sprintf(\Yii::t('app',"headshots....: %d / %d first"),$profile->headshotsCount, $profile->firstHeadshotsCount),$greencolor);
+ if (\Yii::$app->sys->team_only_leaderboards !== true)
+ {
+ imagestring($image, 6, 200, $lineheight*$i++, sprintf(\Yii::t('app',"points.......: %s"),number_format($profile->owner->playerScore->points)),$greencolor);
+ imagestring($image, 6, 200, $lineheight*$i++, sprintf(\Yii::t('app',"rank.........: %s"),$profile->owner->playerScore->points == 0 ? "-":$profile->rank->ordinalPlace),$greencolor);
+ imagestring($image, 6, 200, $lineheight*$i++, sprintf(\Yii::t('app',"level........: %d / %s"),$profile->experience->id, $profile->experience->name),$greencolor);
+ imagestring($image, 6, 200, $lineheight*$i++, sprintf(\Yii::t('app',"flags........: %d"), $profile->totalTreasures),$greencolor);
+ imagestring($image, 6, 200, $lineheight*$i++, sprintf(\Yii::t('app',"challenges...: %d / %d first"),$profile->challengesSolverCount, $profile->firstChallengeSolversCount),$greencolor);
+ imagestring($image, 6, 200, $lineheight*$i++, sprintf(\Yii::t('app',"headshots....: %d / %d first"),$profile->headshotsCount, $profile->firstHeadshotsCount),$greencolor);
+ }
+ else if($profile->owner->teamPlayer)
+ {
+ imagestring($image, 6, 200, $lineheight*$i++, \Yii::t('app',"team.........: {team}",['team'=>$profile->owner->team->name]),$greencolor);
+ imagestring($image, 6, 200, $lineheight*$i++, \Yii::t('app',"team rank....: {rank}",['rank'=>($profile->owner->team->rank !== null ? $profile->owner->team->rank->ordinalPlace : 'empty')]),$greencolor);
+ imagestring($image, 6, 200, $lineheight*$i++, \Yii::t('app',"team points..: {points,plural,=0{0 pts} =1{# pts} other{# pts}}",['points'=>($profile->owner->team->score !== null ? $profile->owner->team->score->points : 0)]),$greencolor);
+ imagestring($image, 6, 200, $lineheight*$i++, \Yii::t('app',"contributed..: {points,plural,=0{0 pts} =1{# pts} other{# pts}}",['points'=>($profile->owner->teamStreamPoints->points ?? 0)]),$greencolor);
+ }
imagedestroy($avatar);
imagedestroy($cover);
imagedestroy($src);
diff --git a/frontend/config/console.php b/frontend/config/console.php
index 4b146d6c7..61ad81f0e 100644
--- a/frontend/config/console.php
+++ b/frontend/config/console.php
@@ -1,54 +1,60 @@
'basic-console',
-// 'language' => 'el-GR',
- 'sourceLanguage' => 'en-US',
- 'basePath' => dirname(__DIR__),
- 'bootstrap' => ['log'],
- 'controllerNamespace' => 'app\commands',
- 'aliases' => [
- '@bower' => '@vendor/bower-asset',
- '@npm' => '@vendor/npm-asset',
- '@tests' => '@app/tests',
+$config = [
+ 'id' => 'basic-console',
+ // 'language' => 'el-GR',
+ 'sourceLanguage' => 'en-US',
+ 'basePath' => dirname(__DIR__),
+ 'bootstrap' => ['log'],
+ 'controllerNamespace' => 'app\commands',
+ 'aliases' => [
+ '@bower' => '@vendor/bower-asset',
+ '@npm' => '@vendor/npm-asset',
+ '@tests' => '@app/tests',
+ ],
+ 'modules' => [
+ 'team' => [
+ 'class' => 'app\modules\team\Module',
],
- 'components' => [
- 'i18n' => [
- 'translations' => [
- 'yii' => [
- 'class' => 'yii\i18n\PhpMessageSource',
- ],
- 'app*' => [
- 'class' => 'yii\i18n\PhpMessageSource',
- 'basePath' => '@app/messages',
- 'sourceLanguage' => 'en-US',
- 'fileMap' => [
- 'app' => 'app.php',
- 'app/error' => 'error.php',
- ],
- ],
- ],
+ ],
+
+ 'components' => [
+ 'i18n' => [
+ 'translations' => [
+ 'yii' => [
+ 'class' => 'yii\i18n\PhpMessageSource',
],
- 'sys'=> [
- 'class' => 'app\components\Sysconfig',
+ 'app*' => [
+ 'class' => 'yii\i18n\PhpMessageSource',
+ 'basePath' => '@app/messages',
+ 'sourceLanguage' => 'en-US',
+ 'fileMap' => [
+ 'app' => 'app.php',
+ 'app/error' => 'error.php',
+ ],
],
- 'cache' => $cache,
- 'log' => [
- 'targets' => [
- [
- 'class' => 'yii\log\FileTarget',
- 'levels' => ['error', 'warning'],
- ],
- ],
+ ],
+ ],
+ 'sys' => [
+ 'class' => 'app\components\Sysconfig',
+ ],
+ 'cache' => $cache,
+ 'log' => [
+ 'targets' => [
+ [
+ 'class' => 'yii\log\FileTarget',
+ 'levels' => ['error', 'warning'],
],
- 'db' => $db,
+ ],
],
- 'params' => $params,
- /*
+ 'db' => $db,
+ ],
+ 'params' => $params,
+ /*
'controllerMap' => [
'fixture' => [ // Fixture generation command line.
'class' => 'yii\faker\FixtureController',
@@ -57,13 +63,12 @@
*/
];
-if(YII_ENV_DEV)
-{
- // configuration adjustments for 'dev' environment
- $config['bootstrap'][]='gii';
- $config['modules']['gii']=[
- 'class' => 'yii\gii\Module',
- ];
+if (YII_ENV_DEV) {
+ // configuration adjustments for 'dev' environment
+ $config['bootstrap'][] = 'gii';
+ $config['modules']['gii'] = [
+ 'class' => 'yii\gii\Module',
+ ];
}
return $config;
diff --git a/frontend/models/Player.php b/frontend/models/Player.php
index 0f4720f07..669cc5b3a 100644
--- a/frontend/models/Player.php
+++ b/frontend/models/Player.php
@@ -408,21 +408,25 @@ public function notify($type = "info", $title, $body, $cc = true, $archive = tru
try {
$publisher = new \app\services\ServerPublisher(Yii::$app->params['serverPublisher']);
$publisher->publish($this->id, 'notification', ['type' => $type, 'title' => $title, 'body' => $body]);
- } catch(\Throwable $e) {
+ } catch (\Throwable $e) {
// on publishing error make sure we store the noticication as pending
- $cc=true;
- $archive=false;
+ $cc = true;
+ $archive = false;
Yii::error($e->getMessage());
}
if ($cc === true) {
$n = new \app\models\Notification;
$n->player_id = $this->id;
- $n->archived = $archive;
+ $n->archived = intval($archive);
$n->category = $type;
$n->title = $title;
$n->body = $body;
- return $n->save();
+ if (!$n->save()) {
+ Yii::error($n->getErrorSummary(true));
+ return false;
+ }
+ return true;
}
return true;
}
diff --git a/frontend/modules/target/actions/SpawnRestAction.php b/frontend/modules/target/actions/SpawnRestAction.php
index e7b0adb6d..916243e49 100644
--- a/frontend/modules/target/actions/SpawnRestAction.php
+++ b/frontend/modules/target/actions/SpawnRestAction.php
@@ -59,7 +59,7 @@ public function run($id,$team=false)
$ti=new TargetInstance;
$ti->player_id=Yii::$app->user->id;
$ti->target_id=$id;
- // pick the least used server currently
+ $ti->team_allowed=intval(\Yii::$app->sys->team_visible_instances);
if(\Yii::$app->user->identity->subscription !== null && \Yii::$app->user->identity->subscription->active > 0 && \Yii::$app->user->identity->subscription->product !== null)
{
$metadata = json_decode(\Yii::$app->user->identity->subscription->product->metadata);
@@ -67,6 +67,8 @@ public function run($id,$team=false)
$ti->team_allowed=($team===false ? 0 : 1);
}
}
+
+ // pick the least used server currently
$ti->server_id=intval(Yii::$app->db->createCommand('select id from server t1 left join target_instance t2 on t1.id=t2.server_id group by t1.id order by count(t2.server_id) limit 1')->queryScalar());
if($ti->save()!==false)
Yii::$app->session->setFlash('success', sprintf(\Yii::t('app','Spawning new instance for [%s]. You will receive a notification when the instance is up.'), $ti->target->name));
diff --git a/frontend/modules/target/models/Treasure.php b/frontend/modules/target/models/Treasure.php
index f58bf6ccd..ccc44983b 100644
--- a/frontend/modules/target/models/Treasure.php
+++ b/frontend/modules/target/models/Treasure.php
@@ -146,7 +146,7 @@ public function getTarget()
*/
public function getLocationRedacted()
{
- return str_replace($this->code,"*REDACTED*",$this->location);
+ return str_replace($this->code,"*REDACTED*",$this->solution);
}
public static function find()
diff --git a/frontend/modules/team/controllers/DefaultController.php b/frontend/modules/team/controllers/DefaultController.php
index d64f9b9b4..4ca6c0a39 100644
--- a/frontend/modules/team/controllers/DefaultController.php
+++ b/frontend/modules/team/controllers/DefaultController.php
@@ -221,7 +221,6 @@ public function actionView($token)
'pageSize' => 10,
]
]);
-
return $this->render('view', [
'team' => $model,
'teamInstanceProvider' => $teamInstanceProvider,
@@ -264,8 +263,23 @@ public function actionMine()
]
]);
+ $teamNetworks = \app\modules\network\models\PrivateNetwork::find()->forTeam(\Yii::$app->user->identity->team->id);
+ $teamNetworksProvider = new ActiveDataProvider([
+ 'query' => $teamNetworks,
+ 'pagination' => [
+ 'pageSizeParam' => 'networks-perpage',
+ 'pageParam' => 'networks-page',
+ 'pageSize' => 5,
+ ],
+ 'sort' => ['defaultOrder' => ['name' => SORT_ASC]],
+ ]);
+
+ $subQuery = TeamStream::find()
+ ->select('stream_id')
+ ->where(['team_id' => \Yii::$app->user->identity->team->id]);
+
$stream = \app\models\Stream::find()->select('stream.*,TS_AGO(ts) as ts_ago')
- ->where(['stream.player_id' => $teamPlayers])
+ ->where(['id' => $subQuery])
->orderBy(['ts' => SORT_DESC, 'id' => SORT_DESC]);
$streamProvider = new ActiveDataProvider([
'query' => $stream,
@@ -314,7 +328,9 @@ public function actionMine()
'teamTargetsProvider' => $targetProgressProvider,
'headshotsProvider' => $headshotsProvider,
'solverProvider' => $solverProvider,
- 'team' => Yii::$app->user->identity->team
+ 'team' => Yii::$app->user->identity->team,
+ 'networksProvider' => $teamNetworksProvider,
+
]);
}
/**
diff --git a/frontend/modules/team/models/Team.php b/frontend/modules/team/models/Team.php
index b84435fe7..258454e7b 100644
--- a/frontend/modules/team/models/Team.php
+++ b/frontend/modules/team/models/Team.php
@@ -25,7 +25,8 @@
* @property Player $owner
* @property TeamPlayer[] $teamPlayers
* @property Player[] $players
- */
+ * @property TeamInvite $inviteOrCreate
+*/
class Team extends \yii\db\ActiveRecord
{
public $uploadedAvatar;
@@ -142,7 +143,7 @@ public function getRank()
*/
public function getTeamPlayers()
{
- return $this->hasMany(TeamPlayer::class, ['team_id' => 'id'])->orderBy(['approved'=>SORT_DESC,'ts'=>SORT_ASC]);
+ return $this->hasMany(TeamPlayer::class, ['team_id' => 'id'])->orderBy(['approved' => SORT_DESC, 'ts' => SORT_ASC]);
}
/**
@@ -161,6 +162,28 @@ public function getInvite()
return $this->hasOne(TeamInvite::class, ['team_id' => 'id']);
}
+ /**
+ * Returns the related TeamInvite model.
+ *
+ * If the invite does not exist yet, it will be created, saved,
+ * and populated into the `invite` relation.
+ *
+ * @return TeamInvite the existing or newly created invite model
+ * @throws \RuntimeException if the invite cannot be created
+ */
+ public function getInviteOrCreate()
+ {
+ if ($this->invite === null) {
+ $invite = new TeamInvite(['team_id'=>$this->id,'token'=>Yii::$app->security->generateRandomString(8)]);
+ if (!$invite->save()) {
+ throw new \RuntimeException('Failed to create TeamInvite');
+ }
+ $this->populateRelation('invite', $invite);
+ }
+
+ return $this->invite;
+ }
+
public function getValidLogo()
{
if ($this->logo === null || trim($this->logo) === '')
@@ -286,21 +309,19 @@ public function getAcademicWord()
/**
* Generate a new invite url
*/
- public function generate_invite(){
- if($this->invite) {
- $this->invite->token=Yii::$app->security->generateRandomString(8);
- if(!$this->invite->save())
- {
- throw new UserException(Yii::t('app','Failed to save invite. [{error}]',['error'=>implode(" ",$this->invite->getErrors())]));
+ public function generate_invite()
+ {
+ if ($this->invite) {
+ $this->invite->token = Yii::$app->security->generateRandomString(8);
+ if (!$this->invite->save()) {
+ throw new UserException(Yii::t('app', 'Failed to save invite. [{error}]', ['error' => implode(" ", $this->invite->getErrors())]));
}
- }
- else {
- $ti=new TeamInvite;
- $ti->team_id=$this->id;
- $ti->token=Yii::$app->security->generateRandomString(8);
- if(!$ti->save())
- {
- throw new UserException(Yii::t('app','Failed to save invite. [{error}]',['error'=>implode(" ",$ti->getErrors())]));
+ } else {
+ $ti = new TeamInvite;
+ $ti->team_id = $this->id;
+ $ti->token = Yii::$app->security->generateRandomString(8);
+ if (!$ti->save()) {
+ throw new UserException(Yii::t('app', 'Failed to save invite. [{error}]', ['error' => implode(" ", $ti->getErrors())]));
}
}
}
diff --git a/frontend/themes/material/modules/target/views/default/_target_card.php b/frontend/themes/material/modules/target/views/default/_target_card.php
index 284e2b9c8..53779fdcd 100644
--- a/frontend/themes/material/modules/target/views/default/_target_card.php
+++ b/frontend/themes/material/modules/target/views/default/_target_card.php
@@ -21,10 +21,10 @@
else
$display_ip=Html::a($target_ip,$target_ip,["class"=>'copy-to-clipboard text-danger text-bold','swal-data'=>"Copied to clipboard",'data-toggle'=>'tooltip','title'=>\Yii::t('app',"The IP of your private instance. Click to copy IP to clipboard.")]);
}
-if($target_ip=='0.0.0.0')
-{
- $this->registerJs("targetUpdates({$target->id});", \yii\web\View::POS_READY);
-}
+//if($target_ip=='0.0.0.0')
+//{
+// $this->registerJs("targetUpdates({$target->id});", \yii\web\View::POS_READY);
+//}
$subtitleARR=[$target->category,ucfirst($target->getDifficultyText($target->average_rating)),boolval($target->rootable) ? "Rootable" : "Non rootable",$target->timer===false ? null:'Timed'];
$subtitle=implode(", ",array_filter($subtitleARR));
Card::begin([
@@ -34,7 +34,7 @@
'icon'=>sprintf(' ", $target->total_treasures, ": Flag".($target->total_treasures > 1 ? 's' : '')." ";
diff --git a/frontend/themes/material/modules/target/views/default/_target_metadata.php b/frontend/themes/material/modules/target/views/default/_target_metadata.php
index f96d082ec..2ee835d76 100644
--- a/frontend/themes/material/modules/target/views/default/_target_metadata.php
+++ b/frontend/themes/material/modules/target/views/default/_target_metadata.php
@@ -1,11 +1,13 @@
user->isGuest && $metadata):?>
+ formatter->divID; ?>
user->identity->isAdmin):?>
- scenario)):?>=\Yii::t('app','Scenario')?>: =\yii\helpers\Markdown::process($metadata->scenario,'gfm')?>', $target->logo),
'color'=>'target',
'subtitle'=>$subtitle,
- 'title'=>sprintf('%s / %s', $target->name, $display_ip),
+ 'title'=>sprintf('%s / %s', $target->name, $target->id, $display_ip),
'footer'=>sprintf('
- instructions)):?>=\Yii::t('app','Instructions')?>: =\yii\helpers\Markdown::process($metadata->instructions,'gfm')?>
- solution)):?>=\Yii::t('app','Solution')?>: =\yii\helpers\Markdown::process($metadata->solution,'gfm')?>
+ scenario)):?>=\Yii::t('app','Scenario')?>: formatter->divID = 'markdown-scenario'; echo \Yii::$app->formatter->asMarkdown($metadata->scenario)?>
+ instructions)):?>=\Yii::t('app','Instructions')?>: formatter->divID = 'markdown-instructions'; echo \Yii::$app->formatter->asMarkdown($metadata->instructions)?>
+ solution)):?>=\Yii::t('app','Solution')?>: formatter->divID = 'markdown-solution'; echo \Yii::$app->formatter->asMarkdown($metadata->solution)?>
- pre_credits)):?>=\Yii::t('app','Pre exploitation credits')?>: =\yii\helpers\Markdown::process($metadata->pre_credits,'gfm')?>
- pre_exploitation)):?>=\Yii::t('app','Pre exploitation details')?>: =\yii\helpers\Markdown::process($metadata->pre_exploitation,'gfm')?>
- player_id===Yii::$app->user->id && $target->progress==100) || Yii::$app->user->identity->isAdmin) && !empty($metadata->post_exploitation)):?>=\Yii::t('app','Post exploitation')?>: =\yii\helpers\Markdown::process($metadata->post_exploitation,'gfm')?>
- player_id===Yii::$app->user->id && $target->progress==100) || Yii::$app->user->identity->isAdmin) && !empty($metadata->post_credits)):?>=\Yii::t('app','Post exploitation credits')?>: =\yii\helpers\Markdown::process($metadata->post_credits,'gfm')?>
+ pre_credits)):?>=\Yii::t('app','Pre exploitation credits')?>: formatter->divID = 'markdown-pre-credits'; echo \Yii::$app->formatter->asMarkdown($metadata->pre_credits)?>
+ pre_exploitation)):?>=\Yii::t('app','Pre exploitation details')?>: formatter->divID = 'markdown-pre-exploitation'; echo \Yii::$app->formatter->asMarkdown($metadata->pre_exploitation)?>
+ player_id===Yii::$app->user->id && $target->progress==100) || Yii::$app->user->identity->isAdmin) && !empty($metadata->post_exploitation)):?>=\Yii::t('app','Post exploitation')?>: formatter->divID = 'markdown-post-exploitation'; echo \Yii::$app->formatter->asMarkdown($metadata->post_exploitation)?>
+ player_id===Yii::$app->user->id && $target->progress==100) || Yii::$app->user->identity->isAdmin) && !empty($metadata->post_credits)):?>=\Yii::t('app','Post exploitation credits')?>: formatter->divID = 'markdown-post-credits'; echo \Yii::$app->formatter->asMarkdown($metadata->post_credits)?>
+ formatter->divID=$oldId; ?>
diff --git a/frontend/themes/material/modules/target/views/default/_versus.php b/frontend/themes/material/modules/target/views/default/_versus.php
index 83185c2f3..cc5e36332 100644
--- a/frontend/themes/material/modules/target/views/default/_versus.php
+++ b/frontend/themes/material/modules/target/views/default/_versus.php
@@ -29,6 +29,18 @@
$this->registerMetaTag(['name' => 'game:points', 'content' => '0']);
$this->registerMetaTag(['name' => 'article:published_time', 'content' => $headshot->created_at]);
}
+$this->registerJsFile('@web/js/showdown.min.js',[
+ 'depends' => [
+ \yii\web\JqueryAsset::class
+ ]
+]);
+$this->registerJsFile('@web/assets/hljs/highlight.min.js',[
+ 'depends' => [
+ \yii\web\JqueryAsset::class
+ ]
+]);
+$this->registerCssFile('@web/assets/hljs/styles/a11y-dark.min.css',['depends' => ['app\assets\MaterialAsset']]);
+
?>
= Html::encode($team->name) ?>]
- owner_id === Yii::$app->user->id || ($team->invite && !$team->inviteonly)): ?>
+ owner_id === Yii::$app->user->id || ($team->inviteOrCreate && !$team->inviteonly)): ?>
owner_id === Yii::$app->user->id) $class .= ' copy-to-clipboard'; ?>
= \Yii::t('app', 'Allow other players to join the team easily by providing them with this link:') ?>
- = Html::a(Url::to(['/team/default/invite', 'token' => $team->invite->token], 'https'), Url::to(['/team/default/invite', 'token' => $team->invite->token], 'https'), ['class' => $class, 'swal-data' => 'Copied to clipboard!']); ?>
+ = Html::a(Url::to(['/team/default/invite', 'token' => $team->inviteOrCreate->token], 'https'), Url::to(['/team/default/invite', 'token' => $team->inviteOrCreate->token], 'https'), ['class' => $class, 'swal-data' => 'Copied to clipboard!']); ?>
= Html::encode($team->recruitment) ?>
@@ -179,8 +179,8 @@