diff --git a/cache/timer.yaml b/cache/timer.yaml new file mode 100644 index 0000000..a4bd127 --- /dev/null +++ b/cache/timer.yaml @@ -0,0 +1 @@ +licenseupdate: 1709153456 diff --git a/plugins/demo/demo.php b/plugins/demo/demo.php index a60cae3..40c7eaf 100644 --- a/plugins/demo/demo.php +++ b/plugins/demo/demo.php @@ -12,10 +12,10 @@ use Psr\Http\Message\ResponseInterface as Response; class demo extends Plugin { # you can add a licence check here - public static function setPremiumLicence() + public static function setPremiumLicense() { - return false; - # return 'MAKER'; +# return false; + return 'MAKER'; # return 'BUSINESS'; } diff --git a/plugins/demo/demo.yaml b/plugins/demo/demo.yaml index 5a734f9..ba435fd 100644 --- a/plugins/demo/demo.yaml +++ b/plugins/demo/demo.yaml @@ -3,7 +3,7 @@ version: 1.0.0 description: Demonstrates the power of Typemill Plugins author: Sebastian Schürmanns homepage: http://typemill.net -license: MIT +license: MAKER dependencies: - register - mail diff --git a/system/typemill/Controllers/ControllerApiSystemExtensions.php b/system/typemill/Controllers/ControllerApiSystemExtensions.php index e8ef1cf..9c58b75 100644 --- a/system/typemill/Controllers/ControllerApiSystemExtensions.php +++ b/system/typemill/Controllers/ControllerApiSystemExtensions.php @@ -55,8 +55,10 @@ class ControllerApiSystemExtensions extends Controller if(isset($definitions['license']) && in_array($definitions['license'], ['MAKER', 'BUSINESS'])) { $license = new License(); + + # checks if license is valid and returns scope $licenseScope = $license->getLicenseScope($this->c->get('urlinfo')); - + if(!isset($licenseScope[$definitions['license']])) { $response->getBody()->write(json_encode([ diff --git a/system/typemill/Controllers/ControllerWebSystem.php b/system/typemill/Controllers/ControllerWebSystem.php index 03a630b..06a6e39 100644 --- a/system/typemill/Controllers/ControllerWebSystem.php +++ b/system/typemill/Controllers/ControllerWebSystem.php @@ -184,9 +184,12 @@ class ControllerWebSystem extends Controller $parser = $this->routeParser ); + $message = false; $license = new License(); $licensefields = $license->getLicenseFields(); - $licensedata = $license->getLicenseData($this->c->get('urlinfo')); + $licensedata = $license->getLicenseFile(); + + # disable input fields if license data exist (readonly) if($licensedata) { foreach($licensefields as $key => $licensefield) @@ -195,14 +198,24 @@ class ControllerWebSystem extends Controller } } + # check license data + $licensecheck = $license->checkLicense($licensedata, $this->c->get('urlinfo')); + if(!$licensecheck) + { + $message = $license->getMessage(); + } + + unset($licensedata['signature']); + return $this->c->get('view')->render($response, 'system/license.twig', [ 'settings' => $this->settings, 'darkmode' => $request->getAttribute('c_darkmode'), 'mainnavi' => $mainNavigation, 'jsdata' => [ - 'systemnavi' => $systemNavigation, + 'systemnavi' => $systemNavigation, 'licensedata' => $licensedata, 'licensefields' => $licensefields, + 'message' => $message, 'labels' => $this->c->get('translations'), 'urlinfo' => $this->c->get('urlinfo') ] ]); diff --git a/system/typemill/Models/License.php b/system/typemill/Models/License.php index 4539db9..369b275 100644 --- a/system/typemill/Models/License.php +++ b/system/typemill/Models/License.php @@ -7,7 +7,7 @@ use Typemill\Static\Translations; class License { - public $message = ''; + private $message = ''; private $plans = [ 'MAKER' => [ @@ -25,59 +25,22 @@ class License return $this->message; } - # used for license management in admin settings - public function getLicenseData(array $urlinfo) + public function getLicenseFile() { - # returns data for settings page - $licensedata = $this->checkLicense(); - if($licensedata) - { - $licensedata['plan'] = $this->plans[$licensedata['plan']]['name']; - $licensedata['domaincheck'] = $this->checkLicenseDomain($licensedata['domain'], $urlinfo); - $licensedata['datecheck'] = $this->checkLicenseDate($licensedata['payed_until']); + $storage = new StorageWrapper('\Typemill\Models\Storage'); - return $licensedata; + $licensefile = $storage->getYaml('basepath', 'settings', 'license.yaml'); + + if($licensefile) + { + return $licensefile; } + $this->message = 'Error loading license: ' . $storage->getError(); + return false; } - # used to activate or deactivate features that require a license - public function getLicenseScope(array $urlinfo) - { - $licensedata = $this->checkLicense(); - - if(!$licensedata) - { - return false; - } - - $domain = $this->checkLicenseDomain($licensedata['domain'], $urlinfo); - $date = $this->checkLicenseDate($licensedata['payed_until']); - - if($domain && $date) - { - return $this->plans[$licensedata['plan']]['scope']; - } - - return false; - } - - public function refreshLicense() - { - # same as update - } - - private function updateLicence() - { - # if license not valid anymore, check server for update - } - - private function controlLicence() - { - # regularly check license on server each month. - } - public function getLicenseFields() { $storage = new StorageWrapper('\Typemill\Models\Storage'); @@ -86,68 +49,152 @@ class License return $licensefields; } - # check the local licence file (like pem or pub) - private function checkLicense() + # used to activate or deactivate features that require a license + public function getLicenseScope(array $urlinfo) { - $storage = new StorageWrapper('\Typemill\Models\Storage'); - - $licensedata = $storage->getYaml('basepath', 'settings', 'license.yaml'); - + $licensedata = $this->getLicenseFile(); if(!$licensedata) { - $this->message = Translations::translate('no license found'); - return false; } - if(!isset($licensedata['license'],$licensedata['email'],$licensedata['domain'],$licensedata['plan'],$licensedata['payed_until'],$licensedata['signature'])) + # this means that a check (and update or call to cache timer) will take place when visit system license + $licensecheck = $this->checkLicense($licensedata,$urlinfo); + if(!$licensecheck) { - $this->message = Translations::translate('License data incomplete'); + return false; + } + + return $this->plans[$licensedata['plan']]['scope']; + } + + # check the local licence file (like pem or pub) + public function checkLicense($licensedata, array $urlinfo) + { + if(!isset( + $licensedata['license'], + $licensedata['email'], + $licensedata['domain'], + $licensedata['plan'], + $licensedata['payed_until'], + $licensedata['signature'] + )) + { + $this->message = Translations::translate('License data are incomplete'); return false; } + # check if license data are valid and not manipulated $licenseStatus = $this->validateLicense($licensedata); if($licenseStatus !== true) { - $this->message = Translations::translate('Validation failed') . ': ' . $licenseStatus; + $this->message = Translations::translate('The license data are invalid.'); return false; } - # check here if payed until is in past - $nextBillDate = new \DateTime($licensedata['payed_until']); - $currentDate = new \DateTime(); + # check if website uses licensed domain + $licenseDomain = $this->checkLicenseDomain($licensedata['domain'], $urlinfo); - if($nextBillDate < $currentDate) + if(!$licenseDomain) + { + $this->message = Translations::translate('The website is running not under the domain of your license.'); + + return false; + } + + # check if subscription period is paid + $subscriptionPaid = $this->checkLicenseDate($licensedata['payed_until']); + + if(!$subscriptionPaid) { - $this->message = Translations::translate('The subscription period is not paid yet.'); + $storage = new StorageWrapper('\Typemill\Models\Storage'); + if(!$storage->timeoutIsOver('licenseupdate', 3600)) + { + $this->message = Translations::translate('The subscription period has not been paid yet. We will check it every 60 minutes.') . $this->message; - return false; + return false; + } + + $update = $this->updateLicense($licensedata); + if(!$update) + { + $this->message = Translations::translate('We tried to check your subscription payment but it failed. Answer from server: ') . $this->message; + + return false; + } } - unset($licensedata['signature']); + return true; + } - return $licensedata; + private function checkLicenseDate(string $payed_until) + { + # check here if payed until is in past + $nextBillDate = new \DateTime($payed_until); + $currentDate = new \DateTime(); + + if($nextBillDate > $currentDate) + { + return true; + } + + return false; + } + + private function checkLicenseDomain(string $licensedomain, array $urlinfo) + { + $licensehost = parse_url($licensedomain, PHP_URL_HOST); + $licensehost = str_replace("www.", "", $licensehost); + + $thishost = parse_url($urlinfo['baseurl'], PHP_URL_HOST); + $thishost = str_replace("www.", "", $thishost); + + $whitelist = ['localhost', '127.0.0.1', 'typemilltest.', $licensehost]; + + foreach($whitelist as $domain) + { + if(substr($thishost, 0, strlen($domain)) == $domain) + { + return true; + } + } + + return false; } private function validateLicense($data) { - $public_key_pem = $this->getPublicKeyPem(); - - $binary_signature = base64_decode($data['signature']); - - $data['email'] = $this->hashMail($data['email']); - unset($data['signature']); + $licensedata = [ + 'email' => $this->hashMail($data['email']), + 'domain' => $data['domain'], + 'license' => $data['license'], + 'plan' => $data['plan'], + 'payed_until' => $data['payed_until'] + ]; + + ksort($licensedata); # test manipulate data - #$data['plan'] = 'wrong'; - - $data = json_encode($data); +# $licensedata['plan'] = 'wrong'; + + $licensedata = json_encode($licensedata); # Check signature - $verified = openssl_verify($data, $binary_signature, $public_key_pem, OPENSSL_ALGO_SHA256); + $public_key_pem = $this->getPublicKeyPem(); + + if(!$public_key_pem) + { + $this->message = Translations::translate('We could not find or read the public_key.pem in the settings-folder.'); + + return false; + } + + $binary_signature = base64_decode($data['signature']); + + $verified = openssl_verify($licensedata, $binary_signature, $public_key_pem, OPENSSL_ALGO_SHA256); if ($verified == 1) { @@ -155,13 +202,13 @@ class License } elseif ($verified == 0) { - $this->message = 'License data are invalid'; + $this->message = Translations::translate('License validation failed'); return false; } else { - $this->message = Translations::translate('There was an error checking the license signature'); + $this->message = Translations::translate('There was an error checking the license signature'); return false; } @@ -170,57 +217,57 @@ class License public function activateLicense($params) { # prepare data for call to licence server + + $readableMail = trim($params['email']); + $licensedata = [ 'license' => $params['license'], - 'email' => $this->hashMail($params['email']), + 'email' => $this->hashMail($readableMail), 'domain' => $params['domain'] ]; - $postdata = http_build_query($licensedata); + $postdata = http_build_query($licensedata); - $authstring = $this->getPublicKeyPem(); - $authstring = hash('sha256', substr($authstring, 0, 50)); + $authstring = $this->getPublicKeyPem(); + $authstring = hash('sha256', substr($authstring, 0, 50)); $options = array ( 'http' => array ( - 'method' => 'POST', + 'method' => 'POST', 'ignore_errors' => true, - 'header' => "Content-Type: application/x-www-form-urlencoded\r\n" . - "Accept: application/json\r\n" . - "Authorization: $authstring\r\n" . - "Connection: close\r\n", - 'content' => $postdata + 'header' => "Content-Type: application/x-www-form-urlencoded\r\n" . + "Accept: application/json\r\n" . + "Authorization: $authstring\r\n" . + "Connection: close\r\n", + 'content' => $postdata ) ); - $context = stream_context_create($options); + $context = stream_context_create($options); - $response = file_get_contents('https://service.typemill.net/api/v1/activate', false, $context); + $response = file_get_contents('https://service.typemill.net/api/v1/activate', false, $context); + + $signedLicense = json_decode($response,true); if(substr($http_response_header[0], -6) != "200 OK") { - $this->message = Translations::translate('the license server responded with: ') . $http_response_header[0]; + $message = $http_response_header[0]; + if(isset($signedLicense['code']) && ($signedLicense['code'] != '')) + { + $message = $signedLicense['code']; + } + $this->message = Translations::translate('the license server responded with: ') . $message; return false; } - $signedLicense = json_decode($response,true); - if(isset($signedLicense['code'])) { -# $this->message = 'Something went wrong. Please check your input data or contact the support.'; - $this->message = $signedLicense['code']; + $this->message = $signedLicense['code']; return false; } -/* - # check for positive and validate response data - if($signedLicense['license']) - { - $this->message = ; - } -*/ - $signedLicense['license']['email'] = trim($params['email']); + $signedLicense['license']['email'] = $readableMail; $storage = new StorageWrapper('\Typemill\Models\Storage'); $result = $storage->updateYaml('settingsFolder', '', 'license.yaml', $signedLicense['license']); @@ -234,34 +281,75 @@ class License return true; } - private function checkLicenseDomain(string $licensedomain, array $urlinfo) + # if license not valid anymore, check server for update + private function updateLicense($data) { - $licensehost = parse_url($licensedomain, PHP_URL_HOST); - $licensehost = str_replace("www.", "", $licensehost); + $readableMail = trim($data['email']); - $thishost = parse_url($urlinfo['baseurl'], PHP_URL_HOST); - $thishost = str_replace("www.", "", $thishost); + $licensedata = [ + 'license' => $data['license'], + 'email' => $this->hashMail($readableMail), + 'domain' => $data['domain'], + 'signature' => $data['signature'] + ]; - $whitelist = ['localhost', '127.0.0.1', 'typemilltest.', $licensehost]; + $postdata = http_build_query($licensedata); - foreach($whitelist as $domain) + $authstring = $this->getPublicKeyPem(); + $authstring = hash('sha256', substr($authstring, 0, 50)); + + $options = array ( + 'http' => array ( + 'method' => 'POST', + 'ignore_errors' => true, + 'header' => "Content-Type: application/x-www-form-urlencoded\r\n" . + "Accept: application/json\r\n" . + "Authorization: $authstring\r\n" . + "Connection: close\r\n", + 'content' => $postdata + ) + ); + + $context = stream_context_create($options); + + $response = file_get_contents('https://service.typemill.net/api/v1/update', false, $context); + + $signedLicense = json_decode($response,true); + + if(substr($http_response_header[0], -6) != "200 OK") { - if(substr($thishost, 0, strlen($domain)) == $domain) + $message = $http_response_header[0]; + + if(isset($signedLicense['code']) && ($signedLicense['code'] != '')) { - return true; + $message = $signedLicense['code']; } + + $this->message = Translations::translate('the license server responded with: ') . $message; + + return false; } - return false; - } - - private function checkLicenseDate(string $payed_until) - { - if(strtotime($payed_until) > strtotime(date('Y-m-d'))) + if(isset($signedLicense['code'])) { - return true; + $this->message = $signedLicense['code']; + + return false; } - return false; + + $signedLicense['license']['email'] = $readableMail; + $storage = new StorageWrapper('\Typemill\Models\Storage'); + + $result = $storage->updateYaml('settingsFolder', '', 'license.yaml', $signedLicense['license']); + + if(!$result) + { + $this->message = 'We could not store the updated license: ' . $storage->getError(); + + return false; + } + + return true; } private function hashMail(string $mail) @@ -286,5 +374,4 @@ class License return false; } - } \ No newline at end of file diff --git a/system/typemill/Models/Storage.php b/system/typemill/Models/Storage.php index 5002351..deef47d 100644 --- a/system/typemill/Models/Storage.php +++ b/system/typemill/Models/Storage.php @@ -486,6 +486,38 @@ class Storage return false; } + ###################### + ## Timeout ## + ###################### + + public function timeoutIsOver($name, $timespan) + { + $location = 'cacheFolder'; + $folder = ''; + $filename = 'timer.yaml'; + + // Get current timers from the YAML file, if it exists + $timers = $this->getYaml($location, $folder, $filename) ?: []; + + $currentTime = time(); + $timeThreshold = $currentTime - $timespan; + + # Check if the name exists and if the timestamp is older than the current time minus the timespan + if (!isset($timers[$name]) || !is_numeric($timers[$name]) || $timers[$name] <= $timeThreshold) + { + # If the name doesn't exist or the timestamp is older, update the timer + $timers[$name] = $currentTime; + + # Update the YAML file with the new or updated timer + $this->updateYaml($location, $folder, $filename, $timers); + + return true; + } + + # If the name exists and the timestamp is not older, return false + return false; + } + ################## ## IMAGES ## diff --git a/system/typemill/Static/Plugins.php b/system/typemill/Static/Plugins.php index 182b8f0..13b8602 100644 --- a/system/typemill/Static/Plugins.php +++ b/system/typemill/Static/Plugins.php @@ -86,14 +86,12 @@ class Plugins return $premiumList['className']; } - $licenseType = false; - if(method_exists($className, 'setPremiumLicense')) { - $licenseType = $className::setPremiumLicense(); + return $className::setPremiumLicense(); } - return $licenseType; + return false; } private static function checkRouteArray($routes,$route) diff --git a/system/typemill/author/js/vue-license.js b/system/typemill/author/js/vue-license.js index 1aedd07..172480d 100644 --- a/system/typemill/author/js/vue-license.js +++ b/system/typemill/author/js/vue-license.js @@ -1,9 +1,8 @@ const app = Vue.createApp({ template: ` -
+
-

Your license is out of date. Please check if the payments for your subscription were successfull.

-

Your license is only valid for the domain listed in your license data below.

+

{{ licensemessage }}

Congratulations! Your license is active and you can enjoy all features until you cancel your subscription. You can manage your subscription at paddle.net

@@ -38,8 +37,7 @@ const app = Vue.createApp({

{{ licenseData.payed_until }}

-

The subscription extends automatically for 12 month every time until you cancel your subscription.

-

For testing, you can also use the domains 'localhost', '127.0.0.1', and the subdomain 'typemilltest.'.

+

The subscription extends automatically for 12 month every time until you cancel your subscription. For testing, you can also use the domains 'localhost', '127.0.0.1', and the subdomain 'typemilltest.'.

@@ -81,6 +79,8 @@ const app = Vue.createApp({ return { licenseData: data.licensedata, formDefinitions: data.licensefields, + licensemessage: data.message, + licensefound: data.licensefound, formData: {}, message: '', messageClass: '', diff --git a/system/typemill/system.php b/system/typemill/system.php index 1e56a05..65209c3 100644 --- a/system/typemill/system.php +++ b/system/typemill/system.php @@ -135,10 +135,12 @@ $timer['container'] = microtime(true); $dispatcher = new EventDispatcher(); /**************************** -* Check Licence * +* Check Licence * ****************************/ $license = new License(); + +# checks if license is valid and returns scope $settings['license'] = $license->getLicenseScope($urlinfo); /****************************