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, ]; } }