Files
wc-licensed-product/src/Product/VersionManager.php
magdev 034593f896 Dashboard widget improvements and download counter feature (v0.3.7)
- Fixed: Dashboard widget "View All Licenses" link used wrong page slug
- Fixed: Download links in customer account resulted in 404 errors
- Removed: Redundant "Status Breakdown" section from dashboard widget
- Changed: License Types section now uses card style layout
- Added: Download counter for licensed product versions
- Added: Download Statistics admin dashboard widget
- Updated translations (356 strings)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 10:17:46 +01:00

374 lines
11 KiB
PHP

<?php
/**
* Version Manager
*
* @package Jeremias\WcLicensedProduct\Product
*/
declare(strict_types=1);
namespace Jeremias\WcLicensedProduct\Product;
use Jeremias\WcLicensedProduct\Installer;
/**
* Manages product versions (CRUD operations)
*/
class VersionManager
{
/**
* Get version by ID
*/
public function getVersionById(int $id): ?ProductVersion
{
global $wpdb;
$tableName = Installer::getVersionsTable();
$row = $wpdb->get_row(
$wpdb->prepare("SELECT * FROM {$tableName} WHERE id = %d", $id),
ARRAY_A
);
return $row ? ProductVersion::fromArray($row) : null;
}
/**
* Get all versions for a product
*/
public function getVersionsByProduct(int $productId): array
{
global $wpdb;
$tableName = Installer::getVersionsTable();
$rows = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$tableName} WHERE product_id = %d ORDER BY major_version DESC, minor_version DESC, patch_version DESC",
$productId
),
ARRAY_A
);
return array_map(fn(array $row) => ProductVersion::fromArray($row), $rows ?: []);
}
/**
* Get latest active version for a product
*/
public function getLatestVersion(int $productId): ?ProductVersion
{
global $wpdb;
$tableName = Installer::getVersionsTable();
$row = $wpdb->get_row(
$wpdb->prepare(
"SELECT * FROM {$tableName} WHERE product_id = %d AND is_active = 1 ORDER BY major_version DESC, minor_version DESC, patch_version DESC LIMIT 1",
$productId
),
ARRAY_A
);
return $row ? ProductVersion::fromArray($row) : null;
}
/**
* Get latest version for a specific major version
*/
public function getLatestVersionForMajor(int $productId, int $majorVersion): ?ProductVersion
{
global $wpdb;
$tableName = Installer::getVersionsTable();
$row = $wpdb->get_row(
$wpdb->prepare(
"SELECT * FROM {$tableName} WHERE product_id = %d AND major_version = %d AND is_active = 1 ORDER BY minor_version DESC, patch_version DESC LIMIT 1",
$productId,
$majorVersion
),
ARRAY_A
);
return $row ? ProductVersion::fromArray($row) : null;
}
/**
* Create a new version
*
* @throws \InvalidArgumentException If file hash validation fails
*/
public function createVersion(
int $productId,
string $version,
?string $releaseNotes = null,
?int $attachmentId = null,
?string $fileHash = null
): ?ProductVersion {
global $wpdb;
// Validate file hash if both attachment and hash are provided
if ($attachmentId !== null && $attachmentId > 0 && $fileHash !== null && $fileHash !== '') {
$validatedHash = $this->validateFileHash($attachmentId, $fileHash);
if ($validatedHash === false) {
return null;
}
$fileHash = $validatedHash;
}
$parsed = ProductVersion::parseVersion($version);
$tableName = Installer::getVersionsTable();
// Build data and formats arrays, handling null values properly
$data = [
'product_id' => $productId,
'version' => $version,
'major_version' => $parsed['major'],
'minor_version' => $parsed['minor'],
'patch_version' => $parsed['patch'],
'release_notes' => $releaseNotes,
'is_active' => 1,
];
$formats = ['%d', '%s', '%d', '%d', '%d', '%s', '%d'];
// Only include attachment_id if it's set
if ($attachmentId !== null && $attachmentId > 0) {
$data['attachment_id'] = $attachmentId;
$formats[] = '%d';
}
// Only include file_hash if it's set
if ($fileHash !== null && $fileHash !== '') {
$data['file_hash'] = $fileHash;
$formats[] = '%s';
}
$result = $wpdb->insert($tableName, $data, $formats);
if ($result === false) {
// Log error for debugging
error_log('WC Licensed Product: Failed to create version - ' . $wpdb->last_error);
return null;
}
return $this->getVersionById((int) $wpdb->insert_id);
}
/**
* Validate file hash against attachment
*
* @return string|false The validated hash (lowercase) or false on mismatch
* @throws \InvalidArgumentException If hash doesn't match
*/
private function validateFileHash(int $attachmentId, string $providedHash): string|false
{
$filePath = get_attached_file($attachmentId);
if (!$filePath || !file_exists($filePath)) {
throw new \InvalidArgumentException(
__('Attachment file not found.', 'wc-licensed-product')
);
}
$calculatedHash = hash_file('sha256', $filePath);
$providedHash = strtolower(trim($providedHash));
if (!hash_equals($calculatedHash, $providedHash)) {
throw new \InvalidArgumentException(
sprintf(
/* translators: 1: provided hash, 2: calculated hash */
__('File checksum does not match. Expected: %1$s, Got: %2$s', 'wc-licensed-product'),
$providedHash,
$calculatedHash
)
);
}
return $calculatedHash;
}
/**
* Update a version
*/
public function updateVersion(
int $versionId,
?string $releaseNotes = null,
?bool $isActive = null,
?int $attachmentId = null
): bool {
global $wpdb;
$data = [];
$formats = [];
if ($releaseNotes !== null) {
$data['release_notes'] = $releaseNotes;
$formats[] = '%s';
}
if ($isActive !== null) {
$data['is_active'] = $isActive ? 1 : 0;
$formats[] = '%d';
}
if ($attachmentId !== null) {
if ($attachmentId > 0) {
$data['attachment_id'] = $attachmentId;
$formats[] = '%d';
} else {
// Set to NULL using raw SQL instead of adding to $data
global $wpdb;
$tableName = Installer::getVersionsTable();
$wpdb->query(
$wpdb->prepare(
"UPDATE {$tableName} SET attachment_id = NULL WHERE id = %d",
$versionId
)
);
}
}
if (empty($data)) {
return true;
}
$tableName = Installer::getVersionsTable();
$result = $wpdb->update(
$tableName,
$data,
['id' => $versionId],
$formats,
['%d']
);
return $result !== false;
}
/**
* Delete a version
*/
public function deleteVersion(int $versionId): bool
{
global $wpdb;
$tableName = Installer::getVersionsTable();
$result = $wpdb->delete(
$tableName,
['id' => $versionId],
['%d']
);
return $result !== false;
}
/**
* Check if version exists for product
*/
public function versionExists(int $productId, string $version): bool
{
global $wpdb;
$tableName = Installer::getVersionsTable();
$count = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM {$tableName} WHERE product_id = %d AND version = %s",
$productId,
$version
)
);
return (int) $count > 0;
}
/**
* Increment download count for a version
*/
public function incrementDownloadCount(int $versionId): bool
{
global $wpdb;
$tableName = Installer::getVersionsTable();
$result = $wpdb->query(
$wpdb->prepare(
"UPDATE {$tableName} SET download_count = download_count + 1 WHERE id = %d",
$versionId
)
);
return $result !== false;
}
/**
* Get total download count across all versions
*/
public function getTotalDownloadCount(): int
{
global $wpdb;
$tableName = Installer::getVersionsTable();
$count = $wpdb->get_var("SELECT COALESCE(SUM(download_count), 0) FROM {$tableName}");
return (int) $count;
}
/**
* Get download statistics per product
*/
public function getDownloadStatistics(): array
{
global $wpdb;
$tableName = Installer::getVersionsTable();
// Get total downloads
$totalDownloads = $this->getTotalDownloadCount();
// Get downloads per product (top 10)
$byProduct = $wpdb->get_results(
"SELECT product_id, SUM(download_count) as downloads
FROM {$tableName}
GROUP BY product_id
ORDER BY downloads DESC
LIMIT 10",
ARRAY_A
);
// Get downloads per version (top 10)
$byVersion = $wpdb->get_results(
"SELECT id, product_id, version, download_count
FROM {$tableName}
WHERE download_count > 0
ORDER BY download_count DESC
LIMIT 10",
ARRAY_A
);
// Enrich product data with names
$productsWithNames = [];
foreach ($byProduct ?: [] as $row) {
$product = wc_get_product((int) $row['product_id']);
$productsWithNames[] = [
'product_id' => (int) $row['product_id'],
'product_name' => $product ? $product->get_name() : __('Unknown Product', 'wc-licensed-product'),
'downloads' => (int) $row['downloads'],
];
}
// Enrich version data with product names
$versionsWithNames = [];
foreach ($byVersion ?: [] as $row) {
$product = wc_get_product((int) $row['product_id']);
$versionsWithNames[] = [
'version_id' => (int) $row['id'],
'product_id' => (int) $row['product_id'],
'product_name' => $product ? $product->get_name() : __('Unknown Product', 'wc-licensed-product'),
'version' => $row['version'],
'downloads' => (int) $row['download_count'],
];
}
return [
'total' => $totalDownloads,
'by_product' => $productsWithNames,
'by_version' => $versionsWithNames,
];
}
}