diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..0bc2912 --- /dev/null +++ b/.gitea/workflows/release.yml @@ -0,0 +1,24 @@ +name: Build & Release Plugin + +on: + workflow_dispatch: + inputs: + release_notes: + description: "Optionele release-opmerkingen" + required: false + push: + branches: [ main ] + paths: + - 'siti-stock-plugin.php' + - 'includes/**' + - 'assets/**' + +jobs: + release: + uses: roberto/ci-workflows/.gitea/workflows/wp-plugin-release.yml@c6393ed47258d6f040ceeed3994b17b7faa3ef23 + with: + main_file: siti-stock-plugin.php + slug: siti-stock-plugin + release_body: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.release_notes || '' }} + secrets: + RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 98eba72..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,119 +0,0 @@ -name: Build & Release Plugin - -on: - workflow_dispatch: - inputs: - release_notes: - description: 'Optionele release-opmerkingen' - required: false - push: - branches: - - main - - master - paths: - - 'siti-stock-plugin.php' - - 'includes/**' - - 'assets/**' - - 'README.md' - - '.github/workflows/release.yml' - -jobs: - release: - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Fetch tags - run: git fetch --tags origin - - - name: Determine plugin version - id: meta - run: | - VERSION=$(grep -E "^[[:space:]]*\\*[[:space:]]*Version:" -m 1 siti-stock-plugin.php | sed -E 's/.*Version:[[:space:]]*//') - VERSION=$(echo "$VERSION" | tr -d '\\r' | xargs) - if [ -z "$VERSION" ]; then - echo "::error::Kon pluginversie niet bepalen." - exit 1 - fi - echo "version=$VERSION" >> "$GITHUB_OUTPUT" - - - name: Check if tag exists - id: tagcheck - run: | - TAG="v${{ steps.meta.outputs.version }}" - if git show-ref --tags --verify --quiet "refs/tags/$TAG"; then - echo "exists=true" >> "$GITHUB_OUTPUT" - else - echo "exists=false" >> "$GITHUB_OUTPUT" - fi - - - name: Maak of update git-tag - if: steps.tagcheck.outputs.exists == 'false' - run: | - TAG="v${{ steps.meta.outputs.version }}" - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git tag "$TAG" "$GITHUB_SHA" - git push origin "$TAG" - - - name: Tag bestaat al – workflow afronden - if: steps.tagcheck.outputs.exists == 'true' - run: | - echo "Tag v${{ steps.meta.outputs.version }} bestaat al. Release wordt overgeslagen." - - - name: Build distributie-zip - if: steps.tagcheck.outputs.exists == 'false' - id: package - run: | - VERSION="${{ steps.meta.outputs.version }}" - SLUG="siti-stock-plugin" - BUILD_ROOT="$RUNNER_TEMP/build" - DEST_DIR="$BUILD_ROOT/$SLUG" - mkdir -p "$DEST_DIR" - - rsync -a ./ "$DEST_DIR" \ - --exclude '.git/' \ - --exclude '.github/' \ - --exclude 'docker/' \ - --exclude 'docs/' \ - --exclude 'dist/' \ - --exclude 'docker-compose.yml' \ - --exclude 'PLAN.md' - - mkdir -p dist - ZIP_PATH="dist/${SLUG}-${VERSION}.zip" - (cd "$BUILD_ROOT" && zip -r "$GITHUB_WORKSPACE/$ZIP_PATH" "$SLUG") - - echo "asset_path=$ZIP_PATH" >> "$GITHUB_OUTPUT" - echo "asset_name=${SLUG}-${VERSION}.zip" >> "$GITHUB_OUTPUT" - - - name: Stel release-body samen - if: steps.tagcheck.outputs.exists == 'false' - id: releasebody - run: | - if [ -n "$RELEASE_NOTES" ]; then - echo "text=$RELEASE_NOTES" >> "$GITHUB_OUTPUT" - else - echo "text=Automatische release op basis van versie ${{ steps.meta.outputs.version }}." >> "$GITHUB_OUTPUT" - fi - env: - RELEASE_NOTES: ${{ github.event.inputs.release_notes }} - - - name: Maak GitHub release - if: steps.tagcheck.outputs.exists == 'false' - uses: ncipollo/release-action@v1 - with: - tag: v${{ steps.meta.outputs.version }} - commit: ${{ github.sha }} - name: Siti Stock Plugin v${{ steps.meta.outputs.version }} - body: ${{ steps.releasebody.outputs.text }} - artifacts: ${{ steps.package.outputs.asset_path }} - generateReleaseNotes: true - token: ${{ secrets.GITHUB_TOKEN }} - allowUpdates: true - updateOnlyUnreleased: true diff --git a/SitiWebUpdater.php b/SitiWebUpdater.php deleted file mode 100644 index e070288..0000000 --- a/SitiWebUpdater.php +++ /dev/null @@ -1,202 +0,0 @@ -file = $file; - - $this->set_plugin_properties(); - add_action( 'admin_init', array( $this, 'set_plugin_properties' ) ); - - return $this; - } - - public function set_plugin_properties() { - if ( ! function_exists( 'get_plugin_data' ) ) { - require_once ABSPATH . 'wp-admin/includes/plugin.php'; - } - - $this->plugin = get_plugin_data( $this->file ); - $this->basename = plugin_basename( $this->file ); - $this->active = is_plugin_active( $this->basename ); - } - - public function set_username( $username ) { - $this->username = $username; - } - - public function set_repository( $repository ) { - $this->repository = $repository; - } - - public function authorize( $token ) { - $this->authorize_token = $token; - } - - private function get_repository_info() { - if ( is_null( $this->github_response ) ) { // Do we have a response? - $args = array(); - $request_uri = sprintf( 'https://api.github.com/repos/%s/%s/releases', $this->username, $this->repository ); // Build URI - - $args = array(); - - if( $this->authorize_token ) { // Is there an access token? - $args['headers']['Authorization'] = "bearer {$this->authorize_token}"; // Set the headers - } - - $response = json_decode( wp_remote_retrieve_body( wp_remote_get( $request_uri, $args ) ), true ); // Get JSON and parse it - - if( is_array( $response ) ) { // If it is an array - $response = current( $response ); // Get the first item - } - - $this->github_response = $response; // Set it to our property - } - } - - private function get_latest_version_from_response() { - if ( empty( $this->github_response['tag_name'] ) ) { - return null; - } - - return ltrim( $this->github_response['tag_name'], 'vV' ); - } - - public function initialize() { - add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'modify_transient' ), 10, 1 ); - add_filter( 'plugins_api', array( $this, 'plugin_popup' ), 10, 3); - add_filter( 'upgrader_post_install', array( $this, 'after_install' ), 10, 3 ); - - // Add Authorization Token to download_package - add_filter( 'upgrader_pre_download', - function() { - add_filter( 'http_request_args', [ $this, 'download_package' ], 15, 2 ); - return false; // upgrader_pre_download filter default return value. - } - ); - } - - public function modify_transient( $transient ) { - - if( property_exists( $transient, 'checked') ) { // Check if transient has a checked property - - if( $checked = $transient->checked ) { // Did Wordpress check for updates? - - $this->get_repository_info(); // Get the repo info - - $latest_version = $this->get_latest_version_from_response(); - - if ( null === $latest_version ) { - return $transient; - } - - $out_of_date = version_compare( $latest_version, $checked[ $this->basename ], 'gt' ); // Check if we're out of date - - if( $out_of_date ) { - - $new_files = $this->github_response['zipball_url']; // Get the ZIP - - $slug = current( explode('/', $this->basename ) ); // Create valid slug - - $plugin = array( // setup our plugin info - 'url' => $this->plugin["PluginURI"], - 'slug' => $slug, - 'package' => $new_files, - 'new_version' => $latest_version - ); - - $transient->response[$this->basename] = (object) $plugin; // Return it in response - } - } - } - - return $transient; // Return filtered transient - } - - public function plugin_popup( $result, $action, $args ) { - - if( ! empty( $args->slug ) ) { // If there is a slug - - if( $args->slug == current( explode( '/' , $this->basename ) ) ) { // And it's our slug - - $this->get_repository_info(); // Get our repo info - $latest_version = $this->get_latest_version_from_response(); - - if ( null === $latest_version ) { - return $result; - } - - // Set it to an array - $plugin = array( - 'name' => $this->plugin["Name"], - 'slug' => $this->basename, - 'requires' => '5.1', - 'tested' => '6.4.3', - 'rating' => '100.0', - 'num_ratings' => '10823', - 'downloaded' => '14249', - 'added' => '2016-01-05', - 'version' => $latest_version, - 'author' => $this->plugin["AuthorName"], - 'author_profile' => $this->plugin["AuthorURI"], - 'last_updated' => $this->github_response['published_at'], - 'homepage' => $this->plugin["PluginURI"], - 'short_description' => $this->plugin["Description"], - 'sections' => array( - 'Description' => $this->plugin["Description"], - 'Updates' => $this->github_response['body'], - ), - 'download_link' => $this->github_response['zipball_url'] - ); - - return (object) $plugin; // Return the data - } - - } - return $result; // Otherwise return default - } - - public function download_package( $args, $url ) { - - if ( null !== $args['filename'] ) { - if( $this->authorize_token ) { - $args = array_merge( $args, array( "headers" => array( "Authorization" => "token {$this->authorize_token}" ) ) ); - } - } - - remove_filter( 'http_request_args', [ $this, 'download_package' ] ); - - return $args; - } - - public function after_install( $response, $hook_extra, $result ) { - global $wp_filesystem; // Get global FS object - - $install_directory = plugin_dir_path( $this->file ); // Our plugin directory - $wp_filesystem->move( $result['destination'], $install_directory ); // Move files to the plugin dir - $result['destination'] = $install_directory; // Set the destination for the rest of the stack - - if ( $this->active ) { // If it was active - activate_plugin( $this->basename ); // Reactivate - } - - return $result; - } -} diff --git a/includes/SitiLicenseValidator.php b/includes/SitiLicenseValidator.php new file mode 100644 index 0000000..baadd8f --- /dev/null +++ b/includes/SitiLicenseValidator.php @@ -0,0 +1,181 @@ +license_key = $license_key ?: get_option( 'siti_license_key' ); + $this->hostname = $hostname ?: parse_url( home_url(), PHP_URL_HOST ); + $this->plugin_version = $plugin_version; + } + + /** + * Set the license key + */ + public function set_license_key( $key ) { + $this->license_key = $key; + } + + /** + * Set the API base URL + */ + public function set_api_base_url( $url ) { + $this->api_base_url = rtrim( $url, '/' ); + } + + /** + * Set the hostname + */ + public function set_hostname( $hostname ) { + $this->hostname = $hostname; + } + + /** + * Set the plugin version for reporting purposes + */ + public function set_plugin_version( $version ) { + $this->plugin_version = $version; + } + + /** + * Verify the license + * + * @return array|WP_Error License data on success, WP_Error on failure + */ + public function verify() { + if ( empty( $this->license_key ) ) { + return new WP_Error( 'missing_license', 'Geen licentiecode ingesteld.' ); + } + + if ( empty( $this->hostname ) ) { + return new WP_Error( 'missing_hostname', 'Kon hostname niet bepalen.' ); + } + + $response = wp_remote_post( $this->api_base_url . '/api/licenses/verify', array( + 'headers' => array( + 'Content-Type' => 'application/json', + ), + 'body' => wp_json_encode( array( + 'key' => $this->license_key, + 'hostname' => $this->hostname, + 'currentVersion' => $this->plugin_version, + ) ), + 'timeout' => 15, + ) ); + + if ( is_wp_error( $response ) ) { + return $response; + } + + $code = wp_remote_retrieve_response_code( $response ); + $body = json_decode( wp_remote_retrieve_body( $response ), true ); + + if ( ! is_array( $body ) ) { + return new WP_Error( 'verification_failed', 'Onverwacht antwoord van licentieserver.' ); + } + + $has_payload = isset( $body['license'] ) || isset( $body['pluginVersion'] ) || isset( $body['version'] ); + + if ( 200 !== $code ) { + if ( $has_payload ) { + $this->store_license_payload( $body ); + } + + $error_message = isset( $body['error'] ) ? $body['error'] : 'Licentie verificatie mislukt.'; + return new WP_Error( 'verification_failed', $error_message ); + } + + if ( empty( $body['valid'] ) ) { + if ( $has_payload ) { + $this->store_license_payload( $body ); + } + + return new WP_Error( 'invalid_license', 'Licentie is ongeldig.' ); + } + + $this->store_license_payload( $body ); + + return $body; + } + + /** + * Get stored license data + */ + public function get_license_data() { + return get_option( 'siti_license_data', array() ); + } + + /** + * Check if license is valid (cached) + */ + public function is_valid() { + $data = $this->get_license_data(); + return ! empty( $data ) && isset( $data['key'] ); + } + + /** + * Get the latest version from license data + */ + public function get_latest_version() { + $data = $this->get_license_data(); + return $data['pluginVersion'] ?? null; + } + + /** + * Download a specific version + * + * @param string $version Version to download (e.g., 'v1.2.3' or 'latest') + * @return string|WP_Error Download URL or error + */ + public function get_download_url( $version = 'latest' ) { + if ( empty( $this->license_key ) || empty( $this->hostname ) ) { + return new WP_Error( 'missing_credentials', 'Licentie of hostname ontbreekt.' ); + } + + // For now, return the API download endpoint + // In practice, you might want to proxy this through WordPress + return $this->api_base_url . '/api/licenses/download?key=' . urlencode( $this->license_key ) . '&hostname=' . urlencode( $this->hostname ) . '&version=' . urlencode( $version ); + } + + /** + * Persist payload data locally for UI purposes + */ + private function store_license_payload( $body ) { + $license_data = array(); + + if ( isset( $body['license'] ) && is_array( $body['license'] ) ) { + $license_data = $body['license']; + } + + if ( empty( $license_data['key'] ) ) { + $license_data['key'] = $this->license_key; + } + + $version = null; + if ( ! empty( $body['pluginVersion'] ) ) { + $version = $body['pluginVersion']; + } elseif ( ! empty( $body['version'] ) ) { + $version = $body['version']; + } + + if ( $version ) { + $license_data['pluginVersion'] = $version; + } + + if ( empty( $license_data ) ) { + return; + } + + update_option( 'siti_license_data', $license_data ); + update_option( 'siti_license_last_check', current_time( 'mysql' ) ); + } +} diff --git a/includes/SitiWebUpdater2.php b/includes/SitiWebUpdater2.php new file mode 100644 index 0000000..7756606 --- /dev/null +++ b/includes/SitiWebUpdater2.php @@ -0,0 +1,510 @@ +file = $file; + $this->option_prefix = 'siti_updater_' . sanitize_key(dirname(plugin_basename($this->file))) . '_'; + $this->set_plugin_properties(); + + // Register this instance globally + self::$instances[$this->option_prefix] = $this; + + add_action('admin_init', array($this, 'set_plugin_properties')); + add_action('admin_notices', array($this, 'admin_notices')); + add_action('siti_updater_daily_check_' . $this->option_prefix, array($this, 'daily_license_check')); + + if (!wp_next_scheduled('siti_updater_daily_check_' . $this->option_prefix)) { + wp_schedule_event(time(), 'daily', 'siti_updater_daily_check_' . $this->option_prefix); + } + + return $this; + } + + public function initialize() + { + add_filter('pre_set_site_transient_update_plugins', array($this, 'check_for_updates')); + add_filter('plugins_api', array($this, 'plugin_info'), 10, 3); + + // Register global settings page only once + if (!self::$global_settings_registered) { + add_action('admin_menu', array($this, 'add_global_settings_page')); + self::$global_settings_registered = true; + } + } + + public function set_plugin_properties() + { + if (!function_exists('get_plugin_data')) { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + } + + $this->plugin = get_plugin_data($this->file, false, false); + $this->basename = plugin_basename($this->file); + $this->active = is_plugin_active($this->basename); + } + + public static function get_instances() + { + return self::$instances; + } + + public function get_plugin_data() + { + return $this->plugin; + } + + public function set_owner($owner) + { + $this->owner = $owner; + } + + public function set_repository($repository) + { + $this->repository = $repository; + } + + public function set_license_key($key) + { + $sanitized_key = sanitize_text_field((string) $key); + $stored_key = get_option($this->option_prefix . 'license_key', ''); + + if ($stored_key !== $sanitized_key) { + $this->clear_cached_license_data(); + } + + $this->license_key = $sanitized_key; + update_option($this->option_prefix . 'license_key', $sanitized_key); + } + + public function get_license_key() + { + if (null === $this->license_key) { + $this->license_key = get_option($this->option_prefix . 'license_key', ''); + } + return $this->license_key; + } + + public function get_license_data() + { + return $this->get_stored_license_data(); + } + + public function set_api_base_url($api_base_url) + { + $this->api_base_url = rtrim((string) $api_base_url, '/'); + } + + private function normalize_version($version) + { + if (!is_string($version)) { + return $version; + } + + $version = trim($version); + if ('' === $version) { + return $version; + } + + if (0 === stripos($version, 'v')) { + $version = ltrim($version, 'vV'); + } + + return $version; + } + + private function get_stored_license_data() + { + $data = get_option($this->option_prefix . 'license_data', array()); + + return is_array($data) ? $data : array(); + } + + private function clear_cached_license_data() + { + delete_option($this->option_prefix . 'license_data'); + delete_option($this->option_prefix . 'last_check'); + } + + private function store_license_data($body) + { + if (!is_array($body)) { + $body = array(); + } + + $license_data = array(); + + if (isset($body['license']) && is_array($body['license'])) { + $license_data = $body['license']; + } + + if (empty($license_data['key'])) { + $license_data['key'] = $this->get_license_key(); + } + + $license_data['licenseValid'] = !empty($body['valid']); + + $version = null; + if (isset($body['pluginVersion']) && $body['pluginVersion']) { + $version = $body['pluginVersion']; + } elseif (isset($body['version']) && $body['version']) { + $version = $body['version']; + } elseif (isset($license_data['pluginVersion']) && $license_data['pluginVersion']) { + $version = $license_data['pluginVersion']; + } + + if ($version) { + $license_data['pluginVersion'] = $this->normalize_version($version); + } + + update_option($this->option_prefix . 'license_data', $license_data); + update_option($this->option_prefix . 'last_check', current_time('mysql')); + } + + private function get_plugin_info() + { + if (is_null($this->plugin_response)) { + $request_uri = sprintf('%s/api/plugins/%s/%s', $this->api_base_url, $this->owner, $this->repository); + + $response = wp_remote_get($request_uri, array('timeout' => 15)); + + if (is_wp_error($response)) { + $this->plugin_response = false; + return; + } + + $code = wp_remote_retrieve_response_code($response); + if (200 !== $code) { + $this->plugin_response = false; + return; + } + + $body = json_decode(wp_remote_retrieve_body($response), true); + if (is_array($body) && isset($body['version'])) { + $body['version'] = $this->normalize_version($body['version']); + } + $this->plugin_response = $body; + } + } + + public function check_for_updates($transient) + { + if (empty($transient->checked)) { + return $transient; + } + + $this->get_plugin_info(); + + if (false === $this->plugin_response || empty($this->plugin_response['version'])) { + return $transient; + } + + $remote_version = $this->normalize_version($this->plugin_response['version']); + $current_version = $this->normalize_version($this->plugin['Version']); + + if ($remote_version && version_compare($remote_version, $current_version, '>')) { + $download_url = $this->get_download_url('latest'); + + if (!is_wp_error($download_url)) { + $transient->response[$this->basename] = (object) array( + 'slug' => dirname($this->basename), + 'new_version' => $remote_version, + 'package' => $download_url, + 'tested' => get_bloginfo('version'), + 'url' => $this->plugin_response['homepage'] ?? '', + ); + } + } + + return $transient; + } + + public function plugin_info($result, $action, $args) + { + if ('plugin_information' !== $action || $args->slug !== dirname($this->basename)) { + return $result; + } + + $this->get_plugin_info(); + + if (false === $this->plugin_response) { + return $result; + } + + return (object) array( + 'name' => $this->plugin_response['name'] ?? $this->plugin['Name'], + 'slug' => $args->slug, + 'version' => $this->normalize_version($this->plugin_response['version'] ?? $this->plugin['Version']), + 'author' => $this->plugin_response['author'] ?? $this->plugin['Author'], + 'author_profile' => $this->plugin_response['author_url'] ?? $this->plugin['AuthorURI'], + 'homepage' => $this->plugin_response['homepage'] ?? '', + 'requires' => $this->plugin_response['requires'] ?? '', + 'tested' => $this->plugin_response['tested'] ?? get_bloginfo('version'), + 'downloaded' => $this->plugin_response['downloads'] ?? 0, + 'last_updated' => $this->plugin_response['last_updated'] ?? '', + 'sections' => array( + 'description' => $this->plugin_response['description'] ?? $this->plugin['Description'], + 'changelog' => $this->plugin_response['changelog'] ?? '', + ), + 'download_link' => $this->get_download_url('latest'), + ); + } + + private function get_download_url($version = 'latest') + { + $license_key = $this->get_license_key(); + if (empty($license_key)) { + return new WP_Error('no_license', 'Geen licentie ingesteld'); + } + + $hostname = parse_url(home_url(), PHP_URL_HOST); + return $this->api_base_url . '/api/licenses/download?key=' . urlencode($license_key) . '&hostname=' . urlencode($hostname) . '&version=' . urlencode($version); + } + + public function verify_license() + { + $license_key = $this->get_license_key(); + if (empty($license_key)) { + return new WP_Error('missing_license', 'Geen licentiecode ingesteld.'); + } + + $hostname = parse_url(home_url(), PHP_URL_HOST); + $current_version = isset($this->plugin['Version']) ? $this->plugin['Version'] : null; + $response = wp_remote_post($this->api_base_url . '/api/licenses/verify', array( + 'headers' => array( + 'Content-Type' => 'application/json', + ), + 'body' => wp_json_encode(array( + 'key' => $license_key, + 'hostname' => $hostname, + 'currentVersion' => $current_version, + 'owner' => $this->owner, + 'repository' => $this->repository, + )), + 'timeout' => 15, + )); + + if (is_wp_error($response)) { + return $response; + } + + $code = wp_remote_retrieve_response_code($response); + $body = json_decode(wp_remote_retrieve_body($response), true); + + if (!is_array($body)) { + $this->clear_cached_license_data(); + return new WP_Error('verification_failed', 'Onverwacht antwoord van licentieserver.'); + } + + $has_license_payload = isset($body['license']) || isset($body['pluginVersion']) || isset($body['version']); + + if (200 !== $code) { + if ($has_license_payload) { + $this->store_license_data($body); + } else { + $this->clear_cached_license_data(); + } + + $error_message = isset($body['error']) ? $body['error'] : 'Licentie verificatie mislukt.'; + return new WP_Error('verification_failed', $error_message); + } + + if (empty($body['valid'])) { + if ($has_license_payload) { + $this->store_license_data($body); + } else { + $this->clear_cached_license_data(); + } + + return new WP_Error('invalid_license', 'Licentie is ongeldig.'); + } + + $this->store_license_data($body); + + return $body; + } + + public function is_license_valid() + { + $data = $this->get_license_data(); + if (empty($data) || empty($data['key'])) { + return false; + } + + if (isset($data['licenseValid'])) { + return (bool) $data['licenseValid']; + } + + return true; + } + + public function get_latest_known_version() + { + $this->get_plugin_info(); + + if (!empty($this->plugin_response['version'])) { + return $this->normalize_version($this->plugin_response['version']); + } + + $license_data = $this->get_license_data(); + if (isset($license_data['pluginVersion']) && $license_data['pluginVersion']) { + return $this->normalize_version($license_data['pluginVersion']); + } + + return null; + } + + public function admin_notices() + { + if (!current_user_can('manage_options')) { + return; + } + + if (!$this->is_license_valid()) { + $settings_url = admin_url('options-general.php?page=siti-plugin-licenses'); + echo '
'; + printf( + esc_html__('%s: Licentie ongeldig of niet ingesteld. Ga naar %s om je licentie in te voeren.', 'siti-updater'), + esc_html($this->plugin['Name']), + '' . esc_html__('plugin licenties', 'siti-updater') . '' + ); + echo '
+ +
+ + + + +