first commit

This commit is contained in:
2026-04-28 15:13:50 +02:00
commit a95acc355b
63745 changed files with 9487948 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
/******/ (() => { // webpackBootstrap
/******/ "use strict";
/******/
/******/
/******/ })()
;

View File

@@ -0,0 +1 @@
.custom-deactivation-modal{max-width:550px}.custom-deactivation-modal .components-modal__header{padding:5px 30px 0 30px;height:60px;border-bottom:1px solid #e5e7eb}.custom-deactivation-modal .components-modal__header-heading{display:flex;align-items:center;gap:3px;font-size:18px;font-weight:600;color:#1e73be}.custom-deactivation-modal .components-modal__content{margin-top:60px;padding:0 30px 30px;border-top:1px solid #eee}.custom-deactivation-modal .components-modal__content h3{color:#3c434a;margin:20px 0}.custom-deactivation-modal .components-modal__content textarea:focus{border-color:#2271b1 !important;box-shadow:0 0 0 1px #2271b1 !important;outline:2px solid rgba(0,0,0,0) !important}.custom-deactivation-modal .components-modal__footer{display:flex;align-items:center;margin:20px -30px -10px -30px;padding:20px 30px 0 30px;border-top:1px solid #eee;gap:10px}.custom-deactivation-modal .components-modal__footer span:last-child{margin-left:auto}@keyframes fadeIn{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}.custom-deactivation-modal button.is-primary{transition:transform .2s,box-shadow .2s}.custom-deactivation-modal button.is-primary:hover:not(:disabled){transform:translateY(-2px);box-shadow:0 4px 8px rgba(0,0,0,.1)}@keyframes bounce{from{transform:translateY(0)}to{transform:translateY(-10px)}}@keyframes pulse{0%{transform:scale(1)}50%{transform:scale(1.1)}100%{transform:scale(1)}}

View File

@@ -0,0 +1 @@
{"version":3,"file":"style.css","mappings":";;;AAAA;EACC;AACD;AACC;EACC;EACA;AACF;AAEC;EACC;EACA;EACA;AAAF;AAGC;EACC;EACA;EACA;AADF;AAGE;EACC;EACA;AADH;AAIE;EACC;AAFH;AAII;EACC;AAFL;AAQC;EACC;EACA;EACA;EACA;EACA;EACA;AANF;AAQE;EACC;AANH,C","sources":["webpack://wp-plugin-feedback/./packages/style.scss"],"sourcesContent":[".custom-deactivation-modal {\n\tmax-width: 500px;\n\n\t.components-modal__header {\n\t\tpadding: 5px 30px 0 30px;\n\t\theight: 60px;\n\t}\n\n\t.components-modal__header-heading {\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tgap: 3px;\n\t}\n\n\t.components-modal__content {\n\t\tmargin-top: 60px;\n\t\tpadding: 0 30px 30px;\n\t\tborder-top: 1px solid #eee;\n\n\t\th3 {\n\t\t\tcolor: #3c434a;\n\t\t\tmargin: 20px 0;\n\t\t}\n\n\t\t.components-radio-control {\n\t\t\tmargin: 20px 0;\n\t\t\t> .components-base-control__field {\n\t\t\t\t> div {\n\t\t\t\t\tgap: 10px;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t.components-modal__footer {\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tmargin: 20px -30px -10px -30px;\n\t\tpadding: 20px 30px 0 30px;\n\t\tborder-top: 1px solid #eee;\n\t\tgap: 10px;\n\n\t\tspan:last-child {\n\t\t\tmargin-left: auto;\n\t\t}\n\t}\n}\n"],"names":[],"sourceRoot":""}

View File

@@ -0,0 +1 @@
<?php return array('dependencies' => array('wp-components', 'wp-element', 'wp-i18n'), 'version' => '362b92c38c58a13958e0');

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,58 @@
<?php
namespace QuadLayers\PluginFeedback;
class AjaxHandler
{
public static $instance;
private function __construct()
{
add_action('wp_ajax_quadlayers_send_feedback', [self::class, 'send_feedback']);
}
public static function instance()
{
if (is_null(self::$instance)) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Sends feedback from the deactivation survey via AJAX.
*/
public static function send_feedback()
{
// Check nonce for security
if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'quadlayers_send_feedback_nonce')) {
wp_send_json_error(['message' => __('Invalid nonce.', 'wp-plugin-feedback')], 400);
return;
}
// Validate required fields
$pluginBasename = sanitize_text_field($_POST['plugin_basename'] ?? '');
$feedbackReason = sanitize_text_field($_POST['feedback_reason'] ?? '');
$feedbackDetails = sanitize_textarea_field($_POST['feedback_details'] ?? '');
$isAnonymous = filter_var($_POST['is_anonymous'] ?? false, FILTER_VALIDATE_BOOLEAN);
$hasFeedback = filter_var($_POST['has_feedback'] ?? false, FILTER_VALIDATE_BOOLEAN);
if (empty($pluginBasename)) {
wp_send_json_error(['message' => __('Missing required fields.', 'wp-plugin-feedback')], 400);
return;
}
// Create a transient with the hash as the key and set the expiration time to 7 days
set_transient('ql_plugin_feedback_' . $pluginBasename, true, 7 * DAY_IN_SECONDS);
// Send feedback (handling anonymous option)
$result = (new Client($pluginBasename))->sendFeedback($feedbackReason, $feedbackDetails, $isAnonymous, $hasFeedback);
if ($result) {
wp_send_json_success(['message' => __('Feedback submitted successfully.', 'wp-plugin-feedback')]);
} else {
wp_send_json_error(['message' => __('Error sending feedback.', 'wp-plugin-feedback')], 500);
}
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace QuadLayers\PluginFeedback;
class Client
{
/** @var Collector */
private $collector;
/** @var Validator */
private $validator;
/** @var Request */
private $request;
/**
* Constructor to initialize the client with plugin slug and version.
*
* @param string $pluginBasename The plugin slug.
*/
public function __construct(
string $pluginBasename
) {
$this->collector = new Collector($pluginBasename);
$this->validator = new Validator();
$this->request = new Request();
}
/**
* Sends feedback to the server.
*
* @param bool $isAnonymous Determines if the feedback is anonymous.
* @param string $feedbackReason The reason for the feedback.
* @param string $feedbackDetails Additional details for the feedback.
* @return bool True if the feedback was sent successfully, False otherwise.
*/
public function sendFeedback(string $feedbackReason = '', string $feedbackDetails = '', $isAnonymous = false, $hasFeedback = false) : bool
{
// Collect data
$data = $this->collector->collectData($feedbackReason, $feedbackDetails, $isAnonymous, $hasFeedback);
// Validate data
if (!$this->validator->validate($data, $isAnonymous)) {
return false;
}
// Send data
return $this->request->send($data);
}
}

View File

@@ -0,0 +1,83 @@
<?php
namespace QuadLayers\PluginFeedback;
class Collector
{
/** @var string */
private $pluginBasename = '';
public function __construct(
?string $pluginBasename = null
) {
$this->pluginBasename = $pluginBasename;
}
/**
* Collects the necessary feedback data.
*
* @param string $pluginBasename The plugin slug.
* @param bool $isAnonymous If true, no personal data is collected.
* @return array The collected data.
*/
public function collectData(string $feedbackReason, string $feedbackDetails, $isAnonymous = false, $hasFeedback = false): array
{
$data = [
'plugin_slug' => $this->getPluginSlug(),
'plugin_version' => $this->getPluginVersion(),
'server_software' => $_SERVER['SERVER_SOFTWARE'] ?? 'Unknown',
'server_php_version' => phpversion(),
'server_mysql_version' => $this->getMySQLVersion(),
'server_wp_version' => get_bloginfo('version'),
'site_url' => get_site_url(),
'site_theme' => wp_get_theme()->get('Name'),
'site_theme_version' => wp_get_theme()->get('Version'),
'site_language' => get_bloginfo('language'),
'site_plugins' => $this->getActivePlugins(),
'feedback_reason' => $feedbackReason,
'feedback_details' => $feedbackDetails,
'is_anonymous' => $isAnonymous,
'has_feedback' => $hasFeedback,
];
// If feedback is not anonymous, collect user email
if (true !== $isAnonymous) {
$data['user_email'] = wp_get_current_user()->user_email;
}
return $data;
}
private function getMySQLVersion(): string
{
global $wpdb;
return $wpdb->get_var("SELECT VERSION()") ?? 'Unknown';
}
private function getActivePlugins(): string
{
$plugins = get_option('active_plugins', []);
$pluginNames = [];
foreach ($plugins as $plugin) {
$pluginData = get_plugin_data(WP_PLUGIN_DIR . '/' . $plugin);
$pluginNames[] = $pluginData['Name'] . ' ' . $pluginData['Version'];
}
return implode(', ', $pluginNames);
}
private function getPluginSlug()
{
return dirname($this->pluginBasename);
}
private function getPluginVersion()
{
$pluginData = get_plugin_data(WP_PLUGIN_DIR . '/' . $this->pluginBasename);
return $pluginData['Version'];
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace QuadLayers\PluginFeedback;
class Load
{
public static $instance;
public static $plugins = array();
public static $options = array();
private function __construct()
{
add_action('admin_init', [self::class, 'ajax']);
add_action('plugins_loaded', [self::class, 'scripts']);
}
public static function instance()
{
if (is_null(self::$instance)) {
self::$instance = new self();
}
return self::$instance;
}
public function add(string $plugin_file, array $options = []): void
{
$pluginBasename = plugin_basename($plugin_file);
// Check if the plugin is already in the list
if (in_array($pluginBasename, self::$plugins)) {
return;
}
// Check if the plugin transient exists
if (get_transient('ql_plugin_feedback_' . $pluginBasename)) {
return;
}
self::$plugins[] = $pluginBasename;
self::$options[$pluginBasename] = $options;
}
public static function scripts(): void
{
// Enqueue the scripts for the deactivation survey
Scripts::instance(self::$plugins, self::$options);
}
public static function ajax(): void
{
// Register AJAX actions
AjaxHandler::instance();
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace QuadLayers\PluginFeedback;
class Request
{
/**
* Sends the feedback data to the remote server.
*
* @param array $data The collected data.
* @return bool True if the data was sent successfully, False otherwise.
*/
public function send(array $data): bool
{
$body = json_encode($data);
// Create unique hash to prevent duplicate submissions
$hash = md5($body);
if (get_transient('ql_plugin_feedback_' . $hash)) {
return true;
}
// Create a transient with the hash as the key and set the expiration time to 7 days
set_transient('ql_plugin_feedback_' . $hash, true, 30 * DAY_IN_SECONDS);
$response = wp_remote_post(
'https://feedback.quadlayers.com/',
array(
'body' => $body,
'headers' => array(
'Content-Type' => 'application/json',
),
'headers' => [
'Content-Type' => 'application/json',
'Content-Length' => strlen($body),
'User-Agent' => 'WordPress/QuadLayers', // Set a custom User-Agent
],
)
);
if (is_wp_error($response)) {
return false;
}
$statusCode = wp_remote_retrieve_response_code($response);
// Return true if the status code is 200 (OK)
return $statusCode === 200;
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace QuadLayers\PluginFeedback;
class Scripts
{
/** @var Scripts|null */
public static $instance;
/** @var array */
public static $plugins;
/** @var array */
public static $options;
private function __construct(array $plugins = [], array $options = [])
{
self::$plugins = $plugins;
self::$options = $options;
add_action('admin_enqueue_scripts', [self::class, 'load']);
}
public static function instance(array $plugins, array $options = [])
{
if (is_null(self::$instance)) {
self::$instance = new self($plugins, $options);
}
return self::$instance;
}
public static function load(): void
{
global $pagenow;
// Only load on the plugins page
if ($pagenow !== 'plugins.php') {
return;
}
$feedback = include plugin_dir_path(__FILE__) . '../build/js/index.asset.php';
wp_enqueue_style('wp-components');
wp_enqueue_script('quadlayers-plugin-feedback', plugins_url('../build/js/index.js', __FILE__), $feedback['dependencies'], '1.0.0', true);
// Prepare plugin options for frontend
$plugin_data = [];
foreach (self::$plugins as $plugin) {
$plugin_data[$plugin] = [
'plugin' => $plugin,
'options' => isset(self::$options[$plugin]) ? self::$options[$plugin] : []
];
}
wp_localize_script(
'quadlayers-plugin-feedback',
'quadlayersPluginFeedback',
[
'nonce' => wp_create_nonce('quadlayers_send_feedback_nonce'),
'plugins' => $plugin_data
]
);
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace QuadLayers\PluginFeedback;
class Validator
{
/**
* Validates the feedback data.
*
* @param array $data The collected data.
* @param bool $isAnonymous Determines if the feedback is anonymous.
* @return bool True if the data is valid, False otherwise.
*/
public function validate(array $data, bool $isAnonymous = false): bool
{
// Validate required fields
if (empty($data['plugin_slug'])) {
return false;
}
return true;
}
}