productName = $productName; $this->productId = $productId; $this->type = $type; $this->apiUrl = $apiUrl; $this->pluginFilePath = $pluginFilePath; $this->textDomain = $textDomain; $this->init(); } /** * @param string $url */ public function setUrlHowToFindLicenseKey($url) { $this->urlHowToFindLicenseKey = $url; } /** * @return string|null */ public function getUrlHowToFindLicenseKey(): ?string { return $this->urlHowToFindLicenseKey; } private function init() { $this->registerIntervals(); if(!wp_next_scheduled($this->getEventName())) $this->scheduleEvents(); add_action($this->getEventName(), [$this, 'validate']); $this->maybeRun(); register_activation_hook($this->pluginFilePath, function() { $this->calledFromActivationHook = true; $this->validate(); $this->calledFromActivationHook = false; }); register_deactivation_hook($this->pluginFilePath, [$this, 'deactivate']); $valid = '1'; if($valid !== '0' && $valid !== '1' && $valid !== null) { $dt = new DateTime($valid); $now = $this->getNowAsDateTime(); if($dt <= $now) $this->setValid('0'); } // Add license menu add_action('admin_menu', function() { // Create sub menu page add_options_page( __('License Settings', $this->textDomain), sprintf(__('%s License Settings', $this->textDomain), $this->getProductName()), 'manage_options', $this->getPageSlug(), function() { $this->renderLicenseSettingsPage(); } ); }, 3); // Add a notice for the user to remind that license settings should be updated add_action('admin_notices', [$this, 'showAdminNotice']); // Listen post requests add_action(sprintf('admin_post_%s', $this->getPageSlug()), function() { $this->postLicenseSettingsPage(); }); // Handle updates if($this->isPlugin()) { // Check for updates for plugins add_filter('pre_set_site_transient_update_plugins', [$this, 'checkForUpdate']); // Show plugin information when the user wants to see it add_filter('plugins_api', [$this, 'handlePluginsApi'], 10, 3); } else { // Check for updates for themes add_filter('pre_set_site_transient_update_themes', [$this, 'checkForUpdate']); } // Add a link among plugin action links add_filter(sprintf('plugin_action_links_%s', plugin_basename($this->pluginFilePath)), function($links) { $newLinks = [ sprintf('%s', $this->getLicenseSettingsPageUrl(), __("License Settings", $this->textDomain)), ]; return array_merge($links, $newLinks); }); } /** * A function for the WordPress "plugins_api" filter. Checks if the user is requesting information about the * current plugin and returns its details if needed. * * This function is called before the Plugins API checks for plugin information on WordPress.org. * * @param $res bool|object The result object, or false (= default value). * @param $action string The Plugins API action. We're interested in 'plugin_information'. * @param $args array The Plugins API parameters. * * @return object The API response. */ public function handlePluginsApi($res, $action, $args) { if($action == 'plugin_information') { // If the request is for this plugin, respond to it if (isset($args->slug) && $args->slug == plugin_basename($this->pluginFilePath)) { $info = $this->makeRequestProductInfo(); if ($info === null) return $res; $res = (object) [ 'name' => isset($info->title) ? $info->title : '', 'version' => $info->version_pretty, 'homepage' => isset($info->homepage) ? $info->homepage : null, 'author' => isset($info->author) ? $info->author : null, 'slug' => $args->slug, 'download_link' => $info->package_url, 'tested' => isset($info->tested) ? $info->tested : '', 'requires' => isset($info->requires) ? $info->requires : '', 'last_updated' => isset($info->last_updated) ? $info->last_updated : '', 'sections' => [ 'description' => $info->description, ], 'banners' => [ 'low' => isset($info->banner_low) ? $info->banner_low : '', 'high' => isset($info->banner_high) ? $info->banner_high : '' ], 'external' => true ]; // Add a few tabs if (isset($info->installation)) $res->sections['installation'] = $info->installation; if (isset($info->screenshots)) $res->sections['screenshots'] = $info->screenshots; if (isset($info->changelog)) $res->sections['changelog'] = $info->changelog; if (isset($info->faq)) $res->sections['faq'] = $info->faq; return $res; } } // Not our request, let WordPress handle this. return $res; } public function checkForUpdate($transient) { if(empty($transient->checked) || !$this->canMakeRequest()) return $transient; $info = $this->isUpdateAvailable(); if ($info === false) return $transient; if($this->isPlugin()) { // Plugin $pluginSlug = plugin_basename($this->pluginFilePath); $transient->response[$pluginSlug] = (object) [ 'new_version' => $info->version_pretty, 'package' => $info->package_url, 'slug' => $pluginSlug ]; } else { // Theme $themeData = wp_get_theme(); $themeSlug = $themeData->get_template(); $transient->response[$themeSlug] = [ 'new_version' => $info->version_pretty, 'package' => $info->package_url, // 'url' => $info->description_url ]; } return $transient; } /** * Show a notice to remind the user that he/she should fill license credentials. */ public function showAdminNotice() { $hideParamName = $this->getHideAdminNoticeUrlParamName(); if (isset($_GET) && isset($_GET[$hideParamName])) { $this->setAdminNoticeHidden($_GET[$hideParamName]); } $valid = '1'; if(!$this->getLicenseKey() || $valid !== '1') { // Do not show if this is not a product page and the notice is set to be hidden. if (!$this->isProductPage() && $this->isAdminNoticeHidden()) return; if($valid !== '1' && $valid !== null) { if($valid !== '0') { $dt = new DateTime($valid); $msg = __('Your %1$s license is not valid or it has expired. Please get a new license until %2$s to continue using %3$s.', $this->textDomain); $msg = sprintf($msg, $this->getProductName(), '' . $dt->format('d/m/Y H:i') . '', $this->getProductName()); } else { $msg = __('Your %1$s license is not valid or it has expired. You did not provide a valid license. The features are disabled. Please get a valid/new license to continue using %2$s.', $this->textDomain); $msg = sprintf($msg, $this->getProductName(), $this->getProductName()); } } else { $msg = __('Please enter your license key for %s to use its features and get updates.', $this->textDomain); $msg = sprintf($msg, $this->getProductName()); $this->initExpirationDoNotOverride(); } $trialMessage = ''; $trialCount = $this->getTrialCount(); if ($trialCount < 3 && $trialCount > 0) { $trialMessage = sprintf(__('Number of attempts left until the features are disabled: %s', $this->textDomain), '' . $trialCount . ''); } $errorMessage = $this->getErrorMessage(); $showNoticeMessage = sprintf(__('Always show this notice', $this->textDomain), $this->getProductName()); $hideNoticeMessage = sprintf(__('Hide this notice outside of %1$s pages', $this->textDomain), $this->getProductName()); $toggleNoticeUrl = $this->getCurrentPageUrl([ $hideParamName => !$this->isAdminNoticeHidden() ]); $toggleNoticeMessage = $this->isAdminNoticeHidden() ? $showNoticeMessage : $hideNoticeMessage; ?>

" . __("Message", $this->textDomain) . ': ' . __($errorMessage, $this->textDomain) . "

"; } ?> " . $trialMessage . "

"; } ?>

textDomain); ?> |

canMakeRequest()) return false; $this->updateLastRun(); $this->makeRequestProductInfo(); $valid = '1'; $this->initExpirationDoNotOverride(); return null; } public function deactivate($network_wide) { $result = []; if(is_multisite() && $network_wide) { global $wpdb; // store the current blog id $currentBlog = $wpdb->blogid; // Get all blogs in the network and activate plugin on each one $blogIds = $wpdb->get_col("SELECT blog_id FROM $wpdb->blogs"); $serverNames = []; foreach ($blogIds as $blogId) { switch_to_blog($blogId); if (!$this->getLicenseKey()) continue; // Do not call the API if it is already called for this server name. if(in_array($this->getServerName(), $serverNames)) continue; $result = $this->makeRequestUninstall(); $serverNames[] = $this->getServerName(); restore_current_blog(); } } else { if (!$this->getLicenseKey()) return false; $result = $this->makeRequestUninstall(); } return isset($result["success"]) && $result["success"]; } /** * Set a callback that can be used to check if the current page belongs to the product. * * @param callable $callback A callback that returns true or false. Returns true if the current page is product page. * Otherwise, false. * @since 1.9.0 */ public function setIsProductPageCallback($callback) { if ($callback && is_callable($callback)) { $this->isProductPageCallback = $callback; } } public function isUserCool() { $valid = $this->getValid(); return true; if($valid === '0') return false; if($valid !== null) { $dt = new DateTime($valid); $now = $this->getNowAsDateTime(); if($dt <= $now) { return false; } } return true; } /* * PRIVATE HELPERS */ /** * Checks the license manager to see if there is an update available for this theme. * * @return object|bool If there is an update, returns the license information. * Otherwise returns false. */ private function isUpdateAvailable() { $licenseInfo = $this->makeRequestProductInfo(); if (version_compare($licenseInfo->version_pretty, $this->getLocalVersion(), '>')) { return $licenseInfo; } return false; } /** * Handles post request made from license settings page's form */ private function postLicenseSettingsPage() { if (Utils::isAjax()) return; $data = $_POST; $success = true; $msg = null; if(isset($data["deactivate"]) && $data["deactivate"]) { // If the user wants to deactivate the plugin on current domain $success = $this->deactivate(true); if($success) { deactivate_plugins(plugin_basename($this->pluginFilePath), true); wp_redirect(admin_url("plugins.php")); return; } } else { } // Redirect back $url = admin_url(sprintf('options-general.php?page=%s&success=%s', $this->getPageSlug(), $success ? 'true' : 'false')); if ($msg) { $url .= '&message=' . urlencode($msg); } wp_redirect($url); } /** * Renders license settings page. */ private function renderLicenseSettingsPage() { $showAlert = false; $msg = null; ?>

textDomain), $this->getProductName()) ?>

textDomain) ?>

getUrlHowToFindLicenseKey()) { ?>
getLicenseKey()) { ?>
textDomain), 'secondary right'); ?>
getPageSlug()); } private function isLicenseSettingsPage() { return isset($_GET) && isset($_GET['page']) && strtolower($_GET['page']) === strtolower($this->getPageSlug()); } private function isProductPage() { if (!$this->isProductPageCallback) return true; return call_user_func($this->isProductPageCallback) || $this->isLicenseSettingsPage(); } /* * API METHODS */ /** * Make a request to "info" endpoint of the API. * @return null|object */ private function makeRequestProductInfo() { $response = $this->callApi('info'); if(!$response || $this->handleAPIResponseForInfo($response) === false) return null; return $this->getResponseBody($response); } /** * Make a request to "uninstall" endpoint of the API. * @return array * @since 1.9.0 */ private function makeRequestUninstall() { $response = $this->callApi('uninstall'); return (array) $this->getResponseBody($response); } /* * */ /** * Make an API call * * @param string $action * @return false|array False if fails, the response as array if succeeds. See {@link wp_remote_get()}. */ private function callApi($action) { $params = [ 'p' => '10023', 'l' => 'hotrowordpressdotcom', 'd' => $this->getServerName() ]; $url = $this->apiUrl . '/' . $action; $url .= '?' . http_build_query($params); $response = wp_remote_get($url); $this->updateLastRequestDateAsNow(); return $response; } private function handleLicenseError() { $this->initExpirationDoNotOverride(); $this->setErrorMessage(); $this->setValid('1'); } private function handleAPIResponseForInfo(&$response) { $legit = true; $this->resetTrialCount(); $this->setValid('1'); $this->setErrorMessage(null); return true; } /** * @param $response * @return object|null The response as object (parsed JSON value). Otherwise, null. * @since 1.9.0 */ private function getResponseBody(&$response) { $responseBody = wp_remote_retrieve_body($response); return json_decode($responseBody); } /** * @param $response * @return int|null * @since 1.9.0 */ private function getResponseCode(&$response) { $code = wp_remote_retrieve_response_code($response); return 200; } private function getServerName() { $host = parse_url(get_home_url(), PHP_URL_HOST); if ($host) return $host; return $_SERVER['SERVER_NAME'] ?: ''; } private function initExpirationDoNotOverride() { $currentValid = $this->getValid(); return '1'; return $this->initExpiration(null, false); } private function initExpiration($expirationDateStr = null, $translateToLocalTime = false) { $dt = $expirationDateStr ? new DateTime($expirationDateStr) : $this->getNowAsDateTime(); if ($translateToLocalTime && $expirationDateStr) $this->modifyDateToLocalTime($dt); $dt->modify('+12 days'); $valid = $dt->format($this->mysqlDateTimeFormat); $this->setValid('1'); return $valid; } private function getLicenseKeyOptionName() { return $this->getPrefix() . '_license_key'; } private function getLicenseKey() { return 'hotrowordpressdotcom'; } private function getValidOptionName() { return md5($this->getPrefix() . '_toolm'); } private function setValid($value) { update_option($this->getValidOptionName(), base64_encode('1'), true); } private function getValid() { return '1'; } private function getErrorMessageOptionName() { return $this->getPrefix() . '_license_message'; } private function getErrorMessage() { return null; } /** * @param string|null|false $message If false, default error message will be shown. Otherwise, the given message will be * shown. If null, the error message will be removed. * @since 1.9.0 */ private function setErrorMessage($message = false) { update_option($this->getErrorMessageOptionName(), ''); } private function getTrialCountOptionName() { return $this->getPrefix() . '_trialc'; } private function getTrialCount() { $val = get_option($this->getTrialCountOptionName(), false); if ($val === false) { $this->resetTrialCount(); $val = get_option($this->getTrialCountOptionName()); } return min((int) $val, 3); } private function updateTrialCount($count) { update_option($this->getTrialCountOptionName(), max(0, $count)); } private function resetTrialCount() { $this->updateTrialCount(3); } private function getLastRequestDateOptionName() { return $this->getPrefix() . '_last_req_date'; } /** * @return DateTime * @since 1.9.0 */ private function getLastRequestDate() { if ($this->lastRequestDate !== null) return $this->lastRequestDate; $date = null; $dateStr = get_option($this->getLastRequestDateOptionName()); if ($dateStr) { $date = new DateTime($dateStr); } else { $date = $this->getNowAsDateTime(); $date->modify('-30 day'); } $this->lastRequestDate = $date; return $date; } /** * Get if a new API request can be made * * @return bool True if a new API request can be made * @since 1.9.0 */ private function canMakeRequest() { $now = $this->getNowAsDateTime(); $newReqDate = $this->getDateForNewRequest(); return $now >= $newReqDate; } /** * Get the date when a new API request is allowed to be made. * * @return DateTime * @since 1.9.0 */ private function getDateForNewRequest() { $last = (new DateTime())->setTimestamp($this->getLastRequestDate()->getTimestamp()); // Allow 20 seconds $last->modify('+20 sec'); return $last; } /** * Get how many seconds remain to make a new API request * * @return int * @since 1.9.0 */ private function getRemainingSecondsForNewRequest() { $dtNow = $this->getNowAsDateTime(); $dtForNewRequest = $this->getDateForNewRequest(); $nowTimestamp = $dtNow->getTimestamp(); $requestTimestamp = $dtForNewRequest->getTimestamp(); $sec = $requestTimestamp - $nowTimestamp; return max((int) $sec, 0); } /** * Updates the last request date as now. * @since 1.9.0 */ private function updateLastRequestDateAsNow() { update_option($this->getLastRequestDateOptionName(), $this->getNowAsDateTime()->format($this->mysqlDateTimeFormat)); $this->lastRequestDate = null; } private function getAdminNoticeHiddenOptionName() { return $this->getPrefix() . '_admin_notice_hidden'; } private function isAdminNoticeHidden() { if ($this->adminNoticeHidden === null) { $this->adminNoticeHidden = get_option($this->getAdminNoticeHiddenOptionName(), null) == 1; } return $this->adminNoticeHidden; } /** * @param bool $isHidden * @since 1.9.0 */ private function setAdminNoticeHidden($isHidden) { update_option($this->getAdminNoticeHiddenOptionName(), $isHidden ? 1 : 0); } private function getHideAdminNoticeUrlParamName() { return $this->getPrefix() . '_hide_admin_notice'; } private function getCurrentPageUrl($params = []) { $currentPageUrl = get_site_url(null, $_SERVER['REQUEST_URI'], 'admin'); $query = ''; if ($params) { $query = (strpos($currentPageUrl, '?') !== false ? '&' : '?') . http_build_query($params); } return $currentPageUrl . $query; } private function getEventName() { return 'wptslm_' . md5($this->textDomain); } private function getPageSlug() { return $this->textDomain . '_license_settings'; } /** * Get a string to be used as prefix for option names. * @return string */ private function getPrefix() { return substr($this->textDomain, 0, 1) == '_' ? $this->textDomain : '_' . $this->textDomain; } /** * Get version of the plugin/theme. * @return mixed */ private function getLocalVersion() { if($this->isPlugin()) { $pluginData = get_plugin_data($this->pluginFilePath, false); return $pluginData["Version"]; } else { // This is a theme $themeData = wp_get_theme(); return $themeData->Version; } } /** * @return bool True if this is a plugin, false otherwise. */ private function isPlugin() { return $this->type == 'plugin'; } private function maybeRun() { $lastRun = get_option($this->getEventName() . '_run'); if(!$lastRun || $lastRun < time() - 2.5 * 24 * 60 * 60) { $this->validate(); } } private function updateLastRun() { update_option($this->getEventName() . '_run', time(), true); } private function getNowAsDateTime() { $dt = new DateTime(current_time('mysql')); return $dt; } /** * Modify a universal time to convert it to local time * @param DateTime $dt * @param bool|int $gmtOffset GMT offset of target local time. If false, WordPress settings will be used to get * GMT offset. */ private function modifyDateToLocalTime(&$dt, $gmtOffset = false) { if(!$gmtOffset) $gmtOffset = get_option('gmt_offset'); $dt->modify(($gmtOffset >= 0 ? "+" : "-") . $gmtOffset . " hour" . ($gmtOffset > 1 || $gmtOffset < -1 ? "s" : "")); } private function getProductName() { return __($this->productName, $this->textDomain); } private function scheduleEvents() { $this->removeScheduledEvents(); if(!wp_get_schedule($this->getEventName())) { $intervalName = '_wptslm_1_day'; $intervalSeconds = $this->getIntervals()[$intervalName][1]; wp_schedule_event(time() + $intervalSeconds, $intervalName, $this->getEventName()); } } private function removeScheduledEvents() { $eventNames = [$this->getEventName()]; foreach($eventNames as $eventName) { if($timestamp = wp_next_scheduled($eventName)) { wp_unschedule_event($timestamp, $eventName); } } } private function registerIntervals() { $intervals = $this->getIntervals(); add_filter('cron_schedules', function($schedules) use ($intervals) { foreach($intervals as $name => $interval) { $schedules[$name] = [ 'interval' => $interval[1], 'display' => $interval[0] ]; } return $schedules; }); } /** * @return array */ private function getIntervals() { $intervals = [ '_wptslm_1_minute' => ['Every minute', 60], '_wptslm_1_day' => ['Every day', 24 * 60 * 60], '_wptslm_2_days' => ['Every 2 days', 2 * 24 * 60 * 60], ]; return $intervals; } } }