1
0
Fork 0
mirror of https://github.com/DanielnetoDotCom/YouPHPTube synced 2025-10-03 01:39:24 +02:00

Refactor YPTWallet configuration: remove unused checks and add donation notification URL handling

https://github.com/WWBN/AVideo/issues/10138
This commit is contained in:
Daniel Neto 2025-07-28 12:16:56 -03:00
parent bc240901ed
commit c147b57f41
4 changed files with 548 additions and 83 deletions

View file

@ -75,9 +75,6 @@ class YPTSocket extends PluginAbstract
'debugAllUsersSocket',
'allow_self_signed',
'forceNonSecure',
'showTotalOnlineUsersPerVideo',
'showTotalOnlineUsersPerLive',
'showTotalOnlineUsersPerLiveLink',
);
}

View file

@ -919,17 +919,6 @@ class YPTWallet extends PluginAbstract
public function getWalletConfigurationHTML($users_id, $wallet, $walletDataObject)
{
global $global;
if (empty($walletDataObject->CryptoWalletEnabled)) {
if (User::isAdmin()) {
YPTWallet::showAdminMessage();
echo '<div class="alert alert-warning" role="alert">
<i class="fa fa-exclamation-triangle"></i>
YPTWallet configuration will only appear if <strong>CryptoWalletEnabled</strong> is enabled in the plugin parameters.
<br>If you have an empty configuration menu, please hide this button by checking the <strong>hideConfiguration</strong> option in the YPTWallet parameters.
</div>';
}
return '';
}
include_once $global['systemRootPath'] . 'plugin/YPTWallet/getWalletConfigurationHTML.php';
}
@ -985,4 +974,149 @@ class YPTWallet extends PluginAbstract
}
return false;
}
static function setDonationNotificationURL($users_id, $url)
{
// Sanitize the URL string for safe database storage
$url = trim($url);
// Remove any null bytes and control characters that could cause issues
$url = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $url);
// HTML encode any special characters to prevent XSS when displayed
$url = htmlspecialchars($url, ENT_QUOTES, 'UTF-8');
// Limit length to prevent database issues
if (strlen($url) > 2048) {
_error_log("URL too long. Maximum 2048 characters allowed");
return false;
}
$user = new User($users_id);
return $user->addExternalOptions('donation_notification_url', $url);
}
static function getDonationNotificationURL($users_id)
{
$user = new User($users_id);
return $user->getExternalOptions('donation_notification_url');
}
public function afterDonation($from_users_id, $how_much, $videos_id, $users_id, $extraParameters)
{
$donation_notification_url = self::getDonationNotificationURL($users_id);
$webhookSecret = self::getDonationNotificationSecret($users_id); // Get user's secret
$obj = AVideoPlugin::getObjectData('YPTWallet');
$data = array(
'from_users_id' => $from_users_id,
'from_users_name' => User::getNameIdentificationById($from_users_id),
'currency' => $obj->currency,
'how_much_human' => YPTWallet::formatCurrency($how_much),
'how_much' => $how_much,
'message' => $extraParameters['message'] ?? '',
'videos_id' => $videos_id,
'users_id' => $users_id,
'time' => time(),
'extraParameters' => $extraParameters
);
if (!empty($donation_notification_url) && isValidURL($donation_notification_url)) {
_error_log("Sending donation notification via POST to URL: {$donation_notification_url} for user ID: {$users_id}");
// Create POST data string
$postData = http_build_query($data);
// Generate signature using user's webhook secret
$signature = hash_hmac('sha256', $postData, $webhookSecret);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $donation_notification_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_TIMEOUT, 1);
curl_setopt($ch, CURLOPT_NOSIGNAL, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_USERAGENT, getSelfUserAgent());
// Add signature to headers
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'Content-Type: application/x-www-form-urlencoded',
'X-Webhook-Signature: sha256=' . $signature,
'X-Webhook-Timestamp: ' . $data['time']
));
// Silent execution
ob_start();
curl_exec($ch);
ob_end_clean();
curl_close($ch);
} else {
_error_log("Donation notification URL is not set or invalid for user ID: {$users_id} " . json_encode($data));
}
}
/**
* Generate a cryptographically secure random string.
* @param int $length
* @return string
*/
private static function generateRandomString($length = 32)
{
if (function_exists('random_bytes')) {
return bin2hex(random_bytes($length / 2));
} elseif (function_exists('openssl_random_pseudo_bytes')) {
return bin2hex(openssl_random_pseudo_bytes($length / 2));
} else {
// fallback (not cryptographically secure)
return substr(str_shuffle(str_repeat('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', $length)), 0, $length);
}
}
static function setDonationNotificationSecret($users_id, $secret = null)
{
// If no secret provided, generate a new one
if (empty($secret)) {
$secret = self::generateRandomString(32);
}
// Sanitize the secret
$secret = trim($secret);
// Limit length for database safety
if (strlen($secret) > 255) {
_error_log("Webhook secret too long. Maximum 255 characters allowed");
return false;
}
// Remove any dangerous characters
$secret = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $secret);
$user = new User($users_id);
return $user->addExternalOptions('donation_notification_secret', $secret);
}
static function getDonationNotificationSecret($users_id)
{
$user = new User($users_id);
$secret = $user->getExternalOptions('donation_notification_secret');
// If no secret exists, generate one
if (empty($secret)) {
$secret = self::generateRandomString(32);
self::setDonationNotificationSecret($users_id, $secret);
}
return $secret;
}
static function regenerateDonationNotificationSecret($users_id)
{
$newSecret = self::generateRandomString(32);
return self::setDonationNotificationSecret($users_id, $newSecret);
}
}

View file

@ -1,37 +1,358 @@
<?php
$myWallet = YPTWallet::getWallet(User::getId());
?>
<div class="panel panel-default">
<div class="panel-heading"><?php echo __("Configurations"); ?></div>
<div class="panel-body">
<form id="form">
<div class="form-group">
<label for="CryptoWallet"><?php echo $walletDataObject->CryptoWalletName; ?>:</label>
<input type="text" class="form-control" name="CryptoWallet" value="<?php echo $myWallet->getCrypto_wallet_address(); ?>">
</div>
<button type="submit" class="btn btn-success"><i class="fas fa-save"></i> <?php echo __("Save"); ?></button>
</form>
</div>
</div>
<script>
$(document).ready(function () {
$("#form").submit(function (event) {
event.preventDefault();
modal.showPleaseWait();
$.ajax({
url: webSiteRootURL+'plugin/YPTWallet/view/saveConfiguration.php',
data: $("#form").serialize(),
type: 'post',
success: function (response) {
if (!response.error) {
avideoAlert("<?php echo __("Congratulations!"); ?>", "<?php echo __("Configuration Saved"); ?>", "success");
} else {
avideoAlert("<?php echo __("Sorry!"); ?>", response.msg, "error");
}
modal.hidePleaseWait();
console.log(response);
}
});
});
});
</script>
<?php
$myWallet = YPTWallet::getWallet(User::getId());
?>
<div class="panel panel-default">
<div class="panel-heading"><?php echo __("Configurations"); ?></div>
<div class="panel-body">
<form id="form">
<div class="form-group" style="<?php echo $walletDataObject->CryptoWalletEnabled ? '' : 'display:none;' ?>">
<label for="CryptoWallet"><?php echo $walletDataObject->CryptoWalletName; ?>:</label>
<input type="text" class="form-control" name="CryptoWallet" value="<?php echo $myWallet->getCrypto_wallet_address(); ?>">
</div>
<div class="form-group">
<label for="donation_notification_url">
<?php echo __('Donation Notification URL'); ?> (Webhook):
<button type="button" class="btn btn-xs btn-info" data-toggle="collapse" data-target="#webhookDocs" style="margin-left: 5px;">
<i class="fa fa-question-circle"></i> <?php echo __('Help'); ?>
</button>
</label>
<input type="url" class="form-control" name="donation_notification_url" value="<?php echo YPTWallet::getDonationNotificationUrl(User::getId()); ?>" placeholder="<?php echo __('Donation Notification URL'); ?>"
title="<?php echo __('This URL will be called when a donation is made.'); ?>">
<div class="well well-sm" style="margin-top: 10px;">
<h5><i class="fa fa-key"></i> <?php echo __('Your Webhook Secret Key:'); ?></h5>
<div class="input-group">
<input type="text" class="form-control" id="webhookSecret" value="<?php echo YPTWallet::getDonationNotificationSecret(User::getId()); ?>" readonly>
<span class="input-group-btn">
<button class="btn btn-default" type="button" onclick="copyToClipboard(document.getElementById('webhookSecret'))">
<i class="fa fa-copy"></i> <?php echo __('Copy'); ?>
</button>
<button class="btn btn-warning" type="button" onclick="regenerateSecret()" title="<?php echo __('Generate new secret key'); ?>">
<i class="fa fa-refresh"></i> <?php echo __('Regenerate'); ?>
</button>
</span>
</div>
<small class="text-muted"><?php echo __('Use this secret to verify webhook signatures. Keep it safe and private!'); ?></small>
</div>
<!-- Webhook Documentation -->
<div class="collapse" id="webhookDocs" style="margin-top: 10px;">
<div class="panel panel-info">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fa fa-info-circle"></i> <?php echo __('Webhook Documentation'); ?>
</h4>
</div>
<div class="panel-body">
<p><i class="fa fa-globe"></i> <strong><?php echo __('How it works:'); ?></strong></p>
<p><?php echo __('When someone makes a donation, AVideo will automatically send a POST request to your URL with the following parameters and security headers:'); ?></p>
<div class="well well-sm">
<h5><i class="fa fa-shield"></i> <?php echo __('Security Headers:'); ?></h5>
<table class="table table-condensed">
<thead>
<tr>
<th><i class="fa fa-tag"></i> <?php echo __('Header'); ?></th>
<th><i class="fa fa-info"></i> <?php echo __('Description'); ?></th>
<th><i class="fa fa-eye"></i> <?php echo __('Example'); ?></th>
</tr>
</thead>
<tbody>
<tr>
<td><code>X-Webhook-Signature</code></td>
<td><?php echo __('HMAC SHA256 signature of the POST data for verification'); ?></td>
<td><code>sha256=abc123...</code></td>
</tr>
<tr>
<td><code>X-Webhook-Timestamp</code></td>
<td><?php echo __('Unix timestamp when the request was sent'); ?></td>
<td><code><?php echo time(); ?></code></td>
</tr>
<tr>
<td><code>Content-Type</code></td>
<td><?php echo __('Request content type'); ?></td>
<td><code>application/x-www-form-urlencoded</code></td>
</tr>
<tr>
<td><code>User-Agent</code></td>
<td><?php echo __('Identifies the request as coming from AVideo'); ?></td>
<td><code>AVideoStreamer_*</code></td>
</tr>
</tbody>
</table>
</div>
<h5><i class="fa fa-list"></i> <?php echo __('Parameters explained:'); ?></h5>
<div class="table-responsive">
<table class="table table-condensed table-striped">
<thead>
<tr>
<th><i class="fa fa-tag"></i> <?php echo __('Parameter'); ?></th>
<th><i class="fa fa-info"></i> <?php echo __('Description'); ?></th>
<th><i class="fa fa-eye"></i> <?php echo __('Example'); ?></th>
</tr>
</thead>
<tbody>
<tr>
<td><code>from_users_id</code></td>
<td><?php echo __('ID of the user who made the donation'); ?></td>
<td><span class="label label-info">1</span></td>
</tr>
<tr>
<td><code>from_users_name</code></td>
<td><?php echo __('Name of the user who made the donation'); ?></td>
<td><span class="label label-info">John Doe</span></td>
</tr>
<tr>
<td><code>currency</code></td>
<td><?php echo __('Currency code configured in wallet'); ?></td>
<td><span class="label label-warning">USD</span></td>
</tr>
<tr>
<td><code>how_much</code></td>
<td><?php echo __('Raw amount donated (numeric value)'); ?></td>
<td><span class="label label-success">21</span></td>
</tr>
<tr>
<td><code>how_much_human</code></td>
<td><?php echo __('Formatted amount with currency symbol'); ?></td>
<td><span class="label label-success">$21.00</span></td>
</tr>
<tr>
<td><code>message</code></td>
<td><?php echo __('Message sent with the donation'); ?></td>
<td><span class="label label-default">Great content</span></td>
</tr>
<tr>
<td><code>videos_id</code></td>
<td><?php echo __('ID of the video (0 for live chat)'); ?></td>
<td><span class="label label-primary">0</span></td>
</tr>
<tr>
<td><code>users_id</code></td>
<td><?php echo __('ID of the user receiving the donation (You)'); ?></td>
<td><span class="label label-info">1</span></td>
</tr>
<tr>
<td><code>time</code></td>
<td><?php echo __('Unix timestamp when the donation was made'); ?></td>
<td><span class="label label-default"><?php echo time(); ?></span></td>
</tr>
<tr>
<td><code>extraParameters[superChat]</code></td>
<td><?php echo __('Super chat flag (1 if super chat)'); ?></td>
<td><span class="label label-warning">1</span></td>
</tr>
<tr>
<td><code>extraParameters[message]</code></td>
<td><?php echo __('Duplicate of message parameter for compatibility'); ?></td>
<td><span class="label label-default">Great content</span></td>
</tr>
<tr>
<td><code>extraParameters[live_transmitions_history_id]</code></td>
<td><?php echo __('Live transmission ID (if donation during live stream)'); ?></td>
<td><span class="label label-danger">32</span></td>
</tr>
</tbody>
</table>
</div>
<div class="alert alert-danger">
<i class="fa fa-shield"></i>
<strong><?php echo __('Security Warning:'); ?></strong>
<?php echo __('ALWAYS verify the webhook signature before processing any data. Never trust webhook data without proper signature verification!'); ?>
</div>
<div class="well well-sm">
<h5><i class="fa fa-code"></i> <?php echo __('PHP Verification Example:'); ?></h5>
<pre><code><?php echo htmlspecialchars('<?php
// Step 1: Get headers and raw POST data
$signature = $_SERVER[\'HTTP_X_WEBHOOK_SIGNATURE\'] ?? \'\';
$timestamp = $_SERVER[\'HTTP_X_WEBHOOK_TIMESTAMP\'] ?? \'\';
$rawPostData = file_get_contents(\'php://input\');
// Step 2: Your webhook secret (copy from above)
$webhookSecret = \'YOUR_WEBHOOK_SECRET_FROM_ABOVE\';
// Step 3: Verify timestamp (optional but recommended)
$currentTime = time();
$timeDifference = $currentTime - intval($timestamp);
if ($timeDifference > 300) { // 5 minutes tolerance
http_response_code(400);
exit(\'Request expired\');
}
// Step 4: Calculate expected signature
$expectedSignature = \'sha256=\' . hash_hmac(\'sha256\', $rawPostData, $webhookSecret);
// Step 5: Verify signature using timing-safe comparison
if (!hash_equals($signature, $expectedSignature)) {
http_response_code(401);
exit(\'Invalid signature\');
}
// Step 6: Signature is valid, process the webhook data
parse_str($rawPostData, $donationData);
// Now you can safely use the donation data
$donorId = $donationData[\'from_users_id\'];
$donorName = $donationData[\'from_users_name\'];
$amount = $donationData[\'how_much\'];
$formattedAmount = $donationData[\'how_much_human\'];
$message = $donationData[\'message\'];
$videoId = $donationData[\'videos_id\'];
$receiverId = $donationData[\'users_id\'];
// Your processing logic here...
// Example: Save to database, send notifications, etc.
// Always respond with 200 OK
http_response_code(200);
echo \'Webhook processed successfully\';
?>'); ?></code></pre>
</div>
<div class="well well-sm">
<h5><i class="fa fa-code"></i> <?php echo __('Node.js/JavaScript Example:'); ?></h5>
<pre><code><?php echo htmlspecialchars('const crypto = require(\'crypto\');
const express = require(\'express\');
const app = express();
// Middleware to get raw body
app.use(\'/webhook\', express.raw({type: \'application/x-www-form-urlencoded\'}));
app.post(\'/webhook\', (req, res) => {
const signature = req.headers[\'x-webhook-signature\'];
const timestamp = req.headers[\'x-webhook-timestamp\'];
const rawBody = req.body;
// Your webhook secret
const webhookSecret = \'YOUR_WEBHOOK_SECRET_FROM_ABOVE\';
// Verify timestamp
const currentTime = Math.floor(Date.now() / 1000);
const timeDifference = currentTime - parseInt(timestamp);
if (timeDifference > 300) {
return res.status(400).send(\'Request expired\');
}
// Calculate expected signature
const expectedSignature = \'sha256=\' + crypto
.createHmac(\'sha256\', webhookSecret)
.update(rawBody)
.digest(\'hex\');
// Verify signature
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature))) {
return res.status(401).send(\'Invalid signature\');
}
// Parse form data
const params = new URLSearchParams(rawBody.toString());
const donationData = Object.fromEntries(params);
// Process webhook data
console.log(\'Donation received:\', donationData);
res.status(200).send(\'OK\');
});'); ?></code></pre>
</div>
<div class="alert alert-info">
<i class="fa fa-key"></i>
<strong><?php echo __('Security Best Practices:'); ?></strong>
<ul class="list-unstyled" style="margin-top: 10px;">
<li><i class="fa fa-check text-success"></i> <?php echo __('Always use hash_equals() or crypto.timingSafeEqual() for signature comparison'); ?></li>
<li><i class="fa fa-check text-success"></i> <?php echo __('Verify timestamp to prevent replay attacks'); ?></li>
<li><i class="fa fa-check text-success"></i> <?php echo __('Use the raw POST body for signature calculation, not parsed data'); ?></li>
<li><i class="fa fa-check text-success"></i> <?php echo __('Keep your webhook secret private and secure'); ?></li>
<li><i class="fa fa-check text-success"></i> <?php echo __('Regenerate your webhook secret if compromised'); ?></li>
<li><i class="fa fa-check text-success"></i> <?php echo __('Always respond with HTTP 200 for valid requests'); ?></li>
</ul>
</div>
<div class="alert alert-warning">
<i class="fa fa-exclamation-triangle"></i>
<strong><?php echo __('Important:'); ?></strong>
<?php echo __('Your webhook endpoint should respond with HTTP 200 status code for successful processing. The request timeout is 1 second, so ensure your endpoint responds quickly.'); ?>
</div>
<div class="alert alert-info">
<i class="fa fa-lightbulb-o"></i>
<strong><?php echo __('Use Cases:'); ?></strong>
<?php echo __('You can use this webhook to integrate with external systems, trigger notifications, update databases, send emails, integrate with Discord/Slack, or create custom donation alerts when donations are received.'); ?>
</div>
</div>
</div>
</div>
</div>
<button type="submit" class="btn btn-success btn-block"><i class="fas fa-save"></i> <?php echo __("Save"); ?></button>
</form>
</div>
</div>
<script>
function copyToClipboard(element) {
element.select();
element.setSelectionRange(0, 99999);
document.execCommand("copy");
avideoToast("<?php echo __('Copied to clipboard!'); ?>");
}
function regenerateSecret() {
avideoConfirmCallBack(
__('Are you sure you want to generate a new webhook secret? This will invalidate the current one and any existing integrations will need to be updated with the new secret.'),
function() {
// Confirm callback - user clicked confirm
modal.showPleaseWait();
$.ajax({
url: webSiteRootURL + 'plugin/YPTWallet/view/saveConfiguration.php',
data: {
regenerate_webhook_secret: 1
},
type: 'post',
success: function(response) {
if (!response.error && response.new_webhook_secret) {
$('#webhookSecret').val(response.webhook_secret);
avideoToastSuccess(__('New webhook secret generated successfully!'));
} else {
avideoAlertError(__('Error generating new secret'));
}
modal.hidePleaseWait();
},
error: function() {
avideoAlertError(__('Failed to regenerate webhook secret'));
modal.hidePleaseWait();
}
});
},
function() {
// Cancel callback - user clicked cancel
console.log("User cancelled webhook secret regeneration");
}
);
}
$(document).ready(function() {
$("#form").submit(function(event) {
event.preventDefault();
modal.showPleaseWait();
$.ajax({
url: webSiteRootURL + 'plugin/YPTWallet/view/saveConfiguration.php',
data: $("#form").serialize(),
type: 'post',
success: function(response) {
if (!response.error) {
avideoAlertSuccess(__("Configuration Saved"));
// Update webhook secret display if returned
if (response.webhook_secret) {
document.getElementById('webhookSecret').value = response.webhook_secret;
}
} else {
avideoAlertError(response.msg);
}
modal.hidePleaseWait();
}
});
});
});
</script>

View file

@ -1,32 +1,45 @@
<?php
if (empty($global['systemRootPath'])) {
$global['systemRootPath'] = '../../../';
}
require_once $global['systemRootPath'] . 'videos/configuration.php';
require_once $global['systemRootPath'] . 'objects/user.php';
$obj = new stdClass();
$obj->error = true;
$obj->msg = "";
$obj->walletBalance = 0;
if (!User::isLogged()) {
$obj->msg = ("Is not Loged");
die(json_encode($obj));
}
$plugin = AVideoPlugin::loadPluginIfEnabled("YPTWallet");
if(empty($plugin)){
$obj->msg = ("Plugin not enabled");
die(json_encode($obj));
}
header('Content-Type: application/json');
$wallet = new Wallet(0);
$wallet->setUsers_id(User::getId());
$wallet->setCrypto_wallet_address($_POST['CryptoWallet']);
if($wallet->save()){
$obj->error = false;
}
$obj->walletBalance = $plugin->getBalanceFormated(User::getId());
echo json_encode($obj);
<?php
if (empty($global['systemRootPath'])) {
$global['systemRootPath'] = '../../../';
}
require_once $global['systemRootPath'] . 'videos/configuration.php';
require_once $global['systemRootPath'] . 'objects/user.php';
$obj = new stdClass();
$obj->error = true;
$obj->msg = "";
$obj->walletBalance = 0;
if (!User::isLogged()) {
$obj->msg = ("Is not Loged");
die(json_encode($obj));
}
$plugin = AVideoPlugin::loadPluginIfEnabled("YPTWallet");
if(empty($plugin)){
$obj->msg = ("Plugin not enabled");
die(json_encode($obj));
}
header('Content-Type: application/json');
$wallet = new Wallet(0);
$wallet->setUsers_id(User::getId());
$wallet->setCrypto_wallet_address($_POST['CryptoWallet']);
if($wallet->save()){
$obj->error = false;
}
if(isset($_REQUEST['donation_notification_url'])){
$obj->donation_notification_url = YPTWallet::setDonationNotificationURL(User::getId(), $_REQUEST['donation_notification_url']);
}
// Handle webhook secret regeneration
if(isset($_REQUEST['regenerate_webhook_secret']) && $_REQUEST['regenerate_webhook_secret'] == '1'){
$obj->new_webhook_secret = YPTWallet::regenerateDonationNotificationSecret(User::getId());
}
// Always return current webhook secret
$obj->webhook_secret = YPTWallet::getDonationNotificationSecret(User::getId());
$obj->walletBalance = $plugin->getBalanceFormated(User::getId());
echo json_encode($obj);