lib/private/License/LicenseManager.php
<?php
/**
*
* @copyright Copyright (c) 2020, ownCloud GmbH
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OC\License;
use OCP\License\ILicenseManager;
use OCP\License\Exceptions\LicenseException;
use OCP\License\Exceptions\LicenseManagerException;
use OCP\IConfig;
use OCP\App\IAppManager;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\ILogger;
class LicenseManager implements ILicenseManager {
public const GRACE_PERIOD = 60 * 60 * 24; // 24h
/** @var LicenseFetcher */
private $licenseFetcher;
/** @var MessageService */
private $messageService;
/** @var IConfig */
private $config;
/** @var IAppManager */
private $appManager;
/** @var ITimeFactory */
private $timeFactory;
/** @var ILogger */
private $logger;
public function __construct(
LicenseFetcher $licenseFetcher,
MessageService $messageService,
IAppManager $appManager,
IConfig $config,
ITimeFactory $timeFactory,
ILogger $logger
) {
$this->licenseFetcher = $licenseFetcher;
$this->messageService = $messageService;
$this->appManager = $appManager;
$this->config = $config;
$this->timeFactory = $timeFactory;
$this->logger = $logger;
}
/**
* Check if "now" is inside the closed interval [t, t+p], where t = $timestamp
* and p = the grace period (24h)
*
* @param int $timestamp the timestamp when the grace period started
* @return bool
*/
private function isNowUnderGracePeriod(int $timestamp): bool {
$currentTime = $this->timeFactory->getTime();
return $timestamp <= $currentTime && $currentTime <= ($timestamp + self::GRACE_PERIOD);
}
/**
* Get the apps that are complaining about not having a valid license
*/
private function getAppComplains(): array {
$apps = [];
$appComplains = $this->config->getAppKeys('core-license-complains');
foreach ($appComplains as $appComplain) {
if ($this->config->getAppValue($appComplain, 'enabled', 'no') === 'yes' && $this->appManager->getAppPath($appComplain) !== false) {
// if the app is installed and hasn't been removed from the FS...
$licenseWithState = $this->getLicenseWithState($appComplain);
$licenseMatchesStoredOne = false;
if ($licenseWithState[0]) { // license exists for the app
$complainedLicense = $this->config->getAppValue('core-license-complains', $appComplain, null);
$licenseMatchesStoredOne = $licenseWithState[0]->getLicenseString() === $complainedLicense;
}
if ($licenseMatchesStoredOne) {
// if current license key matches the one the app is complaining about, mark the app
$apps[] = $appComplain;
} else {
if ($licenseWithState[1] !== ILicenseManager::LICENSE_STATE_VALID) {
// if the new license isn't valid, mark the app
$apps[] = $appComplain;
} else {
// if the new license is valid, remove the previous complain
$this->config->deleteAppValue('core-license-complains', $appComplain);
}
}
} else {
// missing app -> delete complain
$this->config->deleteAppValue('core-license-complains', $appComplain);
}
}
return $apps;
}
/**
* {@inheritDoc}
*
* If the "includeExtras" is used, the following keys will be added:
* - "apps" => list of apps actively using the grace period
* - "demoKeyLink" => an url with a web page to get a demo key
*
* @param bool $includeExtras if the 'apps' key is going to be returned
*/
public function getGracePeriod(bool $includeExtras = false): ?array {
$gracePeriod = $this->config->getAppValue('core', 'grace_period', null);
if ($gracePeriod === null || !$this->isNowUnderGracePeriod($gracePeriod)) {
return null;
}
if ($includeExtras) {
$apps = $this->getAppComplains();
$demoKeyLink = $this->config->getSystemValue('grace_period.demo_key.link', 'https://owncloud.com/try-enterprise/');
return [
'apps' => $apps,
'demoKeyLink' => $demoKeyLink,
'start' => (int)$gracePeriod,
'end' => (int)$gracePeriod + self::GRACE_PERIOD,
];
}
return [
'start' => (int)$gracePeriod,
'end' => (int)$gracePeriod + self::GRACE_PERIOD,
];
}
/**
* @inheritDoc
*/
public function setLicenseString(?string $licenseString) {
// we can only set the ownCloud's license for now
if ($licenseString !== null) {
$this->licenseFetcher->setOwncloudLicense($licenseString);
}
$this->config->deleteAppValues('core-license-complains');
}
/**
* @inheritDoc
*/
public function removeLicenseString() {
// we can only remove the ownCloud's license for now
$this->licenseFetcher->removeOwncloudLicense();
$this->config->deleteAppValues('core-license-complains');
}
/**
* Per-app licenses aren't implemented at the moment. This method will return the state
* of the ownCloud's license for all apps (including public community apps)
* Returns an array with the license object and the state of the license (any of the
* ILicenseManager::LICENSE_STATE_* constants). Note that the license object could be null
* if the license is missing.
* @return array [ILicense|null, ILicenseManager::LICENSE_STATE_*]
*/
private function getLicenseWithState(string $appid) {
// check if license is missing
$licenseObj = $this->licenseFetcher->getOwncloudLicense();
if ($licenseObj === null) {
$licenseState = ILicenseManager::LICENSE_STATE_MISSING;
} else {
// check if the license is valid
if ($licenseObj->isValid()) {
// check if the license has expired
$currentTime = $this->timeFactory->getTime();
if ($licenseObj->getExpirationTime() < $currentTime) {
$licenseState = ILicenseManager::LICENSE_STATE_EXPIRED;
} else {
$daysLeft = ($licenseObj->getExpirationTime() - $currentTime) / 86400;
if ($daysLeft <= ILicenseManager::THRESHOLD_ABOUT_TO_EXPIRE) {
$licenseState = ILicenseManager::LICENSE_STATE_ABOUT_TO_EXPIRE;
} else {
$licenseState = ILicenseManager::LICENSE_STATE_VALID;
}
}
} else {
$licenseState = ILicenseManager::LICENSE_STATE_INVALID;
}
}
return [$licenseObj, $licenseState];
}
/**
* {@inheritdoc}
*
* Per-app licenses aren't implemented at the moment. This method will return the state
* of the ownCloud's license for all apps (including public community apps)
*/
public function getLicenseStateFor(string $appid): int {
$info = $this->getLicenseWithState($appid);
return $info[1]; // license state is the second item.
}
public function getLicenseMessageFor(string $appid, string $language = null): array {
$info = $this->getLicenseWithState($appid);
if ($info[0] === null) {
// no license
$infoForMessage = [
'licenseState' => $info[1],
];
} else {
$infoForMessage = [
'licenseState' => $info[1],
'licenseType' => $info[0]->getType(),
// daysLeft won't be accurate if the license isn't valid, but it's ok.
'daysLeft' => \ceil(($info[0]->getExpirationTime() - $this->timeFactory->getTime()) / 86400),
];
}
$messageData = $this->messageService->getMessageForLicense($infoForMessage, $language);
// need to include license_state and type fields
$messageData['license_state'] = $info[1];
if ($info[0] === null) {
$messageData['type'] = -1;
} else {
$messageData['type'] = $info[0]->getType();
}
return $messageData;
}
public function askLicenseFor(string $appid, string $method, array $params = []) {
$info = $this->getLicenseWithState($appid);
$licenseObj = $info[0];
if ($licenseObj === null) {
throw new LicenseManagerException("No license available for app $appid", 1);
}
if (!\is_callable([$licenseObj, $method])) {
throw new LicenseManagerException("Method $method can't be called", 2);
}
// function names are case-insensitive...
$lowerCaseMethods = \array_map(function ($value) {
return \strtolower($value);
}, $licenseObj->getProtectedMethods());
if (\in_array(\strtolower($method), $lowerCaseMethods, true)) {
throw new LicenseManagerException("License doesn't allow method $method to be called", 3);
}
try {
return $licenseObj->$method(...$params);
} catch (LicenseException $ex) {
throw new LicenseManagerException("Method $method failed", 99, $ex);
}
}
/**
* {@inheritDoc}
*
* This method will register a "complain" if we're under the grace period but the app
* doesn't have a valid license, This way it's easier to keep track of what apps
* have invalid license during the grace period
*
* Options:
* - 'startGracePeriod' => true/false (default true): whether a grace period should
* start (if possible) automatically. Note that if a grace period is ongoing or
* has expired, this option won't have any effect. Set the option to false if you
* don't want to start the grace period.
* - 'disableApp' => true/false (default true): whether the app will be disabled if the
* grace period has finished and there is no valid license. Setting this
* option to false is intended to be used by apps which are mostly open but
* have some parts that require a license: you don't want to disable the whole
* app if there is no valid license.
*/
public function checkLicenseFor(string $appid, array $options = []): bool {
$currentTime = $this->timeFactory->getTime();
$gracePeriod = $this->config->getAppValue('core', 'grace_period', null);
if ($gracePeriod === null) {
if (isset($options['startGracePeriod']) && $options['startGracePeriod'] === false) {
// don't set the grace period. Use a negative value for the "isNowUnderGracePeriod"
$gracePeriod = -self::GRACE_PERIOD;
} else {
$this->config->setAppValue('core', 'grace_period', $currentTime);
$gracePeriod = $currentTime; // set the expected trial mark as now
}
}
$licenseWithState = $this->getLicenseWithState($appid);
$licenseObj = $licenseWithState[0];
$licenseState = $licenseWithState[1];
if (!$this->isNowUnderGracePeriod($gracePeriod)) {
// we aren't under a grace period, so the app must be disabled if the license isn't valid
if ($licenseState !== ILicenseManager::LICENSE_STATE_VALID &&
$licenseState !== ILicenseManager::LICENSE_STATE_ABOUT_TO_EXPIRE
) {
if (!isset($options['disableApp']) || $options['disableApp'] !== false) {
$this->appManager->disableApp($appid);
$this->logger->warning("$appid has been disabled because the license is not valid", ['app' => 'core']);
}
return false;
}
return true;
}
if ($licenseState !== ILicenseManager::LICENSE_STATE_VALID &&
$licenseState !== ILicenseManager::LICENSE_STATE_ABOUT_TO_EXPIRE
) {
// we're under a grace period but the license for the app isn't valid
// mark the app to know there is at least one app using the grace period.
// we'll still return true in this case.
$licenseKey = '';
if ($licenseObj) {
$licenseKey = $licenseObj->getLicenseString();
}
$this->config->setAppValue('core-license-complains', $appid, $licenseKey);
$this->logger->debug("$appid has registered a license complain over license $licenseKey", ['app' => 'core']);
}
return true;
}
}