File List
<?php
/**
* Contains definition of Admin class
*
* @author Lindsay Marshall <lindsay.marshall@ncl.ac.uk>
* @copyright 2012-2021 Newcastle University
* @package Framework
* @subpackage SystemPages
*/
namespace Framework\Pages;
use \Config\Framework as FW;
use \Support\Context;
/**
* A class that contains code to handle any /admin related requests.
*
* Admin status is checked in index.php so does not need to be done here.
*/
final class Admin extends \Framework\SiteAction
{
private const EDITABLE = [FW::TABLE, FW::FORM, FW::CONFIG, FW::PAGE, FW::USER];
private const VIEWABLE = [FW::TABLE, FW::FORM];
private const NOTMODEL = [FW::TABLE];
//private const HASH = 'sha384';
use \Support\NoCache; // don't cache admin pages.
/**
* Calculate integrity checksums for local js and css files
*
* @param \Support\Context $context
*
* @return void
* @psalm-suppress UnusedMethod
* @phpcsSuppress SlevomatCodingStandard.Classes.UnusedPrivateElements
*/
//private function checksum(Context $context) : void
//{
// chdir($context->local()->basedir()); // make sure we are in the root of the site
// $base = $context->local()->base();
// foreach ($context->local()->allconfig() as $fwc)
// {
// switch ($fwc->type)
// {
// case 'css':
// case 'js':
// if (!preg_match('#^(//|htt)#', $fwc->value)) // this is a local file
// {
// $fname = $fwc->value;
// if ($base != '/' && $base !== '')
// { // if there are sub directories then we need to remove them as we are there already...
// if (preg_match('#^'.$base.'(.*)#', $fname, $m))
// {
// $fname = $m[1];
// $fv = '%BASE%'.$fname;
// }
// else
// {
// $context->local()->message(\Framework\Local::ERROR, 'Could not de-base '.$fname.' ('.$base.')');
// break;
// }
// }
// else
// {
// $fv = $fname;
// }
// $hash = hash(self::HASH, file_get_contents('.'.$fname), TRUE);
// $fwc->value = $fv;
// $fwc->integrity = self::HASH.'-'.base64_encode($hash);
// $fwc->crossorigin = 'anonymous';
// \R::store($fwc);
// }
// break;
// }
// }
//}
/**
* Edit admin items
*
* @param Context $context The Context object
* @param array<string> $rest The rest of the URL
*
* @throws \Framework\Exception\Forbidden
* @throws \Framework\Exception\ParameterCount
* @throws \Framework\Exception\InternalError
*/
private function edit(Context $context, array $rest) : string
{
if (count($rest) < 3)
{
throw new \Framework\Exception\ParameterCount('Too few parameters');
}
$kind = $rest[1];
if (!in_array($kind, self::EDITABLE))
{
throw new \Framework\Exception\Forbidden('Not editable');
}
if (($notmodel = \in_array($kind, self::NOTMODEL)))
{
$class = '\\Framework\\Support\\'.$kind;
try
{
/** @psalm-suppress InvalidStringClass */
$obj = new $class($rest[2]);
}
catch (\Throwable $e)
{
$context->local()->message(\Framework\Local::ERROR, $e->getMessage());
$obj = NULL;
}
}
else
{
$obj = $context->load($kind, (int) $rest[2]);
}
$context->local()->addval('bean', $obj);
if (\is_object($obj))
{
$obj->startEdit($context, $rest); // do any special setup that the edit requires
if (($bid = $context->formdata('post')->fetch('bean', '')) !== '')
{ // this is a post
if (($notmodel && $bid != $kind) || $bid != $obj->getID())
{ // something odd...
throw new \Framework\Exception\BadValue('Bean param');
}
\Framework\Utility\CSRFGuard::getinstance()->check();
try
{
[$error, $emess] = $obj->edit($context); // handle the edit result
}
catch (\Throwable $e)
{
$error = TRUE;
$emess = $e->getMessage();
}
if ($error)
{
$context->local()->message(\Framework\Local::ERROR, $emess);
}
// The edit call might divert to somewhere else so sometimes we may not get here.
$context->local()->message(\Framework\Local::MESSAGE, 'Saved');
}
}
return '@edit/'.$kind.'.twig';
}
/**
* View admin items
*
* @param Context $context The Context object
* @param array<string> $rest The rest of the URL
*/
private function view(Context $context, array $rest) : string
{
if (\count($rest) < 3)
{
throw new \Framework\Exception\ParameterCount('Too few parameters');
}
$kind = $rest[1];
if (!\in_array($kind, self::VIEWABLE))
{
throw new \Framework\Exception\Forbidden('Not Viewable');
}
if (\in_array($kind, self::NOTMODEL))
{
$class = '\\Framework\\Support\\'.$kind;
try
{
/** @psalm-suppress InvalidStringClass */
$obj = new $class($rest[2]);
}
catch (\Throwable $e)
{
$context->local()->message(\Framework\Local::ERROR, $e->getMessage());
$obj = NULL;
}
}
else
{
$obj = $context->load($kind, (int) $rest[2]);
}
if (is_object($obj))
{
$obj->view($context, $rest); // do any required set up
$context->local()->addval('bean', $obj);
}
return '@view/'.$kind.'.twig';
}
/**
* Check for version updates and update config info. Check for new CSP needed
*
* @param Context $context The Context object
*/
private function update(Context $context) : string
{
$doit = $context->formdata('get')->fetch('update', 0) == 1;
$updated = [];
$newCSP = [];
$upd = \json_decode(file_get_contents('https://catless.ncl.ac.uk/framework/update/'));
if (isset($upd->fwconfig))
{ // now see if there are any config values that need updating.
$base = $context->local()->base();
foreach ($upd->fwconfig as $cname => $cdata)
{
if (\strpos($cdata->value, '%BASE%') === FALSE && $context->web()->checkCSP($cdata->value, $cdata->type))
{
$newCSP[] = [$cdata->value, $cdata->type];
}
$lval = \R::findOne(FW::CONFIG, 'name=?', [$cname]);
if (\is_object($lval))
{
if (($upderr = $lval->doupdate($cdata, $base, $doit)) !== '')
{
$updated[$cname] = $upderr;
}
}
else
{
$lval = \R::dispense(FW::CONFIG);
$lval->name = $cname;
$lval->local = 0;
foreach ($cdata as $k => $v)
{
$lval->$k = \preg_replace('/%BASE%/', $base, $v); // relocate to this base.
}
\R::store($lval);
$updated[$cname] = $cdata->value;
}
}
if (isset($upd->message))
{ // there is a message about the update
$context->local()->message(\Framework\Local::MESSAGE, $upd->message);
}
$current = \trim(\file_get_contents($context->local()->makebasepath('version.txt')));
$context->local()->addval([
'version' => $upd->version,
'older' => \version_compare($current, $upd->version, '<'),
'updated' => $updated,
'done' => $doit,
'current' => $current,
'newcsp' => $newCSP,
]);
}
return '@admin/update.twig';
}
/**
* Go offline (or online)
*
* Remember that if you go offline rather than adminonly you have to remove the file by hand to get back online!
*
* @param Context $context The Context object
*/
private function offline(Context $context)
{
$local = $context->local();
$adon = $local->makebasepath('admin', 'adminonly');
$adminonly = \file_exists($adon);
$fdt = $context->formdata('post');
if ($fdt->hasForm())
{ // it's a post
$msg = $fdt->mustFetch('msg');
$onlyadmin = $fdt->fetch('onlyadmin', 0);
$online = $fdt->fetch('online', 0);
if ($adminonly && ($online || $fdt->fetch('deladonly', 0) == 1))
{
\unlink($adon);
}
if ($online == 0)
{
$file = $onlyadmin == 1 ? $adon : $local->makebasepath('admin', 'offline');
$fd = \fopen($file, 'w');
\fputs($fd, $msg);
\fclose($fd);
}
else
{
$adminonly = FALSE;
}
$local->message(\Framework\Local::MESSAGE, 'Done');
}
$local->addval([
'adminonly' => $adminonly,
]);
return '@admin/offline.twig';
}
/**
* Handle various admin operations /admin/xxxx
*
* @param Context $context The context object for the site
*/
public function handle(Context $context) : array|string
{
$rest = $context->rest();
$context->setpages(); // most of the pages use pagination so get values if any
switch ($rest[0])
{
case 'beans': // Look at the beans in the database
$context->local()->addval('all', $context->hasadmin() && $context->formdata('get')->exists('all'));
$tpl = '@admin/beans.twig';
break;
case 'checksum': // calculate checksums for locally included files
$context->local()->message(\Framework\Local::WARNING, 'Currently not supported');
$tpl = '@admin/admin.twig';
break;
case 'config': // show and add config items
$tpl = '@admin/config.twig';
break;
case 'contexts': // show and add contexts
$tpl = '@admin/contexts.twig';
break;
case 'edit': // Edit something - forms, user, pages...
$tpl = $this->edit($context, $rest);
break;
case 'forms': // show and add forms
$tpl = '@admin/forms.twig';
break;
case 'info': // generate phpinfo page
$_SERVER['PHP_AUTH_PW'] = '*************'; // hide the password in case it is showing.
phpinfo();
exit; // phpinfo display is all we need
case 'offline':
$tpl = $this->offline($context);
break;
case 'pages': // show and add pages
$tpl = '@admin/pages.twig';
break;
case 'roles': // show and add roles
$tpl = '@admin/roles.twig';
break;
case 'update': // See if we need an update
$tpl = $this->update($context);
break;
case 'users': //show and add users
$tpl = '@admin/users.twig';
break;
case 'view': // view something - forms only at the moment
$tpl = $this->view($context, $rest);
break;
default:
$tpl = '@admin/admin.twig';
break;
}
return $tpl;
}
}
?>