2026-01-21 19:15:19 +01:00
|
|
|
<?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
|
2026-01-22 16:57:54 +01:00
|
|
|
*
|
|
|
|
|
* @throws \InvalidArgumentException If file hash validation fails
|
2026-01-21 19:15:19 +01:00
|
|
|
*/
|
|
|
|
|
public function createVersion(
|
|
|
|
|
int $productId,
|
|
|
|
|
string $version,
|
|
|
|
|
?string $releaseNotes = null,
|
2026-01-22 16:57:54 +01:00
|
|
|
?int $attachmentId = null,
|
|
|
|
|
?string $fileHash = null
|
2026-01-21 19:15:19 +01:00
|
|
|
): ?ProductVersion {
|
|
|
|
|
global $wpdb;
|
|
|
|
|
|
2026-01-22 16:57:54 +01:00
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-21 19:15:19 +01:00
|
|
|
$parsed = ProductVersion::parseVersion($version);
|
|
|
|
|
|
|
|
|
|
$tableName = Installer::getVersionsTable();
|
2026-01-21 22:02:51 +01:00
|
|
|
|
|
|
|
|
// 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,
|
|
|
|
|
];
|
2026-01-22 16:57:54 +01:00
|
|
|
$formats = ['%d', '%s', '%d', '%d', '%d', '%s', '%d'];
|
2026-01-21 22:02:51 +01:00
|
|
|
|
|
|
|
|
// Only include attachment_id if it's set
|
|
|
|
|
if ($attachmentId !== null && $attachmentId > 0) {
|
|
|
|
|
$data['attachment_id'] = $attachmentId;
|
|
|
|
|
$formats[] = '%d';
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-22 16:57:54 +01:00
|
|
|
// Only include file_hash if it's set
|
|
|
|
|
if ($fileHash !== null && $fileHash !== '') {
|
|
|
|
|
$data['file_hash'] = $fileHash;
|
|
|
|
|
$formats[] = '%s';
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-21 22:02:51 +01:00
|
|
|
$result = $wpdb->insert($tableName, $data, $formats);
|
2026-01-21 19:15:19 +01:00
|
|
|
|
|
|
|
|
if ($result === false) {
|
2026-01-21 22:02:51 +01:00
|
|
|
// Log error for debugging
|
|
|
|
|
error_log('WC Licensed Product: Failed to create version - ' . $wpdb->last_error);
|
2026-01-21 19:15:19 +01:00
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $this->getVersionById((int) $wpdb->insert_id);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-22 16:57:54 +01:00
|
|
|
/**
|
|
|
|
|
* 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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-21 19:15:19 +01:00
|
|
|
/**
|
|
|
|
|
* Update a version
|
|
|
|
|
*/
|
|
|
|
|
public function updateVersion(
|
|
|
|
|
int $versionId,
|
|
|
|
|
?string $releaseNotes = null,
|
2026-01-21 19:46:50 +01:00
|
|
|
?bool $isActive = null,
|
|
|
|
|
?int $attachmentId = null
|
2026-01-21 19:15:19 +01:00
|
|
|
): 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';
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-21 19:46:50 +01:00
|
|
|
if ($attachmentId !== null) {
|
2026-01-22 11:57:05 +01:00
|
|
|
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
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-01-21 19:46:50 +01:00
|
|
|
}
|
|
|
|
|
|
2026-01-21 19:15:19 +01:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|