First Commit
This commit is contained in:
219
README.md
Normal file
219
README.md
Normal 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.
|
||||||
435
admin/class-wpdd-admin-payouts.php
Normal file
435
admin/class-wpdd-admin-payouts.php
Normal 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
920
admin/class-wpdd-admin.php
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
682
admin/class-wpdd-settings.php
Normal file
682
admin/class-wpdd-settings.php
Normal 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
451
assets/css/admin.css
Normal 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
469
assets/css/frontend.css
Normal 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
411
assets/js/admin.js
Normal 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
554
assets/js/frontend.js
Normal 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">×</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">×</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
346
assets/js/paypal.js
Normal 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">×</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);
|
||||||
|
});
|
||||||
|
});
|
||||||
491
includes/class-wpdd-ajax.php
Normal file
491
includes/class-wpdd-ajax.php
Normal 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'));
|
||||||
|
}
|
||||||
|
}
|
||||||
177
includes/class-wpdd-creator.php
Normal file
177
includes/class-wpdd-creator.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
377
includes/class-wpdd-customer.php
Normal file
377
includes/class-wpdd-customer.php
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
752
includes/class-wpdd-download-handler.php
Normal file
752
includes/class-wpdd-download-handler.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
180
includes/class-wpdd-file-protection.php
Normal file
180
includes/class-wpdd-file-protection.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
215
includes/class-wpdd-install.php
Normal file
215
includes/class-wpdd-install.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
308
includes/class-wpdd-metaboxes.php
Normal file
308
includes/class-wpdd-metaboxes.php
Normal 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']));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
318
includes/class-wpdd-orders.php
Normal file
318
includes/class-wpdd-orders.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
231
includes/class-wpdd-paypal-payouts.php
Normal file
231
includes/class-wpdd-paypal-payouts.php
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
302
includes/class-wpdd-paypal.php
Normal file
302
includes/class-wpdd-paypal.php
Normal 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 . '¤cy=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);
|
||||||
|
}
|
||||||
|
}
|
||||||
142
includes/class-wpdd-post-types.php
Normal file
142
includes/class-wpdd-post-types.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
116
includes/class-wpdd-roles.php
Normal file
116
includes/class-wpdd-roles.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1167
includes/class-wpdd-shortcodes.php
Normal file
1167
includes/class-wpdd-shortcodes.php
Normal file
File diff suppressed because it is too large
Load Diff
249
includes/class-wpdd-watermark.php
Normal file
249
includes/class-wpdd-watermark.php
Normal 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
1
node_modules/.bin/playwright
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../@playwright/test/cli.js
|
||||||
1
node_modules/.bin/playwright-core
generated
vendored
Symbolic link
1
node_modules/.bin/playwright-core
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../playwright-core/cli.js
|
||||||
202
node_modules/@playwright/test/LICENSE
generated
vendored
Normal file
202
node_modules/@playwright/test/LICENSE
generated
vendored
Normal 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
5
node_modules/@playwright/test/NOTICE
generated
vendored
Normal 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
168
node_modules/@playwright/test/README.md
generated
vendored
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
# 🎭 Playwright
|
||||||
|
|
||||||
|
[](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[](https://webkit.org/)<!-- GEN:stop --> [](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
19
node_modules/@playwright/test/cli.js
generated
vendored
Executable 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
18
node_modules/@playwright/test/index.d.ts
generated
vendored
Normal 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
17
node_modules/@playwright/test/index.js
generated
vendored
Normal 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
18
node_modules/@playwright/test/index.mjs
generated
vendored
Normal 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/reporter.d.ts
generated
vendored
Normal file
17
node_modules/@playwright/test/reporter.d.ts
generated
vendored
Normal 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
17
node_modules/@playwright/test/reporter.js
generated
vendored
Normal 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
17
node_modules/@playwright/test/reporter.mjs
generated
vendored
Normal 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
202
node_modules/playwright-core/LICENSE
generated
vendored
Normal 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
5
node_modules/playwright-core/NOTICE
generated
vendored
Normal 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
3
node_modules/playwright-core/README.md
generated
vendored
Normal 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
1502
node_modules/playwright-core/ThirdPartyNotices.txt
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
5
node_modules/playwright-core/bin/install_media_pack.ps1
generated
vendored
Normal file
5
node_modules/playwright-core/bin/install_media_pack.ps1
generated
vendored
Normal 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
|
||||||
|
}
|
||||||
42
node_modules/playwright-core/bin/reinstall_chrome_beta_linux.sh
generated
vendored
Executable file
42
node_modules/playwright-core/bin/reinstall_chrome_beta_linux.sh
generated
vendored
Executable 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
|
||||||
13
node_modules/playwright-core/bin/reinstall_chrome_beta_mac.sh
generated
vendored
Executable file
13
node_modules/playwright-core/bin/reinstall_chrome_beta_mac.sh
generated
vendored
Executable 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
|
||||||
24
node_modules/playwright-core/bin/reinstall_chrome_beta_win.ps1
generated
vendored
Normal file
24
node_modules/playwright-core/bin/reinstall_chrome_beta_win.ps1
generated
vendored
Normal 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
|
||||||
|
}
|
||||||
42
node_modules/playwright-core/bin/reinstall_chrome_stable_linux.sh
generated
vendored
Executable file
42
node_modules/playwright-core/bin/reinstall_chrome_stable_linux.sh
generated
vendored
Executable 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
|
||||||
12
node_modules/playwright-core/bin/reinstall_chrome_stable_mac.sh
generated
vendored
Executable file
12
node_modules/playwright-core/bin/reinstall_chrome_stable_mac.sh
generated
vendored
Executable 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
|
||||||
24
node_modules/playwright-core/bin/reinstall_chrome_stable_win.ps1
generated
vendored
Normal file
24
node_modules/playwright-core/bin/reinstall_chrome_stable_win.ps1
generated
vendored
Normal 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
|
||||||
|
}
|
||||||
48
node_modules/playwright-core/bin/reinstall_msedge_beta_linux.sh
generated
vendored
Executable file
48
node_modules/playwright-core/bin/reinstall_msedge_beta_linux.sh
generated
vendored
Executable 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
|
||||||
11
node_modules/playwright-core/bin/reinstall_msedge_beta_mac.sh
generated
vendored
Executable file
11
node_modules/playwright-core/bin/reinstall_msedge_beta_mac.sh
generated
vendored
Executable 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
|
||||||
23
node_modules/playwright-core/bin/reinstall_msedge_beta_win.ps1
generated
vendored
Normal file
23
node_modules/playwright-core/bin/reinstall_msedge_beta_win.ps1
generated
vendored
Normal 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
|
||||||
|
}
|
||||||
48
node_modules/playwright-core/bin/reinstall_msedge_dev_linux.sh
generated
vendored
Executable file
48
node_modules/playwright-core/bin/reinstall_msedge_dev_linux.sh
generated
vendored
Executable 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
11
node_modules/playwright-core/bin/reinstall_msedge_dev_mac.sh
generated
vendored
Executable 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
|
||||||
23
node_modules/playwright-core/bin/reinstall_msedge_dev_win.ps1
generated
vendored
Normal file
23
node_modules/playwright-core/bin/reinstall_msedge_dev_win.ps1
generated
vendored
Normal 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
|
||||||
|
}
|
||||||
48
node_modules/playwright-core/bin/reinstall_msedge_stable_linux.sh
generated
vendored
Executable file
48
node_modules/playwright-core/bin/reinstall_msedge_stable_linux.sh
generated
vendored
Executable 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
|
||||||
11
node_modules/playwright-core/bin/reinstall_msedge_stable_mac.sh
generated
vendored
Executable file
11
node_modules/playwright-core/bin/reinstall_msedge_stable_mac.sh
generated
vendored
Executable 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
|
||||||
24
node_modules/playwright-core/bin/reinstall_msedge_stable_win.ps1
generated
vendored
Normal file
24
node_modules/playwright-core/bin/reinstall_msedge_stable_win.ps1
generated
vendored
Normal 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
|
||||||
|
}
|
||||||
18
node_modules/playwright-core/cli.js
generated
vendored
Executable file
18
node_modules/playwright-core/cli.js
generated
vendored
Executable 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
17
node_modules/playwright-core/index.d.ts
generated
vendored
Normal 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
32
node_modules/playwright-core/index.js
generated
vendored
Normal 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
28
node_modules/playwright-core/index.mjs
generated
vendored
Normal 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
65
node_modules/playwright-core/lib/androidServerImpl.js
generated
vendored
Normal 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
123
node_modules/playwright-core/lib/browserServerImpl.js
generated
vendored
Normal 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
97
node_modules/playwright-core/lib/cli/driver.js
generated
vendored
Normal 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
633
node_modules/playwright-core/lib/cli/program.js
generated
vendored
Normal 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
|
||||||
|
});
|
||||||
74
node_modules/playwright-core/lib/cli/programWithTestStub.js
generated
vendored
Normal file
74
node_modules/playwright-core/lib/cli/programWithTestStub.js
generated
vendored
Normal 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
|
||||||
|
});
|
||||||
49
node_modules/playwright-core/lib/client/accessibility.js
generated
vendored
Normal file
49
node_modules/playwright-core/lib/client/accessibility.js
generated
vendored
Normal 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
361
node_modules/playwright-core/lib/client/android.js
generated
vendored
Normal 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
137
node_modules/playwright-core/lib/client/api.js
generated
vendored
Normal 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
79
node_modules/playwright-core/lib/client/artifact.js
generated
vendored
Normal 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
173
node_modules/playwright-core/lib/client/browser.js
generated
vendored
Normal 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
|
||||||
|
});
|
||||||
535
node_modules/playwright-core/lib/client/browserContext.js
generated
vendored
Normal file
535
node_modules/playwright-core/lib/client/browserContext.js
generated
vendored
Normal 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
184
node_modules/playwright-core/lib/client/browserType.js
generated
vendored
Normal 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
51
node_modules/playwright-core/lib/client/cdpSession.js
generated
vendored
Normal 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
201
node_modules/playwright-core/lib/client/channelOwner.js
generated
vendored
Normal 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
|
||||||
|
});
|
||||||
64
node_modules/playwright-core/lib/client/clientHelper.js
generated
vendored
Normal file
64
node_modules/playwright-core/lib/client/clientHelper.js
generated
vendored
Normal 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
|
||||||
|
});
|
||||||
55
node_modules/playwright-core/lib/client/clientInstrumentation.js
generated
vendored
Normal file
55
node_modules/playwright-core/lib/client/clientInstrumentation.js
generated
vendored
Normal 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
|
||||||
|
});
|
||||||
69
node_modules/playwright-core/lib/client/clientStackTrace.js
generated
vendored
Normal file
69
node_modules/playwright-core/lib/client/clientStackTrace.js
generated
vendored
Normal 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
68
node_modules/playwright-core/lib/client/clock.js
generated
vendored
Normal 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
314
node_modules/playwright-core/lib/client/connection.js
generated
vendored
Normal 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
|
||||||
|
});
|
||||||
55
node_modules/playwright-core/lib/client/consoleMessage.js
generated
vendored
Normal file
55
node_modules/playwright-core/lib/client/consoleMessage.js
generated
vendored
Normal 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
44
node_modules/playwright-core/lib/client/coverage.js
generated
vendored
Normal 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
56
node_modules/playwright-core/lib/client/dialog.js
generated
vendored
Normal 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
62
node_modules/playwright-core/lib/client/download.js
generated
vendored
Normal 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
138
node_modules/playwright-core/lib/client/electron.js
generated
vendored
Normal 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
|
||||||
|
});
|
||||||
281
node_modules/playwright-core/lib/client/elementHandle.js
generated
vendored
Normal file
281
node_modules/playwright-core/lib/client/elementHandle.js
generated
vendored
Normal 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
77
node_modules/playwright-core/lib/client/errors.js
generated
vendored
Normal 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
314
node_modules/playwright-core/lib/client/eventEmitter.js
generated
vendored
Normal 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
98
node_modules/playwright-core/lib/client/events.js
generated
vendored
Normal 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
369
node_modules/playwright-core/lib/client/fetch.js
generated
vendored
Normal 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
|
||||||
|
});
|
||||||
46
node_modules/playwright-core/lib/client/fileChooser.js
generated
vendored
Normal file
46
node_modules/playwright-core/lib/client/fileChooser.js
generated
vendored
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
"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 fileChooser_exports = {};
|
||||||
|
__export(fileChooser_exports, {
|
||||||
|
FileChooser: () => FileChooser
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(fileChooser_exports);
|
||||||
|
class FileChooser {
|
||||||
|
constructor(page, elementHandle, isMultiple) {
|
||||||
|
this._page = page;
|
||||||
|
this._elementHandle = elementHandle;
|
||||||
|
this._isMultiple = isMultiple;
|
||||||
|
}
|
||||||
|
element() {
|
||||||
|
return this._elementHandle;
|
||||||
|
}
|
||||||
|
isMultiple() {
|
||||||
|
return this._isMultiple;
|
||||||
|
}
|
||||||
|
page() {
|
||||||
|
return this._page;
|
||||||
|
}
|
||||||
|
async setFiles(files, options) {
|
||||||
|
return await this._elementHandle.setInputFiles(files, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
FileChooser
|
||||||
|
});
|
||||||
34
node_modules/playwright-core/lib/client/fileUtils.js
generated
vendored
Normal file
34
node_modules/playwright-core/lib/client/fileUtils.js
generated
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
"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 fileUtils_exports = {};
|
||||||
|
__export(fileUtils_exports, {
|
||||||
|
fileUploadSizeLimit: () => fileUploadSizeLimit,
|
||||||
|
mkdirIfNeeded: () => mkdirIfNeeded
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(fileUtils_exports);
|
||||||
|
const fileUploadSizeLimit = 50 * 1024 * 1024;
|
||||||
|
async function mkdirIfNeeded(platform, filePath) {
|
||||||
|
await platform.fs().promises.mkdir(platform.path().dirname(filePath), { recursive: true }).catch(() => {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
fileUploadSizeLimit,
|
||||||
|
mkdirIfNeeded
|
||||||
|
});
|
||||||
408
node_modules/playwright-core/lib/client/frame.js
generated
vendored
Normal file
408
node_modules/playwright-core/lib/client/frame.js
generated
vendored
Normal file
@@ -0,0 +1,408 @@
|
|||||||
|
"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 frame_exports = {};
|
||||||
|
__export(frame_exports, {
|
||||||
|
Frame: () => Frame,
|
||||||
|
verifyLoadState: () => verifyLoadState
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(frame_exports);
|
||||||
|
var import_eventEmitter = require("./eventEmitter");
|
||||||
|
var import_channelOwner = require("./channelOwner");
|
||||||
|
var import_clientHelper = require("./clientHelper");
|
||||||
|
var import_elementHandle = require("./elementHandle");
|
||||||
|
var import_events = require("./events");
|
||||||
|
var import_jsHandle = require("./jsHandle");
|
||||||
|
var import_locator = require("./locator");
|
||||||
|
var network = __toESM(require("./network"));
|
||||||
|
var import_types = require("./types");
|
||||||
|
var import_waiter = require("./waiter");
|
||||||
|
var import_assert = require("../utils/isomorphic/assert");
|
||||||
|
var import_locatorUtils = require("../utils/isomorphic/locatorUtils");
|
||||||
|
var import_urlMatch = require("../utils/isomorphic/urlMatch");
|
||||||
|
var import_timeoutSettings = require("./timeoutSettings");
|
||||||
|
class Frame extends import_channelOwner.ChannelOwner {
|
||||||
|
constructor(parent, type, guid, initializer) {
|
||||||
|
super(parent, type, guid, initializer);
|
||||||
|
this._parentFrame = null;
|
||||||
|
this._url = "";
|
||||||
|
this._name = "";
|
||||||
|
this._detached = false;
|
||||||
|
this._childFrames = /* @__PURE__ */ new Set();
|
||||||
|
this._eventEmitter = new import_eventEmitter.EventEmitter(parent._platform);
|
||||||
|
this._eventEmitter.setMaxListeners(0);
|
||||||
|
this._parentFrame = Frame.fromNullable(initializer.parentFrame);
|
||||||
|
if (this._parentFrame)
|
||||||
|
this._parentFrame._childFrames.add(this);
|
||||||
|
this._name = initializer.name;
|
||||||
|
this._url = initializer.url;
|
||||||
|
this._loadStates = new Set(initializer.loadStates);
|
||||||
|
this._channel.on("loadstate", (event) => {
|
||||||
|
if (event.add) {
|
||||||
|
this._loadStates.add(event.add);
|
||||||
|
this._eventEmitter.emit("loadstate", event.add);
|
||||||
|
}
|
||||||
|
if (event.remove)
|
||||||
|
this._loadStates.delete(event.remove);
|
||||||
|
if (!this._parentFrame && event.add === "load" && this._page)
|
||||||
|
this._page.emit(import_events.Events.Page.Load, this._page);
|
||||||
|
if (!this._parentFrame && event.add === "domcontentloaded" && this._page)
|
||||||
|
this._page.emit(import_events.Events.Page.DOMContentLoaded, this._page);
|
||||||
|
});
|
||||||
|
this._channel.on("navigated", (event) => {
|
||||||
|
this._url = event.url;
|
||||||
|
this._name = event.name;
|
||||||
|
this._eventEmitter.emit("navigated", event);
|
||||||
|
if (!event.error && this._page)
|
||||||
|
this._page.emit(import_events.Events.Page.FrameNavigated, this);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
static from(frame) {
|
||||||
|
return frame._object;
|
||||||
|
}
|
||||||
|
static fromNullable(frame) {
|
||||||
|
return frame ? Frame.from(frame) : null;
|
||||||
|
}
|
||||||
|
page() {
|
||||||
|
return this._page;
|
||||||
|
}
|
||||||
|
_timeout(options) {
|
||||||
|
const timeoutSettings = this._page?._timeoutSettings || new import_timeoutSettings.TimeoutSettings(this._platform);
|
||||||
|
return timeoutSettings.timeout(options || {});
|
||||||
|
}
|
||||||
|
_navigationTimeout(options) {
|
||||||
|
const timeoutSettings = this._page?._timeoutSettings || new import_timeoutSettings.TimeoutSettings(this._platform);
|
||||||
|
return timeoutSettings.navigationTimeout(options || {});
|
||||||
|
}
|
||||||
|
async goto(url, options = {}) {
|
||||||
|
const waitUntil = verifyLoadState("waitUntil", options.waitUntil === void 0 ? "load" : options.waitUntil);
|
||||||
|
return network.Response.fromNullable((await this._channel.goto({ url, ...options, waitUntil, timeout: this._navigationTimeout(options) })).response);
|
||||||
|
}
|
||||||
|
_setupNavigationWaiter(options) {
|
||||||
|
const waiter = new import_waiter.Waiter(this._page, "");
|
||||||
|
if (this._page.isClosed())
|
||||||
|
waiter.rejectImmediately(this._page._closeErrorWithReason());
|
||||||
|
waiter.rejectOnEvent(this._page, import_events.Events.Page.Close, () => this._page._closeErrorWithReason());
|
||||||
|
waiter.rejectOnEvent(this._page, import_events.Events.Page.Crash, new Error("Navigation failed because page crashed!"));
|
||||||
|
waiter.rejectOnEvent(this._page, import_events.Events.Page.FrameDetached, new Error("Navigating frame was detached!"), (frame) => frame === this);
|
||||||
|
const timeout = this._page._timeoutSettings.navigationTimeout(options);
|
||||||
|
waiter.rejectOnTimeout(timeout, `Timeout ${timeout}ms exceeded.`);
|
||||||
|
return waiter;
|
||||||
|
}
|
||||||
|
async waitForNavigation(options = {}) {
|
||||||
|
return await this._page._wrapApiCall(async () => {
|
||||||
|
const waitUntil = verifyLoadState("waitUntil", options.waitUntil === void 0 ? "load" : options.waitUntil);
|
||||||
|
const waiter = this._setupNavigationWaiter(options);
|
||||||
|
const toUrl = typeof options.url === "string" ? ` to "${options.url}"` : "";
|
||||||
|
waiter.log(`waiting for navigation${toUrl} until "${waitUntil}"`);
|
||||||
|
const navigatedEvent = await waiter.waitForEvent(this._eventEmitter, "navigated", (event) => {
|
||||||
|
if (event.error)
|
||||||
|
return true;
|
||||||
|
waiter.log(` navigated to "${event.url}"`);
|
||||||
|
return (0, import_urlMatch.urlMatches)(this._page?.context()._options.baseURL, event.url, options.url);
|
||||||
|
});
|
||||||
|
if (navigatedEvent.error) {
|
||||||
|
const e = new Error(navigatedEvent.error);
|
||||||
|
e.stack = "";
|
||||||
|
await waiter.waitForPromise(Promise.reject(e));
|
||||||
|
}
|
||||||
|
if (!this._loadStates.has(waitUntil)) {
|
||||||
|
await waiter.waitForEvent(this._eventEmitter, "loadstate", (s) => {
|
||||||
|
waiter.log(` "${s}" event fired`);
|
||||||
|
return s === waitUntil;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const request = navigatedEvent.newDocument ? network.Request.fromNullable(navigatedEvent.newDocument.request) : null;
|
||||||
|
const response = request ? await waiter.waitForPromise(request._finalRequest()._internalResponse()) : null;
|
||||||
|
waiter.dispose();
|
||||||
|
return response;
|
||||||
|
}, { title: "Wait for navigation" });
|
||||||
|
}
|
||||||
|
async waitForLoadState(state = "load", options = {}) {
|
||||||
|
state = verifyLoadState("state", state);
|
||||||
|
return await this._page._wrapApiCall(async () => {
|
||||||
|
const waiter = this._setupNavigationWaiter(options);
|
||||||
|
if (this._loadStates.has(state)) {
|
||||||
|
waiter.log(` not waiting, "${state}" event already fired`);
|
||||||
|
} else {
|
||||||
|
await waiter.waitForEvent(this._eventEmitter, "loadstate", (s) => {
|
||||||
|
waiter.log(` "${s}" event fired`);
|
||||||
|
return s === state;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
waiter.dispose();
|
||||||
|
}, { title: `Wait for load state "${state}"` });
|
||||||
|
}
|
||||||
|
async waitForURL(url, options = {}) {
|
||||||
|
if ((0, import_urlMatch.urlMatches)(this._page?.context()._options.baseURL, this.url(), url))
|
||||||
|
return await this.waitForLoadState(options.waitUntil, options);
|
||||||
|
await this.waitForNavigation({ url, ...options });
|
||||||
|
}
|
||||||
|
async frameElement() {
|
||||||
|
return import_elementHandle.ElementHandle.from((await this._channel.frameElement()).element);
|
||||||
|
}
|
||||||
|
async evaluateHandle(pageFunction, arg) {
|
||||||
|
(0, import_jsHandle.assertMaxArguments)(arguments.length, 2);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
async evaluate(pageFunction, arg) {
|
||||||
|
(0, import_jsHandle.assertMaxArguments)(arguments.length, 2);
|
||||||
|
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 _evaluateFunction(functionDeclaration) {
|
||||||
|
const result = await this._channel.evaluateExpression({ expression: functionDeclaration, isFunction: true, arg: (0, import_jsHandle.serializeArgument)(void 0) });
|
||||||
|
return (0, import_jsHandle.parseResult)(result.value);
|
||||||
|
}
|
||||||
|
async _evaluateExposeUtilityScript(pageFunction, arg) {
|
||||||
|
(0, import_jsHandle.assertMaxArguments)(arguments.length, 2);
|
||||||
|
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 $(selector, options) {
|
||||||
|
const result = await this._channel.querySelector({ selector, ...options });
|
||||||
|
return import_elementHandle.ElementHandle.fromNullable(result.element);
|
||||||
|
}
|
||||||
|
async waitForSelector(selector, options = {}) {
|
||||||
|
if (options.visibility)
|
||||||
|
throw new Error("options.visibility is not supported, did you mean options.state?");
|
||||||
|
if (options.waitFor && options.waitFor !== "visible")
|
||||||
|
throw new Error("options.waitFor is not supported, did you mean options.state?");
|
||||||
|
const result = await this._channel.waitForSelector({ selector, ...options, timeout: this._timeout(options) });
|
||||||
|
return import_elementHandle.ElementHandle.fromNullable(result.element);
|
||||||
|
}
|
||||||
|
async dispatchEvent(selector, type, eventInit, options = {}) {
|
||||||
|
await this._channel.dispatchEvent({ selector, type, eventInit: (0, import_jsHandle.serializeArgument)(eventInit), ...options, timeout: this._timeout(options) });
|
||||||
|
}
|
||||||
|
async $eval(selector, pageFunction, arg) {
|
||||||
|
(0, import_jsHandle.assertMaxArguments)(arguments.length, 3);
|
||||||
|
const result = await this._channel.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) {
|
||||||
|
(0, import_jsHandle.assertMaxArguments)(arguments.length, 3);
|
||||||
|
const result = await this._channel.evalOnSelectorAll({ selector, expression: String(pageFunction), isFunction: typeof pageFunction === "function", arg: (0, import_jsHandle.serializeArgument)(arg) });
|
||||||
|
return (0, import_jsHandle.parseResult)(result.value);
|
||||||
|
}
|
||||||
|
async $$(selector) {
|
||||||
|
const result = await this._channel.querySelectorAll({ selector });
|
||||||
|
return result.elements.map((e) => import_elementHandle.ElementHandle.from(e));
|
||||||
|
}
|
||||||
|
async _queryCount(selector, options) {
|
||||||
|
return (await this._channel.queryCount({ selector, ...options })).value;
|
||||||
|
}
|
||||||
|
async content() {
|
||||||
|
return (await this._channel.content()).value;
|
||||||
|
}
|
||||||
|
async setContent(html, options = {}) {
|
||||||
|
const waitUntil = verifyLoadState("waitUntil", options.waitUntil === void 0 ? "load" : options.waitUntil);
|
||||||
|
await this._channel.setContent({ html, ...options, waitUntil, timeout: this._navigationTimeout(options) });
|
||||||
|
}
|
||||||
|
name() {
|
||||||
|
return this._name || "";
|
||||||
|
}
|
||||||
|
url() {
|
||||||
|
return this._url;
|
||||||
|
}
|
||||||
|
parentFrame() {
|
||||||
|
return this._parentFrame;
|
||||||
|
}
|
||||||
|
childFrames() {
|
||||||
|
return Array.from(this._childFrames);
|
||||||
|
}
|
||||||
|
isDetached() {
|
||||||
|
return this._detached;
|
||||||
|
}
|
||||||
|
async addScriptTag(options = {}) {
|
||||||
|
const copy = { ...options };
|
||||||
|
if (copy.path) {
|
||||||
|
copy.content = (await this._platform.fs().promises.readFile(copy.path)).toString();
|
||||||
|
copy.content = (0, import_clientHelper.addSourceUrlToScript)(copy.content, copy.path);
|
||||||
|
}
|
||||||
|
return import_elementHandle.ElementHandle.from((await this._channel.addScriptTag({ ...copy })).element);
|
||||||
|
}
|
||||||
|
async addStyleTag(options = {}) {
|
||||||
|
const copy = { ...options };
|
||||||
|
if (copy.path) {
|
||||||
|
copy.content = (await this._platform.fs().promises.readFile(copy.path)).toString();
|
||||||
|
copy.content += "/*# sourceURL=" + copy.path.replace(/\n/g, "") + "*/";
|
||||||
|
}
|
||||||
|
return import_elementHandle.ElementHandle.from((await this._channel.addStyleTag({ ...copy })).element);
|
||||||
|
}
|
||||||
|
async click(selector, options = {}) {
|
||||||
|
return await this._channel.click({ selector, ...options, timeout: this._timeout(options) });
|
||||||
|
}
|
||||||
|
async dblclick(selector, options = {}) {
|
||||||
|
return await this._channel.dblclick({ selector, ...options, timeout: this._timeout(options) });
|
||||||
|
}
|
||||||
|
async dragAndDrop(source, target, options = {}) {
|
||||||
|
return await this._channel.dragAndDrop({ source, target, ...options, timeout: this._timeout(options) });
|
||||||
|
}
|
||||||
|
async tap(selector, options = {}) {
|
||||||
|
return await this._channel.tap({ selector, ...options, timeout: this._timeout(options) });
|
||||||
|
}
|
||||||
|
async fill(selector, value, options = {}) {
|
||||||
|
return await this._channel.fill({ selector, value, ...options, timeout: this._timeout(options) });
|
||||||
|
}
|
||||||
|
async _highlight(selector) {
|
||||||
|
return await this._channel.highlight({ selector });
|
||||||
|
}
|
||||||
|
locator(selector, options) {
|
||||||
|
return new import_locator.Locator(this, selector, options);
|
||||||
|
}
|
||||||
|
getByTestId(testId) {
|
||||||
|
return this.locator((0, import_locatorUtils.getByTestIdSelector)((0, import_locator.testIdAttributeName)(), testId));
|
||||||
|
}
|
||||||
|
getByAltText(text, options) {
|
||||||
|
return this.locator((0, import_locatorUtils.getByAltTextSelector)(text, options));
|
||||||
|
}
|
||||||
|
getByLabel(text, options) {
|
||||||
|
return this.locator((0, import_locatorUtils.getByLabelSelector)(text, options));
|
||||||
|
}
|
||||||
|
getByPlaceholder(text, options) {
|
||||||
|
return this.locator((0, import_locatorUtils.getByPlaceholderSelector)(text, options));
|
||||||
|
}
|
||||||
|
getByText(text, options) {
|
||||||
|
return this.locator((0, import_locatorUtils.getByTextSelector)(text, options));
|
||||||
|
}
|
||||||
|
getByTitle(text, options) {
|
||||||
|
return this.locator((0, import_locatorUtils.getByTitleSelector)(text, options));
|
||||||
|
}
|
||||||
|
getByRole(role, options = {}) {
|
||||||
|
return this.locator((0, import_locatorUtils.getByRoleSelector)(role, options));
|
||||||
|
}
|
||||||
|
frameLocator(selector) {
|
||||||
|
return new import_locator.FrameLocator(this, selector);
|
||||||
|
}
|
||||||
|
async focus(selector, options = {}) {
|
||||||
|
await this._channel.focus({ selector, ...options, timeout: this._timeout(options) });
|
||||||
|
}
|
||||||
|
async textContent(selector, options = {}) {
|
||||||
|
const value = (await this._channel.textContent({ selector, ...options, timeout: this._timeout(options) })).value;
|
||||||
|
return value === void 0 ? null : value;
|
||||||
|
}
|
||||||
|
async innerText(selector, options = {}) {
|
||||||
|
return (await this._channel.innerText({ selector, ...options, timeout: this._timeout(options) })).value;
|
||||||
|
}
|
||||||
|
async innerHTML(selector, options = {}) {
|
||||||
|
return (await this._channel.innerHTML({ selector, ...options, timeout: this._timeout(options) })).value;
|
||||||
|
}
|
||||||
|
async getAttribute(selector, name, options = {}) {
|
||||||
|
const value = (await this._channel.getAttribute({ selector, name, ...options, timeout: this._timeout(options) })).value;
|
||||||
|
return value === void 0 ? null : value;
|
||||||
|
}
|
||||||
|
async inputValue(selector, options = {}) {
|
||||||
|
return (await this._channel.inputValue({ selector, ...options, timeout: this._timeout(options) })).value;
|
||||||
|
}
|
||||||
|
async isChecked(selector, options = {}) {
|
||||||
|
return (await this._channel.isChecked({ selector, ...options, timeout: this._timeout(options) })).value;
|
||||||
|
}
|
||||||
|
async isDisabled(selector, options = {}) {
|
||||||
|
return (await this._channel.isDisabled({ selector, ...options, timeout: this._timeout(options) })).value;
|
||||||
|
}
|
||||||
|
async isEditable(selector, options = {}) {
|
||||||
|
return (await this._channel.isEditable({ selector, ...options, timeout: this._timeout(options) })).value;
|
||||||
|
}
|
||||||
|
async isEnabled(selector, options = {}) {
|
||||||
|
return (await this._channel.isEnabled({ selector, ...options, timeout: this._timeout(options) })).value;
|
||||||
|
}
|
||||||
|
async isHidden(selector, options = {}) {
|
||||||
|
return (await this._channel.isHidden({ selector, ...options })).value;
|
||||||
|
}
|
||||||
|
async isVisible(selector, options = {}) {
|
||||||
|
return (await this._channel.isVisible({ selector, ...options })).value;
|
||||||
|
}
|
||||||
|
async hover(selector, options = {}) {
|
||||||
|
await this._channel.hover({ selector, ...options, timeout: this._timeout(options) });
|
||||||
|
}
|
||||||
|
async selectOption(selector, values, options = {}) {
|
||||||
|
return (await this._channel.selectOption({ selector, ...(0, import_elementHandle.convertSelectOptionValues)(values), ...options, timeout: this._timeout(options) })).values;
|
||||||
|
}
|
||||||
|
async setInputFiles(selector, files, options = {}) {
|
||||||
|
const converted = await (0, import_elementHandle.convertInputFiles)(this._platform, files, this.page().context());
|
||||||
|
await this._channel.setInputFiles({ selector, ...converted, ...options, timeout: this._timeout(options) });
|
||||||
|
}
|
||||||
|
async type(selector, text, options = {}) {
|
||||||
|
await this._channel.type({ selector, text, ...options, timeout: this._timeout(options) });
|
||||||
|
}
|
||||||
|
async press(selector, key, options = {}) {
|
||||||
|
await this._channel.press({ selector, key, ...options, timeout: this._timeout(options) });
|
||||||
|
}
|
||||||
|
async check(selector, options = {}) {
|
||||||
|
await this._channel.check({ selector, ...options, timeout: this._timeout(options) });
|
||||||
|
}
|
||||||
|
async uncheck(selector, options = {}) {
|
||||||
|
await this._channel.uncheck({ selector, ...options, timeout: this._timeout(options) });
|
||||||
|
}
|
||||||
|
async setChecked(selector, checked, options) {
|
||||||
|
if (checked)
|
||||||
|
await this.check(selector, options);
|
||||||
|
else
|
||||||
|
await this.uncheck(selector, options);
|
||||||
|
}
|
||||||
|
async waitForTimeout(timeout) {
|
||||||
|
await this._channel.waitForTimeout({ waitTimeout: timeout });
|
||||||
|
}
|
||||||
|
async waitForFunction(pageFunction, arg, options = {}) {
|
||||||
|
if (typeof options.polling === "string")
|
||||||
|
(0, import_assert.assert)(options.polling === "raf", "Unknown polling option: " + options.polling);
|
||||||
|
const result = await this._channel.waitForFunction({
|
||||||
|
...options,
|
||||||
|
pollingInterval: options.polling === "raf" ? void 0 : options.polling,
|
||||||
|
expression: String(pageFunction),
|
||||||
|
isFunction: typeof pageFunction === "function",
|
||||||
|
arg: (0, import_jsHandle.serializeArgument)(arg),
|
||||||
|
timeout: this._timeout(options)
|
||||||
|
});
|
||||||
|
return import_jsHandle.JSHandle.from(result.handle);
|
||||||
|
}
|
||||||
|
async title() {
|
||||||
|
return (await this._channel.title()).value;
|
||||||
|
}
|
||||||
|
async _expect(expression, options) {
|
||||||
|
const params = { expression, ...options, isNot: !!options.isNot };
|
||||||
|
params.expectedValue = (0, import_jsHandle.serializeArgument)(options.expectedValue);
|
||||||
|
const result = await this._channel.expect(params);
|
||||||
|
if (result.received !== void 0)
|
||||||
|
result.received = (0, import_jsHandle.parseResult)(result.received);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function verifyLoadState(name, waitUntil) {
|
||||||
|
if (waitUntil === "networkidle0")
|
||||||
|
waitUntil = "networkidle";
|
||||||
|
if (!import_types.kLifecycleEvents.has(waitUntil))
|
||||||
|
throw new Error(`${name}: expected one of (load|domcontentloaded|networkidle|commit)`);
|
||||||
|
return waitUntil;
|
||||||
|
}
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
Frame,
|
||||||
|
verifyLoadState
|
||||||
|
});
|
||||||
87
node_modules/playwright-core/lib/client/harRouter.js
generated
vendored
Normal file
87
node_modules/playwright-core/lib/client/harRouter.js
generated
vendored
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
"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 harRouter_exports = {};
|
||||||
|
__export(harRouter_exports, {
|
||||||
|
HarRouter: () => HarRouter
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(harRouter_exports);
|
||||||
|
class HarRouter {
|
||||||
|
static async create(localUtils, file, notFoundAction, options) {
|
||||||
|
const { harId, error } = await localUtils.harOpen({ file });
|
||||||
|
if (error)
|
||||||
|
throw new Error(error);
|
||||||
|
return new HarRouter(localUtils, harId, notFoundAction, options);
|
||||||
|
}
|
||||||
|
constructor(localUtils, harId, notFoundAction, options) {
|
||||||
|
this._localUtils = localUtils;
|
||||||
|
this._harId = harId;
|
||||||
|
this._options = options;
|
||||||
|
this._notFoundAction = notFoundAction;
|
||||||
|
}
|
||||||
|
async _handle(route) {
|
||||||
|
const request = route.request();
|
||||||
|
const response = await this._localUtils.harLookup({
|
||||||
|
harId: this._harId,
|
||||||
|
url: request.url(),
|
||||||
|
method: request.method(),
|
||||||
|
headers: await request.headersArray(),
|
||||||
|
postData: request.postDataBuffer() || void 0,
|
||||||
|
isNavigationRequest: request.isNavigationRequest()
|
||||||
|
});
|
||||||
|
if (response.action === "redirect") {
|
||||||
|
route._platform.log("api", `HAR: ${route.request().url()} redirected to ${response.redirectURL}`);
|
||||||
|
await route._redirectNavigationRequest(response.redirectURL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (response.action === "fulfill") {
|
||||||
|
if (response.status === -1)
|
||||||
|
return;
|
||||||
|
await route.fulfill({
|
||||||
|
status: response.status,
|
||||||
|
headers: Object.fromEntries(response.headers.map((h) => [h.name, h.value])),
|
||||||
|
body: response.body
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (response.action === "error")
|
||||||
|
route._platform.log("api", "HAR: " + response.message);
|
||||||
|
if (this._notFoundAction === "abort") {
|
||||||
|
await route.abort();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await route.fallback();
|
||||||
|
}
|
||||||
|
async addContextRoute(context) {
|
||||||
|
await context.route(this._options.urlMatch || "**/*", (route) => this._handle(route));
|
||||||
|
}
|
||||||
|
async addPageRoute(page) {
|
||||||
|
await page.route(this._options.urlMatch || "**/*", (route) => this._handle(route));
|
||||||
|
}
|
||||||
|
async [Symbol.asyncDispose]() {
|
||||||
|
await this.dispose();
|
||||||
|
}
|
||||||
|
dispose() {
|
||||||
|
this._localUtils.harClose({ harId: this._harId }).catch(() => {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
HarRouter
|
||||||
|
});
|
||||||
84
node_modules/playwright-core/lib/client/input.js
generated
vendored
Normal file
84
node_modules/playwright-core/lib/client/input.js
generated
vendored
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
"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 input_exports = {};
|
||||||
|
__export(input_exports, {
|
||||||
|
Keyboard: () => Keyboard,
|
||||||
|
Mouse: () => Mouse,
|
||||||
|
Touchscreen: () => Touchscreen
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(input_exports);
|
||||||
|
class Keyboard {
|
||||||
|
constructor(page) {
|
||||||
|
this._page = page;
|
||||||
|
}
|
||||||
|
async down(key) {
|
||||||
|
await this._page._channel.keyboardDown({ key });
|
||||||
|
}
|
||||||
|
async up(key) {
|
||||||
|
await this._page._channel.keyboardUp({ key });
|
||||||
|
}
|
||||||
|
async insertText(text) {
|
||||||
|
await this._page._channel.keyboardInsertText({ text });
|
||||||
|
}
|
||||||
|
async type(text, options = {}) {
|
||||||
|
await this._page._channel.keyboardType({ text, ...options });
|
||||||
|
}
|
||||||
|
async press(key, options = {}) {
|
||||||
|
await this._page._channel.keyboardPress({ key, ...options });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Mouse {
|
||||||
|
constructor(page) {
|
||||||
|
this._page = page;
|
||||||
|
}
|
||||||
|
async move(x, y, options = {}) {
|
||||||
|
await this._page._channel.mouseMove({ x, y, ...options });
|
||||||
|
}
|
||||||
|
async down(options = {}) {
|
||||||
|
await this._page._channel.mouseDown({ ...options });
|
||||||
|
}
|
||||||
|
async up(options = {}) {
|
||||||
|
await this._page._channel.mouseUp(options);
|
||||||
|
}
|
||||||
|
async click(x, y, options = {}) {
|
||||||
|
await this._page._channel.mouseClick({ x, y, ...options });
|
||||||
|
}
|
||||||
|
async dblclick(x, y, options = {}) {
|
||||||
|
await this._page._wrapApiCall(async () => {
|
||||||
|
await this.click(x, y, { ...options, clickCount: 2 });
|
||||||
|
}, { title: "Double click" });
|
||||||
|
}
|
||||||
|
async wheel(deltaX, deltaY) {
|
||||||
|
await this._page._channel.mouseWheel({ deltaX, deltaY });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Touchscreen {
|
||||||
|
constructor(page) {
|
||||||
|
this._page = page;
|
||||||
|
}
|
||||||
|
async tap(x, y) {
|
||||||
|
await this._page._channel.touchscreenTap({ x, y });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
Keyboard,
|
||||||
|
Mouse,
|
||||||
|
Touchscreen
|
||||||
|
});
|
||||||
109
node_modules/playwright-core/lib/client/jsHandle.js
generated
vendored
Normal file
109
node_modules/playwright-core/lib/client/jsHandle.js
generated
vendored
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
"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 jsHandle_exports = {};
|
||||||
|
__export(jsHandle_exports, {
|
||||||
|
JSHandle: () => JSHandle,
|
||||||
|
assertMaxArguments: () => assertMaxArguments,
|
||||||
|
parseResult: () => parseResult,
|
||||||
|
serializeArgument: () => serializeArgument
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(jsHandle_exports);
|
||||||
|
var import_channelOwner = require("./channelOwner");
|
||||||
|
var import_errors = require("./errors");
|
||||||
|
var import_serializers = require("../protocol/serializers");
|
||||||
|
class JSHandle extends import_channelOwner.ChannelOwner {
|
||||||
|
static from(handle) {
|
||||||
|
return handle._object;
|
||||||
|
}
|
||||||
|
constructor(parent, type, guid, initializer) {
|
||||||
|
super(parent, type, guid, initializer);
|
||||||
|
this._preview = this._initializer.preview;
|
||||||
|
this._channel.on("previewUpdated", ({ preview }) => this._preview = preview);
|
||||||
|
}
|
||||||
|
async evaluate(pageFunction, arg) {
|
||||||
|
const result = await this._channel.evaluateExpression({ expression: String(pageFunction), isFunction: typeof pageFunction === "function", arg: serializeArgument(arg) });
|
||||||
|
return parseResult(result.value);
|
||||||
|
}
|
||||||
|
async _evaluateFunction(functionDeclaration) {
|
||||||
|
const result = await this._channel.evaluateExpression({ expression: functionDeclaration, isFunction: true, arg: serializeArgument(void 0) });
|
||||||
|
return parseResult(result.value);
|
||||||
|
}
|
||||||
|
async evaluateHandle(pageFunction, arg) {
|
||||||
|
const result = await this._channel.evaluateExpressionHandle({ expression: String(pageFunction), isFunction: typeof pageFunction === "function", arg: serializeArgument(arg) });
|
||||||
|
return JSHandle.from(result.handle);
|
||||||
|
}
|
||||||
|
async getProperty(propertyName) {
|
||||||
|
const result = await this._channel.getProperty({ name: propertyName });
|
||||||
|
return JSHandle.from(result.handle);
|
||||||
|
}
|
||||||
|
async getProperties() {
|
||||||
|
const map = /* @__PURE__ */ new Map();
|
||||||
|
for (const { name, value } of (await this._channel.getPropertyList()).properties)
|
||||||
|
map.set(name, JSHandle.from(value));
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
async jsonValue() {
|
||||||
|
return parseResult((await this._channel.jsonValue()).value);
|
||||||
|
}
|
||||||
|
asElement() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
async [Symbol.asyncDispose]() {
|
||||||
|
await this.dispose();
|
||||||
|
}
|
||||||
|
async dispose() {
|
||||||
|
try {
|
||||||
|
await this._channel.dispose();
|
||||||
|
} catch (e) {
|
||||||
|
if ((0, import_errors.isTargetClosedError)(e))
|
||||||
|
return;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
toString() {
|
||||||
|
return this._preview;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function serializeArgument(arg) {
|
||||||
|
const handles = [];
|
||||||
|
const pushHandle = (channel) => {
|
||||||
|
handles.push(channel);
|
||||||
|
return handles.length - 1;
|
||||||
|
};
|
||||||
|
const value = (0, import_serializers.serializeValue)(arg, (value2) => {
|
||||||
|
if (value2 instanceof JSHandle)
|
||||||
|
return { h: pushHandle(value2._channel) };
|
||||||
|
return { fallThrough: value2 };
|
||||||
|
});
|
||||||
|
return { value, handles };
|
||||||
|
}
|
||||||
|
function parseResult(value) {
|
||||||
|
return (0, import_serializers.parseSerializedValue)(value, void 0);
|
||||||
|
}
|
||||||
|
function assertMaxArguments(count, max) {
|
||||||
|
if (count > max)
|
||||||
|
throw new Error("Too many arguments. If you need to pass more than 1 argument to the function wrap them in an object.");
|
||||||
|
}
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
JSHandle,
|
||||||
|
assertMaxArguments,
|
||||||
|
parseResult,
|
||||||
|
serializeArgument
|
||||||
|
});
|
||||||
39
node_modules/playwright-core/lib/client/jsonPipe.js
generated
vendored
Normal file
39
node_modules/playwright-core/lib/client/jsonPipe.js
generated
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
"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 jsonPipe_exports = {};
|
||||||
|
__export(jsonPipe_exports, {
|
||||||
|
JsonPipe: () => JsonPipe
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(jsonPipe_exports);
|
||||||
|
var import_channelOwner = require("./channelOwner");
|
||||||
|
class JsonPipe extends import_channelOwner.ChannelOwner {
|
||||||
|
static from(jsonPipe) {
|
||||||
|
return jsonPipe._object;
|
||||||
|
}
|
||||||
|
constructor(parent, type, guid, initializer) {
|
||||||
|
super(parent, type, guid, initializer);
|
||||||
|
}
|
||||||
|
channel() {
|
||||||
|
return this._channel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
JsonPipe
|
||||||
|
});
|
||||||
60
node_modules/playwright-core/lib/client/localUtils.js
generated
vendored
Normal file
60
node_modules/playwright-core/lib/client/localUtils.js
generated
vendored
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
"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 localUtils_exports = {};
|
||||||
|
__export(localUtils_exports, {
|
||||||
|
LocalUtils: () => LocalUtils
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(localUtils_exports);
|
||||||
|
var import_channelOwner = require("./channelOwner");
|
||||||
|
class LocalUtils extends import_channelOwner.ChannelOwner {
|
||||||
|
constructor(parent, type, guid, initializer) {
|
||||||
|
super(parent, type, guid, initializer);
|
||||||
|
this.devices = {};
|
||||||
|
for (const { name, descriptor } of initializer.deviceDescriptors)
|
||||||
|
this.devices[name] = descriptor;
|
||||||
|
}
|
||||||
|
async zip(params) {
|
||||||
|
return await this._channel.zip(params);
|
||||||
|
}
|
||||||
|
async harOpen(params) {
|
||||||
|
return await this._channel.harOpen(params);
|
||||||
|
}
|
||||||
|
async harLookup(params) {
|
||||||
|
return await this._channel.harLookup(params);
|
||||||
|
}
|
||||||
|
async harClose(params) {
|
||||||
|
return await this._channel.harClose(params);
|
||||||
|
}
|
||||||
|
async harUnzip(params) {
|
||||||
|
return await this._channel.harUnzip(params);
|
||||||
|
}
|
||||||
|
async tracingStarted(params) {
|
||||||
|
return await this._channel.tracingStarted(params);
|
||||||
|
}
|
||||||
|
async traceDiscarded(params) {
|
||||||
|
return await this._channel.traceDiscarded(params);
|
||||||
|
}
|
||||||
|
async addStackToTracingNoReply(params) {
|
||||||
|
return await this._channel.addStackToTracingNoReply(params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
LocalUtils
|
||||||
|
});
|
||||||
366
node_modules/playwright-core/lib/client/locator.js
generated
vendored
Normal file
366
node_modules/playwright-core/lib/client/locator.js
generated
vendored
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
"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 locator_exports = {};
|
||||||
|
__export(locator_exports, {
|
||||||
|
FrameLocator: () => FrameLocator,
|
||||||
|
Locator: () => Locator,
|
||||||
|
setTestIdAttribute: () => setTestIdAttribute,
|
||||||
|
testIdAttributeName: () => testIdAttributeName
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(locator_exports);
|
||||||
|
var import_elementHandle = require("./elementHandle");
|
||||||
|
var import_locatorGenerators = require("../utils/isomorphic/locatorGenerators");
|
||||||
|
var import_locatorUtils = require("../utils/isomorphic/locatorUtils");
|
||||||
|
var import_stringUtils = require("../utils/isomorphic/stringUtils");
|
||||||
|
var import_rtti = require("../utils/isomorphic/rtti");
|
||||||
|
var import_time = require("../utils/isomorphic/time");
|
||||||
|
class Locator {
|
||||||
|
constructor(frame, selector, options) {
|
||||||
|
this._frame = frame;
|
||||||
|
this._selector = selector;
|
||||||
|
if (options?.hasText)
|
||||||
|
this._selector += ` >> internal:has-text=${(0, import_stringUtils.escapeForTextSelector)(options.hasText, false)}`;
|
||||||
|
if (options?.hasNotText)
|
||||||
|
this._selector += ` >> internal:has-not-text=${(0, import_stringUtils.escapeForTextSelector)(options.hasNotText, false)}`;
|
||||||
|
if (options?.has) {
|
||||||
|
const locator = options.has;
|
||||||
|
if (locator._frame !== frame)
|
||||||
|
throw new Error(`Inner "has" locator must belong to the same frame.`);
|
||||||
|
this._selector += ` >> internal:has=` + JSON.stringify(locator._selector);
|
||||||
|
}
|
||||||
|
if (options?.hasNot) {
|
||||||
|
const locator = options.hasNot;
|
||||||
|
if (locator._frame !== frame)
|
||||||
|
throw new Error(`Inner "hasNot" locator must belong to the same frame.`);
|
||||||
|
this._selector += ` >> internal:has-not=` + JSON.stringify(locator._selector);
|
||||||
|
}
|
||||||
|
if (options?.visible !== void 0)
|
||||||
|
this._selector += ` >> visible=${options.visible ? "true" : "false"}`;
|
||||||
|
if (this._frame._platform.inspectCustom)
|
||||||
|
this[this._frame._platform.inspectCustom] = () => this._inspect();
|
||||||
|
}
|
||||||
|
async _withElement(task, options) {
|
||||||
|
const timeout = this._frame._timeout({ timeout: options.timeout });
|
||||||
|
const deadline = timeout ? (0, import_time.monotonicTime)() + timeout : 0;
|
||||||
|
return await this._frame._wrapApiCall(async () => {
|
||||||
|
const result = await this._frame._channel.waitForSelector({ selector: this._selector, strict: true, state: "attached", timeout });
|
||||||
|
const handle = import_elementHandle.ElementHandle.fromNullable(result.element);
|
||||||
|
if (!handle)
|
||||||
|
throw new Error(`Could not resolve ${this._selector} to DOM Element`);
|
||||||
|
try {
|
||||||
|
return await task(handle, deadline ? deadline - (0, import_time.monotonicTime)() : 0);
|
||||||
|
} finally {
|
||||||
|
await handle.dispose();
|
||||||
|
}
|
||||||
|
}, { title: options.title, internal: options.internal });
|
||||||
|
}
|
||||||
|
_equals(locator) {
|
||||||
|
return this._frame === locator._frame && this._selector === locator._selector;
|
||||||
|
}
|
||||||
|
page() {
|
||||||
|
return this._frame.page();
|
||||||
|
}
|
||||||
|
async boundingBox(options) {
|
||||||
|
return await this._withElement((h) => h.boundingBox(), { title: "Bounding box", timeout: options?.timeout });
|
||||||
|
}
|
||||||
|
async check(options = {}) {
|
||||||
|
return await this._frame.check(this._selector, { strict: true, ...options });
|
||||||
|
}
|
||||||
|
async click(options = {}) {
|
||||||
|
return await this._frame.click(this._selector, { strict: true, ...options });
|
||||||
|
}
|
||||||
|
async dblclick(options = {}) {
|
||||||
|
await this._frame.dblclick(this._selector, { strict: true, ...options });
|
||||||
|
}
|
||||||
|
async dispatchEvent(type, eventInit = {}, options) {
|
||||||
|
return await this._frame.dispatchEvent(this._selector, type, eventInit, { strict: true, ...options });
|
||||||
|
}
|
||||||
|
async dragTo(target, options = {}) {
|
||||||
|
return await this._frame.dragAndDrop(this._selector, target._selector, {
|
||||||
|
strict: true,
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async evaluate(pageFunction, arg, options) {
|
||||||
|
return await this._withElement((h) => h.evaluate(pageFunction, arg), { title: "Evaluate", timeout: options?.timeout });
|
||||||
|
}
|
||||||
|
async _evaluateFunction(functionDeclaration, options) {
|
||||||
|
return await this._withElement((h) => h._evaluateFunction(functionDeclaration), { title: "Evaluate", timeout: options?.timeout });
|
||||||
|
}
|
||||||
|
async evaluateAll(pageFunction, arg) {
|
||||||
|
return await this._frame.$$eval(this._selector, pageFunction, arg);
|
||||||
|
}
|
||||||
|
async evaluateHandle(pageFunction, arg, options) {
|
||||||
|
return await this._withElement((h) => h.evaluateHandle(pageFunction, arg), { title: "Evaluate", timeout: options?.timeout });
|
||||||
|
}
|
||||||
|
async fill(value, options = {}) {
|
||||||
|
return await this._frame.fill(this._selector, value, { strict: true, ...options });
|
||||||
|
}
|
||||||
|
async clear(options = {}) {
|
||||||
|
await this._frame._wrapApiCall(() => this.fill("", options), { title: "Clear" });
|
||||||
|
}
|
||||||
|
async _highlight() {
|
||||||
|
return await this._frame._highlight(this._selector);
|
||||||
|
}
|
||||||
|
async highlight() {
|
||||||
|
return await this._frame._highlight(this._selector);
|
||||||
|
}
|
||||||
|
locator(selectorOrLocator, options) {
|
||||||
|
if ((0, import_rtti.isString)(selectorOrLocator))
|
||||||
|
return new Locator(this._frame, this._selector + " >> " + selectorOrLocator, options);
|
||||||
|
if (selectorOrLocator._frame !== this._frame)
|
||||||
|
throw new Error(`Locators must belong to the same frame.`);
|
||||||
|
return new Locator(this._frame, this._selector + " >> internal:chain=" + JSON.stringify(selectorOrLocator._selector), options);
|
||||||
|
}
|
||||||
|
getByTestId(testId) {
|
||||||
|
return this.locator((0, import_locatorUtils.getByTestIdSelector)(testIdAttributeName(), testId));
|
||||||
|
}
|
||||||
|
getByAltText(text, options) {
|
||||||
|
return this.locator((0, import_locatorUtils.getByAltTextSelector)(text, options));
|
||||||
|
}
|
||||||
|
getByLabel(text, options) {
|
||||||
|
return this.locator((0, import_locatorUtils.getByLabelSelector)(text, options));
|
||||||
|
}
|
||||||
|
getByPlaceholder(text, options) {
|
||||||
|
return this.locator((0, import_locatorUtils.getByPlaceholderSelector)(text, options));
|
||||||
|
}
|
||||||
|
getByText(text, options) {
|
||||||
|
return this.locator((0, import_locatorUtils.getByTextSelector)(text, options));
|
||||||
|
}
|
||||||
|
getByTitle(text, options) {
|
||||||
|
return this.locator((0, import_locatorUtils.getByTitleSelector)(text, options));
|
||||||
|
}
|
||||||
|
getByRole(role, options = {}) {
|
||||||
|
return this.locator((0, import_locatorUtils.getByRoleSelector)(role, options));
|
||||||
|
}
|
||||||
|
frameLocator(selector) {
|
||||||
|
return new FrameLocator(this._frame, this._selector + " >> " + selector);
|
||||||
|
}
|
||||||
|
filter(options) {
|
||||||
|
return new Locator(this._frame, this._selector, options);
|
||||||
|
}
|
||||||
|
async elementHandle(options) {
|
||||||
|
return await this._frame.waitForSelector(this._selector, { strict: true, state: "attached", ...options });
|
||||||
|
}
|
||||||
|
async elementHandles() {
|
||||||
|
return await this._frame.$$(this._selector);
|
||||||
|
}
|
||||||
|
contentFrame() {
|
||||||
|
return new FrameLocator(this._frame, this._selector);
|
||||||
|
}
|
||||||
|
describe(description) {
|
||||||
|
return new Locator(this._frame, this._selector + " >> internal:describe=" + JSON.stringify(description));
|
||||||
|
}
|
||||||
|
first() {
|
||||||
|
return new Locator(this._frame, this._selector + " >> nth=0");
|
||||||
|
}
|
||||||
|
last() {
|
||||||
|
return new Locator(this._frame, this._selector + ` >> nth=-1`);
|
||||||
|
}
|
||||||
|
nth(index) {
|
||||||
|
return new Locator(this._frame, this._selector + ` >> nth=${index}`);
|
||||||
|
}
|
||||||
|
and(locator) {
|
||||||
|
if (locator._frame !== this._frame)
|
||||||
|
throw new Error(`Locators must belong to the same frame.`);
|
||||||
|
return new Locator(this._frame, this._selector + ` >> internal:and=` + JSON.stringify(locator._selector));
|
||||||
|
}
|
||||||
|
or(locator) {
|
||||||
|
if (locator._frame !== this._frame)
|
||||||
|
throw new Error(`Locators must belong to the same frame.`);
|
||||||
|
return new Locator(this._frame, this._selector + ` >> internal:or=` + JSON.stringify(locator._selector));
|
||||||
|
}
|
||||||
|
async focus(options) {
|
||||||
|
return await this._frame.focus(this._selector, { strict: true, ...options });
|
||||||
|
}
|
||||||
|
async blur(options) {
|
||||||
|
await this._frame._channel.blur({ selector: this._selector, strict: true, ...options, timeout: this._frame._timeout(options) });
|
||||||
|
}
|
||||||
|
// options are only here for testing
|
||||||
|
async count(_options) {
|
||||||
|
return await this._frame._queryCount(this._selector, _options);
|
||||||
|
}
|
||||||
|
async _resolveSelector() {
|
||||||
|
return await this._frame._channel.resolveSelector({ selector: this._selector });
|
||||||
|
}
|
||||||
|
async getAttribute(name, options) {
|
||||||
|
return await this._frame.getAttribute(this._selector, name, { strict: true, ...options });
|
||||||
|
}
|
||||||
|
async hover(options = {}) {
|
||||||
|
return await this._frame.hover(this._selector, { strict: true, ...options });
|
||||||
|
}
|
||||||
|
async innerHTML(options) {
|
||||||
|
return await this._frame.innerHTML(this._selector, { strict: true, ...options });
|
||||||
|
}
|
||||||
|
async innerText(options) {
|
||||||
|
return await this._frame.innerText(this._selector, { strict: true, ...options });
|
||||||
|
}
|
||||||
|
async inputValue(options) {
|
||||||
|
return await this._frame.inputValue(this._selector, { strict: true, ...options });
|
||||||
|
}
|
||||||
|
async isChecked(options) {
|
||||||
|
return await this._frame.isChecked(this._selector, { strict: true, ...options });
|
||||||
|
}
|
||||||
|
async isDisabled(options) {
|
||||||
|
return await this._frame.isDisabled(this._selector, { strict: true, ...options });
|
||||||
|
}
|
||||||
|
async isEditable(options) {
|
||||||
|
return await this._frame.isEditable(this._selector, { strict: true, ...options });
|
||||||
|
}
|
||||||
|
async isEnabled(options) {
|
||||||
|
return await this._frame.isEnabled(this._selector, { strict: true, ...options });
|
||||||
|
}
|
||||||
|
async isHidden(options) {
|
||||||
|
return await this._frame.isHidden(this._selector, { strict: true, ...options });
|
||||||
|
}
|
||||||
|
async isVisible(options) {
|
||||||
|
return await this._frame.isVisible(this._selector, { strict: true, ...options });
|
||||||
|
}
|
||||||
|
async press(key, options = {}) {
|
||||||
|
return await this._frame.press(this._selector, key, { strict: true, ...options });
|
||||||
|
}
|
||||||
|
async screenshot(options = {}) {
|
||||||
|
const mask = options.mask;
|
||||||
|
return await this._withElement((h, timeout) => h.screenshot({ ...options, mask, timeout }), { title: "Screenshot", timeout: options.timeout });
|
||||||
|
}
|
||||||
|
async ariaSnapshot(options) {
|
||||||
|
const result = await this._frame._channel.ariaSnapshot({ ...options, selector: this._selector, timeout: this._frame._timeout(options) });
|
||||||
|
return result.snapshot;
|
||||||
|
}
|
||||||
|
async scrollIntoViewIfNeeded(options = {}) {
|
||||||
|
return await this._withElement((h, timeout) => h.scrollIntoViewIfNeeded({ ...options, timeout }), { title: "Scroll into view", timeout: options.timeout });
|
||||||
|
}
|
||||||
|
async selectOption(values, options = {}) {
|
||||||
|
return await this._frame.selectOption(this._selector, values, { strict: true, ...options });
|
||||||
|
}
|
||||||
|
async selectText(options = {}) {
|
||||||
|
return await this._withElement((h, timeout) => h.selectText({ ...options, timeout }), { title: "Select text", timeout: options.timeout });
|
||||||
|
}
|
||||||
|
async setChecked(checked, options) {
|
||||||
|
if (checked)
|
||||||
|
await this.check(options);
|
||||||
|
else
|
||||||
|
await this.uncheck(options);
|
||||||
|
}
|
||||||
|
async setInputFiles(files, options = {}) {
|
||||||
|
return await this._frame.setInputFiles(this._selector, files, { strict: true, ...options });
|
||||||
|
}
|
||||||
|
async tap(options = {}) {
|
||||||
|
return await this._frame.tap(this._selector, { strict: true, ...options });
|
||||||
|
}
|
||||||
|
async textContent(options) {
|
||||||
|
return await this._frame.textContent(this._selector, { strict: true, ...options });
|
||||||
|
}
|
||||||
|
async type(text, options = {}) {
|
||||||
|
return await this._frame.type(this._selector, text, { strict: true, ...options });
|
||||||
|
}
|
||||||
|
async pressSequentially(text, options = {}) {
|
||||||
|
return await this.type(text, options);
|
||||||
|
}
|
||||||
|
async uncheck(options = {}) {
|
||||||
|
return await this._frame.uncheck(this._selector, { strict: true, ...options });
|
||||||
|
}
|
||||||
|
async all() {
|
||||||
|
return new Array(await this.count()).fill(0).map((e, i) => this.nth(i));
|
||||||
|
}
|
||||||
|
async allInnerTexts() {
|
||||||
|
return await this._frame.$$eval(this._selector, (ee) => ee.map((e) => e.innerText));
|
||||||
|
}
|
||||||
|
async allTextContents() {
|
||||||
|
return await this._frame.$$eval(this._selector, (ee) => ee.map((e) => e.textContent || ""));
|
||||||
|
}
|
||||||
|
async waitFor(options) {
|
||||||
|
await this._frame._channel.waitForSelector({ selector: this._selector, strict: true, omitReturnValue: true, ...options, timeout: this._frame._timeout(options) });
|
||||||
|
}
|
||||||
|
async _expect(expression, options) {
|
||||||
|
return this._frame._expect(expression, {
|
||||||
|
...options,
|
||||||
|
selector: this._selector
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_inspect() {
|
||||||
|
return this.toString();
|
||||||
|
}
|
||||||
|
toString() {
|
||||||
|
return (0, import_locatorGenerators.asLocator)("javascript", this._selector);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class FrameLocator {
|
||||||
|
constructor(frame, selector) {
|
||||||
|
this._frame = frame;
|
||||||
|
this._frameSelector = selector;
|
||||||
|
}
|
||||||
|
locator(selectorOrLocator, options) {
|
||||||
|
if ((0, import_rtti.isString)(selectorOrLocator))
|
||||||
|
return new Locator(this._frame, this._frameSelector + " >> internal:control=enter-frame >> " + selectorOrLocator, options);
|
||||||
|
if (selectorOrLocator._frame !== this._frame)
|
||||||
|
throw new Error(`Locators must belong to the same frame.`);
|
||||||
|
return new Locator(this._frame, this._frameSelector + " >> internal:control=enter-frame >> " + selectorOrLocator._selector, options);
|
||||||
|
}
|
||||||
|
getByTestId(testId) {
|
||||||
|
return this.locator((0, import_locatorUtils.getByTestIdSelector)(testIdAttributeName(), testId));
|
||||||
|
}
|
||||||
|
getByAltText(text, options) {
|
||||||
|
return this.locator((0, import_locatorUtils.getByAltTextSelector)(text, options));
|
||||||
|
}
|
||||||
|
getByLabel(text, options) {
|
||||||
|
return this.locator((0, import_locatorUtils.getByLabelSelector)(text, options));
|
||||||
|
}
|
||||||
|
getByPlaceholder(text, options) {
|
||||||
|
return this.locator((0, import_locatorUtils.getByPlaceholderSelector)(text, options));
|
||||||
|
}
|
||||||
|
getByText(text, options) {
|
||||||
|
return this.locator((0, import_locatorUtils.getByTextSelector)(text, options));
|
||||||
|
}
|
||||||
|
getByTitle(text, options) {
|
||||||
|
return this.locator((0, import_locatorUtils.getByTitleSelector)(text, options));
|
||||||
|
}
|
||||||
|
getByRole(role, options = {}) {
|
||||||
|
return this.locator((0, import_locatorUtils.getByRoleSelector)(role, options));
|
||||||
|
}
|
||||||
|
owner() {
|
||||||
|
return new Locator(this._frame, this._frameSelector);
|
||||||
|
}
|
||||||
|
frameLocator(selector) {
|
||||||
|
return new FrameLocator(this._frame, this._frameSelector + " >> internal:control=enter-frame >> " + selector);
|
||||||
|
}
|
||||||
|
first() {
|
||||||
|
return new FrameLocator(this._frame, this._frameSelector + " >> nth=0");
|
||||||
|
}
|
||||||
|
last() {
|
||||||
|
return new FrameLocator(this._frame, this._frameSelector + ` >> nth=-1`);
|
||||||
|
}
|
||||||
|
nth(index) {
|
||||||
|
return new FrameLocator(this._frame, this._frameSelector + ` >> nth=${index}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _testIdAttributeName = "data-testid";
|
||||||
|
function testIdAttributeName() {
|
||||||
|
return _testIdAttributeName;
|
||||||
|
}
|
||||||
|
function setTestIdAttribute(attributeName) {
|
||||||
|
_testIdAttributeName = attributeName;
|
||||||
|
}
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
FrameLocator,
|
||||||
|
Locator,
|
||||||
|
setTestIdAttribute,
|
||||||
|
testIdAttributeName
|
||||||
|
});
|
||||||
744
node_modules/playwright-core/lib/client/network.js
generated
vendored
Normal file
744
node_modules/playwright-core/lib/client/network.js
generated
vendored
Normal file
@@ -0,0 +1,744 @@
|
|||||||
|
"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 network_exports = {};
|
||||||
|
__export(network_exports, {
|
||||||
|
RawHeaders: () => RawHeaders,
|
||||||
|
Request: () => Request,
|
||||||
|
Response: () => Response,
|
||||||
|
Route: () => Route,
|
||||||
|
RouteHandler: () => RouteHandler,
|
||||||
|
WebSocket: () => WebSocket,
|
||||||
|
WebSocketRoute: () => WebSocketRoute,
|
||||||
|
WebSocketRouteHandler: () => WebSocketRouteHandler,
|
||||||
|
validateHeaders: () => validateHeaders
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(network_exports);
|
||||||
|
var import_channelOwner = require("./channelOwner");
|
||||||
|
var import_errors = require("./errors");
|
||||||
|
var import_events = require("./events");
|
||||||
|
var import_fetch = require("./fetch");
|
||||||
|
var import_frame = require("./frame");
|
||||||
|
var import_waiter = require("./waiter");
|
||||||
|
var import_worker = require("./worker");
|
||||||
|
var import_assert = require("../utils/isomorphic/assert");
|
||||||
|
var import_headers = require("../utils/isomorphic/headers");
|
||||||
|
var import_urlMatch = require("../utils/isomorphic/urlMatch");
|
||||||
|
var import_manualPromise = require("../utils/isomorphic/manualPromise");
|
||||||
|
var import_multimap = require("../utils/isomorphic/multimap");
|
||||||
|
var import_rtti = require("../utils/isomorphic/rtti");
|
||||||
|
var import_stackTrace = require("../utils/isomorphic/stackTrace");
|
||||||
|
var import_mimeType = require("../utils/isomorphic/mimeType");
|
||||||
|
class Request extends import_channelOwner.ChannelOwner {
|
||||||
|
constructor(parent, type, guid, initializer) {
|
||||||
|
super(parent, type, guid, initializer);
|
||||||
|
this._redirectedFrom = null;
|
||||||
|
this._redirectedTo = null;
|
||||||
|
this._failureText = null;
|
||||||
|
this._fallbackOverrides = {};
|
||||||
|
this._redirectedFrom = Request.fromNullable(initializer.redirectedFrom);
|
||||||
|
if (this._redirectedFrom)
|
||||||
|
this._redirectedFrom._redirectedTo = this;
|
||||||
|
this._provisionalHeaders = new RawHeaders(initializer.headers);
|
||||||
|
this._timing = {
|
||||||
|
startTime: 0,
|
||||||
|
domainLookupStart: -1,
|
||||||
|
domainLookupEnd: -1,
|
||||||
|
connectStart: -1,
|
||||||
|
secureConnectionStart: -1,
|
||||||
|
connectEnd: -1,
|
||||||
|
requestStart: -1,
|
||||||
|
responseStart: -1,
|
||||||
|
responseEnd: -1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
static from(request) {
|
||||||
|
return request._object;
|
||||||
|
}
|
||||||
|
static fromNullable(request) {
|
||||||
|
return request ? Request.from(request) : null;
|
||||||
|
}
|
||||||
|
url() {
|
||||||
|
return this._fallbackOverrides.url || this._initializer.url;
|
||||||
|
}
|
||||||
|
resourceType() {
|
||||||
|
return this._initializer.resourceType;
|
||||||
|
}
|
||||||
|
method() {
|
||||||
|
return this._fallbackOverrides.method || this._initializer.method;
|
||||||
|
}
|
||||||
|
postData() {
|
||||||
|
return (this._fallbackOverrides.postDataBuffer || this._initializer.postData)?.toString("utf-8") || null;
|
||||||
|
}
|
||||||
|
postDataBuffer() {
|
||||||
|
return this._fallbackOverrides.postDataBuffer || this._initializer.postData || null;
|
||||||
|
}
|
||||||
|
postDataJSON() {
|
||||||
|
const postData = this.postData();
|
||||||
|
if (!postData)
|
||||||
|
return null;
|
||||||
|
const contentType = this.headers()["content-type"];
|
||||||
|
if (contentType?.includes("application/x-www-form-urlencoded")) {
|
||||||
|
const entries = {};
|
||||||
|
const parsed = new URLSearchParams(postData);
|
||||||
|
for (const [k, v] of parsed.entries())
|
||||||
|
entries[k] = v;
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.parse(postData);
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error("POST data is not a valid JSON object: " + postData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
|
headers() {
|
||||||
|
if (this._fallbackOverrides.headers)
|
||||||
|
return RawHeaders._fromHeadersObjectLossy(this._fallbackOverrides.headers).headers();
|
||||||
|
return this._provisionalHeaders.headers();
|
||||||
|
}
|
||||||
|
async _actualHeaders() {
|
||||||
|
if (this._fallbackOverrides.headers)
|
||||||
|
return RawHeaders._fromHeadersObjectLossy(this._fallbackOverrides.headers);
|
||||||
|
if (!this._actualHeadersPromise) {
|
||||||
|
this._actualHeadersPromise = this._wrapApiCall(async () => {
|
||||||
|
return new RawHeaders((await this._channel.rawRequestHeaders()).headers);
|
||||||
|
}, { internal: true });
|
||||||
|
}
|
||||||
|
return await this._actualHeadersPromise;
|
||||||
|
}
|
||||||
|
async allHeaders() {
|
||||||
|
return (await this._actualHeaders()).headers();
|
||||||
|
}
|
||||||
|
async headersArray() {
|
||||||
|
return (await this._actualHeaders()).headersArray();
|
||||||
|
}
|
||||||
|
async headerValue(name) {
|
||||||
|
return (await this._actualHeaders()).get(name);
|
||||||
|
}
|
||||||
|
async response() {
|
||||||
|
return Response.fromNullable((await this._channel.response()).response);
|
||||||
|
}
|
||||||
|
async _internalResponse() {
|
||||||
|
return Response.fromNullable((await this._channel.response()).response);
|
||||||
|
}
|
||||||
|
frame() {
|
||||||
|
if (!this._initializer.frame) {
|
||||||
|
(0, import_assert.assert)(this.serviceWorker());
|
||||||
|
throw new Error("Service Worker requests do not have an associated frame.");
|
||||||
|
}
|
||||||
|
const frame = import_frame.Frame.from(this._initializer.frame);
|
||||||
|
if (!frame._page) {
|
||||||
|
throw new Error([
|
||||||
|
"Frame for this navigation request is not available, because the request",
|
||||||
|
"was issued before the frame is created. You can check whether the request",
|
||||||
|
"is a navigation request by calling isNavigationRequest() method."
|
||||||
|
].join("\n"));
|
||||||
|
}
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
_safePage() {
|
||||||
|
return import_frame.Frame.fromNullable(this._initializer.frame)?._page || null;
|
||||||
|
}
|
||||||
|
serviceWorker() {
|
||||||
|
return this._initializer.serviceWorker ? import_worker.Worker.from(this._initializer.serviceWorker) : null;
|
||||||
|
}
|
||||||
|
isNavigationRequest() {
|
||||||
|
return this._initializer.isNavigationRequest;
|
||||||
|
}
|
||||||
|
redirectedFrom() {
|
||||||
|
return this._redirectedFrom;
|
||||||
|
}
|
||||||
|
redirectedTo() {
|
||||||
|
return this._redirectedTo;
|
||||||
|
}
|
||||||
|
failure() {
|
||||||
|
if (this._failureText === null)
|
||||||
|
return null;
|
||||||
|
return {
|
||||||
|
errorText: this._failureText
|
||||||
|
};
|
||||||
|
}
|
||||||
|
timing() {
|
||||||
|
return this._timing;
|
||||||
|
}
|
||||||
|
async sizes() {
|
||||||
|
const response = await this.response();
|
||||||
|
if (!response)
|
||||||
|
throw new Error("Unable to fetch sizes for failed request");
|
||||||
|
return (await response._channel.sizes()).sizes;
|
||||||
|
}
|
||||||
|
_setResponseEndTiming(responseEndTiming) {
|
||||||
|
this._timing.responseEnd = responseEndTiming;
|
||||||
|
if (this._timing.responseStart === -1)
|
||||||
|
this._timing.responseStart = responseEndTiming;
|
||||||
|
}
|
||||||
|
_finalRequest() {
|
||||||
|
return this._redirectedTo ? this._redirectedTo._finalRequest() : this;
|
||||||
|
}
|
||||||
|
_applyFallbackOverrides(overrides) {
|
||||||
|
if (overrides.url)
|
||||||
|
this._fallbackOverrides.url = overrides.url;
|
||||||
|
if (overrides.method)
|
||||||
|
this._fallbackOverrides.method = overrides.method;
|
||||||
|
if (overrides.headers)
|
||||||
|
this._fallbackOverrides.headers = overrides.headers;
|
||||||
|
if ((0, import_rtti.isString)(overrides.postData))
|
||||||
|
this._fallbackOverrides.postDataBuffer = Buffer.from(overrides.postData, "utf-8");
|
||||||
|
else if (overrides.postData instanceof Buffer)
|
||||||
|
this._fallbackOverrides.postDataBuffer = overrides.postData;
|
||||||
|
else if (overrides.postData)
|
||||||
|
this._fallbackOverrides.postDataBuffer = Buffer.from(JSON.stringify(overrides.postData), "utf-8");
|
||||||
|
}
|
||||||
|
_fallbackOverridesForContinue() {
|
||||||
|
return this._fallbackOverrides;
|
||||||
|
}
|
||||||
|
_targetClosedScope() {
|
||||||
|
return this.serviceWorker()?._closedScope || this._safePage()?._closedOrCrashedScope || new import_manualPromise.LongStandingScope();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Route extends import_channelOwner.ChannelOwner {
|
||||||
|
constructor(parent, type, guid, initializer) {
|
||||||
|
super(parent, type, guid, initializer);
|
||||||
|
this._handlingPromise = null;
|
||||||
|
this._didThrow = false;
|
||||||
|
}
|
||||||
|
static from(route) {
|
||||||
|
return route._object;
|
||||||
|
}
|
||||||
|
request() {
|
||||||
|
return Request.from(this._initializer.request);
|
||||||
|
}
|
||||||
|
async _raceWithTargetClose(promise) {
|
||||||
|
return await this.request()._targetClosedScope().safeRace(promise);
|
||||||
|
}
|
||||||
|
async _startHandling() {
|
||||||
|
this._handlingPromise = new import_manualPromise.ManualPromise();
|
||||||
|
return await this._handlingPromise;
|
||||||
|
}
|
||||||
|
async fallback(options = {}) {
|
||||||
|
this._checkNotHandled();
|
||||||
|
this.request()._applyFallbackOverrides(options);
|
||||||
|
this._reportHandled(false);
|
||||||
|
}
|
||||||
|
async abort(errorCode) {
|
||||||
|
await this._handleRoute(async () => {
|
||||||
|
await this._raceWithTargetClose(this._channel.abort({ errorCode }));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async _redirectNavigationRequest(url) {
|
||||||
|
await this._handleRoute(async () => {
|
||||||
|
await this._raceWithTargetClose(this._channel.redirectNavigationRequest({ url }));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async fetch(options = {}) {
|
||||||
|
return await this._wrapApiCall(async () => {
|
||||||
|
return await this._context.request._innerFetch({ request: this.request(), data: options.postData, ...options });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async fulfill(options = {}) {
|
||||||
|
await this._handleRoute(async () => {
|
||||||
|
await this._innerFulfill(options);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async _handleRoute(callback) {
|
||||||
|
this._checkNotHandled();
|
||||||
|
try {
|
||||||
|
await callback();
|
||||||
|
this._reportHandled(true);
|
||||||
|
} catch (e) {
|
||||||
|
this._didThrow = true;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async _innerFulfill(options = {}) {
|
||||||
|
let fetchResponseUid;
|
||||||
|
let { status: statusOption, headers: headersOption, body } = options;
|
||||||
|
if (options.json !== void 0) {
|
||||||
|
(0, import_assert.assert)(options.body === void 0, "Can specify either body or json parameters");
|
||||||
|
body = JSON.stringify(options.json);
|
||||||
|
}
|
||||||
|
if (options.response instanceof import_fetch.APIResponse) {
|
||||||
|
statusOption ??= options.response.status();
|
||||||
|
headersOption ??= options.response.headers();
|
||||||
|
if (body === void 0 && options.path === void 0) {
|
||||||
|
if (options.response._request._connection === this._connection)
|
||||||
|
fetchResponseUid = options.response._fetchUid();
|
||||||
|
else
|
||||||
|
body = await options.response.body();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let isBase64 = false;
|
||||||
|
let length = 0;
|
||||||
|
if (options.path) {
|
||||||
|
const buffer = await this._platform.fs().promises.readFile(options.path);
|
||||||
|
body = buffer.toString("base64");
|
||||||
|
isBase64 = true;
|
||||||
|
length = buffer.length;
|
||||||
|
} else if ((0, import_rtti.isString)(body)) {
|
||||||
|
isBase64 = false;
|
||||||
|
length = Buffer.byteLength(body);
|
||||||
|
} else if (body) {
|
||||||
|
length = body.length;
|
||||||
|
body = body.toString("base64");
|
||||||
|
isBase64 = true;
|
||||||
|
}
|
||||||
|
const headers = {};
|
||||||
|
for (const header of Object.keys(headersOption || {}))
|
||||||
|
headers[header.toLowerCase()] = String(headersOption[header]);
|
||||||
|
if (options.contentType)
|
||||||
|
headers["content-type"] = String(options.contentType);
|
||||||
|
else if (options.json)
|
||||||
|
headers["content-type"] = "application/json";
|
||||||
|
else if (options.path)
|
||||||
|
headers["content-type"] = (0, import_mimeType.getMimeTypeForPath)(options.path) || "application/octet-stream";
|
||||||
|
if (length && !("content-length" in headers))
|
||||||
|
headers["content-length"] = String(length);
|
||||||
|
await this._raceWithTargetClose(this._channel.fulfill({
|
||||||
|
status: statusOption || 200,
|
||||||
|
headers: (0, import_headers.headersObjectToArray)(headers),
|
||||||
|
body,
|
||||||
|
isBase64,
|
||||||
|
fetchResponseUid
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
async continue(options = {}) {
|
||||||
|
await this._handleRoute(async () => {
|
||||||
|
this.request()._applyFallbackOverrides(options);
|
||||||
|
await this._innerContinue(
|
||||||
|
false
|
||||||
|
/* isFallback */
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_checkNotHandled() {
|
||||||
|
if (!this._handlingPromise)
|
||||||
|
throw new Error("Route is already handled!");
|
||||||
|
}
|
||||||
|
_reportHandled(done) {
|
||||||
|
const chain = this._handlingPromise;
|
||||||
|
this._handlingPromise = null;
|
||||||
|
chain.resolve(done);
|
||||||
|
}
|
||||||
|
async _innerContinue(isFallback) {
|
||||||
|
const options = this.request()._fallbackOverridesForContinue();
|
||||||
|
return await this._raceWithTargetClose(this._channel.continue({
|
||||||
|
url: options.url,
|
||||||
|
method: options.method,
|
||||||
|
headers: options.headers ? (0, import_headers.headersObjectToArray)(options.headers) : void 0,
|
||||||
|
postData: options.postDataBuffer,
|
||||||
|
isFallback
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class WebSocketRoute extends import_channelOwner.ChannelOwner {
|
||||||
|
constructor(parent, type, guid, initializer) {
|
||||||
|
super(parent, type, guid, initializer);
|
||||||
|
this._connected = false;
|
||||||
|
this._server = {
|
||||||
|
onMessage: (handler) => {
|
||||||
|
this._onServerMessage = handler;
|
||||||
|
},
|
||||||
|
onClose: (handler) => {
|
||||||
|
this._onServerClose = handler;
|
||||||
|
},
|
||||||
|
connectToServer: () => {
|
||||||
|
throw new Error(`connectToServer must be called on the page-side WebSocketRoute`);
|
||||||
|
},
|
||||||
|
url: () => {
|
||||||
|
return this._initializer.url;
|
||||||
|
},
|
||||||
|
close: async (options = {}) => {
|
||||||
|
await this._channel.closeServer({ ...options, wasClean: true }).catch(() => {
|
||||||
|
});
|
||||||
|
},
|
||||||
|
send: (message) => {
|
||||||
|
if ((0, import_rtti.isString)(message))
|
||||||
|
this._channel.sendToServer({ message, isBase64: false }).catch(() => {
|
||||||
|
});
|
||||||
|
else
|
||||||
|
this._channel.sendToServer({ message: message.toString("base64"), isBase64: true }).catch(() => {
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async [Symbol.asyncDispose]() {
|
||||||
|
await this.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this._channel.on("messageFromPage", ({ message, isBase64 }) => {
|
||||||
|
if (this._onPageMessage)
|
||||||
|
this._onPageMessage(isBase64 ? Buffer.from(message, "base64") : message);
|
||||||
|
else if (this._connected)
|
||||||
|
this._channel.sendToServer({ message, isBase64 }).catch(() => {
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this._channel.on("messageFromServer", ({ message, isBase64 }) => {
|
||||||
|
if (this._onServerMessage)
|
||||||
|
this._onServerMessage(isBase64 ? Buffer.from(message, "base64") : message);
|
||||||
|
else
|
||||||
|
this._channel.sendToPage({ message, isBase64 }).catch(() => {
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this._channel.on("closePage", ({ code, reason, wasClean }) => {
|
||||||
|
if (this._onPageClose)
|
||||||
|
this._onPageClose(code, reason);
|
||||||
|
else
|
||||||
|
this._channel.closeServer({ code, reason, wasClean }).catch(() => {
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this._channel.on("closeServer", ({ code, reason, wasClean }) => {
|
||||||
|
if (this._onServerClose)
|
||||||
|
this._onServerClose(code, reason);
|
||||||
|
else
|
||||||
|
this._channel.closePage({ code, reason, wasClean }).catch(() => {
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
static from(route) {
|
||||||
|
return route._object;
|
||||||
|
}
|
||||||
|
url() {
|
||||||
|
return this._initializer.url;
|
||||||
|
}
|
||||||
|
async close(options = {}) {
|
||||||
|
await this._channel.closePage({ ...options, wasClean: true }).catch(() => {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
connectToServer() {
|
||||||
|
if (this._connected)
|
||||||
|
throw new Error("Already connected to the server");
|
||||||
|
this._connected = true;
|
||||||
|
this._channel.connect().catch(() => {
|
||||||
|
});
|
||||||
|
return this._server;
|
||||||
|
}
|
||||||
|
send(message) {
|
||||||
|
if ((0, import_rtti.isString)(message))
|
||||||
|
this._channel.sendToPage({ message, isBase64: false }).catch(() => {
|
||||||
|
});
|
||||||
|
else
|
||||||
|
this._channel.sendToPage({ message: message.toString("base64"), isBase64: true }).catch(() => {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
onMessage(handler) {
|
||||||
|
this._onPageMessage = handler;
|
||||||
|
}
|
||||||
|
onClose(handler) {
|
||||||
|
this._onPageClose = handler;
|
||||||
|
}
|
||||||
|
async [Symbol.asyncDispose]() {
|
||||||
|
await this.close();
|
||||||
|
}
|
||||||
|
async _afterHandle() {
|
||||||
|
if (this._connected)
|
||||||
|
return;
|
||||||
|
await this._channel.ensureOpened().catch(() => {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class WebSocketRouteHandler {
|
||||||
|
constructor(baseURL, url, handler) {
|
||||||
|
this._baseURL = baseURL;
|
||||||
|
this.url = url;
|
||||||
|
this.handler = handler;
|
||||||
|
}
|
||||||
|
static prepareInterceptionPatterns(handlers) {
|
||||||
|
const patterns = [];
|
||||||
|
let all = false;
|
||||||
|
for (const handler of handlers) {
|
||||||
|
if ((0, import_rtti.isString)(handler.url))
|
||||||
|
patterns.push({ glob: handler.url });
|
||||||
|
else if ((0, import_rtti.isRegExp)(handler.url))
|
||||||
|
patterns.push({ regexSource: handler.url.source, regexFlags: handler.url.flags });
|
||||||
|
else
|
||||||
|
all = true;
|
||||||
|
}
|
||||||
|
if (all)
|
||||||
|
return [{ glob: "**/*" }];
|
||||||
|
return patterns;
|
||||||
|
}
|
||||||
|
matches(wsURL) {
|
||||||
|
return (0, import_urlMatch.urlMatches)(this._baseURL, wsURL, this.url, true);
|
||||||
|
}
|
||||||
|
async handle(webSocketRoute) {
|
||||||
|
const handler = this.handler;
|
||||||
|
await handler(webSocketRoute);
|
||||||
|
await webSocketRoute._afterHandle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Response extends import_channelOwner.ChannelOwner {
|
||||||
|
constructor(parent, type, guid, initializer) {
|
||||||
|
super(parent, type, guid, initializer);
|
||||||
|
this._finishedPromise = new import_manualPromise.ManualPromise();
|
||||||
|
this._provisionalHeaders = new RawHeaders(initializer.headers);
|
||||||
|
this._request = Request.from(this._initializer.request);
|
||||||
|
Object.assign(this._request._timing, this._initializer.timing);
|
||||||
|
}
|
||||||
|
static from(response) {
|
||||||
|
return response._object;
|
||||||
|
}
|
||||||
|
static fromNullable(response) {
|
||||||
|
return response ? Response.from(response) : null;
|
||||||
|
}
|
||||||
|
url() {
|
||||||
|
return this._initializer.url;
|
||||||
|
}
|
||||||
|
ok() {
|
||||||
|
return this._initializer.status === 0 || this._initializer.status >= 200 && this._initializer.status <= 299;
|
||||||
|
}
|
||||||
|
status() {
|
||||||
|
return this._initializer.status;
|
||||||
|
}
|
||||||
|
statusText() {
|
||||||
|
return this._initializer.statusText;
|
||||||
|
}
|
||||||
|
fromServiceWorker() {
|
||||||
|
return this._initializer.fromServiceWorker;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
|
headers() {
|
||||||
|
return this._provisionalHeaders.headers();
|
||||||
|
}
|
||||||
|
async _actualHeaders() {
|
||||||
|
if (!this._actualHeadersPromise) {
|
||||||
|
this._actualHeadersPromise = (async () => {
|
||||||
|
return new RawHeaders((await this._channel.rawResponseHeaders()).headers);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
return await this._actualHeadersPromise;
|
||||||
|
}
|
||||||
|
async allHeaders() {
|
||||||
|
return (await this._actualHeaders()).headers();
|
||||||
|
}
|
||||||
|
async headersArray() {
|
||||||
|
return (await this._actualHeaders()).headersArray().slice();
|
||||||
|
}
|
||||||
|
async headerValue(name) {
|
||||||
|
return (await this._actualHeaders()).get(name);
|
||||||
|
}
|
||||||
|
async headerValues(name) {
|
||||||
|
return (await this._actualHeaders()).getAll(name);
|
||||||
|
}
|
||||||
|
async finished() {
|
||||||
|
return await this.request()._targetClosedScope().race(this._finishedPromise);
|
||||||
|
}
|
||||||
|
async body() {
|
||||||
|
return (await this._channel.body()).binary;
|
||||||
|
}
|
||||||
|
async text() {
|
||||||
|
const content = await this.body();
|
||||||
|
return content.toString("utf8");
|
||||||
|
}
|
||||||
|
async json() {
|
||||||
|
const content = await this.text();
|
||||||
|
return JSON.parse(content);
|
||||||
|
}
|
||||||
|
request() {
|
||||||
|
return this._request;
|
||||||
|
}
|
||||||
|
frame() {
|
||||||
|
return this._request.frame();
|
||||||
|
}
|
||||||
|
async serverAddr() {
|
||||||
|
return (await this._channel.serverAddr()).value || null;
|
||||||
|
}
|
||||||
|
async securityDetails() {
|
||||||
|
return (await this._channel.securityDetails()).value || null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class WebSocket extends import_channelOwner.ChannelOwner {
|
||||||
|
static from(webSocket) {
|
||||||
|
return webSocket._object;
|
||||||
|
}
|
||||||
|
constructor(parent, type, guid, initializer) {
|
||||||
|
super(parent, type, guid, initializer);
|
||||||
|
this._isClosed = false;
|
||||||
|
this._page = parent;
|
||||||
|
this._channel.on("frameSent", (event) => {
|
||||||
|
if (event.opcode === 1)
|
||||||
|
this.emit(import_events.Events.WebSocket.FrameSent, { payload: event.data });
|
||||||
|
else if (event.opcode === 2)
|
||||||
|
this.emit(import_events.Events.WebSocket.FrameSent, { payload: Buffer.from(event.data, "base64") });
|
||||||
|
});
|
||||||
|
this._channel.on("frameReceived", (event) => {
|
||||||
|
if (event.opcode === 1)
|
||||||
|
this.emit(import_events.Events.WebSocket.FrameReceived, { payload: event.data });
|
||||||
|
else if (event.opcode === 2)
|
||||||
|
this.emit(import_events.Events.WebSocket.FrameReceived, { payload: Buffer.from(event.data, "base64") });
|
||||||
|
});
|
||||||
|
this._channel.on("socketError", ({ error }) => this.emit(import_events.Events.WebSocket.Error, error));
|
||||||
|
this._channel.on("close", () => {
|
||||||
|
this._isClosed = true;
|
||||||
|
this.emit(import_events.Events.WebSocket.Close, this);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
url() {
|
||||||
|
return this._initializer.url;
|
||||||
|
}
|
||||||
|
isClosed() {
|
||||||
|
return this._isClosed;
|
||||||
|
}
|
||||||
|
async waitForEvent(event, optionsOrPredicate = {}) {
|
||||||
|
return await this._wrapApiCall(async () => {
|
||||||
|
const timeout = this._page._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.WebSocket.Error)
|
||||||
|
waiter.rejectOnEvent(this, import_events.Events.WebSocket.Error, new Error("Socket error"));
|
||||||
|
if (event !== import_events.Events.WebSocket.Close)
|
||||||
|
waiter.rejectOnEvent(this, import_events.Events.WebSocket.Close, new Error("Socket closed"));
|
||||||
|
waiter.rejectOnEvent(this._page, import_events.Events.Page.Close, () => this._page._closeErrorWithReason());
|
||||||
|
const result = await waiter.waitForEvent(this, event, predicate);
|
||||||
|
waiter.dispose();
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function validateHeaders(headers) {
|
||||||
|
for (const key of Object.keys(headers)) {
|
||||||
|
const value = headers[key];
|
||||||
|
if (!Object.is(value, void 0) && !(0, import_rtti.isString)(value))
|
||||||
|
throw new Error(`Expected value of header "${key}" to be String, but "${typeof value}" is found.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class RouteHandler {
|
||||||
|
constructor(platform, baseURL, url, handler, times = Number.MAX_SAFE_INTEGER) {
|
||||||
|
this.handledCount = 0;
|
||||||
|
this._ignoreException = false;
|
||||||
|
this._activeInvocations = /* @__PURE__ */ new Set();
|
||||||
|
this._baseURL = baseURL;
|
||||||
|
this._times = times;
|
||||||
|
this.url = url;
|
||||||
|
this.handler = handler;
|
||||||
|
this._savedZone = platform.zones.current().pop();
|
||||||
|
}
|
||||||
|
static prepareInterceptionPatterns(handlers) {
|
||||||
|
const patterns = [];
|
||||||
|
let all = false;
|
||||||
|
for (const handler of handlers) {
|
||||||
|
if ((0, import_rtti.isString)(handler.url))
|
||||||
|
patterns.push({ glob: handler.url });
|
||||||
|
else if ((0, import_rtti.isRegExp)(handler.url))
|
||||||
|
patterns.push({ regexSource: handler.url.source, regexFlags: handler.url.flags });
|
||||||
|
else
|
||||||
|
all = true;
|
||||||
|
}
|
||||||
|
if (all)
|
||||||
|
return [{ glob: "**/*" }];
|
||||||
|
return patterns;
|
||||||
|
}
|
||||||
|
matches(requestURL) {
|
||||||
|
return (0, import_urlMatch.urlMatches)(this._baseURL, requestURL, this.url);
|
||||||
|
}
|
||||||
|
async handle(route) {
|
||||||
|
return await this._savedZone.run(async () => this._handleImpl(route));
|
||||||
|
}
|
||||||
|
async _handleImpl(route) {
|
||||||
|
const handlerInvocation = { complete: new import_manualPromise.ManualPromise(), route };
|
||||||
|
this._activeInvocations.add(handlerInvocation);
|
||||||
|
try {
|
||||||
|
return await this._handleInternal(route);
|
||||||
|
} catch (e) {
|
||||||
|
if (this._ignoreException)
|
||||||
|
return false;
|
||||||
|
if ((0, import_errors.isTargetClosedError)(e)) {
|
||||||
|
(0, import_stackTrace.rewriteErrorMessage)(e, `"${e.message}" while running route callback.
|
||||||
|
Consider awaiting \`await page.unrouteAll({ behavior: 'ignoreErrors' })\`
|
||||||
|
before the end of the test to ignore remaining routes in flight.`);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
} finally {
|
||||||
|
handlerInvocation.complete.resolve();
|
||||||
|
this._activeInvocations.delete(handlerInvocation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async stop(behavior) {
|
||||||
|
if (behavior === "ignoreErrors") {
|
||||||
|
this._ignoreException = true;
|
||||||
|
} else {
|
||||||
|
const promises = [];
|
||||||
|
for (const activation of this._activeInvocations) {
|
||||||
|
if (!activation.route._didThrow)
|
||||||
|
promises.push(activation.complete);
|
||||||
|
}
|
||||||
|
await Promise.all(promises);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async _handleInternal(route) {
|
||||||
|
++this.handledCount;
|
||||||
|
const handledPromise = route._startHandling();
|
||||||
|
const handler = this.handler;
|
||||||
|
const [handled] = await Promise.all([
|
||||||
|
handledPromise,
|
||||||
|
handler(route, route.request())
|
||||||
|
]);
|
||||||
|
return handled;
|
||||||
|
}
|
||||||
|
willExpire() {
|
||||||
|
return this.handledCount + 1 >= this._times;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class RawHeaders {
|
||||||
|
constructor(headers) {
|
||||||
|
this._headersMap = new import_multimap.MultiMap();
|
||||||
|
this._headersArray = headers;
|
||||||
|
for (const header of headers)
|
||||||
|
this._headersMap.set(header.name.toLowerCase(), header.value);
|
||||||
|
}
|
||||||
|
static _fromHeadersObjectLossy(headers) {
|
||||||
|
const headersArray = Object.entries(headers).map(([name, value]) => ({
|
||||||
|
name,
|
||||||
|
value
|
||||||
|
})).filter((header) => header.value !== void 0);
|
||||||
|
return new RawHeaders(headersArray);
|
||||||
|
}
|
||||||
|
get(name) {
|
||||||
|
const values = this.getAll(name);
|
||||||
|
if (!values || !values.length)
|
||||||
|
return null;
|
||||||
|
return values.join(name.toLowerCase() === "set-cookie" ? "\n" : ", ");
|
||||||
|
}
|
||||||
|
getAll(name) {
|
||||||
|
return [...this._headersMap.get(name.toLowerCase())];
|
||||||
|
}
|
||||||
|
headers() {
|
||||||
|
const result = {};
|
||||||
|
for (const name of this._headersMap.keys())
|
||||||
|
result[name] = this.get(name);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
headersArray() {
|
||||||
|
return this._headersArray;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
RawHeaders,
|
||||||
|
Request,
|
||||||
|
Response,
|
||||||
|
Route,
|
||||||
|
RouteHandler,
|
||||||
|
WebSocket,
|
||||||
|
WebSocketRoute,
|
||||||
|
WebSocketRouteHandler,
|
||||||
|
validateHeaders
|
||||||
|
});
|
||||||
709
node_modules/playwright-core/lib/client/page.js
generated
vendored
Normal file
709
node_modules/playwright-core/lib/client/page.js
generated
vendored
Normal file
@@ -0,0 +1,709 @@
|
|||||||
|
"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 page_exports = {};
|
||||||
|
__export(page_exports, {
|
||||||
|
BindingCall: () => BindingCall,
|
||||||
|
Page: () => Page
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(page_exports);
|
||||||
|
var import_accessibility = require("./accessibility");
|
||||||
|
var import_artifact = require("./artifact");
|
||||||
|
var import_channelOwner = require("./channelOwner");
|
||||||
|
var import_clientHelper = require("./clientHelper");
|
||||||
|
var import_coverage = require("./coverage");
|
||||||
|
var import_download = require("./download");
|
||||||
|
var import_elementHandle = require("./elementHandle");
|
||||||
|
var import_errors = require("./errors");
|
||||||
|
var import_events = require("./events");
|
||||||
|
var import_fileChooser = require("./fileChooser");
|
||||||
|
var import_frame = require("./frame");
|
||||||
|
var import_harRouter = require("./harRouter");
|
||||||
|
var import_input = require("./input");
|
||||||
|
var import_jsHandle = require("./jsHandle");
|
||||||
|
var import_network = require("./network");
|
||||||
|
var import_video = require("./video");
|
||||||
|
var import_waiter = require("./waiter");
|
||||||
|
var import_worker = require("./worker");
|
||||||
|
var import_timeoutSettings = require("./timeoutSettings");
|
||||||
|
var import_assert = require("../utils/isomorphic/assert");
|
||||||
|
var import_fileUtils = require("./fileUtils");
|
||||||
|
var import_headers = require("../utils/isomorphic/headers");
|
||||||
|
var import_stringUtils = require("../utils/isomorphic/stringUtils");
|
||||||
|
var import_urlMatch = require("../utils/isomorphic/urlMatch");
|
||||||
|
var import_manualPromise = require("../utils/isomorphic/manualPromise");
|
||||||
|
var import_rtti = require("../utils/isomorphic/rtti");
|
||||||
|
class Page extends import_channelOwner.ChannelOwner {
|
||||||
|
constructor(parent, type, guid, initializer) {
|
||||||
|
super(parent, type, guid, initializer);
|
||||||
|
this._frames = /* @__PURE__ */ new Set();
|
||||||
|
this._workers = /* @__PURE__ */ new Set();
|
||||||
|
this._closed = false;
|
||||||
|
this._closedOrCrashedScope = new import_manualPromise.LongStandingScope();
|
||||||
|
this._routes = [];
|
||||||
|
this._webSocketRoutes = [];
|
||||||
|
this._bindings = /* @__PURE__ */ new Map();
|
||||||
|
this._video = null;
|
||||||
|
this._closeWasCalled = false;
|
||||||
|
this._harRouters = [];
|
||||||
|
this._locatorHandlers = /* @__PURE__ */ new Map();
|
||||||
|
this._browserContext = parent;
|
||||||
|
this._timeoutSettings = new import_timeoutSettings.TimeoutSettings(this._platform, this._browserContext._timeoutSettings);
|
||||||
|
this.accessibility = new import_accessibility.Accessibility(this._channel);
|
||||||
|
this.keyboard = new import_input.Keyboard(this);
|
||||||
|
this.mouse = new import_input.Mouse(this);
|
||||||
|
this.request = this._browserContext.request;
|
||||||
|
this.touchscreen = new import_input.Touchscreen(this);
|
||||||
|
this.clock = this._browserContext.clock;
|
||||||
|
this._mainFrame = import_frame.Frame.from(initializer.mainFrame);
|
||||||
|
this._mainFrame._page = this;
|
||||||
|
this._frames.add(this._mainFrame);
|
||||||
|
this._viewportSize = initializer.viewportSize;
|
||||||
|
this._closed = initializer.isClosed;
|
||||||
|
this._opener = Page.fromNullable(initializer.opener);
|
||||||
|
this._channel.on("bindingCall", ({ binding }) => this._onBinding(BindingCall.from(binding)));
|
||||||
|
this._channel.on("close", () => this._onClose());
|
||||||
|
this._channel.on("crash", () => this._onCrash());
|
||||||
|
this._channel.on("download", ({ url, suggestedFilename, artifact }) => {
|
||||||
|
const artifactObject = import_artifact.Artifact.from(artifact);
|
||||||
|
this.emit(import_events.Events.Page.Download, new import_download.Download(this, url, suggestedFilename, artifactObject));
|
||||||
|
});
|
||||||
|
this._channel.on("fileChooser", ({ element, isMultiple }) => this.emit(import_events.Events.Page.FileChooser, new import_fileChooser.FileChooser(this, import_elementHandle.ElementHandle.from(element), isMultiple)));
|
||||||
|
this._channel.on("frameAttached", ({ frame }) => this._onFrameAttached(import_frame.Frame.from(frame)));
|
||||||
|
this._channel.on("frameDetached", ({ frame }) => this._onFrameDetached(import_frame.Frame.from(frame)));
|
||||||
|
this._channel.on("locatorHandlerTriggered", ({ uid }) => this._onLocatorHandlerTriggered(uid));
|
||||||
|
this._channel.on("route", ({ route }) => this._onRoute(import_network.Route.from(route)));
|
||||||
|
this._channel.on("webSocketRoute", ({ webSocketRoute }) => this._onWebSocketRoute(import_network.WebSocketRoute.from(webSocketRoute)));
|
||||||
|
this._channel.on("video", ({ artifact }) => {
|
||||||
|
const artifactObject = import_artifact.Artifact.from(artifact);
|
||||||
|
this._forceVideo()._artifactReady(artifactObject);
|
||||||
|
});
|
||||||
|
this._channel.on("viewportSizeChanged", ({ viewportSize }) => this._viewportSize = viewportSize);
|
||||||
|
this._channel.on("webSocket", ({ webSocket }) => this.emit(import_events.Events.Page.WebSocket, import_network.WebSocket.from(webSocket)));
|
||||||
|
this._channel.on("worker", ({ worker }) => this._onWorker(import_worker.Worker.from(worker)));
|
||||||
|
this.coverage = new import_coverage.Coverage(this._channel);
|
||||||
|
this.once(import_events.Events.Page.Close, () => this._closedOrCrashedScope.close(this._closeErrorWithReason()));
|
||||||
|
this.once(import_events.Events.Page.Crash, () => this._closedOrCrashedScope.close(new import_errors.TargetClosedError()));
|
||||||
|
this._setEventToSubscriptionMapping(/* @__PURE__ */ new Map([
|
||||||
|
[import_events.Events.Page.Console, "console"],
|
||||||
|
[import_events.Events.Page.Dialog, "dialog"],
|
||||||
|
[import_events.Events.Page.Request, "request"],
|
||||||
|
[import_events.Events.Page.Response, "response"],
|
||||||
|
[import_events.Events.Page.RequestFinished, "requestFinished"],
|
||||||
|
[import_events.Events.Page.RequestFailed, "requestFailed"],
|
||||||
|
[import_events.Events.Page.FileChooser, "fileChooser"]
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
static from(page) {
|
||||||
|
return page._object;
|
||||||
|
}
|
||||||
|
static fromNullable(page) {
|
||||||
|
return page ? Page.from(page) : null;
|
||||||
|
}
|
||||||
|
_onFrameAttached(frame) {
|
||||||
|
frame._page = this;
|
||||||
|
this._frames.add(frame);
|
||||||
|
if (frame._parentFrame)
|
||||||
|
frame._parentFrame._childFrames.add(frame);
|
||||||
|
this.emit(import_events.Events.Page.FrameAttached, frame);
|
||||||
|
}
|
||||||
|
_onFrameDetached(frame) {
|
||||||
|
this._frames.delete(frame);
|
||||||
|
frame._detached = true;
|
||||||
|
if (frame._parentFrame)
|
||||||
|
frame._parentFrame._childFrames.delete(frame);
|
||||||
|
this.emit(import_events.Events.Page.FrameDetached, frame);
|
||||||
|
}
|
||||||
|
async _onRoute(route) {
|
||||||
|
route._context = this.context();
|
||||||
|
const routeHandlers = this._routes.slice();
|
||||||
|
for (const routeHandler of routeHandlers) {
|
||||||
|
if (this._closeWasCalled || this._browserContext._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 this._browserContext._onRoute(route);
|
||||||
|
}
|
||||||
|
async _onWebSocketRoute(webSocketRoute) {
|
||||||
|
const routeHandler = this._webSocketRoutes.find((route) => route.matches(webSocketRoute.url()));
|
||||||
|
if (routeHandler)
|
||||||
|
await routeHandler.handle(webSocketRoute);
|
||||||
|
else
|
||||||
|
await this._browserContext._onWebSocketRoute(webSocketRoute);
|
||||||
|
}
|
||||||
|
async _onBinding(bindingCall) {
|
||||||
|
const func = this._bindings.get(bindingCall._initializer.name);
|
||||||
|
if (func) {
|
||||||
|
await bindingCall.call(func);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this._browserContext._onBinding(bindingCall);
|
||||||
|
}
|
||||||
|
_onWorker(worker) {
|
||||||
|
this._workers.add(worker);
|
||||||
|
worker._page = this;
|
||||||
|
this.emit(import_events.Events.Page.Worker, worker);
|
||||||
|
}
|
||||||
|
_onClose() {
|
||||||
|
this._closed = true;
|
||||||
|
this._browserContext._pages.delete(this);
|
||||||
|
this._browserContext._backgroundPages.delete(this);
|
||||||
|
this._disposeHarRouters();
|
||||||
|
this.emit(import_events.Events.Page.Close, this);
|
||||||
|
}
|
||||||
|
_onCrash() {
|
||||||
|
this.emit(import_events.Events.Page.Crash, this);
|
||||||
|
}
|
||||||
|
context() {
|
||||||
|
return this._browserContext;
|
||||||
|
}
|
||||||
|
async opener() {
|
||||||
|
if (!this._opener || this._opener.isClosed())
|
||||||
|
return null;
|
||||||
|
return this._opener;
|
||||||
|
}
|
||||||
|
mainFrame() {
|
||||||
|
return this._mainFrame;
|
||||||
|
}
|
||||||
|
frame(frameSelector) {
|
||||||
|
const name = (0, import_rtti.isString)(frameSelector) ? frameSelector : frameSelector.name;
|
||||||
|
const url = (0, import_rtti.isObject)(frameSelector) ? frameSelector.url : void 0;
|
||||||
|
(0, import_assert.assert)(name || url, "Either name or url matcher should be specified");
|
||||||
|
return this.frames().find((f) => {
|
||||||
|
if (name)
|
||||||
|
return f.name() === name;
|
||||||
|
return (0, import_urlMatch.urlMatches)(this._browserContext._options.baseURL, f.url(), url);
|
||||||
|
}) || null;
|
||||||
|
}
|
||||||
|
frames() {
|
||||||
|
return [...this._frames];
|
||||||
|
}
|
||||||
|
setDefaultNavigationTimeout(timeout) {
|
||||||
|
this._timeoutSettings.setDefaultNavigationTimeout(timeout);
|
||||||
|
}
|
||||||
|
setDefaultTimeout(timeout) {
|
||||||
|
this._timeoutSettings.setDefaultTimeout(timeout);
|
||||||
|
}
|
||||||
|
_forceVideo() {
|
||||||
|
if (!this._video)
|
||||||
|
this._video = new import_video.Video(this, this._connection);
|
||||||
|
return this._video;
|
||||||
|
}
|
||||||
|
video() {
|
||||||
|
if (!this._browserContext._options.recordVideo)
|
||||||
|
return null;
|
||||||
|
return this._forceVideo();
|
||||||
|
}
|
||||||
|
async $(selector, options) {
|
||||||
|
return await this._mainFrame.$(selector, options);
|
||||||
|
}
|
||||||
|
async waitForSelector(selector, options) {
|
||||||
|
return await this._mainFrame.waitForSelector(selector, options);
|
||||||
|
}
|
||||||
|
async dispatchEvent(selector, type, eventInit, options) {
|
||||||
|
return await this._mainFrame.dispatchEvent(selector, type, eventInit, options);
|
||||||
|
}
|
||||||
|
async evaluateHandle(pageFunction, arg) {
|
||||||
|
(0, import_jsHandle.assertMaxArguments)(arguments.length, 2);
|
||||||
|
return await this._mainFrame.evaluateHandle(pageFunction, arg);
|
||||||
|
}
|
||||||
|
async $eval(selector, pageFunction, arg) {
|
||||||
|
(0, import_jsHandle.assertMaxArguments)(arguments.length, 3);
|
||||||
|
return await this._mainFrame.$eval(selector, pageFunction, arg);
|
||||||
|
}
|
||||||
|
async $$eval(selector, pageFunction, arg) {
|
||||||
|
(0, import_jsHandle.assertMaxArguments)(arguments.length, 3);
|
||||||
|
return await this._mainFrame.$$eval(selector, pageFunction, arg);
|
||||||
|
}
|
||||||
|
async $$(selector) {
|
||||||
|
return await this._mainFrame.$$(selector);
|
||||||
|
}
|
||||||
|
async addScriptTag(options = {}) {
|
||||||
|
return await this._mainFrame.addScriptTag(options);
|
||||||
|
}
|
||||||
|
async addStyleTag(options = {}) {
|
||||||
|
return await this._mainFrame.addStyleTag(options);
|
||||||
|
}
|
||||||
|
async exposeFunction(name, callback) {
|
||||||
|
await this._channel.exposeBinding({ name });
|
||||||
|
const binding = (source, ...args) => callback(...args);
|
||||||
|
this._bindings.set(name, binding);
|
||||||
|
}
|
||||||
|
async exposeBinding(name, callback, options = {}) {
|
||||||
|
await this._channel.exposeBinding({ name, needsHandle: options.handle });
|
||||||
|
this._bindings.set(name, callback);
|
||||||
|
}
|
||||||
|
async setExtraHTTPHeaders(headers) {
|
||||||
|
(0, import_network.validateHeaders)(headers);
|
||||||
|
await this._channel.setExtraHTTPHeaders({ headers: (0, import_headers.headersObjectToArray)(headers) });
|
||||||
|
}
|
||||||
|
url() {
|
||||||
|
return this._mainFrame.url();
|
||||||
|
}
|
||||||
|
async content() {
|
||||||
|
return await this._mainFrame.content();
|
||||||
|
}
|
||||||
|
async setContent(html, options) {
|
||||||
|
return await this._mainFrame.setContent(html, options);
|
||||||
|
}
|
||||||
|
async goto(url, options) {
|
||||||
|
return await this._mainFrame.goto(url, options);
|
||||||
|
}
|
||||||
|
async reload(options = {}) {
|
||||||
|
const waitUntil = (0, import_frame.verifyLoadState)("waitUntil", options.waitUntil === void 0 ? "load" : options.waitUntil);
|
||||||
|
return import_network.Response.fromNullable((await this._channel.reload({ ...options, waitUntil, timeout: this._timeoutSettings.navigationTimeout(options) })).response);
|
||||||
|
}
|
||||||
|
async addLocatorHandler(locator, handler, options = {}) {
|
||||||
|
if (locator._frame !== this._mainFrame)
|
||||||
|
throw new Error(`Locator must belong to the main frame of this page`);
|
||||||
|
if (options.times === 0)
|
||||||
|
return;
|
||||||
|
const { uid } = await this._channel.registerLocatorHandler({ selector: locator._selector, noWaitAfter: options.noWaitAfter });
|
||||||
|
this._locatorHandlers.set(uid, { locator, handler, times: options.times });
|
||||||
|
}
|
||||||
|
async _onLocatorHandlerTriggered(uid) {
|
||||||
|
let remove = false;
|
||||||
|
try {
|
||||||
|
const handler = this._locatorHandlers.get(uid);
|
||||||
|
if (handler && handler.times !== 0) {
|
||||||
|
if (handler.times !== void 0)
|
||||||
|
handler.times--;
|
||||||
|
await handler.handler(handler.locator);
|
||||||
|
}
|
||||||
|
remove = handler?.times === 0;
|
||||||
|
} finally {
|
||||||
|
if (remove)
|
||||||
|
this._locatorHandlers.delete(uid);
|
||||||
|
this._channel.resolveLocatorHandlerNoReply({ uid, remove }).catch(() => {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async removeLocatorHandler(locator) {
|
||||||
|
for (const [uid, data] of this._locatorHandlers) {
|
||||||
|
if (data.locator._equals(locator)) {
|
||||||
|
this._locatorHandlers.delete(uid);
|
||||||
|
await this._channel.unregisterLocatorHandler({ uid }).catch(() => {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async waitForLoadState(state, options) {
|
||||||
|
return await this._mainFrame.waitForLoadState(state, options);
|
||||||
|
}
|
||||||
|
async waitForNavigation(options) {
|
||||||
|
return await this._mainFrame.waitForNavigation(options);
|
||||||
|
}
|
||||||
|
async waitForURL(url, options) {
|
||||||
|
return await this._mainFrame.waitForURL(url, options);
|
||||||
|
}
|
||||||
|
async waitForRequest(urlOrPredicate, options = {}) {
|
||||||
|
const predicate = async (request) => {
|
||||||
|
if ((0, import_rtti.isString)(urlOrPredicate) || (0, import_rtti.isRegExp)(urlOrPredicate))
|
||||||
|
return (0, import_urlMatch.urlMatches)(this._browserContext._options.baseURL, request.url(), urlOrPredicate);
|
||||||
|
return await urlOrPredicate(request);
|
||||||
|
};
|
||||||
|
const trimmedUrl = trimUrl(urlOrPredicate);
|
||||||
|
const logLine = trimmedUrl ? `waiting for request ${trimmedUrl}` : void 0;
|
||||||
|
return await this._waitForEvent(import_events.Events.Page.Request, { predicate, timeout: options.timeout }, logLine);
|
||||||
|
}
|
||||||
|
async waitForResponse(urlOrPredicate, options = {}) {
|
||||||
|
const predicate = async (response) => {
|
||||||
|
if ((0, import_rtti.isString)(urlOrPredicate) || (0, import_rtti.isRegExp)(urlOrPredicate))
|
||||||
|
return (0, import_urlMatch.urlMatches)(this._browserContext._options.baseURL, response.url(), urlOrPredicate);
|
||||||
|
return await urlOrPredicate(response);
|
||||||
|
};
|
||||||
|
const trimmedUrl = trimUrl(urlOrPredicate);
|
||||||
|
const logLine = trimmedUrl ? `waiting for response ${trimmedUrl}` : void 0;
|
||||||
|
return await this._waitForEvent(import_events.Events.Page.Response, { predicate, timeout: options.timeout }, logLine);
|
||||||
|
}
|
||||||
|
async waitForEvent(event, optionsOrPredicate = {}) {
|
||||||
|
return await this._waitForEvent(event, optionsOrPredicate, `waiting for event "${event}"`);
|
||||||
|
}
|
||||||
|
_closeErrorWithReason() {
|
||||||
|
return new import_errors.TargetClosedError(this._closeReason || this._browserContext._effectiveCloseReason());
|
||||||
|
}
|
||||||
|
async _waitForEvent(event, optionsOrPredicate, logLine) {
|
||||||
|
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);
|
||||||
|
if (logLine)
|
||||||
|
waiter.log(logLine);
|
||||||
|
waiter.rejectOnTimeout(timeout, `Timeout ${timeout}ms exceeded while waiting for event "${event}"`);
|
||||||
|
if (event !== import_events.Events.Page.Crash)
|
||||||
|
waiter.rejectOnEvent(this, import_events.Events.Page.Crash, new Error("Page crashed"));
|
||||||
|
if (event !== import_events.Events.Page.Close)
|
||||||
|
waiter.rejectOnEvent(this, import_events.Events.Page.Close, () => this._closeErrorWithReason());
|
||||||
|
const result = await waiter.waitForEvent(this, event, predicate);
|
||||||
|
waiter.dispose();
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async goBack(options = {}) {
|
||||||
|
const waitUntil = (0, import_frame.verifyLoadState)("waitUntil", options.waitUntil === void 0 ? "load" : options.waitUntil);
|
||||||
|
return import_network.Response.fromNullable((await this._channel.goBack({ ...options, waitUntil, timeout: this._timeoutSettings.navigationTimeout(options) })).response);
|
||||||
|
}
|
||||||
|
async goForward(options = {}) {
|
||||||
|
const waitUntil = (0, import_frame.verifyLoadState)("waitUntil", options.waitUntil === void 0 ? "load" : options.waitUntil);
|
||||||
|
return import_network.Response.fromNullable((await this._channel.goForward({ ...options, waitUntil, timeout: this._timeoutSettings.navigationTimeout(options) })).response);
|
||||||
|
}
|
||||||
|
async requestGC() {
|
||||||
|
await this._channel.requestGC();
|
||||||
|
}
|
||||||
|
async emulateMedia(options = {}) {
|
||||||
|
await this._channel.emulateMedia({
|
||||||
|
media: options.media === null ? "no-override" : options.media,
|
||||||
|
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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async setViewportSize(viewportSize) {
|
||||||
|
this._viewportSize = viewportSize;
|
||||||
|
await this._channel.setViewportSize({ viewportSize });
|
||||||
|
}
|
||||||
|
viewportSize() {
|
||||||
|
return this._viewportSize || null;
|
||||||
|
}
|
||||||
|
async evaluate(pageFunction, arg) {
|
||||||
|
(0, import_jsHandle.assertMaxArguments)(arguments.length, 2);
|
||||||
|
return await this._mainFrame.evaluate(pageFunction, arg);
|
||||||
|
}
|
||||||
|
async _evaluateFunction(functionDeclaration) {
|
||||||
|
return this._mainFrame._evaluateFunction(functionDeclaration);
|
||||||
|
}
|
||||||
|
async addInitScript(script, arg) {
|
||||||
|
const source = await (0, import_clientHelper.evaluationScript)(this._platform, script, arg);
|
||||||
|
await this._channel.addInitScript({ source });
|
||||||
|
}
|
||||||
|
async route(url, handler, options = {}) {
|
||||||
|
this._routes.unshift(new import_network.RouteHandler(this._platform, this._browserContext._options.baseURL, url, handler, options.times));
|
||||||
|
await this._updateInterceptionPatterns({ title: "Route requests" });
|
||||||
|
}
|
||||||
|
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._browserContext._recordIntoHAR(har, this, options);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const harRouter = await import_harRouter.HarRouter.create(localUtils, har, options.notFound || "abort", { urlMatch: options.url });
|
||||||
|
this._harRouters.push(harRouter);
|
||||||
|
await harRouter.addPageRoute(this);
|
||||||
|
}
|
||||||
|
async routeWebSocket(url, handler) {
|
||||||
|
this._webSocketRoutes.unshift(new import_network.WebSocketRouteHandler(this._browserContext._options.baseURL, url, handler));
|
||||||
|
await this._updateWebSocketInterceptionPatterns({ title: "Route WebSockets" });
|
||||||
|
}
|
||||||
|
_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 = import_network.RouteHandler.prepareInterceptionPatterns(this._routes);
|
||||||
|
await this._wrapApiCall(() => this._channel.setNetworkInterceptionPatterns({ patterns }), options);
|
||||||
|
}
|
||||||
|
async _updateWebSocketInterceptionPatterns(options) {
|
||||||
|
const patterns = import_network.WebSocketRouteHandler.prepareInterceptionPatterns(this._webSocketRoutes);
|
||||||
|
await this._wrapApiCall(() => this._channel.setWebSocketInterceptionPatterns({ patterns }), options);
|
||||||
|
}
|
||||||
|
async screenshot(options = {}) {
|
||||||
|
const mask = options.mask;
|
||||||
|
const copy = { ...options, mask: void 0, timeout: this._timeoutSettings.timeout(options) };
|
||||||
|
if (!copy.type)
|
||||||
|
copy.type = (0, import_elementHandle.determineScreenshotType)(options);
|
||||||
|
if (mask) {
|
||||||
|
copy.mask = mask.map((locator) => ({
|
||||||
|
frame: locator._frame._channel,
|
||||||
|
selector: locator._selector
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
const result = await this._channel.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 _expectScreenshot(options) {
|
||||||
|
const mask = options?.mask ? options?.mask.map((locator2) => ({
|
||||||
|
frame: locator2._frame._channel,
|
||||||
|
selector: locator2._selector
|
||||||
|
})) : void 0;
|
||||||
|
const locator = options.locator ? {
|
||||||
|
frame: options.locator._frame._channel,
|
||||||
|
selector: options.locator._selector
|
||||||
|
} : void 0;
|
||||||
|
return await this._channel.expectScreenshot({
|
||||||
|
...options,
|
||||||
|
isNot: !!options.isNot,
|
||||||
|
locator,
|
||||||
|
mask
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async title() {
|
||||||
|
return await this._mainFrame.title();
|
||||||
|
}
|
||||||
|
async bringToFront() {
|
||||||
|
await this._channel.bringToFront();
|
||||||
|
}
|
||||||
|
async [Symbol.asyncDispose]() {
|
||||||
|
await this.close();
|
||||||
|
}
|
||||||
|
async close(options = {}) {
|
||||||
|
this._closeReason = options.reason;
|
||||||
|
this._closeWasCalled = true;
|
||||||
|
try {
|
||||||
|
if (this._ownedContext)
|
||||||
|
await this._ownedContext.close();
|
||||||
|
else
|
||||||
|
await this._channel.close(options);
|
||||||
|
} catch (e) {
|
||||||
|
if ((0, import_errors.isTargetClosedError)(e) && !options.runBeforeUnload)
|
||||||
|
return;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isClosed() {
|
||||||
|
return this._closed;
|
||||||
|
}
|
||||||
|
async click(selector, options) {
|
||||||
|
return await this._mainFrame.click(selector, options);
|
||||||
|
}
|
||||||
|
async dragAndDrop(source, target, options) {
|
||||||
|
return await this._mainFrame.dragAndDrop(source, target, options);
|
||||||
|
}
|
||||||
|
async dblclick(selector, options) {
|
||||||
|
await this._mainFrame.dblclick(selector, options);
|
||||||
|
}
|
||||||
|
async tap(selector, options) {
|
||||||
|
return await this._mainFrame.tap(selector, options);
|
||||||
|
}
|
||||||
|
async fill(selector, value, options) {
|
||||||
|
return await this._mainFrame.fill(selector, value, options);
|
||||||
|
}
|
||||||
|
locator(selector, options) {
|
||||||
|
return this.mainFrame().locator(selector, options);
|
||||||
|
}
|
||||||
|
getByTestId(testId) {
|
||||||
|
return this.mainFrame().getByTestId(testId);
|
||||||
|
}
|
||||||
|
getByAltText(text, options) {
|
||||||
|
return this.mainFrame().getByAltText(text, options);
|
||||||
|
}
|
||||||
|
getByLabel(text, options) {
|
||||||
|
return this.mainFrame().getByLabel(text, options);
|
||||||
|
}
|
||||||
|
getByPlaceholder(text, options) {
|
||||||
|
return this.mainFrame().getByPlaceholder(text, options);
|
||||||
|
}
|
||||||
|
getByText(text, options) {
|
||||||
|
return this.mainFrame().getByText(text, options);
|
||||||
|
}
|
||||||
|
getByTitle(text, options) {
|
||||||
|
return this.mainFrame().getByTitle(text, options);
|
||||||
|
}
|
||||||
|
getByRole(role, options = {}) {
|
||||||
|
return this.mainFrame().getByRole(role, options);
|
||||||
|
}
|
||||||
|
frameLocator(selector) {
|
||||||
|
return this.mainFrame().frameLocator(selector);
|
||||||
|
}
|
||||||
|
async focus(selector, options) {
|
||||||
|
return await this._mainFrame.focus(selector, options);
|
||||||
|
}
|
||||||
|
async textContent(selector, options) {
|
||||||
|
return await this._mainFrame.textContent(selector, options);
|
||||||
|
}
|
||||||
|
async innerText(selector, options) {
|
||||||
|
return await this._mainFrame.innerText(selector, options);
|
||||||
|
}
|
||||||
|
async innerHTML(selector, options) {
|
||||||
|
return await this._mainFrame.innerHTML(selector, options);
|
||||||
|
}
|
||||||
|
async getAttribute(selector, name, options) {
|
||||||
|
return await this._mainFrame.getAttribute(selector, name, options);
|
||||||
|
}
|
||||||
|
async inputValue(selector, options) {
|
||||||
|
return await this._mainFrame.inputValue(selector, options);
|
||||||
|
}
|
||||||
|
async isChecked(selector, options) {
|
||||||
|
return await this._mainFrame.isChecked(selector, options);
|
||||||
|
}
|
||||||
|
async isDisabled(selector, options) {
|
||||||
|
return await this._mainFrame.isDisabled(selector, options);
|
||||||
|
}
|
||||||
|
async isEditable(selector, options) {
|
||||||
|
return await this._mainFrame.isEditable(selector, options);
|
||||||
|
}
|
||||||
|
async isEnabled(selector, options) {
|
||||||
|
return await this._mainFrame.isEnabled(selector, options);
|
||||||
|
}
|
||||||
|
async isHidden(selector, options) {
|
||||||
|
return await this._mainFrame.isHidden(selector, options);
|
||||||
|
}
|
||||||
|
async isVisible(selector, options) {
|
||||||
|
return await this._mainFrame.isVisible(selector, options);
|
||||||
|
}
|
||||||
|
async hover(selector, options) {
|
||||||
|
return await this._mainFrame.hover(selector, options);
|
||||||
|
}
|
||||||
|
async selectOption(selector, values, options) {
|
||||||
|
return await this._mainFrame.selectOption(selector, values, options);
|
||||||
|
}
|
||||||
|
async setInputFiles(selector, files, options) {
|
||||||
|
return await this._mainFrame.setInputFiles(selector, files, options);
|
||||||
|
}
|
||||||
|
async type(selector, text, options) {
|
||||||
|
return await this._mainFrame.type(selector, text, options);
|
||||||
|
}
|
||||||
|
async press(selector, key, options) {
|
||||||
|
return await this._mainFrame.press(selector, key, options);
|
||||||
|
}
|
||||||
|
async check(selector, options) {
|
||||||
|
return await this._mainFrame.check(selector, options);
|
||||||
|
}
|
||||||
|
async uncheck(selector, options) {
|
||||||
|
return await this._mainFrame.uncheck(selector, options);
|
||||||
|
}
|
||||||
|
async setChecked(selector, checked, options) {
|
||||||
|
return await this._mainFrame.setChecked(selector, checked, options);
|
||||||
|
}
|
||||||
|
async waitForTimeout(timeout) {
|
||||||
|
return await this._mainFrame.waitForTimeout(timeout);
|
||||||
|
}
|
||||||
|
async waitForFunction(pageFunction, arg, options) {
|
||||||
|
return await this._mainFrame.waitForFunction(pageFunction, arg, options);
|
||||||
|
}
|
||||||
|
workers() {
|
||||||
|
return [...this._workers];
|
||||||
|
}
|
||||||
|
async pause(_options) {
|
||||||
|
if (this._platform.isJSDebuggerAttached())
|
||||||
|
return;
|
||||||
|
const defaultNavigationTimeout = this._browserContext._timeoutSettings.defaultNavigationTimeout();
|
||||||
|
const defaultTimeout = this._browserContext._timeoutSettings.defaultTimeout();
|
||||||
|
this._browserContext.setDefaultNavigationTimeout(0);
|
||||||
|
this._browserContext.setDefaultTimeout(0);
|
||||||
|
this._instrumentation?.onWillPause({ keepTestTimeout: !!_options?.__testHookKeepTestTimeout });
|
||||||
|
await this._closedOrCrashedScope.safeRace(this.context()._channel.pause());
|
||||||
|
this._browserContext.setDefaultNavigationTimeout(defaultNavigationTimeout);
|
||||||
|
this._browserContext.setDefaultTimeout(defaultTimeout);
|
||||||
|
}
|
||||||
|
async pdf(options = {}) {
|
||||||
|
const transportOptions = { ...options };
|
||||||
|
if (transportOptions.margin)
|
||||||
|
transportOptions.margin = { ...transportOptions.margin };
|
||||||
|
if (typeof options.width === "number")
|
||||||
|
transportOptions.width = options.width + "px";
|
||||||
|
if (typeof options.height === "number")
|
||||||
|
transportOptions.height = options.height + "px";
|
||||||
|
for (const margin of ["top", "right", "bottom", "left"]) {
|
||||||
|
const index = margin;
|
||||||
|
if (options.margin && typeof options.margin[index] === "number")
|
||||||
|
transportOptions.margin[index] = transportOptions.margin[index] + "px";
|
||||||
|
}
|
||||||
|
const result = await this._channel.pdf(transportOptions);
|
||||||
|
if (options.path) {
|
||||||
|
const platform = this._platform;
|
||||||
|
await platform.fs().promises.mkdir(platform.path().dirname(options.path), { recursive: true });
|
||||||
|
await platform.fs().promises.writeFile(options.path, result.pdf);
|
||||||
|
}
|
||||||
|
return result.pdf;
|
||||||
|
}
|
||||||
|
async _snapshotForAI(options = {}) {
|
||||||
|
const result = await this._channel.snapshotForAI({ timeout: this._timeoutSettings.timeout(options) });
|
||||||
|
return result.snapshot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class BindingCall extends import_channelOwner.ChannelOwner {
|
||||||
|
static from(channel) {
|
||||||
|
return channel._object;
|
||||||
|
}
|
||||||
|
constructor(parent, type, guid, initializer) {
|
||||||
|
super(parent, type, guid, initializer);
|
||||||
|
}
|
||||||
|
async call(func) {
|
||||||
|
try {
|
||||||
|
const frame = import_frame.Frame.from(this._initializer.frame);
|
||||||
|
const source = {
|
||||||
|
context: frame._page.context(),
|
||||||
|
page: frame._page,
|
||||||
|
frame
|
||||||
|
};
|
||||||
|
let result;
|
||||||
|
if (this._initializer.handle)
|
||||||
|
result = await func(source, import_jsHandle.JSHandle.from(this._initializer.handle));
|
||||||
|
else
|
||||||
|
result = await func(source, ...this._initializer.args.map(import_jsHandle.parseResult));
|
||||||
|
this._channel.resolve({ result: (0, import_jsHandle.serializeArgument)(result) }).catch(() => {
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
this._channel.reject({ error: (0, import_errors.serializeError)(e) }).catch(() => {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function trimUrl(param) {
|
||||||
|
if ((0, import_rtti.isRegExp)(param))
|
||||||
|
return `/${(0, import_stringUtils.trimStringWithEllipsis)(param.source, 50)}/${param.flags}`;
|
||||||
|
if ((0, import_rtti.isString)(param))
|
||||||
|
return `"${(0, import_stringUtils.trimStringWithEllipsis)(param, 50)}"`;
|
||||||
|
}
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
BindingCall,
|
||||||
|
Page
|
||||||
|
});
|
||||||
74
node_modules/playwright-core/lib/client/platform.js
generated
vendored
Normal file
74
node_modules/playwright-core/lib/client/platform.js
generated
vendored
Normal 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 platform_exports = {};
|
||||||
|
__export(platform_exports, {
|
||||||
|
emptyPlatform: () => emptyPlatform
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(platform_exports);
|
||||||
|
var import_colors = require("../utils/isomorphic/colors");
|
||||||
|
const noopZone = {
|
||||||
|
push: () => noopZone,
|
||||||
|
pop: () => noopZone,
|
||||||
|
run: (func) => func(),
|
||||||
|
data: () => void 0
|
||||||
|
};
|
||||||
|
const emptyPlatform = {
|
||||||
|
name: "empty",
|
||||||
|
boxedStackPrefixes: () => [],
|
||||||
|
calculateSha1: async () => {
|
||||||
|
throw new Error("Not implemented");
|
||||||
|
},
|
||||||
|
colors: import_colors.webColors,
|
||||||
|
createGuid: () => {
|
||||||
|
throw new Error("Not implemented");
|
||||||
|
},
|
||||||
|
defaultMaxListeners: () => 10,
|
||||||
|
env: {},
|
||||||
|
fs: () => {
|
||||||
|
throw new Error("Not implemented");
|
||||||
|
},
|
||||||
|
inspectCustom: void 0,
|
||||||
|
isDebugMode: () => false,
|
||||||
|
isJSDebuggerAttached: () => false,
|
||||||
|
isLogEnabled(name) {
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
isUnderTest: () => false,
|
||||||
|
log(name, message) {
|
||||||
|
},
|
||||||
|
path: () => {
|
||||||
|
throw new Error("Function not implemented.");
|
||||||
|
},
|
||||||
|
pathSeparator: "/",
|
||||||
|
showInternalStackFrames: () => false,
|
||||||
|
streamFile(path, writable) {
|
||||||
|
throw new Error("Streams are not available");
|
||||||
|
},
|
||||||
|
streamReadable: (channel) => {
|
||||||
|
throw new Error("Streams are not available");
|
||||||
|
},
|
||||||
|
streamWritable: (channel) => {
|
||||||
|
throw new Error("Streams are not available");
|
||||||
|
},
|
||||||
|
zones: { empty: noopZone, current: () => noopZone }
|
||||||
|
};
|
||||||
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
|
0 && (module.exports = {
|
||||||
|
emptyPlatform
|
||||||
|
});
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user