Skip to content

Commit

Permalink
firmware: use model for more generic set and validation #4881
Browse files Browse the repository at this point in the history
We do have to jump through a few hoops to make this work.  First and
foremost during validation the model should have a "cleansed" view
of its data which means we add the subscription as a separate field
and append it to the mirror after validation.

It might be good to straighten this out later, also in the get path
so that we can hide all required translation in the controller until
we can move this to a standard GUI component and straighten out the
mirror read on the other end when subscriptions are required (but
currently no appended).
  • Loading branch information
fichtner committed Mar 8, 2023
1 parent 08ac6a2 commit 4c65524
Show file tree
Hide file tree
Showing 6 changed files with 155 additions and 166 deletions.
2 changes: 1 addition & 1 deletion plist
Expand Up @@ -270,7 +270,6 @@
/usr/local/opnsense/mvc/app/controllers/OPNsense/Core/Api/FirmwareController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Core/Api/MenuController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Core/Api/SystemController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Core/Api/repositories/opnsense.xml
/usr/local/opnsense/mvc/app/controllers/OPNsense/Core/FirmwareController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Core/HaltController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Core/IndexController.php
Expand Down Expand Up @@ -554,6 +553,7 @@
/usr/local/opnsense/mvc/app/models/OPNsense/Core/Menu/Menu.xml
/usr/local/opnsense/mvc/app/models/OPNsense/Core/Migrations/M1_0_0.php
/usr/local/opnsense/mvc/app/models/OPNsense/Core/Migrations/M1_0_1.php
/usr/local/opnsense/mvc/app/models/OPNsense/Core/repositories/opnsense.xml
/usr/local/opnsense/mvc/app/models/OPNsense/Cron/ACL/ACL.xml
/usr/local/opnsense/mvc/app/models/OPNsense/Cron/Cron.php
/usr/local/opnsense/mvc/app/models/OPNsense/Cron/Cron.xml
Expand Down
Expand Up @@ -29,7 +29,7 @@

namespace OPNsense\Core\Api;

use OPNsense\Base\ApiControllerBase;
use OPNsense\Base\ApiMutableModelControllerBase;
use OPNsense\Core\Backend;
use OPNsense\Core\Config;
use OPNsense\Core\Firmware;
Expand All @@ -38,35 +38,10 @@
* Class FirmwareController
* @package OPNsense\Core
*/
class FirmwareController extends ApiControllerBase
class FirmwareController extends ApiMutableModelControllerBase
{
/**
* @var null|BaseModel model object to work on
*/
private $modelHandle = null;

/**
* Get (or create) model object
* @return null|BaseModel
*/
private function getModel()
{
if ($this->modelHandle == null) {
$this->modelHandle = new Firmware();
}
return $this->modelHandle;
}

/**
* Query model items
* @return array plain model settings (non repeating items)
* @throws \ReflectionException when not bound to model
*/
protected function getModelNodes()
{
return $this->getModel()->getNodes();
return $result;
}
protected static $internalModelName = 'firmware';
protected static $internalModelClass = 'OPNsense\Core\Firmware';

/**
* return bytes in human-readable form
Expand Down Expand Up @@ -1006,125 +981,17 @@ public function infoAction()
* list firmware mirror and flavour options
* @return array
*/
public function getFirmwareOptionsAction()
public function getOptionsAction()
{
$families = [];
$families_has_subscription = [];
$flavours = [];
$flavours_allow_custom = false;
$flavours_has_subscription = [];
$mirrors = [];
$mirrors_allow_custom = false;
$mirrors_has_subscription = [];

$this->sessionClose(); // long running action, close session

foreach (glob(__DIR__ . "/repositories/*.xml") as $xml) {
$repositoryXml = simplexml_load_file($xml);
if ($repositoryXml === false || $repositoryXml->getName() != 'firmware') {
syslog(LOG_ERR, 'unable to parse firmware file ' . $xml);
} else {
if (isset($repositoryXml->mirrors->mirror)) {
if (isset($repositoryXml->mirrors->attributes()->allow_custom)) {
$mirrors_allow_custom = (strtolower($repositoryXml->mirrors->attributes()->allow_custom) == "true");
}
foreach ($repositoryXml->mirrors->mirror as $mirror) {
$mirrors[(string)$mirror->url] = (string)$mirror->description;
$attr = $mirror->attributes();
if (isset($attr->has_subscription) && strtolower($attr->has_subscription) == "true") {
$mirrors_has_subscription[] = (string)$mirror->url;
}
}
}
if (isset($repositoryXml->flavours->flavour)) {
if (isset($repositoryXml->flavours->attributes()->allow_custom)) {
$flavours_allow_custom = (strtolower($repositoryXml->flavours->attributes()->allow_custom) == "true");
}
foreach ($repositoryXml->flavours->flavour as $flavour) {
$flavours[(string)$flavour->name] = (string)$flavour->description;
$attr = $flavour->attributes();
if (isset($attr->has_subscription) && strtolower($attr->has_subscription) == "true") {
$flavours_has_subscription[] = (string)$flavour->name;
}
}
}
if (isset($repositoryXml->families->family)) {
foreach ($repositoryXml->families->family as $family) {
$families[(string)$family->name] = (string)$family->description;
$attr = $family->attributes();
if (isset($attr->has_subscription) && strtolower($attr->has_subscription) == "true") {
$families_has_subscription[] = (string)$family->name;
}
}
}
}
}
return [
/* provide a full set of data even though the frontend does not use it */
'families' => $families,
'families_allow_custom' => 0,
'families_has_subscription' => $families_has_subscription,
'flavours' => $flavours,
'flavours_allow_custom' => $flavours_allow_custom,
'flavours_has_subscription' => $flavours_has_subscription,
'mirrors' => $mirrors,
'mirrors_allow_custom' => $mirrors_allow_custom,
'mirrors_has_subscription' => $mirrors_has_subscription,
];
}

/**
* Validate firmware options
* @param $selectedMirror selected mirror url
* @param $selectedFlavour selected flavour
* @param $selectedType selected family type
* @param $selSubscription selected subscription id
* @return array with validation failure messages
*/
private function validateFirmwareOptions($selectedMirror, $selectedFlavour, $selectedType, $selSubscription)
{
$validOptions = $this->getFirmwareOptionsAction();
$invalid_msgs = [];

if (!$validOptions['mirrors_allow_custom'] && !isset($validOptions['mirrors'][$selectedMirror])) {
$invalid_msgs[] = sprintf(gettext('Unable to set invalid firmware mirror: %s'), $selectedMirror);
}

if (!$validOptions['flavours_allow_custom'] && !isset($validOptions['flavours'][$selectedFlavour])) {
$invalid_msgs[] = sprintf(gettext('Unable to set invalid firmware flavour: %s'), $selectedFlavour);
}

if (!isset($validOptions['families'][$selectedType])) {
$invalid_msgs[] = sprintf(gettext('Unable to set invalid firmware release type: %s'), $validOptions['families'][$selectedType]);
}

if (in_array($selectedMirror, $validOptions['mirrors_has_subscription'])) {
if (!preg_match('/[a-z0-9]{8}(-[a-z0-9]{4}){3}-[a-z0-9]{12}/i', $selSubscription)) {
$invalid_msgs[] = gettext('A valid subscription is required for this firmware mirror.');
}
if (!preg_match('/\//', $selectedFlavour)) {
/* error when flat flavour is used, but not when directory location was selected */
if (!in_array($selectedFlavour, $validOptions['flavours_has_subscription'])) {
$invalid_msgs[] = sprintf(gettext('Subscription requires the following flavour: %s'), $validOptions['flavours'][$validOptions['flavours_has_subscription'][0]]);
}
if (!in_array($selectedType, $validOptions['families_has_subscription'])) {
$invalid_msgs[] = sprintf(gettext('Subscription requires the following type: %s'), $validOptions['families'][$validOptions['families_has_subscription'][0]]);
}
}
} else {
if (!empty($selSubscription)) {
$invalid_msgs[] = gettext('Subscription cannot be set for non-subscription firmware mirror.');
}
}

return $invalid_msgs;
return $this->getModel()->getRepositoryOptions();
}

/**
* retrieve current firmware configuration options
* @return array
*/
public function getFirmwareConfigAction()
public function getAction()
{
return $this->getModelNodes();
}
Expand All @@ -1133,7 +1000,7 @@ public function getFirmwareConfigAction()
* set firmware configuration options
* @return array status
*/
public function setFirmwareConfigAction()
public function setAction()
{
$response = ['status' => 'failure'];

Expand All @@ -1143,37 +1010,38 @@ public function setFirmwareConfigAction()

$mdl = $this->getModel();

/* XXX emulates: $mdl->setNodes($this->request->getPost(static::$internalModelName)); */

$selectedMirror = filter_var($this->request->getPost('mirror', null, ''), FILTER_SANITIZE_URL);
$selectedFlavour = filter_var($this->request->getPost('flavour', null, ''), FILTER_SANITIZE_URL);
$selectedType = filter_var($this->request->getPost('type', null, ''), FILTER_SANITIZE_URL);
$selSubscription = filter_var($this->request->getPost('subscription', null, ''), FILTER_SANITIZE_URL);

$ret = $this->validateFirmwareOptions($selectedMirror, $selectedFlavour, $selectedType, $selSubscription);
if (count($ret)) {
$response['status_msg'] = $ret;
$mdl->mirror = $selectedMirror;
$mdl->flavour = $selectedFlavour;
$mdl->type = $selectedType;
$mdl->subscription = $selSubscription;
/* discards plugins on purpose for the time being */

$ret = $this->validate();
if (!empty($ret['result'])) {
$response['status_msg'] = array_values($ret['validations'] ?? [gettext('Unkown firmware validation error')]);
return $response;
}

$response['status'] = 'ok';

if (!empty($selSubscription)) {
/* prepend subscription */
$selectedMirror .= '/' . $selSubscription;
}

$mdl->mirror = $selectedMirror;
$mdl->flavour = $selectedFlavour;
$mdl->type = $selectedType;
/* discards plugins on purpose for the time being */
if (!empty((string)$mdl->subscription)) {
/* append subscription */
$mdl->mirror = (string)$mdl->mirror . '/' . (string)$mdl->subscription;
}

$mdl->serializeToConfig();
Config::getInstance()->save();
$response['status'] = 'ok';
$this->save();

$this->sessionClose(); // long running action, close session
$this->sessionClose(); // long running action, close session

$backend = new Backend();
$backend->configdRun('firmware flush');
$backend->configdRun('firmware configure');
$backend = new Backend();
$backend->configdRun('firmware flush');
$backend->configdRun('firmware configure');

return $response;
}
Expand Down
119 changes: 118 additions & 1 deletion src/opnsense/mvc/app/models/OPNsense/Core/Firmware.php
@@ -1,7 +1,7 @@
<?php

/*
* Copyright (C) 2021 Deciso B.V.
* Copyright (C) 2021-2023 Deciso B.V.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
Expand Down Expand Up @@ -29,11 +29,128 @@
namespace OPNsense\Core;

use OPNsense\Base\BaseModel;
use Phalcon\Messages\Message;

/**
* Class Firmware
* @package OPNsense\Core
*/
class Firmware extends BaseModel
{
/**
* helper function to list firmware mirror and flavour options
* @return array
*/
public function getRepositoryOptions()
{
$families = [];
$families_has_subscription = [];
$flavours = [];
$flavours_allow_custom = false;
$flavours_has_subscription = [];
$mirrors = [];
$mirrors_allow_custom = false;
$mirrors_has_subscription = [];

foreach (glob(__DIR__ . "/repositories/*.xml") as $xml) {
$repositoryXml = simplexml_load_file($xml);
if ($repositoryXml === false || $repositoryXml->getName() != 'firmware') {
syslog(LOG_ERR, 'unable to parse firmware file ' . $xml);
} else {
if (isset($repositoryXml->mirrors->mirror)) {
if (isset($repositoryXml->mirrors->attributes()->allow_custom)) {
$mirrors_allow_custom = (strtolower($repositoryXml->mirrors->attributes()->allow_custom) == "true");
}
foreach ($repositoryXml->mirrors->mirror as $mirror) {
$mirrors[(string)$mirror->url] = (string)$mirror->description;
$attr = $mirror->attributes();
if (isset($attr->has_subscription) && strtolower($attr->has_subscription) == "true") {
$mirrors_has_subscription[] = (string)$mirror->url;
}
}
}
if (isset($repositoryXml->flavours->flavour)) {
if (isset($repositoryXml->flavours->attributes()->allow_custom)) {
$flavours_allow_custom = (strtolower($repositoryXml->flavours->attributes()->allow_custom) == "true");
}
foreach ($repositoryXml->flavours->flavour as $flavour) {
$flavours[(string)$flavour->name] = (string)$flavour->description;
$attr = $flavour->attributes();
if (isset($attr->has_subscription) && strtolower($attr->has_subscription) == "true") {
$flavours_has_subscription[] = (string)$flavour->name;
}
}
}
if (isset($repositoryXml->families->family)) {
foreach ($repositoryXml->families->family as $family) {
$families[(string)$family->name] = (string)$family->description;
$attr = $family->attributes();
if (isset($attr->has_subscription) && strtolower($attr->has_subscription) == "true") {
$families_has_subscription[] = (string)$family->name;
}
}
}
}
}
return [
/* provide a full set of data even though the frontend does not use it */
'families' => $families,
'families_allow_custom' => 0,
'families_has_subscription' => $families_has_subscription,
'flavours' => $flavours,
'flavours_allow_custom' => $flavours_allow_custom,
'flavours_has_subscription' => $flavours_has_subscription,
'mirrors' => $mirrors,
'mirrors_allow_custom' => $mirrors_allow_custom,
'mirrors_has_subscription' => $mirrors_has_subscription,
];
}

/**
* {@inheritdoc}
*/
public function performValidation($validateFullModel = true)
{
$validOptions = $this->getRepositoryOptions();

/* XXX for now make sure the subscription is removed if given */
$mirror_stripped = (string)$this->mirror;
if (!empty((string)$this->subscription)) {
$mirror_stripped = str_replace('/' . (string)$this->subscription, '', $mirror_stripped);
}

/* standard model validations */
$messages = parent::performValidation($validateFullModel);

/* extended validations */
if (!$validOptions['mirrors_allow_custom'] && !isset($validOptions['mirrors'][$mirror_stripped])) {
$messages->appendMessage(new Message(gettext('Unable to set invalid firmware mirror'), 'mirror'));
}
if (!$validOptions['flavours_allow_custom'] && !isset($validOptions['flavours'][(string)$this->flavour])) {
$messages->appendMessage(new Message(gettext('Unable to set invalid firmware flavour'), 'flavour'));
}
if (!isset($validOptions['families'][(string)$this->type])) {
$messages->appendMessage(new Message(gettext('Unable to set invalid firmware release type'), 'type'));
}
if (in_array($mirror_stripped, $validOptions['mirrors_has_subscription'])) {
if (!preg_match('/^[a-z0-9]{8}(-[a-z0-9]{4}){3}-[a-z0-9]{12}$/i', (string)$this->subscription)) {
$messages->appendMessage(new Message(gettext('A valid subscription is required for this firmware mirror'), 'subscription'));
}
if (!preg_match('/\//', (string)$this->flavour)) {
/* error when flat flavour is used, but not when directory location was selected for development and testing */
if (!in_array((string)$this->flavour, $validOptions['flavours_has_subscription'])) {
$messages->appendMessage(new Message(sprintf(gettext('Subscription requires the following flavour: %s'), $validOptions['flavours'][$validOptions['flavours_has_subscription'][0]]), 'flavour'));
}
if (!in_array((string)$this->type, $validOptions['families_has_subscription'])) {
$messages->appendMessage(new Message(sprintf(gettext('Subscription requires the following type: %s'), $validOptions['families'][$validOptions['families_has_subscription'][0]]), 'type'));
}
}
} else {
if (!empty((string)$this->subscription)) {
$messages->appendMessage(new Message(gettext('Subscription cannot be set for non-subscription firmware mirror'), 'subscription'));
}
}

return $messages;
}
}

0 comments on commit 4c65524

Please sign in to comment.