First Commit

This commit is contained in:
2025-08-28 19:35:28 -07:00
commit 5aa0777fd3
507 changed files with 158447 additions and 0 deletions

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 MiB

142
CLAUDE.md Normal file
View File

@@ -0,0 +1,142 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## WordPress Digital Download Plugin
This is a comprehensive WordPress plugin for creating a digital download marketplace. The plugin allows creators to sell digital products with PayPal integration, secure file protection, and watermarking capabilities.
## Development Commands
### Testing
- `npm test` - Run all Playwright E2E tests
- `npm run test:headed` - Run tests with browser UI visible
- `npm run test:debug` - Run tests in debug mode
- `npm run test:ui` - Run tests with Playwright UI
### E2E Testing Configuration
- Tests are configured for `https://streamers.channel` (live site)
- Test files located in `tests/e2e/`
- Uses Playwright with Chromium browser
- Admin credentials stored in test file for WordPress login tests
## Architecture Overview
### Plugin Structure
The plugin follows WordPress standards with a singleton main class `WP_Digital_Download` that orchestrates all components:
**Main Plugin File**: `wp-digital-download.php`
- Defines plugin constants and initializes the main class
- Handles dependency loading and hook initialization
- Manages script/style enqueuing with cache busting
### Core Components
**Product Management** (`includes/class-wpdd-post-types.php`, `includes/class-wpdd-metaboxes.php`)
- Custom post type `wpdd_product` for digital products
- Rich metadata system for pricing, files, download limits
- Creator attribution and sales tracking
**Payment Processing** (`includes/class-wpdd-paypal.php`)
- PayPal API integration (sandbox/live modes)
- Order processing and webhook handling
- Support for both paid and free products
**File Security** (`includes/class-wpdd-file-protection.php`, `includes/class-wpdd-download-handler.php`)
- Secure file storage outside web root
- Token-based download authentication
- Time-limited and usage-limited downloads
- Download tracking and logging
**User Management** (`includes/class-wpdd-roles.php`, `includes/class-wpdd-customer.php`)
- Custom roles: Digital Customer, Digital Creator
- Customer purchase history and account management
- Guest checkout support
**Frontend Display** (`includes/class-wpdd-shortcodes.php`)
- Shortcodes: `[wpdd_shop]`, `[wpdd_checkout]`, `[wpdd_customer_purchases]`, etc.
- Product grids, filtering, and pagination
- Responsive design support
**Admin Interface** (`admin/class-wpdd-admin.php`, `admin/class-wpdd-settings.php`)
- Admin dashboard for orders and sales management
- Plugin settings and PayPal configuration
- Product editing interface
**Additional Features**
- Watermarking (`includes/class-wpdd-watermark.php`): Dynamic image/PDF watermarks
- AJAX handlers (`includes/class-wpdd-ajax.php`): Frontend interactions
- Installation routines (`includes/class-wpdd-install.php`): Database setup and pages
### Database Schema
- `wp_wpdd_orders` - Purchase records
- `wp_wpdd_downloads` - Download tracking
- `wp_wpdd_download_links` - Secure download tokens
- Product metadata stored in `wp_postmeta`
### Key Shortcodes
- `[wpdd_shop]` - Main product storefront with filtering/sorting
- `[wpdd_customer_purchases]` - Customer purchase history (login required)
- `[wpdd_checkout]` - Payment processing form
- `[wpdd_thank_you]` - Order confirmation page
- `[wpdd_product id="123"]` - Single product display
## Development Notes
### File Loading Pattern
All classes are loaded conditionally with existence checks and error logging. Admin classes are only loaded in admin context.
### Security Considerations
- CSRF protection via nonces on all forms
- Input sanitization and validation
- Capability checks for admin functions
- Secure file delivery system
- XSS prevention in user inputs
### Frontend Assets
- Cache busting using file modification times
- Separate CSS/JS for admin and frontend
- jQuery dependencies for interactive features
- Localized AJAX endpoints with nonces
### Testing Strategy
Comprehensive Playwright E2E tests covering:
- Product display and search functionality
- Free download workflow (with/without account creation)
- Admin panel integration
- Security validation (CSRF, XSS prevention)
- Responsive design testing
- Performance benchmarks
## Remote Deployment
- Website plugin folder is mounted locally via sshfs at /home/jknapp/remote-sftp
- Live site hosted at `https://streamers.channel`
- Test admin user: `playwright` (credentials in test files)
## Important Patterns
### Error Handling
Extensive error logging throughout with descriptive messages for missing classes/files.
If you find a bug or error, you should fix it in the code, deploy the changes, and test again. You should continue the process until the issue is fixed. Once you fix the issue, continue testing.
### Hook System
Uses WordPress actions/filters:
- `wpdd_order_completed` - Post-purchase processing
- `wpdd_download_started` - Pre-download hooks
- `wpdd_customer_registered` - New customer events
### Search Integration
Plugin automatically includes `wpdd_product` post type in WordPress search results via `pre_get_posts` filter.
### Website Current Status
- The site is currently hooked up to PayPal Sandbox
- The website has some test products
- One of the products is a free image and gets watermarked
- One is a PDF for $10 and should also be watermarked
### PayPal Sandbox Customer Credentials
These are sandbox only PayPal Test account. The credentials should be used to test the purchase process.
- sb-a7cpw45634739@personal.example.com
- 3[I$ppb?

219
README.md Normal file
View File

@@ -0,0 +1,219 @@
# WP Digital Download
A comprehensive WordPress plugin for creating a digital download marketplace where creators can sell digital products with PayPal integration.
## Features
### Core Functionality
- **Custom Post Type**: Products with rich metadata
- **User Roles**: Separate roles for customers and creators
- **File Management**: Secure file upload and protection
- **PayPal Integration**: Complete payment processing
- **Download Protection**: Secure, time-limited downloads
- **Watermarking**: Automatic watermarking for images and PDFs
- **Purchase History**: Customer account management
- **Admin Dashboard**: Complete order and sales management
### Key Components
#### Custom Post Types
- **wpdd_product**: Digital products with pricing, files, and settings
- **Product Categories & Tags**: Organization and filtering
- **Creator Attribution**: Products linked to their creators
#### User Management
- **Digital Customer Role**: Purchase and download permissions
- **Digital Creator Role**: Product management permissions
- **Admin Capabilities**: Full system management
#### Payment Processing
- **PayPal Integration**: Sandbox and live modes
- **Free Products**: No-payment downloads
- **Guest Checkout**: Optional account creation
- **Order Management**: Complete transaction tracking
#### File Protection
- **Protected Directory**: Files stored outside web root access
- **Secure Downloads**: Token-based download links
- **Download Limits**: Configurable per product
- **Expiry Dates**: Time-limited access
#### Watermarking
- **Image Watermarking**: PNG, JPG, GIF support
- **PDF Watermarking**: Text overlay on PDF files
- **Dynamic Content**: Customer info in watermarks
- **Configurable Settings**: Per-product control
## Installation
1. Upload the plugin files to `/wp-content/plugins/wp-digital-download/`
2. Activate the plugin through the WordPress admin
3. Configure PayPal settings in Digital Products > Settings
4. Create your first product
5. Add the shop shortcode `[wpdd_shop]` to a page
## Configuration
### PayPal Setup
1. Create a PayPal Developer account
2. Create a new application
3. Get Client ID and Secret
4. Configure in Settings > PayPal Settings
### Pages Setup
The plugin automatically creates these pages:
- **Shop**: `[wpdd_shop]` - Product listing
- **Checkout**: `[wpdd_checkout]` - Payment processing
- **My Purchases**: `[wpdd_customer_purchases]` - Customer downloads
- **Thank You**: `[wpdd_thank_you]` - Order confirmation
## Shortcodes
### Main Shortcodes
- `[wpdd_shop]` - Display product storefront
- `[wpdd_customer_purchases]` - Customer purchase history
- `[wpdd_checkout]` - Checkout form
- `[wpdd_thank_you]` - Thank you page
- `[wpdd_product id="123"]` - Single product display
### Shop Shortcode Options
```
[wpdd_shop columns="3" per_page="12" category="design" orderby="date" order="DESC" show_filters="yes"]
```
## Database Structure
### Tables Created
- `wp_wpdd_orders` - Purchase records
- `wp_wpdd_downloads` - Download tracking
- `wp_wpdd_download_links` - Secure download tokens
### Key Meta Fields
- `_wpdd_price` - Product price
- `_wpdd_sale_price` - Sale price
- `_wpdd_is_free` - Free product flag
- `_wpdd_files` - Associated files
- `_wpdd_download_limit` - Download restrictions
- `_wpdd_download_expiry` - Access duration
- `_wpdd_enable_watermark` - Watermark settings
- `_wpdd_sales_count` - Sales tracking
## File Structure
```
wp-digital-download/
├── wp-digital-download.php # Main plugin file
├── includes/ # Core functionality
│ ├── class-wpdd-install.php # Installation routines
│ ├── class-wpdd-post-types.php # Custom post types
│ ├── class-wpdd-roles.php # User roles
│ ├── class-wpdd-metaboxes.php # Product editing
│ ├── class-wpdd-shortcodes.php # Frontend display
│ ├── class-wpdd-paypal.php # Payment processing
│ ├── class-wpdd-download-handler.php # File delivery
│ ├── class-wpdd-customer.php # Customer management
│ ├── class-wpdd-orders.php # Order processing
│ ├── class-wpdd-file-protection.php # Security
│ ├── class-wpdd-watermark.php # Image processing
│ └── class-wpdd-ajax.php # AJAX handlers
├── admin/ # Admin interface
│ ├── class-wpdd-admin.php # Admin pages
│ └── class-wpdd-settings.php # Configuration
└── assets/ # Frontend resources
├── css/
│ ├── frontend.css # Public styling
│ └── admin.css # Admin styling
└── js/
├── frontend.js # Public scripts
├── admin.js # Admin scripts
└── paypal.js # Payment integration
```
## Hooks & Filters
### Actions
- `wpdd_order_completed` - Fired when order is completed
- `wpdd_download_started` - Before file delivery
- `wpdd_customer_registered` - New customer account
### Filters
- `wpdd_product_price` - Modify displayed price
- `wpdd_download_url` - Customize download URLs
- `wpdd_watermark_text` - Modify watermark content
- `wpdd_email_content` - Customize email templates
## Security Features
### File Protection
- Files stored in protected directory with .htaccess rules
- Token-based download authentication
- Time-limited access links
- IP and user agent logging
### Input Validation
- All user inputs sanitized
- Nonce verification on forms
- Capability checks for admin functions
- SQL injection prevention
### Payment Security
- PayPal webhook verification
- Transaction ID tracking
- Duplicate payment prevention
- Secure credential storage
## Customization
### Template Override
Create templates in your theme:
```
your-theme/
└── wpdd-templates/
├── single-product.php
├── shop.php
└── checkout.php
```
### CSS Customization
Override default styles:
```css
.wpdd-product-card {
/* Your custom styles */
}
```
### Adding Payment Methods
Extend payment options by hooking into:
```php
add_action('wpdd_payment_methods', 'add_stripe_payment');
```
## Requirements
- WordPress 5.0+
- PHP 7.4+
- MySQL 5.6+
- cURL extension
- GD library (for watermarking)
## Changelog
### Version 1.0.0
- Initial release
- PayPal integration
- File protection system
- Watermarking capabilities
- Customer management
- Admin dashboard
## Support
For support and feature requests, please create an issue in the GitHub repository.
## License
This plugin is licensed under the GPL v2 or later.
---
**Note**: This plugin handles financial transactions. Always test thoroughly in a development environment before deploying to production.

View File

@@ -0,0 +1,435 @@
<?php
if (!defined('ABSPATH')) {
exit;
}
class WPDD_Admin_Payouts {
public static function init() {
add_action('admin_menu', array(__CLASS__, 'add_menu_page'));
add_action('admin_post_wpdd_process_payout', array(__CLASS__, 'process_payout'));
add_action('admin_post_wpdd_bulk_payouts', array(__CLASS__, 'process_bulk_payouts'));
add_action('admin_enqueue_scripts', array(__CLASS__, 'enqueue_scripts'));
}
public static function add_menu_page() {
add_submenu_page(
'edit.php?post_type=wpdd_product',
__('Creator Payouts', 'wp-digital-download'),
__('Payouts', 'wp-digital-download'),
'manage_options',
'wpdd-payouts',
array(__CLASS__, 'render_page')
);
}
public static function enqueue_scripts($hook) {
if ($hook !== 'wpdd_product_page_wpdd-payouts') {
return;
}
wp_enqueue_script('wpdd-admin-payouts', WPDD_PLUGIN_URL . 'assets/js/admin-payouts.js', array('jquery'), WPDD_VERSION, true);
wp_localize_script('wpdd-admin-payouts', 'wpdd_payouts', array(
'ajax_url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('wpdd_payouts'),
'confirm_payout' => __('Are you sure you want to process this payout?', 'wp-digital-download'),
'confirm_bulk' => __('Are you sure you want to process all selected payouts?', 'wp-digital-download')
));
}
public static function render_page() {
global $wpdb;
// Get filter parameters
$status_filter = isset($_GET['status']) ? sanitize_text_field($_GET['status']) : 'pending';
$creator_filter = isset($_GET['creator']) ? intval($_GET['creator']) : 0;
// Get creators with balance
$creators = WPDD_Creator::get_creators_with_balance();
$currency = get_option('wpdd_currency', 'USD');
$threshold = floatval(get_option('wpdd_payout_threshold', 0));
// Get payout history
$query = "SELECT p.*, u.display_name, u.user_email
FROM {$wpdb->prefix}wpdd_payouts p
INNER JOIN {$wpdb->users} u ON p.creator_id = u.ID
WHERE 1=1";
if ($status_filter && $status_filter !== 'all') {
$query .= $wpdb->prepare(" AND p.status = %s", $status_filter);
}
if ($creator_filter) {
$query .= $wpdb->prepare(" AND p.creator_id = %d", $creator_filter);
}
$query .= " ORDER BY p.created_at DESC LIMIT 100";
$payouts = $wpdb->get_results($query);
?>
<div class="wrap">
<h1><?php _e('Creator Payouts', 'wp-digital-download'); ?></h1>
<?php if (isset($_GET['message'])) : ?>
<?php if ($_GET['message'] === 'success') : ?>
<div class="notice notice-success is-dismissible">
<p><?php _e('Payout processed successfully.', 'wp-digital-download'); ?></p>
</div>
<?php elseif ($_GET['message'] === 'error') : ?>
<div class="notice notice-error is-dismissible">
<p><?php _e('Error processing payout. Please try again.', 'wp-digital-download'); ?></p>
</div>
<?php endif; ?>
<?php endif; ?>
<div class="wpdd-payout-stats">
<h2><?php _e('Pending Payouts', 'wp-digital-download'); ?></h2>
<?php if (!empty($creators)) : ?>
<form method="post" action="<?php echo admin_url('admin-post.php'); ?>">
<input type="hidden" name="action" value="wpdd_bulk_payouts">
<?php wp_nonce_field('wpdd_bulk_payouts', 'wpdd_nonce'); ?>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th class="check-column">
<input type="checkbox" id="select-all-creators">
</th>
<th><?php _e('Creator', 'wp-digital-download'); ?></th>
<th><?php _e('PayPal Email', 'wp-digital-download'); ?></th>
<th><?php _e('Current Balance', 'wp-digital-download'); ?></th>
<th><?php _e('Total Sales', 'wp-digital-download'); ?></th>
<th><?php _e('Net Earnings', 'wp-digital-download'); ?></th>
<th><?php _e('Actions', 'wp-digital-download'); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($creators as $creator) :
$total_earnings = WPDD_Creator::get_creator_total_earnings($creator->ID);
$net_earnings = WPDD_Creator::get_creator_net_earnings($creator->ID);
$can_payout = !empty($creator->paypal_email) && floatval($creator->balance) > 0;
$auto_eligible = $threshold > 0 && floatval($creator->balance) >= $threshold;
?>
<tr <?php echo $auto_eligible ? 'style="background-color: #d4edda;"' : ''; ?>>
<td>
<?php if ($can_payout) : ?>
<input type="checkbox" name="creator_ids[]" value="<?php echo esc_attr($creator->ID); ?>">
<?php endif; ?>
</td>
<td>
<strong><?php echo esc_html($creator->display_name); ?></strong><br>
<small><?php echo esc_html($creator->user_email); ?></small>
</td>
<td>
<?php if (!empty($creator->paypal_email)) : ?>
<?php echo esc_html($creator->paypal_email); ?>
<?php else : ?>
<span style="color: #dc3545;"><?php _e('Not set', 'wp-digital-download'); ?></span>
<?php endif; ?>
</td>
<td>
<strong><?php echo wpdd_format_price($creator->balance, $currency); ?></strong>
<?php if ($auto_eligible) : ?>
<br><span class="dashicons dashicons-yes" style="color: #28a745;"></span>
<small><?php _e('Auto-payout eligible', 'wp-digital-download'); ?></small>
<?php endif; ?>
</td>
<td><?php echo wpdd_format_price($total_earnings, $currency); ?></td>
<td><?php echo wpdd_format_price($net_earnings, $currency); ?></td>
<td>
<?php if ($can_payout) : ?>
<form method="post" action="<?php echo admin_url('admin-post.php'); ?>" style="display: inline;">
<input type="hidden" name="action" value="wpdd_process_payout">
<input type="hidden" name="creator_id" value="<?php echo esc_attr($creator->ID); ?>">
<?php wp_nonce_field('wpdd_process_payout_' . $creator->ID, 'wpdd_nonce'); ?>
<button type="submit" class="button button-primary wpdd-payout-btn">
<?php _e('Process Payout', 'wp-digital-download'); ?>
</button>
</form>
<?php else : ?>
<button class="button" disabled><?php _e('Cannot Process', 'wp-digital-download'); ?></button>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<div class="tablenav bottom">
<div class="alignleft actions">
<button type="submit" class="button button-primary" name="bulk_action" value="process">
<?php _e('Process Selected Payouts', 'wp-digital-download'); ?>
</button>
<?php if ($threshold > 0) : ?>
<button type="submit" class="button" name="bulk_action" value="auto">
<?php printf(__('Process All Above %s', 'wp-digital-download'), wpdd_format_price($threshold, $currency)); ?>
</button>
<?php endif; ?>
</div>
</div>
</form>
<?php else : ?>
<p><?php _e('No creators with pending payouts.', 'wp-digital-download'); ?></p>
<?php endif; ?>
</div>
<div class="wpdd-payout-history">
<h2><?php _e('Payout History', 'wp-digital-download'); ?></h2>
<div class="tablenav top">
<div class="alignleft actions">
<select name="status_filter" onchange="window.location.href='<?php echo admin_url('edit.php?post_type=wpdd_product&page=wpdd-payouts'); ?>&status=' + this.value">
<option value="all" <?php selected($status_filter, 'all'); ?>><?php _e('All Statuses', 'wp-digital-download'); ?></option>
<option value="pending" <?php selected($status_filter, 'pending'); ?>><?php _e('Pending', 'wp-digital-download'); ?></option>
<option value="completed" <?php selected($status_filter, 'completed'); ?>><?php _e('Completed', 'wp-digital-download'); ?></option>
<option value="failed" <?php selected($status_filter, 'failed'); ?>><?php _e('Failed', 'wp-digital-download'); ?></option>
</select>
</div>
</div>
<?php if (!empty($payouts)) : ?>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th><?php _e('Date', 'wp-digital-download'); ?></th>
<th><?php _e('Creator', 'wp-digital-download'); ?></th>
<th><?php _e('Amount', 'wp-digital-download'); ?></th>
<th><?php _e('PayPal Email', 'wp-digital-download'); ?></th>
<th><?php _e('Transaction ID', 'wp-digital-download'); ?></th>
<th><?php _e('Status', 'wp-digital-download'); ?></th>
<th><?php _e('Method', 'wp-digital-download'); ?></th>
<th><?php _e('Processed By', 'wp-digital-download'); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($payouts as $payout) :
$processor = $payout->processed_by ? get_userdata($payout->processed_by) : null;
?>
<tr>
<td>
<?php echo esc_html(date_i18n(get_option('date_format') . ' ' . get_option('time_format'), strtotime($payout->created_at))); ?>
<?php if ($payout->processed_at) : ?>
<br><small><?php _e('Processed:', 'wp-digital-download'); ?> <?php echo esc_html(date_i18n(get_option('date_format'), strtotime($payout->processed_at))); ?></small>
<?php endif; ?>
</td>
<td>
<strong><?php echo esc_html($payout->display_name); ?></strong><br>
<small><?php echo esc_html($payout->user_email); ?></small>
</td>
<td><strong><?php echo wpdd_format_price($payout->amount, $payout->currency); ?></strong></td>
<td><?php echo esc_html($payout->paypal_email); ?></td>
<td>
<?php echo esc_html($payout->transaction_id ?: '-'); ?>
<?php if ($payout->notes) : ?>
<br><small><?php echo esc_html($payout->notes); ?></small>
<?php endif; ?>
</td>
<td>
<?php
$status_class = '';
switch($payout->status) {
case 'completed':
$status_class = 'notice-success';
break;
case 'failed':
$status_class = 'notice-error';
break;
case 'pending':
$status_class = 'notice-warning';
break;
}
?>
<span class="notice <?php echo esc_attr($status_class); ?>" style="padding: 2px 8px; display: inline-block;">
<?php echo esc_html(ucfirst($payout->status)); ?>
</span>
</td>
<td><?php echo esc_html(ucfirst($payout->payout_method)); ?></td>
<td>
<?php if ($processor) : ?>
<?php echo esc_html($processor->display_name); ?>
<?php else : ?>
<?php echo $payout->payout_method === 'automatic' ? __('System', 'wp-digital-download') : '-'; ?>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else : ?>
<p><?php _e('No payout history found.', 'wp-digital-download'); ?></p>
<?php endif; ?>
</div>
<style>
.wpdd-payout-stats,
.wpdd-payout-history {
margin-top: 30px;
background: #fff;
padding: 20px;
border: 1px solid #ccd0d4;
box-shadow: 0 1px 1px rgba(0,0,0,.04);
}
.wpdd-payout-btn:hover {
cursor: pointer;
}
#select-all-creators {
margin: 0;
}
</style>
<script>
jQuery(document).ready(function($) {
$('#select-all-creators').on('change', function() {
$('input[name="creator_ids[]"]').prop('checked', this.checked);
});
$('.wpdd-payout-btn').on('click', function(e) {
if (!confirm(wpdd_payouts.confirm_payout)) {
e.preventDefault();
}
});
$('button[name="bulk_action"]').on('click', function(e) {
var checkedCount = $('input[name="creator_ids[]"]:checked').length;
if (checkedCount === 0) {
alert('Please select at least one creator for payout.');
e.preventDefault();
} else if (!confirm(wpdd_payouts.confirm_bulk)) {
e.preventDefault();
}
});
});
</script>
</div>
<?php
}
public static function process_payout() {
if (!current_user_can('manage_options')) {
wp_die(__('You do not have permission to perform this action.', 'wp-digital-download'));
}
$creator_id = isset($_POST['creator_id']) ? intval($_POST['creator_id']) : 0;
if (!$creator_id || !wp_verify_nonce($_POST['wpdd_nonce'], 'wpdd_process_payout_' . $creator_id)) {
wp_redirect(admin_url('edit.php?post_type=wpdd_product&page=wpdd-payouts&message=error'));
exit;
}
$result = self::create_payout($creator_id);
if ($result) {
wp_redirect(admin_url('edit.php?post_type=wpdd_product&page=wpdd-payouts&message=success'));
} else {
wp_redirect(admin_url('edit.php?post_type=wpdd_product&page=wpdd-payouts&message=error'));
}
exit;
}
public static function process_bulk_payouts() {
if (!current_user_can('manage_options')) {
wp_die(__('You do not have permission to perform this action.', 'wp-digital-download'));
}
if (!wp_verify_nonce($_POST['wpdd_nonce'], 'wpdd_bulk_payouts')) {
wp_redirect(admin_url('edit.php?post_type=wpdd_product&page=wpdd-payouts&message=error'));
exit;
}
$bulk_action = isset($_POST['bulk_action']) ? sanitize_text_field($_POST['bulk_action']) : '';
if ($bulk_action === 'auto') {
// Process all creators above threshold
$threshold = floatval(get_option('wpdd_payout_threshold', 0));
if ($threshold > 0) {
$creators = WPDD_Creator::get_creators_with_balance();
foreach ($creators as $creator) {
if (floatval($creator->balance) >= $threshold && !empty($creator->paypal_email)) {
self::create_payout($creator->ID);
}
}
}
} else {
// Process selected creators
$creator_ids = isset($_POST['creator_ids']) ? array_map('intval', $_POST['creator_ids']) : array();
foreach ($creator_ids as $creator_id) {
self::create_payout($creator_id);
}
}
wp_redirect(admin_url('edit.php?post_type=wpdd_product&page=wpdd-payouts&message=success'));
exit;
}
public static function create_payout($creator_id, $method = 'manual') {
global $wpdb;
$balance = WPDD_Creator::get_creator_balance($creator_id);
$paypal_email = get_user_meta($creator_id, 'wpdd_paypal_email', true);
if ($balance <= 0 || empty($paypal_email)) {
return false;
}
$currency = get_option('wpdd_currency', 'USD');
$current_user_id = get_current_user_id();
// Create payout record
$wpdb->insert(
$wpdb->prefix . 'wpdd_payouts',
array(
'creator_id' => $creator_id,
'amount' => $balance,
'currency' => $currency,
'paypal_email' => $paypal_email,
'status' => 'pending',
'payout_method' => $method,
'processed_by' => $current_user_id,
'created_at' => current_time('mysql')
),
array('%d', '%f', '%s', '%s', '%s', '%s', '%d', '%s')
);
$payout_id = $wpdb->insert_id;
// Try to process via PayPal API
$result = WPDD_PayPal_Payouts::process_payout($payout_id);
if ($result['success']) {
// Update payout status
$wpdb->update(
$wpdb->prefix . 'wpdd_payouts',
array(
'status' => 'completed',
'transaction_id' => $result['transaction_id'],
'processed_at' => current_time('mysql')
),
array('id' => $payout_id),
array('%s', '%s', '%s'),
array('%d')
);
// Reset creator balance
update_user_meta($creator_id, 'wpdd_creator_balance', 0);
return true;
} else {
// Update with error
$wpdb->update(
$wpdb->prefix . 'wpdd_payouts',
array(
'status' => 'failed',
'notes' => $result['error']
),
array('id' => $payout_id),
array('%s', '%s'),
array('%d')
);
return false;
}
}
}

920
admin/class-wpdd-admin.php Normal file
View File

@@ -0,0 +1,920 @@
<?php
if (!defined('ABSPATH')) {
exit;
}
class WPDD_Admin {
public static function init() {
add_action('admin_menu', array(__CLASS__, 'add_admin_menus'));
add_filter('manage_wpdd_product_posts_columns', array(__CLASS__, 'add_product_columns'));
add_action('manage_wpdd_product_posts_custom_column', array(__CLASS__, 'render_product_columns'), 10, 2);
add_filter('manage_edit-wpdd_product_sortable_columns', array(__CLASS__, 'make_columns_sortable'));
add_action('pre_get_posts', array(__CLASS__, 'sort_products_by_column'));
add_action('admin_init', array(__CLASS__, 'handle_admin_actions'));
// Initialize admin payouts
if (class_exists('WPDD_Admin_Payouts')) {
WPDD_Admin_Payouts::init();
}
}
public static function add_admin_menus() {
add_submenu_page(
'edit.php?post_type=wpdd_product',
__('Orders', 'wp-digital-download'),
__('Orders', 'wp-digital-download'),
'wpdd_manage_orders',
'wpdd-orders',
array(__CLASS__, 'render_orders_page')
);
add_submenu_page(
'edit.php?post_type=wpdd_product',
__('Reports', 'wp-digital-download'),
__('Reports', 'wp-digital-download'),
'wpdd_view_reports',
'wpdd-reports',
array(__CLASS__, 'render_reports_page')
);
add_submenu_page(
'edit.php?post_type=wpdd_product',
__('Customers', 'wp-digital-download'),
__('Customers', 'wp-digital-download'),
'wpdd_manage_orders',
'wpdd-customers',
array(__CLASS__, 'render_customers_page')
);
add_submenu_page(
'edit.php?post_type=wpdd_product',
__('Shortcodes', 'wp-digital-download'),
__('Shortcodes', 'wp-digital-download'),
'edit_wpdd_products',
'wpdd-shortcodes',
array(__CLASS__, 'render_shortcodes_page')
);
}
public static function add_product_columns($columns) {
$new_columns = array();
foreach ($columns as $key => $value) {
$new_columns[$key] = $value;
if ($key === 'title') {
$new_columns['wpdd_price'] = __('Price', 'wp-digital-download');
$new_columns['wpdd_sales'] = __('Sales', 'wp-digital-download');
$new_columns['wpdd_revenue'] = __('Revenue', 'wp-digital-download');
$new_columns['wpdd_files'] = __('Files', 'wp-digital-download');
}
}
return $new_columns;
}
public static function render_product_columns($column, $post_id) {
switch ($column) {
case 'wpdd_price':
$price = get_post_meta($post_id, '_wpdd_price', true);
$sale_price = get_post_meta($post_id, '_wpdd_sale_price', true);
$is_free = get_post_meta($post_id, '_wpdd_is_free', true);
if ($is_free) {
echo '<span style="color: green;">' . __('Free', 'wp-digital-download') . '</span>';
} elseif ($sale_price && $sale_price < $price) {
echo '<del>$' . number_format($price, 2) . '</del> ';
echo '<strong>$' . number_format($sale_price, 2) . '</strong>';
} else {
echo '$' . number_format($price, 2);
}
break;
case 'wpdd_sales':
global $wpdb;
$sales = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->prefix}wpdd_orders
WHERE product_id = %d AND status = 'completed'",
$post_id
));
echo intval($sales);
break;
case 'wpdd_revenue':
global $wpdb;
$revenue = $wpdb->get_var($wpdb->prepare(
"SELECT SUM(amount) FROM {$wpdb->prefix}wpdd_orders
WHERE product_id = %d AND status = 'completed'",
$post_id
));
echo '$' . number_format($revenue ?: 0, 2);
break;
case 'wpdd_files':
$files = get_post_meta($post_id, '_wpdd_files', true);
$count = is_array($files) ? count($files) : 0;
echo $count;
break;
}
}
public static function make_columns_sortable($columns) {
$columns['wpdd_price'] = 'wpdd_price';
$columns['wpdd_sales'] = 'wpdd_sales';
$columns['wpdd_revenue'] = 'wpdd_revenue';
return $columns;
}
public static function sort_products_by_column($query) {
if (!is_admin() || !$query->is_main_query()) {
return;
}
if ($query->get('post_type') !== 'wpdd_product') {
return;
}
$orderby = $query->get('orderby');
switch ($orderby) {
case 'wpdd_price':
$query->set('meta_key', '_wpdd_price');
$query->set('orderby', 'meta_value_num');
break;
case 'wpdd_sales':
$query->set('meta_key', '_wpdd_sales_count');
$query->set('orderby', 'meta_value_num');
break;
}
}
public static function render_orders_page() {
global $wpdb;
$page = isset($_GET['paged']) ? max(1, intval($_GET['paged'])) : 1;
$per_page = 20;
$offset = ($page - 1) * $per_page;
$where = array('1=1');
if (isset($_GET['status']) && $_GET['status']) {
$where[] = $wpdb->prepare("o.status = %s", sanitize_text_field($_GET['status']));
}
if (isset($_GET['product_id']) && $_GET['product_id']) {
$where[] = $wpdb->prepare("o.product_id = %d", intval($_GET['product_id']));
}
if (isset($_GET['search']) && $_GET['search']) {
$search = '%' . $wpdb->esc_like($_GET['search']) . '%';
$where[] = $wpdb->prepare(
"(o.order_number LIKE %s OR o.customer_email LIKE %s OR o.customer_name LIKE %s)",
$search, $search, $search
);
}
$where_clause = implode(' AND ', $where);
$total = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}wpdd_orders o WHERE {$where_clause}");
$orders = $wpdb->get_results($wpdb->prepare(
"SELECT o.*, p.post_title as product_name
FROM {$wpdb->prefix}wpdd_orders o
LEFT JOIN {$wpdb->posts} p ON o.product_id = p.ID
WHERE {$where_clause}
ORDER BY o.purchase_date DESC
LIMIT %d OFFSET %d",
$per_page,
$offset
));
?>
<div class="wrap">
<h1><?php _e('Orders', 'wp-digital-download'); ?></h1>
<form method="get">
<input type="hidden" name="post_type" value="wpdd_product" />
<input type="hidden" name="page" value="wpdd-orders" />
<div class="tablenav top">
<div class="alignleft actions">
<select name="status">
<option value=""><?php _e('All Statuses', 'wp-digital-download'); ?></option>
<option value="pending" <?php selected(isset($_GET['status']) && $_GET['status'] == 'pending'); ?>>
<?php _e('Pending', 'wp-digital-download'); ?>
</option>
<option value="completed" <?php selected(isset($_GET['status']) && $_GET['status'] == 'completed'); ?>>
<?php _e('Completed', 'wp-digital-download'); ?>
</option>
<option value="failed" <?php selected(isset($_GET['status']) && $_GET['status'] == 'failed'); ?>>
<?php _e('Failed', 'wp-digital-download'); ?>
</option>
</select>
<input type="text" name="search" placeholder="<?php _e('Search orders...', 'wp-digital-download'); ?>"
value="<?php echo isset($_GET['search']) ? esc_attr($_GET['search']) : ''; ?>" />
<input type="submit" class="button" value="<?php _e('Filter', 'wp-digital-download'); ?>" />
</div>
</div>
</form>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th><?php _e('Order', 'wp-digital-download'); ?></th>
<th><?php _e('Product', 'wp-digital-download'); ?></th>
<th><?php _e('Customer', 'wp-digital-download'); ?></th>
<th><?php _e('Amount', 'wp-digital-download'); ?></th>
<th><?php _e('Status', 'wp-digital-download'); ?></th>
<th><?php _e('Date', 'wp-digital-download'); ?></th>
<th><?php _e('Actions', 'wp-digital-download'); ?></th>
</tr>
</thead>
<tbody>
<?php if ($orders) : ?>
<?php foreach ($orders as $order) : ?>
<tr>
<td><strong>#<?php echo esc_html($order->order_number); ?></strong></td>
<td>
<a href="<?php echo get_edit_post_link($order->product_id); ?>">
<?php echo esc_html($order->product_name); ?>
</a>
</td>
<td>
<?php echo esc_html($order->customer_name); ?><br>
<small><?php echo esc_html($order->customer_email); ?></small>
</td>
<td>$<?php echo number_format($order->amount, 2); ?></td>
<td>
<span class="wpdd-status wpdd-status-<?php echo esc_attr($order->status); ?>">
<?php echo ucfirst($order->status); ?>
</span>
</td>
<td><?php echo date_i18n(get_option('date_format'), strtotime($order->purchase_date)); ?></td>
<td>
<a href="<?php echo wp_nonce_url(
add_query_arg(array(
'action' => 'wpdd_view_order',
'order_id' => $order->id
)),
'wpdd_view_order_' . $order->id
); ?>" class="button button-small">
<?php _e('View', 'wp-digital-download'); ?>
</a>
</td>
</tr>
<?php endforeach; ?>
<?php else : ?>
<tr>
<td colspan="7"><?php _e('No orders found.', 'wp-digital-download'); ?></td>
</tr>
<?php endif; ?>
</tbody>
</table>
<?php
$total_pages = ceil($total / $per_page);
if ($total_pages > 1) {
echo '<div class="tablenav bottom"><div class="tablenav-pages">';
echo paginate_links(array(
'base' => add_query_arg('paged', '%#%'),
'format' => '',
'current' => $page,
'total' => $total_pages
));
echo '</div></div>';
}
?>
</div>
<style>
.wpdd-status {
padding: 3px 8px;
border-radius: 3px;
font-size: 12px;
font-weight: 600;
}
.wpdd-status-completed { background: #d4edda; color: #155724; }
.wpdd-status-pending { background: #fff3cd; color: #856404; }
.wpdd-status-failed { background: #f8d7da; color: #721c24; }
</style>
<?php
}
public static function render_reports_page() {
global $wpdb;
$date_range = isset($_GET['range']) ? sanitize_text_field($_GET['range']) : '30days';
switch ($date_range) {
case '7days':
$start_date = date('Y-m-d', strtotime('-7 days'));
break;
case '30days':
$start_date = date('Y-m-d', strtotime('-30 days'));
break;
case '3months':
$start_date = date('Y-m-d', strtotime('-3 months'));
break;
case 'year':
$start_date = date('Y-m-d', strtotime('-1 year'));
break;
default:
$start_date = date('Y-m-d', strtotime('-30 days'));
}
$stats = $wpdb->get_row($wpdb->prepare(
"SELECT
COUNT(*) as total_orders,
SUM(amount) as total_revenue,
COUNT(DISTINCT customer_id) as unique_customers,
COUNT(DISTINCT product_id) as products_sold
FROM {$wpdb->prefix}wpdd_orders
WHERE status = 'completed'
AND purchase_date >= %s",
$start_date
));
$top_products = $wpdb->get_results($wpdb->prepare(
"SELECT
p.ID,
p.post_title,
COUNT(o.id) as sales,
SUM(o.amount) as revenue
FROM {$wpdb->prefix}wpdd_orders o
INNER JOIN {$wpdb->posts} p ON o.product_id = p.ID
WHERE o.status = 'completed'
AND o.purchase_date >= %s
GROUP BY p.ID
ORDER BY revenue DESC
LIMIT 10",
$start_date
));
$top_creators = $wpdb->get_results($wpdb->prepare(
"SELECT
u.ID,
u.display_name,
COUNT(o.id) as sales,
SUM(o.amount) as revenue
FROM {$wpdb->prefix}wpdd_orders o
INNER JOIN {$wpdb->users} u ON o.creator_id = u.ID
WHERE o.status = 'completed'
AND o.purchase_date >= %s
GROUP BY u.ID
ORDER BY revenue DESC
LIMIT 10",
$start_date
));
?>
<div class="wrap">
<h1><?php _e('Reports', 'wp-digital-download'); ?></h1>
<div class="wpdd-date-filter">
<form method="get">
<input type="hidden" name="post_type" value="wpdd_product" />
<input type="hidden" name="page" value="wpdd-reports" />
<select name="range" onchange="this.form.submit()">
<option value="7days" <?php selected($date_range, '7days'); ?>>
<?php _e('Last 7 Days', 'wp-digital-download'); ?>
</option>
<option value="30days" <?php selected($date_range, '30days'); ?>>
<?php _e('Last 30 Days', 'wp-digital-download'); ?>
</option>
<option value="3months" <?php selected($date_range, '3months'); ?>>
<?php _e('Last 3 Months', 'wp-digital-download'); ?>
</option>
<option value="year" <?php selected($date_range, 'year'); ?>>
<?php _e('Last Year', 'wp-digital-download'); ?>
</option>
</select>
</form>
</div>
<div class="wpdd-stats-grid">
<div class="wpdd-stat-box">
<h3><?php _e('Total Revenue', 'wp-digital-download'); ?></h3>
<p class="wpdd-stat-value">$<?php echo number_format($stats->total_revenue ?: 0, 2); ?></p>
</div>
<div class="wpdd-stat-box">
<h3><?php _e('Total Orders', 'wp-digital-download'); ?></h3>
<p class="wpdd-stat-value"><?php echo intval($stats->total_orders); ?></p>
</div>
<div class="wpdd-stat-box">
<h3><?php _e('Unique Customers', 'wp-digital-download'); ?></h3>
<p class="wpdd-stat-value"><?php echo intval($stats->unique_customers); ?></p>
</div>
<div class="wpdd-stat-box">
<h3><?php _e('Products Sold', 'wp-digital-download'); ?></h3>
<p class="wpdd-stat-value"><?php echo intval($stats->products_sold); ?></p>
</div>
</div>
<div class="wpdd-reports-tables">
<div class="wpdd-report-section">
<h2><?php _e('Top Products', 'wp-digital-download'); ?></h2>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th><?php _e('Product', 'wp-digital-download'); ?></th>
<th><?php _e('Sales', 'wp-digital-download'); ?></th>
<th><?php _e('Revenue', 'wp-digital-download'); ?></th>
</tr>
</thead>
<tbody>
<?php if ($top_products) : ?>
<?php foreach ($top_products as $product) : ?>
<tr>
<td>
<a href="<?php echo get_edit_post_link($product->ID); ?>">
<?php echo esc_html($product->post_title); ?>
</a>
</td>
<td><?php echo intval($product->sales); ?></td>
<td>$<?php echo number_format($product->revenue, 2); ?></td>
</tr>
<?php endforeach; ?>
<?php else : ?>
<tr>
<td colspan="3"><?php _e('No data available.', 'wp-digital-download'); ?></td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<div class="wpdd-report-section">
<h2><?php _e('Top Creators', 'wp-digital-download'); ?></h2>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th><?php _e('Creator', 'wp-digital-download'); ?></th>
<th><?php _e('Sales', 'wp-digital-download'); ?></th>
<th><?php _e('Revenue', 'wp-digital-download'); ?></th>
</tr>
</thead>
<tbody>
<?php if ($top_creators) : ?>
<?php foreach ($top_creators as $creator) : ?>
<tr>
<td>
<a href="<?php echo get_edit_user_link($creator->ID); ?>">
<?php echo esc_html($creator->display_name); ?>
</a>
</td>
<td><?php echo intval($creator->sales); ?></td>
<td>$<?php echo number_format($creator->revenue, 2); ?></td>
</tr>
<?php endforeach; ?>
<?php else : ?>
<tr>
<td colspan="3"><?php _e('No data available.', 'wp-digital-download'); ?></td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<style>
.wpdd-date-filter {
margin: 20px 0;
}
.wpdd-stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin: 30px 0;
}
.wpdd-stat-box {
background: white;
padding: 20px;
border: 1px solid #ccd0d4;
border-radius: 4px;
}
.wpdd-stat-box h3 {
margin: 0 0 10px 0;
color: #23282d;
}
.wpdd-stat-value {
font-size: 32px;
font-weight: 600;
color: #2271b1;
margin: 0;
}
.wpdd-reports-tables {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
margin-top: 30px;
}
.wpdd-report-section h2 {
margin-bottom: 15px;
}
@media (max-width: 1200px) {
.wpdd-reports-tables {
grid-template-columns: 1fr;
}
}
</style>
<?php
}
public static function render_customers_page() {
global $wpdb;
// Get all users who have made purchases, regardless of their role
$customers = $wpdb->get_results(
"SELECT
u.ID,
u.user_email,
u.display_name,
u.user_registered,
COUNT(o.id) as total_orders,
SUM(o.amount) as total_spent,
MAX(o.purchase_date) as last_order_date
FROM {$wpdb->users} u
INNER JOIN {$wpdb->prefix}wpdd_orders o ON u.ID = o.customer_id AND o.status = 'completed'
GROUP BY u.ID
ORDER BY total_spent DESC"
);
?>
<div class="wrap">
<h1><?php _e('Customers', 'wp-digital-download'); ?></h1>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th><?php _e('Customer', 'wp-digital-download'); ?></th>
<th><?php _e('Email', 'wp-digital-download'); ?></th>
<th><?php _e('Orders', 'wp-digital-download'); ?></th>
<th><?php _e('Total Spent', 'wp-digital-download'); ?></th>
<th><?php _e('Registered', 'wp-digital-download'); ?></th>
<th><?php _e('Last Order', 'wp-digital-download'); ?></th>
<th><?php _e('Actions', 'wp-digital-download'); ?></th>
</tr>
</thead>
<tbody>
<?php if ($customers) : ?>
<?php foreach ($customers as $customer) : ?>
<tr>
<td>
<strong><?php echo esc_html($customer->display_name); ?></strong>
</td>
<td><?php echo esc_html($customer->user_email); ?></td>
<td><?php echo intval($customer->total_orders); ?></td>
<td>$<?php echo number_format($customer->total_spent ?: 0, 2); ?></td>
<td><?php echo date_i18n(get_option('date_format'), strtotime($customer->user_registered)); ?></td>
<td>
<?php
echo $customer->last_order_date
? date_i18n(get_option('date_format'), strtotime($customer->last_order_date))
: '-';
?>
</td>
<td>
<a href="<?php echo get_edit_user_link($customer->ID); ?>" class="button button-small">
<?php _e('Edit', 'wp-digital-download'); ?>
</a>
<a href="<?php echo add_query_arg(array(
'post_type' => 'wpdd_product',
'page' => 'wpdd-orders',
'customer_id' => $customer->ID
), admin_url('edit.php')); ?>" class="button button-small">
<?php _e('View Orders', 'wp-digital-download'); ?>
</a>
</td>
</tr>
<?php endforeach; ?>
<?php else : ?>
<tr>
<td colspan="7"><?php _e('No customers found.', 'wp-digital-download'); ?></td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<?php
}
public static function handle_admin_actions() {
if (isset($_GET['action']) && $_GET['action'] === 'wpdd_view_order') {
if (!isset($_GET['order_id']) || !wp_verify_nonce($_GET['_wpnonce'] ?? '', 'wpdd_view_order_' . $_GET['order_id'])) {
return;
}
self::view_order_details(intval($_GET['order_id']));
}
}
private static function view_order_details($order_id) {
global $wpdb;
$order = $wpdb->get_row($wpdb->prepare(
"SELECT o.*, p.post_title as product_name
FROM {$wpdb->prefix}wpdd_orders o
LEFT JOIN {$wpdb->posts} p ON o.product_id = p.ID
WHERE o.id = %d",
$order_id
));
if (!$order) {
wp_die(__('Order not found.', 'wp-digital-download'));
}
include WPDD_PLUGIN_PATH . 'admin/views/order-details.php';
exit;
}
public static function render_shortcodes_page() {
?>
<div class="wrap">
<h1><?php _e('Available Shortcodes', 'wp-digital-download'); ?></h1>
<div class="wpdd-shortcodes-intro">
<p><?php _e('Use these shortcodes to display digital download content on your pages and posts. Simply copy and paste the shortcode into any page or post editor.', 'wp-digital-download'); ?></p>
</div>
<div class="wpdd-shortcodes-grid">
<!-- Shop Shortcode -->
<div class="wpdd-shortcode-card">
<h3><?php _e('Shop Page', 'wp-digital-download'); ?></h3>
<div class="wpdd-shortcode-example">
<code>[wpdd_shop]</code>
</div>
<p><?php _e('Displays a grid of all available products with filtering and pagination.', 'wp-digital-download'); ?></p>
<h4><?php _e('Available Parameters:', 'wp-digital-download'); ?></h4>
<ul class="wpdd-params-list">
<li><strong>posts_per_page</strong> - Number of products per page (default: 12)</li>
<li><strong>columns</strong> - Grid columns (default: 3, options: 1-6)</li>
<li><strong>orderby</strong> - Sort order (date, title, price, menu_order)</li>
<li><strong>order</strong> - ASC or DESC (default: DESC)</li>
<li><strong>category</strong> - Show only specific categories (comma separated slugs)</li>
<li><strong>show_filters</strong> - Show search/filter form (yes/no, default: yes)</li>
</ul>
<h4><?php _e('Examples:', 'wp-digital-download'); ?></h4>
<div class="wpdd-shortcode-examples">
<code>[wpdd_shop posts_per_page="6" columns="2"]</code><br>
<code>[wpdd_shop category="music,videos" show_filters="no"]</code><br>
<code>[wpdd_shop orderby="price" order="ASC" columns="4"]</code>
</div>
</div>
<!-- Checkout Shortcode -->
<div class="wpdd-shortcode-card">
<h3><?php _e('Checkout Page', 'wp-digital-download'); ?></h3>
<div class="wpdd-shortcode-example">
<code>[wpdd_checkout]</code>
</div>
<p><?php _e('Displays the checkout form for purchasing products. Typically used on a dedicated checkout page.', 'wp-digital-download'); ?></p>
<p class="wpdd-note"><?php _e('Note: This shortcode automatically detects the product to purchase from the URL parameter.', 'wp-digital-download'); ?></p>
</div>
<!-- Customer Purchases Shortcode -->
<div class="wpdd-shortcode-card">
<h3><?php _e('Customer Purchases', 'wp-digital-download'); ?></h3>
<div class="wpdd-shortcode-example">
<code>[wpdd_customer_purchases]</code>
</div>
<p><?php _e('Shows a table of customer\'s purchase history with download links. Requires user to be logged in.', 'wp-digital-download'); ?></p>
<p class="wpdd-note"><?php _e('Note: This page also supports guest access via email links sent after purchase.', 'wp-digital-download'); ?></p>
</div>
<!-- Thank You Shortcode -->
<div class="wpdd-shortcode-card">
<h3><?php _e('Thank You Page', 'wp-digital-download'); ?></h3>
<div class="wpdd-shortcode-example">
<code>[wpdd_thank_you]</code>
</div>
<p><?php _e('Displays order confirmation and download links after successful purchase. Used on the thank you page.', 'wp-digital-download'); ?></p>
<p class="wpdd-note"><?php _e('Note: This shortcode requires an order_id parameter in the URL.', 'wp-digital-download'); ?></p>
</div>
<!-- Single Product Shortcode -->
<div class="wpdd-shortcode-card">
<h3><?php _e('Single Product', 'wp-digital-download'); ?></h3>
<div class="wpdd-shortcode-example">
<code>[wpdd_single_product id="123"]</code>
</div>
<p><?php _e('Display a single product card anywhere on your site.', 'wp-digital-download'); ?></p>
<h4><?php _e('Parameters:', 'wp-digital-download'); ?></h4>
<ul class="wpdd-params-list">
<li><strong>id</strong> - Product ID (required)</li>
</ul>
<h4><?php _e('Example:', 'wp-digital-download'); ?></h4>
<div class="wpdd-shortcode-examples">
<code>[wpdd_single_product id="456"]</code>
</div>
</div>
<!-- Buy Button Shortcode -->
<div class="wpdd-shortcode-card">
<h3><?php _e('Buy Button', 'wp-digital-download'); ?></h3>
<div class="wpdd-shortcode-example">
<code>[wpdd_buy_button id="123"]</code>
</div>
<p><?php _e('Display just a buy button for a specific product.', 'wp-digital-download'); ?></p>
<h4><?php _e('Parameters:', 'wp-digital-download'); ?></h4>
<ul class="wpdd-params-list">
<li><strong>id</strong> - Product ID (default: current post ID)</li>
<li><strong>text</strong> - Button text (default: "Buy Now")</li>
<li><strong>class</strong> - CSS class for styling</li>
</ul>
<h4><?php _e('Examples:', 'wp-digital-download'); ?></h4>
<div class="wpdd-shortcode-examples">
<code>[wpdd_buy_button id="789" text="Purchase Now"]</code><br>
<code>[wpdd_buy_button text="Get This Product" class="my-custom-button"]</code>
</div>
</div>
</div>
<!-- Page Setup Section -->
<div class="wpdd-page-setup">
<h2><?php _e('Required Pages Setup', 'wp-digital-download'); ?></h2>
<p><?php _e('For the plugin to work correctly, you need these pages with their respective shortcodes:', 'wp-digital-download'); ?></p>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th><?php _e('Page Name', 'wp-digital-download'); ?></th>
<th><?php _e('Shortcode', 'wp-digital-download'); ?></th>
<th><?php _e('Purpose', 'wp-digital-download'); ?></th>
<th><?php _e('Status', 'wp-digital-download'); ?></th>
</tr>
</thead>
<tbody>
<tr>
<td><strong><?php _e('Shop', 'wp-digital-download'); ?></strong></td>
<td><code>[wpdd_shop]</code></td>
<td><?php _e('Main product listing page', 'wp-digital-download'); ?></td>
<td>
<?php
$shop_page_id = get_option('wpdd_shop_page_id');
if ($shop_page_id && get_post($shop_page_id)) {
echo '<span style="color: green;">✓ ' . __('Created', 'wp-digital-download') . '</span>';
echo ' (<a href="' . get_edit_post_link($shop_page_id) . '">' . __('Edit', 'wp-digital-download') . '</a>)';
} else {
echo '<span style="color: red;">✗ ' . __('Missing', 'wp-digital-download') . '</span>';
}
?>
</td>
</tr>
<tr>
<td><strong><?php _e('Checkout', 'wp-digital-download'); ?></strong></td>
<td><code>[wpdd_checkout]</code></td>
<td><?php _e('Purchase processing page', 'wp-digital-download'); ?></td>
<td>
<?php
$checkout_page_id = get_option('wpdd_checkout_page_id');
if ($checkout_page_id && get_post($checkout_page_id)) {
echo '<span style="color: green;">✓ ' . __('Created', 'wp-digital-download') . '</span>';
echo ' (<a href="' . get_edit_post_link($checkout_page_id) . '">' . __('Edit', 'wp-digital-download') . '</a>)';
} else {
echo '<span style="color: red;">✗ ' . __('Missing', 'wp-digital-download') . '</span>';
}
?>
</td>
</tr>
<tr>
<td><strong><?php _e('My Purchases', 'wp-digital-download'); ?></strong></td>
<td><code>[wpdd_customer_purchases]</code></td>
<td><?php _e('Customer purchase history', 'wp-digital-download'); ?></td>
<td>
<?php
$purchases_page_id = get_option('wpdd_purchases_page_id');
if ($purchases_page_id && get_post($purchases_page_id)) {
echo '<span style="color: green;">✓ ' . __('Created', 'wp-digital-download') . '</span>';
echo ' (<a href="' . get_edit_post_link($purchases_page_id) . '">' . __('Edit', 'wp-digital-download') . '</a>)';
} else {
echo '<span style="color: red;">✗ ' . __('Missing', 'wp-digital-download') . '</span>';
}
?>
</td>
</tr>
<tr>
<td><strong><?php _e('Thank You', 'wp-digital-download'); ?></strong></td>
<td><code>[wpdd_thank_you]</code></td>
<td><?php _e('Post-purchase confirmation', 'wp-digital-download'); ?></td>
<td>
<?php
$thank_you_page_id = get_option('wpdd_thank_you_page_id');
if ($thank_you_page_id && get_post($thank_you_page_id)) {
echo '<span style="color: green;">✓ ' . __('Created', 'wp-digital-download') . '</span>';
echo ' (<a href="' . get_edit_post_link($thank_you_page_id) . '">' . __('Edit', 'wp-digital-download') . '</a>)';
} else {
echo '<span style="color: red;">✗ ' . __('Missing', 'wp-digital-download') . '</span>';
}
?>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<style>
.wpdd-shortcodes-intro {
background: #f1f1f1;
padding: 15px;
border-radius: 5px;
margin: 20px 0;
}
.wpdd-shortcodes-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(450px, 1fr));
gap: 20px;
margin: 30px 0;
}
.wpdd-shortcode-card {
background: #fff;
border: 1px solid #ccd0d4;
border-radius: 5px;
padding: 20px;
}
.wpdd-shortcode-card h3 {
margin-top: 0;
color: #23282d;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
}
.wpdd-shortcode-example {
background: #f8f9fa;
padding: 10px;
border-radius: 3px;
margin: 10px 0;
font-family: monospace;
border-left: 4px solid #2271b1;
}
.wpdd-shortcode-examples {
background: #f8f9fa;
padding: 10px;
border-radius: 3px;
margin: 10px 0;
}
.wpdd-shortcode-examples code {
display: block;
margin: 5px 0;
color: #d63384;
}
.wpdd-params-list {
background: #fafafa;
padding: 10px 30px;
border-radius: 3px;
margin: 10px 0;
}
.wpdd-params-list li {
margin: 8px 0;
}
.wpdd-note {
background: #fff3cd;
border: 1px solid #ffeaa7;
color: #856404;
padding: 10px;
border-radius: 3px;
font-style: italic;
}
.wpdd-page-setup {
margin-top: 40px;
padding-top: 30px;
border-top: 2px solid #ddd;
}
.wpdd-page-setup h2 {
color: #23282d;
margin-bottom: 15px;
}
</style>
<?php
}
}

View File

@@ -0,0 +1,682 @@
<?php
if (!defined('ABSPATH')) {
exit;
}
class WPDD_Settings {
public static function init() {
add_action('admin_menu', array(__CLASS__, 'add_settings_page'));
add_action('admin_init', array(__CLASS__, 'register_settings'));
}
public static function add_settings_page() {
add_submenu_page(
'edit.php?post_type=wpdd_product',
__('Settings', 'wp-digital-download'),
__('Settings', 'wp-digital-download'),
'wpdd_manage_settings',
'wpdd-settings',
array(__CLASS__, 'render_settings_page')
);
}
public static function register_settings() {
register_setting('wpdd_settings', 'wpdd_paypal_mode');
register_setting('wpdd_settings', 'wpdd_paypal_client_id');
register_setting('wpdd_settings', 'wpdd_paypal_secret');
register_setting('wpdd_settings', 'wpdd_admin_email');
register_setting('wpdd_settings', 'wpdd_from_name');
register_setting('wpdd_settings', 'wpdd_from_email');
register_setting('wpdd_settings', 'wpdd_currency');
register_setting('wpdd_settings', 'wpdd_enable_guest_checkout');
register_setting('wpdd_settings', 'wpdd_default_download_limit');
register_setting('wpdd_settings', 'wpdd_default_download_expiry');
register_setting('wpdd_settings', 'wpdd_enable_watermark');
register_setting('wpdd_settings', 'wpdd_watermark_text');
register_setting('wpdd_settings', 'wpdd_terms_page');
register_setting('wpdd_settings', 'wpdd_privacy_page');
register_setting('wpdd_settings', 'wpdd_commission_rate', array(
'sanitize_callback' => array(__CLASS__, 'sanitize_commission_rate')
));
register_setting('wpdd_settings', 'wpdd_payout_threshold', array(
'sanitize_callback' => array(__CLASS__, 'sanitize_payout_threshold')
));
register_setting('wpdd_settings', 'wpdd_file_access_method');
register_setting('wpdd_settings', 'wpdd_disable_admin_bar');
add_settings_section(
'wpdd_general_settings',
__('General Settings', 'wp-digital-download'),
array(__CLASS__, 'general_section_callback'),
'wpdd_settings'
);
add_settings_section(
'wpdd_paypal_settings',
__('PayPal Settings', 'wp-digital-download'),
array(__CLASS__, 'paypal_section_callback'),
'wpdd_settings'
);
add_settings_section(
'wpdd_email_settings',
__('Email Settings', 'wp-digital-download'),
array(__CLASS__, 'email_section_callback'),
'wpdd_settings'
);
add_settings_section(
'wpdd_download_settings',
__('Download Settings', 'wp-digital-download'),
array(__CLASS__, 'download_section_callback'),
'wpdd_settings'
);
add_settings_section(
'wpdd_watermark_settings',
__('Watermark Settings', 'wp-digital-download'),
array(__CLASS__, 'watermark_section_callback'),
'wpdd_settings'
);
self::add_general_fields();
self::add_paypal_fields();
self::add_email_fields();
self::add_download_fields();
self::add_watermark_fields();
}
private static function add_general_fields() {
add_settings_field(
'wpdd_currency',
__('Currency', 'wp-digital-download'),
array(__CLASS__, 'currency_field'),
'wpdd_settings',
'wpdd_general_settings',
array(
'name' => 'wpdd_currency'
)
);
add_settings_field(
'wpdd_enable_guest_checkout',
__('Guest Checkout', 'wp-digital-download'),
array(__CLASS__, 'checkbox_field'),
'wpdd_settings',
'wpdd_general_settings',
array(
'name' => 'wpdd_enable_guest_checkout',
'label' => __('Allow guest customers to purchase without creating an account', 'wp-digital-download')
)
);
add_settings_field(
'wpdd_commission_rate',
__('Platform Commission Rate (%)', 'wp-digital-download'),
array(__CLASS__, 'number_field'),
'wpdd_settings',
'wpdd_general_settings',
array(
'name' => 'wpdd_commission_rate',
'description' => __('Platform commission rate from sales (0-100). Creators receive the remainder.', 'wp-digital-download'),
'min' => 0,
'max' => 100,
'step' => 0.01
)
);
add_settings_field(
'wpdd_payout_threshold',
__('Automatic Payout Threshold ($)', 'wp-digital-download'),
array(__CLASS__, 'number_field'),
'wpdd_settings',
'wpdd_general_settings',
array(
'name' => 'wpdd_payout_threshold',
'description' => __('Minimum balance for automatic payouts (0 to disable)', 'wp-digital-download'),
'min' => 0,
'step' => 0.01
)
);
add_settings_field(
'wpdd_terms_page',
__('Terms & Conditions Page', 'wp-digital-download'),
array(__CLASS__, 'page_dropdown_field'),
'wpdd_settings',
'wpdd_general_settings',
array('name' => 'wpdd_terms_page')
);
add_settings_field(
'wpdd_privacy_page',
__('Privacy Policy Page', 'wp-digital-download'),
array(__CLASS__, 'page_dropdown_field'),
'wpdd_settings',
'wpdd_general_settings',
array('name' => 'wpdd_privacy_page')
);
}
private static function add_paypal_fields() {
add_settings_field(
'wpdd_paypal_mode',
__('PayPal Mode', 'wp-digital-download'),
array(__CLASS__, 'select_field'),
'wpdd_settings',
'wpdd_paypal_settings',
array(
'name' => 'wpdd_paypal_mode',
'options' => array(
'sandbox' => __('Sandbox (Testing)', 'wp-digital-download'),
'live' => __('Live (Production)', 'wp-digital-download')
)
)
);
add_settings_field(
'wpdd_paypal_client_id',
__('PayPal Client ID', 'wp-digital-download'),
array(__CLASS__, 'text_field'),
'wpdd_settings',
'wpdd_paypal_settings',
array('name' => 'wpdd_paypal_client_id')
);
add_settings_field(
'wpdd_paypal_secret',
__('PayPal Secret', 'wp-digital-download'),
array(__CLASS__, 'password_field'),
'wpdd_settings',
'wpdd_paypal_settings',
array('name' => 'wpdd_paypal_secret')
);
}
private static function add_email_fields() {
add_settings_field(
'wpdd_admin_email',
__('Admin Email', 'wp-digital-download'),
array(__CLASS__, 'email_field'),
'wpdd_settings',
'wpdd_email_settings',
array(
'name' => 'wpdd_admin_email',
'description' => __('Email address for admin notifications', 'wp-digital-download')
)
);
add_settings_field(
'wpdd_from_name',
__('From Name', 'wp-digital-download'),
array(__CLASS__, 'text_field'),
'wpdd_settings',
'wpdd_email_settings',
array(
'name' => 'wpdd_from_name',
'description' => __('Name shown in email headers', 'wp-digital-download')
)
);
add_settings_field(
'wpdd_from_email',
__('From Email', 'wp-digital-download'),
array(__CLASS__, 'email_field'),
'wpdd_settings',
'wpdd_email_settings',
array(
'name' => 'wpdd_from_email',
'description' => __('Email address shown in email headers', 'wp-digital-download')
)
);
}
private static function add_download_fields() {
add_settings_field(
'wpdd_default_download_limit',
__('Default Download Limit', 'wp-digital-download'),
array(__CLASS__, 'number_field'),
'wpdd_settings',
'wpdd_download_settings',
array(
'name' => 'wpdd_default_download_limit',
'description' => __('Default number of downloads allowed per purchase (0 = unlimited)', 'wp-digital-download'),
'min' => 0
)
);
add_settings_field(
'wpdd_default_download_expiry',
__('Default Download Expiry (days)', 'wp-digital-download'),
array(__CLASS__, 'number_field'),
'wpdd_settings',
'wpdd_download_settings',
array(
'name' => 'wpdd_default_download_expiry',
'description' => __('Default number of days downloads remain available (0 = never expires)', 'wp-digital-download'),
'min' => 0
)
);
add_settings_field(
'wpdd_file_access_method',
__('File Access Method', 'wp-digital-download'),
array(__CLASS__, 'select_field'),
'wpdd_settings',
'wpdd_download_settings',
array(
'name' => 'wpdd_file_access_method',
'options' => array(
'direct' => __('Direct Download', 'wp-digital-download'),
'redirect' => __('Redirect to File', 'wp-digital-download'),
'force' => __('Force Download', 'wp-digital-download')
),
'description' => __('How files are delivered to customers', 'wp-digital-download')
)
);
}
private static function add_watermark_fields() {
add_settings_field(
'wpdd_enable_watermark',
__('Enable Watermarking', 'wp-digital-download'),
array(__CLASS__, 'checkbox_field'),
'wpdd_settings',
'wpdd_watermark_settings',
array(
'name' => 'wpdd_enable_watermark',
'label' => __('Enable watermarking for images and PDFs by default', 'wp-digital-download')
)
);
add_settings_field(
'wpdd_watermark_text',
__('Default Watermark Text', 'wp-digital-download'),
array(__CLASS__, 'text_field'),
'wpdd_settings',
'wpdd_watermark_settings',
array(
'name' => 'wpdd_watermark_text',
'description' => __('Available placeholders: {customer_name}, {customer_email}, {order_id}, {date}, {site_name}', 'wp-digital-download')
)
);
}
public static function render_settings_page() {
?>
<div class="wrap">
<h1><?php _e('WP Digital Download Settings', 'wp-digital-download'); ?></h1>
<div class="wpdd-settings-sidebar">
<div class="wpdd-settings-box">
<h3><?php _e('Quick Setup', 'wp-digital-download'); ?></h3>
<p><?php _e('To get started quickly:', 'wp-digital-download'); ?></p>
<ol>
<li><?php _e('Configure PayPal settings above', 'wp-digital-download'); ?></li>
<li><?php _e('Create your first product', 'wp-digital-download'); ?></li>
<li><?php _e('Add the shop shortcode [wpdd_shop] to a page', 'wp-digital-download'); ?></li>
<li><?php _e('Test with a free product first', 'wp-digital-download'); ?></li>
</ol>
</div>
<div class="wpdd-settings-box">
<h3><?php _e('Available Shortcodes', 'wp-digital-download'); ?></h3>
<ul>
<li><code>[wpdd_shop]</code> - <?php _e('Display product storefront', 'wp-digital-download'); ?></li>
<li><code>[wpdd_customer_purchases]</code> - <?php _e('Customer purchase history', 'wp-digital-download'); ?></li>
<li><code>[wpdd_checkout]</code> - <?php _e('Checkout page', 'wp-digital-download'); ?></li>
<li><code>[wpdd_thank_you]</code> - <?php _e('Thank you page', 'wp-digital-download'); ?></li>
<li><code>[wpdd_product id="123"]</code> - <?php _e('Single product display', 'wp-digital-download'); ?></li>
</ul>
</div>
<div class="wpdd-settings-box">
<h3><?php _e('System Status', 'wp-digital-download'); ?></h3>
<?php self::system_status(); ?>
</div>
</div>
<form method="post" action="options.php" class="wpdd-settings-form">
<?php
settings_fields('wpdd_settings');
do_settings_sections('wpdd_settings');
submit_button();
?>
</form>
</div>
<style>
.wpdd-settings-sidebar {
float: right;
width: 300px;
margin-left: 20px;
position: relative;
z-index: 10;
}
.wpdd-settings-form {
overflow: hidden;
margin-right: 340px;
}
.wpdd-settings-box {
background: white;
border: 1px solid #ccd0d4;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
.wpdd-settings-box h3 {
margin-top: 0;
}
.wpdd-settings-box code {
background: #f1f1f1;
padding: 2px 5px;
font-size: 12px;
}
.wpdd-status-good { color: #46b450; }
.wpdd-status-warning { color: #ffb900; }
.wpdd-status-error { color: #dc3232; }
@media (max-width: 1200px) {
.wpdd-settings-sidebar {
float: none;
width: 100%;
margin-left: 0;
margin-top: 30px;
}
.wpdd-settings-form {
margin-right: 0;
}
}
</style>
<?php
}
public static function general_section_callback() {
echo '<p>' . __('Configure basic plugin settings.', 'wp-digital-download') . '</p>';
}
public static function paypal_section_callback() {
echo '<p>' . __('Configure PayPal payment settings. You can get your API credentials from the PayPal Developer Dashboard.', 'wp-digital-download') . '</p>';
}
public static function email_section_callback() {
echo '<p>' . __('Configure email settings for purchase notifications.', 'wp-digital-download') . '</p>';
}
public static function download_section_callback() {
echo '<p>' . __('Configure default download and file protection settings.', 'wp-digital-download') . '</p>';
}
public static function watermark_section_callback() {
echo '<p>' . __('Configure watermarking settings for images and PDF files.', 'wp-digital-download') . '</p>';
}
public static function text_field($args) {
$name = $args['name'] ?? '';
$value = get_option($name, '');
$description = isset($args['description']) ? $args['description'] : '';
if (empty($name)) {
echo '<p>Error: Field name not provided</p>';
return;
}
printf(
'<input type="text" id="%s" name="%s" value="%s" class="regular-text" />',
esc_attr($name),
esc_attr($name),
esc_attr($value)
);
if ($description) {
printf('<p class="description">%s</p>', esc_html($description));
}
}
public static function password_field($args) {
$name = $args['name'] ?? '';
$value = get_option($name, '');
$description = isset($args['description']) ? $args['description'] : '';
if (empty($name)) {
echo '<p>Error: Field name not provided</p>';
return;
}
printf(
'<input type="password" id="%s" name="%s" value="%s" class="regular-text" />',
esc_attr($name),
esc_attr($name),
esc_attr($value)
);
if ($description) {
printf('<p class="description">%s</p>', esc_html($description));
}
}
public static function email_field($args) {
$name = $args['name'] ?? '';
$value = get_option($name, '');
$description = isset($args['description']) ? $args['description'] : '';
if (empty($name)) {
echo '<p>Error: Field name not provided</p>';
return;
}
printf(
'<input type="email" id="%s" name="%s" value="%s" class="regular-text" />',
esc_attr($name),
esc_attr($name),
esc_attr($value)
);
if ($description) {
printf('<p class="description">%s</p>', esc_html($description));
}
}
public static function number_field($args) {
$name = $args['name'] ?? '';
$value = get_option($name, '');
$description = isset($args['description']) ? $args['description'] : '';
$min = isset($args['min']) ? $args['min'] : 0;
$max = isset($args['max']) ? $args['max'] : '';
if (empty($name)) {
echo '<p>Error: Field name not provided</p>';
return;
}
printf(
'<input type="number" id="%s" name="%s" value="%s" min="%s" %s />',
esc_attr($name),
esc_attr($name),
esc_attr($value),
esc_attr($min),
$max ? 'max="' . esc_attr($max) . '"' : ''
);
if ($description) {
printf('<p class="description">%s</p>', esc_html($description));
}
}
public static function checkbox_field($args) {
$name = $args['name'] ?? '';
$value = get_option($name, 0);
$label = $args['label'] ?? '';
if (empty($name)) {
echo '<p>Error: Field name not provided</p>';
return;
}
printf(
'<label><input type="checkbox" id="%s" name="%s" value="1" %s /> %s</label>',
esc_attr($name),
esc_attr($name),
checked($value, 1, false),
esc_html($label)
);
}
public static function select_field($args) {
$name = $args['name'] ?? '';
$value = get_option($name, '');
$options = $args['options'] ?? array();
$description = isset($args['description']) ? $args['description'] : '';
if (empty($name)) {
echo '<p>Error: Field name not provided</p>';
return;
}
printf('<select id="%s" name="%s">', esc_attr($name), esc_attr($name));
foreach ($options as $option_value => $option_label) {
printf(
'<option value="%s" %s>%s</option>',
esc_attr($option_value),
selected($value, $option_value, false),
esc_html($option_label)
);
}
echo '</select>';
if ($description) {
printf('<p class="description">%s</p>', esc_html($description));
}
}
public static function currency_field($args) {
$currencies = array(
'USD' => 'US Dollar ($)',
'EUR' => 'Euro (€)',
'GBP' => 'British Pound (£)',
'CAD' => 'Canadian Dollar (C$)',
'AUD' => 'Australian Dollar (A$)',
'JPY' => 'Japanese Yen (¥)'
);
$args['options'] = $currencies;
self::select_field($args);
}
public static function page_dropdown_field($args) {
$name = $args['name'] ?? '';
$value = get_option($name, '');
if (empty($name)) {
echo '<p>Error: Field name not provided</p>';
return;
}
wp_dropdown_pages(array(
'name' => $name,
'id' => $name,
'selected' => $value,
'show_option_none' => __('— Select —', 'wp-digital-download'),
'option_none_value' => ''
));
}
private static function system_status() {
$status = array();
$upload_dir = wp_upload_dir();
$protected_dir = trailingslashit($upload_dir['basedir']) . WPDD_UPLOADS_DIR;
if (is_writable($upload_dir['basedir'])) {
$status[] = array(
'label' => __('Upload Directory', 'wp-digital-download'),
'value' => __('Writable', 'wp-digital-download'),
'class' => 'wpdd-status-good'
);
} else {
$status[] = array(
'label' => __('Upload Directory', 'wp-digital-download'),
'value' => __('Not Writable', 'wp-digital-download'),
'class' => 'wpdd-status-error'
);
}
if (file_exists($protected_dir)) {
$status[] = array(
'label' => __('Protected Directory', 'wp-digital-download'),
'value' => __('Exists', 'wp-digital-download'),
'class' => 'wpdd-status-good'
);
} else {
$status[] = array(
'label' => __('Protected Directory', 'wp-digital-download'),
'value' => __('Missing', 'wp-digital-download'),
'class' => 'wpdd-status-warning'
);
}
if (function_exists('imagecreatefrompng')) {
$status[] = array(
'label' => __('GD Library', 'wp-digital-download'),
'value' => __('Available', 'wp-digital-download'),
'class' => 'wpdd-status-good'
);
} else {
$status[] = array(
'label' => __('GD Library', 'wp-digital-download'),
'value' => __('Not Available', 'wp-digital-download'),
'class' => 'wpdd-status-warning'
);
}
$paypal_client_id = get_option('wpdd_paypal_client_id');
if ($paypal_client_id) {
$status[] = array(
'label' => __('PayPal', 'wp-digital-download'),
'value' => __('Configured', 'wp-digital-download'),
'class' => 'wpdd-status-good'
);
} else {
$status[] = array(
'label' => __('PayPal', 'wp-digital-download'),
'value' => __('Not Configured', 'wp-digital-download'),
'class' => 'wpdd-status-warning'
);
}
echo '<ul>';
foreach ($status as $item) {
printf(
'<li>%s: <span class="%s">%s</span></li>',
esc_html($item['label']),
esc_attr($item['class']),
esc_html($item['value'])
);
}
echo '</ul>';
}
public static function sanitize_commission_rate($input) {
$value = floatval($input);
if ($value < 0) {
$value = 0;
add_settings_error('wpdd_commission_rate', 'invalid_rate', __('Commission rate cannot be less than 0%. Set to 0%.', 'wp-digital-download'));
} elseif ($value > 100) {
$value = 100;
add_settings_error('wpdd_commission_rate', 'invalid_rate', __('Commission rate cannot exceed 100%. Set to 100%.', 'wp-digital-download'));
}
return $value;
}
public static function sanitize_payout_threshold($input) {
$value = floatval($input);
if ($value < 0) {
$value = 0;
add_settings_error('wpdd_payout_threshold', 'invalid_threshold', __('Payout threshold cannot be negative. Set to 0 (disabled).', 'wp-digital-download'));
}
return $value;
}
}

451
assets/css/admin.css Normal file
View File

@@ -0,0 +1,451 @@
/* WP Digital Download - Admin Styles */
/* Product Metaboxes */
.wpdd-metabox-content {
padding: 10px 0;
}
.wpdd-metabox-content p {
margin-bottom: 15px;
}
.wpdd-metabox-content label {
display: block;
margin-bottom: 5px;
font-weight: 600;
}
.wpdd-metabox-content input[type="text"],
.wpdd-metabox-content input[type="number"],
.wpdd-metabox-content input[type="email"] {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.wpdd-metabox-content input[type="checkbox"] {
margin-right: 8px;
}
.wpdd-metabox-content .description {
font-style: italic;
color: #666;
font-size: 12px;
margin-top: 5px;
}
/* Price fields toggle */
#wpdd_is_free:checked ~ .wpdd-price-field {
opacity: 0.5;
pointer-events: none;
}
/* Files Metabox */
.wpdd-files-container {
padding: 10px 0;
}
#wpdd-files-list {
margin-bottom: 20px;
}
.wpdd-file-item {
border: 1px solid #ddd;
border-radius: 4px;
margin-bottom: 15px;
background: #f9f9f9;
}
.wpdd-file-header {
display: flex;
align-items: center;
padding: 10px 15px;
background: #f1f1f1;
border-bottom: 1px solid #ddd;
}
.wpdd-file-handle {
cursor: move;
margin-right: 10px;
color: #666;
}
.wpdd-file-header input[type="text"] {
flex: 1;
margin-right: 10px;
padding: 5px 8px;
border: 1px solid #ddd;
border-radius: 3px;
}
.wpdd-file-content {
padding: 15px;
}
.wpdd-file-url {
display: flex;
gap: 10px;
align-items: center;
}
.wpdd-file-url-input {
flex: 1;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.wpdd-upload-file,
.wpdd-remove-file {
white-space: nowrap;
}
.wpdd-remove-file {
background: #dc3545;
color: white;
border: none;
padding: 5px 10px;
border-radius: 3px;
cursor: pointer;
}
.wpdd-remove-file:hover {
background: #c82333;
}
#wpdd-add-file {
background: #0073aa;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
}
#wpdd-add-file:hover {
background: #005a87;
}
/* Settings Grid */
.wpdd-settings-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
}
.wpdd-setting-group {
background: #f9f9f9;
padding: 20px;
border-radius: 6px;
border: 1px solid #e1e5e9;
}
.wpdd-setting-group h4 {
margin: 0 0 15px 0;
color: #23282d;
font-size: 14px;
font-weight: 600;
}
.wpdd-setting-group p {
margin-bottom: 15px;
}
.wpdd-setting-group label {
display: block;
margin-bottom: 5px;
font-weight: 500;
font-size: 13px;
}
.wpdd-setting-group input[type="text"],
.wpdd-setting-group input[type="number"] {
width: 100%;
padding: 6px 8px;
border: 1px solid #ddd;
border-radius: 3px;
font-size: 13px;
}
.wpdd-setting-group .description {
font-size: 12px;
color: #666;
font-style: italic;
margin-top: 5px;
}
@media (max-width: 782px) {
.wpdd-settings-grid {
grid-template-columns: 1fr;
}
}
/* Stats Metabox */
.wpdd-stats p {
margin-bottom: 8px;
font-size: 13px;
}
.wpdd-stats strong {
color: #23282d;
}
/* Product List Columns */
.column-wpdd_price,
.column-wpdd_sales,
.column-wpdd_revenue,
.column-wpdd_files {
width: 10%;
}
/* File Upload Progress */
.wpdd-upload-progress {
width: 100%;
height: 20px;
background: #f1f1f1;
border-radius: 10px;
overflow: hidden;
margin: 10px 0;
}
.wpdd-upload-progress-bar {
height: 100%;
background: #0073aa;
transition: width 0.3s;
border-radius: 10px;
}
/* Drag and Drop Sorting */
.wpdd-file-item.ui-sortable-helper {
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
transform: rotate(2deg);
}
.wpdd-file-item.ui-sortable-placeholder {
border: 2px dashed #0073aa;
background: transparent;
}
/* Admin Dashboard Widgets */
.wpdd-sales-summary .wpdd-stats-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
margin-bottom: 20px;
}
.wpdd-sales-summary .wpdd-stat {
text-align: center;
padding: 10px;
background: #f0f0f1;
border-radius: 4px;
}
.wpdd-sales-summary .wpdd-stat-value {
display: block;
font-size: 24px;
font-weight: 600;
color: #2271b1;
}
.wpdd-sales-summary .wpdd-stat-label {
display: block;
font-size: 12px;
color: #646970;
margin-top: 5px;
}
/* Orders Page */
.wpdd-status {
padding: 3px 8px;
border-radius: 3px;
font-size: 12px;
font-weight: 600;
}
.wpdd-status-completed {
background: #d4edda;
color: #155724;
}
.wpdd-status-pending {
background: #fff3cd;
color: #856404;
}
.wpdd-status-failed {
background: #f8d7da;
color: #721c24;
}
/* Reports Page */
.wpdd-date-filter {
margin: 20px 0;
}
.wpdd-stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin: 30px 0;
}
.wpdd-stat-box {
background: white;
padding: 20px;
border: 1px solid #ccd0d4;
border-radius: 4px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.wpdd-stat-box h3 {
margin: 0 0 10px 0;
color: #23282d;
font-size: 14px;
font-weight: 600;
}
.wpdd-stat-value {
font-size: 32px;
font-weight: 600;
color: #2271b1;
margin: 0;
line-height: 1.2;
}
.wpdd-reports-tables {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
margin-top: 30px;
}
.wpdd-report-section h2 {
margin-bottom: 15px;
font-size: 18px;
}
@media (max-width: 1200px) {
.wpdd-reports-tables {
grid-template-columns: 1fr;
}
}
/* Settings Page */
.wpdd-settings-sidebar {
float: right;
width: 300px;
margin-left: 20px;
}
.wpdd-settings-box {
background: white;
border: 1px solid #ccd0d4;
padding: 20px;
margin-bottom: 20px;
border-radius: 4px;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
.wpdd-settings-box h3 {
margin-top: 0;
font-size: 16px;
}
.wpdd-settings-box code {
background: #f1f1f1;
padding: 2px 5px;
font-size: 12px;
border-radius: 3px;
}
.wpdd-settings-box ul {
margin: 0;
padding-left: 20px;
}
.wpdd-settings-box li {
margin-bottom: 8px;
font-size: 13px;
}
.wpdd-status-good {
color: #46b450;
}
.wpdd-status-warning {
color: #ffb900;
}
.wpdd-status-error {
color: #dc3232;
}
#wpforms-settings .form-table {
max-width: calc(100% - 340px);
}
@media (max-width: 1200px) {
.wpdd-settings-sidebar {
float: none;
width: 100%;
margin-left: 0;
margin-top: 30px;
}
#wpforms-settings .form-table {
max-width: 100%;
}
}
/* Responsive adjustments */
@media (max-width: 782px) {
.wpdd-file-header {
flex-direction: column;
align-items: stretch;
gap: 10px;
}
.wpdd-file-url {
flex-direction: column;
gap: 10px;
}
.wpdd-settings-sidebar {
width: 100%;
margin-left: 0;
margin-top: 20px;
float: none;
}
.wpdd-stats-grid {
grid-template-columns: 1fr;
}
}
/* Loading states */
.wpdd-loading {
position: relative;
pointer-events: none;
opacity: 0.6;
}
.wpdd-loading::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 20px;
height: 20px;
margin: -10px 0 0 -10px;
border: 2px solid #ccc;
border-top: 2px solid #0073aa;
border-radius: 50%;
animation: wpdd-spin 1s linear infinite;
}
@keyframes wpdd-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

469
assets/css/frontend.css Normal file
View File

@@ -0,0 +1,469 @@
/* WP Digital Download - Frontend Styles */
/* Shop Grid */
.wpdd-shop-container {
max-width: 1200px;
margin: 0 auto;
}
.wpdd-shop-filters {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
margin-bottom: 30px;
}
.wpdd-filter-form {
display: flex;
gap: 15px;
align-items: center;
flex-wrap: wrap;
}
.wpdd-filter-form input[type="text"],
.wpdd-filter-form select {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
min-width: 150px;
}
.wpdd-filter-submit {
padding: 8px 16px;
background: #0073aa;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background 0.3s;
}
.wpdd-filter-submit:hover {
background: #005a87;
}
.wpdd-products-grid {
display: grid;
gap: 30px;
margin-bottom: 40px;
}
.wpdd-columns-1 { grid-template-columns: 1fr; }
.wpdd-columns-2 { grid-template-columns: repeat(2, 1fr); }
.wpdd-columns-3 { grid-template-columns: repeat(3, 1fr); }
.wpdd-columns-4 { grid-template-columns: repeat(4, 1fr); }
@media (max-width: 768px) {
.wpdd-columns-2,
.wpdd-columns-3,
.wpdd-columns-4 {
grid-template-columns: 1fr;
}
}
@media (min-width: 769px) and (max-width: 1024px) {
.wpdd-columns-3,
.wpdd-columns-4 {
grid-template-columns: repeat(2, 1fr);
}
}
/* Product Cards */
.wpdd-product-card {
background: white;
border: 1px solid #e1e5e9;
border-radius: 8px;
overflow: hidden;
transition: transform 0.3s, box-shadow 0.3s;
height: 100%;
display: flex;
flex-direction: column;
}
.wpdd-product-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 25px rgba(0,0,0,0.1);
}
.wpdd-product-image {
position: relative;
overflow: hidden;
height: 200px;
}
.wpdd-product-image img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s;
}
.wpdd-product-card:hover .wpdd-product-image img {
transform: scale(1.05);
}
.wpdd-product-info {
padding: 20px;
flex: 1;
display: flex;
flex-direction: column;
}
.wpdd-product-title {
margin: 0 0 10px 0;
font-size: 18px;
line-height: 1.4;
}
.wpdd-product-title a {
color: #333;
text-decoration: none;
transition: color 0.3s;
}
.wpdd-product-title a:hover {
color: #0073aa;
}
.wpdd-product-meta {
color: #666;
font-size: 14px;
margin-bottom: 10px;
}
.wpdd-product-creator {
font-style: italic;
}
.wpdd-product-excerpt {
color: #555;
font-size: 14px;
line-height: 1.5;
margin-bottom: 15px;
flex: 1;
}
.wpdd-product-price {
margin-bottom: 15px;
font-weight: bold;
}
.wpdd-price-free {
color: #28a745;
font-size: 18px;
}
.wpdd-price-regular {
color: #333;
font-size: 18px;
}
.wpdd-price-sale {
color: #dc3545;
font-size: 18px;
}
.wpdd-price-strike {
text-decoration: line-through;
color: #999;
font-size: 14px;
}
.wpdd-product-actions {
margin-top: auto;
display: flex;
flex-direction: column;
gap: 10px;
}
.wpdd-product-actions .wpdd-btn {
width: 100%;
justify-content: center;
}
/* Buttons */
.wpdd-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 10px 20px;
border: none;
border-radius: 4px;
text-decoration: none;
text-align: center;
cursor: pointer;
transition: all 0.3s;
font-size: 14px;
line-height: 1.4;
box-sizing: border-box;
}
.wpdd-btn-primary {
background: #0073aa;
color: white;
}
.wpdd-btn-primary:hover {
background: #005a87;
color: white;
}
.wpdd-btn-view {
background: #f8f9fa;
color: #333;
border: 1px solid #dee2e6;
}
.wpdd-btn-view:hover {
background: #e9ecef;
color: #333;
}
.wpdd-btn-buy {
background: #28a745;
color: white;
}
.wpdd-btn-buy:hover {
background: #218838;
color: white;
}
.wpdd-owned-product {
background: #17a2b8 !important;
color: white !important;
padding: 10px 30px !important;
}
.wpdd-owned-product:hover {
background: #138496 !important;
color: white !important;
}
.wpdd-btn-view:hover {
background: #e9ecef;
color: #333;
}
.wpdd-btn-download {
background: #28a745;
color: white;
}
.wpdd-btn-download:hover {
background: #218838;
color: white;
}
.wpdd-btn-large {
padding: 15px 30px;
font-size: 16px;
}
/* Customer Purchases */
.wpdd-customer-purchases {
max-width: 1000px;
margin: 0 auto;
}
.wpdd-purchases-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 30px;
background: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.wpdd-purchases-table th,
.wpdd-purchases-table td {
padding: 15px;
text-align: left;
border-bottom: 1px solid #e1e5e9;
}
.wpdd-purchases-table th {
background: #f8f9fa;
font-weight: 600;
color: #495057;
}
.wpdd-purchases-table tr:hover {
background: #f8f9fa;
}
.wpdd-download-expired {
color: #dc3545;
font-style: italic;
}
/* Checkout */
.wpdd-checkout {
max-width: 600px;
margin: 0 auto;
}
.wpdd-checkout-product {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
margin-bottom: 30px;
text-align: center;
}
.wpdd-checkout-product h3 {
margin: 0 0 15px 0;
}
.wpdd-checkout-product img {
max-width: 150px;
height: auto;
border-radius: 4px;
margin-bottom: 15px;
}
.wpdd-checkout-price {
font-size: 24px;
font-weight: bold;
color: #0073aa;
}
.wpdd-checkout-section {
background: white;
padding: 25px;
border: 1px solid #e1e5e9;
border-radius: 8px;
margin-bottom: 20px;
}
.wpdd-checkout-section h4 {
margin: 0 0 20px 0;
padding-bottom: 10px;
border-bottom: 1px solid #e1e5e9;
}
.wpdd-checkout-section p {
margin-bottom: 15px;
}
.wpdd-checkout-section label {
display: block;
margin-bottom: 5px;
font-weight: 500;
}
.wpdd-checkout-section input[type="text"],
.wpdd-checkout-section input[type="email"] {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
}
.wpdd-checkout-section input[type="checkbox"] {
margin-right: 8px;
}
/* Thank You Page */
.wpdd-thank-you {
max-width: 600px;
margin: 0 auto;
text-align: center;
}
.wpdd-order-details {
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.wpdd-order-info {
background: #f8f9fa;
padding: 20px;
border-radius: 6px;
margin: 20px 0;
text-align: left;
}
.wpdd-download-section {
margin: 30px 0;
}
.wpdd-download-section h3 {
color: #28a745;
margin-bottom: 15px;
}
/* Pagination */
.wpdd-pagination {
text-align: center;
margin: 40px 0;
}
.wpdd-pagination .page-numbers {
display: inline-block;
padding: 8px 12px;
margin: 0 4px;
text-decoration: none;
border: 1px solid #dee2e6;
color: #495057;
border-radius: 4px;
transition: all 0.3s;
}
.wpdd-pagination .page-numbers:hover,
.wpdd-pagination .page-numbers.current {
background: #0073aa;
color: white;
border-color: #0073aa;
}
/* Responsive */
@media (max-width: 768px) {
.wpdd-shop-filters .wpdd-filter-form {
flex-direction: column;
align-items: stretch;
}
.wpdd-filter-form input,
.wpdd-filter-form select {
min-width: auto;
width: 100%;
}
.wpdd-purchases-table {
font-size: 14px;
}
.wpdd-purchases-table th,
.wpdd-purchases-table td {
padding: 10px;
}
.wpdd-checkout-section {
padding: 20px;
}
}
/* No products message */
.wpdd-no-products {
text-align: center;
color: #666;
font-size: 18px;
margin: 50px 0;
}
/* Login required message */
.wpdd-login-required {
background: #fff3cd;
color: #856404;
padding: 15px;
border-radius: 4px;
border: 1px solid #ffeaa7;
}
.wpdd-login-required a {
color: #856404;
font-weight: bold;
}

411
assets/js/admin.js Normal file
View File

@@ -0,0 +1,411 @@
jQuery(document).ready(function($) {
'use strict';
/**
* Main WPDD Admin Object
*/
window.WPDD_Admin = {
init: function() {
this.initFileManager();
this.initPriceToggle();
this.initFormValidation();
},
initFileManager: function() {
var fileIndex = $('#wpdd-files-list .wpdd-file-item').length;
// Add new file
$('#wpdd-add-file').on('click', function(e) {
e.preventDefault();
var template = $('#wpdd-file-template').html();
template = template.replace(/INDEX/g, fileIndex);
var $newFile = $(template);
$newFile.attr('data-index', fileIndex);
$('#wpdd-files-list').append($newFile);
fileIndex++;
WPDD_Admin.updateFileIndices();
});
// Remove file
$(document).on('click', '.wpdd-remove-file', function(e) {
e.preventDefault();
if (confirm('Are you sure you want to remove this file?')) {
$(this).closest('.wpdd-file-item').remove();
WPDD_Admin.updateFileIndices();
}
});
// Upload file
$(document).on('click', '.wpdd-upload-file', function(e) {
e.preventDefault();
var $button = $(this);
var $container = $button.closest('.wpdd-file-item');
var $urlInput = $container.find('.wpdd-file-url-input');
var $idInput = $container.find('.wpdd-file-id');
var $nameInput = $container.find('input[name*="[name]"]');
// Create file input element
var $fileInput = $('<input type="file" style="display:none;" />');
$fileInput.on('change', function(event) {
var file = event.target.files[0];
if (!file) return;
// Show loading state
$button.prop('disabled', true).text('Uploading...');
var formData = new FormData();
formData.append('action', 'wpdd_upload_protected_file');
formData.append('file', file);
formData.append('nonce', wpdd_admin_nonce);
$.ajax({
url: ajaxurl,
type: 'POST',
data: formData,
processData: false,
contentType: false,
success: function(response) {
if (response.success) {
$urlInput.val(response.data.protected_url);
$idInput.val(response.data.file_id);
if (!$nameInput.val()) {
$nameInput.val(file.name);
}
WPDD_Admin.showAdminNotice('File uploaded to protected directory', 'success');
} else {
WPDD_Admin.showAdminNotice(response.data || 'Upload failed', 'error');
}
$button.prop('disabled', false).text('Upload File');
},
error: function() {
WPDD_Admin.showAdminNotice('Upload failed', 'error');
$button.prop('disabled', false).text('Upload File');
}
});
// Clean up
$fileInput.remove();
});
// Trigger file selection
$('body').append($fileInput);
$fileInput.trigger('click');
});
// Make files sortable
if ($.fn.sortable) {
$('#wpdd-files-list').sortable({
handle: '.wpdd-file-handle',
placeholder: 'wpdd-file-placeholder',
update: function() {
WPDD_Admin.updateFileIndices();
}
});
}
},
moveToProtectedDirectory: function(attachmentId, $container) {
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'wpdd_move_to_protected',
attachment_id: attachmentId,
nonce: wpdd_admin_nonce
},
success: function(response) {
if (response.success && response.data.protected_url) {
$container.find('.wpdd-file-url-input').val(response.data.protected_url);
}
}
});
},
updateFileIndices: function() {
$('#wpdd-files-list .wpdd-file-item').each(function(index) {
var $item = $(this);
$item.attr('data-index', index);
// Update input names
$item.find('input').each(function() {
var name = $(this).attr('name');
if (name) {
name = name.replace(/\[\d+\]/, '[' + index + ']');
$(this).attr('name', name);
}
});
});
},
initPriceToggle: function() {
$('#wpdd_is_free').on('change', function() {
var $priceFields = $('.wpdd-price-field');
if ($(this).is(':checked')) {
$priceFields.slideUp();
} else {
$priceFields.slideDown();
}
}).trigger('change');
},
initFormValidation: function() {
// Validate PayPal settings
$('input[name="wpdd_paypal_client_id"], input[name="wpdd_paypal_secret"]').on('blur', function() {
var $field = $(this);
var value = $field.val().trim();
if (value && value.length < 10) {
$field.addClass('error');
WPDD_Admin.showAdminNotice('Invalid PayPal credential format', 'error');
} else {
$field.removeClass('error');
}
});
// Validate email fields
$('input[type="email"]').on('blur', function() {
var $field = $(this);
var value = $field.val().trim();
var emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (value && !emailRegex.test(value)) {
$field.addClass('error');
} else {
$field.removeClass('error');
}
});
// Validate number fields
$('input[type="number"]').on('input', function() {
var $field = $(this);
var value = parseFloat($field.val());
var min = parseFloat($field.attr('min'));
var max = parseFloat($field.attr('max'));
if (isNaN(value) || (min !== undefined && value < min) || (max !== undefined && value > max)) {
$field.addClass('error');
} else {
$field.removeClass('error');
}
});
},
showAdminNotice: function(message, type) {
type = type || 'info';
var notice = $('<div class="notice notice-' + type + ' is-dismissible">' +
'<p>' + message + '</p>' +
'<button type="button" class="notice-dismiss"></button>' +
'</div>');
$('.wrap h1').first().after(notice);
// Handle dismiss
notice.find('.notice-dismiss').on('click', function() {
notice.fadeOut(function() {
notice.remove();
});
});
// Auto-dismiss after 5 seconds for non-error notices
if (type !== 'error') {
setTimeout(function() {
notice.fadeOut(function() {
notice.remove();
});
}, 5000);
}
},
initReportsCharts: function() {
// Placeholder for future chart implementation
// Could integrate Chart.js or similar library
},
initBulkActions: function() {
// Handle bulk actions for orders, customers, etc.
$('.bulkactions select').on('change', function() {
var action = $(this).val();
var $button = $(this).siblings('input[type="submit"]');
if (action) {
$button.prop('disabled', false);
} else {
$button.prop('disabled', true);
}
});
},
initQuickEdit: function() {
// Quick edit functionality for products
$('.editinline').on('click', function() {
var $row = $(this).closest('tr');
var productId = $row.find('.check-column input').val();
// Populate quick edit fields with current values
setTimeout(function() {
var $quickEdit = $('.inline-edit-row');
// Pre-fill price from the displayed value
var price = $row.find('.column-wpdd_price').text().replace('$', '');
if (price !== 'Free') {
$quickEdit.find('input[name="_wpdd_price"]').val(price);
}
}, 100);
});
},
initFilePreview: function() {
// Show file preview/details when hovering over file names
$(document).on('mouseenter', '.wpdd-file-url-input', function() {
var url = $(this).val();
if (url) {
var filename = url.split('/').pop();
var extension = filename.split('.').pop().toLowerCase();
var fileType = WPDD_Admin.getFileType(extension);
$(this).attr('title', 'File: ' + filename + ' (Type: ' + fileType + ')');
}
});
},
getFileType: function(extension) {
var types = {
'pdf': 'PDF Document',
'doc': 'Word Document',
'docx': 'Word Document',
'zip': 'Archive',
'rar': 'Archive',
'jpg': 'Image',
'jpeg': 'Image',
'png': 'Image',
'gif': 'Image',
'mp3': 'Audio',
'wav': 'Audio',
'mp4': 'Video',
'avi': 'Video'
};
return types[extension] || 'File';
},
initColorPicker: function() {
// Initialize color picker for watermark settings
if ($.fn.wpColorPicker) {
$('.wpdd-color-picker').wpColorPicker();
}
},
initTabs: function() {
// Settings page tabs
$('.wpdd-settings-tabs').on('click', 'a', function(e) {
e.preventDefault();
var $tab = $(this);
var target = $tab.attr('href');
// Update active tab
$tab.siblings().removeClass('nav-tab-active');
$tab.addClass('nav-tab-active');
// Show/hide content
$('.wpdd-settings-content').hide();
$(target).show();
});
}
};
// Initialize admin functionality
WPDD_Admin.init();
// Add admin-specific styles
$('<style>')
.prop('type', 'text/css')
.html(`
.wpdd-file-item.ui-sortable-helper {
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
transform: rotate(2deg);
}
.wpdd-file-placeholder {
border: 2px dashed #0073aa !important;
background: transparent !important;
height: 80px !important;
}
input.error {
border-color: #dc3232 !important;
box-shadow: 0 0 2px rgba(220, 50, 50, 0.3) !important;
}
.wpdd-loading-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(255,255,255,0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.wpdd-spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #0073aa;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.wpdd-help-tip {
color: #666;
cursor: help;
margin-left: 5px;
}
.wpdd-help-tip:hover {
color: #0073aa;
}
`)
.appendTo('head');
// Initialize additional features based on current page
var currentScreen = window.pagenow || '';
if (currentScreen === 'wpdd_product') {
WPDD_Admin.initQuickEdit();
WPDD_Admin.initFilePreview();
}
if (currentScreen.includes('wpdd-settings')) {
WPDD_Admin.initColorPicker();
WPDD_Admin.initTabs();
}
if (currentScreen.includes('wpdd-reports')) {
WPDD_Admin.initReportsCharts();
}
// Global admin features
WPDD_Admin.initBulkActions();
});

554
assets/js/frontend.js Normal file
View File

@@ -0,0 +1,554 @@
jQuery(document).ready(function($) {
'use strict';
/**
* Main WPDD Frontend Object
*/
window.WPDD = {
init: function() {
this.bindEvents();
this.initProductCards();
this.initCheckout();
},
bindEvents: function() {
// Add to cart buttons
$(document).on('click', '.wpdd-add-to-cart', this.addToCart);
// Free download buttons
$(document).on('click', '.wpdd-free-download', this.processFreeDownload);
// Product quickview
$(document).on('click', '.wpdd-quickview', this.showQuickview);
// Filter form submission
$(document).on('submit', '.wpdd-filter-form', this.handleFilters);
// Download status check
$(document).on('click', '.wpdd-check-download', this.checkDownloadStatus);
},
initProductCards: function() {
// Add hover effects and animations
$('.wpdd-product-card').each(function() {
var $card = $(this);
var $image = $card.find('.wpdd-product-image img');
$card.hover(
function() {
$(this).addClass('hovered');
},
function() {
$(this).removeClass('hovered');
}
);
});
},
initCheckout: function() {
// Free product checkout handler
$('#wpdd-checkout-form').on('submit', function(e) {
var $form = $(this);
var isFree = $form.data('product-free') == '1';
if (isFree) {
e.preventDefault();
WPDD.processFreeCheckout($form);
}
});
// Price display toggle based on free checkbox
$('#wpdd_is_free').on('change', function() {
var $priceFields = $('.wpdd-price-field');
if ($(this).is(':checked')) {
$priceFields.hide();
} else {
$priceFields.show();
}
});
},
addToCart: function(e) {
e.preventDefault();
var $button = $(this);
var productId = $button.data('product-id');
if (!productId) {
WPDD.showNotice('Invalid product', 'error');
return;
}
$button.addClass('loading').prop('disabled', true);
$.ajax({
url: wpdd_ajax.url,
type: 'POST',
data: {
action: 'wpdd_add_to_cart',
product_id: productId,
nonce: wpdd_ajax.nonce
},
success: function(response) {
if (response.success) {
WPDD.showNotice(response.data.message, 'success');
// Update cart count if element exists
$('.wpdd-cart-count').text(response.data.cart_count);
// Show checkout button
if (response.data.checkout_url) {
$button.after('<a href="' + response.data.checkout_url +
'" class="wpdd-btn wpdd-btn-primary">Checkout</a>');
}
} else {
WPDD.showNotice(response.data, 'error');
}
},
error: function() {
WPDD.showNotice('An error occurred. Please try again.', 'error');
},
complete: function() {
$button.removeClass('loading').prop('disabled', false);
}
});
},
processFreeDownload: function(e) {
e.preventDefault();
var $button = $(this);
var productId = $button.data('product-id');
var $form = $button.closest('form');
if (!productId) {
WPDD.showNotice('Invalid product', 'error');
return;
}
var customerEmail = $form.find('input[name="customer_email"]').val();
var customerName = $form.find('input[name="customer_name"]').val();
if (!customerEmail && !WPDD.isUserLoggedIn()) {
WPDD.showNotice('Please provide your email address', 'error');
return;
}
$button.addClass('loading').prop('disabled', true);
$.ajax({
url: wpdd_ajax.url,
type: 'POST',
data: {
action: 'wpdd_process_free_download',
product_id: productId,
customer_email: customerEmail,
customer_name: customerName,
nonce: wpdd_ajax.nonce
},
success: function(response) {
if (response.success) {
window.location.href = response.data.redirect_url;
} else {
WPDD.showNotice(response.data, 'error');
}
},
error: function() {
WPDD.showNotice('An error occurred. Please try again.', 'error');
},
complete: function() {
$button.removeClass('loading').prop('disabled', false);
}
});
},
processFreeCheckout: function($form) {
var formData = $form.serialize();
var $submitBtn = $form.find('button[type="submit"]');
var productId = $form.find('input[name="product_id"]').val();
$submitBtn.addClass('loading').prop('disabled', true);
// Add visual feedback
$submitBtn.text('Processing...');
$.ajax({
url: wpdd_ajax.url,
type: 'POST',
data: formData + '&action=wpdd_process_free_download&nonce=' + wpdd_ajax.nonce + '&product_id=' + productId,
success: function(response) {
if (response.success) {
window.location.href = response.data.redirect_url;
} else {
WPDD.showNotice(response.data || 'Failed to process download', 'error');
$submitBtn.text('Get Free Download');
}
},
error: function(xhr, status, error) {
console.error('AJAX Error:', status, error);
WPDD.showNotice('An error occurred. Please try again.', 'error');
$submitBtn.text('Get Free Download');
},
complete: function() {
$submitBtn.removeClass('loading').prop('disabled', false);
}
});
},
showQuickview: function(e) {
e.preventDefault();
var productId = $(this).data('product-id');
if (!productId) {
return;
}
$.ajax({
url: wpdd_ajax.url,
type: 'POST',
data: {
action: 'wpdd_get_product_details',
product_id: productId,
nonce: wpdd_ajax.nonce
},
success: function(response) {
if (response.success) {
WPDD.displayQuickview(response.data);
} else {
WPDD.showNotice('Unable to load product details', 'error');
}
},
error: function() {
WPDD.showNotice('An error occurred. Please try again.', 'error');
}
});
},
displayQuickview: function(product) {
var modal = $('<div class="wpdd-modal"><div class="wpdd-modal-content"></div></div>');
var content = '';
content += '<div class="wpdd-modal-header">';
content += '<h2>' + product.title + '</h2>';
content += '<button class="wpdd-modal-close">&times;</button>';
content += '</div>';
content += '<div class="wpdd-modal-body">';
if (product.thumbnail) {
content += '<img src="' + product.thumbnail + '" alt="' + product.title + '" class="wpdd-modal-image">';
}
content += '<div class="wpdd-modal-details">';
content += '<p class="wpdd-modal-price">';
if (product.is_free) {
content += '<span class="wpdd-price-free">Free</span>';
} else {
content += '<span class="wpdd-price-regular">$' + product.final_price + '</span>';
}
content += '</p>';
content += '<p class="wpdd-modal-creator">by ' + product.creator.name + '</p>';
content += '<div class="wpdd-modal-description">' + product.description + '</div>';
content += '<p><strong>Files:</strong> ' + product.files_count + '</p>';
content += '<p><strong>Download Limit:</strong> ' + product.download_limit + '</p>';
content += '<p><strong>Expires:</strong> ' + product.download_expiry + '</p>';
content += '</div>';
content += '</div>';
content += '<div class="wpdd-modal-footer">';
content += '<a href="' + window.location.origin + '?product_id=' + product.id +
'" class="wpdd-btn wpdd-btn-primary">View Details</a>';
content += '</div>';
modal.find('.wpdd-modal-content').html(content);
$('body').append(modal);
modal.addClass('active');
// Close modal events
modal.on('click', '.wpdd-modal-close, .wpdd-modal', function(e) {
if (e.target === this) {
modal.removeClass('active');
setTimeout(function() {
modal.remove();
}, 300);
}
});
},
handleFilters: function(e) {
// Let the form submit naturally for now
// Could be enhanced with AJAX filtering
},
checkDownloadStatus: function(e) {
e.preventDefault();
var productId = $(this).data('product-id');
var $button = $(this);
if (!WPDD.isUserLoggedIn()) {
WPDD.showNotice('Please login to check download status', 'error');
return;
}
$button.addClass('loading');
$.ajax({
url: wpdd_ajax.url,
type: 'POST',
data: {
action: 'wpdd_check_download_status',
product_id: productId,
nonce: wpdd_ajax.nonce
},
success: function(response) {
if (response.success) {
var data = response.data;
if (data.can_download && data.download_url) {
window.location.href = data.download_url;
} else {
WPDD.showNotice(data.message, data.can_download ? 'success' : 'error');
}
} else {
WPDD.showNotice(response.data, 'error');
}
},
error: function() {
WPDD.showNotice('An error occurred. Please try again.', 'error');
},
complete: function() {
$button.removeClass('loading');
}
});
},
showNotice: function(message, type) {
type = type || 'info';
var notice = $('<div class="wpdd-notice wpdd-notice-' + type + '">' +
'<p>' + message + '</p>' +
'<button class="wpdd-notice-dismiss">&times;</button>' +
'</div>');
$('body').prepend(notice);
notice.addClass('active');
// Auto-dismiss after 5 seconds
setTimeout(function() {
notice.removeClass('active');
setTimeout(function() {
notice.remove();
}, 300);
}, 5000);
// Manual dismiss
notice.find('.wpdd-notice-dismiss').on('click', function() {
notice.removeClass('active');
setTimeout(function() {
notice.remove();
}, 300);
});
},
isUserLoggedIn: function() {
return $('body').hasClass('logged-in');
},
formatPrice: function(price) {
return '$' + parseFloat(price).toFixed(2);
}
};
// Initialize frontend functionality
WPDD.init();
// Add modal and notice styles if not already present
if (!$('#wpdd-dynamic-styles').length) {
var styles = `
<style id="wpdd-dynamic-styles">
.wpdd-modal {
display: flex;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.8);
z-index: 999999;
opacity: 0;
visibility: hidden;
transition: all 0.3s;
align-items: center;
justify-content: center;
}
.wpdd-modal.active {
opacity: 1;
visibility: visible;
}
.wpdd-modal-content {
background: white;
max-width: 600px;
width: 90%;
max-height: 80vh;
border-radius: 8px;
overflow: hidden;
transform: scale(0.8);
transition: transform 0.3s;
}
.wpdd-modal.active .wpdd-modal-content {
transform: scale(1);
}
.wpdd-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 1px solid #eee;
}
.wpdd-modal-header h2 {
margin: 0;
font-size: 20px;
}
.wpdd-modal-close {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #666;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
}
.wpdd-modal-body {
padding: 20px;
max-height: 50vh;
overflow-y: auto;
}
.wpdd-modal-image {
width: 100%;
max-width: 200px;
height: auto;
float: left;
margin: 0 20px 20px 0;
border-radius: 4px;
}
.wpdd-modal-footer {
padding: 20px;
border-top: 1px solid #eee;
text-align: right;
}
.wpdd-notice {
position: fixed;
top: 32px;
right: 20px;
max-width: 400px;
z-index: 999999;
opacity: 0;
transform: translateX(100%);
transition: all 0.3s;
border-radius: 4px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.wpdd-notice.active {
opacity: 1;
transform: translateX(0);
}
.wpdd-notice p {
margin: 0;
padding: 15px 40px 15px 15px;
}
.wpdd-notice-dismiss {
position: absolute;
top: 5px;
right: 10px;
background: none;
border: none;
font-size: 18px;
cursor: pointer;
color: inherit;
opacity: 0.7;
}
.wpdd-notice-success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.wpdd-notice-error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.wpdd-notice-info {
background: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
.loading {
position: relative;
pointer-events: none;
opacity: 0.7;
}
.loading::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 16px;
height: 16px;
margin: -8px 0 0 -8px;
border: 2px solid transparent;
border-top: 2px solid currentColor;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@media (max-width: 600px) {
.wpdd-modal-content {
width: 95%;
}
.wpdd-modal-image {
float: none;
margin: 0 0 20px 0;
max-width: 100%;
}
.wpdd-notice {
right: 10px;
left: 10px;
max-width: none;
}
}
</style>
`;
$('head').append(styles);
}
});

346
assets/js/paypal.js Normal file
View File

@@ -0,0 +1,346 @@
jQuery(document).ready(function($) {
'use strict';
/**
* PayPal Integration Object
*/
window.WPDD_PayPal = {
init: function() {
if (typeof paypal !== 'undefined') {
this.renderPayPalButton();
}
},
renderPayPalButton: function() {
var $container = $('#wpdd-paypal-button');
if (!$container.length) {
return;
}
var $form = $container.closest('form');
paypal.Buttons({
createOrder: function(data, actions) {
return WPDD_PayPal.createOrder($form);
},
onApprove: function(data, actions) {
return WPDD_PayPal.captureOrder(data.orderID);
},
onError: function(err) {
console.error('PayPal Error:', err);
WPDD_PayPal.showError('Payment processing failed. Please try again.');
},
onCancel: function(data) {
WPDD_PayPal.showNotice('Payment was cancelled.', 'info');
}
}).render('#wpdd-paypal-button');
},
createOrder: function($form) {
return new Promise(function(resolve, reject) {
var formData = $form.serialize();
$.ajax({
url: wpdd_paypal.ajax_url,
type: 'POST',
data: formData + '&action=wpdd_create_paypal_order&nonce=' + wpdd_paypal.nonce,
success: function(response) {
if (response.success) {
resolve(response.data.orderID);
} else {
reject(new Error(response.data || 'Failed to create PayPal order'));
}
},
error: function(xhr, status, error) {
reject(new Error('Network error: ' + error));
}
});
});
},
captureOrder: function(orderID) {
return new Promise(function(resolve, reject) {
$.ajax({
url: wpdd_paypal.ajax_url,
type: 'POST',
data: {
action: 'wpdd_capture_paypal_order',
orderID: orderID,
nonce: wpdd_paypal.nonce
},
success: function(response) {
if (response.success) {
// Redirect to thank you page
window.location.href = response.data.redirect_url;
} else {
reject(new Error(response.data || 'Failed to capture payment'));
}
},
error: function(xhr, status, error) {
reject(new Error('Network error: ' + error));
}
});
});
},
showError: function(message) {
this.showNotice(message, 'error');
},
showNotice: function(message, type) {
type = type || 'info';
// Remove existing notices
$('.wpdd-paypal-notice').remove();
var notice = $('<div class="wpdd-paypal-notice wpdd-notice-' + type + '">' +
'<p>' + message + '</p>' +
'<button class="wpdd-notice-dismiss">&times;</button>' +
'</div>');
$('#wpdd-paypal-button').before(notice);
// Auto-dismiss after 5 seconds
setTimeout(function() {
notice.fadeOut(function() {
notice.remove();
});
}, 5000);
// Manual dismiss
notice.find('.wpdd-notice-dismiss').on('click', function() {
notice.fadeOut(function() {
notice.remove();
});
});
},
validateForm: function($form) {
var isValid = true;
var errors = [];
// Check required fields
$form.find('input[required]').each(function() {
var $field = $(this);
var value = $field.val().trim();
var fieldName = $field.attr('name') || $field.attr('id');
if (!value) {
isValid = false;
errors.push(fieldName + ' is required');
$field.addClass('error');
} else {
$field.removeClass('error');
}
});
// Validate email format
$form.find('input[type="email"]').each(function() {
var $field = $(this);
var value = $field.val().trim();
var emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (value && !emailRegex.test(value)) {
isValid = false;
errors.push('Please enter a valid email address');
$field.addClass('error');
}
});
if (!isValid) {
this.showError(errors.join('<br>'));
}
return isValid;
},
showLoadingState: function() {
$('#wpdd-paypal-button').addClass('wpdd-loading');
},
hideLoadingState: function() {
$('#wpdd-paypal-button').removeClass('wpdd-loading');
},
disableForm: function($form) {
$form.find('input, select, button').prop('disabled', true);
},
enableForm: function($form) {
$form.find('input, select, button').prop('disabled', false);
}
};
// Initialize PayPal integration after object definition
WPDD_PayPal.init();
// Add PayPal-specific styles
$('<style>')
.prop('type', 'text/css')
.html(`
#wpdd-paypal-button {
margin: 20px 0;
min-height: 45px;
}
.wpdd-paypal-notice {
padding: 12px 16px;
margin-bottom: 15px;
border-radius: 4px;
position: relative;
}
.wpdd-notice-success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.wpdd-notice-error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.wpdd-notice-info {
background: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
.wpdd-notice-dismiss {
position: absolute;
top: 8px;
right: 12px;
background: none;
border: none;
font-size: 16px;
cursor: pointer;
color: inherit;
opacity: 0.7;
padding: 0;
line-height: 1;
}
.wpdd-notice-dismiss:hover {
opacity: 1;
}
.wpdd-loading {
position: relative;
opacity: 0.6;
pointer-events: none;
}
.wpdd-loading::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 20px;
height: 20px;
margin: -10px 0 0 -10px;
border: 2px solid #ccc;
border-top: 2px solid #0073aa;
border-radius: 50%;
animation: wpdd-paypal-spin 1s linear infinite;
}
@keyframes wpdd-paypal-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
input.error {
border-color: #dc3545 !important;
box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25) !important;
}
.wpdd-checkout-section {
transition: opacity 0.3s;
}
.wpdd-checkout-section.disabled {
opacity: 0.5;
pointer-events: none;
}
/* PayPal button container responsive */
@media (max-width: 400px) {
#wpdd-paypal-button {
min-height: 40px;
}
}
/* Form validation styling */
.wpdd-form-row {
margin-bottom: 15px;
}
.wpdd-form-row.has-error input {
border-color: #dc3545;
}
.wpdd-field-error {
color: #dc3545;
font-size: 12px;
margin-top: 5px;
display: none;
}
.wpdd-form-row.has-error .wpdd-field-error {
display: block;
}
`)
.appendTo('head');
// Form validation enhancement
$(document).on('blur', '#wpdd-checkout-form input[required]', function() {
var $field = $(this);
var $row = $field.closest('.wpdd-form-row');
var value = $field.val().trim();
if (!value) {
$row.addClass('has-error');
} else {
$row.removeClass('has-error');
}
});
// Real-time email validation
$(document).on('input', '#wpdd-checkout-form input[type="email"]', function() {
var $field = $(this);
var $row = $field.closest('.wpdd-form-row');
var value = $field.val().trim();
var emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (value && !emailRegex.test(value)) {
$row.addClass('has-error');
} else {
$row.removeClass('has-error');
}
});
// Prevent double-submission
var formSubmitting = false;
$(document).on('submit', '#wpdd-checkout-form', function(e) {
if (formSubmitting) {
e.preventDefault();
return false;
}
formSubmitting = true;
// Re-enable after 5 seconds as failsafe
setTimeout(function() {
formSubmitting = false;
}, 5000);
});
});

View File

@@ -0,0 +1,491 @@
<?php
if (!defined('ABSPATH')) {
exit;
}
class WPDD_Ajax {
public static function init() {
add_action('wp_ajax_wpdd_process_free_download', array(__CLASS__, 'process_free_download'));
add_action('wp_ajax_nopriv_wpdd_process_free_download', array(__CLASS__, 'process_free_download'));
add_action('wp_ajax_wpdd_add_to_cart', array(__CLASS__, 'add_to_cart'));
add_action('wp_ajax_nopriv_wpdd_add_to_cart', array(__CLASS__, 'add_to_cart'));
add_action('wp_ajax_wpdd_get_product_details', array(__CLASS__, 'get_product_details'));
add_action('wp_ajax_nopriv_wpdd_get_product_details', array(__CLASS__, 'get_product_details'));
add_action('wp_ajax_wpdd_check_download_status', array(__CLASS__, 'check_download_status'));
add_action('wp_ajax_nopriv_wpdd_check_download_status', array(__CLASS__, 'check_download_status'));
add_action('wp_ajax_wpdd_move_to_protected', array(__CLASS__, 'move_to_protected'));
add_action('wp_ajax_wpdd_upload_protected_file', array(__CLASS__, 'upload_protected_file'));
// Customer management actions
add_action('wp_ajax_wpdd_change_password', array(__CLASS__, 'change_password'));
}
public static function process_free_download() {
check_ajax_referer('wpdd-ajax-nonce', 'nonce');
$product_id = isset($_POST['product_id']) ? intval($_POST['product_id']) : 0;
if (!$product_id) {
wp_send_json_error(__('Invalid product.', 'wp-digital-download'));
}
$product = get_post($product_id);
if (!$product || $product->post_type !== 'wpdd_product') {
wp_send_json_error(__('Product not found.', 'wp-digital-download'));
}
// IMPORTANT: Always check the actual database values, never trust client input
$is_free = get_post_meta($product_id, '_wpdd_is_free', true);
$price = get_post_meta($product_id, '_wpdd_price', true);
$sale_price = get_post_meta($product_id, '_wpdd_sale_price', true);
// Calculate actual price
$actual_price = ($sale_price && $sale_price < $price) ? $sale_price : $price;
// Security check: Verify this is actually a free product
if (!$is_free && $actual_price > 0) {
// Log potential security breach attempt
error_log('WPDD Security: Attempted to download paid product ' . $product_id . ' as free from IP: ' . $_SERVER['REMOTE_ADDR']);
wp_send_json_error(__('This product is not available for free download.', 'wp-digital-download'));
}
$customer_data = array(
'email' => sanitize_email($_POST['customer_email'] ?? ''),
'name' => sanitize_text_field($_POST['customer_name'] ?? '')
);
if (!is_user_logged_in() && empty($customer_data['email'])) {
wp_send_json_error(__('Please provide your email address.', 'wp-digital-download'));
}
// Create account if requested
if (!is_user_logged_in() && isset($_POST['create_account']) && $_POST['create_account'] == '1') {
$user_id = wp_create_user(
$customer_data['email'],
wp_generate_password(),
$customer_data['email']
);
if (!is_wp_error($user_id)) {
wp_update_user(array(
'ID' => $user_id,
'display_name' => $customer_data['name'],
'first_name' => $customer_data['name']
));
// Set customer role (remove default role first)
$user = new WP_User($user_id);
$user->remove_role('subscriber'); // Remove default WordPress role
$user->add_role('wpdd_customer');
// Send new account email
wp_new_user_notification($user_id, null, 'user');
// Log them in
wp_set_current_user($user_id);
wp_set_auth_cookie($user_id);
}
}
$order_id = WPDD_Orders::create_order($product_id, $customer_data, 'free');
if ($order_id) {
$order = WPDD_Orders::get_order($order_id);
wp_send_json_success(array(
'redirect_url' => add_query_arg(
'order_id',
$order->order_number,
get_permalink(get_option('wpdd_thank_you_page_id'))
)
));
} else {
wp_send_json_error(__('Failed to process download. Please try again.', 'wp-digital-download'));
}
}
public static function add_to_cart() {
check_ajax_referer('wpdd-ajax-nonce', 'nonce');
$product_id = isset($_POST['product_id']) ? intval($_POST['product_id']) : 0;
if (!$product_id) {
wp_send_json_error(__('Invalid product.', 'wp-digital-download'));
}
$product = get_post($product_id);
if (!$product || $product->post_type !== 'wpdd_product') {
wp_send_json_error(__('Product not found.', 'wp-digital-download'));
}
if (!isset($_SESSION['wpdd_cart'])) {
$_SESSION['wpdd_cart'] = array();
}
if (in_array($product_id, $_SESSION['wpdd_cart'])) {
wp_send_json_error(__('Product already in cart.', 'wp-digital-download'));
}
$_SESSION['wpdd_cart'][] = $product_id;
wp_send_json_success(array(
'message' => __('Product added to cart.', 'wp-digital-download'),
'cart_count' => count($_SESSION['wpdd_cart']),
'checkout_url' => add_query_arg(
'product_id',
$product_id,
get_permalink(get_option('wpdd_checkout_page_id'))
)
));
}
public static function get_product_details() {
check_ajax_referer('wpdd-ajax-nonce', 'nonce');
$product_id = isset($_POST['product_id']) ? intval($_POST['product_id']) : 0;
if (!$product_id) {
wp_send_json_error(__('Invalid product.', 'wp-digital-download'));
}
$product = get_post($product_id);
if (!$product || $product->post_type !== 'wpdd_product') {
wp_send_json_error(__('Product not found.', 'wp-digital-download'));
}
$price = get_post_meta($product_id, '_wpdd_price', true);
$sale_price = get_post_meta($product_id, '_wpdd_sale_price', true);
$is_free = get_post_meta($product_id, '_wpdd_is_free', true);
$files = get_post_meta($product_id, '_wpdd_files', true);
$download_limit = get_post_meta($product_id, '_wpdd_download_limit', true);
$download_expiry = get_post_meta($product_id, '_wpdd_download_expiry', true);
$creator = get_userdata($product->post_author);
$data = array(
'id' => $product_id,
'title' => $product->post_title,
'description' => $product->post_content,
'excerpt' => $product->post_excerpt,
'price' => $price,
'sale_price' => $sale_price,
'is_free' => $is_free,
'final_price' => $is_free ? 0 : (($sale_price && $sale_price < $price) ? $sale_price : $price),
'creator' => array(
'id' => $creator->ID,
'name' => $creator->display_name,
'avatar' => get_avatar_url($creator->ID)
),
'files_count' => is_array($files) ? count($files) : 0,
'download_limit' => $download_limit ?: __('Unlimited', 'wp-digital-download'),
'download_expiry' => $download_expiry ? sprintf(__('%d days', 'wp-digital-download'), $download_expiry) : __('Never expires', 'wp-digital-download'),
'thumbnail' => get_the_post_thumbnail_url($product_id, 'full'),
'categories' => wp_get_post_terms($product_id, 'wpdd_product_category', array('fields' => 'names')),
'tags' => wp_get_post_terms($product_id, 'wpdd_product_tag', array('fields' => 'names'))
);
if (is_user_logged_in()) {
$current_user = wp_get_current_user();
$data['can_download'] = WPDD_Customer::can_download_product($current_user->ID, $product_id);
}
wp_send_json_success($data);
}
public static function check_download_status() {
check_ajax_referer('wpdd-ajax-nonce', 'nonce');
if (!is_user_logged_in()) {
wp_send_json_error(__('Please login to check download status.', 'wp-digital-download'));
}
$product_id = isset($_POST['product_id']) ? intval($_POST['product_id']) : 0;
if (!$product_id) {
wp_send_json_error(__('Invalid product.', 'wp-digital-download'));
}
$current_user = wp_get_current_user();
global $wpdb;
$order = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}wpdd_orders
WHERE customer_id = %d
AND product_id = %d
AND status = 'completed'
ORDER BY purchase_date DESC
LIMIT 1",
$current_user->ID,
$product_id
));
if (!$order) {
wp_send_json_success(array(
'has_purchased' => false,
'message' => __('You have not purchased this product.', 'wp-digital-download')
));
}
$download_limit = get_post_meta($product_id, '_wpdd_download_limit', true);
$download_expiry = get_post_meta($product_id, '_wpdd_download_expiry', true);
$can_download = true;
$message = '';
if ($download_expiry > 0) {
$expiry_date = date('Y-m-d H:i:s', strtotime($order->purchase_date . ' + ' . $download_expiry . ' days'));
if (current_time('mysql') > $expiry_date) {
$can_download = false;
$message = __('Your download period has expired.', 'wp-digital-download');
}
}
if ($can_download && $download_limit > 0 && $order->download_count >= $download_limit) {
$can_download = false;
$message = __('You have reached the download limit.', 'wp-digital-download');
}
wp_send_json_success(array(
'has_purchased' => true,
'can_download' => $can_download,
'download_count' => $order->download_count,
'download_limit' => $download_limit ?: 0,
'purchase_date' => $order->purchase_date,
'message' => $message ?: __('You can download this product.', 'wp-digital-download'),
'download_url' => $can_download ? wp_nonce_url(
add_query_arg(array('wpdd_download' => $order->id)),
'wpdd_download_' . $order->id
) : ''
));
}
public static function move_to_protected() {
check_ajax_referer('wpdd-admin-nonce', 'nonce');
if (!current_user_can('edit_wpdd_products')) {
wp_send_json_error(__('You do not have permission to perform this action.', 'wp-digital-download'));
}
$attachment_id = isset($_POST['attachment_id']) ? intval($_POST['attachment_id']) : 0;
if (!$attachment_id) {
wp_send_json_error(__('Invalid attachment ID.', 'wp-digital-download'));
}
$file_path = get_attached_file($attachment_id);
if (!$file_path || !file_exists($file_path)) {
wp_send_json_error(__('File not found.', 'wp-digital-download'));
}
// Create protected directory if it doesn't exist
$upload_dir = wp_upload_dir();
$protected_dir = trailingslashit($upload_dir['basedir']) . WPDD_UPLOADS_DIR;
if (!file_exists($protected_dir)) {
wp_mkdir_p($protected_dir);
// Add .htaccess protection
$htaccess_content = "Options -Indexes\ndeny from all\n";
file_put_contents($protected_dir . '/.htaccess', $htaccess_content);
// Add index.php protection
$index_content = "<?php\n// Silence is golden.\n";
file_put_contents($protected_dir . '/index.php', $index_content);
}
// Create subdirectory based on year/month
$time = current_time('mysql');
$y = substr($time, 0, 4);
$m = substr($time, 5, 2);
$subdir = "$y/$m";
$protected_subdir = trailingslashit($protected_dir) . $subdir;
if (!file_exists($protected_subdir)) {
wp_mkdir_p($protected_subdir);
}
// Generate unique filename
$filename = basename($file_path);
$unique_filename = wp_unique_filename($protected_subdir, $filename);
$new_file_path = trailingslashit($protected_subdir) . $unique_filename;
// Move file to protected directory
if (rename($file_path, $new_file_path)) {
// Update attachment metadata
update_attached_file($attachment_id, $new_file_path);
// Store protected path reference
update_post_meta($attachment_id, '_wpdd_protected_path', $new_file_path);
// Generate secure token for download URL
$token = wp_generate_password(32, false);
update_post_meta($attachment_id, '_wpdd_download_token', $token);
wp_send_json_success(array(
'protected_url' => home_url('?wpdd_file_download=' . $attachment_id . '&token=' . $token),
'protected_path' => $new_file_path,
'message' => __('File moved to protected directory.', 'wp-digital-download')
));
} else {
wp_send_json_error(__('Failed to move file to protected directory.', 'wp-digital-download'));
}
}
public static function upload_protected_file() {
check_ajax_referer('wpdd-admin-nonce', 'nonce');
if (!current_user_can('edit_wpdd_products')) {
wp_send_json_error(__('You do not have permission to upload files.', 'wp-digital-download'));
}
if (!isset($_FILES['file'])) {
wp_send_json_error(__('No file uploaded.', 'wp-digital-download'));
}
$uploaded_file = $_FILES['file'];
// Check for upload errors
if ($uploaded_file['error'] !== UPLOAD_ERR_OK) {
wp_send_json_error(__('Upload failed.', 'wp-digital-download'));
}
// Validate file type
$allowed_types = array(
'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx',
'zip', 'rar', '7z', 'tar', 'gz',
'jpg', 'jpeg', 'png', 'gif', 'svg', 'webp',
'mp3', 'wav', 'flac', 'aac', 'ogg',
'mp4', 'avi', 'mkv', 'mov', 'wmv',
'txt', 'rtf', 'epub', 'mobi'
);
$file_ext = strtolower(pathinfo($uploaded_file['name'], PATHINFO_EXTENSION));
if (!in_array($file_ext, $allowed_types)) {
wp_send_json_error(__('File type not allowed.', 'wp-digital-download'));
}
// Create protected directory structure
$upload_dir = wp_upload_dir();
$protected_dir = trailingslashit($upload_dir['basedir']) . WPDD_UPLOADS_DIR;
if (!file_exists($protected_dir)) {
wp_mkdir_p($protected_dir);
// Add .htaccess protection
$htaccess_content = "Options -Indexes\ndeny from all\n";
file_put_contents($protected_dir . '/.htaccess', $htaccess_content);
// Add index.php protection
$index_content = "<?php\n// Silence is golden.\n";
file_put_contents($protected_dir . '/index.php', $index_content);
}
// Create subdirectory based on year/month
$time = current_time('mysql');
$y = substr($time, 0, 4);
$m = substr($time, 5, 2);
$subdir = "$y/$m";
$protected_subdir = trailingslashit($protected_dir) . $subdir;
if (!file_exists($protected_subdir)) {
wp_mkdir_p($protected_subdir);
}
// Generate unique filename
$filename = sanitize_file_name($uploaded_file['name']);
$unique_filename = wp_unique_filename($protected_subdir, $filename);
$file_path = trailingslashit($protected_subdir) . $unique_filename;
// Move uploaded file to protected directory
if (move_uploaded_file($uploaded_file['tmp_name'], $file_path)) {
// Generate secure token for download URL
$token = wp_generate_password(32, false);
$file_id = 'wpdd_' . uniqid();
// Store file metadata
$file_meta = array(
'file_path' => $file_path,
'file_name' => $unique_filename,
'original_name' => $uploaded_file['name'],
'file_size' => filesize($file_path),
'file_type' => $uploaded_file['type'],
'upload_date' => current_time('mysql'),
'token' => $token
);
// Store in options table (in production, consider custom table)
update_option('wpdd_protected_file_' . $file_id, $file_meta);
wp_send_json_success(array(
'protected_url' => home_url('?wpdd_protected_download=' . $file_id . '&token=' . $token),
'file_id' => $file_id,
'file_name' => $unique_filename,
'file_size' => size_format($file_meta['file_size']),
'message' => __('File uploaded successfully to protected directory.', 'wp-digital-download')
));
} else {
wp_send_json_error(__('Failed to save uploaded file.', 'wp-digital-download'));
}
}
/**
* Handle password change for customers
*/
public static function change_password() {
check_ajax_referer('wpdd_change_password', 'nonce');
if (!is_user_logged_in()) {
wp_send_json_error(__('You must be logged in to change your password.', 'wp-digital-download'));
}
$current_user = wp_get_current_user();
// Only allow customers to change their own password this way
if (!in_array('wpdd_customer', $current_user->roles)) {
wp_send_json_error(__('This function is only available for customers.', 'wp-digital-download'));
}
$current_password = sanitize_text_field($_POST['current_password'] ?? '');
$new_password = sanitize_text_field($_POST['new_password'] ?? '');
$confirm_password = sanitize_text_field($_POST['confirm_password'] ?? '');
if (empty($current_password) || empty($new_password) || empty($confirm_password)) {
wp_send_json_error(__('All password fields are required.', 'wp-digital-download'));
}
if ($new_password !== $confirm_password) {
wp_send_json_error(__('New passwords do not match.', 'wp-digital-download'));
}
if (strlen($new_password) < 8) {
wp_send_json_error(__('New password must be at least 8 characters long.', 'wp-digital-download'));
}
// Verify current password
if (!wp_check_password($current_password, $current_user->user_pass, $current_user->ID)) {
wp_send_json_error(__('Current password is incorrect.', 'wp-digital-download'));
}
// Update password
$result = wp_update_user(array(
'ID' => $current_user->ID,
'user_pass' => $new_password
));
if (is_wp_error($result)) {
wp_send_json_error(__('Failed to update password. Please try again.', 'wp-digital-download'));
}
// Re-authenticate user to prevent logout
wp_set_current_user($current_user->ID);
wp_set_auth_cookie($current_user->ID);
wp_send_json_success(__('Password changed successfully!', 'wp-digital-download'));
}
}

View File

@@ -0,0 +1,177 @@
<?php
if (!defined('ABSPATH')) {
exit;
}
class WPDD_Creator {
public static function init() {
add_action('show_user_profile', array(__CLASS__, 'add_profile_fields'));
add_action('edit_user_profile', array(__CLASS__, 'add_profile_fields'));
add_action('personal_options_update', array(__CLASS__, 'save_profile_fields'));
add_action('edit_user_profile_update', array(__CLASS__, 'save_profile_fields'));
add_action('user_register', array(__CLASS__, 'set_default_fields'));
add_action('wpdd_order_completed', array(__CLASS__, 'add_earnings_to_balance'));
}
public static function add_profile_fields($user) {
// Only show for creators and admins
if (!self::is_creator($user->ID) && !current_user_can('manage_options')) {
return;
}
?>
<h3><?php _e('Creator Payout Settings', 'wp-digital-download'); ?></h3>
<table class="form-table">
<tr>
<th><label for="wpdd_paypal_email"><?php _e('PayPal Email', 'wp-digital-download'); ?></label></th>
<td>
<input type="email" name="wpdd_paypal_email" id="wpdd_paypal_email"
value="<?php echo esc_attr(get_user_meta($user->ID, 'wpdd_paypal_email', true)); ?>"
class="regular-text" />
<p class="description"><?php _e('PayPal email address for receiving payouts', 'wp-digital-download'); ?></p>
</td>
</tr>
<?php if (current_user_can('manage_options')) : ?>
<tr>
<th><label><?php _e('Creator Balance', 'wp-digital-download'); ?></label></th>
<td>
<?php
$balance = self::get_creator_balance($user->ID);
$currency = get_option('wpdd_currency', 'USD');
echo '<strong>' . wpdd_format_price($balance, $currency) . '</strong>';
?>
<p class="description"><?php _e('Current unpaid earnings balance', 'wp-digital-download'); ?></p>
</td>
</tr>
<tr>
<th><label><?php _e('Total Earnings', 'wp-digital-download'); ?></label></th>
<td>
<?php
$total = self::get_creator_total_earnings($user->ID);
echo '<strong>' . wpdd_format_price($total, $currency) . '</strong>';
?>
<p class="description"><?php _e('Total lifetime earnings', 'wp-digital-download'); ?></p>
</td>
</tr>
<?php endif; ?>
</table>
<?php
}
public static function save_profile_fields($user_id) {
if (!current_user_can('edit_user', $user_id)) {
return;
}
if (isset($_POST['wpdd_paypal_email'])) {
$email = sanitize_email($_POST['wpdd_paypal_email']);
if (!empty($email) && !is_email($email)) {
return;
}
update_user_meta($user_id, 'wpdd_paypal_email', $email);
}
}
public static function set_default_fields($user_id) {
if (self::is_creator($user_id)) {
update_user_meta($user_id, 'wpdd_creator_balance', 0);
update_user_meta($user_id, 'wpdd_total_earnings', 0);
}
}
public static function is_creator($user_id) {
$user = get_userdata($user_id);
return $user && in_array('wpdd_creator', (array) $user->roles);
}
public static function get_creator_balance($user_id) {
return floatval(get_user_meta($user_id, 'wpdd_creator_balance', true));
}
public static function get_creator_total_earnings($user_id) {
global $wpdb;
$total = $wpdb->get_var($wpdb->prepare(
"SELECT SUM(o.total)
FROM {$wpdb->prefix}wpdd_orders o
INNER JOIN {$wpdb->posts} p ON o.product_id = p.ID
WHERE p.post_author = %d
AND o.status = 'completed'",
$user_id
));
return floatval($total);
}
public static function get_creator_net_earnings($user_id) {
$total = self::get_creator_total_earnings($user_id);
$commission_rate = floatval(get_option('wpdd_commission_rate', 0));
$net = $total * (1 - ($commission_rate / 100));
return $net;
}
public static function add_earnings_to_balance($order_id) {
global $wpdb;
$order = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}wpdd_orders WHERE id = %d",
$order_id
));
if (!$order || $order->status !== 'completed') {
return;
}
$product = get_post($order->product_id);
if (!$product) {
return;
}
$creator_id = $product->post_author;
$commission_rate = floatval(get_option('wpdd_commission_rate', 0));
$creator_share = $order->total * (1 - ($commission_rate / 100));
// Update creator balance
$current_balance = self::get_creator_balance($creator_id);
update_user_meta($creator_id, 'wpdd_creator_balance', $current_balance + $creator_share);
// Log the earning
$wpdb->insert(
$wpdb->prefix . 'wpdd_creator_earnings',
array(
'creator_id' => $creator_id,
'order_id' => $order_id,
'product_id' => $order->product_id,
'sale_amount' => $order->total,
'commission_rate' => $commission_rate,
'creator_earning' => $creator_share,
'created_at' => current_time('mysql')
),
array('%d', '%d', '%d', '%f', '%f', '%f', '%s')
);
}
public static function get_creators_with_balance() {
global $wpdb;
$threshold = floatval(get_option('wpdd_payout_threshold', 0));
$query = "SELECT u.ID, u.display_name, u.user_email,
um1.meta_value as paypal_email,
um2.meta_value as balance
FROM {$wpdb->users} u
INNER JOIN {$wpdb->usermeta} um ON u.ID = um.user_id
LEFT JOIN {$wpdb->usermeta} um1 ON u.ID = um1.user_id AND um1.meta_key = 'wpdd_paypal_email'
LEFT JOIN {$wpdb->usermeta} um2 ON u.ID = um2.user_id AND um2.meta_key = 'wpdd_creator_balance'
WHERE um.meta_key = '{$wpdb->prefix}capabilities'
AND um.meta_value LIKE '%wpdd_creator%'
AND CAST(um2.meta_value AS DECIMAL(10,2)) > 0";
if ($threshold > 0) {
$query .= $wpdb->prepare(" AND CAST(um2.meta_value AS DECIMAL(10,2)) >= %f", $threshold);
}
return $wpdb->get_results($query);
}
}

View File

@@ -0,0 +1,377 @@
<?php
if (!defined('ABSPATH')) {
exit;
}
class WPDD_Customer {
public static function init() {
add_action('wp_dashboard_setup', array(__CLASS__, 'add_dashboard_widgets'));
add_filter('login_redirect', array(__CLASS__, 'login_redirect'), 10, 3);
add_action('show_user_profile', array(__CLASS__, 'add_customer_fields'));
add_action('edit_user_profile', array(__CLASS__, 'add_customer_fields'));
// Block wp-admin access for customers
add_action('admin_init', array(__CLASS__, 'restrict_admin_access'));
// Add frontend logout and account management
add_action('wp_footer', array(__CLASS__, 'add_customer_scripts'));
}
public static function add_dashboard_widgets() {
if (current_user_can('wpdd_view_purchases')) {
wp_add_dashboard_widget(
'wpdd_customer_recent_purchases',
__('Recent Purchases', 'wp-digital-download'),
array(__CLASS__, 'recent_purchases_widget')
);
}
if (current_user_can('wpdd_view_own_sales')) {
wp_add_dashboard_widget(
'wpdd_creator_sales_summary',
__('Sales Summary', 'wp-digital-download'),
array(__CLASS__, 'sales_summary_widget')
);
}
}
public static function recent_purchases_widget() {
global $wpdb;
$current_user = wp_get_current_user();
$recent_orders = $wpdb->get_results($wpdb->prepare(
"SELECT o.*, p.post_title as product_name
FROM {$wpdb->prefix}wpdd_orders o
LEFT JOIN {$wpdb->posts} p ON o.product_id = p.ID
WHERE o.customer_id = %d
AND o.status = 'completed'
ORDER BY o.purchase_date DESC
LIMIT 5",
$current_user->ID
));
if ($recent_orders) {
echo '<ul>';
foreach ($recent_orders as $order) {
printf(
'<li>%s - <a href="%s">%s</a> ($%s)</li>',
date_i18n(get_option('date_format'), strtotime($order->purchase_date)),
get_permalink($order->product_id),
esc_html($order->product_name),
number_format($order->amount, 2)
);
}
echo '</ul>';
printf(
'<p><a href="%s" class="button">%s</a></p>',
get_permalink(get_option('wpdd_purchases_page_id')),
__('View All Purchases', 'wp-digital-download')
);
} else {
echo '<p>' . __('No purchases yet.', 'wp-digital-download') . '</p>';
printf(
'<p><a href="%s" class="button button-primary">%s</a></p>',
get_permalink(get_option('wpdd_shop_page_id')),
__('Browse Products', 'wp-digital-download')
);
}
}
public static function sales_summary_widget() {
global $wpdb;
$current_user = wp_get_current_user();
$stats = $wpdb->get_row($wpdb->prepare(
"SELECT
COUNT(*) as total_sales,
SUM(amount) as total_revenue,
COUNT(DISTINCT product_id) as products_sold
FROM {$wpdb->prefix}wpdd_orders
WHERE creator_id = %d
AND status = 'completed'
AND purchase_date >= DATE_SUB(NOW(), INTERVAL 30 DAY)",
$current_user->ID
));
$recent_sales = $wpdb->get_results($wpdb->prepare(
"SELECT o.*, p.post_title as product_name
FROM {$wpdb->prefix}wpdd_orders o
LEFT JOIN {$wpdb->posts} p ON o.product_id = p.ID
WHERE o.creator_id = %d
AND o.status = 'completed'
ORDER BY o.purchase_date DESC
LIMIT 5",
$current_user->ID
));
?>
<div class="wpdd-sales-summary">
<div class="wpdd-stats-grid">
<div class="wpdd-stat">
<span class="wpdd-stat-value"><?php echo intval($stats->total_sales); ?></span>
<span class="wpdd-stat-label"><?php _e('Sales (30 days)', 'wp-digital-download'); ?></span>
</div>
<div class="wpdd-stat">
<span class="wpdd-stat-value">$<?php echo number_format($stats->total_revenue ?: 0, 2); ?></span>
<span class="wpdd-stat-label"><?php _e('Revenue (30 days)', 'wp-digital-download'); ?></span>
</div>
</div>
<?php if ($recent_sales) : ?>
<h4><?php _e('Recent Sales', 'wp-digital-download'); ?></h4>
<ul>
<?php foreach ($recent_sales as $sale) : ?>
<li>
<?php echo date_i18n(get_option('date_format'), strtotime($sale->purchase_date)); ?> -
<?php echo esc_html($sale->product_name); ?>
($<?php echo number_format($sale->amount, 2); ?>)
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
<p>
<a href="<?php echo admin_url('edit.php?post_type=wpdd_product'); ?>" class="button">
<?php _e('Manage Products', 'wp-digital-download'); ?>
</a>
</p>
</div>
<style>
.wpdd-stats-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
margin-bottom: 20px;
}
.wpdd-stat {
text-align: center;
padding: 10px;
background: #f0f0f1;
border-radius: 4px;
}
.wpdd-stat-value {
display: block;
font-size: 24px;
font-weight: 600;
color: #2271b1;
}
.wpdd-stat-label {
display: block;
font-size: 12px;
color: #646970;
margin-top: 5px;
}
</style>
<?php
}
public static function login_redirect($redirect_to, $requested_redirect_to, $user) {
if (!is_wp_error($user) && in_array('wpdd_customer', $user->roles)) {
$purchases_page = get_option('wpdd_purchases_page_id');
if ($purchases_page) {
return get_permalink($purchases_page);
}
}
return $redirect_to;
}
public static function add_customer_fields($user) {
if (!in_array('wpdd_customer', $user->roles)) {
return;
}
global $wpdb;
$total_purchases = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->prefix}wpdd_orders
WHERE customer_id = %d AND status = 'completed'",
$user->ID
));
$total_spent = $wpdb->get_var($wpdb->prepare(
"SELECT SUM(amount) FROM {$wpdb->prefix}wpdd_orders
WHERE customer_id = %d AND status = 'completed'",
$user->ID
));
?>
<h3><?php _e('Customer Information', 'wp-digital-download'); ?></h3>
<table class="form-table">
<tr>
<th><?php _e('Total Purchases', 'wp-digital-download'); ?></th>
<td><?php echo intval($total_purchases); ?></td>
</tr>
<tr>
<th><?php _e('Total Spent', 'wp-digital-download'); ?></th>
<td>$<?php echo number_format($total_spent ?: 0, 2); ?></td>
</tr>
</table>
<?php
}
public static function get_customer_purchases($customer_id) {
global $wpdb;
return $wpdb->get_results($wpdb->prepare(
"SELECT o.*, p.post_title as product_name
FROM {$wpdb->prefix}wpdd_orders o
LEFT JOIN {$wpdb->posts} p ON o.product_id = p.ID
WHERE o.customer_id = %d
AND o.status = 'completed'
ORDER BY o.purchase_date DESC",
$customer_id
));
}
public static function can_download_product($customer_id, $product_id) {
global $wpdb;
$order = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}wpdd_orders
WHERE customer_id = %d
AND product_id = %d
AND status = 'completed'
ORDER BY purchase_date DESC
LIMIT 1",
$customer_id,
$product_id
));
if (!$order) {
return false;
}
$download_limit = get_post_meta($product_id, '_wpdd_download_limit', true);
$download_expiry = get_post_meta($product_id, '_wpdd_download_expiry', true);
if ($download_expiry > 0) {
$expiry_date = date('Y-m-d H:i:s', strtotime($order->purchase_date . ' + ' . $download_expiry . ' days'));
if (current_time('mysql') > $expiry_date) {
return false;
}
}
if ($download_limit > 0 && $order->download_count >= $download_limit) {
return false;
}
return true;
}
/**
* Block wp-admin access for customers
*/
public static function restrict_admin_access() {
$current_user = wp_get_current_user();
// Only block for wpdd_customer role, allow creators and admins
if (in_array('wpdd_customer', $current_user->roles) && !current_user_can('manage_options')) {
// Allow AJAX requests
if (defined('DOING_AJAX') && DOING_AJAX) {
return;
}
// Redirect to purchases page
$purchases_page = get_option('wpdd_purchases_page_id');
$redirect_url = $purchases_page ? get_permalink($purchases_page) : home_url();
wp_redirect($redirect_url);
exit;
}
}
/**
* Add frontend customer scripts and functionality
*/
public static function add_customer_scripts() {
if (is_user_logged_in()) {
$current_user = wp_get_current_user();
// Only for customers
if (in_array('wpdd_customer', $current_user->roles)) {
?>
<script>
// Add logout functionality to customer pages
document.addEventListener('DOMContentLoaded', function() {
// Add logout link to customer navigation if it exists
var customerNav = document.querySelector('.wpdd-customer-nav, .wpdd-shop-filters, .wpdd-customer-purchases');
if (customerNav && !document.querySelector('.wpdd-customer-logout')) {
var logoutLink = document.createElement('div');
logoutLink.className = 'wpdd-customer-logout';
logoutLink.style.cssText = 'margin-top: 10px; padding: 10px; background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 4px;';
logoutLink.innerHTML = '<strong>Welcome, <?php echo esc_js($current_user->display_name); ?>!</strong> | ' +
'<a href="<?php echo wp_logout_url(get_permalink()); ?>" style="color: #dc3545;">Logout</a> | ' +
'<a href="#" onclick="wpdd_show_password_form()" style="color: #007cba;">Change Password</a>';
customerNav.appendChild(logoutLink);
}
});
// Password change functionality
function wpdd_show_password_form() {
var passwordForm = document.getElementById('wpdd-password-form');
if (passwordForm) {
passwordForm.style.display = passwordForm.style.display === 'none' ? 'block' : 'none';
return;
}
var formHtml = '<div id="wpdd-password-form" style="margin-top: 15px; padding: 15px; background: white; border: 2px solid #007cba; border-radius: 4px;">' +
'<h4>Change Password</h4>' +
'<form id="wpdd-change-password" onsubmit="wpdd_change_password(event)">' +
'<p><input type="password" name="current_password" placeholder="Current Password" required style="width: 100%; margin-bottom: 10px; padding: 8px;"></p>' +
'<p><input type="password" name="new_password" placeholder="New Password" required style="width: 100%; margin-bottom: 10px; padding: 8px;"></p>' +
'<p><input type="password" name="confirm_password" placeholder="Confirm New Password" required style="width: 100%; margin-bottom: 10px; padding: 8px;"></p>' +
'<p><button type="submit" style="background: #007cba; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer;">Update Password</button> ' +
'<button type="button" onclick="wpdd_hide_password_form()" style="background: #6c757d; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer;">Cancel</button></p>' +
'</form></div>';
var logoutDiv = document.querySelector('.wpdd-customer-logout');
if (logoutDiv) {
logoutDiv.insertAdjacentHTML('afterend', formHtml);
}
}
function wpdd_hide_password_form() {
var passwordForm = document.getElementById('wpdd-password-form');
if (passwordForm) {
passwordForm.remove();
}
}
function wpdd_change_password(event) {
event.preventDefault();
var form = event.target;
var formData = new FormData(form);
if (formData.get('new_password') !== formData.get('confirm_password')) {
alert('New passwords do not match!');
return;
}
formData.append('action', 'wpdd_change_password');
formData.append('nonce', '<?php echo wp_create_nonce('wpdd_change_password'); ?>');
fetch('<?php echo admin_url('admin-ajax.php'); ?>', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Password changed successfully!');
wpdd_hide_password_form();
} else {
alert('Error: ' + (data.data || 'Failed to change password'));
}
})
.catch(error => {
alert('Error: ' + error.message);
});
}
</script>
<?php
}
}
}
}

View File

@@ -0,0 +1,752 @@
<?php
if (!defined('ABSPATH')) {
exit;
}
class WPDD_Download_Handler {
public static function init() {
// Use priority 1 to ensure our handler runs early
add_action('init', array(__CLASS__, 'handle_download_request'), 1);
add_action('init', array(__CLASS__, 'handle_secure_file_download'), 1);
add_action('init', array(__CLASS__, 'handle_protected_download'), 1);
// Also hook to template_redirect as a fallback
add_action('template_redirect', array(__CLASS__, 'handle_download_request'), 1);
// Debug: Add a test endpoint for admins
if (current_user_can('manage_options') && isset($_GET['wpdd_test_download'])) {
add_action('init', function() {
wp_die('WPDD Download Handler is loaded and working! Current time: ' . current_time('mysql'));
}, 0);
}
}
public static function handle_download_request() {
// TESTING: Add a test parameter to verify changes are taking effect
if (isset($_GET['test_update_working'])) {
wp_die('TEST: Changes are working! File updated successfully.');
}
// Early exit if not a download request
if (!isset($_GET['wpdd_download']) && !isset($_GET['wpdd_download_token'])) {
return;
}
// Debug logging for admin users
if (current_user_can('manage_options')) {
error_log('WPDD Debug: Download request detected!');
error_log('WPDD Debug: GET params: ' . print_r($_GET, true));
error_log('WPDD Debug: Current action: ' . current_action());
}
if (isset($_GET['wpdd_download'])) {
// Add debug output before processing
if (current_user_can('manage_options')) {
error_log('WPDD Debug: Processing download by order ID: ' . $_GET['wpdd_download']);
}
self::process_download_by_order();
exit; // Make sure we exit after processing
}
if (isset($_GET['wpdd_download_token'])) {
if (current_user_can('manage_options')) {
error_log('WPDD Debug: Processing download by token: ' . $_GET['wpdd_download_token']);
}
self::process_download_by_token();
exit; // Make sure we exit after processing
}
}
public static function handle_protected_download() {
if (!isset($_GET['wpdd_protected_download'])) {
return;
}
$file_id = sanitize_text_field($_GET['wpdd_protected_download']);
$token = sanitize_text_field($_GET['token'] ?? '');
if (!$file_id || !$token) {
wp_die(__('Invalid download link.', 'wp-digital-download'));
}
// Get file metadata
$file_meta = get_option('wpdd_protected_file_' . $file_id);
if (!$file_meta || $file_meta['token'] !== $token) {
wp_die(__('Invalid download token.', 'wp-digital-download'));
}
if (!file_exists($file_meta['file_path'])) {
wp_die(__('File not found.', 'wp-digital-download'));
}
// Deliver the file
self::deliver_protected_file($file_meta['file_path']);
}
private static function process_download_by_order() {
$download_link_id = intval($_GET['wpdd_download']);
// Debug nonce verification
if (current_user_can('manage_options')) {
error_log('WPDD Debug: Download Link ID: ' . $download_link_id);
error_log('WPDD Debug: Nonce received: ' . ($_GET['_wpnonce'] ?? 'none'));
error_log('WPDD Debug: Expected nonce action: wpdd_download_' . $download_link_id);
}
if (!wp_verify_nonce($_GET['_wpnonce'] ?? '', 'wpdd_download_' . $download_link_id)) {
if (current_user_can('manage_options')) {
error_log('WPDD Debug: Nonce verification failed!');
}
wp_die(__('Invalid download link.', 'wp-digital-download'));
}
global $wpdb;
// First get the download link to find the associated order
$download_link = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}wpdd_download_links WHERE id = %d",
$download_link_id
));
if (!$download_link) {
wp_die(__('Invalid download link.', 'wp-digital-download'));
}
$order_id = $download_link->order_id;
// Check by email if guest
if (isset($_GET['customer_email']) && isset($_GET['key'])) {
$email = sanitize_email($_GET['customer_email']);
$key = sanitize_text_field($_GET['key']);
// Verify the key
$expected_key = substr(md5($email . AUTH_KEY), 0, 10);
if ($key !== $expected_key) {
wp_die(__('Invalid access key.', 'wp-digital-download'));
}
$order = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}wpdd_orders
WHERE id = %d AND customer_email = %s AND status = 'completed'",
$order_id,
$email
));
} elseif (is_user_logged_in()) {
$current_user = wp_get_current_user();
$order = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}wpdd_orders
WHERE id = %d AND (customer_id = %d OR customer_email = %s) AND status = 'completed'",
$order_id,
$current_user->ID,
$current_user->user_email
));
} else {
// For unregistered users, try to look up order by order number from URL if available
if (isset($_GET['order_id'])) {
$order_number = sanitize_text_field($_GET['order_id']);
// Look up order by order ID and verify it matches the order number
$order = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}wpdd_orders
WHERE id = %d AND order_number = %s AND status = 'completed'",
$order_id,
$order_number
));
if (current_user_can('manage_options')) {
error_log('WPDD Debug: Guest order lookup - Order ID: ' . $order_id . ', Order Number: ' . $order_number . ', Found: ' . ($order ? 'Yes' : 'No'));
}
if (!$order) {
wp_die(__('Invalid order or order not found.', 'wp-digital-download'));
}
} else {
// Debug: Show what parameters we have
$debug_info = '';
if (current_user_can('manage_options')) {
$debug_info = '<br><br>Debug info:<br>';
$debug_info .= 'GET params: ' . print_r($_GET, true);
$debug_info .= '<br>User logged in: ' . (is_user_logged_in() ? 'Yes' : 'No');
}
wp_die(__('You must be logged in to download this product or provide a valid order reference.', 'wp-digital-download') . $debug_info);
}
}
if (!$order) {
wp_die(__('Invalid order or you do not have permission to download this product.', 'wp-digital-download'));
}
self::process_download($order);
}
private static function process_download_by_token() {
$token = sanitize_text_field($_GET['wpdd_download_token']);
global $wpdb;
$download_link = $wpdb->get_row($wpdb->prepare(
"SELECT dl.*, o.*
FROM {$wpdb->prefix}wpdd_download_links dl
INNER JOIN {$wpdb->prefix}wpdd_orders o ON dl.order_id = o.id
WHERE dl.token = %s",
$token
));
if (!$download_link) {
wp_die(__('Invalid download link.', 'wp-digital-download'));
}
if ($download_link->expires_at < current_time('mysql')) {
// Check if user still has downloads remaining
if ($download_link->max_downloads > 0 && $download_link->download_count >= $download_link->max_downloads) {
wp_die(__('This download link has expired and you have no downloads remaining.', 'wp-digital-download'));
}
// Only send refresh email if this appears to be a real user attempt (not automated)
// Check if the customer is unregistered (has no user account)
$is_unregistered_customer = ($download_link->customer_id == 0);
if ($is_unregistered_customer) {
// Generate new token and send refresh email only for unregistered customers
$new_token = self::refresh_download_token($download_link->order_id, $download_link->customer_email);
if ($new_token) {
wp_die(sprintf(
__('Your download link has expired. A new download link has been sent to %s. Please check your email and try again.', 'wp-digital-download'),
esc_html($download_link->customer_email)
));
} else {
wp_die(__('This download link has expired and could not be refreshed. Please contact support.', 'wp-digital-download'));
}
} else {
// For registered users, just show expired message (they can log in to get new links)
wp_die(__('This download link has expired. Please log in to your account to get a new download link.', 'wp-digital-download'));
}
}
if ($download_link->max_downloads > 0 && $download_link->download_count >= $download_link->max_downloads) {
wp_die(__('Download limit exceeded.', 'wp-digital-download'));
}
$wpdb->update(
$wpdb->prefix . 'wpdd_download_links',
array('download_count' => $download_link->download_count + 1),
array('id' => $download_link->id),
array('%d'),
array('%d')
);
self::process_download($download_link);
}
/**
* Refresh an expired download token and send new link via email
*/
private static function refresh_download_token($order_id, $customer_email) {
global $wpdb;
// Generate new token with extended expiry (72 hours as suggested)
$new_token = wp_hash(uniqid() . $order_id . time());
$new_expires_at = date('Y-m-d H:i:s', strtotime('+72 hours'));
// Update the existing download link with new token and expiry
$updated = $wpdb->update(
$wpdb->prefix . 'wpdd_download_links',
array(
'token' => $new_token,
'expires_at' => $new_expires_at,
'refreshed_at' => current_time('mysql')
),
array('order_id' => $order_id),
array('%s', '%s', '%s'),
array('%d')
);
if (!$updated) {
return false;
}
// Send refresh email
self::send_refresh_email($order_id, $new_token, $customer_email);
return $new_token;
}
/**
* Send refresh email with new download link
*/
private static function send_refresh_email($order_id, $token, $customer_email) {
global $wpdb;
// Get order details
$order = $wpdb->get_row($wpdb->prepare(
"SELECT o.*, p.post_title as product_name
FROM {$wpdb->prefix}wpdd_orders o
LEFT JOIN {$wpdb->posts} p ON o.product_id = p.ID
WHERE o.id = %d",
$order_id
));
if (!$order) {
return false;
}
$download_url = add_query_arg(array(
'wpdd_download_token' => $token
), home_url());
$subject = sprintf(__('New Download Link for %s', 'wp-digital-download'), $order->product_name);
$message = sprintf(
__("Hello %s,\n\nYour download link for \"%s\" has expired, so we've generated a new one for you.\n\nThis new link is valid for 72 hours and can be used for your remaining downloads.\n\nDownload Link: %s\n\nOrder Number: %s\nPurchase Date: %s\n\nIf you have any issues, please contact our support team.\n\nBest regards,\n%s", 'wp-digital-download'),
$order->customer_name,
$order->product_name,
$download_url,
$order->order_number,
date_i18n(get_option('date_format'), strtotime($order->purchase_date)),
get_bloginfo('name')
);
$headers = array('Content-Type: text/plain; charset=UTF-8');
return wp_mail($customer_email, $subject, $message, $headers);
}
/**
* Create download token for orders that don't have one (legacy orders)
*/
public static function ensure_download_token($order_id) {
global $wpdb;
// Check if token already exists
$existing_token = $wpdb->get_var($wpdb->prepare(
"SELECT token FROM {$wpdb->prefix}wpdd_download_links WHERE order_id = %d",
$order_id
));
if ($existing_token) {
return $existing_token;
}
// Create new token for legacy order
$token = wp_hash(uniqid() . $order_id . time());
$expires_at = date('Y-m-d H:i:s', strtotime('+7 days'));
$wpdb->insert(
$wpdb->prefix . 'wpdd_download_links',
array(
'order_id' => $order_id,
'token' => $token,
'expires_at' => $expires_at,
'max_downloads' => 5,
'created_at' => current_time('mysql')
),
array('%d', '%s', '%s', '%d', '%s')
);
return $token;
}
private static function process_download($order) {
$product_id = $order->product_id;
$files = get_post_meta($product_id, '_wpdd_files', true);
// Debug output for admins
if (current_user_can('manage_options') && empty($files)) {
wp_die(sprintf(__('Debug: No files found for product ID %d. Files data: %s', 'wp-digital-download'),
$product_id, '<pre>' . print_r($files, true) . '</pre>'));
}
if (empty($files)) {
wp_die(__('No files available for download.', 'wp-digital-download'));
}
// Debug output for admins
if (current_user_can('manage_options')) {
error_log('WPDD Debug: Files for product ' . $product_id . ': ' . print_r($files, true));
}
$download_limit = get_post_meta($product_id, '_wpdd_download_limit', true);
$download_expiry = get_post_meta($product_id, '_wpdd_download_expiry', true);
if ($download_expiry > 0) {
$expiry_date = date('Y-m-d H:i:s', strtotime($order->purchase_date . ' + ' . $download_expiry . ' days'));
if (current_time('mysql') > $expiry_date) {
wp_die(__('Your download period has expired.', 'wp-digital-download'));
}
}
if ($download_limit > 0 && $order->download_count >= $download_limit) {
wp_die(__('You have reached the download limit for this product.', 'wp-digital-download'));
}
global $wpdb;
$wpdb->update(
$wpdb->prefix . 'wpdd_orders',
array('download_count' => $order->download_count + 1),
array('id' => $order->id),
array('%d'),
array('%d')
);
$file_index = isset($_GET['file']) ? intval($_GET['file']) : 0;
// Handle array structure - files might be indexed or not
$file_list = array_values($files); // Reindex to ensure numeric keys
if (count($file_list) > 1 && !isset($_GET['file'])) {
self::show_file_selection($file_list, $order);
exit;
}
if (!isset($file_list[$file_index])) {
// Debug output for admins
if (current_user_can('manage_options')) {
wp_die(sprintf(__('Debug: File index %d not found. Available files: %s', 'wp-digital-download'),
$file_index, '<pre>' . print_r($file_list, true) . '</pre>'));
}
wp_die(__('File not found.', 'wp-digital-download'));
}
$file = $file_list[$file_index];
self::log_download($order, $product_id, $file['id'] ?? $file_index);
// Debug for admins
if (current_user_can('manage_options')) {
error_log('WPDD Debug: Processing file - ID: ' . ($file['id'] ?? 'none') . ', URL: ' . ($file['url'] ?? 'none'));
}
// Check if this is a protected file
if (isset($file['id']) && strpos($file['id'], 'wpdd_') === 0) {
// This is a protected file, get its metadata
$file_meta = get_option('wpdd_protected_file_' . $file['id']);
if (current_user_can('manage_options')) {
error_log('WPDD Debug: Protected file detected - ' . $file['id']);
error_log('WPDD Debug: File meta exists: ' . ($file_meta ? 'Yes' : 'No'));
if ($file_meta) {
error_log('WPDD Debug: File path exists: ' . (file_exists($file_meta['file_path']) ? 'Yes' : 'No'));
}
}
if ($file_meta && file_exists($file_meta['file_path'])) {
$enable_watermark = get_post_meta($product_id, '_wpdd_enable_watermark', true);
if ($enable_watermark && self::is_watermarkable($file_meta['file_path'])) {
$watermarked_file = WPDD_Watermark::apply_watermark($file_meta['file_path'], $order);
if ($watermarked_file) {
self::deliver_protected_file($watermarked_file, true);
} else {
self::deliver_protected_file($file_meta['file_path']);
}
} else {
self::deliver_protected_file($file_meta['file_path']);
}
return;
}
}
// Check if URL contains protected download parameter (alternative check)
if (isset($file['url']) && strpos($file['url'], 'wpdd_protected_download=') !== false) {
// Extract file_id from URL
if (preg_match('/wpdd_protected_download=([^&]+)/', $file['url'], $matches)) {
$file_id = $matches[1];
$file_meta = get_option('wpdd_protected_file_' . $file_id);
if (current_user_can('manage_options')) {
error_log('WPDD Debug: Protected URL detected - ' . $file_id);
}
if ($file_meta && file_exists($file_meta['file_path'])) {
$enable_watermark = get_post_meta($product_id, '_wpdd_enable_watermark', true);
if ($enable_watermark && self::is_watermarkable($file_meta['file_path'])) {
$watermarked_file = WPDD_Watermark::apply_watermark($file_meta['file_path'], $order);
if ($watermarked_file) {
self::deliver_protected_file($watermarked_file, true);
} else {
self::deliver_protected_file($file_meta['file_path']);
}
} else {
self::deliver_protected_file($file_meta['file_path']);
}
return;
}
}
}
// Regular file handling (backward compatibility)
$enable_watermark = get_post_meta($product_id, '_wpdd_enable_watermark', true);
if ($enable_watermark && self::is_watermarkable($file['url'])) {
$watermarked_file = WPDD_Watermark::apply_watermark($file['url'], $order);
if ($watermarked_file) {
self::deliver_file($watermarked_file, $file['name'], true);
} else {
self::deliver_file($file['url'], $file['name']);
}
} else {
self::deliver_file($file['url'], $file['name']);
}
}
private static function show_file_selection($files, $order) {
?>
<!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
<meta charset="<?php bloginfo('charset'); ?>">
<title><?php _e('Select File to Download', 'wp-digital-download'); ?></title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
max-width: 600px;
margin: 50px auto;
padding: 20px;
background: #f5f5f5;
}
.container {
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
h1 {
color: #333;
margin-bottom: 20px;
}
.file-list {
list-style: none;
padding: 0;
}
.file-item {
margin-bottom: 15px;
padding: 15px;
background: #f9f9f9;
border-radius: 4px;
display: flex;
justify-content: space-between;
align-items: center;
}
.file-name {
font-weight: 500;
}
.download-btn {
padding: 8px 16px;
background: #2271b1;
color: white;
text-decoration: none;
border-radius: 4px;
transition: background 0.2s;
}
.download-btn:hover {
background: #135e96;
}
</style>
</head>
<body>
<div class="container">
<h1><?php _e('Select File to Download', 'wp-digital-download'); ?></h1>
<ul class="file-list">
<?php foreach ($files as $index => $file) : ?>
<li class="file-item">
<span class="file-name"><?php echo esc_html($file['name'] ?? 'File ' . ($index + 1)); ?></span>
<a href="<?php echo add_query_arg('file', $index); ?>" class="download-btn">
<?php _e('Download', 'wp-digital-download'); ?>
</a>
</li>
<?php endforeach; ?>
</ul>
</div>
</body>
</html>
<?php
}
public static function handle_secure_file_download() {
if (!isset($_GET['wpdd_file_download'])) {
return;
}
$attachment_id = intval($_GET['wpdd_file_download']);
$token = sanitize_text_field($_GET['token'] ?? '');
if (!$attachment_id || !$token) {
wp_die(__('Invalid download link.', 'wp-digital-download'));
}
// Verify token
$stored_token = get_post_meta($attachment_id, '_wpdd_download_token', true);
if ($token !== $stored_token) {
wp_die(__('Invalid download token.', 'wp-digital-download'));
}
// Check if user has permission to download
if (!is_user_logged_in()) {
wp_die(__('You must be logged in to download this file.', 'wp-digital-download'));
}
// Get protected file path
$protected_path = get_post_meta($attachment_id, '_wpdd_protected_path', true);
if (!$protected_path || !file_exists($protected_path)) {
// Fallback to regular attachment path
$protected_path = get_attached_file($attachment_id);
}
if (!$protected_path || !file_exists($protected_path)) {
wp_die(__('File not found.', 'wp-digital-download'));
}
// Deliver the file
self::deliver_protected_file($protected_path);
}
private static function deliver_protected_file($file_path, $is_temp = false) {
if (!file_exists($file_path)) {
wp_die(__('File not found.', 'wp-digital-download'));
}
$file_name = basename($file_path);
$file_size = filesize($file_path);
$file_type = wp_check_filetype($file_path);
nocache_headers();
header('Content-Type: ' . ($file_type['type'] ?: 'application/octet-stream'));
header('Content-Disposition: attachment; filename="' . $file_name . '"');
header('Content-Length: ' . $file_size);
header('Content-Transfer-Encoding: binary');
if (ob_get_level()) {
ob_end_clean();
}
readfile($file_path);
if ($is_temp && file_exists($file_path)) {
unlink($file_path);
}
exit;
}
private static function deliver_file($file_path, $file_name, $is_temp = false) {
$original_path = $file_path;
// Check if this is a protected file URL
if (strpos($file_path, 'wpdd_file_download=') !== false) {
// This is already a protected URL, just redirect to it
wp_redirect($file_path);
exit;
}
// Check if file is in protected directory
$upload_dir = wp_upload_dir();
$protected_dir = trailingslashit($upload_dir['basedir']) . WPDD_UPLOADS_DIR;
if (strpos($file_path, $protected_dir) === 0) {
// File is in protected directory, deliver directly
if (!file_exists($file_path)) {
wp_die(__('Protected file not found.', 'wp-digital-download'));
}
self::deliver_protected_file($file_path);
return;
}
// Convert URL to file path if needed
if (filter_var($file_path, FILTER_VALIDATE_URL)) {
$upload_dir = wp_upload_dir();
$site_url = get_site_url();
// Debug logging
if (current_user_can('manage_options')) {
error_log('WPDD Debug: Original URL: ' . $file_path);
error_log('WPDD Debug: Upload baseurl: ' . $upload_dir['baseurl']);
error_log('WPDD Debug: Upload basedir: ' . $upload_dir['basedir']);
}
// Handle various URL formats
if (strpos($file_path, $upload_dir['baseurl']) === 0) {
$file_path = str_replace($upload_dir['baseurl'], $upload_dir['basedir'], $file_path);
} elseif (strpos($file_path, $site_url) === 0) {
// Handle site URL paths
$file_path = str_replace($site_url . '/wp-content/uploads', $upload_dir['basedir'], $file_path);
} else {
// External URL - just redirect
wp_redirect($file_path);
exit;
}
}
// Debug logging
if (current_user_can('manage_options')) {
error_log('WPDD Debug: Final file path: ' . $file_path);
error_log('WPDD Debug: File exists: ' . (file_exists($file_path) ? 'Yes' : 'No'));
}
if (!file_exists($file_path)) {
// Debug output for admin users
if (current_user_can('manage_options')) {
wp_die(sprintf(__('File not found at path: %s<br>Original: %s', 'wp-digital-download'),
$file_path, $original_path));
}
wp_die(__('File not found. Please contact the administrator.', 'wp-digital-download'));
}
$file_size = filesize($file_path);
$file_type = wp_check_filetype($file_path);
if (empty($file_name)) {
$file_name = basename($file_path);
}
nocache_headers();
header('Content-Type: ' . ($file_type['type'] ?: 'application/octet-stream'));
header('Content-Disposition: attachment; filename="' . $file_name . '"');
header('Content-Length: ' . $file_size);
header('Content-Transfer-Encoding: binary');
if (ob_get_level()) {
ob_end_clean();
}
readfile($file_path);
if ($is_temp && file_exists($file_path)) {
unlink($file_path);
}
exit;
}
private static function log_download($order, $product_id, $file_id) {
global $wpdb;
$wpdb->insert(
$wpdb->prefix . 'wpdd_downloads',
array(
'order_id' => $order->id,
'product_id' => $product_id,
'customer_id' => $order->customer_id,
'file_id' => $file_id,
'download_date' => current_time('mysql'),
'ip_address' => $_SERVER['REMOTE_ADDR'],
'user_agent' => $_SERVER['HTTP_USER_AGENT']
),
array('%d', '%d', '%d', '%s', '%s', '%s', '%s')
);
}
private static function is_watermarkable($file_url) {
$supported_types = array('jpg', 'jpeg', 'png', 'gif', 'pdf');
$file_extension = strtolower(pathinfo($file_url, PATHINFO_EXTENSION));
return in_array($file_extension, $supported_types);
}
}

View File

@@ -0,0 +1,180 @@
<?php
if (!defined('ABSPATH')) {
exit;
}
class WPDD_File_Protection {
public static function init() {
add_action('template_redirect', array(__CLASS__, 'protect_direct_access'));
add_filter('mod_rewrite_rules', array(__CLASS__, 'add_rewrite_rules'));
add_action('init', array(__CLASS__, 'add_download_endpoint'));
}
public static function protect_direct_access() {
$request_uri = $_SERVER['REQUEST_URI'];
$upload_dir = wp_upload_dir();
$protected_dir = '/' . WPDD_UPLOADS_DIR . '/';
if (strpos($request_uri, $protected_dir) !== false) {
wp_die(__('Direct access to this file is not allowed.', 'wp-digital-download'), 403);
}
}
public static function add_rewrite_rules($rules) {
$upload_dir = wp_upload_dir();
$protected_path = str_replace(ABSPATH, '', $upload_dir['basedir']) . '/' . WPDD_UPLOADS_DIR;
$new_rules = "# WP Digital Download Protection\n";
$new_rules .= "<IfModule mod_rewrite.c>\n";
$new_rules .= "RewriteRule ^" . $protected_path . "/.*$ - [F,L]\n";
$new_rules .= "</IfModule>\n\n";
return $new_rules . $rules;
}
public static function add_download_endpoint() {
add_rewrite_endpoint('wpdd-download', EP_ROOT);
}
public static function move_to_protected_directory($attachment_id) {
$upload_dir = wp_upload_dir();
$protected_dir = trailingslashit($upload_dir['basedir']) . WPDD_UPLOADS_DIR;
if (!file_exists($protected_dir)) {
wp_mkdir_p($protected_dir);
self::create_protection_files($protected_dir);
}
$file_path = get_attached_file($attachment_id);
if (!file_exists($file_path)) {
return false;
}
$filename = basename($file_path);
$unique_filename = wp_unique_filename($protected_dir, $filename);
$new_path = trailingslashit($protected_dir) . $unique_filename;
if (copy($file_path, $new_path)) {
$protected_url = trailingslashit($upload_dir['baseurl']) . WPDD_UPLOADS_DIR . '/' . $unique_filename;
update_post_meta($attachment_id, '_wpdd_protected_file', $new_path);
update_post_meta($attachment_id, '_wpdd_protected_url', $protected_url);
return array(
'path' => $new_path,
'url' => $protected_url
);
}
return false;
}
public static function create_protection_files($directory) {
$htaccess_content = "Options -Indexes\n";
$htaccess_content .= "deny from all\n";
$htaccess_file = trailingslashit($directory) . '.htaccess';
if (!file_exists($htaccess_file)) {
file_put_contents($htaccess_file, $htaccess_content);
}
$index_content = "<?php\n// Silence is golden.\n";
$index_file = trailingslashit($directory) . 'index.php';
if (!file_exists($index_file)) {
file_put_contents($index_file, $index_content);
}
$nginx_content = "location ~* ^/wp-content/uploads/" . WPDD_UPLOADS_DIR . " {\n";
$nginx_content .= " deny all;\n";
$nginx_content .= " return 403;\n";
$nginx_content .= "}\n";
$nginx_file = trailingslashit($directory) . 'nginx.conf';
if (!file_exists($nginx_file)) {
file_put_contents($nginx_file, $nginx_content);
}
}
public static function get_protected_file_url($file_path) {
$upload_dir = wp_upload_dir();
$protected_dir = trailingslashit($upload_dir['basedir']) . WPDD_UPLOADS_DIR;
if (strpos($file_path, $protected_dir) === 0) {
$relative_path = str_replace($protected_dir, '', $file_path);
return trailingslashit($upload_dir['baseurl']) . WPDD_UPLOADS_DIR . $relative_path;
}
return $file_path;
}
public static function is_protected_file($file_path) {
$upload_dir = wp_upload_dir();
$protected_dir = trailingslashit($upload_dir['basedir']) . WPDD_UPLOADS_DIR;
return strpos($file_path, $protected_dir) === 0;
}
public static function generate_secure_download_url($file_path, $order_id, $expiry_hours = 24) {
$token = wp_hash($file_path . $order_id . time());
$expiry = time() + ($expiry_hours * 3600);
set_transient('wpdd_download_' . $token, array(
'file_path' => $file_path,
'order_id' => $order_id,
'expiry' => $expiry
), $expiry_hours * 3600);
return add_query_arg(array(
'wpdd_secure_download' => $token
), home_url());
}
public static function handle_secure_download() {
if (!isset($_GET['wpdd_secure_download'])) {
return;
}
$token = sanitize_text_field($_GET['wpdd_secure_download']);
$data = get_transient('wpdd_download_' . $token);
if (!$data) {
wp_die(__('Invalid or expired download link.', 'wp-digital-download'));
}
if ($data['expiry'] < time()) {
delete_transient('wpdd_download_' . $token);
wp_die(__('This download link has expired.', 'wp-digital-download'));
}
if (!file_exists($data['file_path'])) {
wp_die(__('File not found.', 'wp-digital-download'));
}
delete_transient('wpdd_download_' . $token);
self::serve_download($data['file_path']);
}
private static function serve_download($file_path) {
$file_name = basename($file_path);
$file_size = filesize($file_path);
$file_type = wp_check_filetype($file_path);
nocache_headers();
header('Content-Type: ' . ($file_type['type'] ?: 'application/octet-stream'));
header('Content-Disposition: attachment; filename="' . $file_name . '"');
header('Content-Length: ' . $file_size);
header('Content-Transfer-Encoding: binary');
if (ob_get_level()) {
ob_end_clean();
}
readfile($file_path);
exit;
}
}

View File

@@ -0,0 +1,215 @@
<?php
if (!defined('ABSPATH')) {
exit;
}
class WPDD_Install {
public static function activate() {
// Load required files if not already loaded
if (!class_exists('WPDD_Post_Types')) {
require_once WPDD_PLUGIN_PATH . 'includes/class-wpdd-post-types.php';
}
if (!class_exists('WPDD_Roles')) {
require_once WPDD_PLUGIN_PATH . 'includes/class-wpdd-roles.php';
}
// Register post types and taxonomies before flushing rules
WPDD_Post_Types::register_post_types();
WPDD_Post_Types::register_taxonomies();
self::create_tables();
self::create_pages();
self::create_upload_protection();
WPDD_Roles::create_roles();
// Flush rewrite rules after post types are registered
flush_rewrite_rules();
}
public static function deactivate() {
flush_rewrite_rules();
}
private static function create_tables() {
global $wpdb;
$charset_collate = $wpdb->get_charset_collate();
$sql = array();
$sql[] = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}wpdd_orders (
id bigint(20) NOT NULL AUTO_INCREMENT,
order_number varchar(50) NOT NULL,
product_id bigint(20) NOT NULL,
customer_id bigint(20) NOT NULL,
creator_id bigint(20) NOT NULL,
status varchar(20) NOT NULL DEFAULT 'pending',
payment_method varchar(50) DEFAULT NULL,
transaction_id varchar(100) DEFAULT NULL,
amount decimal(10,2) NOT NULL,
currency varchar(10) NOT NULL DEFAULT 'USD',
customer_email varchar(100) NOT NULL,
customer_name varchar(100) DEFAULT NULL,
purchase_date datetime DEFAULT CURRENT_TIMESTAMP,
download_count int(11) DEFAULT 0,
notes text DEFAULT NULL,
PRIMARY KEY (id),
KEY order_number (order_number),
KEY product_id (product_id),
KEY customer_id (customer_id),
KEY creator_id (creator_id),
KEY status (status)
) $charset_collate;";
$sql[] = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}wpdd_downloads (
id bigint(20) NOT NULL AUTO_INCREMENT,
order_id bigint(20) NOT NULL,
product_id bigint(20) NOT NULL,
customer_id bigint(20) NOT NULL,
file_id varchar(100) NOT NULL,
download_date datetime DEFAULT CURRENT_TIMESTAMP,
ip_address varchar(45) DEFAULT NULL,
user_agent text DEFAULT NULL,
PRIMARY KEY (id),
KEY order_id (order_id),
KEY product_id (product_id),
KEY customer_id (customer_id)
) $charset_collate;";
$sql[] = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}wpdd_download_links (
id bigint(20) NOT NULL AUTO_INCREMENT,
order_id bigint(20) NOT NULL,
token varchar(64) NOT NULL,
expires_at datetime NOT NULL,
download_count int(11) DEFAULT 0,
max_downloads int(11) DEFAULT 5,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY token (token),
KEY order_id (order_id)
) $charset_collate;";
$sql[] = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}wpdd_creator_earnings (
id bigint(20) NOT NULL AUTO_INCREMENT,
creator_id bigint(20) NOT NULL,
order_id bigint(20) NOT NULL,
product_id bigint(20) NOT NULL,
sale_amount decimal(10,2) NOT NULL,
commission_rate decimal(5,2) NOT NULL,
creator_earning decimal(10,2) NOT NULL,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY creator_id (creator_id),
KEY order_id (order_id),
KEY product_id (product_id)
) $charset_collate;";
$sql[] = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}wpdd_payouts (
id bigint(20) NOT NULL AUTO_INCREMENT,
creator_id bigint(20) NOT NULL,
amount decimal(10,2) NOT NULL,
currency varchar(10) NOT NULL,
paypal_email varchar(100) NOT NULL,
transaction_id varchar(100) DEFAULT NULL,
status varchar(20) NOT NULL DEFAULT 'pending',
payout_method varchar(20) NOT NULL DEFAULT 'manual',
notes text DEFAULT NULL,
processed_by bigint(20) DEFAULT NULL,
processed_at datetime DEFAULT NULL,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY creator_id (creator_id),
KEY status (status),
KEY transaction_id (transaction_id)
) $charset_collate;";
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
foreach ($sql as $query) {
dbDelta($query);
}
update_option('wpdd_db_version', WPDD_VERSION);
}
private static function create_pages() {
$pages = array(
'shop' => array(
'title' => __('Shop', 'wp-digital-download'),
'content' => '[wpdd_shop]',
'option' => 'wpdd_shop_page_id'
),
'my-purchases' => array(
'title' => __('My Purchases', 'wp-digital-download'),
'content' => '[wpdd_customer_purchases]',
'option' => 'wpdd_purchases_page_id'
),
'checkout' => array(
'title' => __('Checkout', 'wp-digital-download'),
'content' => '[wpdd_checkout]',
'option' => 'wpdd_checkout_page_id'
),
'thank-you' => array(
'title' => __('Thank You', 'wp-digital-download'),
'content' => '[wpdd_thank_you]',
'option' => 'wpdd_thank_you_page_id'
)
);
foreach ($pages as $slug => $page) {
// Check if page already exists
$existing_page_id = get_option($page['option']);
if ($existing_page_id && get_post($existing_page_id)) {
continue; // Page already exists, skip creation
}
// Check if a page with this slug already exists
$existing_page = get_page_by_path($slug);
if ($existing_page) {
update_option($page['option'], $existing_page->ID);
continue;
}
// Create the page
$page_id = wp_insert_post(array(
'post_title' => $page['title'],
'post_content' => $page['content'],
'post_status' => 'publish',
'post_type' => 'page',
'post_name' => $slug
));
if ($page_id && !is_wp_error($page_id)) {
update_option($page['option'], $page_id);
}
}
}
private static function create_upload_protection() {
$upload_dir = wp_upload_dir();
$protection_dir = trailingslashit($upload_dir['basedir']) . WPDD_UPLOADS_DIR;
if (!file_exists($protection_dir)) {
wp_mkdir_p($protection_dir);
}
$htaccess_content = "Options -Indexes\n";
$htaccess_content .= "deny from all\n";
$htaccess_file = trailingslashit($protection_dir) . '.htaccess';
if (!file_exists($htaccess_file)) {
file_put_contents($htaccess_file, $htaccess_content);
}
$index_content = "<?php\n// Silence is golden.\n";
$index_file = trailingslashit($protection_dir) . 'index.php';
if (!file_exists($index_file)) {
file_put_contents($index_file, $index_content);
}
}
}

View File

@@ -0,0 +1,308 @@
<?php
if (!defined('ABSPATH')) {
exit;
}
class WPDD_Metaboxes {
public static function init() {
add_action('add_meta_boxes', array(__CLASS__, 'add_meta_boxes'));
add_action('save_post_wpdd_product', array(__CLASS__, 'save_product_meta'), 10, 2);
}
public static function add_meta_boxes() {
add_meta_box(
'wpdd_product_pricing',
__('Product Pricing', 'wp-digital-download'),
array(__CLASS__, 'render_pricing_metabox'),
'wpdd_product',
'side',
'high'
);
add_meta_box(
'wpdd_product_files',
__('Downloadable Files', 'wp-digital-download'),
array(__CLASS__, 'render_files_metabox'),
'wpdd_product',
'normal',
'high'
);
add_meta_box(
'wpdd_product_settings',
__('Product Settings', 'wp-digital-download'),
array(__CLASS__, 'render_settings_metabox'),
'wpdd_product',
'normal',
'default'
);
add_meta_box(
'wpdd_product_stats',
__('Product Statistics', 'wp-digital-download'),
array(__CLASS__, 'render_stats_metabox'),
'wpdd_product',
'side',
'low'
);
}
public static function render_pricing_metabox($post) {
wp_nonce_field('wpdd_save_product_meta', 'wpdd_product_meta_nonce');
$price = get_post_meta($post->ID, '_wpdd_price', true);
$is_free = get_post_meta($post->ID, '_wpdd_is_free', true);
$sale_price = get_post_meta($post->ID, '_wpdd_sale_price', true);
?>
<div class="wpdd-metabox-content">
<p>
<label>
<input type="checkbox" name="wpdd_is_free" id="wpdd_is_free" value="1" <?php checked($is_free, '1'); ?> />
<?php _e('This is a free product', 'wp-digital-download'); ?>
</label>
</p>
<p class="wpdd-price-field">
<label for="wpdd_price"><?php _e('Regular Price', 'wp-digital-download'); ?> ($)</label>
<input type="number" id="wpdd_price" name="wpdd_price" value="<?php echo esc_attr($price); ?>" step="0.01" min="0" />
</p>
<p class="wpdd-price-field">
<label for="wpdd_sale_price"><?php _e('Sale Price', 'wp-digital-download'); ?> ($)</label>
<input type="number" id="wpdd_sale_price" name="wpdd_sale_price" value="<?php echo esc_attr($sale_price); ?>" step="0.01" min="0" />
<span class="description"><?php _e('Leave blank for no sale', 'wp-digital-download'); ?></span>
</p>
</div>
<?php
}
public static function render_files_metabox($post) {
$files = get_post_meta($post->ID, '_wpdd_files', true);
if (!is_array($files)) {
$files = array();
}
?>
<div class="wpdd-files-container">
<div id="wpdd-files-list">
<?php if (!empty($files)) : ?>
<?php foreach ($files as $index => $file) : ?>
<div class="wpdd-file-item" data-index="<?php echo $index; ?>">
<div class="wpdd-file-header">
<span class="wpdd-file-handle dashicons dashicons-menu"></span>
<input type="text" name="wpdd_files[<?php echo $index; ?>][name]"
value="<?php echo esc_attr($file['name']); ?>"
placeholder="<?php _e('File Name', 'wp-digital-download'); ?>" />
<button type="button" class="button wpdd-remove-file">
<?php _e('Remove', 'wp-digital-download'); ?>
</button>
</div>
<div class="wpdd-file-content">
<div class="wpdd-file-url">
<input type="text" name="wpdd_files[<?php echo $index; ?>][url]"
value="<?php echo esc_url($file['url']); ?>"
placeholder="<?php _e('File URL', 'wp-digital-download'); ?>"
class="wpdd-file-url-input" />
<button type="button" class="button wpdd-upload-file">
<?php _e('Upload File', 'wp-digital-download'); ?>
</button>
</div>
<input type="hidden" name="wpdd_files[<?php echo $index; ?>][id]"
value="<?php echo esc_attr($file['id']); ?>"
class="wpdd-file-id" />
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
<button type="button" id="wpdd-add-file" class="button button-primary">
<?php _e('Add File', 'wp-digital-download'); ?>
</button>
<template id="wpdd-file-template">
<div class="wpdd-file-item" data-index="">
<div class="wpdd-file-header">
<span class="wpdd-file-handle dashicons dashicons-menu"></span>
<input type="text" name="wpdd_files[INDEX][name]"
placeholder="<?php _e('File Name', 'wp-digital-download'); ?>" />
<button type="button" class="button wpdd-remove-file">
<?php _e('Remove', 'wp-digital-download'); ?>
</button>
</div>
<div class="wpdd-file-content">
<div class="wpdd-file-url">
<input type="text" name="wpdd_files[INDEX][url]"
placeholder="<?php _e('File URL', 'wp-digital-download'); ?>"
class="wpdd-file-url-input" />
<button type="button" class="button wpdd-upload-file">
<?php _e('Upload File', 'wp-digital-download'); ?>
</button>
</div>
<input type="hidden" name="wpdd_files[INDEX][id]" class="wpdd-file-id" />
</div>
</div>
</template>
</div>
<?php
}
public static function render_settings_metabox($post) {
$download_limit = get_post_meta($post->ID, '_wpdd_download_limit', true);
$download_expiry = get_post_meta($post->ID, '_wpdd_download_expiry', true);
$enable_watermark = get_post_meta($post->ID, '_wpdd_enable_watermark', true);
$watermark_text = get_post_meta($post->ID, '_wpdd_watermark_text', true);
?>
<div class="wpdd-settings-grid">
<div class="wpdd-setting-group">
<h4><?php _e('Download Settings', 'wp-digital-download'); ?></h4>
<p>
<label for="wpdd_download_limit">
<?php _e('Download Limit', 'wp-digital-download'); ?>
</label>
<input type="number" id="wpdd_download_limit" name="wpdd_download_limit"
value="<?php echo esc_attr($download_limit ?: 5); ?>" min="0" />
<span class="description">
<?php _e('Number of times a customer can download after purchase. 0 = unlimited', 'wp-digital-download'); ?>
</span>
</p>
<p>
<label for="wpdd_download_expiry">
<?php _e('Download Expiry (days)', 'wp-digital-download'); ?>
</label>
<input type="number" id="wpdd_download_expiry" name="wpdd_download_expiry"
value="<?php echo esc_attr($download_expiry ?: 30); ?>" min="0" />
<span class="description">
<?php _e('Number of days download link remains valid. 0 = never expires', 'wp-digital-download'); ?>
</span>
</p>
</div>
<div class="wpdd-setting-group">
<h4><?php _e('Watermark Settings', 'wp-digital-download'); ?></h4>
<p>
<label>
<input type="checkbox" name="wpdd_enable_watermark" value="1"
<?php checked($enable_watermark, '1'); ?> />
<?php _e('Enable watermarking for images and PDFs', 'wp-digital-download'); ?>
</label>
</p>
<p>
<label for="wpdd_watermark_text">
<?php _e('Watermark Text', 'wp-digital-download'); ?>
</label>
<input type="text" id="wpdd_watermark_text" name="wpdd_watermark_text"
value="<?php echo esc_attr($watermark_text); ?>"
placeholder="<?php _e('e.g., {customer_email} - {order_id}', 'wp-digital-download'); ?>" />
<span class="description">
<?php _e('Available placeholders: {customer_name}, {customer_email}, {order_id}, {date}', 'wp-digital-download'); ?>
</span>
</p>
</div>
</div>
<?php
}
public static function render_stats_metabox($post) {
global $wpdb;
$total_sales = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->prefix}wpdd_orders
WHERE product_id = %d AND status = 'completed'",
$post->ID
));
$total_revenue = $wpdb->get_var($wpdb->prepare(
"SELECT SUM(amount) FROM {$wpdb->prefix}wpdd_orders
WHERE product_id = %d AND status = 'completed'",
$post->ID
));
$total_downloads = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->prefix}wpdd_downloads
WHERE product_id = %d",
$post->ID
));
?>
<div class="wpdd-stats">
<p>
<strong><?php _e('Total Sales:', 'wp-digital-download'); ?></strong>
<?php echo intval($total_sales); ?>
</p>
<p>
<strong><?php _e('Total Revenue:', 'wp-digital-download'); ?></strong>
$<?php echo number_format(floatval($total_revenue), 2); ?>
</p>
<p>
<strong><?php _e('Total Downloads:', 'wp-digital-download'); ?></strong>
<?php echo intval($total_downloads); ?>
</p>
</div>
<?php
}
public static function save_product_meta($post_id, $post) {
if (!isset($_POST['wpdd_product_meta_nonce']) ||
!wp_verify_nonce($_POST['wpdd_product_meta_nonce'], 'wpdd_save_product_meta')) {
return;
}
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
return;
}
if (!current_user_can('edit_post', $post_id)) {
return;
}
update_post_meta($post_id, '_wpdd_is_free',
isset($_POST['wpdd_is_free']) ? '1' : '0');
if (isset($_POST['wpdd_price'])) {
update_post_meta($post_id, '_wpdd_price',
floatval($_POST['wpdd_price']));
}
if (isset($_POST['wpdd_sale_price'])) {
update_post_meta($post_id, '_wpdd_sale_price',
floatval($_POST['wpdd_sale_price']));
}
if (isset($_POST['wpdd_files']) && is_array($_POST['wpdd_files'])) {
$files = array();
foreach ($_POST['wpdd_files'] as $file) {
if (!empty($file['url'])) {
$files[] = array(
'id' => sanitize_text_field($file['id']),
'name' => sanitize_text_field($file['name']),
'url' => esc_url_raw($file['url'])
);
}
}
update_post_meta($post_id, '_wpdd_files', $files);
}
if (isset($_POST['wpdd_download_limit'])) {
update_post_meta($post_id, '_wpdd_download_limit',
intval($_POST['wpdd_download_limit']));
}
if (isset($_POST['wpdd_download_expiry'])) {
update_post_meta($post_id, '_wpdd_download_expiry',
intval($_POST['wpdd_download_expiry']));
}
update_post_meta($post_id, '_wpdd_enable_watermark',
isset($_POST['wpdd_enable_watermark']) ? '1' : '0');
if (isset($_POST['wpdd_watermark_text'])) {
update_post_meta($post_id, '_wpdd_watermark_text',
sanitize_text_field($_POST['wpdd_watermark_text']));
}
}
}

View File

@@ -0,0 +1,318 @@
<?php
if (!defined('ABSPATH')) {
exit;
}
class WPDD_Orders {
public static function create_order($product_id, $customer_data, $payment_method = 'free') {
global $wpdb;
$product = get_post($product_id);
if (!$product || $product->post_type !== 'wpdd_product') {
return false;
}
$price = get_post_meta($product_id, '_wpdd_price', true);
$sale_price = get_post_meta($product_id, '_wpdd_sale_price', true);
$is_free = get_post_meta($product_id, '_wpdd_is_free', true);
$amount = $is_free ? 0 : (($sale_price && $sale_price < $price) ? $sale_price : $price);
$order_number = 'WPDD-' . strtoupper(uniqid());
$customer_id = 0;
if (is_user_logged_in()) {
$current_user = wp_get_current_user();
$customer_id = $current_user->ID;
$customer_email = $current_user->user_email;
$customer_name = $current_user->display_name;
} else {
$customer_email = $customer_data['email'];
$customer_name = $customer_data['name'];
}
$result = $wpdb->insert(
$wpdb->prefix . 'wpdd_orders',
array(
'order_number' => $order_number,
'product_id' => $product_id,
'customer_id' => $customer_id,
'creator_id' => $product->post_author,
'status' => ($payment_method === 'free' || $amount == 0) ? 'completed' : 'pending',
'payment_method' => $payment_method,
'amount' => $amount,
'currency' => 'USD',
'customer_email' => $customer_email,
'customer_name' => $customer_name,
'purchase_date' => current_time('mysql')
),
array('%s', '%d', '%d', '%d', '%s', '%s', '%f', '%s', '%s', '%s', '%s')
);
if ($result) {
$order_id = $wpdb->insert_id;
if ($payment_method === 'free' || $amount == 0) {
self::complete_order($order_id);
}
return $order_id;
}
return false;
}
public static function complete_order($order_id, $transaction_id = null) {
global $wpdb;
$order = self::get_order($order_id);
if (!$order) {
return false;
}
$update_data = array(
'status' => 'completed'
);
if ($transaction_id) {
$update_data['transaction_id'] = $transaction_id;
}
$result = $wpdb->update(
$wpdb->prefix . 'wpdd_orders',
$update_data,
array('id' => $order_id),
array('%s', '%s'),
array('%d')
);
if ($result) {
self::generate_download_link($order_id);
self::send_order_emails($order_id);
update_post_meta(
$order->product_id,
'_wpdd_sales_count',
intval(get_post_meta($order->product_id, '_wpdd_sales_count', true)) + 1
);
do_action('wpdd_order_completed', $order_id);
return true;
}
return false;
}
public static function get_order($order_id) {
global $wpdb;
if (is_numeric($order_id)) {
return $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}wpdd_orders WHERE id = %d",
$order_id
));
} else {
return $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}wpdd_orders WHERE order_number = %s",
$order_id
));
}
}
public static function get_orders($args = array()) {
global $wpdb;
$defaults = array(
'status' => '',
'customer_id' => 0,
'creator_id' => 0,
'product_id' => 0,
'limit' => 20,
'offset' => 0,
'orderby' => 'purchase_date',
'order' => 'DESC'
);
$args = wp_parse_args($args, $defaults);
$where = array('1=1');
if ($args['status']) {
$where[] = $wpdb->prepare("status = %s", $args['status']);
}
if ($args['customer_id']) {
$where[] = $wpdb->prepare("customer_id = %d", $args['customer_id']);
}
if ($args['creator_id']) {
$where[] = $wpdb->prepare("creator_id = %d", $args['creator_id']);
}
if ($args['product_id']) {
$where[] = $wpdb->prepare("product_id = %d", $args['product_id']);
}
$where_clause = implode(' AND ', $where);
$query = $wpdb->prepare(
"SELECT o.*, p.post_title as product_name,
u.display_name as customer_display_name,
c.display_name as creator_display_name
FROM {$wpdb->prefix}wpdd_orders o
LEFT JOIN {$wpdb->posts} p ON o.product_id = p.ID
LEFT JOIN {$wpdb->users} u ON o.customer_id = u.ID
LEFT JOIN {$wpdb->users} c ON o.creator_id = c.ID
WHERE {$where_clause}
ORDER BY {$args['orderby']} {$args['order']}
LIMIT %d OFFSET %d",
$args['limit'],
$args['offset']
);
return $wpdb->get_results($query);
}
private static function generate_download_link($order_id) {
global $wpdb;
$token = wp_hash(uniqid() . $order_id . time());
$expires_at = date('Y-m-d H:i:s', strtotime('+7 days'));
$wpdb->insert(
$wpdb->prefix . 'wpdd_download_links',
array(
'order_id' => $order_id,
'token' => $token,
'expires_at' => $expires_at,
'max_downloads' => 5,
'created_at' => current_time('mysql')
),
array('%d', '%s', '%s', '%d', '%s')
);
return $token;
}
private static function send_order_emails($order_id) {
$order = self::get_order($order_id);
if (!$order) {
return;
}
self::send_customer_email($order);
self::send_creator_email($order);
self::send_admin_email($order);
}
private static function send_customer_email($order) {
global $wpdb;
$product = get_post($order->product_id);
$download_link = $wpdb->get_var($wpdb->prepare(
"SELECT token FROM {$wpdb->prefix}wpdd_download_links
WHERE order_id = %d ORDER BY id DESC LIMIT 1",
$order->id
));
$download_url = add_query_arg(array(
'wpdd_download_token' => $download_link
), home_url());
$subject = sprintf(
__('Your purchase of %s from %s', 'wp-digital-download'),
$product->post_title,
get_bloginfo('name')
);
$message = sprintf(
__("Hi %s,\n\nThank you for your purchase!\n\n", 'wp-digital-download'),
$order->customer_name
);
$message .= sprintf(__("Order Number: %s\n", 'wp-digital-download'), $order->order_number);
$message .= sprintf(__("Product: %s\n", 'wp-digital-download'), $product->post_title);
if ($order->amount > 0) {
$message .= sprintf(__("Amount: $%s\n", 'wp-digital-download'), number_format($order->amount, 2));
}
$message .= "\n" . __("Download your product here:\n", 'wp-digital-download');
$message .= $download_url . "\n\n";
$message .= __("This download link will expire in 7 days.\n\n", 'wp-digital-download');
if ($order->customer_id) {
$purchases_url = get_permalink(get_option('wpdd_purchases_page_id'));
$message .= sprintf(
__("You can also access your downloads anytime from your account:\n%s\n\n", 'wp-digital-download'),
$purchases_url
);
}
$message .= sprintf(__("Best regards,\n%s", 'wp-digital-download'), get_bloginfo('name'));
wp_mail($order->customer_email, $subject, $message);
}
private static function send_creator_email($order) {
$creator = get_userdata($order->creator_id);
if (!$creator) {
return;
}
$product = get_post($order->product_id);
$subject = sprintf(
__('New sale: %s', 'wp-digital-download'),
$product->post_title
);
$message = sprintf(
__("Hi %s,\n\nYou have a new sale!\n\n", 'wp-digital-download'),
$creator->display_name
);
$message .= sprintf(__("Product: %s\n", 'wp-digital-download'), $product->post_title);
$message .= sprintf(__("Customer: %s\n", 'wp-digital-download'), $order->customer_name);
$message .= sprintf(__("Amount: $%s\n", 'wp-digital-download'), number_format($order->amount, 2));
$message .= sprintf(__("Order Number: %s\n", 'wp-digital-download'), $order->order_number);
$message .= "\n" . sprintf(
__("View your sales dashboard:\n%s\n", 'wp-digital-download'),
admin_url()
);
wp_mail($creator->user_email, $subject, $message);
}
private static function send_admin_email($order) {
$admin_email = get_option('wpdd_admin_email', get_option('admin_email'));
if (!$admin_email) {
return;
}
$product = get_post($order->product_id);
$subject = sprintf(
__('[%s] New Digital Download Sale', 'wp-digital-download'),
get_bloginfo('name')
);
$message = __("A new digital download sale has been completed.\n\n", 'wp-digital-download');
$message .= sprintf(__("Order Number: %s\n", 'wp-digital-download'), $order->order_number);
$message .= sprintf(__("Product: %s\n", 'wp-digital-download'), $product->post_title);
$message .= sprintf(__("Customer: %s (%s)\n", 'wp-digital-download'), $order->customer_name, $order->customer_email);
$message .= sprintf(__("Amount: $%s\n", 'wp-digital-download'), number_format($order->amount, 2));
$message .= sprintf(__("Payment Method: %s\n", 'wp-digital-download'), $order->payment_method);
if ($order->transaction_id) {
$message .= sprintf(__("Transaction ID: %s\n", 'wp-digital-download'), $order->transaction_id);
}
wp_mail($admin_email, $subject, $message);
}
}

View File

@@ -0,0 +1,231 @@
<?php
if (!defined('ABSPATH')) {
exit;
}
class WPDD_PayPal_Payouts {
public static function init() {
// Schedule cron event for automatic payouts
if (!wp_next_scheduled('wpdd_process_automatic_payouts')) {
wp_schedule_event(time(), 'daily', 'wpdd_process_automatic_payouts');
}
add_action('wpdd_process_automatic_payouts', array(__CLASS__, 'process_automatic_payouts'));
}
public static function process_automatic_payouts() {
$threshold = floatval(get_option('wpdd_payout_threshold', 0));
if ($threshold <= 0) {
return;
}
$creators = WPDD_Creator::get_creators_with_balance();
foreach ($creators as $creator) {
if (floatval($creator->balance) >= $threshold && !empty($creator->paypal_email)) {
WPDD_Admin_Payouts::create_payout($creator->ID, 'automatic');
}
}
}
public static function process_payout($payout_id) {
global $wpdb;
// Get payout details
$payout = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}wpdd_payouts WHERE id = %d",
$payout_id
));
if (!$payout) {
return array('success' => false, 'error' => 'Payout not found');
}
// Get PayPal credentials
$mode = get_option('wpdd_paypal_mode', 'sandbox');
$client_id = get_option('wpdd_paypal_client_id');
$secret = get_option('wpdd_paypal_secret');
if (empty($client_id) || empty($secret)) {
return array('success' => false, 'error' => 'PayPal credentials not configured');
}
// Get access token
$token_result = self::get_access_token($client_id, $secret, $mode);
if (!$token_result['success']) {
return array('success' => false, 'error' => $token_result['error']);
}
$access_token = $token_result['token'];
// Create payout batch
$batch_result = self::create_payout_batch($payout, $access_token, $mode);
if ($batch_result['success']) {
return array(
'success' => true,
'transaction_id' => $batch_result['batch_id']
);
} else {
return array(
'success' => false,
'error' => $batch_result['error']
);
}
}
private static function get_access_token($client_id, $secret, $mode) {
$base_url = $mode === 'sandbox'
? 'https://api-m.sandbox.paypal.com'
: 'https://api-m.paypal.com';
$response = wp_remote_post(
$base_url . '/v1/oauth2/token',
array(
'headers' => array(
'Authorization' => 'Basic ' . base64_encode($client_id . ':' . $secret),
'Content-Type' => 'application/x-www-form-urlencoded'
),
'body' => 'grant_type=client_credentials',
'timeout' => 30
)
);
if (is_wp_error($response)) {
return array('success' => false, 'error' => $response->get_error_message());
}
$body = json_decode(wp_remote_retrieve_body($response), true);
if (isset($body['access_token'])) {
return array('success' => true, 'token' => $body['access_token']);
} else {
$error = isset($body['error_description']) ? $body['error_description'] : 'Failed to get access token';
return array('success' => false, 'error' => $error);
}
}
private static function create_payout_batch($payout, $access_token, $mode) {
$base_url = $mode === 'sandbox'
? 'https://api-m.sandbox.paypal.com'
: 'https://api-m.paypal.com';
$batch_id = 'WPDD_' . $payout->id . '_' . time();
$payout_data = array(
'sender_batch_header' => array(
'sender_batch_id' => $batch_id,
'email_subject' => 'You have received a payout!',
'email_message' => 'You have received a payout from ' . get_bloginfo('name')
),
'items' => array(
array(
'recipient_type' => 'EMAIL',
'amount' => array(
'value' => number_format($payout->amount, 2, '.', ''),
'currency' => $payout->currency
),
'receiver' => $payout->paypal_email,
'note' => 'Payout for your sales on ' . get_bloginfo('name'),
'sender_item_id' => 'payout_' . $payout->id
)
)
);
$response = wp_remote_post(
$base_url . '/v1/payments/payouts',
array(
'headers' => array(
'Authorization' => 'Bearer ' . $access_token,
'Content-Type' => 'application/json'
),
'body' => json_encode($payout_data),
'timeout' => 30
)
);
if (is_wp_error($response)) {
return array('success' => false, 'error' => $response->get_error_message());
}
$response_code = wp_remote_retrieve_response_code($response);
$body = json_decode(wp_remote_retrieve_body($response), true);
if ($response_code === 201 && isset($body['batch_header']['payout_batch_id'])) {
return array(
'success' => true,
'batch_id' => $body['batch_header']['payout_batch_id']
);
} else {
$error = 'Failed to create payout batch';
if (isset($body['message'])) {
$error = $body['message'];
} elseif (isset($body['error_description'])) {
$error = $body['error_description'];
}
return array('success' => false, 'error' => $error);
}
}
public static function check_batch_status($batch_id, $mode = null) {
if (!$mode) {
$mode = get_option('wpdd_paypal_mode', 'sandbox');
}
$client_id = get_option('wpdd_paypal_client_id');
$secret = get_option('wpdd_paypal_secret');
if (empty($client_id) || empty($secret)) {
return array('success' => false, 'error' => 'PayPal credentials not configured');
}
// Get access token
$token_result = self::get_access_token($client_id, $secret, $mode);
if (!$token_result['success']) {
return array('success' => false, 'error' => $token_result['error']);
}
$access_token = $token_result['token'];
$base_url = $mode === 'sandbox'
? 'https://api-m.sandbox.paypal.com'
: 'https://api-m.paypal.com';
$response = wp_remote_get(
$base_url . '/v1/payments/payouts/' . $batch_id,
array(
'headers' => array(
'Authorization' => 'Bearer ' . $access_token,
'Content-Type' => 'application/json'
),
'timeout' => 30
)
);
if (is_wp_error($response)) {
return array('success' => false, 'error' => $response->get_error_message());
}
$body = json_decode(wp_remote_retrieve_body($response), true);
if (isset($body['batch_header'])) {
return array(
'success' => true,
'status' => $body['batch_header']['batch_status'],
'data' => $body
);
} else {
return array('success' => false, 'error' => 'Failed to get batch status');
}
}
public static function deactivate() {
wp_clear_scheduled_hook('wpdd_process_automatic_payouts');
}
}

View File

@@ -0,0 +1,302 @@
<?php
if (!defined('ABSPATH')) {
exit;
}
class WPDD_PayPal {
public static function init() {
add_action('wp_enqueue_scripts', array(__CLASS__, 'enqueue_paypal_sdk'));
add_action('wp_ajax_wpdd_create_paypal_order', array(__CLASS__, 'create_order'));
add_action('wp_ajax_nopriv_wpdd_create_paypal_order', array(__CLASS__, 'create_order'));
add_action('wp_ajax_wpdd_capture_paypal_order', array(__CLASS__, 'capture_order'));
add_action('wp_ajax_nopriv_wpdd_capture_paypal_order', array(__CLASS__, 'capture_order'));
}
public static function enqueue_paypal_sdk() {
if (!is_page(get_option('wpdd_checkout_page_id'))) {
return;
}
$client_id = get_option('wpdd_paypal_client_id');
$mode = get_option('wpdd_paypal_mode', 'sandbox');
if (!$client_id) {
return;
}
wp_enqueue_script(
'paypal-sdk',
'https://www.paypal.com/sdk/js?client-id=' . $client_id . '&currency=USD',
array(),
null,
true
);
wp_enqueue_script(
'wpdd-paypal',
WPDD_PLUGIN_URL . 'assets/js/paypal.js',
array('jquery', 'paypal-sdk'),
WPDD_VERSION,
true
);
wp_localize_script('wpdd-paypal', 'wpdd_paypal', array(
'ajax_url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('wpdd-paypal-nonce'),
'mode' => $mode
));
}
public static function create_order() {
check_ajax_referer('wpdd-paypal-nonce', 'nonce');
$product_id = isset($_POST['product_id']) ? intval($_POST['product_id']) : 0;
if (!$product_id) {
wp_send_json_error('Invalid product');
}
$product = get_post($product_id);
if (!$product || $product->post_type !== 'wpdd_product') {
wp_send_json_error('Product not found');
}
$price = get_post_meta($product_id, '_wpdd_price', true);
$sale_price = get_post_meta($product_id, '_wpdd_sale_price', true);
$final_price = ($sale_price && $sale_price < $price) ? $sale_price : $price;
$order_data = array(
'intent' => 'CAPTURE',
'purchase_units' => array(
array(
'reference_id' => 'wpdd_' . $product_id . '_' . time(),
'description' => substr($product->post_title, 0, 127),
'amount' => array(
'currency_code' => 'USD',
'value' => number_format($final_price, 2, '.', '')
)
)
),
'application_context' => array(
'brand_name' => get_bloginfo('name'),
'return_url' => add_query_arg('wpdd_paypal_return', '1', get_permalink(get_option('wpdd_thank_you_page_id'))),
'cancel_url' => add_query_arg('wpdd_paypal_cancel', '1', get_permalink(get_option('wpdd_checkout_page_id')))
)
);
$paypal_order = self::api_request('/v2/checkout/orders', $order_data, 'POST');
if (isset($paypal_order['id'])) {
$_SESSION['wpdd_paypal_order_' . $paypal_order['id']] = array(
'product_id' => $product_id,
'amount' => $final_price,
'customer_email' => sanitize_email($_POST['customer_email'] ?? ''),
'customer_name' => sanitize_text_field($_POST['customer_name'] ?? '')
);
wp_send_json_success(array('orderID' => $paypal_order['id']));
} else {
wp_send_json_error('Failed to create PayPal order');
}
}
public static function capture_order() {
check_ajax_referer('wpdd-paypal-nonce', 'nonce');
$paypal_order_id = isset($_POST['orderID']) ? sanitize_text_field($_POST['orderID']) : '';
if (!$paypal_order_id) {
wp_send_json_error('Invalid order ID');
}
$capture_response = self::api_request('/v2/checkout/orders/' . $paypal_order_id . '/capture', array(), 'POST');
if (isset($capture_response['status']) && $capture_response['status'] === 'COMPLETED') {
$session_data = $_SESSION['wpdd_paypal_order_' . $paypal_order_id] ?? array();
// Add error logging for debugging session issues
if (empty($session_data)) {
error_log('WPDD PayPal Error: No session data found for PayPal order ' . $paypal_order_id);
error_log('WPDD PayPal Debug: Session ID: ' . session_id());
error_log('WPDD PayPal Debug: Available sessions: ' . print_r($_SESSION ?? array(), true));
wp_send_json_error('Session data not found - order cannot be processed');
return;
}
$order_number = 'WPDD-' . strtoupper(uniqid());
global $wpdb;
$customer_id = 0;
$customer_email = $session_data['customer_email'];
$customer_name = $session_data['customer_name'];
if (is_user_logged_in()) {
$current_user = wp_get_current_user();
$customer_id = $current_user->ID;
$customer_email = $current_user->user_email;
$customer_name = $current_user->display_name;
} elseif (!empty($_POST['create_account']) && !empty($customer_email)) {
$username = strstr($customer_email, '@', true) . '_' . wp_rand(1000, 9999);
$password = wp_generate_password();
$user_id = wp_create_user($username, $password, $customer_email);
if (!is_wp_error($user_id)) {
$customer_id = $user_id;
wp_update_user(array(
'ID' => $user_id,
'display_name' => $customer_name,
'first_name' => $customer_name
));
$user = new WP_User($user_id);
$user->set_role('wpdd_customer');
wp_new_user_notification($user_id, null, 'both');
}
}
$product = get_post($session_data['product_id']);
$wpdb->insert(
$wpdb->prefix . 'wpdd_orders',
array(
'order_number' => $order_number,
'product_id' => $session_data['product_id'],
'customer_id' => $customer_id,
'creator_id' => $product->post_author,
'status' => 'completed',
'payment_method' => 'paypal',
'transaction_id' => $capture_response['id'],
'amount' => $session_data['amount'],
'currency' => 'USD',
'customer_email' => $customer_email,
'customer_name' => $customer_name,
'purchase_date' => current_time('mysql')
),
array('%s', '%d', '%d', '%d', '%s', '%s', '%s', '%f', '%s', '%s', '%s', '%s')
);
$order_id = $wpdb->insert_id;
self::generate_download_link($order_id);
self::send_purchase_email($order_id);
update_post_meta($session_data['product_id'], '_wpdd_sales_count',
intval(get_post_meta($session_data['product_id'], '_wpdd_sales_count', true)) + 1);
unset($_SESSION['wpdd_paypal_order_' . $paypal_order_id]);
wp_send_json_success(array(
'redirect_url' => add_query_arg(
'order_id',
$order_number,
get_permalink(get_option('wpdd_thank_you_page_id'))
)
));
} else {
wp_send_json_error('Payment capture failed');
}
}
private static function api_request($endpoint, $data = array(), $method = 'GET') {
$mode = get_option('wpdd_paypal_mode', 'sandbox');
$client_id = get_option('wpdd_paypal_client_id');
$secret = get_option('wpdd_paypal_secret');
if (!$client_id || !$secret) {
return false;
}
$base_url = $mode === 'live'
? 'https://api.paypal.com'
: 'https://api.sandbox.paypal.com';
$auth = base64_encode($client_id . ':' . $secret);
$args = array(
'method' => $method,
'headers' => array(
'Authorization' => 'Basic ' . $auth,
'Content-Type' => 'application/json'
),
'timeout' => 30
);
if (!empty($data)) {
$args['body'] = json_encode($data);
}
$response = wp_remote_request($base_url . $endpoint, $args);
if (is_wp_error($response)) {
return false;
}
$body = wp_remote_retrieve_body($response);
return json_decode($body, true);
}
private static function generate_download_link($order_id) {
global $wpdb;
$token = wp_hash(uniqid() . $order_id . time());
$expires_at = date('Y-m-d H:i:s', strtotime('+7 days'));
$wpdb->insert(
$wpdb->prefix . 'wpdd_download_links',
array(
'order_id' => $order_id,
'token' => $token,
'expires_at' => $expires_at,
'max_downloads' => 5,
'created_at' => current_time('mysql')
),
array('%d', '%s', '%s', '%d', '%s')
);
return $token;
}
private static function send_purchase_email($order_id) {
global $wpdb;
$order = $wpdb->get_row($wpdb->prepare(
"SELECT o.*, p.post_title as product_name, dl.token
FROM {$wpdb->prefix}wpdd_orders o
LEFT JOIN {$wpdb->posts} p ON o.product_id = p.ID
LEFT JOIN {$wpdb->prefix}wpdd_download_links dl ON o.id = dl.order_id
WHERE o.id = %d",
$order_id
));
if (!$order) {
return;
}
$download_url = add_query_arg(array(
'wpdd_download_token' => $order->token
), home_url());
$subject = sprintf(__('Your Purchase from %s', 'wp-digital-download'), get_bloginfo('name'));
$message = sprintf(
__("Hi %s,\n\nThank you for your purchase!\n\n", 'wp-digital-download'),
$order->customer_name
);
$message .= sprintf(__("Order Number: %s\n", 'wp-digital-download'), $order->order_number);
$message .= sprintf(__("Product: %s\n", 'wp-digital-download'), $order->product_name);
$message .= sprintf(__("Amount: $%s\n\n", 'wp-digital-download'), number_format($order->amount, 2));
$message .= __("Download your product here:\n", 'wp-digital-download');
$message .= $download_url . "\n\n";
$message .= __("This download link will expire in 7 days.\n\n", 'wp-digital-download');
$message .= sprintf(__("Best regards,\n%s", 'wp-digital-download'), get_bloginfo('name'));
wp_mail($order->customer_email, $subject, $message);
}
}

View File

@@ -0,0 +1,142 @@
<?php
if (!defined('ABSPATH')) {
exit;
}
class WPDD_Post_Types {
public static function init() {
// Register immediately since we're already in the init hook
self::register_post_types();
self::register_taxonomies();
add_filter('post_type_link', array(__CLASS__, 'product_permalink'), 10, 2);
}
public static function register_post_types() {
$labels = array(
'name' => __('Products', 'wp-digital-download'),
'singular_name' => __('Product', 'wp-digital-download'),
'menu_name' => __('Digital Products', 'wp-digital-download'),
'add_new' => __('Add New', 'wp-digital-download'),
'add_new_item' => __('Add New Product', 'wp-digital-download'),
'edit_item' => __('Edit Product', 'wp-digital-download'),
'new_item' => __('New Product', 'wp-digital-download'),
'view_item' => __('View Product', 'wp-digital-download'),
'view_items' => __('View Products', 'wp-digital-download'),
'search_items' => __('Search Products', 'wp-digital-download'),
'not_found' => __('No products found', 'wp-digital-download'),
'not_found_in_trash' => __('No products found in Trash', 'wp-digital-download'),
'all_items' => __('All Products', 'wp-digital-download'),
'archives' => __('Product Archives', 'wp-digital-download'),
'attributes' => __('Product Attributes', 'wp-digital-download'),
'insert_into_item' => __('Insert into product', 'wp-digital-download'),
'uploaded_to_this_item' => __('Uploaded to this product', 'wp-digital-download'),
'featured_image' => __('Product Image', 'wp-digital-download'),
'set_featured_image' => __('Set product image', 'wp-digital-download'),
'remove_featured_image' => __('Remove product image', 'wp-digital-download'),
'use_featured_image' => __('Use as product image', 'wp-digital-download'),
);
$args = array(
'labels' => $labels,
'public' => true,
'publicly_queryable' => true,
'show_ui' => true,
'show_in_menu' => true,
'query_var' => true,
'rewrite' => array('slug' => 'product'),
'capability_type' => 'post',
'capabilities' => array(
'edit_post' => 'edit_wpdd_product',
'read_post' => 'read_wpdd_product',
'delete_post' => 'delete_wpdd_product',
'edit_posts' => 'edit_wpdd_products',
'edit_others_posts' => 'edit_others_wpdd_products',
'publish_posts' => 'publish_wpdd_products',
'read_private_posts' => 'read_private_wpdd_products',
'delete_posts' => 'delete_wpdd_products',
'delete_private_posts' => 'delete_private_wpdd_products',
'delete_published_posts' => 'delete_published_wpdd_products',
'delete_others_posts' => 'delete_others_wpdd_products',
'edit_private_posts' => 'edit_private_wpdd_products',
'edit_published_posts' => 'edit_published_wpdd_products',
),
'map_meta_cap' => true,
'has_archive' => true,
'hierarchical' => false,
'menu_position' => 25,
'menu_icon' => 'dashicons-download',
'supports' => array('title', 'editor', 'thumbnail', 'excerpt', 'author'),
'show_in_rest' => true,
);
register_post_type('wpdd_product', $args);
}
public static function register_taxonomies() {
$labels = array(
'name' => __('Product Categories', 'wp-digital-download'),
'singular_name' => __('Product Category', 'wp-digital-download'),
'search_items' => __('Search Categories', 'wp-digital-download'),
'all_items' => __('All Categories', 'wp-digital-download'),
'parent_item' => __('Parent Category', 'wp-digital-download'),
'parent_item_colon' => __('Parent Category:', 'wp-digital-download'),
'edit_item' => __('Edit Category', 'wp-digital-download'),
'update_item' => __('Update Category', 'wp-digital-download'),
'add_new_item' => __('Add New Category', 'wp-digital-download'),
'new_item_name' => __('New Category Name', 'wp-digital-download'),
'menu_name' => __('Categories', 'wp-digital-download'),
);
$args = array(
'labels' => $labels,
'hierarchical' => true,
'public' => true,
'show_ui' => true,
'show_admin_column' => true,
'query_var' => true,
'rewrite' => array('slug' => 'product-category'),
'show_in_rest' => true,
);
register_taxonomy('wpdd_product_category', 'wpdd_product', $args);
$tag_labels = array(
'name' => __('Product Tags', 'wp-digital-download'),
'singular_name' => __('Product Tag', 'wp-digital-download'),
'search_items' => __('Search Tags', 'wp-digital-download'),
'popular_items' => __('Popular Tags', 'wp-digital-download'),
'all_items' => __('All Tags', 'wp-digital-download'),
'edit_item' => __('Edit Tag', 'wp-digital-download'),
'update_item' => __('Update Tag', 'wp-digital-download'),
'add_new_item' => __('Add New Tag', 'wp-digital-download'),
'new_item_name' => __('New Tag Name', 'wp-digital-download'),
'separate_items_with_commas' => __('Separate tags with commas', 'wp-digital-download'),
'add_or_remove_items' => __('Add or remove tags', 'wp-digital-download'),
'choose_from_most_used' => __('Choose from the most used tags', 'wp-digital-download'),
'menu_name' => __('Tags', 'wp-digital-download'),
);
$tag_args = array(
'labels' => $tag_labels,
'hierarchical' => false,
'public' => true,
'show_ui' => true,
'show_admin_column' => true,
'query_var' => true,
'rewrite' => array('slug' => 'product-tag'),
'show_in_rest' => true,
);
register_taxonomy('wpdd_product_tag', 'wpdd_product', $tag_args);
}
public static function product_permalink($permalink, $post) {
if ($post->post_type !== 'wpdd_product') {
return $permalink;
}
return $permalink;
}
}

View File

@@ -0,0 +1,116 @@
<?php
if (!defined('ABSPATH')) {
exit;
}
class WPDD_Roles {
public static function init() {
// Call immediately since we're already in init hook
self::maybe_create_roles();
}
public static function maybe_create_roles() {
if (get_option('wpdd_roles_created') !== WPDD_VERSION) {
self::create_roles();
update_option('wpdd_roles_created', WPDD_VERSION);
}
}
public static function create_roles() {
self::create_customer_role();
self::create_creator_role();
self::add_admin_capabilities();
}
private static function create_customer_role() {
add_role(
'wpdd_customer',
__('Digital Customer', 'wp-digital-download'),
array(
'read' => true,
'wpdd_view_purchases' => true,
'wpdd_download_products' => true,
)
);
}
private static function create_creator_role() {
add_role(
'wpdd_creator',
__('Digital Creator', 'wp-digital-download'),
array(
'read' => true,
'upload_files' => true,
'edit_posts' => false,
'delete_posts' => false,
'publish_posts' => false,
'edit_wpdd_products' => true,
'edit_published_wpdd_products' => true,
'publish_wpdd_products' => true,
'delete_wpdd_products' => true,
'delete_published_wpdd_products' => true,
'edit_private_wpdd_products' => true,
'delete_private_wpdd_products' => true,
'wpdd_view_own_sales' => true,
'wpdd_manage_own_products' => true,
'wpdd_upload_product_files' => true,
'wpdd_view_reports' => true,
)
);
}
private static function add_admin_capabilities() {
$role = get_role('administrator');
if ($role) {
$role->add_cap('edit_wpdd_products');
$role->add_cap('edit_others_wpdd_products');
$role->add_cap('edit_published_wpdd_products');
$role->add_cap('publish_wpdd_products');
$role->add_cap('delete_wpdd_products');
$role->add_cap('delete_others_wpdd_products');
$role->add_cap('delete_published_wpdd_products');
$role->add_cap('edit_private_wpdd_products');
$role->add_cap('delete_private_wpdd_products');
$role->add_cap('wpdd_manage_settings');
$role->add_cap('wpdd_view_all_sales');
$role->add_cap('wpdd_manage_all_products');
$role->add_cap('wpdd_view_reports');
$role->add_cap('wpdd_manage_orders');
}
}
public static function remove_roles() {
remove_role('wpdd_customer');
remove_role('wpdd_creator');
$role = get_role('administrator');
if ($role) {
$caps = array(
'edit_wpdd_products',
'edit_others_wpdd_products',
'edit_published_wpdd_products',
'publish_wpdd_products',
'delete_wpdd_products',
'delete_others_wpdd_products',
'delete_published_wpdd_products',
'edit_private_wpdd_products',
'delete_private_wpdd_products',
'wpdd_manage_settings',
'wpdd_view_all_sales',
'wpdd_manage_all_products',
'wpdd_view_reports',
'wpdd_manage_orders'
);
foreach ($caps as $cap) {
$role->remove_cap($cap);
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,249 @@
<?php
if (!defined('ABSPATH')) {
exit;
}
class WPDD_Watermark {
public static function apply_watermark($file_path, $order) {
$file_extension = strtolower(pathinfo($file_path, PATHINFO_EXTENSION));
if (filter_var($file_path, FILTER_VALIDATE_URL)) {
$upload_dir = wp_upload_dir();
if (strpos($file_path, $upload_dir['baseurl']) === 0) {
$file_path = str_replace($upload_dir['baseurl'], $upload_dir['basedir'], $file_path);
} else {
return false;
}
}
if (!file_exists($file_path)) {
return false;
}
$product_id = $order->product_id;
$watermark_text = get_post_meta($product_id, '_wpdd_watermark_text', true);
if (empty($watermark_text)) {
$watermark_text = '{customer_email}';
}
$watermark_text = self::parse_watermark_placeholders($watermark_text, $order);
switch ($file_extension) {
case 'jpg':
case 'jpeg':
case 'png':
case 'gif':
return self::watermark_image($file_path, $watermark_text);
case 'pdf':
return self::watermark_pdf($file_path, $watermark_text);
default:
return false;
}
}
private static function parse_watermark_placeholders($text, $order) {
$customer = get_userdata($order->customer_id);
$replacements = array(
'{customer_name}' => $order->customer_name,
'{customer_email}' => $order->customer_email,
'{order_id}' => $order->order_number,
'{date}' => date_i18n(get_option('date_format')),
'{site_name}' => get_bloginfo('name')
);
return str_replace(array_keys($replacements), array_values($replacements), $text);
}
private static function watermark_image($file_path, $watermark_text) {
if (!function_exists('imagecreatefrompng')) {
return false;
}
$file_info = pathinfo($file_path);
$extension = strtolower($file_info['extension']);
switch ($extension) {
case 'jpg':
case 'jpeg':
$image = imagecreatefromjpeg($file_path);
break;
case 'png':
$image = imagecreatefrompng($file_path);
break;
case 'gif':
$image = imagecreatefromgif($file_path);
break;
default:
return false;
}
if (!$image) {
return false;
}
$width = imagesx($image);
$height = imagesy($image);
$font_size = max(10, min(30, $width / 40));
$font_file = WPDD_PLUGIN_PATH . 'assets/fonts/arial.ttf';
if (!file_exists($font_file)) {
$font = 5;
$text_width = imagefontwidth($font) * strlen($watermark_text);
$text_height = imagefontheight($font);
} else {
$bbox = imagettfbbox($font_size, 0, $font_file, $watermark_text);
$text_width = $bbox[2] - $bbox[0];
$text_height = $bbox[1] - $bbox[7];
}
$x = ($width - $text_width) / 2;
$y = $height - 50;
$text_color = imagecolorallocatealpha($image, 255, 255, 255, 30);
$shadow_color = imagecolorallocatealpha($image, 0, 0, 0, 50);
if (file_exists($font_file)) {
imagettftext($image, $font_size, 0, $x + 2, $y + 2, $shadow_color, $font_file, $watermark_text);
imagettftext($image, $font_size, 0, $x, $y, $text_color, $font_file, $watermark_text);
} else {
imagestring($image, $font, $x + 2, $y + 2, $watermark_text, $shadow_color);
imagestring($image, $font, $x, $y, $watermark_text, $text_color);
}
$temp_dir = get_temp_dir();
$temp_file = $temp_dir . 'wpdd_watermark_' . uniqid() . '.' . $extension;
switch ($extension) {
case 'jpg':
case 'jpeg':
imagejpeg($image, $temp_file, 90);
break;
case 'png':
imagepng($image, $temp_file, 9);
break;
case 'gif':
imagegif($image, $temp_file);
break;
}
imagedestroy($image);
return $temp_file;
}
private static function watermark_pdf($file_path, $watermark_text) {
if (!class_exists('TCPDF') && !class_exists('FPDF')) {
return self::watermark_pdf_basic($file_path, $watermark_text);
}
if (class_exists('TCPDF')) {
return self::watermark_pdf_tcpdf($file_path, $watermark_text);
}
return false;
}
private static function watermark_pdf_basic($file_path, $watermark_text) {
$temp_dir = get_temp_dir();
$temp_file = $temp_dir . 'wpdd_watermark_' . uniqid() . '.pdf';
if (copy($file_path, $temp_file)) {
return $temp_file;
}
return false;
}
private static function watermark_pdf_tcpdf($file_path, $watermark_text) {
require_once ABSPATH . 'wp-admin/includes/class-pclzip.php';
$pdf = new TCPDF();
$pdf->SetProtection(array('print'), '', null, 0, null);
$pagecount = $pdf->setSourceFile($file_path);
for ($i = 1; $i <= $pagecount; $i++) {
$tplidx = $pdf->importPage($i);
$pdf->AddPage();
$pdf->useTemplate($tplidx);
$pdf->SetFont('helvetica', '', 12);
$pdf->SetTextColor(200, 200, 200);
$pdf->SetAlpha(0.5);
$pdf->StartTransform();
$pdf->Rotate(45, $pdf->getPageWidth() / 2, $pdf->getPageHeight() / 2);
$pdf->Text(
$pdf->getPageWidth() / 2 - 50,
$pdf->getPageHeight() / 2,
$watermark_text
);
$pdf->StopTransform();
$pdf->SetAlpha(1);
}
$temp_dir = get_temp_dir();
$temp_file = $temp_dir . 'wpdd_watermark_' . uniqid() . '.pdf';
$pdf->Output($temp_file, 'F');
return $temp_file;
}
public static function add_text_watermark($content, $watermark_text) {
$watermark_html = sprintf(
'<div style="position: fixed; top: 50%%; left: 50%%; transform: translate(-50%%, -50%%) rotate(-45deg);
opacity: 0.1; font-size: 48px; color: #000; z-index: -1; user-select: none;">%s</div>',
esc_html($watermark_text)
);
return $watermark_html . $content;
}
public static function get_watermark_settings() {
return array(
'enabled' => get_option('wpdd_watermark_enabled', false),
'text' => get_option('wpdd_watermark_text', '{customer_email}'),
'position' => get_option('wpdd_watermark_position', 'center'),
'opacity' => get_option('wpdd_watermark_opacity', 30),
'font_size' => get_option('wpdd_watermark_font_size', 'auto'),
'color' => get_option('wpdd_watermark_color', '#ffffff')
);
}
public static function preview_watermark($file_type = 'image') {
$settings = self::get_watermark_settings();
$preview_text = str_replace(
array('{customer_name}', '{customer_email}', '{order_id}', '{date}', '{site_name}'),
array('John Doe', 'john@example.com', 'WPDD-123456', date_i18n(get_option('date_format')), get_bloginfo('name')),
$settings['text']
);
if ($file_type === 'image') {
$width = 600;
$height = 400;
$image = imagecreatetruecolor($width, $height);
$bg_color = imagecolorallocate($image, 240, 240, 240);
imagefill($image, 0, 0, $bg_color);
$text_color = imagecolorallocatealpha($image, 100, 100, 100, 50);
$font_size = 20;
$x = ($width - (strlen($preview_text) * 10)) / 2;
$y = $height / 2;
imagestring($image, 5, $x, $y, $preview_text, $text_color);
header('Content-Type: image/png');
imagepng($image);
imagedestroy($image);
}
}
}

1
node_modules/.bin/playwright generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../@playwright/test/cli.js

1
node_modules/.bin/playwright-core generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../playwright-core/cli.js

72
node_modules/.package-lock.json generated vendored Normal file
View File

@@ -0,0 +1,72 @@
{
"name": "wp-digital-download-tests",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"node_modules/@playwright/test": {
"version": "1.55.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.55.0.tgz",
"integrity": "sha512-04IXzPwHrW69XusN/SIdDdKZBzMfOT9UNT/YiJit/xpy2VuAoB8NHc8Aplb96zsWDddLnbkPL3TsmrS04ZU2xQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.55.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"ideallyInert": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/playwright": {
"version": "1.55.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.0.tgz",
"integrity": "sha512-sdCWStblvV1YU909Xqx0DhOjPZE4/5lJsIS84IfN9dAZfcl/CIZ5O8l3o0j7hPMjDvqoTF8ZUcc+i/GL5erstA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.55.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.55.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.0.tgz",
"integrity": "sha512-GvZs4vU3U5ro2nZpeiwyb0zuFaqb9sUiAJuyrWpcGouD8y9/HLgGbNRjIph7zU9D3hnPaisMl9zG9CgFi/biIg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
}
}
}

202
node_modules/@playwright/test/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Portions Copyright (c) Microsoft Corporation.
Portions Copyright 2017 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

5
node_modules/@playwright/test/NOTICE generated vendored Normal file
View File

@@ -0,0 +1,5 @@
Playwright
Copyright (c) Microsoft Corporation
This software contains code derived from the Puppeteer project (https://github.com/puppeteer/puppeteer),
available under the Apache 2.0 license (https://github.com/puppeteer/puppeteer/blob/master/LICENSE).

168
node_modules/@playwright/test/README.md generated vendored Normal file
View File

@@ -0,0 +1,168 @@
# 🎭 Playwright
[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[![Chromium version](https://img.shields.io/badge/chromium-140.0.7339.16-blue.svg?logo=google-chrome)](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[![Firefox version](https://img.shields.io/badge/firefox-141.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[![WebKit version](https://img.shields.io/badge/webkit-26.0-blue.svg?logo=safari)](https://webkit.org/)<!-- GEN:stop --> [![Join Discord](https://img.shields.io/badge/join-discord-informational)](https://aka.ms/playwright/discord)
## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright)
Playwright is a framework for Web Testing and Automation. It allows testing [Chromium](https://www.chromium.org/Home), [Firefox](https://www.mozilla.org/en-US/firefox/new/) and [WebKit](https://webkit.org/) with a single API. Playwright is built to enable cross-browser web automation that is **ever-green**, **capable**, **reliable** and **fast**.
| | Linux | macOS | Windows |
| :--- | :---: | :---: | :---: |
| Chromium <!-- GEN:chromium-version -->140.0.7339.16<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| WebKit <!-- GEN:webkit-version -->26.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Firefox <!-- GEN:firefox-version -->141.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
Headless execution is supported for all browsers on all platforms. Check out [system requirements](https://playwright.dev/docs/intro#system-requirements) for details.
Looking for Playwright for [Python](https://playwright.dev/python/docs/intro), [.NET](https://playwright.dev/dotnet/docs/intro), or [Java](https://playwright.dev/java/docs/intro)?
## Installation
Playwright has its own test runner for end-to-end tests, we call it Playwright Test.
### Using init command
The easiest way to get started with Playwright Test is to run the init command.
```Shell
# Run from your project's root directory
npm init playwright@latest
# Or create a new project
npm init playwright@latest new-project
```
This will create a configuration file, optionally add examples, a GitHub Action workflow and a first test example.spec.ts. You can now jump directly to writing assertions section.
### Manually
Add dependency and install browsers.
```Shell
npm i -D @playwright/test
# install supported browsers
npx playwright install
```
You can optionally install only selected browsers, see [install browsers](https://playwright.dev/docs/cli#install-browsers) for more details. Or you can install no browsers at all and use existing [browser channels](https://playwright.dev/docs/browsers).
* [Getting started](https://playwright.dev/docs/intro)
* [API reference](https://playwright.dev/docs/api/class-playwright)
## Capabilities
### Resilient • No flaky tests
**Auto-wait**. Playwright waits for elements to be actionable prior to performing actions. It also has a rich set of introspection events. The combination of the two eliminates the need for artificial timeouts - a primary cause of flaky tests.
**Web-first assertions**. Playwright assertions are created specifically for the dynamic web. Checks are automatically retried until the necessary conditions are met.
**Tracing**. Configure test retry strategy, capture execution trace, videos and screenshots to eliminate flakes.
### No trade-offs • No limits
Browsers run web content belonging to different origins in different processes. Playwright is aligned with the architecture of the modern browsers and runs tests out-of-process. This makes Playwright free of the typical in-process test runner limitations.
**Multiple everything**. Test scenarios that span multiple tabs, multiple origins and multiple users. Create scenarios with different contexts for different users and run them against your server, all in one test.
**Trusted events**. Hover elements, interact with dynamic controls and produce trusted events. Playwright uses real browser input pipeline indistinguishable from the real user.
Test frames, pierce Shadow DOM. Playwright selectors pierce shadow DOM and allow entering frames seamlessly.
### Full isolation • Fast execution
**Browser contexts**. Playwright creates a browser context for each test. Browser context is equivalent to a brand new browser profile. This delivers full test isolation with zero overhead. Creating a new browser context only takes a handful of milliseconds.
**Log in once**. Save the authentication state of the context and reuse it in all the tests. This bypasses repetitive log-in operations in each test, yet delivers full isolation of independent tests.
### Powerful Tooling
**[Codegen](https://playwright.dev/docs/codegen)**. Generate tests by recording your actions. Save them into any language.
**[Playwright inspector](https://playwright.dev/docs/inspector)**. Inspect page, generate selectors, step through the test execution, see click points and explore execution logs.
**[Trace Viewer](https://playwright.dev/docs/trace-viewer)**. Capture all the information to investigate the test failure. Playwright trace contains test execution screencast, live DOM snapshots, action explorer, test source and many more.
Looking for Playwright for [TypeScript](https://playwright.dev/docs/intro), [JavaScript](https://playwright.dev/docs/intro), [Python](https://playwright.dev/python/docs/intro), [.NET](https://playwright.dev/dotnet/docs/intro), or [Java](https://playwright.dev/java/docs/intro)?
## Examples
To learn how to run these Playwright Test examples, check out our [getting started docs](https://playwright.dev/docs/intro).
#### Page screenshot
This code snippet navigates to Playwright homepage and saves a screenshot.
```TypeScript
import { test } from '@playwright/test';
test('Page Screenshot', async ({ page }) => {
await page.goto('https://playwright.dev/');
await page.screenshot({ path: `example.png` });
});
```
#### Mobile and geolocation
This snippet emulates Mobile Safari on a device at given geolocation, navigates to maps.google.com, performs the action and takes a screenshot.
```TypeScript
import { test, devices } from '@playwright/test';
test.use({
...devices['iPhone 13 Pro'],
locale: 'en-US',
geolocation: { longitude: 12.492507, latitude: 41.889938 },
permissions: ['geolocation'],
})
test('Mobile and geolocation', async ({ page }) => {
await page.goto('https://maps.google.com');
await page.getByText('Your location').click();
await page.waitForRequest(/.*preview\/pwa/);
await page.screenshot({ path: 'colosseum-iphone.png' });
});
```
#### Evaluate in browser context
This code snippet navigates to example.com, and executes a script in the page context.
```TypeScript
import { test } from '@playwright/test';
test('Evaluate in browser context', async ({ page }) => {
await page.goto('https://www.example.com/');
const dimensions = await page.evaluate(() => {
return {
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight,
deviceScaleFactor: window.devicePixelRatio
}
});
console.log(dimensions);
});
```
#### Intercept network requests
This code snippet sets up request routing for a page to log all network requests.
```TypeScript
import { test } from '@playwright/test';
test('Intercept network requests', async ({ page }) => {
// Log and continue all network requests
await page.route('**', route => {
console.log(route.request().url());
route.continue();
});
await page.goto('http://todomvc.com');
});
```
## Resources
* [Documentation](https://playwright.dev)
* [API reference](https://playwright.dev/docs/api/class-playwright/)
* [Contribution guide](CONTRIBUTING.md)
* [Changelog](https://github.com/microsoft/playwright/releases)

19
node_modules/@playwright/test/cli.js generated vendored Executable file
View File

@@ -0,0 +1,19 @@
#!/usr/bin/env node
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const { program } = require('playwright/lib/program');
program.parse(process.argv);

18
node_modules/@playwright/test/index.d.ts generated vendored Normal file
View File

@@ -0,0 +1,18 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from 'playwright/test';
export { default } from 'playwright/test';

17
node_modules/@playwright/test/index.js generated vendored Normal file
View File

@@ -0,0 +1,17 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
module.exports = require('playwright/test');

18
node_modules/@playwright/test/index.mjs generated vendored Normal file
View File

@@ -0,0 +1,18 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from 'playwright/test';
export { default } from 'playwright/test';

35
node_modules/@playwright/test/package.json generated vendored Normal file
View File

@@ -0,0 +1,35 @@
{
"name": "@playwright/test",
"version": "1.55.0",
"description": "A high-level API to automate web browsers",
"repository": {
"type": "git",
"url": "git+https://github.com/microsoft/playwright.git"
},
"homepage": "https://playwright.dev",
"engines": {
"node": ">=18"
},
"author": {
"name": "Microsoft Corporation"
},
"license": "Apache-2.0",
"exports": {
".": {
"types": "./index.d.ts",
"import": "./index.mjs",
"require": "./index.js",
"default": "./index.js"
},
"./cli": "./cli.js",
"./package.json": "./package.json",
"./reporter": "./reporter.js"
},
"bin": {
"playwright": "cli.js"
},
"scripts": {},
"dependencies": {
"playwright": "1.55.0"
}
}

17
node_modules/@playwright/test/reporter.d.ts generated vendored Normal file
View File

@@ -0,0 +1,17 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from 'playwright/types/testReporter';

17
node_modules/@playwright/test/reporter.js generated vendored Normal file
View File

@@ -0,0 +1,17 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// We only export types in reporter.d.ts.

17
node_modules/@playwright/test/reporter.mjs generated vendored Normal file
View File

@@ -0,0 +1,17 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// We only export types in reporter.d.ts.

202
node_modules/playwright-core/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Portions Copyright (c) Microsoft Corporation.
Portions Copyright 2017 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

5
node_modules/playwright-core/NOTICE generated vendored Normal file
View File

@@ -0,0 +1,5 @@
Playwright
Copyright (c) Microsoft Corporation
This software contains code derived from the Puppeteer project (https://github.com/puppeteer/puppeteer),
available under the Apache 2.0 license (https://github.com/puppeteer/puppeteer/blob/master/LICENSE).

3
node_modules/playwright-core/README.md generated vendored Normal file
View File

@@ -0,0 +1,3 @@
# playwright-core
This package contains the no-browser flavor of [Playwright](http://github.com/microsoft/playwright).

1502
node_modules/playwright-core/ThirdPartyNotices.txt generated vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,5 @@
$osInfo = Get-WmiObject -Class Win32_OperatingSystem
# check if running on Windows Server
if ($osInfo.ProductType -eq 3) {
Install-WindowsFeature Server-Media-Foundation
}

View File

@@ -0,0 +1,42 @@
#!/usr/bin/env bash
set -e
set -x
if [[ $(arch) == "aarch64" ]]; then
echo "ERROR: not supported on Linux Arm64"
exit 1
fi
if [ -z "$PLAYWRIGHT_HOST_PLATFORM_OVERRIDE" ]; then
if [[ ! -f "/etc/os-release" ]]; then
echo "ERROR: cannot install on unknown linux distribution (/etc/os-release is missing)"
exit 1
fi
ID=$(bash -c 'source /etc/os-release && echo $ID')
if [[ "${ID}" != "ubuntu" && "${ID}" != "debian" ]]; then
echo "ERROR: cannot install on $ID distribution - only Ubuntu and Debian are supported"
exit 1
fi
fi
# 1. make sure to remove old beta if any.
if dpkg --get-selections | grep -q "^google-chrome-beta[[:space:]]*install$" >/dev/null; then
apt-get remove -y google-chrome-beta
fi
# 2. Update apt lists (needed to install curl and chrome dependencies)
apt-get update
# 3. Install curl to download chrome
if ! command -v curl >/dev/null; then
apt-get install -y curl
fi
# 4. download chrome beta from dl.google.com and install it.
cd /tmp
curl -O https://dl.google.com/linux/direct/google-chrome-beta_current_amd64.deb
apt-get install -y ./google-chrome-beta_current_amd64.deb
rm -rf ./google-chrome-beta_current_amd64.deb
cd -
google-chrome-beta --version

View File

@@ -0,0 +1,13 @@
#!/usr/bin/env bash
set -e
set -x
rm -rf "/Applications/Google Chrome Beta.app"
cd /tmp
curl --retry 3 -o ./googlechromebeta.dmg -k https://dl.google.com/chrome/mac/universal/beta/googlechromebeta.dmg
hdiutil attach -nobrowse -quiet -noautofsck -noautoopen -mountpoint /Volumes/googlechromebeta.dmg ./googlechromebeta.dmg
cp -pR "/Volumes/googlechromebeta.dmg/Google Chrome Beta.app" /Applications
hdiutil detach /Volumes/googlechromebeta.dmg
rm -rf /tmp/googlechromebeta.dmg
/Applications/Google\ Chrome\ Beta.app/Contents/MacOS/Google\ Chrome\ Beta --version

View File

@@ -0,0 +1,24 @@
$ErrorActionPreference = 'Stop'
$url = 'https://dl.google.com/tag/s/dl/chrome/install/beta/googlechromebetastandaloneenterprise64.msi'
Write-Host "Downloading Google Chrome Beta"
$wc = New-Object net.webclient
$msiInstaller = "$env:temp\google-chrome-beta.msi"
$wc.Downloadfile($url, $msiInstaller)
Write-Host "Installing Google Chrome Beta"
$arguments = "/i `"$msiInstaller`" /quiet"
Start-Process msiexec.exe -ArgumentList $arguments -Wait
Remove-Item $msiInstaller
$suffix = "\\Google\\Chrome Beta\\Application\\chrome.exe"
if (Test-Path "${env:ProgramFiles(x86)}$suffix") {
(Get-Item "${env:ProgramFiles(x86)}$suffix").VersionInfo
} elseif (Test-Path "${env:ProgramFiles}$suffix") {
(Get-Item "${env:ProgramFiles}$suffix").VersionInfo
} else {
Write-Host "ERROR: Failed to install Google Chrome Beta."
Write-Host "ERROR: This could be due to insufficient privileges, in which case re-running as Administrator may help."
exit 1
}

View File

@@ -0,0 +1,42 @@
#!/usr/bin/env bash
set -e
set -x
if [[ $(arch) == "aarch64" ]]; then
echo "ERROR: not supported on Linux Arm64"
exit 1
fi
if [ -z "$PLAYWRIGHT_HOST_PLATFORM_OVERRIDE" ]; then
if [[ ! -f "/etc/os-release" ]]; then
echo "ERROR: cannot install on unknown linux distribution (/etc/os-release is missing)"
exit 1
fi
ID=$(bash -c 'source /etc/os-release && echo $ID')
if [[ "${ID}" != "ubuntu" && "${ID}" != "debian" ]]; then
echo "ERROR: cannot install on $ID distribution - only Ubuntu and Debian are supported"
exit 1
fi
fi
# 1. make sure to remove old stable if any.
if dpkg --get-selections | grep -q "^google-chrome[[:space:]]*install$" >/dev/null; then
apt-get remove -y google-chrome
fi
# 2. Update apt lists (needed to install curl and chrome dependencies)
apt-get update
# 3. Install curl to download chrome
if ! command -v curl >/dev/null; then
apt-get install -y curl
fi
# 4. download chrome stable from dl.google.com and install it.
cd /tmp
curl -O https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
apt-get install -y ./google-chrome-stable_current_amd64.deb
rm -rf ./google-chrome-stable_current_amd64.deb
cd -
google-chrome --version

View File

@@ -0,0 +1,12 @@
#!/usr/bin/env bash
set -e
set -x
rm -rf "/Applications/Google Chrome.app"
cd /tmp
curl --retry 3 -o ./googlechrome.dmg -k https://dl.google.com/chrome/mac/universal/stable/GGRO/googlechrome.dmg
hdiutil attach -nobrowse -quiet -noautofsck -noautoopen -mountpoint /Volumes/googlechrome.dmg ./googlechrome.dmg
cp -pR "/Volumes/googlechrome.dmg/Google Chrome.app" /Applications
hdiutil detach /Volumes/googlechrome.dmg
rm -rf /tmp/googlechrome.dmg
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --version

View File

@@ -0,0 +1,24 @@
$ErrorActionPreference = 'Stop'
$url = 'https://dl.google.com/tag/s/dl/chrome/install/googlechromestandaloneenterprise64.msi'
$wc = New-Object net.webclient
$msiInstaller = "$env:temp\google-chrome.msi"
Write-Host "Downloading Google Chrome"
$wc.Downloadfile($url, $msiInstaller)
Write-Host "Installing Google Chrome"
$arguments = "/i `"$msiInstaller`" /quiet"
Start-Process msiexec.exe -ArgumentList $arguments -Wait
Remove-Item $msiInstaller
$suffix = "\\Google\\Chrome\\Application\\chrome.exe"
if (Test-Path "${env:ProgramFiles(x86)}$suffix") {
(Get-Item "${env:ProgramFiles(x86)}$suffix").VersionInfo
} elseif (Test-Path "${env:ProgramFiles}$suffix") {
(Get-Item "${env:ProgramFiles}$suffix").VersionInfo
} else {
Write-Host "ERROR: Failed to install Google Chrome."
Write-Host "ERROR: This could be due to insufficient privileges, in which case re-running as Administrator may help."
exit 1
}

View File

@@ -0,0 +1,48 @@
#!/usr/bin/env bash
set -e
set -x
if [[ $(arch) == "aarch64" ]]; then
echo "ERROR: not supported on Linux Arm64"
exit 1
fi
if [ -z "$PLAYWRIGHT_HOST_PLATFORM_OVERRIDE" ]; then
if [[ ! -f "/etc/os-release" ]]; then
echo "ERROR: cannot install on unknown linux distribution (/etc/os-release is missing)"
exit 1
fi
ID=$(bash -c 'source /etc/os-release && echo $ID')
if [[ "${ID}" != "ubuntu" && "${ID}" != "debian" ]]; then
echo "ERROR: cannot install on $ID distribution - only Ubuntu and Debian are supported"
exit 1
fi
fi
# 1. make sure to remove old beta if any.
if dpkg --get-selections | grep -q "^microsoft-edge-beta[[:space:]]*install$" >/dev/null; then
apt-get remove -y microsoft-edge-beta
fi
# 2. Install curl to download Microsoft gpg key
if ! command -v curl >/dev/null; then
apt-get update
apt-get install -y curl
fi
# GnuPG is not preinstalled in slim images
if ! command -v gpg >/dev/null; then
apt-get update
apt-get install -y gpg
fi
# 3. Add the GPG key, the apt repo, update the apt cache, and install the package
curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > /tmp/microsoft.gpg
install -o root -g root -m 644 /tmp/microsoft.gpg /etc/apt/trusted.gpg.d/
sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/repos/edge stable main" > /etc/apt/sources.list.d/microsoft-edge-dev.list'
rm /tmp/microsoft.gpg
apt-get update && apt-get install -y microsoft-edge-beta
microsoft-edge-beta --version

View File

@@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -e
set -x
cd /tmp
curl --retry 3 -o ./msedge_beta.pkg -k "$1"
# Note: there's no way to uninstall previously installed MSEdge.
# However, running PKG again seems to update installation.
sudo installer -pkg /tmp/msedge_beta.pkg -target /
rm -rf /tmp/msedge_beta.pkg
/Applications/Microsoft\ Edge\ Beta.app/Contents/MacOS/Microsoft\ Edge\ Beta --version

View File

@@ -0,0 +1,23 @@
$ErrorActionPreference = 'Stop'
$url = $args[0]
Write-Host "Downloading Microsoft Edge Beta"
$wc = New-Object net.webclient
$msiInstaller = "$env:temp\microsoft-edge-beta.msi"
$wc.Downloadfile($url, $msiInstaller)
Write-Host "Installing Microsoft Edge Beta"
$arguments = "/i `"$msiInstaller`" /quiet"
Start-Process msiexec.exe -ArgumentList $arguments -Wait
Remove-Item $msiInstaller
$suffix = "\\Microsoft\\Edge Beta\\Application\\msedge.exe"
if (Test-Path "${env:ProgramFiles(x86)}$suffix") {
(Get-Item "${env:ProgramFiles(x86)}$suffix").VersionInfo
} elseif (Test-Path "${env:ProgramFiles}$suffix") {
(Get-Item "${env:ProgramFiles}$suffix").VersionInfo
} else {
Write-Host "ERROR: Failed to install Microsoft Edge Beta."
Write-Host "ERROR: This could be due to insufficient privileges, in which case re-running as Administrator may help."
exit 1
}

View File

@@ -0,0 +1,48 @@
#!/usr/bin/env bash
set -e
set -x
if [[ $(arch) == "aarch64" ]]; then
echo "ERROR: not supported on Linux Arm64"
exit 1
fi
if [ -z "$PLAYWRIGHT_HOST_PLATFORM_OVERRIDE" ]; then
if [[ ! -f "/etc/os-release" ]]; then
echo "ERROR: cannot install on unknown linux distribution (/etc/os-release is missing)"
exit 1
fi
ID=$(bash -c 'source /etc/os-release && echo $ID')
if [[ "${ID}" != "ubuntu" && "${ID}" != "debian" ]]; then
echo "ERROR: cannot install on $ID distribution - only Ubuntu and Debian are supported"
exit 1
fi
fi
# 1. make sure to remove old dev if any.
if dpkg --get-selections | grep -q "^microsoft-edge-dev[[:space:]]*install$" >/dev/null; then
apt-get remove -y microsoft-edge-dev
fi
# 2. Install curl to download Microsoft gpg key
if ! command -v curl >/dev/null; then
apt-get update
apt-get install -y curl
fi
# GnuPG is not preinstalled in slim images
if ! command -v gpg >/dev/null; then
apt-get update
apt-get install -y gpg
fi
# 3. Add the GPG key, the apt repo, update the apt cache, and install the package
curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > /tmp/microsoft.gpg
install -o root -g root -m 644 /tmp/microsoft.gpg /etc/apt/trusted.gpg.d/
sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/repos/edge stable main" > /etc/apt/sources.list.d/microsoft-edge-dev.list'
rm /tmp/microsoft.gpg
apt-get update && apt-get install -y microsoft-edge-dev
microsoft-edge-dev --version

11
node_modules/playwright-core/bin/reinstall_msedge_dev_mac.sh generated vendored Executable file
View File

@@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -e
set -x
cd /tmp
curl --retry 3 -o ./msedge_dev.pkg -k "$1"
# Note: there's no way to uninstall previously installed MSEdge.
# However, running PKG again seems to update installation.
sudo installer -pkg /tmp/msedge_dev.pkg -target /
rm -rf /tmp/msedge_dev.pkg
/Applications/Microsoft\ Edge\ Dev.app/Contents/MacOS/Microsoft\ Edge\ Dev --version

View File

@@ -0,0 +1,23 @@
$ErrorActionPreference = 'Stop'
$url = $args[0]
Write-Host "Downloading Microsoft Edge Dev"
$wc = New-Object net.webclient
$msiInstaller = "$env:temp\microsoft-edge-dev.msi"
$wc.Downloadfile($url, $msiInstaller)
Write-Host "Installing Microsoft Edge Dev"
$arguments = "/i `"$msiInstaller`" /quiet"
Start-Process msiexec.exe -ArgumentList $arguments -Wait
Remove-Item $msiInstaller
$suffix = "\\Microsoft\\Edge Dev\\Application\\msedge.exe"
if (Test-Path "${env:ProgramFiles(x86)}$suffix") {
(Get-Item "${env:ProgramFiles(x86)}$suffix").VersionInfo
} elseif (Test-Path "${env:ProgramFiles}$suffix") {
(Get-Item "${env:ProgramFiles}$suffix").VersionInfo
} else {
Write-Host "ERROR: Failed to install Microsoft Edge Dev."
Write-Host "ERROR: This could be due to insufficient privileges, in which case re-running as Administrator may help."
exit 1
}

View File

@@ -0,0 +1,48 @@
#!/usr/bin/env bash
set -e
set -x
if [[ $(arch) == "aarch64" ]]; then
echo "ERROR: not supported on Linux Arm64"
exit 1
fi
if [ -z "$PLAYWRIGHT_HOST_PLATFORM_OVERRIDE" ]; then
if [[ ! -f "/etc/os-release" ]]; then
echo "ERROR: cannot install on unknown linux distribution (/etc/os-release is missing)"
exit 1
fi
ID=$(bash -c 'source /etc/os-release && echo $ID')
if [[ "${ID}" != "ubuntu" && "${ID}" != "debian" ]]; then
echo "ERROR: cannot install on $ID distribution - only Ubuntu and Debian are supported"
exit 1
fi
fi
# 1. make sure to remove old stable if any.
if dpkg --get-selections | grep -q "^microsoft-edge-stable[[:space:]]*install$" >/dev/null; then
apt-get remove -y microsoft-edge-stable
fi
# 2. Install curl to download Microsoft gpg key
if ! command -v curl >/dev/null; then
apt-get update
apt-get install -y curl
fi
# GnuPG is not preinstalled in slim images
if ! command -v gpg >/dev/null; then
apt-get update
apt-get install -y gpg
fi
# 3. Add the GPG key, the apt repo, update the apt cache, and install the package
curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > /tmp/microsoft.gpg
install -o root -g root -m 644 /tmp/microsoft.gpg /etc/apt/trusted.gpg.d/
sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/repos/edge stable main" > /etc/apt/sources.list.d/microsoft-edge-stable.list'
rm /tmp/microsoft.gpg
apt-get update && apt-get install -y microsoft-edge-stable
microsoft-edge-stable --version

View File

@@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -e
set -x
cd /tmp
curl --retry 3 -o ./msedge_stable.pkg -k "$1"
# Note: there's no way to uninstall previously installed MSEdge.
# However, running PKG again seems to update installation.
sudo installer -pkg /tmp/msedge_stable.pkg -target /
rm -rf /tmp/msedge_stable.pkg
/Applications/Microsoft\ Edge.app/Contents/MacOS/Microsoft\ Edge --version

View File

@@ -0,0 +1,24 @@
$ErrorActionPreference = 'Stop'
$url = $args[0]
Write-Host "Downloading Microsoft Edge"
$wc = New-Object net.webclient
$msiInstaller = "$env:temp\microsoft-edge-stable.msi"
$wc.Downloadfile($url, $msiInstaller)
Write-Host "Installing Microsoft Edge"
$arguments = "/i `"$msiInstaller`" /quiet"
Start-Process msiexec.exe -ArgumentList $arguments -Wait
Remove-Item $msiInstaller
$suffix = "\\Microsoft\\Edge\\Application\\msedge.exe"
if (Test-Path "${env:ProgramFiles(x86)}$suffix") {
(Get-Item "${env:ProgramFiles(x86)}$suffix").VersionInfo
} elseif (Test-Path "${env:ProgramFiles}$suffix") {
(Get-Item "${env:ProgramFiles}$suffix").VersionInfo
} else {
Write-Host "ERROR: Failed to install Microsoft Edge."
Write-Host "ERROR: This could be due to insufficient privileges, in which case re-running as Administrator may help."
exit 1
}

80
node_modules/playwright-core/browsers.json generated vendored Normal file
View File

@@ -0,0 +1,80 @@
{
"comment": "Do not edit this file, use utils/roll_browser.js",
"browsers": [
{
"name": "chromium",
"revision": "1187",
"installByDefault": true,
"browserVersion": "140.0.7339.16"
},
{
"name": "chromium-headless-shell",
"revision": "1187",
"installByDefault": true,
"browserVersion": "140.0.7339.16"
},
{
"name": "chromium-tip-of-tree",
"revision": "1357",
"installByDefault": false,
"browserVersion": "141.0.7342.0"
},
{
"name": "chromium-tip-of-tree-headless-shell",
"revision": "1357",
"installByDefault": false,
"browserVersion": "141.0.7342.0"
},
{
"name": "firefox",
"revision": "1490",
"installByDefault": true,
"browserVersion": "141.0"
},
{
"name": "firefox-beta",
"revision": "1485",
"installByDefault": false,
"browserVersion": "142.0b4"
},
{
"name": "webkit",
"revision": "2203",
"installByDefault": true,
"revisionOverrides": {
"debian11-x64": "2105",
"debian11-arm64": "2105",
"mac10.14": "1446",
"mac10.15": "1616",
"mac11": "1816",
"mac11-arm64": "1816",
"mac12": "2009",
"mac12-arm64": "2009",
"mac13": "2140",
"mac13-arm64": "2140",
"ubuntu20.04-x64": "2092",
"ubuntu20.04-arm64": "2092"
},
"browserVersion": "26.0"
},
{
"name": "ffmpeg",
"revision": "1011",
"installByDefault": true,
"revisionOverrides": {
"mac12": "1010",
"mac12-arm64": "1010"
}
},
{
"name": "winldd",
"revision": "1007",
"installByDefault": false
},
{
"name": "android",
"revision": "1001",
"installByDefault": false
}
]
}

18
node_modules/playwright-core/cli.js generated vendored Executable file
View File

@@ -0,0 +1,18 @@
#!/usr/bin/env node
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const { program } = require('./lib/cli/programWithTestStub');
program.parse(process.argv);

17
node_modules/playwright-core/index.d.ts generated vendored Normal file
View File

@@ -0,0 +1,17 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './types/types';

32
node_modules/playwright-core/index.js generated vendored Normal file
View File

@@ -0,0 +1,32 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const minimumMajorNodeVersion = 18;
const currentNodeVersion = process.versions.node;
const semver = currentNodeVersion.split('.');
const [major] = [+semver[0]];
if (major < minimumMajorNodeVersion) {
console.error(
'You are running Node.js ' +
currentNodeVersion +
'.\n' +
`Playwright requires Node.js ${minimumMajorNodeVersion} or higher. \n` +
'Please update your version of Node.js.'
);
process.exit(1);
}
module.exports = require('./lib/inprocess');

28
node_modules/playwright-core/index.mjs generated vendored Normal file
View File

@@ -0,0 +1,28 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import playwright from './index.js';
export const chromium = playwright.chromium;
export const firefox = playwright.firefox;
export const webkit = playwright.webkit;
export const selectors = playwright.selectors;
export const devices = playwright.devices;
export const errors = playwright.errors;
export const request = playwright.request;
export const _electron = playwright._electron;
export const _android = playwright._android;
export default playwright;

65
node_modules/playwright-core/lib/androidServerImpl.js generated vendored Normal file
View File

@@ -0,0 +1,65 @@
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var androidServerImpl_exports = {};
__export(androidServerImpl_exports, {
AndroidServerLauncherImpl: () => AndroidServerLauncherImpl
});
module.exports = __toCommonJS(androidServerImpl_exports);
var import_playwrightServer = require("./remote/playwrightServer");
var import_playwright = require("./server/playwright");
var import_crypto = require("./server/utils/crypto");
var import_utilsBundle = require("./utilsBundle");
var import_progress = require("./server/progress");
class AndroidServerLauncherImpl {
async launchServer(options = {}) {
const playwright = (0, import_playwright.createPlaywright)({ sdkLanguage: "javascript", isServer: true });
const controller = new import_progress.ProgressController();
let devices = await controller.run((progress) => playwright.android.devices(progress, {
host: options.adbHost,
port: options.adbPort,
omitDriverInstall: options.omitDriverInstall
}));
if (devices.length === 0)
throw new Error("No devices found");
if (options.deviceSerialNumber) {
devices = devices.filter((d) => d.serial === options.deviceSerialNumber);
if (devices.length === 0)
throw new Error(`No device with serial number '${options.deviceSerialNumber}' was found`);
}
if (devices.length > 1)
throw new Error(`More than one device found. Please specify deviceSerialNumber`);
const device = devices[0];
const path = options.wsPath ? options.wsPath.startsWith("/") ? options.wsPath : `/${options.wsPath}` : `/${(0, import_crypto.createGuid)()}`;
const server = new import_playwrightServer.PlaywrightServer({ mode: "launchServer", path, maxConnections: 1, preLaunchedAndroidDevice: device });
const wsEndpoint = await server.listen(options.port, options.host);
const browserServer = new import_utilsBundle.ws.EventEmitter();
browserServer.wsEndpoint = () => wsEndpoint;
browserServer.close = () => device.close();
browserServer.kill = () => device.close();
device.on("close", () => {
server.close();
browserServer.emit("close");
});
return browserServer;
}
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
AndroidServerLauncherImpl
});

123
node_modules/playwright-core/lib/browserServerImpl.js generated vendored Normal file
View File

@@ -0,0 +1,123 @@
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var browserServerImpl_exports = {};
__export(browserServerImpl_exports, {
BrowserServerLauncherImpl: () => BrowserServerLauncherImpl
});
module.exports = __toCommonJS(browserServerImpl_exports);
var import_playwrightServer = require("./remote/playwrightServer");
var import_helper = require("./server/helper");
var import_playwright = require("./server/playwright");
var import_crypto = require("./server/utils/crypto");
var import_debug = require("./server/utils/debug");
var import_stackTrace = require("./utils/isomorphic/stackTrace");
var import_time = require("./utils/isomorphic/time");
var import_utilsBundle = require("./utilsBundle");
var validatorPrimitives = __toESM(require("./protocol/validatorPrimitives"));
var import_progress = require("./server/progress");
class BrowserServerLauncherImpl {
constructor(browserName) {
this._browserName = browserName;
}
async launchServer(options = {}) {
const playwright = (0, import_playwright.createPlaywright)({ sdkLanguage: "javascript", isServer: true });
const metadata = { id: "", startTime: 0, endTime: 0, type: "Internal", method: "", params: {}, log: [], internal: true };
const validatorContext = {
tChannelImpl: (names, arg, path) => {
throw new validatorPrimitives.ValidationError(`${path}: channels are not expected in launchServer`);
},
binary: "buffer",
isUnderTest: import_debug.isUnderTest
};
let launchOptions = {
...options,
ignoreDefaultArgs: Array.isArray(options.ignoreDefaultArgs) ? options.ignoreDefaultArgs : void 0,
ignoreAllDefaultArgs: !!options.ignoreDefaultArgs && !Array.isArray(options.ignoreDefaultArgs),
env: options.env ? envObjectToArray(options.env) : void 0,
timeout: options.timeout ?? import_time.DEFAULT_PLAYWRIGHT_LAUNCH_TIMEOUT
};
let browser;
try {
const controller = new import_progress.ProgressController(metadata);
browser = await controller.run(async (progress) => {
if (options._userDataDir !== void 0) {
const validator = validatorPrimitives.scheme["BrowserTypeLaunchPersistentContextParams"];
launchOptions = validator({ ...launchOptions, userDataDir: options._userDataDir }, "", validatorContext);
const context = await playwright[this._browserName].launchPersistentContext(progress, options._userDataDir, launchOptions);
return context._browser;
} else {
const validator = validatorPrimitives.scheme["BrowserTypeLaunchParams"];
launchOptions = validator(launchOptions, "", validatorContext);
return await playwright[this._browserName].launch(progress, launchOptions, toProtocolLogger(options.logger));
}
});
} catch (e) {
const log = import_helper.helper.formatBrowserLogs(metadata.log);
(0, import_stackTrace.rewriteErrorMessage)(e, `${e.message} Failed to launch browser.${log}`);
throw e;
}
return this.launchServerOnExistingBrowser(browser, options);
}
async launchServerOnExistingBrowser(browser, options) {
const path = options.wsPath ? options.wsPath.startsWith("/") ? options.wsPath : `/${options.wsPath}` : `/${(0, import_crypto.createGuid)()}`;
const server = new import_playwrightServer.PlaywrightServer({ mode: options._sharedBrowser ? "launchServerShared" : "launchServer", path, maxConnections: Infinity, preLaunchedBrowser: browser, debugController: options._debugController });
const wsEndpoint = await server.listen(options.port, options.host);
const browserServer = new import_utilsBundle.ws.EventEmitter();
browserServer.process = () => browser.options.browserProcess.process;
browserServer.wsEndpoint = () => wsEndpoint;
browserServer.close = () => browser.options.browserProcess.close();
browserServer[Symbol.asyncDispose] = browserServer.close;
browserServer.kill = () => browser.options.browserProcess.kill();
browserServer._disconnectForTest = () => server.close();
browserServer._userDataDirForTest = browser._userDataDirForTest;
browser.options.browserProcess.onclose = (exitCode, signal) => {
server.close();
browserServer.emit("close", exitCode, signal);
};
return browserServer;
}
}
function toProtocolLogger(logger) {
return logger ? (direction, message) => {
if (logger.isEnabled("protocol", "verbose"))
logger.log("protocol", "verbose", (direction === "send" ? "SEND \u25BA " : "\u25C0 RECV ") + JSON.stringify(message), [], {});
} : void 0;
}
function envObjectToArray(env) {
const result = [];
for (const name in env) {
if (!Object.is(env[name], void 0))
result.push({ name, value: String(env[name]) });
}
return result;
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
BrowserServerLauncherImpl
});

97
node_modules/playwright-core/lib/cli/driver.js generated vendored Normal file
View File

@@ -0,0 +1,97 @@
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var driver_exports = {};
__export(driver_exports, {
launchBrowserServer: () => launchBrowserServer,
printApiJson: () => printApiJson,
runDriver: () => runDriver,
runServer: () => runServer
});
module.exports = __toCommonJS(driver_exports);
var import_fs = __toESM(require("fs"));
var playwright = __toESM(require("../.."));
var import_pipeTransport = require("../server/utils/pipeTransport");
var import_playwrightServer = require("../remote/playwrightServer");
var import_server = require("../server");
var import_processLauncher = require("../server/utils/processLauncher");
function printApiJson() {
console.log(JSON.stringify(require("../../api.json")));
}
function runDriver() {
const dispatcherConnection = new import_server.DispatcherConnection();
new import_server.RootDispatcher(dispatcherConnection, async (rootScope, { sdkLanguage }) => {
const playwright2 = (0, import_server.createPlaywright)({ sdkLanguage });
return new import_server.PlaywrightDispatcher(rootScope, playwright2);
});
const transport = new import_pipeTransport.PipeTransport(process.stdout, process.stdin);
transport.onmessage = (message) => dispatcherConnection.dispatch(JSON.parse(message));
const isJavaScriptLanguageBinding = !process.env.PW_LANG_NAME || process.env.PW_LANG_NAME === "javascript";
const replacer = !isJavaScriptLanguageBinding && String.prototype.toWellFormed ? (key, value) => {
if (typeof value === "string")
return value.toWellFormed();
return value;
} : void 0;
dispatcherConnection.onmessage = (message) => transport.send(JSON.stringify(message, replacer));
transport.onclose = () => {
dispatcherConnection.onmessage = () => {
};
(0, import_processLauncher.gracefullyProcessExitDoNotHang)(0);
};
process.on("SIGINT", () => {
});
}
async function runServer(options) {
const {
port,
host,
path = "/",
maxConnections = Infinity,
extension
} = options;
const server = new import_playwrightServer.PlaywrightServer({ mode: extension ? "extension" : "default", path, maxConnections });
const wsEndpoint = await server.listen(port, host);
process.on("exit", () => server.close().catch(console.error));
console.log("Listening on " + wsEndpoint);
process.stdin.on("close", () => (0, import_processLauncher.gracefullyProcessExitDoNotHang)(0));
}
async function launchBrowserServer(browserName, configFile) {
let options = {};
if (configFile)
options = JSON.parse(import_fs.default.readFileSync(configFile).toString());
const browserType = playwright[browserName];
const server = await browserType.launchServer(options);
console.log(server.wsEndpoint());
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
launchBrowserServer,
printApiJson,
runDriver,
runServer
});

633
node_modules/playwright-core/lib/cli/program.js generated vendored Normal file
View File

@@ -0,0 +1,633 @@
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var program_exports = {};
__export(program_exports, {
program: () => import_utilsBundle2.program
});
module.exports = __toCommonJS(program_exports);
var import_fs = __toESM(require("fs"));
var import_os = __toESM(require("os"));
var import_path = __toESM(require("path"));
var playwright = __toESM(require("../.."));
var import_driver = require("./driver");
var import_server = require("../server");
var import_utils = require("../utils");
var import_traceViewer = require("../server/trace/viewer/traceViewer");
var import_utils2 = require("../utils");
var import_ascii = require("../server/utils/ascii");
var import_utilsBundle = require("../utilsBundle");
var import_utilsBundle2 = require("../utilsBundle");
const packageJSON = require("../../package.json");
import_utilsBundle.program.version("Version " + (process.env.PW_CLI_DISPLAY_VERSION || packageJSON.version)).name(buildBasePlaywrightCLICommand(process.env.PW_LANG_NAME));
import_utilsBundle.program.command("mark-docker-image [dockerImageNameTemplate]", { hidden: true }).description("mark docker image").allowUnknownOption(true).action(function(dockerImageNameTemplate) {
(0, import_utils2.assert)(dockerImageNameTemplate, "dockerImageNameTemplate is required");
(0, import_server.writeDockerVersion)(dockerImageNameTemplate).catch(logErrorAndExit);
});
commandWithOpenOptions("open [url]", "open page in browser specified via -b, --browser", []).action(function(url, options) {
open(options, url).catch(logErrorAndExit);
}).addHelpText("afterAll", `
Examples:
$ open
$ open -b webkit https://example.com`);
commandWithOpenOptions(
"codegen [url]",
"open page and generate code for user actions",
[
["-o, --output <file name>", "saves the generated script to a file"],
["--target <language>", `language to generate, one of javascript, playwright-test, python, python-async, python-pytest, csharp, csharp-mstest, csharp-nunit, java, java-junit`, codegenId()],
["--test-id-attribute <attributeName>", "use the specified attribute to generate data test ID selectors"]
]
).action(async function(url, options) {
await codegen(options, url);
}).addHelpText("afterAll", `
Examples:
$ codegen
$ codegen --target=python
$ codegen -b webkit https://example.com`);
function suggestedBrowsersToInstall() {
return import_server.registry.executables().filter((e) => e.installType !== "none" && e.type !== "tool").map((e) => e.name).join(", ");
}
function defaultBrowsersToInstall(options) {
let executables = import_server.registry.defaultExecutables();
if (options.noShell)
executables = executables.filter((e) => e.name !== "chromium-headless-shell");
if (options.onlyShell)
executables = executables.filter((e) => e.name !== "chromium");
return executables;
}
function checkBrowsersToInstall(args, options) {
if (options.noShell && options.onlyShell)
throw new Error(`Only one of --no-shell and --only-shell can be specified`);
const faultyArguments = [];
const executables = [];
const handleArgument = (arg) => {
const executable = import_server.registry.findExecutable(arg);
if (!executable || executable.installType === "none")
faultyArguments.push(arg);
else
executables.push(executable);
if (executable?.browserName === "chromium")
executables.push(import_server.registry.findExecutable("ffmpeg"));
};
for (const arg of args) {
if (arg === "chromium") {
if (!options.onlyShell)
handleArgument("chromium");
if (!options.noShell)
handleArgument("chromium-headless-shell");
} else {
handleArgument(arg);
}
}
if (process.platform === "win32")
executables.push(import_server.registry.findExecutable("winldd"));
if (faultyArguments.length)
throw new Error(`Invalid installation targets: ${faultyArguments.map((name) => `'${name}'`).join(", ")}. Expecting one of: ${suggestedBrowsersToInstall()}`);
return executables;
}
function printInstalledBrowsers(browsers2) {
const browserPaths = /* @__PURE__ */ new Set();
for (const browser of browsers2)
browserPaths.add(browser.browserPath);
console.log(` Browsers:`);
for (const browserPath of [...browserPaths].sort())
console.log(` ${browserPath}`);
console.log(` References:`);
const references = /* @__PURE__ */ new Set();
for (const browser of browsers2)
references.add(browser.referenceDir);
for (const reference of [...references].sort())
console.log(` ${reference}`);
}
function printGroupedByPlaywrightVersion(browsers2) {
const dirToVersion = /* @__PURE__ */ new Map();
for (const browser of browsers2) {
if (dirToVersion.has(browser.referenceDir))
continue;
const packageJSON2 = require(import_path.default.join(browser.referenceDir, "package.json"));
const version = packageJSON2.version;
dirToVersion.set(browser.referenceDir, version);
}
const groupedByPlaywrightMinorVersion = /* @__PURE__ */ new Map();
for (const browser of browsers2) {
const version = dirToVersion.get(browser.referenceDir);
let entries = groupedByPlaywrightMinorVersion.get(version);
if (!entries) {
entries = [];
groupedByPlaywrightMinorVersion.set(version, entries);
}
entries.push(browser);
}
const sortedVersions = [...groupedByPlaywrightMinorVersion.keys()].sort((a, b) => {
const aComponents = a.split(".");
const bComponents = b.split(".");
const aMajor = parseInt(aComponents[0], 10);
const bMajor = parseInt(bComponents[0], 10);
if (aMajor !== bMajor)
return aMajor - bMajor;
const aMinor = parseInt(aComponents[1], 10);
const bMinor = parseInt(bComponents[1], 10);
if (aMinor !== bMinor)
return aMinor - bMinor;
return aComponents.slice(2).join(".").localeCompare(bComponents.slice(2).join("."));
});
for (const version of sortedVersions) {
console.log(`
Playwright version: ${version}`);
printInstalledBrowsers(groupedByPlaywrightMinorVersion.get(version));
}
}
import_utilsBundle.program.command("install [browser...]").description("ensure browsers necessary for this version of Playwright are installed").option("--with-deps", "install system dependencies for browsers").option("--dry-run", "do not execute installation, only print information").option("--list", "prints list of browsers from all playwright installations").option("--force", "force reinstall of stable browser channels").option("--only-shell", "only install headless shell when installing chromium").option("--no-shell", "do not install chromium headless shell").action(async function(args, options) {
if (options.shell === false)
options.noShell = true;
if ((0, import_utils.isLikelyNpxGlobal)()) {
console.error((0, import_ascii.wrapInASCIIBox)([
`WARNING: It looks like you are running 'npx playwright install' without first`,
`installing your project's dependencies.`,
``,
`To avoid unexpected behavior, please install your dependencies first, and`,
`then run Playwright's install command:`,
``,
` npm install`,
` npx playwright install`,
``,
`If your project does not yet depend on Playwright, first install the`,
`applicable npm package (most commonly @playwright/test), and`,
`then run Playwright's install command to download the browsers:`,
``,
` npm install @playwright/test`,
` npx playwright install`,
``
].join("\n"), 1));
}
try {
const hasNoArguments = !args.length;
const executables = hasNoArguments ? defaultBrowsersToInstall(options) : checkBrowsersToInstall(args, options);
if (options.withDeps)
await import_server.registry.installDeps(executables, !!options.dryRun);
if (options.dryRun && options.list)
throw new Error(`Only one of --dry-run and --list can be specified`);
if (options.dryRun) {
for (const executable of executables) {
const version = executable.browserVersion ? `version ` + executable.browserVersion : "";
console.log(`browser: ${executable.name}${version ? " " + version : ""}`);
console.log(` Install location: ${executable.directory ?? "<system>"}`);
if (executable.downloadURLs?.length) {
const [url, ...fallbacks] = executable.downloadURLs;
console.log(` Download url: ${url}`);
for (let i = 0; i < fallbacks.length; ++i)
console.log(` Download fallback ${i + 1}: ${fallbacks[i]}`);
}
console.log(``);
}
} else if (options.list) {
const browsers2 = await import_server.registry.listInstalledBrowsers();
printGroupedByPlaywrightVersion(browsers2);
} else {
const forceReinstall = hasNoArguments ? false : !!options.force;
await import_server.registry.install(executables, forceReinstall);
await import_server.registry.validateHostRequirementsForExecutablesIfNeeded(executables, process.env.PW_LANG_NAME || "javascript").catch((e) => {
e.name = "Playwright Host validation warning";
console.error(e);
});
}
} catch (e) {
console.log(`Failed to install browsers
${e}`);
(0, import_utils.gracefullyProcessExitDoNotHang)(1);
}
}).addHelpText("afterAll", `
Examples:
- $ install
Install default browsers.
- $ install chrome firefox
Install custom browsers, supports ${suggestedBrowsersToInstall()}.`);
import_utilsBundle.program.command("uninstall").description("Removes browsers used by this installation of Playwright from the system (chromium, firefox, webkit, ffmpeg). This does not include branded channels.").option("--all", "Removes all browsers used by any Playwright installation from the system.").action(async (options) => {
delete process.env.PLAYWRIGHT_SKIP_BROWSER_GC;
await import_server.registry.uninstall(!!options.all).then(({ numberOfBrowsersLeft }) => {
if (!options.all && numberOfBrowsersLeft > 0) {
console.log("Successfully uninstalled Playwright browsers for the current Playwright installation.");
console.log(`There are still ${numberOfBrowsersLeft} browsers left, used by other Playwright installations.
To uninstall Playwright browsers for all installations, re-run with --all flag.`);
}
}).catch(logErrorAndExit);
});
import_utilsBundle.program.command("install-deps [browser...]").description("install dependencies necessary to run browsers (will ask for sudo permissions)").option("--dry-run", "Do not execute installation commands, only print them").action(async function(args, options) {
try {
if (!args.length)
await import_server.registry.installDeps(defaultBrowsersToInstall({}), !!options.dryRun);
else
await import_server.registry.installDeps(checkBrowsersToInstall(args, {}), !!options.dryRun);
} catch (e) {
console.log(`Failed to install browser dependencies
${e}`);
(0, import_utils.gracefullyProcessExitDoNotHang)(1);
}
}).addHelpText("afterAll", `
Examples:
- $ install-deps
Install dependencies for default browsers.
- $ install-deps chrome firefox
Install dependencies for specific browsers, supports ${suggestedBrowsersToInstall()}.`);
const browsers = [
{ alias: "cr", name: "Chromium", type: "chromium" },
{ alias: "ff", name: "Firefox", type: "firefox" },
{ alias: "wk", name: "WebKit", type: "webkit" }
];
for (const { alias, name, type } of browsers) {
commandWithOpenOptions(`${alias} [url]`, `open page in ${name}`, []).action(function(url, options) {
open({ ...options, browser: type }, url).catch(logErrorAndExit);
}).addHelpText("afterAll", `
Examples:
$ ${alias} https://example.com`);
}
commandWithOpenOptions(
"screenshot <url> <filename>",
"capture a page screenshot",
[
["--wait-for-selector <selector>", "wait for selector before taking a screenshot"],
["--wait-for-timeout <timeout>", "wait for timeout in milliseconds before taking a screenshot"],
["--full-page", "whether to take a full page screenshot (entire scrollable area)"]
]
).action(function(url, filename, command) {
screenshot(command, command, url, filename).catch(logErrorAndExit);
}).addHelpText("afterAll", `
Examples:
$ screenshot -b webkit https://example.com example.png`);
commandWithOpenOptions(
"pdf <url> <filename>",
"save page as pdf",
[
["--paper-format <format>", "paper format: Letter, Legal, Tabloid, Ledger, A0, A1, A2, A3, A4, A5, A6"],
["--wait-for-selector <selector>", "wait for given selector before saving as pdf"],
["--wait-for-timeout <timeout>", "wait for given timeout in milliseconds before saving as pdf"]
]
).action(function(url, filename, options) {
pdf(options, options, url, filename).catch(logErrorAndExit);
}).addHelpText("afterAll", `
Examples:
$ pdf https://example.com example.pdf`);
import_utilsBundle.program.command("run-driver", { hidden: true }).action(function(options) {
(0, import_driver.runDriver)();
});
import_utilsBundle.program.command("run-server").option("--port <port>", "Server port").option("--host <host>", "Server host").option("--path <path>", "Endpoint Path", "/").option("--max-clients <maxClients>", "Maximum clients").option("--mode <mode>", 'Server mode, either "default" or "extension"').action(function(options) {
(0, import_driver.runServer)({
port: options.port ? +options.port : void 0,
host: options.host,
path: options.path,
maxConnections: options.maxClients ? +options.maxClients : Infinity,
extension: options.mode === "extension" || !!process.env.PW_EXTENSION_MODE
}).catch(logErrorAndExit);
});
import_utilsBundle.program.command("print-api-json", { hidden: true }).action(function(options) {
(0, import_driver.printApiJson)();
});
import_utilsBundle.program.command("launch-server", { hidden: true }).requiredOption("--browser <browserName>", 'Browser name, one of "chromium", "firefox" or "webkit"').option("--config <path-to-config-file>", "JSON file with launchServer options").action(function(options) {
(0, import_driver.launchBrowserServer)(options.browser, options.config);
});
import_utilsBundle.program.command("show-trace [trace...]").option("-b, --browser <browserType>", "browser to use, one of cr, chromium, ff, firefox, wk, webkit", "chromium").option("-h, --host <host>", "Host to serve trace on; specifying this option opens trace in a browser tab").option("-p, --port <port>", "Port to serve trace on, 0 for any free port; specifying this option opens trace in a browser tab").option("--stdin", "Accept trace URLs over stdin to update the viewer").description("show trace viewer").action(function(traces, options) {
if (options.browser === "cr")
options.browser = "chromium";
if (options.browser === "ff")
options.browser = "firefox";
if (options.browser === "wk")
options.browser = "webkit";
const openOptions = {
host: options.host,
port: +options.port,
isServer: !!options.stdin
};
if (options.port !== void 0 || options.host !== void 0)
(0, import_traceViewer.runTraceInBrowser)(traces, openOptions).catch(logErrorAndExit);
else
(0, import_traceViewer.runTraceViewerApp)(traces, options.browser, openOptions, true).catch(logErrorAndExit);
}).addHelpText("afterAll", `
Examples:
$ show-trace https://example.com/trace.zip`);
async function launchContext(options, extraOptions) {
validateOptions(options);
const browserType = lookupBrowserType(options);
const launchOptions = extraOptions;
if (options.channel)
launchOptions.channel = options.channel;
launchOptions.handleSIGINT = false;
const contextOptions = (
// Copy the device descriptor since we have to compare and modify the options.
options.device ? { ...playwright.devices[options.device] } : {}
);
if (!extraOptions.headless)
contextOptions.deviceScaleFactor = import_os.default.platform() === "darwin" ? 2 : 1;
if (browserType.name() === "webkit" && process.platform === "linux") {
delete contextOptions.hasTouch;
delete contextOptions.isMobile;
}
if (contextOptions.isMobile && browserType.name() === "firefox")
contextOptions.isMobile = void 0;
if (options.blockServiceWorkers)
contextOptions.serviceWorkers = "block";
if (options.proxyServer) {
launchOptions.proxy = {
server: options.proxyServer
};
if (options.proxyBypass)
launchOptions.proxy.bypass = options.proxyBypass;
}
if (options.viewportSize) {
try {
const [width, height] = options.viewportSize.split(",").map((n) => +n);
if (isNaN(width) || isNaN(height))
throw new Error("bad values");
contextOptions.viewport = { width, height };
} catch (e) {
throw new Error('Invalid viewport size format: use "width,height", for example --viewport-size="800,600"');
}
}
if (options.geolocation) {
try {
const [latitude, longitude] = options.geolocation.split(",").map((n) => parseFloat(n.trim()));
contextOptions.geolocation = {
latitude,
longitude
};
} catch (e) {
throw new Error('Invalid geolocation format, should be "lat,long". For example --geolocation="37.819722,-122.478611"');
}
contextOptions.permissions = ["geolocation"];
}
if (options.userAgent)
contextOptions.userAgent = options.userAgent;
if (options.lang)
contextOptions.locale = options.lang;
if (options.colorScheme)
contextOptions.colorScheme = options.colorScheme;
if (options.timezone)
contextOptions.timezoneId = options.timezone;
if (options.loadStorage)
contextOptions.storageState = options.loadStorage;
if (options.ignoreHttpsErrors)
contextOptions.ignoreHTTPSErrors = true;
if (options.saveHar) {
contextOptions.recordHar = { path: import_path.default.resolve(process.cwd(), options.saveHar), mode: "minimal" };
if (options.saveHarGlob)
contextOptions.recordHar.urlFilter = options.saveHarGlob;
contextOptions.serviceWorkers = "block";
}
let browser;
let context;
if (options.userDataDir) {
context = await browserType.launchPersistentContext(options.userDataDir, { ...launchOptions, ...contextOptions });
browser = context.browser();
} else {
browser = await browserType.launch(launchOptions);
context = await browser.newContext(contextOptions);
}
let closingBrowser = false;
async function closeBrowser() {
if (closingBrowser)
return;
closingBrowser = true;
if (options.saveStorage)
await context.storageState({ path: options.saveStorage }).catch((e) => null);
if (options.saveHar)
await context.close();
await browser.close();
}
context.on("page", (page) => {
page.on("dialog", () => {
});
page.on("close", () => {
const hasPage = browser.contexts().some((context2) => context2.pages().length > 0);
if (hasPage)
return;
closeBrowser().catch(() => {
});
});
});
process.on("SIGINT", async () => {
await closeBrowser();
(0, import_utils.gracefullyProcessExitDoNotHang)(130);
});
const timeout = options.timeout ? parseInt(options.timeout, 10) : 0;
context.setDefaultTimeout(timeout);
context.setDefaultNavigationTimeout(timeout);
delete launchOptions.headless;
delete launchOptions.executablePath;
delete launchOptions.handleSIGINT;
delete contextOptions.deviceScaleFactor;
return { browser, browserName: browserType.name(), context, contextOptions, launchOptions, closeBrowser };
}
async function openPage(context, url) {
let page = context.pages()[0];
if (!page)
page = await context.newPage();
if (url) {
if (import_fs.default.existsSync(url))
url = "file://" + import_path.default.resolve(url);
else if (!url.startsWith("http") && !url.startsWith("file://") && !url.startsWith("about:") && !url.startsWith("data:"))
url = "http://" + url;
await page.goto(url);
}
return page;
}
async function open(options, url) {
const { context } = await launchContext(options, { headless: !!process.env.PWTEST_CLI_HEADLESS, executablePath: process.env.PWTEST_CLI_EXECUTABLE_PATH });
await openPage(context, url);
}
async function codegen(options, url) {
const { target: language, output: outputFile, testIdAttribute: testIdAttributeName } = options;
const tracesDir = import_path.default.join(import_os.default.tmpdir(), `playwright-recorder-trace-${Date.now()}`);
const { context, browser, launchOptions, contextOptions, closeBrowser } = await launchContext(options, {
headless: !!process.env.PWTEST_CLI_HEADLESS,
executablePath: process.env.PWTEST_CLI_EXECUTABLE_PATH,
tracesDir
});
const donePromise = new import_utils.ManualPromise();
maybeSetupTestHooks(browser, closeBrowser, donePromise);
import_utilsBundle.dotenv.config({ path: "playwright.env" });
await context._enableRecorder({
language,
launchOptions,
contextOptions,
device: options.device,
saveStorage: options.saveStorage,
mode: "recording",
testIdAttributeName,
outputFile: outputFile ? import_path.default.resolve(outputFile) : void 0,
handleSIGINT: false
});
await openPage(context, url);
donePromise.resolve();
}
async function maybeSetupTestHooks(browser, closeBrowser, donePromise) {
if (!process.env.PWTEST_CLI_IS_UNDER_TEST)
return;
const logs = [];
require("playwright-core/lib/utilsBundle").debug.log = (...args) => {
const line = require("util").format(...args) + "\n";
logs.push(line);
process.stderr.write(line);
};
browser.on("disconnected", () => {
const hasCrashLine = logs.some((line) => line.includes("process did exit:") && !line.includes("process did exit: exitCode=0, signal=null"));
if (hasCrashLine) {
process.stderr.write("Detected browser crash.\n");
(0, import_utils.gracefullyProcessExitDoNotHang)(1);
}
});
const close = async () => {
await donePromise;
await closeBrowser();
};
if (process.env.PWTEST_CLI_EXIT_AFTER_TIMEOUT) {
setTimeout(close, +process.env.PWTEST_CLI_EXIT_AFTER_TIMEOUT);
return;
}
let stdin = "";
process.stdin.on("data", (data) => {
stdin += data.toString();
if (stdin.startsWith("exit")) {
process.stdin.destroy();
close();
}
});
}
async function waitForPage(page, captureOptions) {
if (captureOptions.waitForSelector) {
console.log(`Waiting for selector ${captureOptions.waitForSelector}...`);
await page.waitForSelector(captureOptions.waitForSelector);
}
if (captureOptions.waitForTimeout) {
console.log(`Waiting for timeout ${captureOptions.waitForTimeout}...`);
await page.waitForTimeout(parseInt(captureOptions.waitForTimeout, 10));
}
}
async function screenshot(options, captureOptions, url, path2) {
const { context } = await launchContext(options, { headless: true });
console.log("Navigating to " + url);
const page = await openPage(context, url);
await waitForPage(page, captureOptions);
console.log("Capturing screenshot into " + path2);
await page.screenshot({ path: path2, fullPage: !!captureOptions.fullPage });
await page.close();
}
async function pdf(options, captureOptions, url, path2) {
if (options.browser !== "chromium")
throw new Error("PDF creation is only working with Chromium");
const { context } = await launchContext({ ...options, browser: "chromium" }, { headless: true });
console.log("Navigating to " + url);
const page = await openPage(context, url);
await waitForPage(page, captureOptions);
console.log("Saving as pdf into " + path2);
await page.pdf({ path: path2, format: captureOptions.paperFormat });
await page.close();
}
function lookupBrowserType(options) {
let name = options.browser;
if (options.device) {
const device = playwright.devices[options.device];
name = device.defaultBrowserType;
}
let browserType;
switch (name) {
case "chromium":
browserType = playwright.chromium;
break;
case "webkit":
browserType = playwright.webkit;
break;
case "firefox":
browserType = playwright.firefox;
break;
case "cr":
browserType = playwright.chromium;
break;
case "wk":
browserType = playwright.webkit;
break;
case "ff":
browserType = playwright.firefox;
break;
}
if (browserType)
return browserType;
import_utilsBundle.program.help();
}
function validateOptions(options) {
if (options.device && !(options.device in playwright.devices)) {
const lines = [`Device descriptor not found: '${options.device}', available devices are:`];
for (const name in playwright.devices)
lines.push(` "${name}"`);
throw new Error(lines.join("\n"));
}
if (options.colorScheme && !["light", "dark"].includes(options.colorScheme))
throw new Error('Invalid color scheme, should be one of "light", "dark"');
}
function logErrorAndExit(e) {
if (process.env.PWDEBUGIMPL)
console.error(e);
else
console.error(e.name + ": " + e.message);
(0, import_utils.gracefullyProcessExitDoNotHang)(1);
}
function codegenId() {
return process.env.PW_LANG_NAME || "playwright-test";
}
function commandWithOpenOptions(command, description, options) {
let result = import_utilsBundle.program.command(command).description(description);
for (const option of options)
result = result.option(option[0], ...option.slice(1));
return result.option("-b, --browser <browserType>", "browser to use, one of cr, chromium, ff, firefox, wk, webkit", "chromium").option("--block-service-workers", "block service workers").option("--channel <channel>", 'Chromium distribution channel, "chrome", "chrome-beta", "msedge-dev", etc').option("--color-scheme <scheme>", 'emulate preferred color scheme, "light" or "dark"').option("--device <deviceName>", 'emulate device, for example "iPhone 11"').option("--geolocation <coordinates>", 'specify geolocation coordinates, for example "37.819722,-122.478611"').option("--ignore-https-errors", "ignore https errors").option("--load-storage <filename>", "load context storage state from the file, previously saved with --save-storage").option("--lang <language>", 'specify language / locale, for example "en-GB"').option("--proxy-server <proxy>", 'specify proxy server, for example "http://myproxy:3128" or "socks5://myproxy:8080"').option("--proxy-bypass <bypass>", 'comma-separated domains to bypass proxy, for example ".com,chromium.org,.domain.com"').option("--save-har <filename>", "save HAR file with all network activity at the end").option("--save-har-glob <glob pattern>", "filter entries in the HAR by matching url against this glob pattern").option("--save-storage <filename>", "save context storage state at the end, for later use with --load-storage").option("--timezone <time zone>", 'time zone to emulate, for example "Europe/Rome"').option("--timeout <timeout>", "timeout for Playwright actions in milliseconds, no timeout by default").option("--user-agent <ua string>", "specify user agent string").option("--user-data-dir <directory>", "use the specified user data directory instead of a new context").option("--viewport-size <size>", 'specify browser viewport size in pixels, for example "1280, 720"');
}
function buildBasePlaywrightCLICommand(cliTargetLang) {
switch (cliTargetLang) {
case "python":
return `playwright`;
case "java":
return `mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="...options.."`;
case "csharp":
return `pwsh bin/Debug/netX/playwright.ps1`;
default: {
const packageManagerCommand = (0, import_utils2.getPackageManagerExecCommand)();
return `${packageManagerCommand} playwright`;
}
}
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
program
});

View File

@@ -0,0 +1,74 @@
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var programWithTestStub_exports = {};
__export(programWithTestStub_exports, {
program: () => import_program2.program
});
module.exports = __toCommonJS(programWithTestStub_exports);
var import_processLauncher = require("../server/utils/processLauncher");
var import_utils = require("../utils");
var import_program = require("./program");
var import_program2 = require("./program");
function printPlaywrightTestError(command) {
const packages = [];
for (const pkg of ["playwright", "playwright-chromium", "playwright-firefox", "playwright-webkit"]) {
try {
require.resolve(pkg);
packages.push(pkg);
} catch (e) {
}
}
if (!packages.length)
packages.push("playwright");
const packageManager = (0, import_utils.getPackageManager)();
if (packageManager === "yarn") {
console.error(`Please install @playwright/test package before running "yarn playwright ${command}"`);
console.error(` yarn remove ${packages.join(" ")}`);
console.error(" yarn add -D @playwright/test");
} else if (packageManager === "pnpm") {
console.error(`Please install @playwright/test package before running "pnpm exec playwright ${command}"`);
console.error(` pnpm remove ${packages.join(" ")}`);
console.error(" pnpm add -D @playwright/test");
} else {
console.error(`Please install @playwright/test package before running "npx playwright ${command}"`);
console.error(` npm uninstall ${packages.join(" ")}`);
console.error(" npm install -D @playwright/test");
}
}
const kExternalPlaywrightTestCommands = [
["test", "Run tests with Playwright Test."],
["show-report", "Show Playwright Test HTML report."],
["merge-reports", "Merge Playwright Test Blob reports"]
];
function addExternalPlaywrightTestCommands() {
for (const [command, description] of kExternalPlaywrightTestCommands) {
const playwrightTest = import_program.program.command(command).allowUnknownOption(true).allowExcessArguments(true);
playwrightTest.description(`${description} Available in @playwright/test package.`);
playwrightTest.action(async () => {
printPlaywrightTestError(command);
(0, import_processLauncher.gracefullyProcessExitDoNotHang)(1);
});
}
}
if (!process.env.PW_LANG_NAME)
addExternalPlaywrightTestCommands();
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
program
});

View File

@@ -0,0 +1,49 @@
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var accessibility_exports = {};
__export(accessibility_exports, {
Accessibility: () => Accessibility
});
module.exports = __toCommonJS(accessibility_exports);
function axNodeFromProtocol(axNode) {
const result = {
...axNode,
value: axNode.valueNumber !== void 0 ? axNode.valueNumber : axNode.valueString,
checked: axNode.checked === "checked" ? true : axNode.checked === "unchecked" ? false : axNode.checked,
pressed: axNode.pressed === "pressed" ? true : axNode.pressed === "released" ? false : axNode.pressed,
children: axNode.children ? axNode.children.map(axNodeFromProtocol) : void 0
};
delete result.valueNumber;
delete result.valueString;
return result;
}
class Accessibility {
constructor(channel) {
this._channel = channel;
}
async snapshot(options = {}) {
const root = options.root ? options.root._elementChannel : void 0;
const result = await this._channel.accessibilitySnapshot({ interestingOnly: options.interestingOnly, root });
return result.rootAXNode ? axNodeFromProtocol(result.rootAXNode) : null;
}
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
Accessibility
});

361
node_modules/playwright-core/lib/client/android.js generated vendored Normal file
View File

@@ -0,0 +1,361 @@
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var android_exports = {};
__export(android_exports, {
Android: () => Android,
AndroidDevice: () => AndroidDevice,
AndroidInput: () => AndroidInput,
AndroidSocket: () => AndroidSocket,
AndroidWebView: () => AndroidWebView
});
module.exports = __toCommonJS(android_exports);
var import_eventEmitter = require("./eventEmitter");
var import_browserContext = require("./browserContext");
var import_channelOwner = require("./channelOwner");
var import_errors = require("./errors");
var import_events = require("./events");
var import_waiter = require("./waiter");
var import_timeoutSettings = require("./timeoutSettings");
var import_rtti = require("../utils/isomorphic/rtti");
var import_time = require("../utils/isomorphic/time");
var import_timeoutRunner = require("../utils/isomorphic/timeoutRunner");
var import_webSocket = require("./webSocket");
class Android extends import_channelOwner.ChannelOwner {
static from(android) {
return android._object;
}
constructor(parent, type, guid, initializer) {
super(parent, type, guid, initializer);
this._timeoutSettings = new import_timeoutSettings.TimeoutSettings(this._platform);
}
setDefaultTimeout(timeout) {
this._timeoutSettings.setDefaultTimeout(timeout);
}
async devices(options = {}) {
const { devices } = await this._channel.devices(options);
return devices.map((d) => AndroidDevice.from(d));
}
async launchServer(options = {}) {
if (!this._serverLauncher)
throw new Error("Launching server is not supported");
return await this._serverLauncher.launchServer(options);
}
async connect(wsEndpoint, options = {}) {
return await this._wrapApiCall(async () => {
const deadline = options.timeout ? (0, import_time.monotonicTime)() + options.timeout : 0;
const headers = { "x-playwright-browser": "android", ...options.headers };
const connectParams = { wsEndpoint, headers, slowMo: options.slowMo, timeout: options.timeout || 0 };
const connection = await (0, import_webSocket.connectOverWebSocket)(this._connection, connectParams);
let device;
connection.on("close", () => {
device?._didClose();
});
const result = await (0, import_timeoutRunner.raceAgainstDeadline)(async () => {
const playwright = await connection.initializePlaywright();
if (!playwright._initializer.preConnectedAndroidDevice) {
connection.close();
throw new Error("Malformed endpoint. Did you use Android.launchServer method?");
}
device = AndroidDevice.from(playwright._initializer.preConnectedAndroidDevice);
device._shouldCloseConnectionOnClose = true;
device.on(import_events.Events.AndroidDevice.Close, () => connection.close());
return device;
}, deadline);
if (!result.timedOut) {
return result.result;
} else {
connection.close();
throw new Error(`Timeout ${options.timeout}ms exceeded`);
}
});
}
}
class AndroidDevice extends import_channelOwner.ChannelOwner {
constructor(parent, type, guid, initializer) {
super(parent, type, guid, initializer);
this._webViews = /* @__PURE__ */ new Map();
this._shouldCloseConnectionOnClose = false;
this._android = parent;
this.input = new AndroidInput(this);
this._timeoutSettings = new import_timeoutSettings.TimeoutSettings(this._platform, parent._timeoutSettings);
this._channel.on("webViewAdded", ({ webView }) => this._onWebViewAdded(webView));
this._channel.on("webViewRemoved", ({ socketName }) => this._onWebViewRemoved(socketName));
this._channel.on("close", () => this._didClose());
}
static from(androidDevice) {
return androidDevice._object;
}
_onWebViewAdded(webView) {
const view = new AndroidWebView(this, webView);
this._webViews.set(webView.socketName, view);
this.emit(import_events.Events.AndroidDevice.WebView, view);
}
_onWebViewRemoved(socketName) {
const view = this._webViews.get(socketName);
this._webViews.delete(socketName);
if (view)
view.emit(import_events.Events.AndroidWebView.Close);
}
setDefaultTimeout(timeout) {
this._timeoutSettings.setDefaultTimeout(timeout);
}
serial() {
return this._initializer.serial;
}
model() {
return this._initializer.model;
}
webViews() {
return [...this._webViews.values()];
}
async webView(selector, options) {
const predicate = (v) => {
if (selector.pkg)
return v.pkg() === selector.pkg;
if (selector.socketName)
return v._socketName() === selector.socketName;
return false;
};
const webView = [...this._webViews.values()].find(predicate);
if (webView)
return webView;
return await this.waitForEvent("webview", { ...options, predicate });
}
async wait(selector, options = {}) {
await this._channel.wait({ androidSelector: toSelectorChannel(selector), ...options, timeout: this._timeoutSettings.timeout(options) });
}
async fill(selector, text, options = {}) {
await this._channel.fill({ androidSelector: toSelectorChannel(selector), text, ...options, timeout: this._timeoutSettings.timeout(options) });
}
async press(selector, key, options = {}) {
await this.tap(selector, options);
await this.input.press(key);
}
async tap(selector, options = {}) {
await this._channel.tap({ androidSelector: toSelectorChannel(selector), ...options, timeout: this._timeoutSettings.timeout(options) });
}
async drag(selector, dest, options = {}) {
await this._channel.drag({ androidSelector: toSelectorChannel(selector), dest, ...options, timeout: this._timeoutSettings.timeout(options) });
}
async fling(selector, direction, options = {}) {
await this._channel.fling({ androidSelector: toSelectorChannel(selector), direction, ...options, timeout: this._timeoutSettings.timeout(options) });
}
async longTap(selector, options = {}) {
await this._channel.longTap({ androidSelector: toSelectorChannel(selector), ...options, timeout: this._timeoutSettings.timeout(options) });
}
async pinchClose(selector, percent, options = {}) {
await this._channel.pinchClose({ androidSelector: toSelectorChannel(selector), percent, ...options, timeout: this._timeoutSettings.timeout(options) });
}
async pinchOpen(selector, percent, options = {}) {
await this._channel.pinchOpen({ androidSelector: toSelectorChannel(selector), percent, ...options, timeout: this._timeoutSettings.timeout(options) });
}
async scroll(selector, direction, percent, options = {}) {
await this._channel.scroll({ androidSelector: toSelectorChannel(selector), direction, percent, ...options, timeout: this._timeoutSettings.timeout(options) });
}
async swipe(selector, direction, percent, options = {}) {
await this._channel.swipe({ androidSelector: toSelectorChannel(selector), direction, percent, ...options, timeout: this._timeoutSettings.timeout(options) });
}
async info(selector) {
return (await this._channel.info({ androidSelector: toSelectorChannel(selector) })).info;
}
async screenshot(options = {}) {
const { binary } = await this._channel.screenshot();
if (options.path)
await this._platform.fs().promises.writeFile(options.path, binary);
return binary;
}
async [Symbol.asyncDispose]() {
await this.close();
}
async close() {
try {
if (this._shouldCloseConnectionOnClose)
this._connection.close();
else
await this._channel.close();
} catch (e) {
if ((0, import_errors.isTargetClosedError)(e))
return;
throw e;
}
}
_didClose() {
this.emit(import_events.Events.AndroidDevice.Close, this);
}
async shell(command) {
const { result } = await this._channel.shell({ command });
return result;
}
async open(command) {
return AndroidSocket.from((await this._channel.open({ command })).socket);
}
async installApk(file, options) {
await this._channel.installApk({ file: await loadFile(this._platform, file), args: options && options.args });
}
async push(file, path, options) {
await this._channel.push({ file: await loadFile(this._platform, file), path, mode: options ? options.mode : void 0 });
}
async launchBrowser(options = {}) {
const contextOptions = await (0, import_browserContext.prepareBrowserContextParams)(this._platform, options);
const result = await this._channel.launchBrowser(contextOptions);
const context = import_browserContext.BrowserContext.from(result.context);
const selectors = this._android._playwright.selectors;
selectors._contextsForSelectors.add(context);
context.once(import_events.Events.BrowserContext.Close, () => selectors._contextsForSelectors.delete(context));
await context._initializeHarFromOptions(options.recordHar);
return context;
}
async waitForEvent(event, optionsOrPredicate = {}) {
return await this._wrapApiCall(async () => {
const timeout = this._timeoutSettings.timeout(typeof optionsOrPredicate === "function" ? {} : optionsOrPredicate);
const predicate = typeof optionsOrPredicate === "function" ? optionsOrPredicate : optionsOrPredicate.predicate;
const waiter = import_waiter.Waiter.createForEvent(this, event);
waiter.rejectOnTimeout(timeout, `Timeout ${timeout}ms exceeded while waiting for event "${event}"`);
if (event !== import_events.Events.AndroidDevice.Close)
waiter.rejectOnEvent(this, import_events.Events.AndroidDevice.Close, () => new import_errors.TargetClosedError());
const result = await waiter.waitForEvent(this, event, predicate);
waiter.dispose();
return result;
});
}
}
class AndroidSocket extends import_channelOwner.ChannelOwner {
static from(androidDevice) {
return androidDevice._object;
}
constructor(parent, type, guid, initializer) {
super(parent, type, guid, initializer);
this._channel.on("data", ({ data }) => this.emit(import_events.Events.AndroidSocket.Data, data));
this._channel.on("close", () => this.emit(import_events.Events.AndroidSocket.Close));
}
async write(data) {
await this._channel.write({ data });
}
async close() {
await this._channel.close();
}
async [Symbol.asyncDispose]() {
await this.close();
}
}
async function loadFile(platform, file) {
if ((0, import_rtti.isString)(file))
return await platform.fs().promises.readFile(file);
return file;
}
class AndroidInput {
constructor(device) {
this._device = device;
}
async type(text) {
await this._device._channel.inputType({ text });
}
async press(key) {
await this._device._channel.inputPress({ key });
}
async tap(point) {
await this._device._channel.inputTap({ point });
}
async swipe(from, segments, steps) {
await this._device._channel.inputSwipe({ segments, steps });
}
async drag(from, to, steps) {
await this._device._channel.inputDrag({ from, to, steps });
}
}
function toSelectorChannel(selector) {
const {
checkable,
checked,
clazz,
clickable,
depth,
desc,
enabled,
focusable,
focused,
hasChild,
hasDescendant,
longClickable,
pkg,
res,
scrollable,
selected,
text
} = selector;
const toRegex = (value) => {
if (value === void 0)
return void 0;
if ((0, import_rtti.isRegExp)(value))
return value.source;
return "^" + value.replace(/[|\\{}()[\]^$+*?.]/g, "\\$&").replace(/-/g, "\\x2d") + "$";
};
return {
checkable,
checked,
clazz: toRegex(clazz),
pkg: toRegex(pkg),
desc: toRegex(desc),
res: toRegex(res),
text: toRegex(text),
clickable,
depth,
enabled,
focusable,
focused,
hasChild: hasChild ? { androidSelector: toSelectorChannel(hasChild.selector) } : void 0,
hasDescendant: hasDescendant ? { androidSelector: toSelectorChannel(hasDescendant.selector), maxDepth: hasDescendant.maxDepth } : void 0,
longClickable,
scrollable,
selected
};
}
class AndroidWebView extends import_eventEmitter.EventEmitter {
constructor(device, data) {
super(device._platform);
this._device = device;
this._data = data;
}
pid() {
return this._data.pid;
}
pkg() {
return this._data.pkg;
}
_socketName() {
return this._data.socketName;
}
async page() {
if (!this._pagePromise)
this._pagePromise = this._fetchPage();
return await this._pagePromise;
}
async _fetchPage() {
const { context } = await this._device._channel.connectToWebView({ socketName: this._data.socketName });
return import_browserContext.BrowserContext.from(context).pages()[0];
}
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
Android,
AndroidDevice,
AndroidInput,
AndroidSocket,
AndroidWebView
});

137
node_modules/playwright-core/lib/client/api.js generated vendored Normal file
View File

@@ -0,0 +1,137 @@
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var api_exports = {};
__export(api_exports, {
APIRequest: () => import_fetch.APIRequest,
APIRequestContext: () => import_fetch.APIRequestContext,
APIResponse: () => import_fetch.APIResponse,
Accessibility: () => import_accessibility.Accessibility,
Android: () => import_android.Android,
AndroidDevice: () => import_android.AndroidDevice,
AndroidInput: () => import_android.AndroidInput,
AndroidSocket: () => import_android.AndroidSocket,
AndroidWebView: () => import_android.AndroidWebView,
Browser: () => import_browser.Browser,
BrowserContext: () => import_browserContext.BrowserContext,
BrowserType: () => import_browserType.BrowserType,
CDPSession: () => import_cdpSession.CDPSession,
Clock: () => import_clock.Clock,
ConsoleMessage: () => import_consoleMessage.ConsoleMessage,
Coverage: () => import_coverage.Coverage,
Dialog: () => import_dialog.Dialog,
Download: () => import_download.Download,
Electron: () => import_electron.Electron,
ElectronApplication: () => import_electron.ElectronApplication,
ElementHandle: () => import_elementHandle.ElementHandle,
FileChooser: () => import_fileChooser.FileChooser,
Frame: () => import_frame.Frame,
FrameLocator: () => import_locator.FrameLocator,
JSHandle: () => import_jsHandle.JSHandle,
Keyboard: () => import_input.Keyboard,
Locator: () => import_locator.Locator,
Mouse: () => import_input.Mouse,
Page: () => import_page.Page,
Playwright: () => import_playwright.Playwright,
Request: () => import_network.Request,
Response: () => import_network.Response,
Route: () => import_network.Route,
Selectors: () => import_selectors.Selectors,
TimeoutError: () => import_errors.TimeoutError,
Touchscreen: () => import_input.Touchscreen,
Tracing: () => import_tracing.Tracing,
Video: () => import_video.Video,
WebError: () => import_webError.WebError,
WebSocket: () => import_network.WebSocket,
WebSocketRoute: () => import_network.WebSocketRoute,
Worker: () => import_worker.Worker
});
module.exports = __toCommonJS(api_exports);
var import_accessibility = require("./accessibility");
var import_android = require("./android");
var import_browser = require("./browser");
var import_browserContext = require("./browserContext");
var import_browserType = require("./browserType");
var import_clock = require("./clock");
var import_consoleMessage = require("./consoleMessage");
var import_coverage = require("./coverage");
var import_dialog = require("./dialog");
var import_download = require("./download");
var import_electron = require("./electron");
var import_locator = require("./locator");
var import_elementHandle = require("./elementHandle");
var import_fileChooser = require("./fileChooser");
var import_errors = require("./errors");
var import_frame = require("./frame");
var import_input = require("./input");
var import_jsHandle = require("./jsHandle");
var import_network = require("./network");
var import_fetch = require("./fetch");
var import_page = require("./page");
var import_selectors = require("./selectors");
var import_tracing = require("./tracing");
var import_video = require("./video");
var import_worker = require("./worker");
var import_cdpSession = require("./cdpSession");
var import_playwright = require("./playwright");
var import_webError = require("./webError");
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
APIRequest,
APIRequestContext,
APIResponse,
Accessibility,
Android,
AndroidDevice,
AndroidInput,
AndroidSocket,
AndroidWebView,
Browser,
BrowserContext,
BrowserType,
CDPSession,
Clock,
ConsoleMessage,
Coverage,
Dialog,
Download,
Electron,
ElectronApplication,
ElementHandle,
FileChooser,
Frame,
FrameLocator,
JSHandle,
Keyboard,
Locator,
Mouse,
Page,
Playwright,
Request,
Response,
Route,
Selectors,
TimeoutError,
Touchscreen,
Tracing,
Video,
WebError,
WebSocket,
WebSocketRoute,
Worker
});

79
node_modules/playwright-core/lib/client/artifact.js generated vendored Normal file
View File

@@ -0,0 +1,79 @@
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var artifact_exports = {};
__export(artifact_exports, {
Artifact: () => Artifact
});
module.exports = __toCommonJS(artifact_exports);
var import_channelOwner = require("./channelOwner");
var import_stream = require("./stream");
var import_fileUtils = require("./fileUtils");
class Artifact extends import_channelOwner.ChannelOwner {
static from(channel) {
return channel._object;
}
async pathAfterFinished() {
if (this._connection.isRemote())
throw new Error(`Path is not available when connecting remotely. Use saveAs() to save a local copy.`);
return (await this._channel.pathAfterFinished()).value;
}
async saveAs(path) {
if (!this._connection.isRemote()) {
await this._channel.saveAs({ path });
return;
}
const result = await this._channel.saveAsStream();
const stream = import_stream.Stream.from(result.stream);
await (0, import_fileUtils.mkdirIfNeeded)(this._platform, path);
await new Promise((resolve, reject) => {
stream.stream().pipe(this._platform.fs().createWriteStream(path)).on("finish", resolve).on("error", reject);
});
}
async failure() {
return (await this._channel.failure()).error || null;
}
async createReadStream() {
const result = await this._channel.stream();
const stream = import_stream.Stream.from(result.stream);
return stream.stream();
}
async readIntoBuffer() {
const stream = await this.createReadStream();
return await new Promise((resolve, reject) => {
const chunks = [];
stream.on("data", (chunk) => {
chunks.push(chunk);
});
stream.on("end", () => {
resolve(Buffer.concat(chunks));
});
stream.on("error", reject);
});
}
async cancel() {
return await this._channel.cancel();
}
async delete() {
return await this._channel.delete();
}
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
Artifact
});

173
node_modules/playwright-core/lib/client/browser.js generated vendored Normal file
View File

@@ -0,0 +1,173 @@
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var browser_exports = {};
__export(browser_exports, {
Browser: () => Browser
});
module.exports = __toCommonJS(browser_exports);
var import_artifact = require("./artifact");
var import_browserContext = require("./browserContext");
var import_cdpSession = require("./cdpSession");
var import_channelOwner = require("./channelOwner");
var import_errors = require("./errors");
var import_events = require("./events");
var import_fileUtils = require("./fileUtils");
class Browser extends import_channelOwner.ChannelOwner {
constructor(parent, type, guid, initializer) {
super(parent, type, guid, initializer);
this._contexts = /* @__PURE__ */ new Set();
this._isConnected = true;
this._shouldCloseConnectionOnClose = false;
this._options = {};
this._name = initializer.name;
this._channel.on("context", ({ context }) => this._didCreateContext(import_browserContext.BrowserContext.from(context)));
this._channel.on("close", () => this._didClose());
this._closedPromise = new Promise((f) => this.once(import_events.Events.Browser.Disconnected, f));
}
static from(browser) {
return browser._object;
}
browserType() {
return this._browserType;
}
async newContext(options = {}) {
return await this._innerNewContext(options, false);
}
async _newContextForReuse(options = {}) {
return await this._innerNewContext(options, true);
}
async _disconnectFromReusedContext(reason) {
const context = [...this._contexts].find((context2) => context2._forReuse);
if (!context)
return;
await this._instrumentation.runBeforeCloseBrowserContext(context);
for (const page of context.pages())
page._onClose();
context._onClose();
await this._channel.disconnectFromReusedContext({ reason });
}
async _innerNewContext(options = {}, forReuse) {
options = this._browserType._playwright.selectors._withSelectorOptions({
...this._browserType._playwright._defaultContextOptions,
...options
});
const contextOptions = await (0, import_browserContext.prepareBrowserContextParams)(this._platform, options);
const response = forReuse ? await this._channel.newContextForReuse(contextOptions) : await this._channel.newContext(contextOptions);
const context = import_browserContext.BrowserContext.from(response.context);
if (forReuse)
context._forReuse = true;
if (options.logger)
context._logger = options.logger;
await context._initializeHarFromOptions(options.recordHar);
await this._instrumentation.runAfterCreateBrowserContext(context);
return context;
}
_connectToBrowserType(browserType, browserOptions, logger) {
this._browserType = browserType;
this._options = browserOptions;
this._logger = logger;
for (const context of this._contexts)
this._setupBrowserContext(context);
}
_didCreateContext(context) {
context._browser = this;
this._contexts.add(context);
if (this._browserType)
this._setupBrowserContext(context);
}
_setupBrowserContext(context) {
context._logger = this._logger;
context.tracing._tracesDir = this._options.tracesDir;
this._browserType._contexts.add(context);
this._browserType._playwright.selectors._contextsForSelectors.add(context);
context.setDefaultTimeout(this._browserType._playwright._defaultContextTimeout);
context.setDefaultNavigationTimeout(this._browserType._playwright._defaultContextNavigationTimeout);
}
contexts() {
return [...this._contexts];
}
version() {
return this._initializer.version;
}
async newPage(options = {}) {
return await this._wrapApiCall(async () => {
const context = await this.newContext(options);
const page = await context.newPage();
page._ownedContext = context;
context._ownerPage = page;
return page;
}, { title: "Create page" });
}
isConnected() {
return this._isConnected;
}
async newBrowserCDPSession() {
return import_cdpSession.CDPSession.from((await this._channel.newBrowserCDPSession()).session);
}
async _launchServer(options = {}) {
const serverLauncher = this._browserType._serverLauncher;
const browserImpl = this._connection.toImpl?.(this);
if (!serverLauncher || !browserImpl)
throw new Error("Launching server is not supported");
return await serverLauncher.launchServerOnExistingBrowser(browserImpl, {
_sharedBrowser: true,
...options
});
}
async startTracing(page, options = {}) {
this._path = options.path;
await this._channel.startTracing({ ...options, page: page ? page._channel : void 0 });
}
async stopTracing() {
const artifact = import_artifact.Artifact.from((await this._channel.stopTracing()).artifact);
const buffer = await artifact.readIntoBuffer();
await artifact.delete();
if (this._path) {
await (0, import_fileUtils.mkdirIfNeeded)(this._platform, this._path);
await this._platform.fs().promises.writeFile(this._path, buffer);
this._path = void 0;
}
return buffer;
}
async [Symbol.asyncDispose]() {
await this.close();
}
async close(options = {}) {
this._closeReason = options.reason;
try {
if (this._shouldCloseConnectionOnClose)
this._connection.close();
else
await this._channel.close(options);
await this._closedPromise;
} catch (e) {
if ((0, import_errors.isTargetClosedError)(e))
return;
throw e;
}
}
_didClose() {
this._isConnected = false;
this.emit(import_events.Events.Browser.Disconnected, this);
}
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
Browser
});

View File

@@ -0,0 +1,535 @@
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var browserContext_exports = {};
__export(browserContext_exports, {
BrowserContext: () => BrowserContext,
prepareBrowserContextParams: () => prepareBrowserContextParams,
toClientCertificatesProtocol: () => toClientCertificatesProtocol
});
module.exports = __toCommonJS(browserContext_exports);
var import_artifact = require("./artifact");
var import_cdpSession = require("./cdpSession");
var import_channelOwner = require("./channelOwner");
var import_clientHelper = require("./clientHelper");
var import_clock = require("./clock");
var import_consoleMessage = require("./consoleMessage");
var import_dialog = require("./dialog");
var import_errors = require("./errors");
var import_events = require("./events");
var import_fetch = require("./fetch");
var import_frame = require("./frame");
var import_harRouter = require("./harRouter");
var network = __toESM(require("./network"));
var import_page = require("./page");
var import_tracing = require("./tracing");
var import_waiter = require("./waiter");
var import_webError = require("./webError");
var import_worker = require("./worker");
var import_timeoutSettings = require("./timeoutSettings");
var import_fileUtils = require("./fileUtils");
var import_headers = require("../utils/isomorphic/headers");
var import_urlMatch = require("../utils/isomorphic/urlMatch");
var import_rtti = require("../utils/isomorphic/rtti");
var import_stackTrace = require("../utils/isomorphic/stackTrace");
class BrowserContext extends import_channelOwner.ChannelOwner {
constructor(parent, type, guid, initializer) {
super(parent, type, guid, initializer);
this._pages = /* @__PURE__ */ new Set();
this._routes = [];
this._webSocketRoutes = [];
// Browser is null for browser contexts created outside of normal browser, e.g. android or electron.
this._browser = null;
this._bindings = /* @__PURE__ */ new Map();
this._forReuse = false;
this._backgroundPages = /* @__PURE__ */ new Set();
this._serviceWorkers = /* @__PURE__ */ new Set();
this._harRecorders = /* @__PURE__ */ new Map();
this._closingStatus = "none";
this._harRouters = [];
this._options = initializer.options;
this._timeoutSettings = new import_timeoutSettings.TimeoutSettings(this._platform);
this.tracing = import_tracing.Tracing.from(initializer.tracing);
this.request = import_fetch.APIRequestContext.from(initializer.requestContext);
this.request._timeoutSettings = this._timeoutSettings;
this.clock = new import_clock.Clock(this);
this._channel.on("bindingCall", ({ binding }) => this._onBinding(import_page.BindingCall.from(binding)));
this._channel.on("close", () => this._onClose());
this._channel.on("page", ({ page }) => this._onPage(import_page.Page.from(page)));
this._channel.on("route", ({ route }) => this._onRoute(network.Route.from(route)));
this._channel.on("webSocketRoute", ({ webSocketRoute }) => this._onWebSocketRoute(network.WebSocketRoute.from(webSocketRoute)));
this._channel.on("backgroundPage", ({ page }) => {
const backgroundPage = import_page.Page.from(page);
this._backgroundPages.add(backgroundPage);
this.emit(import_events.Events.BrowserContext.BackgroundPage, backgroundPage);
});
this._channel.on("serviceWorker", ({ worker }) => {
const serviceWorker = import_worker.Worker.from(worker);
serviceWorker._context = this;
this._serviceWorkers.add(serviceWorker);
this.emit(import_events.Events.BrowserContext.ServiceWorker, serviceWorker);
});
this._channel.on("console", (event) => {
const consoleMessage = new import_consoleMessage.ConsoleMessage(this._platform, event);
this.emit(import_events.Events.BrowserContext.Console, consoleMessage);
const page = consoleMessage.page();
if (page)
page.emit(import_events.Events.Page.Console, consoleMessage);
});
this._channel.on("pageError", ({ error, page }) => {
const pageObject = import_page.Page.from(page);
const parsedError = (0, import_errors.parseError)(error);
this.emit(import_events.Events.BrowserContext.WebError, new import_webError.WebError(pageObject, parsedError));
if (pageObject)
pageObject.emit(import_events.Events.Page.PageError, parsedError);
});
this._channel.on("dialog", ({ dialog }) => {
const dialogObject = import_dialog.Dialog.from(dialog);
let hasListeners = this.emit(import_events.Events.BrowserContext.Dialog, dialogObject);
const page = dialogObject.page();
if (page)
hasListeners = page.emit(import_events.Events.Page.Dialog, dialogObject) || hasListeners;
if (!hasListeners) {
if (dialogObject.type() === "beforeunload")
dialog.accept({}).catch(() => {
});
else
dialog.dismiss().catch(() => {
});
}
});
this._channel.on("request", ({ request, page }) => this._onRequest(network.Request.from(request), import_page.Page.fromNullable(page)));
this._channel.on("requestFailed", ({ request, failureText, responseEndTiming, page }) => this._onRequestFailed(network.Request.from(request), responseEndTiming, failureText, import_page.Page.fromNullable(page)));
this._channel.on("requestFinished", (params) => this._onRequestFinished(params));
this._channel.on("response", ({ response, page }) => this._onResponse(network.Response.from(response), import_page.Page.fromNullable(page)));
this._channel.on("recorderEvent", ({ event, data, page, code }) => {
if (event === "actionAdded")
this._onRecorderEventSink?.actionAdded?.(import_page.Page.from(page), data, code);
else if (event === "actionUpdated")
this._onRecorderEventSink?.actionUpdated?.(import_page.Page.from(page), data, code);
else if (event === "signalAdded")
this._onRecorderEventSink?.signalAdded?.(import_page.Page.from(page), data);
});
this._closedPromise = new Promise((f) => this.once(import_events.Events.BrowserContext.Close, f));
this._setEventToSubscriptionMapping(/* @__PURE__ */ new Map([
[import_events.Events.BrowserContext.Console, "console"],
[import_events.Events.BrowserContext.Dialog, "dialog"],
[import_events.Events.BrowserContext.Request, "request"],
[import_events.Events.BrowserContext.Response, "response"],
[import_events.Events.BrowserContext.RequestFinished, "requestFinished"],
[import_events.Events.BrowserContext.RequestFailed, "requestFailed"]
]));
}
static from(context) {
return context._object;
}
static fromNullable(context) {
return context ? BrowserContext.from(context) : null;
}
async _initializeHarFromOptions(recordHar) {
if (!recordHar)
return;
const defaultContent = recordHar.path.endsWith(".zip") ? "attach" : "embed";
await this._recordIntoHAR(recordHar.path, null, {
url: recordHar.urlFilter,
updateContent: recordHar.content ?? (recordHar.omitContent ? "omit" : defaultContent),
updateMode: recordHar.mode ?? "full"
});
}
_onPage(page) {
this._pages.add(page);
this.emit(import_events.Events.BrowserContext.Page, page);
if (page._opener && !page._opener.isClosed())
page._opener.emit(import_events.Events.Page.Popup, page);
}
_onRequest(request, page) {
this.emit(import_events.Events.BrowserContext.Request, request);
if (page)
page.emit(import_events.Events.Page.Request, request);
}
_onResponse(response, page) {
this.emit(import_events.Events.BrowserContext.Response, response);
if (page)
page.emit(import_events.Events.Page.Response, response);
}
_onRequestFailed(request, responseEndTiming, failureText, page) {
request._failureText = failureText || null;
request._setResponseEndTiming(responseEndTiming);
this.emit(import_events.Events.BrowserContext.RequestFailed, request);
if (page)
page.emit(import_events.Events.Page.RequestFailed, request);
}
_onRequestFinished(params) {
const { responseEndTiming } = params;
const request = network.Request.from(params.request);
const response = network.Response.fromNullable(params.response);
const page = import_page.Page.fromNullable(params.page);
request._setResponseEndTiming(responseEndTiming);
this.emit(import_events.Events.BrowserContext.RequestFinished, request);
if (page)
page.emit(import_events.Events.Page.RequestFinished, request);
if (response)
response._finishedPromise.resolve(null);
}
async _onRoute(route) {
route._context = this;
const page = route.request()._safePage();
const routeHandlers = this._routes.slice();
for (const routeHandler of routeHandlers) {
if (page?._closeWasCalled || this._closingStatus !== "none")
return;
if (!routeHandler.matches(route.request().url()))
continue;
const index = this._routes.indexOf(routeHandler);
if (index === -1)
continue;
if (routeHandler.willExpire())
this._routes.splice(index, 1);
const handled = await routeHandler.handle(route);
if (!this._routes.length)
this._updateInterceptionPatterns({ internal: true }).catch(() => {
});
if (handled)
return;
}
await route._innerContinue(
true
/* isFallback */
).catch(() => {
});
}
async _onWebSocketRoute(webSocketRoute) {
const routeHandler = this._webSocketRoutes.find((route) => route.matches(webSocketRoute.url()));
if (routeHandler)
await routeHandler.handle(webSocketRoute);
else
webSocketRoute.connectToServer();
}
async _onBinding(bindingCall) {
const func = this._bindings.get(bindingCall._initializer.name);
if (!func)
return;
await bindingCall.call(func);
}
setDefaultNavigationTimeout(timeout) {
this._timeoutSettings.setDefaultNavigationTimeout(timeout);
}
setDefaultTimeout(timeout) {
this._timeoutSettings.setDefaultTimeout(timeout);
}
browser() {
return this._browser;
}
pages() {
return [...this._pages];
}
async newPage() {
if (this._ownerPage)
throw new Error("Please use browser.newContext()");
return import_page.Page.from((await this._channel.newPage()).page);
}
async cookies(urls) {
if (!urls)
urls = [];
if (urls && typeof urls === "string")
urls = [urls];
return (await this._channel.cookies({ urls })).cookies;
}
async addCookies(cookies) {
await this._channel.addCookies({ cookies });
}
async clearCookies(options = {}) {
await this._channel.clearCookies({
name: (0, import_rtti.isString)(options.name) ? options.name : void 0,
nameRegexSource: (0, import_rtti.isRegExp)(options.name) ? options.name.source : void 0,
nameRegexFlags: (0, import_rtti.isRegExp)(options.name) ? options.name.flags : void 0,
domain: (0, import_rtti.isString)(options.domain) ? options.domain : void 0,
domainRegexSource: (0, import_rtti.isRegExp)(options.domain) ? options.domain.source : void 0,
domainRegexFlags: (0, import_rtti.isRegExp)(options.domain) ? options.domain.flags : void 0,
path: (0, import_rtti.isString)(options.path) ? options.path : void 0,
pathRegexSource: (0, import_rtti.isRegExp)(options.path) ? options.path.source : void 0,
pathRegexFlags: (0, import_rtti.isRegExp)(options.path) ? options.path.flags : void 0
});
}
async grantPermissions(permissions, options) {
await this._channel.grantPermissions({ permissions, ...options });
}
async clearPermissions() {
await this._channel.clearPermissions();
}
async setGeolocation(geolocation) {
await this._channel.setGeolocation({ geolocation: geolocation || void 0 });
}
async setExtraHTTPHeaders(headers) {
network.validateHeaders(headers);
await this._channel.setExtraHTTPHeaders({ headers: (0, import_headers.headersObjectToArray)(headers) });
}
async setOffline(offline) {
await this._channel.setOffline({ offline });
}
async setHTTPCredentials(httpCredentials) {
await this._channel.setHTTPCredentials({ httpCredentials: httpCredentials || void 0 });
}
async addInitScript(script, arg) {
const source = await (0, import_clientHelper.evaluationScript)(this._platform, script, arg);
await this._channel.addInitScript({ source });
}
async exposeBinding(name, callback, options = {}) {
await this._channel.exposeBinding({ name, needsHandle: options.handle });
this._bindings.set(name, callback);
}
async exposeFunction(name, callback) {
await this._channel.exposeBinding({ name });
const binding = (source, ...args) => callback(...args);
this._bindings.set(name, binding);
}
async route(url, handler, options = {}) {
this._routes.unshift(new network.RouteHandler(this._platform, this._options.baseURL, url, handler, options.times));
await this._updateInterceptionPatterns({ title: "Route requests" });
}
async routeWebSocket(url, handler) {
this._webSocketRoutes.unshift(new network.WebSocketRouteHandler(this._options.baseURL, url, handler));
await this._updateWebSocketInterceptionPatterns({ title: "Route WebSockets" });
}
async _recordIntoHAR(har, page, options = {}) {
const { harId } = await this._channel.harStart({
page: page?._channel,
options: {
zip: har.endsWith(".zip"),
content: options.updateContent ?? "attach",
urlGlob: (0, import_rtti.isString)(options.url) ? options.url : void 0,
urlRegexSource: (0, import_rtti.isRegExp)(options.url) ? options.url.source : void 0,
urlRegexFlags: (0, import_rtti.isRegExp)(options.url) ? options.url.flags : void 0,
mode: options.updateMode ?? "minimal"
}
});
this._harRecorders.set(harId, { path: har, content: options.updateContent ?? "attach" });
}
async routeFromHAR(har, options = {}) {
const localUtils = this._connection.localUtils();
if (!localUtils)
throw new Error("Route from har is not supported in thin clients");
if (options.update) {
await this._recordIntoHAR(har, null, options);
return;
}
const harRouter = await import_harRouter.HarRouter.create(localUtils, har, options.notFound || "abort", { urlMatch: options.url });
this._harRouters.push(harRouter);
await harRouter.addContextRoute(this);
}
_disposeHarRouters() {
this._harRouters.forEach((router) => router.dispose());
this._harRouters = [];
}
async unrouteAll(options) {
await this._unrouteInternal(this._routes, [], options?.behavior);
this._disposeHarRouters();
}
async unroute(url, handler) {
const removed = [];
const remaining = [];
for (const route of this._routes) {
if ((0, import_urlMatch.urlMatchesEqual)(route.url, url) && (!handler || route.handler === handler))
removed.push(route);
else
remaining.push(route);
}
await this._unrouteInternal(removed, remaining, "default");
}
async _unrouteInternal(removed, remaining, behavior) {
this._routes = remaining;
if (behavior && behavior !== "default") {
const promises = removed.map((routeHandler) => routeHandler.stop(behavior));
await Promise.all(promises);
}
await this._updateInterceptionPatterns({ title: "Unroute requests" });
}
async _updateInterceptionPatterns(options) {
const patterns = network.RouteHandler.prepareInterceptionPatterns(this._routes);
await this._wrapApiCall(() => this._channel.setNetworkInterceptionPatterns({ patterns }), options);
}
async _updateWebSocketInterceptionPatterns(options) {
const patterns = network.WebSocketRouteHandler.prepareInterceptionPatterns(this._webSocketRoutes);
await this._wrapApiCall(() => this._channel.setWebSocketInterceptionPatterns({ patterns }), options);
}
_effectiveCloseReason() {
return this._closeReason || this._browser?._closeReason;
}
async waitForEvent(event, optionsOrPredicate = {}) {
return await this._wrapApiCall(async () => {
const timeout = this._timeoutSettings.timeout(typeof optionsOrPredicate === "function" ? {} : optionsOrPredicate);
const predicate = typeof optionsOrPredicate === "function" ? optionsOrPredicate : optionsOrPredicate.predicate;
const waiter = import_waiter.Waiter.createForEvent(this, event);
waiter.rejectOnTimeout(timeout, `Timeout ${timeout}ms exceeded while waiting for event "${event}"`);
if (event !== import_events.Events.BrowserContext.Close)
waiter.rejectOnEvent(this, import_events.Events.BrowserContext.Close, () => new import_errors.TargetClosedError(this._effectiveCloseReason()));
const result = await waiter.waitForEvent(this, event, predicate);
waiter.dispose();
return result;
});
}
async storageState(options = {}) {
const state = await this._channel.storageState({ indexedDB: options.indexedDB });
if (options.path) {
await (0, import_fileUtils.mkdirIfNeeded)(this._platform, options.path);
await this._platform.fs().promises.writeFile(options.path, JSON.stringify(state, void 0, 2), "utf8");
}
return state;
}
backgroundPages() {
return [...this._backgroundPages];
}
serviceWorkers() {
return [...this._serviceWorkers];
}
async newCDPSession(page) {
if (!(page instanceof import_page.Page) && !(page instanceof import_frame.Frame))
throw new Error("page: expected Page or Frame");
const result = await this._channel.newCDPSession(page instanceof import_page.Page ? { page: page._channel } : { frame: page._channel });
return import_cdpSession.CDPSession.from(result.session);
}
_onClose() {
this._closingStatus = "closed";
this._browser?._contexts.delete(this);
this._browser?._browserType._contexts.delete(this);
this._browser?._browserType._playwright.selectors._contextsForSelectors.delete(this);
this._disposeHarRouters();
this.tracing._resetStackCounter();
this.emit(import_events.Events.BrowserContext.Close, this);
}
async [Symbol.asyncDispose]() {
await this.close();
}
async close(options = {}) {
if (this._closingStatus !== "none")
return;
this._closeReason = options.reason;
this._closingStatus = "closing";
await this.request.dispose(options);
await this._instrumentation.runBeforeCloseBrowserContext(this);
await this._wrapApiCall(async () => {
for (const [harId, harParams] of this._harRecorders) {
const har = await this._channel.harExport({ harId });
const artifact = import_artifact.Artifact.from(har.artifact);
const isCompressed = harParams.content === "attach" || harParams.path.endsWith(".zip");
const needCompressed = harParams.path.endsWith(".zip");
if (isCompressed && !needCompressed) {
const localUtils = this._connection.localUtils();
if (!localUtils)
throw new Error("Uncompressed har is not supported in thin clients");
await artifact.saveAs(harParams.path + ".tmp");
await localUtils.harUnzip({ zipFile: harParams.path + ".tmp", harFile: harParams.path });
} else {
await artifact.saveAs(harParams.path);
}
await artifact.delete();
}
}, { internal: true });
await this._channel.close(options);
await this._closedPromise;
}
async _enableRecorder(params, eventSink) {
if (eventSink)
this._onRecorderEventSink = eventSink;
await this._channel.enableRecorder(params);
}
async _disableRecorder() {
this._onRecorderEventSink = void 0;
await this._channel.disableRecorder();
}
}
async function prepareStorageState(platform, storageState) {
if (typeof storageState !== "string")
return storageState;
try {
return JSON.parse(await platform.fs().promises.readFile(storageState, "utf8"));
} catch (e) {
(0, import_stackTrace.rewriteErrorMessage)(e, `Error reading storage state from ${storageState}:
` + e.message);
throw e;
}
}
async function prepareBrowserContextParams(platform, options) {
if (options.videoSize && !options.videosPath)
throw new Error(`"videoSize" option requires "videosPath" to be specified`);
if (options.extraHTTPHeaders)
network.validateHeaders(options.extraHTTPHeaders);
const contextParams = {
...options,
viewport: options.viewport === null ? void 0 : options.viewport,
noDefaultViewport: options.viewport === null,
extraHTTPHeaders: options.extraHTTPHeaders ? (0, import_headers.headersObjectToArray)(options.extraHTTPHeaders) : void 0,
storageState: options.storageState ? await prepareStorageState(platform, options.storageState) : void 0,
serviceWorkers: options.serviceWorkers,
colorScheme: options.colorScheme === null ? "no-override" : options.colorScheme,
reducedMotion: options.reducedMotion === null ? "no-override" : options.reducedMotion,
forcedColors: options.forcedColors === null ? "no-override" : options.forcedColors,
contrast: options.contrast === null ? "no-override" : options.contrast,
acceptDownloads: toAcceptDownloadsProtocol(options.acceptDownloads),
clientCertificates: await toClientCertificatesProtocol(platform, options.clientCertificates)
};
if (!contextParams.recordVideo && options.videosPath) {
contextParams.recordVideo = {
dir: options.videosPath,
size: options.videoSize
};
}
if (contextParams.recordVideo && contextParams.recordVideo.dir)
contextParams.recordVideo.dir = platform.path().resolve(contextParams.recordVideo.dir);
return contextParams;
}
function toAcceptDownloadsProtocol(acceptDownloads) {
if (acceptDownloads === void 0)
return void 0;
if (acceptDownloads)
return "accept";
return "deny";
}
async function toClientCertificatesProtocol(platform, certs) {
if (!certs)
return void 0;
const bufferizeContent = async (value, path) => {
if (value)
return value;
if (path)
return await platform.fs().promises.readFile(path);
};
return await Promise.all(certs.map(async (cert) => ({
origin: cert.origin,
cert: await bufferizeContent(cert.cert, cert.certPath),
key: await bufferizeContent(cert.key, cert.keyPath),
pfx: await bufferizeContent(cert.pfx, cert.pfxPath),
passphrase: cert.passphrase
})));
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
BrowserContext,
prepareBrowserContextParams,
toClientCertificatesProtocol
});

184
node_modules/playwright-core/lib/client/browserType.js generated vendored Normal file
View File

@@ -0,0 +1,184 @@
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var browserType_exports = {};
__export(browserType_exports, {
BrowserType: () => BrowserType
});
module.exports = __toCommonJS(browserType_exports);
var import_browser = require("./browser");
var import_browserContext = require("./browserContext");
var import_channelOwner = require("./channelOwner");
var import_clientHelper = require("./clientHelper");
var import_events = require("./events");
var import_assert = require("../utils/isomorphic/assert");
var import_headers = require("../utils/isomorphic/headers");
var import_time = require("../utils/isomorphic/time");
var import_timeoutRunner = require("../utils/isomorphic/timeoutRunner");
var import_webSocket = require("./webSocket");
var import_timeoutSettings = require("./timeoutSettings");
class BrowserType extends import_channelOwner.ChannelOwner {
constructor() {
super(...arguments);
this._contexts = /* @__PURE__ */ new Set();
}
static from(browserType) {
return browserType._object;
}
executablePath() {
if (!this._initializer.executablePath)
throw new Error("Browser is not supported on current platform");
return this._initializer.executablePath;
}
name() {
return this._initializer.name;
}
async launch(options = {}) {
(0, import_assert.assert)(!options.userDataDir, "userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead");
(0, import_assert.assert)(!options.port, "Cannot specify a port without launching as a server.");
const logger = options.logger || this._playwright._defaultLaunchOptions?.logger;
options = { ...this._playwright._defaultLaunchOptions, ...options };
const launchOptions = {
...options,
ignoreDefaultArgs: Array.isArray(options.ignoreDefaultArgs) ? options.ignoreDefaultArgs : void 0,
ignoreAllDefaultArgs: !!options.ignoreDefaultArgs && !Array.isArray(options.ignoreDefaultArgs),
env: options.env ? (0, import_clientHelper.envObjectToArray)(options.env) : void 0,
timeout: new import_timeoutSettings.TimeoutSettings(this._platform).launchTimeout(options)
};
return await this._wrapApiCall(async () => {
const browser = import_browser.Browser.from((await this._channel.launch(launchOptions)).browser);
browser._connectToBrowserType(this, options, logger);
return browser;
});
}
async launchServer(options = {}) {
if (!this._serverLauncher)
throw new Error("Launching server is not supported");
options = { ...this._playwright._defaultLaunchOptions, ...options };
return await this._serverLauncher.launchServer(options);
}
async launchPersistentContext(userDataDir, options = {}) {
const logger = options.logger || this._playwright._defaultLaunchOptions?.logger;
(0, import_assert.assert)(!options.port, "Cannot specify a port without launching as a server.");
options = this._playwright.selectors._withSelectorOptions({
...this._playwright._defaultLaunchOptions,
...this._playwright._defaultContextOptions,
...options
});
const contextParams = await (0, import_browserContext.prepareBrowserContextParams)(this._platform, options);
const persistentParams = {
...contextParams,
ignoreDefaultArgs: Array.isArray(options.ignoreDefaultArgs) ? options.ignoreDefaultArgs : void 0,
ignoreAllDefaultArgs: !!options.ignoreDefaultArgs && !Array.isArray(options.ignoreDefaultArgs),
env: options.env ? (0, import_clientHelper.envObjectToArray)(options.env) : void 0,
channel: options.channel,
userDataDir: this._platform.path().isAbsolute(userDataDir) || !userDataDir ? userDataDir : this._platform.path().resolve(userDataDir),
timeout: new import_timeoutSettings.TimeoutSettings(this._platform).launchTimeout(options)
};
const context = await this._wrapApiCall(async () => {
const result = await this._channel.launchPersistentContext(persistentParams);
const browser = import_browser.Browser.from(result.browser);
browser._connectToBrowserType(this, options, logger);
const context2 = import_browserContext.BrowserContext.from(result.context);
await context2._initializeHarFromOptions(options.recordHar);
return context2;
});
await this._instrumentation.runAfterCreateBrowserContext(context);
return context;
}
async connect(optionsOrWsEndpoint, options) {
if (typeof optionsOrWsEndpoint === "string")
return await this._connect({ ...options, wsEndpoint: optionsOrWsEndpoint });
(0, import_assert.assert)(optionsOrWsEndpoint.wsEndpoint, "options.wsEndpoint is required");
return await this._connect(optionsOrWsEndpoint);
}
async _connect(params) {
const logger = params.logger;
return await this._wrapApiCall(async () => {
const deadline = params.timeout ? (0, import_time.monotonicTime)() + params.timeout : 0;
const headers = { "x-playwright-browser": this.name(), ...params.headers };
const connectParams = {
wsEndpoint: params.wsEndpoint,
headers,
exposeNetwork: params.exposeNetwork ?? params._exposeNetwork,
slowMo: params.slowMo,
timeout: params.timeout || 0
};
if (params.__testHookRedirectPortForwarding)
connectParams.socksProxyRedirectPortForTest = params.__testHookRedirectPortForwarding;
const connection = await (0, import_webSocket.connectOverWebSocket)(this._connection, connectParams);
let browser;
connection.on("close", () => {
for (const context of browser?.contexts() || []) {
for (const page of context.pages())
page._onClose();
context._onClose();
}
setTimeout(() => browser?._didClose(), 0);
});
const result = await (0, import_timeoutRunner.raceAgainstDeadline)(async () => {
if (params.__testHookBeforeCreateBrowser)
await params.__testHookBeforeCreateBrowser();
const playwright = await connection.initializePlaywright();
if (!playwright._initializer.preLaunchedBrowser) {
connection.close();
throw new Error("Malformed endpoint. Did you use BrowserType.launchServer method?");
}
playwright.selectors = this._playwright.selectors;
browser = import_browser.Browser.from(playwright._initializer.preLaunchedBrowser);
browser._connectToBrowserType(this, {}, logger);
browser._shouldCloseConnectionOnClose = true;
browser.on(import_events.Events.Browser.Disconnected, () => connection.close());
return browser;
}, deadline);
if (!result.timedOut) {
return result.result;
} else {
connection.close();
throw new Error(`Timeout ${params.timeout}ms exceeded`);
}
});
}
async connectOverCDP(endpointURLOrOptions, options) {
if (typeof endpointURLOrOptions === "string")
return await this._connectOverCDP(endpointURLOrOptions, options);
const endpointURL = "endpointURL" in endpointURLOrOptions ? endpointURLOrOptions.endpointURL : endpointURLOrOptions.wsEndpoint;
(0, import_assert.assert)(endpointURL, "Cannot connect over CDP without wsEndpoint.");
return await this.connectOverCDP(endpointURL, endpointURLOrOptions);
}
async _connectOverCDP(endpointURL, params = {}) {
if (this.name() !== "chromium")
throw new Error("Connecting over CDP is only supported in Chromium.");
const headers = params.headers ? (0, import_headers.headersObjectToArray)(params.headers) : void 0;
const result = await this._channel.connectOverCDP({
endpointURL,
headers,
slowMo: params.slowMo,
timeout: new import_timeoutSettings.TimeoutSettings(this._platform).timeout(params)
});
const browser = import_browser.Browser.from(result.browser);
browser._connectToBrowserType(this, {}, params.logger);
if (result.defaultContext)
await this._instrumentation.runAfterCreateBrowserContext(import_browserContext.BrowserContext.from(result.defaultContext));
return browser;
}
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
BrowserType
});

51
node_modules/playwright-core/lib/client/cdpSession.js generated vendored Normal file
View File

@@ -0,0 +1,51 @@
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var cdpSession_exports = {};
__export(cdpSession_exports, {
CDPSession: () => CDPSession
});
module.exports = __toCommonJS(cdpSession_exports);
var import_channelOwner = require("./channelOwner");
class CDPSession extends import_channelOwner.ChannelOwner {
static from(cdpSession) {
return cdpSession._object;
}
constructor(parent, type, guid, initializer) {
super(parent, type, guid, initializer);
this._channel.on("event", ({ method, params }) => {
this.emit(method, params);
});
this.on = super.on;
this.addListener = super.addListener;
this.off = super.removeListener;
this.removeListener = super.removeListener;
this.once = super.once;
}
async send(method, params) {
const result = await this._channel.send({ method, params });
return result.result;
}
async detach() {
return await this._channel.detach();
}
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
CDPSession
});

201
node_modules/playwright-core/lib/client/channelOwner.js generated vendored Normal file
View File

@@ -0,0 +1,201 @@
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var channelOwner_exports = {};
__export(channelOwner_exports, {
ChannelOwner: () => ChannelOwner
});
module.exports = __toCommonJS(channelOwner_exports);
var import_eventEmitter = require("./eventEmitter");
var import_validator = require("../protocol/validator");
var import_protocolMetainfo = require("../utils/isomorphic/protocolMetainfo");
var import_clientStackTrace = require("./clientStackTrace");
var import_stackTrace = require("../utils/isomorphic/stackTrace");
class ChannelOwner extends import_eventEmitter.EventEmitter {
constructor(parent, type, guid, initializer) {
const connection = parent instanceof ChannelOwner ? parent._connection : parent;
super(connection._platform);
this._objects = /* @__PURE__ */ new Map();
this._eventToSubscriptionMapping = /* @__PURE__ */ new Map();
this._wasCollected = false;
this.setMaxListeners(0);
this._connection = connection;
this._type = type;
this._guid = guid;
this._parent = parent instanceof ChannelOwner ? parent : void 0;
this._instrumentation = this._connection._instrumentation;
this._connection._objects.set(guid, this);
if (this._parent) {
this._parent._objects.set(guid, this);
this._logger = this._parent._logger;
}
this._channel = this._createChannel(new import_eventEmitter.EventEmitter(connection._platform));
this._initializer = initializer;
}
_setEventToSubscriptionMapping(mapping) {
this._eventToSubscriptionMapping = mapping;
}
_updateSubscription(event, enabled) {
const protocolEvent = this._eventToSubscriptionMapping.get(String(event));
if (protocolEvent)
this._channel.updateSubscription({ event: protocolEvent, enabled }).catch(() => {
});
}
on(event, listener) {
if (!this.listenerCount(event))
this._updateSubscription(event, true);
super.on(event, listener);
return this;
}
addListener(event, listener) {
if (!this.listenerCount(event))
this._updateSubscription(event, true);
super.addListener(event, listener);
return this;
}
prependListener(event, listener) {
if (!this.listenerCount(event))
this._updateSubscription(event, true);
super.prependListener(event, listener);
return this;
}
off(event, listener) {
super.off(event, listener);
if (!this.listenerCount(event))
this._updateSubscription(event, false);
return this;
}
removeListener(event, listener) {
super.removeListener(event, listener);
if (!this.listenerCount(event))
this._updateSubscription(event, false);
return this;
}
_adopt(child) {
child._parent._objects.delete(child._guid);
this._objects.set(child._guid, child);
child._parent = this;
}
_dispose(reason) {
if (this._parent)
this._parent._objects.delete(this._guid);
this._connection._objects.delete(this._guid);
this._wasCollected = reason === "gc";
for (const object of [...this._objects.values()])
object._dispose(reason);
this._objects.clear();
}
_debugScopeState() {
return {
_guid: this._guid,
objects: Array.from(this._objects.values()).map((o) => o._debugScopeState())
};
}
_validatorToWireContext() {
return {
tChannelImpl: tChannelImplToWire,
binary: this._connection.rawBuffers() ? "buffer" : "toBase64",
isUnderTest: () => this._platform.isUnderTest()
};
}
_createChannel(base) {
const channel = new Proxy(base, {
get: (obj, prop) => {
if (typeof prop === "string") {
const validator = (0, import_validator.maybeFindValidator)(this._type, prop, "Params");
const { internal } = import_protocolMetainfo.methodMetainfo.get(this._type + "." + prop) || {};
if (validator) {
return async (params) => {
return await this._wrapApiCall(async (apiZone) => {
const validatedParams = validator(params, "", this._validatorToWireContext());
if (!apiZone.internal && !apiZone.reported) {
apiZone.reported = true;
this._instrumentation.onApiCallBegin(apiZone, { type: this._type, method: prop, params });
logApiCall(this._platform, this._logger, `=> ${apiZone.apiName} started`);
return await this._connection.sendMessageToServer(this, prop, validatedParams, apiZone);
}
return await this._connection.sendMessageToServer(this, prop, validatedParams, { internal: true });
}, { internal });
};
}
}
return obj[prop];
}
});
channel._object = this;
return channel;
}
async _wrapApiCall(func, options) {
const logger = this._logger;
const existingApiZone = this._platform.zones.current().data();
if (existingApiZone)
return await func(existingApiZone);
const stackTrace = (0, import_clientStackTrace.captureLibraryStackTrace)(this._platform);
const apiZone = { title: options?.title, apiName: stackTrace.apiName, frames: stackTrace.frames, internal: options?.internal ?? false, reported: false, userData: void 0, stepId: void 0 };
try {
const result = await this._platform.zones.current().push(apiZone).run(async () => await func(apiZone));
if (!options?.internal) {
logApiCall(this._platform, logger, `<= ${apiZone.apiName} succeeded`);
this._instrumentation.onApiCallEnd(apiZone);
}
return result;
} catch (e) {
const innerError = (this._platform.showInternalStackFrames() || this._platform.isUnderTest()) && e.stack ? "\n<inner error>\n" + e.stack : "";
if (apiZone.apiName && !apiZone.apiName.includes("<anonymous>"))
e.message = apiZone.apiName + ": " + e.message;
const stackFrames = "\n" + (0, import_stackTrace.stringifyStackFrames)(stackTrace.frames).join("\n") + innerError;
if (stackFrames.trim())
e.stack = e.message + stackFrames;
else
e.stack = "";
if (!options?.internal) {
const recoveryHandlers = [];
apiZone.error = e;
this._instrumentation.onApiCallRecovery(apiZone, e, recoveryHandlers);
for (const handler of recoveryHandlers) {
const recoverResult = await handler();
if (recoverResult.status === "recovered")
return recoverResult.value;
}
logApiCall(this._platform, logger, `<= ${apiZone.apiName} failed`);
this._instrumentation.onApiCallEnd(apiZone);
}
throw e;
}
}
toJSON() {
return {
_type: this._type,
_guid: this._guid
};
}
}
function logApiCall(platform, logger, message) {
if (logger && logger.isEnabled("api", "info"))
logger.log("api", "info", message, [], { color: "cyan" });
platform.log("api", message);
}
function tChannelImplToWire(names, arg, path, context) {
if (arg._object instanceof ChannelOwner && (names === "*" || names.includes(arg._object._type)))
return { guid: arg._object._guid };
throw new import_validator.ValidationError(`${path}: expected channel ${names.toString()}`);
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
ChannelOwner
});

View File

@@ -0,0 +1,64 @@
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var clientHelper_exports = {};
__export(clientHelper_exports, {
addSourceUrlToScript: () => addSourceUrlToScript,
envObjectToArray: () => envObjectToArray,
evaluationScript: () => evaluationScript
});
module.exports = __toCommonJS(clientHelper_exports);
var import_rtti = require("../utils/isomorphic/rtti");
function envObjectToArray(env) {
const result = [];
for (const name in env) {
if (!Object.is(env[name], void 0))
result.push({ name, value: String(env[name]) });
}
return result;
}
async function evaluationScript(platform, fun, arg, addSourceUrl = true) {
if (typeof fun === "function") {
const source = fun.toString();
const argString = Object.is(arg, void 0) ? "undefined" : JSON.stringify(arg);
return `(${source})(${argString})`;
}
if (arg !== void 0)
throw new Error("Cannot evaluate a string with arguments");
if ((0, import_rtti.isString)(fun))
return fun;
if (fun.content !== void 0)
return fun.content;
if (fun.path !== void 0) {
let source = await platform.fs().promises.readFile(fun.path, "utf8");
if (addSourceUrl)
source = addSourceUrlToScript(source, fun.path);
return source;
}
throw new Error("Either path or content property must be present");
}
function addSourceUrlToScript(source, path) {
return `${source}
//# sourceURL=${path.replace(/\n/g, "")}`;
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
addSourceUrlToScript,
envObjectToArray,
evaluationScript
});

View File

@@ -0,0 +1,55 @@
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var clientInstrumentation_exports = {};
__export(clientInstrumentation_exports, {
createInstrumentation: () => createInstrumentation
});
module.exports = __toCommonJS(clientInstrumentation_exports);
function createInstrumentation() {
const listeners = [];
return new Proxy({}, {
get: (obj, prop) => {
if (typeof prop !== "string")
return obj[prop];
if (prop === "addListener")
return (listener) => listeners.push(listener);
if (prop === "removeListener")
return (listener) => listeners.splice(listeners.indexOf(listener), 1);
if (prop === "removeAllListeners")
return () => listeners.splice(0, listeners.length);
if (prop.startsWith("run")) {
return async (...params) => {
for (const listener of listeners)
await listener[prop]?.(...params);
};
}
if (prop.startsWith("on")) {
return (...params) => {
for (const listener of listeners)
listener[prop]?.(...params);
};
}
return obj[prop];
}
});
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
createInstrumentation
});

View File

@@ -0,0 +1,69 @@
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var clientStackTrace_exports = {};
__export(clientStackTrace_exports, {
captureLibraryStackTrace: () => captureLibraryStackTrace
});
module.exports = __toCommonJS(clientStackTrace_exports);
var import_stackTrace = require("../utils/isomorphic/stackTrace");
function captureLibraryStackTrace(platform) {
const stack = (0, import_stackTrace.captureRawStack)();
let parsedFrames = stack.map((line) => {
const frame = (0, import_stackTrace.parseStackFrame)(line, platform.pathSeparator, platform.showInternalStackFrames());
if (!frame || !frame.file)
return null;
const isPlaywrightLibrary = !!platform.coreDir && frame.file.startsWith(platform.coreDir);
const parsed = {
frame,
frameText: line,
isPlaywrightLibrary
};
return parsed;
}).filter(Boolean);
let apiName = "";
for (let i = 0; i < parsedFrames.length - 1; i++) {
const parsedFrame = parsedFrames[i];
if (parsedFrame.isPlaywrightLibrary && !parsedFrames[i + 1].isPlaywrightLibrary) {
apiName = apiName || normalizeAPIName(parsedFrame.frame.function);
break;
}
}
function normalizeAPIName(name) {
if (!name)
return "";
const match = name.match(/(API|JS|CDP|[A-Z])(.*)/);
if (!match)
return name;
return match[1].toLowerCase() + match[2];
}
const filterPrefixes = platform.boxedStackPrefixes();
parsedFrames = parsedFrames.filter((f) => {
if (filterPrefixes.some((prefix) => f.frame.file.startsWith(prefix)))
return false;
return true;
});
return {
frames: parsedFrames.map((p) => p.frame),
apiName
};
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
captureLibraryStackTrace
});

68
node_modules/playwright-core/lib/client/clock.js generated vendored Normal file
View File

@@ -0,0 +1,68 @@
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var clock_exports = {};
__export(clock_exports, {
Clock: () => Clock
});
module.exports = __toCommonJS(clock_exports);
class Clock {
constructor(browserContext) {
this._browserContext = browserContext;
}
async install(options = {}) {
await this._browserContext._channel.clockInstall(options.time !== void 0 ? parseTime(options.time) : {});
}
async fastForward(ticks) {
await this._browserContext._channel.clockFastForward(parseTicks(ticks));
}
async pauseAt(time) {
await this._browserContext._channel.clockPauseAt(parseTime(time));
}
async resume() {
await this._browserContext._channel.clockResume({});
}
async runFor(ticks) {
await this._browserContext._channel.clockRunFor(parseTicks(ticks));
}
async setFixedTime(time) {
await this._browserContext._channel.clockSetFixedTime(parseTime(time));
}
async setSystemTime(time) {
await this._browserContext._channel.clockSetSystemTime(parseTime(time));
}
}
function parseTime(time) {
if (typeof time === "number")
return { timeNumber: time };
if (typeof time === "string")
return { timeString: time };
if (!isFinite(time.getTime()))
throw new Error(`Invalid date: ${time}`);
return { timeNumber: time.getTime() };
}
function parseTicks(ticks) {
return {
ticksNumber: typeof ticks === "number" ? ticks : void 0,
ticksString: typeof ticks === "string" ? ticks : void 0
};
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
Clock
});

314
node_modules/playwright-core/lib/client/connection.js generated vendored Normal file
View File

@@ -0,0 +1,314 @@
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var connection_exports = {};
__export(connection_exports, {
Connection: () => Connection
});
module.exports = __toCommonJS(connection_exports);
var import_eventEmitter = require("./eventEmitter");
var import_android = require("./android");
var import_artifact = require("./artifact");
var import_browser = require("./browser");
var import_browserContext = require("./browserContext");
var import_browserType = require("./browserType");
var import_cdpSession = require("./cdpSession");
var import_channelOwner = require("./channelOwner");
var import_clientInstrumentation = require("./clientInstrumentation");
var import_dialog = require("./dialog");
var import_electron = require("./electron");
var import_elementHandle = require("./elementHandle");
var import_errors = require("./errors");
var import_fetch = require("./fetch");
var import_frame = require("./frame");
var import_jsHandle = require("./jsHandle");
var import_jsonPipe = require("./jsonPipe");
var import_localUtils = require("./localUtils");
var import_network = require("./network");
var import_page = require("./page");
var import_playwright = require("./playwright");
var import_stream = require("./stream");
var import_tracing = require("./tracing");
var import_worker = require("./worker");
var import_writableStream = require("./writableStream");
var import_validator = require("../protocol/validator");
var import_stackTrace = require("../utils/isomorphic/stackTrace");
class Root extends import_channelOwner.ChannelOwner {
constructor(connection) {
super(connection, "Root", "", {});
}
async initialize() {
return import_playwright.Playwright.from((await this._channel.initialize({
sdkLanguage: "javascript"
})).playwright);
}
}
class DummyChannelOwner extends import_channelOwner.ChannelOwner {
}
class Connection extends import_eventEmitter.EventEmitter {
constructor(platform, localUtils, instrumentation, headers = []) {
super(platform);
this._objects = /* @__PURE__ */ new Map();
this.onmessage = (message) => {
};
this._lastId = 0;
this._callbacks = /* @__PURE__ */ new Map();
this._isRemote = false;
this._rawBuffers = false;
this._tracingCount = 0;
this._instrumentation = instrumentation || (0, import_clientInstrumentation.createInstrumentation)();
this._localUtils = localUtils;
this._rootObject = new Root(this);
this.headers = headers;
}
markAsRemote() {
this._isRemote = true;
}
isRemote() {
return this._isRemote;
}
useRawBuffers() {
this._rawBuffers = true;
}
rawBuffers() {
return this._rawBuffers;
}
localUtils() {
return this._localUtils;
}
async initializePlaywright() {
return await this._rootObject.initialize();
}
getObjectWithKnownName(guid) {
return this._objects.get(guid);
}
setIsTracing(isTracing) {
if (isTracing)
this._tracingCount++;
else
this._tracingCount--;
}
async sendMessageToServer(object, method, params, options) {
if (this._closedError)
throw this._closedError;
if (object._wasCollected)
throw new Error("The object has been collected to prevent unbounded heap growth.");
const guid = object._guid;
const type = object._type;
const id = ++this._lastId;
const message = { id, guid, method, params };
if (this._platform.isLogEnabled("channel")) {
this._platform.log("channel", "SEND> " + JSON.stringify(message));
}
const location = options.frames?.[0] ? { file: options.frames[0].file, line: options.frames[0].line, column: options.frames[0].column } : void 0;
const metadata = { title: options.title, location, internal: options.internal, stepId: options.stepId };
if (this._tracingCount && options.frames && type !== "LocalUtils")
this._localUtils?.addStackToTracingNoReply({ callData: { stack: options.frames ?? [], id } }).catch(() => {
});
this._platform.zones.empty.run(() => this.onmessage({ ...message, metadata }));
return await new Promise((resolve, reject) => this._callbacks.set(id, { resolve, reject, title: options.title, type, method }));
}
_validatorFromWireContext() {
return {
tChannelImpl: this._tChannelImplFromWire.bind(this),
binary: this._rawBuffers ? "buffer" : "fromBase64",
isUnderTest: () => this._platform.isUnderTest()
};
}
dispatch(message) {
if (this._closedError)
return;
const { id, guid, method, params, result, error, log } = message;
if (id) {
if (this._platform.isLogEnabled("channel"))
this._platform.log("channel", "<RECV " + JSON.stringify(message));
const callback = this._callbacks.get(id);
if (!callback)
throw new Error(`Cannot find command to respond: ${id}`);
this._callbacks.delete(id);
if (error && !result) {
const parsedError = (0, import_errors.parseError)(error);
(0, import_stackTrace.rewriteErrorMessage)(parsedError, parsedError.message + formatCallLog(this._platform, log));
callback.reject(parsedError);
} else {
const validator2 = (0, import_validator.findValidator)(callback.type, callback.method, "Result");
callback.resolve(validator2(result, "", this._validatorFromWireContext()));
}
return;
}
if (this._platform.isLogEnabled("channel"))
this._platform.log("channel", "<EVENT " + JSON.stringify(message));
if (method === "__create__") {
this._createRemoteObject(guid, params.type, params.guid, params.initializer);
return;
}
const object = this._objects.get(guid);
if (!object)
throw new Error(`Cannot find object to "${method}": ${guid}`);
if (method === "__adopt__") {
const child = this._objects.get(params.guid);
if (!child)
throw new Error(`Unknown new child: ${params.guid}`);
object._adopt(child);
return;
}
if (method === "__dispose__") {
object._dispose(params.reason);
return;
}
const validator = (0, import_validator.findValidator)(object._type, method, "Event");
object._channel.emit(method, validator(params, "", this._validatorFromWireContext()));
}
close(cause) {
if (this._closedError)
return;
this._closedError = new import_errors.TargetClosedError(cause);
for (const callback of this._callbacks.values())
callback.reject(this._closedError);
this._callbacks.clear();
this.emit("close");
}
_tChannelImplFromWire(names, arg, path, context) {
if (arg && typeof arg === "object" && typeof arg.guid === "string") {
const object = this._objects.get(arg.guid);
if (!object)
throw new Error(`Object with guid ${arg.guid} was not bound in the connection`);
if (names !== "*" && !names.includes(object._type))
throw new import_validator.ValidationError(`${path}: expected channel ${names.toString()}`);
return object._channel;
}
throw new import_validator.ValidationError(`${path}: expected channel ${names.toString()}`);
}
_createRemoteObject(parentGuid, type, guid, initializer) {
const parent = this._objects.get(parentGuid);
if (!parent)
throw new Error(`Cannot find parent object ${parentGuid} to create ${guid}`);
let result;
const validator = (0, import_validator.findValidator)(type, "", "Initializer");
initializer = validator(initializer, "", this._validatorFromWireContext());
switch (type) {
case "Android":
result = new import_android.Android(parent, type, guid, initializer);
break;
case "AndroidSocket":
result = new import_android.AndroidSocket(parent, type, guid, initializer);
break;
case "AndroidDevice":
result = new import_android.AndroidDevice(parent, type, guid, initializer);
break;
case "APIRequestContext":
result = new import_fetch.APIRequestContext(parent, type, guid, initializer);
break;
case "Artifact":
result = new import_artifact.Artifact(parent, type, guid, initializer);
break;
case "BindingCall":
result = new import_page.BindingCall(parent, type, guid, initializer);
break;
case "Browser":
result = new import_browser.Browser(parent, type, guid, initializer);
break;
case "BrowserContext":
result = new import_browserContext.BrowserContext(parent, type, guid, initializer);
break;
case "BrowserType":
result = new import_browserType.BrowserType(parent, type, guid, initializer);
break;
case "CDPSession":
result = new import_cdpSession.CDPSession(parent, type, guid, initializer);
break;
case "Dialog":
result = new import_dialog.Dialog(parent, type, guid, initializer);
break;
case "Electron":
result = new import_electron.Electron(parent, type, guid, initializer);
break;
case "ElectronApplication":
result = new import_electron.ElectronApplication(parent, type, guid, initializer);
break;
case "ElementHandle":
result = new import_elementHandle.ElementHandle(parent, type, guid, initializer);
break;
case "Frame":
result = new import_frame.Frame(parent, type, guid, initializer);
break;
case "JSHandle":
result = new import_jsHandle.JSHandle(parent, type, guid, initializer);
break;
case "JsonPipe":
result = new import_jsonPipe.JsonPipe(parent, type, guid, initializer);
break;
case "LocalUtils":
result = new import_localUtils.LocalUtils(parent, type, guid, initializer);
if (!this._localUtils)
this._localUtils = result;
break;
case "Page":
result = new import_page.Page(parent, type, guid, initializer);
break;
case "Playwright":
result = new import_playwright.Playwright(parent, type, guid, initializer);
break;
case "Request":
result = new import_network.Request(parent, type, guid, initializer);
break;
case "Response":
result = new import_network.Response(parent, type, guid, initializer);
break;
case "Route":
result = new import_network.Route(parent, type, guid, initializer);
break;
case "Stream":
result = new import_stream.Stream(parent, type, guid, initializer);
break;
case "SocksSupport":
result = new DummyChannelOwner(parent, type, guid, initializer);
break;
case "Tracing":
result = new import_tracing.Tracing(parent, type, guid, initializer);
break;
case "WebSocket":
result = new import_network.WebSocket(parent, type, guid, initializer);
break;
case "WebSocketRoute":
result = new import_network.WebSocketRoute(parent, type, guid, initializer);
break;
case "Worker":
result = new import_worker.Worker(parent, type, guid, initializer);
break;
case "WritableStream":
result = new import_writableStream.WritableStream(parent, type, guid, initializer);
break;
default:
throw new Error("Missing type " + type);
}
return result;
}
}
function formatCallLog(platform, log) {
if (!log || !log.some((l) => !!l))
return "";
return `
Call log:
${platform.colors.dim(log.join("\n"))}
`;
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
Connection
});

View File

@@ -0,0 +1,55 @@
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var consoleMessage_exports = {};
__export(consoleMessage_exports, {
ConsoleMessage: () => ConsoleMessage
});
module.exports = __toCommonJS(consoleMessage_exports);
var import_jsHandle = require("./jsHandle");
var import_page = require("./page");
class ConsoleMessage {
constructor(platform, event) {
this._page = "page" in event && event.page ? import_page.Page.from(event.page) : null;
this._event = event;
if (platform.inspectCustom)
this[platform.inspectCustom] = () => this._inspect();
}
page() {
return this._page;
}
type() {
return this._event.type;
}
text() {
return this._event.text;
}
args() {
return this._event.args.map(import_jsHandle.JSHandle.from);
}
location() {
return this._event.location;
}
_inspect() {
return this.text();
}
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
ConsoleMessage
});

44
node_modules/playwright-core/lib/client/coverage.js generated vendored Normal file
View File

@@ -0,0 +1,44 @@
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var coverage_exports = {};
__export(coverage_exports, {
Coverage: () => Coverage
});
module.exports = __toCommonJS(coverage_exports);
class Coverage {
constructor(channel) {
this._channel = channel;
}
async startJSCoverage(options = {}) {
await this._channel.startJSCoverage(options);
}
async stopJSCoverage() {
return (await this._channel.stopJSCoverage()).entries;
}
async startCSSCoverage(options = {}) {
await this._channel.startCSSCoverage(options);
}
async stopCSSCoverage() {
return (await this._channel.stopCSSCoverage()).entries;
}
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
Coverage
});

56
node_modules/playwright-core/lib/client/dialog.js generated vendored Normal file
View File

@@ -0,0 +1,56 @@
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var dialog_exports = {};
__export(dialog_exports, {
Dialog: () => Dialog
});
module.exports = __toCommonJS(dialog_exports);
var import_channelOwner = require("./channelOwner");
var import_page = require("./page");
class Dialog extends import_channelOwner.ChannelOwner {
static from(dialog) {
return dialog._object;
}
constructor(parent, type, guid, initializer) {
super(parent, type, guid, initializer);
this._page = import_page.Page.fromNullable(initializer.page);
}
page() {
return this._page;
}
type() {
return this._initializer.type;
}
message() {
return this._initializer.message;
}
defaultValue() {
return this._initializer.defaultValue;
}
async accept(promptText) {
await this._channel.accept({ promptText });
}
async dismiss() {
await this._channel.dismiss();
}
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
Dialog
});

62
node_modules/playwright-core/lib/client/download.js generated vendored Normal file
View File

@@ -0,0 +1,62 @@
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var download_exports = {};
__export(download_exports, {
Download: () => Download
});
module.exports = __toCommonJS(download_exports);
class Download {
constructor(page, url, suggestedFilename, artifact) {
this._page = page;
this._url = url;
this._suggestedFilename = suggestedFilename;
this._artifact = artifact;
}
page() {
return this._page;
}
url() {
return this._url;
}
suggestedFilename() {
return this._suggestedFilename;
}
async path() {
return await this._artifact.pathAfterFinished();
}
async saveAs(path) {
return await this._artifact.saveAs(path);
}
async failure() {
return await this._artifact.failure();
}
async createReadStream() {
return await this._artifact.createReadStream();
}
async cancel() {
return await this._artifact.cancel();
}
async delete() {
return await this._artifact.delete();
}
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
Download
});

138
node_modules/playwright-core/lib/client/electron.js generated vendored Normal file
View File

@@ -0,0 +1,138 @@
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var electron_exports = {};
__export(electron_exports, {
Electron: () => Electron,
ElectronApplication: () => ElectronApplication
});
module.exports = __toCommonJS(electron_exports);
var import_browserContext = require("./browserContext");
var import_channelOwner = require("./channelOwner");
var import_clientHelper = require("./clientHelper");
var import_consoleMessage = require("./consoleMessage");
var import_errors = require("./errors");
var import_events = require("./events");
var import_jsHandle = require("./jsHandle");
var import_waiter = require("./waiter");
var import_timeoutSettings = require("./timeoutSettings");
class Electron extends import_channelOwner.ChannelOwner {
static from(electron) {
return electron._object;
}
constructor(parent, type, guid, initializer) {
super(parent, type, guid, initializer);
}
async launch(options = {}) {
options = this._playwright.selectors._withSelectorOptions(options);
const params = {
...await (0, import_browserContext.prepareBrowserContextParams)(this._platform, options),
env: (0, import_clientHelper.envObjectToArray)(options.env ? options.env : this._platform.env),
tracesDir: options.tracesDir,
timeout: new import_timeoutSettings.TimeoutSettings(this._platform).launchTimeout(options)
};
const app = ElectronApplication.from((await this._channel.launch(params)).electronApplication);
this._playwright.selectors._contextsForSelectors.add(app._context);
app.once(import_events.Events.ElectronApplication.Close, () => this._playwright.selectors._contextsForSelectors.delete(app._context));
await app._context._initializeHarFromOptions(options.recordHar);
app._context.tracing._tracesDir = options.tracesDir;
return app;
}
}
class ElectronApplication extends import_channelOwner.ChannelOwner {
constructor(parent, type, guid, initializer) {
super(parent, type, guid, initializer);
this._windows = /* @__PURE__ */ new Set();
this._timeoutSettings = new import_timeoutSettings.TimeoutSettings(this._platform);
this._context = import_browserContext.BrowserContext.from(initializer.context);
for (const page of this._context._pages)
this._onPage(page);
this._context.on(import_events.Events.BrowserContext.Page, (page) => this._onPage(page));
this._channel.on("close", () => {
this.emit(import_events.Events.ElectronApplication.Close);
});
this._channel.on("console", (event) => this.emit(import_events.Events.ElectronApplication.Console, new import_consoleMessage.ConsoleMessage(this._platform, event)));
this._setEventToSubscriptionMapping(/* @__PURE__ */ new Map([
[import_events.Events.ElectronApplication.Console, "console"]
]));
}
static from(electronApplication) {
return electronApplication._object;
}
process() {
return this._connection.toImpl?.(this)?.process();
}
_onPage(page) {
this._windows.add(page);
this.emit(import_events.Events.ElectronApplication.Window, page);
page.once(import_events.Events.Page.Close, () => this._windows.delete(page));
}
windows() {
return [...this._windows];
}
async firstWindow(options) {
if (this._windows.size)
return this._windows.values().next().value;
return await this.waitForEvent("window", options);
}
context() {
return this._context;
}
async [Symbol.asyncDispose]() {
await this.close();
}
async close() {
try {
await this._context.close();
} catch (e) {
if ((0, import_errors.isTargetClosedError)(e))
return;
throw e;
}
}
async waitForEvent(event, optionsOrPredicate = {}) {
return await this._wrapApiCall(async () => {
const timeout = this._timeoutSettings.timeout(typeof optionsOrPredicate === "function" ? {} : optionsOrPredicate);
const predicate = typeof optionsOrPredicate === "function" ? optionsOrPredicate : optionsOrPredicate.predicate;
const waiter = import_waiter.Waiter.createForEvent(this, event);
waiter.rejectOnTimeout(timeout, `Timeout ${timeout}ms exceeded while waiting for event "${event}"`);
if (event !== import_events.Events.ElectronApplication.Close)
waiter.rejectOnEvent(this, import_events.Events.ElectronApplication.Close, () => new import_errors.TargetClosedError());
const result = await waiter.waitForEvent(this, event, predicate);
waiter.dispose();
return result;
});
}
async browserWindow(page) {
const result = await this._channel.browserWindow({ page: page._channel });
return import_jsHandle.JSHandle.from(result.handle);
}
async evaluate(pageFunction, arg) {
const result = await this._channel.evaluateExpression({ expression: String(pageFunction), isFunction: typeof pageFunction === "function", arg: (0, import_jsHandle.serializeArgument)(arg) });
return (0, import_jsHandle.parseResult)(result.value);
}
async evaluateHandle(pageFunction, arg) {
const result = await this._channel.evaluateExpressionHandle({ expression: String(pageFunction), isFunction: typeof pageFunction === "function", arg: (0, import_jsHandle.serializeArgument)(arg) });
return import_jsHandle.JSHandle.from(result.handle);
}
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
Electron,
ElectronApplication
});

View File

@@ -0,0 +1,281 @@
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var elementHandle_exports = {};
__export(elementHandle_exports, {
ElementHandle: () => ElementHandle,
convertInputFiles: () => convertInputFiles,
convertSelectOptionValues: () => convertSelectOptionValues,
determineScreenshotType: () => determineScreenshotType
});
module.exports = __toCommonJS(elementHandle_exports);
var import_frame = require("./frame");
var import_jsHandle = require("./jsHandle");
var import_assert = require("../utils/isomorphic/assert");
var import_fileUtils = require("./fileUtils");
var import_rtti = require("../utils/isomorphic/rtti");
var import_writableStream = require("./writableStream");
var import_mimeType = require("../utils/isomorphic/mimeType");
class ElementHandle extends import_jsHandle.JSHandle {
static from(handle) {
return handle._object;
}
static fromNullable(handle) {
return handle ? ElementHandle.from(handle) : null;
}
constructor(parent, type, guid, initializer) {
super(parent, type, guid, initializer);
this._frame = parent;
this._elementChannel = this._channel;
}
asElement() {
return this;
}
async ownerFrame() {
return import_frame.Frame.fromNullable((await this._elementChannel.ownerFrame()).frame);
}
async contentFrame() {
return import_frame.Frame.fromNullable((await this._elementChannel.contentFrame()).frame);
}
async getAttribute(name) {
const value = (await this._elementChannel.getAttribute({ name })).value;
return value === void 0 ? null : value;
}
async inputValue() {
return (await this._elementChannel.inputValue()).value;
}
async textContent() {
const value = (await this._elementChannel.textContent()).value;
return value === void 0 ? null : value;
}
async innerText() {
return (await this._elementChannel.innerText()).value;
}
async innerHTML() {
return (await this._elementChannel.innerHTML()).value;
}
async isChecked() {
return (await this._elementChannel.isChecked()).value;
}
async isDisabled() {
return (await this._elementChannel.isDisabled()).value;
}
async isEditable() {
return (await this._elementChannel.isEditable()).value;
}
async isEnabled() {
return (await this._elementChannel.isEnabled()).value;
}
async isHidden() {
return (await this._elementChannel.isHidden()).value;
}
async isVisible() {
return (await this._elementChannel.isVisible()).value;
}
async dispatchEvent(type, eventInit = {}) {
await this._elementChannel.dispatchEvent({ type, eventInit: (0, import_jsHandle.serializeArgument)(eventInit) });
}
async scrollIntoViewIfNeeded(options = {}) {
await this._elementChannel.scrollIntoViewIfNeeded({ ...options, timeout: this._frame._timeout(options) });
}
async hover(options = {}) {
await this._elementChannel.hover({ ...options, timeout: this._frame._timeout(options) });
}
async click(options = {}) {
return await this._elementChannel.click({ ...options, timeout: this._frame._timeout(options) });
}
async dblclick(options = {}) {
return await this._elementChannel.dblclick({ ...options, timeout: this._frame._timeout(options) });
}
async tap(options = {}) {
return await this._elementChannel.tap({ ...options, timeout: this._frame._timeout(options) });
}
async selectOption(values, options = {}) {
const result = await this._elementChannel.selectOption({ ...convertSelectOptionValues(values), ...options, timeout: this._frame._timeout(options) });
return result.values;
}
async fill(value, options = {}) {
return await this._elementChannel.fill({ value, ...options, timeout: this._frame._timeout(options) });
}
async selectText(options = {}) {
await this._elementChannel.selectText({ ...options, timeout: this._frame._timeout(options) });
}
async setInputFiles(files, options = {}) {
const frame = await this.ownerFrame();
if (!frame)
throw new Error("Cannot set input files to detached element");
const converted = await convertInputFiles(this._platform, files, frame.page().context());
await this._elementChannel.setInputFiles({ ...converted, ...options, timeout: this._frame._timeout(options) });
}
async focus() {
await this._elementChannel.focus();
}
async type(text, options = {}) {
await this._elementChannel.type({ text, ...options, timeout: this._frame._timeout(options) });
}
async press(key, options = {}) {
await this._elementChannel.press({ key, ...options, timeout: this._frame._timeout(options) });
}
async check(options = {}) {
return await this._elementChannel.check({ ...options, timeout: this._frame._timeout(options) });
}
async uncheck(options = {}) {
return await this._elementChannel.uncheck({ ...options, timeout: this._frame._timeout(options) });
}
async setChecked(checked, options) {
if (checked)
await this.check(options);
else
await this.uncheck(options);
}
async boundingBox() {
const value = (await this._elementChannel.boundingBox()).value;
return value === void 0 ? null : value;
}
async screenshot(options = {}) {
const mask = options.mask;
const copy = { ...options, mask: void 0, timeout: this._frame._timeout(options) };
if (!copy.type)
copy.type = determineScreenshotType(options);
if (mask) {
copy.mask = mask.map((locator) => ({
frame: locator._frame._channel,
selector: locator._selector
}));
}
const result = await this._elementChannel.screenshot(copy);
if (options.path) {
await (0, import_fileUtils.mkdirIfNeeded)(this._platform, options.path);
await this._platform.fs().promises.writeFile(options.path, result.binary);
}
return result.binary;
}
async $(selector) {
return ElementHandle.fromNullable((await this._elementChannel.querySelector({ selector })).element);
}
async $$(selector) {
const result = await this._elementChannel.querySelectorAll({ selector });
return result.elements.map((h) => ElementHandle.from(h));
}
async $eval(selector, pageFunction, arg) {
const result = await this._elementChannel.evalOnSelector({ selector, expression: String(pageFunction), isFunction: typeof pageFunction === "function", arg: (0, import_jsHandle.serializeArgument)(arg) });
return (0, import_jsHandle.parseResult)(result.value);
}
async $$eval(selector, pageFunction, arg) {
const result = await this._elementChannel.evalOnSelectorAll({ selector, expression: String(pageFunction), isFunction: typeof pageFunction === "function", arg: (0, import_jsHandle.serializeArgument)(arg) });
return (0, import_jsHandle.parseResult)(result.value);
}
async waitForElementState(state, options = {}) {
return await this._elementChannel.waitForElementState({ state, ...options, timeout: this._frame._timeout(options) });
}
async waitForSelector(selector, options = {}) {
const result = await this._elementChannel.waitForSelector({ selector, ...options, timeout: this._frame._timeout(options) });
return ElementHandle.fromNullable(result.element);
}
}
function convertSelectOptionValues(values) {
if (values === null)
return {};
if (!Array.isArray(values))
values = [values];
if (!values.length)
return {};
for (let i = 0; i < values.length; i++)
(0, import_assert.assert)(values[i] !== null, `options[${i}]: expected object, got null`);
if (values[0] instanceof ElementHandle)
return { elements: values.map((v) => v._elementChannel) };
if ((0, import_rtti.isString)(values[0]))
return { options: values.map((valueOrLabel) => ({ valueOrLabel })) };
return { options: values };
}
function filePayloadExceedsSizeLimit(payloads) {
return payloads.reduce((size, item) => size + (item.buffer ? item.buffer.byteLength : 0), 0) >= import_fileUtils.fileUploadSizeLimit;
}
async function resolvePathsAndDirectoryForInputFiles(platform, items) {
let localPaths;
let localDirectory;
for (const item of items) {
const stat = await platform.fs().promises.stat(item);
if (stat.isDirectory()) {
if (localDirectory)
throw new Error("Multiple directories are not supported");
localDirectory = platform.path().resolve(item);
} else {
localPaths ??= [];
localPaths.push(platform.path().resolve(item));
}
}
if (localPaths?.length && localDirectory)
throw new Error("File paths must be all files or a single directory");
return [localPaths, localDirectory];
}
async function convertInputFiles(platform, files, context) {
const items = Array.isArray(files) ? files.slice() : [files];
if (items.some((item) => typeof item === "string")) {
if (!items.every((item) => typeof item === "string"))
throw new Error("File paths cannot be mixed with buffers");
const [localPaths, localDirectory] = await resolvePathsAndDirectoryForInputFiles(platform, items);
if (context._connection.isRemote()) {
const files2 = localDirectory ? (await platform.fs().promises.readdir(localDirectory, { withFileTypes: true, recursive: true })).filter((f) => f.isFile()).map((f) => platform.path().join(f.path, f.name)) : localPaths;
const { writableStreams, rootDir } = await context._wrapApiCall(async () => context._channel.createTempFiles({
rootDirName: localDirectory ? platform.path().basename(localDirectory) : void 0,
items: await Promise.all(files2.map(async (file) => {
const lastModifiedMs = (await platform.fs().promises.stat(file)).mtimeMs;
return {
name: localDirectory ? platform.path().relative(localDirectory, file) : platform.path().basename(file),
lastModifiedMs
};
}))
}), { internal: true });
for (let i = 0; i < files2.length; i++) {
const writable = import_writableStream.WritableStream.from(writableStreams[i]);
await platform.streamFile(files2[i], writable.stream());
}
return {
directoryStream: rootDir,
streams: localDirectory ? void 0 : writableStreams
};
}
return {
localPaths,
localDirectory
};
}
const payloads = items;
if (filePayloadExceedsSizeLimit(payloads))
throw new Error("Cannot set buffer larger than 50Mb, please write it to a file and pass its path instead.");
return { payloads };
}
function determineScreenshotType(options) {
if (options.path) {
const mimeType = (0, import_mimeType.getMimeTypeForPath)(options.path);
if (mimeType === "image/png")
return "png";
else if (mimeType === "image/jpeg")
return "jpeg";
throw new Error(`path: unsupported mime type "${mimeType}"`);
}
return options.type;
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
ElementHandle,
convertInputFiles,
convertSelectOptionValues,
determineScreenshotType
});

77
node_modules/playwright-core/lib/client/errors.js generated vendored Normal file
View File

@@ -0,0 +1,77 @@
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var errors_exports = {};
__export(errors_exports, {
TargetClosedError: () => TargetClosedError,
TimeoutError: () => TimeoutError,
isTargetClosedError: () => isTargetClosedError,
parseError: () => parseError,
serializeError: () => serializeError
});
module.exports = __toCommonJS(errors_exports);
var import_serializers = require("../protocol/serializers");
var import_rtti = require("../utils/isomorphic/rtti");
class TimeoutError extends Error {
constructor(message) {
super(message);
this.name = "TimeoutError";
}
}
class TargetClosedError extends Error {
constructor(cause) {
super(cause || "Target page, context or browser has been closed");
}
}
function isTargetClosedError(error) {
return error instanceof TargetClosedError;
}
function serializeError(e) {
if ((0, import_rtti.isError)(e))
return { error: { message: e.message, stack: e.stack, name: e.name } };
return { value: (0, import_serializers.serializeValue)(e, (value) => ({ fallThrough: value })) };
}
function parseError(error) {
if (!error.error) {
if (error.value === void 0)
throw new Error("Serialized error must have either an error or a value");
return (0, import_serializers.parseSerializedValue)(error.value, void 0);
}
if (error.error.name === "TimeoutError") {
const e2 = new TimeoutError(error.error.message);
e2.stack = error.error.stack || "";
return e2;
}
if (error.error.name === "TargetClosedError") {
const e2 = new TargetClosedError(error.error.message);
e2.stack = error.error.stack || "";
return e2;
}
const e = new Error(error.error.message);
e.stack = error.error.stack || "";
e.name = error.error.name;
return e;
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
TargetClosedError,
TimeoutError,
isTargetClosedError,
parseError,
serializeError
});

314
node_modules/playwright-core/lib/client/eventEmitter.js generated vendored Normal file
View File

@@ -0,0 +1,314 @@
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var eventEmitter_exports = {};
__export(eventEmitter_exports, {
EventEmitter: () => EventEmitter
});
module.exports = __toCommonJS(eventEmitter_exports);
class EventEmitter {
constructor(platform) {
this._events = void 0;
this._eventsCount = 0;
this._maxListeners = void 0;
this._pendingHandlers = /* @__PURE__ */ new Map();
this._platform = platform;
if (this._events === void 0 || this._events === Object.getPrototypeOf(this)._events) {
this._events = /* @__PURE__ */ Object.create(null);
this._eventsCount = 0;
}
this._maxListeners = this._maxListeners || void 0;
this.on = this.addListener;
this.off = this.removeListener;
}
setMaxListeners(n) {
if (typeof n !== "number" || n < 0 || Number.isNaN(n))
throw new RangeError('The value of "n" is out of range. It must be a non-negative number. Received ' + n + ".");
this._maxListeners = n;
return this;
}
getMaxListeners() {
return this._maxListeners === void 0 ? this._platform.defaultMaxListeners() : this._maxListeners;
}
emit(type, ...args) {
const events = this._events;
if (events === void 0)
return false;
const handler = events?.[type];
if (handler === void 0)
return false;
if (typeof handler === "function") {
this._callHandler(type, handler, args);
} else {
const len = handler.length;
const listeners = handler.slice();
for (let i = 0; i < len; ++i)
this._callHandler(type, listeners[i], args);
}
return true;
}
_callHandler(type, handler, args) {
const promise = Reflect.apply(handler, this, args);
if (!(promise instanceof Promise))
return;
let set = this._pendingHandlers.get(type);
if (!set) {
set = /* @__PURE__ */ new Set();
this._pendingHandlers.set(type, set);
}
set.add(promise);
promise.catch((e) => {
if (this._rejectionHandler)
this._rejectionHandler(e);
else
throw e;
}).finally(() => set.delete(promise));
}
addListener(type, listener) {
return this._addListener(type, listener, false);
}
on(type, listener) {
return this._addListener(type, listener, false);
}
_addListener(type, listener, prepend) {
checkListener(listener);
let events = this._events;
let existing;
if (events === void 0) {
events = this._events = /* @__PURE__ */ Object.create(null);
this._eventsCount = 0;
} else {
if (events.newListener !== void 0) {
this.emit("newListener", type, unwrapListener(listener));
events = this._events;
}
existing = events[type];
}
if (existing === void 0) {
existing = events[type] = listener;
++this._eventsCount;
} else {
if (typeof existing === "function") {
existing = events[type] = prepend ? [listener, existing] : [existing, listener];
} else if (prepend) {
existing.unshift(listener);
} else {
existing.push(listener);
}
const m = this.getMaxListeners();
if (m > 0 && existing.length > m && !existing.warned) {
existing.warned = true;
const w = new Error("Possible EventEmitter memory leak detected. " + existing.length + " " + String(type) + " listeners added. Use emitter.setMaxListeners() to increase limit");
w.name = "MaxListenersExceededWarning";
w.emitter = this;
w.type = type;
w.count = existing.length;
if (!this._platform.isUnderTest()) {
console.warn(w);
}
}
}
return this;
}
prependListener(type, listener) {
return this._addListener(type, listener, true);
}
once(type, listener) {
checkListener(listener);
this.on(type, new OnceWrapper(this, type, listener).wrapperFunction);
return this;
}
prependOnceListener(type, listener) {
checkListener(listener);
this.prependListener(type, new OnceWrapper(this, type, listener).wrapperFunction);
return this;
}
removeListener(type, listener) {
checkListener(listener);
const events = this._events;
if (events === void 0)
return this;
const list = events[type];
if (list === void 0)
return this;
if (list === listener || list.listener === listener) {
if (--this._eventsCount === 0) {
this._events = /* @__PURE__ */ Object.create(null);
} else {
delete events[type];
if (events.removeListener)
this.emit("removeListener", type, list.listener ?? listener);
}
} else if (typeof list !== "function") {
let position = -1;
let originalListener;
for (let i = list.length - 1; i >= 0; i--) {
if (list[i] === listener || wrappedListener(list[i]) === listener) {
originalListener = wrappedListener(list[i]);
position = i;
break;
}
}
if (position < 0)
return this;
if (position === 0)
list.shift();
else
list.splice(position, 1);
if (list.length === 1)
events[type] = list[0];
if (events.removeListener !== void 0)
this.emit("removeListener", type, originalListener || listener);
}
return this;
}
off(type, listener) {
return this.removeListener(type, listener);
}
removeAllListeners(type, options) {
this._removeAllListeners(type);
if (!options)
return this;
if (options.behavior === "wait") {
const errors = [];
this._rejectionHandler = (error) => errors.push(error);
return this._waitFor(type).then(() => {
if (errors.length)
throw errors[0];
});
}
if (options.behavior === "ignoreErrors")
this._rejectionHandler = () => {
};
return Promise.resolve();
}
_removeAllListeners(type) {
const events = this._events;
if (!events)
return;
if (!events.removeListener) {
if (type === void 0) {
this._events = /* @__PURE__ */ Object.create(null);
this._eventsCount = 0;
} else if (events[type] !== void 0) {
if (--this._eventsCount === 0)
this._events = /* @__PURE__ */ Object.create(null);
else
delete events[type];
}
return;
}
if (type === void 0) {
const keys = Object.keys(events);
let key;
for (let i = 0; i < keys.length; ++i) {
key = keys[i];
if (key === "removeListener")
continue;
this._removeAllListeners(key);
}
this._removeAllListeners("removeListener");
this._events = /* @__PURE__ */ Object.create(null);
this._eventsCount = 0;
return;
}
const listeners = events[type];
if (typeof listeners === "function") {
this.removeListener(type, listeners);
} else if (listeners !== void 0) {
for (let i = listeners.length - 1; i >= 0; i--)
this.removeListener(type, listeners[i]);
}
}
listeners(type) {
return this._listeners(this, type, true);
}
rawListeners(type) {
return this._listeners(this, type, false);
}
listenerCount(type) {
const events = this._events;
if (events !== void 0) {
const listener = events[type];
if (typeof listener === "function")
return 1;
if (listener !== void 0)
return listener.length;
}
return 0;
}
eventNames() {
return this._eventsCount > 0 && this._events ? Reflect.ownKeys(this._events) : [];
}
async _waitFor(type) {
let promises = [];
if (type) {
promises = [...this._pendingHandlers.get(type) || []];
} else {
promises = [];
for (const [, pending] of this._pendingHandlers)
promises.push(...pending);
}
await Promise.all(promises);
}
_listeners(target, type, unwrap) {
const events = target._events;
if (events === void 0)
return [];
const listener = events[type];
if (listener === void 0)
return [];
if (typeof listener === "function")
return unwrap ? [unwrapListener(listener)] : [listener];
return unwrap ? unwrapListeners(listener) : listener.slice();
}
}
function checkListener(listener) {
if (typeof listener !== "function")
throw new TypeError('The "listener" argument must be of type Function. Received type ' + typeof listener);
}
class OnceWrapper {
constructor(eventEmitter, eventType, listener) {
this._fired = false;
this._eventEmitter = eventEmitter;
this._eventType = eventType;
this._listener = listener;
this.wrapperFunction = this._handle.bind(this);
this.wrapperFunction.listener = listener;
}
_handle(...args) {
if (this._fired)
return;
this._fired = true;
this._eventEmitter.removeListener(this._eventType, this.wrapperFunction);
return this._listener.apply(this._eventEmitter, args);
}
}
function unwrapListener(l) {
return wrappedListener(l) ?? l;
}
function unwrapListeners(arr) {
return arr.map((l) => wrappedListener(l) ?? l);
}
function wrappedListener(l) {
return l.listener;
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
EventEmitter
});

98
node_modules/playwright-core/lib/client/events.js generated vendored Normal file
View File

@@ -0,0 +1,98 @@
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var events_exports = {};
__export(events_exports, {
Events: () => Events
});
module.exports = __toCommonJS(events_exports);
const Events = {
AndroidDevice: {
WebView: "webview",
Close: "close"
},
AndroidSocket: {
Data: "data",
Close: "close"
},
AndroidWebView: {
Close: "close"
},
Browser: {
Disconnected: "disconnected"
},
BrowserContext: {
Console: "console",
Close: "close",
Dialog: "dialog",
Page: "page",
// Can't use just 'error' due to node.js special treatment of error events.
// @see https://nodejs.org/api/events.html#events_error_events
WebError: "weberror",
BackgroundPage: "backgroundpage",
ServiceWorker: "serviceworker",
Request: "request",
Response: "response",
RequestFailed: "requestfailed",
RequestFinished: "requestfinished"
},
BrowserServer: {
Close: "close"
},
Page: {
Close: "close",
Crash: "crash",
Console: "console",
Dialog: "dialog",
Download: "download",
FileChooser: "filechooser",
DOMContentLoaded: "domcontentloaded",
// Can't use just 'error' due to node.js special treatment of error events.
// @see https://nodejs.org/api/events.html#events_error_events
PageError: "pageerror",
Request: "request",
Response: "response",
RequestFailed: "requestfailed",
RequestFinished: "requestfinished",
FrameAttached: "frameattached",
FrameDetached: "framedetached",
FrameNavigated: "framenavigated",
Load: "load",
Popup: "popup",
WebSocket: "websocket",
Worker: "worker"
},
WebSocket: {
Close: "close",
Error: "socketerror",
FrameReceived: "framereceived",
FrameSent: "framesent"
},
Worker: {
Close: "close"
},
ElectronApplication: {
Close: "close",
Console: "console",
Window: "window"
}
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
Events
});

369
node_modules/playwright-core/lib/client/fetch.js generated vendored Normal file
View File

@@ -0,0 +1,369 @@
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var fetch_exports = {};
__export(fetch_exports, {
APIRequest: () => APIRequest,
APIRequestContext: () => APIRequestContext,
APIResponse: () => APIResponse
});
module.exports = __toCommonJS(fetch_exports);
var import_browserContext = require("./browserContext");
var import_channelOwner = require("./channelOwner");
var import_errors = require("./errors");
var import_network = require("./network");
var import_tracing = require("./tracing");
var import_assert = require("../utils/isomorphic/assert");
var import_fileUtils = require("./fileUtils");
var import_headers = require("../utils/isomorphic/headers");
var import_rtti = require("../utils/isomorphic/rtti");
var import_timeoutSettings = require("./timeoutSettings");
class APIRequest {
constructor(playwright) {
this._contexts = /* @__PURE__ */ new Set();
this._playwright = playwright;
}
async newContext(options = {}) {
options = {
...this._playwright._defaultContextOptions,
...options
};
const storageState = typeof options.storageState === "string" ? JSON.parse(await this._playwright._platform.fs().promises.readFile(options.storageState, "utf8")) : options.storageState;
const context = APIRequestContext.from((await this._playwright._channel.newRequest({
...options,
extraHTTPHeaders: options.extraHTTPHeaders ? (0, import_headers.headersObjectToArray)(options.extraHTTPHeaders) : void 0,
storageState,
tracesDir: this._playwright._defaultLaunchOptions?.tracesDir,
// We do not expose tracesDir in the API, so do not allow options to accidentally override it.
clientCertificates: await (0, import_browserContext.toClientCertificatesProtocol)(this._playwright._platform, options.clientCertificates)
})).request);
this._contexts.add(context);
context._request = this;
context._timeoutSettings.setDefaultTimeout(options.timeout ?? this._playwright._defaultContextTimeout);
context._tracing._tracesDir = this._playwright._defaultLaunchOptions?.tracesDir;
await context._instrumentation.runAfterCreateRequestContext(context);
return context;
}
}
class APIRequestContext extends import_channelOwner.ChannelOwner {
static from(channel) {
return channel._object;
}
constructor(parent, type, guid, initializer) {
super(parent, type, guid, initializer);
this._tracing = import_tracing.Tracing.from(initializer.tracing);
this._timeoutSettings = new import_timeoutSettings.TimeoutSettings(this._platform);
}
async [Symbol.asyncDispose]() {
await this.dispose();
}
async dispose(options = {}) {
this._closeReason = options.reason;
await this._instrumentation.runBeforeCloseRequestContext(this);
try {
await this._channel.dispose(options);
} catch (e) {
if ((0, import_errors.isTargetClosedError)(e))
return;
throw e;
}
this._tracing._resetStackCounter();
this._request?._contexts.delete(this);
}
async delete(url, options) {
return await this.fetch(url, {
...options,
method: "DELETE"
});
}
async head(url, options) {
return await this.fetch(url, {
...options,
method: "HEAD"
});
}
async get(url, options) {
return await this.fetch(url, {
...options,
method: "GET"
});
}
async patch(url, options) {
return await this.fetch(url, {
...options,
method: "PATCH"
});
}
async post(url, options) {
return await this.fetch(url, {
...options,
method: "POST"
});
}
async put(url, options) {
return await this.fetch(url, {
...options,
method: "PUT"
});
}
async fetch(urlOrRequest, options = {}) {
const url = (0, import_rtti.isString)(urlOrRequest) ? urlOrRequest : void 0;
const request = (0, import_rtti.isString)(urlOrRequest) ? void 0 : urlOrRequest;
return await this._innerFetch({ url, request, ...options });
}
async _innerFetch(options = {}) {
return await this._wrapApiCall(async () => {
if (this._closeReason)
throw new import_errors.TargetClosedError(this._closeReason);
(0, import_assert.assert)(options.request || typeof options.url === "string", "First argument must be either URL string or Request");
(0, import_assert.assert)((options.data === void 0 ? 0 : 1) + (options.form === void 0 ? 0 : 1) + (options.multipart === void 0 ? 0 : 1) <= 1, `Only one of 'data', 'form' or 'multipart' can be specified`);
(0, import_assert.assert)(options.maxRedirects === void 0 || options.maxRedirects >= 0, `'maxRedirects' must be greater than or equal to '0'`);
(0, import_assert.assert)(options.maxRetries === void 0 || options.maxRetries >= 0, `'maxRetries' must be greater than or equal to '0'`);
const url = options.url !== void 0 ? options.url : options.request.url();
const method = options.method || options.request?.method();
let encodedParams = void 0;
if (typeof options.params === "string")
encodedParams = options.params;
else if (options.params instanceof URLSearchParams)
encodedParams = options.params.toString();
const headersObj = options.headers || options.request?.headers();
const headers = headersObj ? (0, import_headers.headersObjectToArray)(headersObj) : void 0;
let jsonData;
let formData;
let multipartData;
let postDataBuffer;
if (options.data !== void 0) {
if ((0, import_rtti.isString)(options.data)) {
if (isJsonContentType(headers))
jsonData = isJsonParsable(options.data) ? options.data : JSON.stringify(options.data);
else
postDataBuffer = Buffer.from(options.data, "utf8");
} else if (Buffer.isBuffer(options.data)) {
postDataBuffer = options.data;
} else if (typeof options.data === "object" || typeof options.data === "number" || typeof options.data === "boolean") {
jsonData = JSON.stringify(options.data);
} else {
throw new Error(`Unexpected 'data' type`);
}
} else if (options.form) {
if (globalThis.FormData && options.form instanceof FormData) {
formData = [];
for (const [name, value] of options.form.entries()) {
if (typeof value !== "string")
throw new Error(`Expected string for options.form["${name}"], found File. Please use options.multipart instead.`);
formData.push({ name, value });
}
} else {
formData = objectToArray(options.form);
}
} else if (options.multipart) {
multipartData = [];
if (globalThis.FormData && options.multipart instanceof FormData) {
const form = options.multipart;
for (const [name, value] of form.entries()) {
if ((0, import_rtti.isString)(value)) {
multipartData.push({ name, value });
} else {
const file = {
name: value.name,
mimeType: value.type,
buffer: Buffer.from(await value.arrayBuffer())
};
multipartData.push({ name, file });
}
}
} else {
for (const [name, value] of Object.entries(options.multipart))
multipartData.push(await toFormField(this._platform, name, value));
}
}
if (postDataBuffer === void 0 && jsonData === void 0 && formData === void 0 && multipartData === void 0)
postDataBuffer = options.request?.postDataBuffer() || void 0;
const fixtures = {
__testHookLookup: options.__testHookLookup
};
const result = await this._channel.fetch({
url,
params: typeof options.params === "object" ? objectToArray(options.params) : void 0,
encodedParams,
method,
headers,
postData: postDataBuffer,
jsonData,
formData,
multipartData,
timeout: this._timeoutSettings.timeout(options),
failOnStatusCode: options.failOnStatusCode,
ignoreHTTPSErrors: options.ignoreHTTPSErrors,
maxRedirects: options.maxRedirects,
maxRetries: options.maxRetries,
...fixtures
});
return new APIResponse(this, result.response);
});
}
async storageState(options = {}) {
const state = await this._channel.storageState({ indexedDB: options.indexedDB });
if (options.path) {
await (0, import_fileUtils.mkdirIfNeeded)(this._platform, options.path);
await this._platform.fs().promises.writeFile(options.path, JSON.stringify(state, void 0, 2), "utf8");
}
return state;
}
}
async function toFormField(platform, name, value) {
const typeOfValue = typeof value;
if (isFilePayload(value)) {
const payload = value;
if (!Buffer.isBuffer(payload.buffer))
throw new Error(`Unexpected buffer type of 'data.${name}'`);
return { name, file: filePayloadToJson(payload) };
} else if (typeOfValue === "string" || typeOfValue === "number" || typeOfValue === "boolean") {
return { name, value: String(value) };
} else {
return { name, file: await readStreamToJson(platform, value) };
}
}
function isJsonParsable(value) {
if (typeof value !== "string")
return false;
try {
JSON.parse(value);
return true;
} catch (e) {
if (e instanceof SyntaxError)
return false;
else
throw e;
}
}
class APIResponse {
constructor(context, initializer) {
this._request = context;
this._initializer = initializer;
this._headers = new import_network.RawHeaders(this._initializer.headers);
if (context._platform.inspectCustom)
this[context._platform.inspectCustom] = () => this._inspect();
}
ok() {
return this._initializer.status >= 200 && this._initializer.status <= 299;
}
url() {
return this._initializer.url;
}
status() {
return this._initializer.status;
}
statusText() {
return this._initializer.statusText;
}
headers() {
return this._headers.headers();
}
headersArray() {
return this._headers.headersArray();
}
async body() {
return await this._request._wrapApiCall(async () => {
try {
const result = await this._request._channel.fetchResponseBody({ fetchUid: this._fetchUid() });
if (result.binary === void 0)
throw new Error("Response has been disposed");
return result.binary;
} catch (e) {
if ((0, import_errors.isTargetClosedError)(e))
throw new Error("Response has been disposed");
throw e;
}
}, { internal: true });
}
async text() {
const content = await this.body();
return content.toString("utf8");
}
async json() {
const content = await this.text();
return JSON.parse(content);
}
async [Symbol.asyncDispose]() {
await this.dispose();
}
async dispose() {
await this._request._channel.disposeAPIResponse({ fetchUid: this._fetchUid() });
}
_inspect() {
const headers = this.headersArray().map(({ name, value }) => ` ${name}: ${value}`);
return `APIResponse: ${this.status()} ${this.statusText()}
${headers.join("\n")}`;
}
_fetchUid() {
return this._initializer.fetchUid;
}
async _fetchLog() {
const { log } = await this._request._channel.fetchLog({ fetchUid: this._fetchUid() });
return log;
}
}
function filePayloadToJson(payload) {
return {
name: payload.name,
mimeType: payload.mimeType,
buffer: payload.buffer
};
}
async function readStreamToJson(platform, stream) {
const buffer = await new Promise((resolve, reject) => {
const chunks = [];
stream.on("data", (chunk) => chunks.push(chunk));
stream.on("end", () => resolve(Buffer.concat(chunks)));
stream.on("error", (err) => reject(err));
});
const streamPath = Buffer.isBuffer(stream.path) ? stream.path.toString("utf8") : stream.path;
return {
name: platform.path().basename(streamPath),
buffer
};
}
function isJsonContentType(headers) {
if (!headers)
return false;
for (const { name, value } of headers) {
if (name.toLocaleLowerCase() === "content-type")
return value === "application/json";
}
return false;
}
function objectToArray(map) {
if (!map)
return void 0;
const result = [];
for (const [name, value] of Object.entries(map)) {
if (value !== void 0)
result.push({ name, value: String(value) });
}
return result;
}
function isFilePayload(value) {
return typeof value === "object" && value["name"] && value["mimeType"] && value["buffer"];
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
APIRequest,
APIRequestContext,
APIResponse
});

Some files were not shown because too many files have changed in this diff Show More