Merge pull request #1841 from nextcloud/feature/ncp-app-adminsection

Add admin section to nextcloudpi nc app
This commit is contained in:
Tobias Knöppler 2023-10-20 16:53:15 +02:00 committed by GitHub
commit 5b246e1916
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 20211 additions and 32 deletions

View File

@ -16,7 +16,9 @@ git clone -b "$BRANCH" --depth 20 -q --bare https://github.com/nextcloud/nextclo
cd "$TEMPDIR" || exit 1
VER=$( git describe --always --tags | grep -oP "v\d+\.\d+\.\d+" )
[[ -f "/usr/local/etc/instance.cfg" ]] && {
canary="$(get_app_param ncp-community.sh CANARY)"
[[ "$canary" != "yes" ]] && [[ -f "/usr/local/etc/instance.cfg" ]] && {
cohorte_id="$(jq .cohorteId /usr/local/etc/instance.cfg)"
[[ -f "./staged_rollouts/${VER}.txt" ]] && {
grep "^${cohorte_id}$" "./staged_rollouts/${VER}.txt" || {

View File

@ -77,7 +77,6 @@ fi
cat > /usr/local/etc/instance.cfg <<EOF
{
"cohorteId": ${cohorte_id},
"canary": false
}
EOF
}

View File

@ -0,0 +1,7 @@
#!/bin/bash
# Configure various settings for community participation
install() { :; }
configure() { :; }

View File

@ -212,6 +212,30 @@ function find_app_param_num()
}
function get_app_params() {
local script="${1?}"
local cfg_file="${CFGDIR}/${script%.sh}.cfg"
[[ -f "$cfg_file" ]] && {
local cfg="$( cat "$cfg_file" )"
local param_count="$(jq ".params | length" <<<"$cfg")"
local i=0
local json="{"
while [[ $i -lt $param_count ]]
do
param_id="$(jq -r ".params[$i].id" <<<"$cfg")"
param_val="$(jq -r ".params[$i].value" <<<"$cfg")"
json="${json}"$'\n'" \"${param_id}\": \"${param_val}\""
i=$((i+1))
[[ $i -lt $param_count ]] && json="${json},"
done
json="${json}"$'\n'"}"
echo "$json"
return 0
}
return 1
}
install_template() {
local template="${1?}"
local target="${2?}"

View File

@ -0,0 +1,35 @@
{
"id": "ncp-community",
"name": "NCP Community",
"title": "NCP Community Settings",
"description": "Configure various NCP community options",
"info": "",
"infotitle": "",
"params": [
{
"id": "CANARY",
"name": "Enable canary (testing) channel for updates",
"value": "no",
"type": "bool"
},
{
"id": "ADMIN_NOTIFICATIONS",
"name": "Enable notifications about changes in NCP",
"value": "yes",
"type": "bool"
},
{
"id": "USAGE_SURVEYS",
"name": "Help me improve NCP by participating in occasional usage surveys",
"value": "no",
"type": "bool"
},
{
"id": "NOTIFICATION_ACCOUNTS",
"name": "Limit notifications to these accounts",
"value": "",
"suggest": "Comma separated list of nextcloud accounts",
"type": "text"
}
]
}

View File

@ -84,14 +84,6 @@ rm /.ncp-image
cd -
rm -rf "${TEMPDIR}"
cohorte_id=$((RANDOM % 100))
cat <<EOF > /usr/local/etc/instance.cfg
{
"cohorteId": ${cohorte_id},
"canary": false
}
EOF
IP="$(get_ip)"
echo "Done.

7
ncp-app/.eslintrc.js Normal file
View File

@ -0,0 +1,7 @@
// SPDX-FileCopyrightText: Tobias Knöppler <tobias@knoeppler.net>
// SPDX-License-Identifier: AGPL-3.0-or-later
module.exports = {
extends: [
'@nextcloud',
]
}

3
ncp-app/.gitattributes vendored Normal file
View File

@ -0,0 +1,3 @@
# SPDX-FileCopyrightText: Tobias Knöppler <tobias@knoeppler.net>
# SPDX-License-Identifier: AGPL-3.0-or-later
/js/* binary

9
ncp-app/.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
# SPDX-FileCopyrightText: Tobias Knöppler <tobias@knoeppler.net>
# SPDX-License-Identifier: AGPL-3.0-or-later
.idea
*.iml
/vendor/
/build/
node_modules/
/.php_cs.cache
js/*hot-update.*

View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
// SPDX-FileCopyrightText: Tobias Knöppler <tobias@knoeppler.net>
// SPDX-License-Identifier: AGPL-3.0-or-later
require_once './vendor/autoload.php';
use Nextcloud\CodingStandard\Config;
$config = new Config();
$config
->getFinder()
->ignoreVCSIgnored(true)
->notPath('build')
->notPath('l10n')
->notPath('src')
->notPath('vendor')
->in(__DIR__);
return $config;

164
ncp-app/Makefile Normal file
View File

@ -0,0 +1,164 @@
# SPDX-FileCopyrightText: Bernhard Posselt <dev@bernhard-posselt.com>
# SPDX-License-Identifier: AGPL-3.0-or-later
# Generic Makefile for building and packaging a Nextcloud app which uses npm and
# Composer.
#
# Dependencies:
# * make
# * which
# * curl: used if phpunit and composer are not installed to fetch them from the web
# * tar: for building the archive
# * npm: for building and testing everything JS
#
# If no composer.json is in the app root directory, the Composer step
# will be skipped. The same goes for the package.json which can be located in
# the app root or the js/ directory.
#
# The npm command by launches the npm build script:
#
# npm run build
#
# The npm test command launches the npm test script:
#
# npm run test
#
# The idea behind this is to be completely testing and build tool agnostic. All
# build tools and additional package managers should be installed locally in
# your project, since this won't pollute people's global namespace.
#
# The following npm scripts in your package.json install and update the bower
# and npm dependencies and use gulp as build system (notice how everything is
# run from the node_modules folder):
#
# "scripts": {
# "test": "node node_modules/gulp-cli/bin/gulp.js karma",
# "prebuild": "npm install && node_modules/bower/bin/bower install && node_modules/bower/bin/bower update",
# "build": "node node_modules/gulp-cli/bin/gulp.js"
# },
app_name=$(notdir $(CURDIR))
build_tools_directory=$(CURDIR)/build/tools
source_build_directory=$(CURDIR)/build/artifacts/source
source_package_name=$(source_build_directory)/$(app_name)
appstore_build_directory=$(CURDIR)/build/artifacts
appstore_package_name=$(appstore_build_directory)/$(app_name)
npm=$(shell which npm 2> /dev/null)
composer=$(shell which composer 2> /dev/null)
all: build
# Fetches the PHP and JS dependencies and compiles the JS. If no composer.json
# is present, the composer step is skipped, if no package.json or js/package.json
# is present, the npm step is skipped
.PHONY: build
build:
ifneq (,$(wildcard $(CURDIR)/composer.json))
make composer
endif
ifneq (,$(wildcard $(CURDIR)/package.json))
make npm
endif
ifneq (,$(wildcard $(CURDIR)/js/package.json))
make npm
endif
cp js-src/*.js js/
# Installs and updates the composer dependencies. If composer is not installed
# a copy is fetched from the web
.PHONY: composer
composer:
ifeq (, $(composer))
@echo "No composer command available, downloading a copy from the web"
mkdir -p $(build_tools_directory)
curl -sS https://getcomposer.org/installer | php
mv composer.phar $(build_tools_directory)
php $(build_tools_directory)/composer.phar install --prefer-dist
else
composer install --prefer-dist
endif
# Installs npm dependencies
.PHONY: npm
npm:
ifeq (,$(wildcard $(CURDIR)/package.json))
cd js && $(npm) run build
else
npm run build
endif
# Removes the appstore build
.PHONY: clean
clean:
rm -rf ./build
# Same as clean but also removes dependencies installed by composer, bower and
# npm
.PHONY: distclean
distclean: clean
rm -rf vendor
rm -rf node_modules
rm -rf js/vendor
rm -rf js/node_modules
# Builds the source and appstore package
.PHONY: dist
dist:
make source
make appstore
# Builds the source package
.PHONY: source
source:
rm -rf $(source_build_directory)
mkdir -p $(source_build_directory)
tar cvzf $(source_package_name).tar.gz \
--exclude-vcs \
--exclude="../$(app_name)/build" \
--exclude="../$(app_name)/js/node_modules" \
--exclude="../$(app_name)/node_modules" \
--exclude="../$(app_name)/*.log" \
--exclude="../$(app_name)/js/*.log" \
../$(app_name) \
# Builds the source package for the app store, ignores php tests, js tests
# and build related folders that are unnecessary for an appstore release
.PHONY: appstore
appstore:
rm -rf $(appstore_build_directory)
mkdir -p $(appstore_build_directory)
tar cvzf $(appstore_package_name).tar.gz \
--exclude-vcs \
--exclude="../$(app_name)/build" \
--exclude="../$(app_name)/tests" \
--exclude="../$(app_name)/Makefile" \
--exclude="../$(app_name)/*.log" \
--exclude="../$(app_name)/phpunit*xml" \
--exclude="../$(app_name)/composer.*" \
--exclude="../$(app_name)/node_modules" \
--exclude="../$(app_name)/js/node_modules" \
--exclude="../$(app_name)/js/tests" \
--exclude="../$(app_name)/js/test" \
--exclude="../$(app_name)/js/*.log" \
--exclude="../$(app_name)/js/package.json" \
--exclude="../$(app_name)/js/bower.json" \
--exclude="../$(app_name)/js/karma.*" \
--exclude="../$(app_name)/js/protractor.*" \
--exclude="../$(app_name)/package.json" \
--exclude="../$(app_name)/bower.json" \
--exclude="../$(app_name)/karma.*" \
--exclude="../$(app_name)/protractor\.*" \
--exclude="../$(app_name)/.*" \
--exclude="../$(app_name)/js/.*" \
--exclude="../$(app_name)/webpack.config.js" \
--exclude="../$(app_name)/stylelint.config.js" \
--exclude="../$(app_name)/CHANGELOG.md" \
--exclude="../$(app_name)/README.md" \
--exclude="../$(app_name)/package-lock.json" \
--exclude="../$(app_name)/LICENSES" \
../$(app_name) \
.PHONY: test
test: composer
$(CURDIR)/vendor/phpunit/phpunit/phpunit -c phpunit.xml
$(CURDIR)/vendor/phpunit/phpunit/phpunit -c phpunit.integration.xml

View File

@ -2,17 +2,17 @@
<info xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd">
<id>nextcloudpi</id>
<name>NextCloudPi</name>
<name>NextcloudPi</name>
<summary>Nextcloud management tools</summary>
<description><![CDATA[NextCloudPi features a preconfigured Nextcloud instance and a complete set of tools around it for easy management.]]></description>
<version>0.0.1</version>
<version>0.0.2</version>
<licence>agpl</licence>
<author mail="nachoparker@ownyourbits.com" homepage="https://ownyourbits.com">nachoparker</author>
<namespace>NextCloudPi</namespace>
<namespace>NextcloudPi</namespace>
<category>tools</category>
<bugs>https://github.com/nextcloud/nextcloudpi/issues</bugs>
<dependencies>
<nextcloud min-version="14" max-version="27"/>
<nextcloud min-version="22" max-version="27"/>
</dependencies>
<navigations>
<navigation>
@ -20,4 +20,7 @@
<route>nextcloudpi.page.index</route>
</navigation>
</navigations>
<settings>
<admin>OCA\NextcloudPi\Settings\AdminSettings</admin>
</settings>
</info>

View File

@ -2,13 +2,14 @@
/**
* Create your routes in here. The name is the lowercase name of the controller
* without the controller part, the stuff after the hash is the method.
* e.g. page#index -> OCA\NextCloudPi\Controller\PageController->index()
* e.g. page#index -> OCA\NextcloudPi\Controller\PageController->index()
*
* The controller class has to be registered in the application.php file since
* it's instantiated in there
*/
return [
'routes' => [
['name' => 'page#index', 'url' => '/', 'verb' => 'GET'],
]
'routes' => [
['name' => 'page#index', 'url' => '/', 'verb' => 'GET'],
['name' => 'settings#save', 'url' => '/api/settings', 'verb' => 'POST']
]
];

5
ncp-app/babel.config.js Normal file
View File

@ -0,0 +1,5 @@
// SPDX-FileCopyrightText: Tobias Knöppler <tobias@knoeppler.net>
// SPDX-License-Identifier: AGPL-3.0-or-later
const babelConfig = require('@nextcloud/babel-config')
module.exports = babelConfig

38
ncp-app/composer.json Normal file
View File

@ -0,0 +1,38 @@
{
"name": "nextcloud/nextcloudpi",
"description": "NextcloudPi App",
"type": "project",
"license": "AGPL-3.0-or-later",
"authors": [
{
"name": "Tobias Knöppler"
}
],
"require-dev": {
"phpunit/phpunit": "^9",
"sabre/dav": "^4.1",
"sabre/xml": "^2.2",
"symfony/event-dispatcher": "^5.3.11",
"christophwurst/nextcloud": "dev-master@dev",
"psalm/phar": "^4.10",
"nextcloud/coding-standard": "^1.0"
},
"scripts": {
"lint": "find nc-app/nextcloudpi -name \\\\*.php -not -path './vendor/*' -print0 | xargs -0 -n1 php -l",
"cs:check": "php-cs-fixer fix --dry-run --diff",
"cs:fix": "php-cs-fixer fix",
"psalm": "psalm.phar --threads=1",
"psalm:update-baseline": "psalm.phar --threads=1 --update-baseline",
"psalm:update-baseline:force": "psalm.phar --threads=1 --update-baseline --set-baseline=tests/psalm-baseline.xml",
"psalm:clear": "psalm.phar --clear-cache && psalm --clear-global-cache",
"psalm:fix": "psalm.phar --alter --issues=InvalidReturnType,InvalidNullableReturnType,MissingParamType,InvalidFalsableReturnType"
},
"config": {
"allow-plugins": {
"composer/package-versions-deprecated": true
},
"platform": {
"php": "7.4"
}
}
}

2839
ncp-app/composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

38
ncp-app/css/admin.css Normal file
View File

@ -0,0 +1,38 @@
#nextcloudpi li {
display: flex;
flex-direction: row;
justify-content: start;
margin-left: 2em;
}
#nextcloudpi li div:first-of-type {
width: 10em;
white-space: nowrap;
}
#nextcloudpi li * {
line-height: 36px;
}
#nextcloudpi hidden {
display: none;
}
#nextcloudpi .error-message {
padding: 1em;
color: red;
border-width: 1px;
border-color: red;
border-style: dashed;
}
#nextcloudpi input[name="notificationAccounts"] {
width: 27em;
}
.divider {
height: .5em;
border-top-width: 1px;
border-top-style: dotted;
border-top-color: var(--color-main-text);
}

6
ncp-app/js-src/script.js Normal file
View File

@ -0,0 +1,6 @@
// open the NCP web panel
var url = window.location.protocol + '//' + window.location.hostname + ':4443';
if ( !window.open( url, '_blank' ) ) // try to open in a new tab first
window.location.href = url;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
// SPDX-FileCopyrightText: Tobias Knöppler <tobias@knoeppler.net>
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace OCA\NextcloudPi\AppInfo;
use OCP\AppFramework\App;
class Application extends App {
public const APP_ID = 'nextcloudpi';
public function __construct() {
parent::__construct(self::APP_ID);
}
}

View File

@ -1,5 +1,5 @@
<?php
namespace OCA\NextCloudPi\Controller;
namespace OCA\NextcloudPi\Controller;
use OCP\IRequest;
use OCP\AppFramework\Http\TemplateResponse;

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace OCA\NextcloudPi\Controller;
use OCA\NextcloudPi\Exceptions\InvalidSettingsException;
use OCA\NextcloudPi\Exceptions\SaveSettingsException;
use OCA\NextcloudPi\Service\SettingsService;
use OCP\IRequest;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Controller;
class SettingsController extends Controller {
/** @var SettingsService */
private $service;
/**
* SettingsController constructor
* @param SettingsService $service
*/
public function __construct(SettingsService $service) {
$this->service = $service;
}
/**
* @NoCSRFRequired
* @CORS
*
* @param array $settings
*/
public function save(array $settings): JSONResponse {
try {
$this->service->saveSettings($settings);
return new JSONResponse([]);
} catch(InvalidSettingsException $e) {
return new JSONResponse(["error" => $e->getMessage()], Http::STATUS_BAD_REQUEST);
} catch(SaveSettingsException $e) {
return new JSONResponse(["error" => $e->getMessage()], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}
}
?>

View File

@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace OCA\NextcloudPi\Exceptions;
use Exception;
class InvalidSettingsException extends Exception {
}
?>

View File

@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace OCA\NextcloudPi\Exceptions;
use Exception;
class SaveSettingsException extends Exception {
}
?>

View File

@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
namespace OCA\NextcloudPi\Service;
use OCA\NextcloudPi\Exceptions\InvalidSettingsException;
use OCA\NextcloudPi\Exceptions\SaveSettingsException;
use Psr\Log\LoggerInterface;
class SettingsService {
private LoggerInterface $logger;
public function __construct(LoggerInterface $logger) {
$this->logger = $logger;
}
/**
* @param $name string of the config
* @param array $defaults Default value to use if the config can't be loaded
* @return array
*/
public function getConfig(string $name, array $defaults): array
{
[$ret, $config_str, $stderr] = $this->runCommand( "bash -c \"sudo /home/www/ncp-app-bridge.sh config $name\"");
$config = null;
if ($ret == 0) {
try {
$config = json_decode($config_str, true, 512, JSON_THROW_ON_ERROR);
} catch (\Exception $e) {
$this->logger->error($e);
}
}
if ($config == null) {
$this->logger->error("Failed to retrieve ncp config (exit code: $ret)");
return $defaults;
}
return $config;
}
/**
* @param $name string of the config
* @param string $defaults Default value to use if the file can't be loaded
* @return string
*/
public function getFileContent(string $name, string $defaults): string
{
[$ret, $file_contents, $stderr] = $this->runCommand( "bash -c \"sudo /home/www/ncp-app-bridge.sh file $name\"");
if ($ret != 0) {
return $defaults;
}
return $file_contents;
}
/**
* @throws InvalidSettingsException
* @throws SaveSettingsException
*/
public function saveSettings(array $settings) {
$parseBool = function ($val): string {
return $val ? "yes" : "no";
};
$identityFn = function ($val) {
return $val;
};
$settings_map = [
"canary" => ["ncp-community", "CANARY", $parseBool],
"adminNotifications" => ["ncp-community", "ADMIN_NOTIFICATIONS", $parseBool],
"usageSurveys" => ["ncp-community", "USAGE_SURVEYS", $parseBool],
"notificationAccounts" => ["ncp-community", "NOTIFICATION_ACCOUNTS", $identityFn]
];
foreach ($settings as $k => $value) {
[$cfgName, $fieldName, $fn] = $settings_map[$k];
if ($cfgName == null || $fieldName == null) {
throw new InvalidSettingsException("key error for '$k'");
}
$parsed = $fn($value);
$cmd = "bash -c \"sudo /home/www/ncp-app-bridge.sh config '$cfgName' '$fieldName=$parsed'\"";
[$ret, $stdout, $stderr] = $this->runCommand($cmd);
if ($ret !== 0) {
throw new SaveSettingsException(
"Failed to save NCP settings '$cfgName/$fieldName': \n error output from command:\n\n$cmd"
. str_replace("\n", "\n> ", $stderr));
}
}
}
private function runCommand(string $cmd): array {
$descriptorSpec = [
0 => ["pipe", "r"],
1 => ["pipe", "w"],
2 => ["pipe", "w"]
];
$proc = proc_open($cmd, $descriptorSpec, $pipes, "/home/www-data", null);
$stdout = stream_get_contents($pipes[1]);
fclose($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
fclose($pipes[2]);
return [proc_close($proc), $stdout, $stderr];
}
}
?>

View File

@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace OCA\NextcloudPi\Settings;
use OCA\NextcloudPi\Service\SettingsService;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\Settings\ISettings;
class AdminSettings implements ISettings {
/** @var SettingsService */
private $service;
/**
* AdminSettings constructor
* @param SettingsService $service
*/
public function __construct(SettingsService $service) {
$this->service = $service;
}
/**
* @return TemplateResponse
*/
public function getForm() {
$ncp_config = $this->service->getConfig("ncp",
["nextcloud_version" => "unknown", "php_version" => "unknown", "release" => "unknown"]);
$community_config = $this->service->getConfig("ncp-community",
[
"CANARY" => 'no',
"USAGE_SURVEYS" => 'no',
"ADMIN_NOTIFICATIONS" => 'no',
"NOTIFICATION_ACCOUNTS" => ""
]);
$ncp_version = trim($this->service->getFileContent("ncp-version", "unknown"));
return new TemplateResponse('nextcloudpi', 'admin', [
'community' => $community_config,
'ncp' => $ncp_config,
'ncp_version' => $ncp_version
]);
}
/**
* @return string
*/
public function getSection() {
return "server";
}
/**
* @return int
*/
public function getPriority() {
return 1;
}
}
?>

BIN
ncp-app/ncp.tar.gz Normal file

Binary file not shown.

16325
ncp-app/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

43
ncp-app/package.json Normal file
View File

@ -0,0 +1,43 @@
{
"name": "nextcloudpi",
"description": "NextcloudPi App",
"version": "0.0.1",
"author": "Tobias Knöppler <tobias@knoeppler.net>",
"contributors": [],
"bugs": {
"url": "https://github.com/nextcloud/nextcloudpi"
},
"license": "agpl",
"private": true,
"scripts": {
"build": "webpack --node-env production --progress",
"dev": "webpack --node-env development --progress",
"watch": "webpack --node-env development --progress --watch",
"serve": "webpack --node-env development serve --progress",
"lint": "eslint --ext .js,.vue src",
"lint:fix": "eslint --ext .js,.vue src --fix",
"stylelint": "stylelint css/*.css css/*.scss ../../src/**/*.scss src/**/*.vue",
"stylelint:fix": "stylelint css/*.css css/*.scss ../../src/**/*.scss src/**/*.vue --fix"
},
"dependencies": {
"@nextcloud/axios": "^1.10.0",
"@nextcloud/dialogs": "^3.1.4",
"@nextcloud/router": "^2.0.0",
"@nextcloud/vue": "^5.4.0",
"vue": "^2.7.0"
},
"browserslist": [
"extends @nextcloud/browserslist-config"
],
"engines": {
"node": "^16.0.0",
"npm": "^7.0.0 || ^8.0.0"
},
"devDependencies": {
"@nextcloud/babel-config": "^1.0.0",
"@nextcloud/browserslist-config": "^2.2.0",
"@nextcloud/eslint-config": "^8.0.0",
"@nextcloud/stylelint-config": "^2.1.2",
"@nextcloud/webpack-vue-config": "^5.2.1"
}
}

38
ncp-app/psalm.xml Normal file
View File

@ -0,0 +1,38 @@
<?xml version="1.0"?>
<psalm
errorLevel="4"
resolveFromConfigFile="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
errorBaseline="tests/psalm-baseline.xml"
>
<!--
SPDX-FileCopyrightText: Tobias Knöppler <tobias@knoeppler.net>
SPDX-License-Identifier: CC0-1.0
-->
<projectFiles>
<directory name="lib" />
<ignoreFiles>
<directory name="vendor" />
</ignoreFiles>
</projectFiles>
<extraFiles>
<directory name="vendor" />
<ignoreFiles>
<directory name="vendor/phpunit/php-code-coverage" />
<directory name="vendor/psalm" />
</ignoreFiles>
</extraFiles>
<issueHandlers>
<UndefinedDocblockClass>
<errorLevel type="suppress">
<referencedClass name="OC\AppFramework\OCS\BaseResponse"/>
<referencedClass name="Doctrine\DBAL\Schema\Schema" />
<referencedClass name="Doctrine\DBAL\Schema\SchemaException" />
<referencedClass name="Doctrine\DBAL\Driver\Statement" />
<referencedClass name="Doctrine\DBAL\Schema\Table" />
</errorLevel>
</UndefinedDocblockClass>
</issueHandlers>
</psalm>

66
ncp-app/src/main-admin.js Normal file
View File

@ -0,0 +1,66 @@
/**
* SPDX-FileCopyrightText: 2018 John Molakvoæ <skjnldsv@protonmail.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import {generateFilePath, generateUrl} from '@nextcloud/router'
import axios from '@nextcloud/axios'
//
// import Vue from 'vue'
// import App from './App.vue'
// eslint-disable-next-line
__webpack_public_path__ = generateFilePath(appName, '', 'js/')
//
// Vue.mixin({ methods: { t, n } })
//
// export default new Vue({
// el: '#nextcloudpi',
// render: h => h(App),
// })
async function saveSettings() {
let settings = collectSettings();
console.log("Saving nextcloudpi settings: ", settings);
try {
let response = await axios.post(generateUrl('/apps/nextcloudpi/api/settings'), {settings: settings})
console.log("Saving ncp settings succeeded")
return {success: true, error: null}
} catch (e) {
// console.log("axios failure: ", arguments)
console.error(e)
let errMsg = e.response.data.error;
throw Error(`${errMsg ? errMsg : e.message} (HTTP ${e.response.status})`)
}
}
function collectSettings() {
let settings = {};
document.querySelectorAll("#nextcloudpi input").forEach(element => {
if (element.type === "checkbox") {
settings[element.name] = element.checked;
} else {
settings[element.name] = element.value;
}
});
return settings;
}
window.addEventListener('load', () => {
console.log("Listening to ncp settings changes");
let errorBox = document.querySelector("#nextcloudpi .error-message");
document.querySelectorAll("#nextcloudpi input").forEach(element => {
element.addEventListener("change", async () => {
saveSettings()
.then(() => {
errorBox.classList.add("hidden");
})
.catch(e => {
console.error(e);
errorBox.innerText = "Failed to save NextcloudPi settings: " + e.message;
errorBox.classList.remove("hidden");
})
})
})
})

View File

@ -0,0 +1,5 @@
// SPDX-FileCopyrightText: Nextcloud contributors
// SPDX-License-Identifier: AGPL-3.0-or-later
const stylelintConfig = require('@nextcloud/stylelint-config')
module.exports = stylelintConfig

View File

@ -0,0 +1,42 @@
<?php
script('nextcloudpi', 'nextcloudpi-admin');
style('nextcloudpi', 'admin');
?>
<div id="nextcloudpi" class="section">
<h2>NextcloudPi</h2>
<h3>System Info</h3>
<ul>
<li>
<div>NCP Version:</div><div><?php echo $_["ncp_version"] ?></div>
</li>
<li>
<div>PHP Version:</div><div><?php echo $_["ncp"]["php_version"] ?></div>
</li>
<li>
<div>Debian Release:</div><div><?php echo $_["ncp"]["release"] ?></div>
</li>
</ul>
<h3>Settings</h3>
<ul>
<li>
<input name="canary" type="checkbox" <?php echo $_['community']['CANARY'] === 'yes' ? ' checked="checked"' : ''; ?>"/>
<label for="canary">Enable updates from canary (testing) channel</label>
</li>
<li>
<input name="adminNotifications" type="checkbox" <?php echo $_['community']['ADMIN_NOTIFICATIONS'] === 'yes' ? ' checked="checked"' : ''; ?>"/>
<label for="adminNotifications">Enable notifications about relevant changes in NCP</label>
</li>
<li>
<input name="usageSurveys" type="checkbox" <?php echo $_['community']['USAGE_SURVEYS'] === 'yes' ? ' checked="checked"' : ''; ?>"/>
<label for="usageSurveys">Enable notifications for surveys that help to improve NCP</label>
</li>
<li>
<div>Accounts to notify:</div>
<input type="text" name="notificationAccounts"
placeholder="comma separated list of accounts. Default is: all admins"
value="<?php echo $_['community']['NOTIFICATION_ACCOUNTS']; ?>"/>
</li>
</ul>
<p class="error-message hidden"></p>
</div>

12
ncp-app/webpack.config.js Normal file
View File

@ -0,0 +1,12 @@
// SPDX-FileCopyrightText: Tobias Knöppler <tobias@knoeppler.net>
// SPDX-License-Identifier: AGPL-3.0-or-later
const path = require('path')
const webpackConfig = require('@nextcloud/webpack-vue-config')
module.exports = {...webpackConfig,
...{
entry: {
admin: path.join(__dirname, 'src/main-admin')
}
}
}

View File

@ -136,7 +136,7 @@ HTML;
<div id="header-left">
<a href="https://nextcloudpi.com" id="nextcloudpi" target="_blank" tabindex="1">
<div class="logo-icon">
<h1 class="hidden-visually">NextCloudPi</h1>
<h1 class="hidden-visually">NextcloudPi</h1>
</div>
</a>
<a id=versionlink target="_blank" href="https://github.com/nextcloud/nextcloudpi/blob/master/changelog.md">

54
ncp.sh
View File

@ -176,7 +176,59 @@ grep -q '[\\&#;`|*?~<>^()[{}$&]' <<< "$*" && exit 1
tar $pigz -tf "$file" data &>/dev/null
EOF
chmod 700 /home/www/ncp-backup-launcher.sh
echo "www-data ALL = NOPASSWD: /home/www/ncp-launcher.sh , /home/www/ncp-backup-launcher.sh, /sbin/halt, /sbin/reboot" >> /etc/sudoers
cat > /home/www/ncp-app-bridge.sh <<'EOF'
#!/bin/bash
set -e
grep -q '[\\&#;`|*?~<>^()[{}$&]' <<< "$*" && exit 1
action="${1?}"
[[ "$action" == "config" ]] && {
config_type="${2?}"
arg="${3}"
[[ -z "$arg" ]] || {
key="${arg%=*}"
val="${arg#*=}"
}
if [[ "$config_type" == "ncp" ]]
then
config_path="/usr/local/etc/ncp.cfg"
elif [[ "$config_type" == "ncp-community" ]]
then
. /usr/local/etc/library.sh
[[ -z "${key}" ]] || {
set_app_param ncp-community.sh "${key}" "${val}"
}
get_app_params ncp-community.sh
exit $?
else
echo "ERROR: Invalid config name '${config_type}'" >&2
exit 1
fi
[[ -z "${key}" ]] || {
cfg="$(jq ".${key} = \"${val}\"" <"$config_path")"
echo "$cfg" > "$config_path"
}
cat "$config_path"
exit 0
}
[[ "$action" == "file" ]] && {
file="${2?}"
if [[ "$file" == "ncp-version" ]]
then
cat /usr/local/etc/ncp-version
else
echo "ERROR: Invalid file '${file}'" >&2
exit 1
fi
exit 0
}
EOF
chmod 700 /home/www/ncp-app-bridge.sh
echo "www-data ALL = NOPASSWD: /home/www/ncp-launcher.sh , /home/www/ncp-backup-launcher.sh, /home/www/ncp-app-bridge.sh, /sbin/halt, /sbin/reboot" >> /etc/sudoers
# NCP AUTO TRUSTED DOMAIN
mkdir -p /usr/lib/systemd/system

View File

@ -11,18 +11,20 @@ Use at your own risk!
More at https://ownyourbits.com
"""
import json
import sys
import time
import urllib
import os
import getopt
import configparser
import signal
import re
import time
from pathlib import Path
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.firefox.webdriver import WebDriver
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.firefox.options import Options
from selenium.common.exceptions import NoSuchElementException, WebDriverException, TimeoutException
@ -48,14 +50,14 @@ class TestFailed(Exception):
class Test:
title = "test"
title = "test"
def new(self, title):
self.title = title
print("[check] " + "{:16}".format(title), end=' ', flush = True)
def check(self, expression, msg=None):
if expression:
if expression and not isinstance(expression, Exception):
print(tc.green + "ok" + tc.normal)
self.log("ok")
else:
@ -66,7 +68,6 @@ class Test:
exc_args.append(expression)
if msg is not None:
exc_args.append(msg)
raise TestFailed(*exc_args)
def report(self, title, expression, msg=None):
@ -112,6 +113,13 @@ class VisibilityOfElementLocatedByAnyLocator:
class ConfigTestFailure(Exception):
pass
def is_admin_notifications_checkbox(item: WebElement):
try:
input_item = item.find_element(By.TAG_NAME, "input")
return input_item.get_attribute("name") == "adminNotifications"
except:
return False
def test_nextcloud(IP: str, nc_port: str, driver: WebDriver):
""" Login and assert admin page checks"""
@ -132,7 +140,10 @@ def test_nextcloud(IP: str, nc_port: str, driver: WebDriver):
try:
driver.find_element(By.ID, "submit").click()
except NoSuchElementException:
pass
try:
driver.find_element(By.CSS_SELECTOR, ".login-form button[type=submit]").click()
except NoSuchElementException:
pass
test.report("password", "Wrong password" not in driver.page_source, msg="Failed to login with provided password")
@ -154,10 +165,11 @@ def test_nextcloud(IP: str, nc_port: str, driver: WebDriver):
infos = driver.find_elements(By.CSS_SELECTOR, "#postsetupchecks > .info > li")
for info in infos:
if re.match(r'.*Your installation has no default phone region set.*', info.text):
if re.match(r'.*Your installation has no default phone region set.*', info.text) \
or re.match(r'The PHP module "imagick" is not enabled', info.text):
continue
else:
print('text', info.text)
php_modules = info.find_elements(By.CSS_SELECTOR, "li")
if len(php_modules) != 1:
raise ConfigTestFailure(f"Could not find the list of php modules within the info message "
@ -174,6 +186,88 @@ def test_nextcloud(IP: str, nc_port: str, driver: WebDriver):
except Exception as e:
test.check(e)
try:
overlay_close_btn = driver.find_element(By.CLASS_NAME, "modal-container__close")
if overlay_close_btn.is_displayed():
overlay_close_btn.click()
time.sleep(3)
except NoSuchElementException:
pass
test.new("admin section (1)")
try:
driver.get(f"https://{IP}:{nc_port}/index.php/settings/admin")
except Exception as e:
test.check(e, msg=f"{tc.red}error:{tc.normal} unable to reach {tc.yellow + IP + tc.normal}")
old_admin_notifications_value = None
list_items = driver.find_elements(By.CSS_SELECTOR, "#nextcloudpi li")
try:
wait.until(lambda drv: drv.find_element(By.ID, "nextcloudpi").is_displayed())
expected = {
"ncp_version": False,
"php_version": False,
"debian_release": False,
"canary": False,
"admin_notifications": False,
# "usage_surveys": False,
"notification_accounts": False
}
version_re = re.compile(r'^(v\d+\.\d+\.\d+)$')
with (Path(__file__).parent / '../etc/ncp.cfg').open('r') as cfg_file:
ncp_cfg = json.load(cfg_file)
for li in list_items:
try:
inp = li.find_element(By.TAG_NAME, "input")
inp_name = inp.get_attribute("name")
inp_value = inp.get_attribute("value") if inp.get_attribute("type") != "checkbox" else inp.is_selected()
if inp_name == "canary":
expected["canary"] = True
elif inp_name == "adminNotifications":
old_admin_notifications_value = inp_value
expected["admin_notifications"] = True
elif inp_name == "usageSurveys":
expected["usage_surveys"] = True
elif inp_name == "notificationAccounts":
expected["notification_accounts"] = True
except:
divs = li.find_elements(By.TAG_NAME, "div")
if 'ncp version' in divs[0].text.lower() and version_re.match(divs[1].text):
expected['ncp_version'] = True
elif 'php version' in divs[0].text.lower() and divs[1].text == ncp_cfg['php_version']:
expected['php_version'] = True
elif 'debian release' in divs[0].text.lower() and divs[1].text == ncp_cfg['release']:
expected['debian_release'] = True
failed = list(map(lambda item: item[0], filter(lambda item: not item[1], expected.items())))
test.check(len(failed) == 0, f"checks failed for admin section: [{', '.join(failed)}]")
except Exception as e:
test.check(e)
test.new("admin section (2)")
wait = WebDriverWait(driver, 10)
try:
li = next(filter(is_admin_notifications_checkbox, list_items))
li.find_element(By.TAG_NAME, "input").click()
time.sleep(5)
wait.until(lambda drv: drv.find_element(By.CSS_SELECTOR, "#nextcloudpi .error-message:not(.hidden)"))
error_box = driver.find_element(By.CSS_SELECTOR, "#nextcloudpi .error-message")
test.check(False, str(error_box.text))
except Exception as e:
if isinstance(e, TestFailed):
raise e
test.check(True)
test.new("admin section (3)")
try:
driver.refresh()
except Exception as e:
test.check(e, msg=f"{tc.red}error:{tc.normal} unable to reach {tc.yellow + IP + tc.normal}")
try:
list_items = driver.find_elements(By.CSS_SELECTOR, "#nextcloudpi li")
li = next(filter(is_admin_notifications_checkbox, list_items))
test.check(li.find_element(By.TAG_NAME, "input").is_selected() != old_admin_notifications_value,
"Toggling admin notifications didn't work")
except Exception as e:
test.check(e)
if __name__ == "__main__":
signal.signal(signal.SIGINT, signal_handler)
@ -231,13 +325,17 @@ if __name__ == "__main__":
print("---------------------------")
driver = webdriver.Firefox(options=options)
failed=False
try:
test_nextcloud(IP, nc_port, driver)
except Exception as e:
print(e)
print(traceback.format_exc())
failed=True
finally:
driver.close()
if failed:
sys.exit(1)
# License
#

View File

@ -151,7 +151,8 @@ chmod 770 /var/www/ncp-web
# install NC app
rm -rf /var/www/ncp-app
cp -r ncp-app /var/www/
mkdir -p /var/www/ncp-app
cp -r ncp-app/{appinfo,css,img,js,lib,templates} /var/www/ncp-app/
# install ncp-previewgenerator
rm -rf /var/www/ncp-previewgenerator

View File

@ -5,7 +5,61 @@
cat > /usr/local/etc/instance.cfg <<EOF
{
"cohorteId": ${cohorte_id},
"canary": false
}
EOF
}
cat > /home/www/ncp-app-bridge.sh <<'EOF'
#!/bin/bash
set -e
grep -q '[\\&#;`|*?~<>^()[{}$&]' <<< "$*" && exit 1
action="${1?}"
[[ "$action" == "config" ]] && {
config_type="${2?}"
arg="${3}"
[[ -z "$arg" ]] || {
key="${arg%=*}"
val="${arg#*=}"
}
if [[ "$config_type" == "ncp" ]]
then
config_path="/usr/local/etc/ncp.cfg"
elif [[ "$config_type" == "ncp-community" ]]
then
. /usr/local/etc/library.sh
[[ -z "${key}" ]] || {
set_app_param ncp-community.sh "${key}" "${val}"
}
get_app_params ncp-community.sh
exit $?
else
echo "ERROR: Invalid config name '${config_type}'" >&2
exit 1
fi
[[ -z "${key}" ]] || {
cfg="$(jq ".${key} = \"${val}\"" <"$config_path")"
echo "$cfg" > "$config_path"
}
cat "$config_path"
exit 0
}
[[ "$action" == "file" ]] && {
file="${2?}"
if [[ "$file" == "ncp-version" ]]
then
cat /usr/local/etc/ncp-version
else
echo "ERROR: Invalid file '${file}'" >&2
exit 1
fi
exit 0
}
EOF
chmod 700 /home/www/ncp-app-bridge.sh
sed -i 's|www-data ALL = NOPASSWD: .*|www-data ALL = NOPASSWD: /home/www/ncp-launcher.sh , /home/www/ncp-backup-launcher.sh, /home/www/ncp-app-bridge.sh, /sbin/halt, /sbin/reboot|' /etc/sudoers
ncc upgrade