Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/erp/i18n/languages/erp.php
+++ b/erp/i18n/languages/erp.php
@@ -1,5 +1,31 @@
<?php
return [
+ __( 'Dropdown', 'erp' ),
+ __( 'Oops! No item found.', 'erp' ),
+ __( 'More', 'erp' ),
+ __( 'Upload Image', 'erp' ),
+ __( 'Select date', 'erp' ),
+ __( 'Save Changes', 'erp' ),
+ __( 'Get WooCommerce Extension', 'erp' ),
+ __( 'Activate WooCommerce', 'erp' ),
+ __( 'Get WP ERP Pro', 'erp' ),
+ __( 'Test Connection', 'erp' ),
+ __( 'Testing Connection', 'erp' ),
+ __( ' Test Connection', 'erp' ),
+ __( ' OK', 'erp' ),
+ __( 'Delete', 'erp' ),
+ __( 'Are you sure to delete this ?', 'erp' ),
+ __( 'Cancel', 'erp' ),
+ __( 'Template Name', 'erp' ),
+ __( 'Description', 'erp' ),
+ __( 'Disable / Enable', 'erp' ),
+ __( 'Integration', 'erp' ),
+ __( 'Extension', 'erp' ),
+ __( 'Version', 'erp' ),
+ __( 'License Key', 'erp' ),
+ __( 'Status', 'erp' ),
+ __( 'Settings', 'erp' ),
+ __( 'Search', 'erp' ),
_x( 'ltr', 'text direction' ),
__( 'Name', 'erp' ),
__( 'Start Date', 'erp' ),
@@ -7,9 +33,6 @@
__( 'Start date', 'erp' ),
__( 'End date', 'erp' ),
__( 'Add New', 'erp' ),
- __( 'Save Changes', 'erp' ),
- __( 'Dropdown', 'erp' ),
- __( 'Upload Image', 'erp' ),
__( 'Please complete these fields', 'erp' ),
__( 'Submit', 'erp' ),
__( 'Form Field', 'erp' ),
@@ -19,11 +42,7 @@
__( 'Incoming Email Setting', 'erp' ),
__( 'Click to Authorize your gmail account', 'erp' ),
__( 'Disconnect', 'erp' ),
- __( 'Test Connection', 'erp' ),
__( 'Click on the Above Button Before Saving the Setting', 'erp' ),
- __( 'Testing Connection', 'erp' ),
- __( ' Test Connection', 'erp' ),
- __( ' OK', 'erp' ),
__( 'Test Mail', 'erp' ),
__( 'An Email Address to Test the Connection', 'erp' ),
__( 'Email here', 'erp' ),
@@ -37,37 +56,18 @@
__( 'Body', 'erp' ),
__( 'Template Tags', 'erp' ),
__( 'Save', 'erp' ),
- __( 'Cancel', 'erp' ),
- __( 'Template Name', 'erp' ),
- __( 'Description', 'erp' ),
- __( 'Disable / Enable', 'erp' ),
__( 'Saved Replies', 'erp' ),
__( 'Enable/Disable', 'erp' ),
__( 'Add new Template', 'erp' ),
__( 'Edit', 'erp' ),
__( 'Short Codes', 'erp' ),
- __( 'Delete', 'erp' ),
- __( 'Are you sure to delete this ?', 'erp' ),
__( ' Integration', 'erp' ),
__( 'Active Gateway', 'erp' ),
__( 'Test Dropbox Connection', 'erp' ),
- __( 'Integration', 'erp' ),
- __( 'Select date', 'erp' ),
__( 'No extensions found.', 'erp' ),
- __( 'Extension', 'erp' ),
- __( 'Version', 'erp' ),
- __( 'License Key', 'erp' ),
- __( 'Status', 'erp' ),
- __( 'Settings', 'erp' ),
- __( 'More', 'erp' ),
- __( 'Search', 'erp' ),
__( 'Sorry ! Nothings found for your query. Please try again !', 'erp' ),
- __( 'Oops! No item found.', 'erp' ),
__( 'Please search', 'erp' ),
__( 'Connect with WooCommerce', 'erp' ),
- __( 'Get WooCommerce Extension', 'erp' ),
- __( 'Activate WooCommerce', 'erp' ),
- __( 'Get WP ERP Pro', 'erp' ),
__( 'Accounts', 'erp' ),
__( 'New Transaction', 'erp' ),
__( 'To edit a bank account, please navigate to the Chart of Accounts.', 'erp' ),
--- a/erp/includes/Headway/Headway.php
+++ b/erp/includes/Headway/Headway.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace WeDevsERPHeadway;
+
+class Headway {
+
+ public function __construct() {
+ add_filter( 'script_loader_tag', [ $this, 'add_async_attribute' ], 10, 3 );
+ add_action( 'admin_enqueue_scripts', [ $this, 'register_assets' ] );
+ }
+
+ public function register_assets() {
+ wp_register_script(
+ 'erp-headway',
+ plugin_dir_url( __FILE__ ) . 'headway.js',
+ [ 'jquery' ],
+ WPERP_VERSION,
+ true
+ );
+
+ wp_register_style(
+ 'erp-headway',
+ plugin_dir_url( __FILE__ ) . 'headway.css',
+ [],
+ WPERP_VERSION
+ );
+ }
+
+ public function add_async_attribute( $tag, $handle, $src ) {
+ if ( 'erp-headway' === $handle ) {
+ return str_replace( ' src', ' async src', $tag );
+ }
+ return $tag;
+ }
+}
--- a/erp/includes/Scripts.php
+++ b/erp/includes/Scripts.php
@@ -36,14 +36,6 @@
$this->version = WPERP_VERSION;
$this->action( 'admin_enqueue_scripts', 'scripts_handler' );
- add_filter( 'script_loader_tag', [ $this, 'add_async_to_headway' ], 10, 3 );
- }
-
- public function add_async_to_headway( $tag, $handle, $src ) {
- if ( 'erp-headway' === $handle ) {
- return str_replace( ' src', ' async src', $tag );
- }
- return $tag;
}
/**
@@ -120,8 +112,6 @@
// date range picker
wp_register_script( 'erp-daterangepicker', $vendor . '/daterangepicker/daterangepicker.min.js', [ 'jquery' ], $this->version, true );
- // headway changelog widget
- wp_register_script( 'erp-headway', WPERP_ASSETS . '/vendor/headway.js', [ 'jquery' ], $this->version, true );
}
/**
--- a/erp/includes/functions.php
+++ b/erp/includes/functions.php
@@ -4155,10 +4155,11 @@
}
/**
- * Load the Headway changelog badge widget (mirrors WPUF pattern)
+ * Load the Headway changelog badge widget.
*/
function erp_load_headway_badge() {
wp_enqueue_script( 'erp-headway' );
+ wp_enqueue_style( 'erp-headway' );
?>
<span class="erp-headway-wrap">
<button type="button" id="erp-headway-btn" class="erp-cta-hover-btn erp-headway-trigger" title="<?php esc_attr_e( "What's New", 'erp' ); ?>">
--- a/erp/modules/crm/includes/CLI/Commands.php
+++ b/erp/modules/crm/includes/CLI/Commands.php
@@ -43,11 +43,16 @@
}
}
- // Load all Seed*.php files, except SeedCommand.php which is loaded separately.
+ // Load all Seed*.php files. SeedCommand.php loaded last so sub-commands register first.
foreach ( glob( $seed_dir . '/Seed*.php' ) as $seed_file ) {
if ( basename( $seed_file ) === 'SeedCommand.php' ) {
continue;
}
require_once $seed_file;
}
+
+ $seed_command = $seed_dir . '/SeedCommand.php';
+ if ( file_exists( $seed_command ) ) {
+ require_once $seed_command;
+ }
}
--- a/erp/modules/crm/includes/CLI/Seed/CrmDataProvider.php
+++ b/erp/modules/crm/includes/CLI/Seed/CrmDataProvider.php
@@ -26,6 +26,21 @@
[ 'name' => 'NexGen Retail', 'website' => 'https://nexgenretail.example.com' ],
[ 'name' => 'Stellar Manufacturing', 'website' => 'https://stellarmfg.example.com' ],
[ 'name' => 'Pacific Trading Co', 'website' => 'https://pacifictrading.example.com' ],
+ [ 'name' => 'Apex Digital Agency', 'website' => 'https://apexdigital.example.com' ],
+ [ 'name' => 'Ironclad Security', 'website' => 'https://ironcladsec.example.com' ],
+ [ 'name' => 'Meridian Analytics', 'website' => 'https://meridiananalytics.example.com' ],
+ [ 'name' => 'Cascade Renewable Energy', 'website' => 'https://cascaderenew.example.com' ],
+ [ 'name' => 'Vanguard Pharma', 'website' => 'https://vanguardpharma.example.com' ],
+ [ 'name' => 'Northstar Ventures', 'website' => 'https://northstarvc.example.com' ],
+ [ 'name' => 'Redwood Hospitality', 'website' => 'https://redwoodhosp.example.com' ],
+ [ 'name' => 'Titan Aerospace', 'website' => 'https://titanaero.example.com' ],
+ [ 'name' => 'Luminary Creative', 'website' => 'https://luminarycreative.example.com' ],
+ [ 'name' => 'Cobalt Fintech', 'website' => 'https://cobaltfintech.example.com' ],
+ [ 'name' => 'Anchor Real Estate', 'website' => 'https://anchorre.example.com' ],
+ [ 'name' => 'Vantage Telecom', 'website' => 'https://vantagetelecom.example.com' ],
+ [ 'name' => 'Sterling Legal Group', 'website' => 'https://sterlinglegl.example.com' ],
+ [ 'name' => 'Oasis Wellness', 'website' => 'https://oasiswellness.example.com' ],
+ [ 'name' => 'Frontier Agritech', 'website' => 'https://frontieragri.example.com' ],
];
}
@@ -43,6 +58,14 @@
'Andrew', 'Ella', 'Joshua', 'Madison', 'Kenneth', 'Scarlett', 'Kevin', 'Victoria',
'Brian', 'Aria', 'George', 'Grace', 'Timothy', 'Chloe', 'Ronald', 'Camila',
'Edward', 'Penelope', 'Jason', 'Riley',
+ 'Nathan', 'Zoe', 'Ryan', 'Hannah', 'Tyler', 'Lily', 'Brandon', 'Natalie',
+ 'Samuel', 'Leah', 'Benjamin', 'Audrey', 'Jacob', 'Savannah', 'Logan', 'Brooklyn',
+ 'Ethan', 'Bella', 'Dylan', 'Claire', 'Zachary', 'Skylar', 'Austin', 'Lucy',
+ 'Lucas', 'Paisley', 'Mason', 'Everly', 'Liam', 'Anna', 'Noah', 'Caroline',
+ 'Aiden', 'Genesis', 'Caleb', 'Aaliyah', 'Hunter', 'Kennedy', 'Connor', 'Sadie',
+ 'Jordan', 'Hailey', 'Adrian', 'Eva', 'Gavin', 'Naomi', 'Ian', 'Aurora',
+ 'Carlos', 'Maya', 'Luis', 'Layla', 'Alex', 'Elena', 'Omar', 'Fatima',
+ 'Ravi', 'Priya', 'Arjun', 'Ananya', 'Wei', 'Lin', 'Yusuf', 'Amira',
];
}
@@ -60,6 +83,12 @@
'Walker', 'Young', 'Allen', 'King', 'Wright', 'Scott', 'Torres', 'Nguyen',
'Hill', 'Flores', 'Green', 'Adams', 'Nelson', 'Baker', 'Hall', 'Rivera',
'Campbell', 'Mitchell', 'Carter', 'Roberts',
+ 'Chen', 'Patel', 'Kumar', 'Singh', 'Kim', 'Park', 'Yamamoto', 'Nakamura',
+ 'Ahmed', 'Hassan', 'Ali', 'Khan', 'Sharma', 'Gupta', 'Nair', 'Rao',
+ 'Santos', 'Silva', 'Oliveira', 'Ferreira', 'Dubois', 'Dupont', 'Bernard', 'Morin',
+ 'Müller', 'Schmidt', 'Fischer', 'Weber', 'Rossi', 'Ferrari', 'Russo', 'Esposito',
+ 'Murphy', 'Kelly', 'Sullivan', 'Walsh', 'Andersen', 'Larsen', 'Eriksson', 'Lindqvist',
+ 'Morrison', 'Dixon', 'Fletcher', 'Barnes', 'Owens', 'Bryant', 'Simmons', 'Ford',
];
}
@@ -77,6 +106,13 @@
'Purchasing Manager', 'Operations Manager', 'Customer Success Manager',
'IT Director', 'Finance Director', 'Creative Director',
'Sales Representative', 'Marketing Coordinator', 'Office Manager',
+ 'Chief Revenue Officer', 'VP of Engineering', 'VP of Product',
+ 'Head of Growth', 'Head of Customer Experience', 'Head of Partnerships',
+ 'Solutions Architect', 'Senior Account Manager', 'Regional Sales Director',
+ 'Digital Marketing Specialist', 'Data Analyst', 'DevOps Engineer',
+ 'Procurement Specialist', 'Supply Chain Manager', 'Legal Counsel',
+ 'Brand Manager', 'Content Strategist', 'UX Designer',
+ 'Enterprise Architect', 'Security Engineer', 'Cloud Architect',
];
}
@@ -123,6 +159,14 @@
[ 'name' => 'Partner Network', 'description' => 'Business partners and affiliates' ],
[ 'name' => 'Webinar Registrants', 'description' => 'Contacts who registered for webinars' ],
[ 'name' => 'Inactive Contacts', 'description' => 'Contacts with no activity in last 6 months' ],
+ [ 'name' => 'Cold Leads', 'description' => 'Prospects with no recent engagement' ],
+ [ 'name' => 'Warm Prospects', 'description' => 'Prospects showing active interest' ],
+ [ 'name' => 'Churned Customers', 'description' => 'Previously active customers who cancelled' ],
+ [ 'name' => 'Referral Sources', 'description' => 'Contacts who have referred new business' ],
+ [ 'name' => 'SMB Segment', 'description' => 'Small and medium business contacts' ],
+ [ 'name' => 'Mid-Market Segment', 'description' => 'Mid-market company contacts' ],
+ [ 'name' => 'Strategic Accounts', 'description' => 'Key accounts with long-term potential' ],
+ [ 'name' => 'Conference Leads 2025', 'description' => 'Leads acquired at 2025 conferences' ],
];
}
@@ -136,6 +180,10 @@
'123 Main St', '456 Oak Ave', '789 Pine Rd', '321 Elm Blvd', '654 Maple Dr',
'987 Cedar Ln', '147 Birch Way', '258 Walnut St', '369 Cherry Ave', '741 Spruce Rd',
'852 Willow Blvd', '963 Ash Dr', '159 Poplar Ln', '357 Hickory Way', '468 Chestnut St',
+ '24 Riverside Dr', '88 Harbor View Rd', '200 Innovation Blvd', '15 Commerce St',
+ '77 Parkside Ave', '1400 Technology Way', '300 Lakeshore Dr', '55 Gateway Pl',
+ '901 Industrial Pkwy', '12 Summit Ridge Rd', '640 Westfield Ct', '18 Orchard Ln',
+ '500 Executive Dr', '3300 Campus Way', '47 Millbrook Rd',
];
}
@@ -156,9 +204,26 @@
[ 'city' => 'San Diego', 'state' => 'CA', 'postal' => '92101', 'country' => 'US' ],
[ 'city' => 'Dallas', 'state' => 'TX', 'postal' => '75201', 'country' => 'US' ],
[ 'city' => 'San Jose', 'state' => 'CA', 'postal' => '95101', 'country' => 'US' ],
+ [ 'city' => 'Seattle', 'state' => 'WA', 'postal' => '98101', 'country' => 'US' ],
+ [ 'city' => 'Denver', 'state' => 'CO', 'postal' => '80201', 'country' => 'US' ],
+ [ 'city' => 'Boston', 'state' => 'MA', 'postal' => '02101', 'country' => 'US' ],
+ [ 'city' => 'Atlanta', 'state' => 'GA', 'postal' => '30301', 'country' => 'US' ],
+ [ 'city' => 'Miami', 'state' => 'FL', 'postal' => '33101', 'country' => 'US' ],
+ [ 'city' => 'Portland', 'state' => 'OR', 'postal' => '97201', 'country' => 'US' ],
+ [ 'city' => 'Austin', 'state' => 'TX', 'postal' => '78701', 'country' => 'US' ],
+ [ 'city' => 'Minneapolis', 'state' => 'MN', 'postal' => '55401', 'country' => 'US' ],
[ 'city' => 'London', 'state' => '', 'postal' => 'EC1A 1BB', 'country' => 'GB' ],
+ [ 'city' => 'Manchester', 'state' => '', 'postal' => 'M1 1AA', 'country' => 'GB' ],
[ 'city' => 'Toronto', 'state' => 'ON', 'postal' => 'M5H 2N2', 'country' => 'CA' ],
+ [ 'city' => 'Vancouver', 'state' => 'BC', 'postal' => 'V6B 1A1', 'country' => 'CA' ],
[ 'city' => 'Sydney', 'state' => 'NSW', 'postal' => '2000', 'country' => 'AU' ],
+ [ 'city' => 'Melbourne', 'state' => 'VIC', 'postal' => '3000', 'country' => 'AU' ],
+ [ 'city' => 'Berlin', 'state' => '', 'postal' => '10115', 'country' => 'DE' ],
+ [ 'city' => 'Paris', 'state' => '', 'postal' => '75001', 'country' => 'FR' ],
+ [ 'city' => 'Amsterdam', 'state' => '', 'postal' => '1011', 'country' => 'NL' ],
+ [ 'city' => 'Singapore', 'state' => '', 'postal' => '018989', 'country' => 'SG' ],
+ [ 'city' => 'Dubai', 'state' => '', 'postal' => '00000', 'country' => 'AE' ],
+ [ 'city' => 'Mumbai', 'state' => 'MH', 'postal' => '400001', 'country' => 'IN' ],
];
}
@@ -184,6 +249,21 @@
'Integration requirements documented. Technical team to review.',
'Quarterly business review scheduled. Prepare performance metrics.',
'Upsell opportunity identified. They need additional user licenses.',
+ 'Left voicemail. Will try again tomorrow.',
+ 'Sent intro email. Awaiting response.',
+ 'Contact referred us to their CTO. Meeting being arranged.',
+ 'Pilot evaluation extended by two weeks due to internal resourcing.',
+ 'Security questionnaire completed and submitted to procurement.',
+ 'Executive sponsor confirmed — deal fast-tracked to contract stage.',
+ 'Trial feedback very positive. Moving to commercial discussion.',
+ 'Pricing objection resolved with revised tier proposal.',
+ 'Decision delayed to next quarter due to board freeze.',
+ 'New contact at company after key champion left — re-engaging.',
+ 'Requested API documentation and integration guide.',
+ 'Confirmed mobile compatibility requirement — checking with product team.',
+ 'Customer attended webinar and asked follow-up questions on reporting.',
+ 'Annual review call completed. Customer expanded to 3 additional departments.',
+ 'Partner intro meeting went well. Joint go-to-market discussion started.',
];
}
@@ -209,6 +289,16 @@
'Renewal Discussion',
'Feature Request Review',
'Training Session',
+ 'Security Review Meeting',
+ 'Integration Scoping Session',
+ 'Pilot Kickoff',
+ 'Champion Alignment Call',
+ 'Competitive Differentiator Walkthrough',
+ 'Post-Implementation Review',
+ 'Annual Account Planning',
+ 'Stakeholder Introduction',
+ 'Procurement Process Overview',
+ 'Pricing Workshop',
];
}
@@ -231,6 +321,14 @@
'Account review',
'Feedback collection',
'Referral request',
+ 'Contract status check',
+ 'Escalation follow-up',
+ 'Onboarding progress check',
+ 'Decision timeline inquiry',
+ 'Reference check assistance',
+ 'Stakeholder introduction',
+ 'Win/loss debrief',
+ 'Reactivation outreach',
];
}
@@ -256,6 +354,20 @@
'Update contact information',
'Process refund request',
'Schedule training session',
+ 'Complete security questionnaire',
+ 'Draft SOW document',
+ 'Arrange reference call',
+ 'Submit legal NDA for review',
+ 'Confirm budget approval',
+ 'Upload signed contract',
+ 'Create onboarding checklist',
+ 'Send product roadmap summary',
+ 'Identify upsell opportunity',
+ 'Log call notes in CRM',
+ 'Send invoice to accounts',
+ 'Coordinate with support on open ticket',
+ 'Prepare executive summary',
+ 'Request LinkedIn introduction',
];
}
@@ -278,6 +390,16 @@
'Special offer just for you',
'Invoice attached',
'Contract for your review',
+ 'Next steps after our call',
+ 'Resources from today's demo',
+ 'Action items from our meeting',
+ 'Your trial is expiring soon',
+ 'Case study relevant to your industry',
+ 'Referral program invitation',
+ 'Quarterly newsletter — Q1 2025',
+ 'Security compliance documentation enclosed',
+ 'Integration guide for your team',
+ 'Checking in — any questions?',
];
}
@@ -310,6 +432,16 @@
[ 'title' => 'Closed Won', 'probability' => 100, 'order' => 7, 'life_stage' => 'customer' ],
],
],
+ [
+ 'title' => 'Partner Pipeline',
+ 'stages' => [
+ [ 'title' => 'Initial Contact', 'probability' => 10, 'order' => 1, 'life_stage' => 'lead' ],
+ [ 'title' => 'Partner Evaluation', 'probability' => 30, 'order' => 2, 'life_stage' => 'lead' ],
+ [ 'title' => 'Agreement Draft', 'probability' => 60, 'order' => 3, 'life_stage' => 'opportunity' ],
+ [ 'title' => 'Executive Sign-off', 'probability' => 85, 'order' => 4, 'life_stage' => 'opportunity' ],
+ [ 'title' => 'Active Partner', 'probability' => 100, 'order' => 5, 'life_stage' => 'customer' ],
+ ],
+ ],
];
}
@@ -335,6 +467,21 @@
'Consulting Engagement',
'Security Audit Services',
'Data Analytics Package',
+ 'Cloud Infrastructure Setup',
+ 'Managed Services Contract',
+ 'SaaS Expansion Deal',
+ 'API Access Agreement',
+ 'Compliance Toolkit License',
+ 'Customer Portal Deployment',
+ 'Mobile App Development',
+ 'ERP Implementation',
+ 'Business Intelligence Dashboard',
+ 'Automation Workflow Package',
+ 'Staff Augmentation Contract',
+ 'Digital Transformation Retainer',
+ 'Co-Marketing Partnership',
+ 'Reseller Agreement',
+ 'Premium Support Plan',
];
}
@@ -355,6 +502,14 @@
'Internal restructuring',
'Decision maker changed',
'Requirements changed',
+ 'Went with in-house solution',
+ 'Procurement process stalled',
+ 'Security concerns not resolved',
+ 'Integration complexity too high',
+ 'Company acquired',
+ 'Evaluation criteria shifted',
+ 'ROI not justified',
+ 'Executive sponsor left',
];
}
@@ -364,7 +519,7 @@
* @return array
*/
public static function currencies() {
- return [ 'USD', 'EUR', 'GBP', 'CAD', 'AUD' ];
+ return [ 'USD', 'EUR', 'GBP', 'CAD', 'AUD', 'SGD', 'AED', 'INR', 'JPY', 'NZD' ];
}
/**
--- a/erp/modules/crm/includes/CLI/Seed/SeedCompanies.php
+++ b/erp/modules/crm/includes/CLI/Seed/SeedCompanies.php
@@ -34,17 +34,23 @@
$cities = CrmDataProvider::cities();
$streets = CrmDataProvider::streets();
- $progress = $this->progress( 'Creating companies', min( $count, count( $companies ) ) );
+ $progress = $this->progress( 'Creating companies', $count );
$created_ids = [];
$fail_count = 0;
- for ( $i = 0; $i < $count && $i < count( $companies ); $i++ ) {
- $company_data = $companies[ $i ];
+ for ( $i = 0; $i < $count; $i++ ) {
+ $template = $companies[ $i % count( $companies ) ];
+ $cycle = (int) floor( $i / count( $companies ) );
+ $company_data = [
+ 'name' => $cycle > 0 ? $template['name'] . ' ' . ( $cycle + 1 ) : $template['name'],
+ 'website' => $template['website'],
+ ];
$city_data = $this->random_element( $cities );
// Generate a unique email for the company.
$email_domain = str_replace( [ 'https://', 'http://', 'www.' ], '', $company_data['website'] );
- $email = 'info@' . str_replace( '.example.com', '.test', $email_domain );
+ $email_base = str_replace( '.example.com', '.test', $email_domain );
+ $email = $cycle > 0 ? 'info' . ( $cycle + 1 ) . '@' . $email_base : 'info@' . $email_base;
$data = [
'type' => 'company',
--- a/erp/modules/crm/includes/CLI/Seed/SeedContactGroups.php
+++ b/erp/modules/crm/includes/CLI/Seed/SeedContactGroups.php
@@ -33,11 +33,16 @@
$count = (int) ( $assoc_args['count'] ?? 8 );
$groups = CrmDataProvider::contact_groups();
- $progress = $this->progress( 'Creating contact groups', min( $count, count( $groups ) ) );
+ $progress = $this->progress( 'Creating contact groups', $count );
$created_ids = [];
- for ( $i = 0; $i < $count && $i < count( $groups ); $i++ ) {
- $group = $groups[ $i ];
+ for ( $i = 0; $i < $count; $i++ ) {
+ $template = $groups[ $i % count( $groups ) ];
+ $cycle = (int) floor( $i / count( $groups ) );
+ $group = [
+ 'name' => $cycle > 0 ? $template['name'] . ' ' . ( $cycle + 1 ) : $template['name'],
+ 'description' => $template['description'],
+ ];
// Check if group already exists.
$existing = $wpdb->get_var(
--- a/erp/modules/crm/includes/functions-customer.php
+++ b/erp/modules/crm/includes/functions-customer.php
@@ -608,11 +608,18 @@
}
}
+ $invite_contact = ( isset( $postdata['invite_contact'] ) && ! empty( $postdata['invite_contact'] ) ) ? array_map( 'intval', (array) $postdata['invite_contact'] ) : [];
+
+ // Ensure the creator always appears in invite_contact so the schedule shows in their own calendar view.
+ if ( ! empty( $postdata['created_by'] ) && ! in_array( (int) $postdata['created_by'], $invite_contact, true ) ) {
+ $invite_contact[] = (int) $postdata['created_by'];
+ }
+
$extra_data = [
'schedule_title' => ( isset( $postdata['schedule_title'] ) && ! empty( $postdata['schedule_title'] ) ) ? $postdata['schedule_title'] : '',
'all_day' => isset( $postdata['all_day'] ) ? (string) $postdata['all_day'] : 'false',
'allow_notification' => isset( $postdata['allow_notification'] ) ? (string) $postdata['allow_notification'] : false,
- 'invite_contact' => ( isset( $postdata['invite_contact'] ) && ! empty( $postdata['invite_contact'] ) ) ? $postdata['invite_contact'] : [],
+ 'invite_contact' => $invite_contact,
'attachments' => ! empty ( $attachments ) ? $attachments : []
];
@@ -620,8 +627,8 @@
$extra_data['notification_time'] = ( isset( $postdata['notification_time'] ) && $extra_data['allow_notification'] == 'true' ) ? $postdata['notification_time'] : '';
$extra_data['notification_time_interval'] = ( isset( $postdata['notification_time_interval'] ) && $extra_data['allow_notification'] == 'true' ) ? $postdata['notification_time_interval'] : '';
- $start_time = ( isset( $postdata['start_time'] ) && ( $extra_data['all_day'] === 'false' ) ) ? $postdata['start_time'] : '00:00:00';
- $end_time = ( isset( $postdata['end_time'] ) && ( $extra_data['all_day'] === 'false' ) ) ? $postdata['end_time'] : '00:00:00';
+ $start_time = ( ! empty( $postdata['start_time'] ) && ( $extra_data['all_day'] === 'false' ) ) ? $postdata['start_time'] : '00:00:00';
+ $end_time = ( ! empty( $postdata['end_time'] ) && ( $extra_data['all_day'] === 'false' ) ) ? $postdata['end_time'] : '00:00:00';
if ( $extra_data['allow_notification'] == 'true' ) {
$notify_date = new DateTime( $postdata['start_date'] . $start_time );
@@ -791,8 +798,10 @@
}
$value['extra']['invited_user'] = [
- 'id' => $value['created_by']['ID'],
- 'name' => get_the_author_meta( 'display_name', $value['created_by']['ID'] )
+ [
+ 'id' => $value['created_by']['ID'],
+ 'name' => get_the_author_meta( 'display_name', $value['created_by']['ID'] ),
+ ],
];
}
@@ -2373,7 +2382,7 @@
} elseif ( 'if_has' === $search_val ) {
$custom_sql['where'][] = "( $field is not null AND $field != '' ) $add_or";
} else {
- $custom_sql['where'][] = "$field $search_condition '$search_val' $add_or";
+ $custom_sql['where'][] = $wpdb->prepare( "$field $search_condition %s $add_or", $search_val );
}
$j ++;
@@ -2392,9 +2401,9 @@
$add_or = ( $j === count( $value ) - 1 ) ? '' : ' OR ';
if ( count( $key_value ) > 1 ) {
- $custom_sql['where'][] = "( country $condition '$key_value[0]' AND state $condition '$key_value[1]')$add_or";
+ $custom_sql['where'][] = $wpdb->prepare( "( country $condition %s AND state $condition %s )$add_or", $key_value[0], $key_value[1] );
} else {
- $custom_sql['where'][] = "(country $condition '$key_value[0]')$add_or";
+ $custom_sql['where'][] = $wpdb->prepare( "(country $condition %s )$add_or", $key_value[0] );
}
$j ++;
@@ -2423,16 +2432,16 @@
switch ( $condition ) {
case 'NOT LIKE':
$search = str_replace( '!~', '', $search );
- $and_clause[] = "( subscriber.group_id = {$search} AND subscriber.unsubscribe_at IS NOT NULL )";
+ $and_clause[] = $wpdb->prepare( "( subscriber.group_id = %d AND subscriber.unsubscribe_at IS NOT NULL )", $search );
break;
case '!=':
$search = str_replace( '!', '', $search );
- $and_clause[] = "subscriber.group_id != {$search}";
+ $and_clause[] = $wpdb->prepare( "subscriber.group_id != %d", $search );
break;
default:
- $and_clause[] = "( subscriber.group_id = {$search} AND subscriber.unsubscribe_at IS NULL )";
+ $and_clause[] = $wpdb->prepare( "( subscriber.group_id = %d AND subscriber.unsubscribe_at IS NULL )", $search );
break;
}
}
@@ -2568,7 +2577,7 @@
return $sql;
}
- $sql['post_where_queries'][] = 'AND people.id = ' . $args['test_user'];
+ $sql['post_where_queries'][] = 'AND people.id = ' . absint( $args['test_user'] );
return $sql;
}
@@ -2852,7 +2861,7 @@
if ( $schedule['start_date'] < current_time( 'mysql' ) ) {
$time = gmdate( 'g:i a', strtotime( $schedule['start_date'] ) );
} else {
- if ( gmdate( 'g:i a', strtotime( $schedule['start_date'] ) ) == gmdate( 'g:i a', strtotime( $schedule['end_date'] ) ) || ! $schedule['end_date'] ) {
+ if ( ! $schedule['end_date'] || gmdate( 'g:i a', strtotime( $schedule['start_date'] ) ) == gmdate( 'g:i a', strtotime( $schedule['end_date'] ) ) ) {
$time = gmdate( 'g:i a', strtotime( $schedule['start_date'] ) );
} else {
$time = gmdate( 'g:i a', strtotime( $schedule['start_date'] ) ) . ' to ' . gmdate( 'g:i a', strtotime( $schedule['end_date'] ) );
--- a/erp/modules/crm/views/js-templates/single-schedule-details.php
+++ b/erp/modules/crm/views/js-templates/single-schedule-details.php
@@ -7,7 +7,7 @@
<# } #>
<# if ( data.schedule.type == 'tasks' ) { #>
- <?php esc_attr_e( 'assigned a task', 'erp' ); ?>
+ <?php esc_attr_e( 'assigned task', 'erp' ); ?>
<# } else if( ( data.schedule.type == 'log_activity' ) && ( new Date() < new Date( data.schedule.start_date ) ) ) { #>
<?php esc_attr_e( 'have scheduled', 'erp' ); ?>
<# } else { #>
--- a/erp/modules/crm/views/schedules.php
+++ b/erp/modules/crm/views/schedules.php
@@ -150,6 +150,20 @@
'step': 15
});
+ // Pre-fill time fields with current time so schedule doesn't default to 12:00 AM
+ var now = new Date();
+ var hh = now.getHours();
+ var mm = Math.round( now.getMinutes() / 15 ) * 15;
+ if ( mm === 60 ) { mm = 0; hh++; }
+ var ampm = hh >= 12 ? 'pm' : 'am';
+ var hour12 = hh % 12 || 12;
+ var defaultTime = hour12 + ':' + ( mm < 10 ? '0' + mm : mm ) + ' ' + ampm;
+ jQuery( '.erp-time-field' ).each( function() {
+ if ( ! jQuery( this ).val() ) {
+ jQuery( this ).val( defaultTime );
+ }
+ } );
+
$( 'select.erp-crm-contact-list-dropdown' ).select2({
allowClear: true,
placeholder: $(this).attr( 'data-placeholder' ),
--- a/erp/modules/hrm/includes/API/CompanyController.php
+++ b/erp/modules/hrm/includes/API/CompanyController.php
@@ -164,6 +164,16 @@
'permission_callback' => '__return_true',
],
] );
+
+ register_rest_route( $this->namespace, '/' . $this->rest_base . '/work-days', [
+ [
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => [ $this, 'get_work_days' ],
+ 'permission_callback' => function ( $request ) {
+ return current_user_can( 'erp_view_list' );
+ },
+ ],
+ ] );
}
/**
@@ -319,6 +329,50 @@
return $response;
}
+ /**
+ * Get the company weekly work-day schedule.
+ *
+ * Mirrors WP ERP HR settings → Work Days. Returns the raw per-weekday hours
+ * (0 = non-working) plus derived state labels and working/weekend lists so
+ * clients (e.g. the mobile app) can render the schedule without re-deriving
+ * the full/half/off mapping themselves.
+ *
+ * @param WP_REST_Request $request
+ *
+ * @return WP_REST_Response
+ */
+ public function get_work_days( $request ) {
+ $hours = erp_hr_get_work_days();
+
+ $labels = [];
+ $working_days = [];
+ $weekends = [];
+
+ foreach ( $hours as $day => $value ) {
+ $value = (int) $value;
+
+ if ( 0 === $value ) {
+ $labels[ $day ] = 'non_working';
+ $weekends[] = $day;
+ } elseif ( $value >= 8 ) {
+ $labels[ $day ] = 'full_day';
+ $working_days[] = $day;
+ } else {
+ $labels[ $day ] = 'half_day';
+ $working_days[] = $day;
+ }
+ }
+
+ return rest_ensure_response(
+ [
+ 'work_days' => $hours, // { mon: 8, ..., sat: 0, sun: 0 }
+ 'states' => $labels, // { mon: 'full_day', ..., sun: 'non_working' }
+ 'working_days' => $working_days, // [ 'mon', ..., 'fri' ]
+ 'weekends' => $weekends, // [ 'sat', 'sun' ]
+ ]
+ );
+ }
+
public function get_genders() {
return rest_ensure_response( erp_hr_get_genders() );
}
--- a/erp/modules/hrm/includes/API/EmployeesController.php
+++ b/erp/modules/hrm/includes/API/EmployeesController.php
@@ -1267,10 +1267,18 @@
return new WP_Error( 'rest_invalid_financial_year', __( 'No financial year defined for current year.', 'erp' ), [ 'status' => 404 ] );
}
+ // Default to all leave statuses: 1 = Approved, 2 = Pending, 3 = Rejected.
+ // Allow callers to filter by passing a `status` query param (single value or comma-separated list).
+ $status = [ 1, 2, 3 ];
+
+ if ( ! empty( $request['status'] ) ) {
+ $status = array_filter( array_map( 'absint', wp_parse_list( $request['status'] ) ) );
+ }
+
$args = [
'user_id' => $user_id,
'f_year' => $f_year->id,
- 'status' => 1,
+ 'status' => $status,
'orderby' => 'created_at',
'policy_id' => 0,
'number' => -1,
@@ -1278,6 +1286,29 @@
];
$leaves = erp_hr_get_leave_requests( $args );
+ if ( ! empty( $leaves['data'] ) ) {
+ foreach ( $leaves['data'] as $leave ) {
+ $attachments = [];
+ $leave_attachment = get_user_meta( $leave->user_id, 'leave_document_' . $leave->id );
+
+ if ( ! empty( $leave_attachment ) ) {
+ foreach ( $leave_attachment as $attachment_id ) {
+ $file_url = wp_get_attachment_url( $attachment_id );
+
+ if ( $file_url ) {
+ $attachments[] = [
+ 'id' => (int) $attachment_id,
+ 'url' => esc_url_raw( $file_url ),
+ 'name' => basename( $file_url ),
+ ];
+ }
+ }
+ }
+
+ $leave->attachments = $attachments;
+ }
+ }
+
$response = rest_ensure_response( $leaves['data'] );
$response = $this->format_collection_response( $response, $request, $leaves['total'] );
--- a/erp/modules/hrm/includes/API/LeaveRequestsController.php
+++ b/erp/modules/hrm/includes/API/LeaveRequestsController.php
@@ -4,6 +4,7 @@
use WeDevsERPAPIREST_Controller;
use WeDevsERPHRMEmployee;
+use WeDevsERPHRMModelsLeaveEntitlement;
use WP_Error;
use WP_REST_Response;
use WP_REST_Server;
@@ -182,6 +183,149 @@
},
],
]);
+
+ register_rest_route( $this->namespace, '/' . $this->rest_base . '/calculate-days', [
+ [
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => [ $this, 'calculate_leave_days' ],
+ 'args' => [
+ 'employee_id' => [
+ 'description' => __( 'Employee (user) id. Defaults to the current user; only leave managers may query others.', 'erp' ),
+ 'type' => 'integer',
+ 'required' => false,
+ 'sanitize_callback' => 'absint',
+ ],
+ 'type' => [
+ 'description' => __( 'Leave entitlement (policy) id.', 'erp' ),
+ 'type' => 'integer',
+ 'required' => true,
+ 'sanitize_callback' => 'absint',
+ ],
+ 'from' => [
+ 'description' => __( 'Start date (Y-m-d).', 'erp' ),
+ 'type' => 'string',
+ 'required' => true,
+ 'sanitize_callback' => 'sanitize_text_field',
+ ],
+ 'to' => [
+ 'description' => __( 'End date (Y-m-d).', 'erp' ),
+ 'type' => 'string',
+ 'required' => true,
+ 'sanitize_callback' => 'sanitize_text_field',
+ ],
+ ],
+ 'permission_callback' => function ( $request ) {
+ $employee_id = absint( $request['employee_id'] );
+
+ if ( $employee_id <= 0 ) {
+ $employee_id = get_current_user_id();
+ }
+
+ // erp_leave_create_request maps to a self-or-HR-manager meta cap
+ // (see erp_hr_map_meta_caps): the employee passes for themselves,
+ // an HR manager passes for anyone.
+ return current_user_can( 'erp_leave_create_request', $employee_id );
+ },
+ ],
+ ]);
+ }
+
+ /**
+ * Calculate the work-day breakdown for a prospective leave request.
+ *
+ * Mirrors the admin-ajax `erp-hr-leave-request-req-date` handler so clients
+ * (e.g. the mobile app) can preview the day-by-day breakdown, with weekends,
+ * holidays and the sandwich rule applied, before submitting a leave request.
+ *
+ * The employee defaults to the current user. An `employee_id` override is
+ * honoured only for users who can manage leave (`erp_leave_manage`), so an
+ * ordinary employee cannot inspect another employee's leave duration.
+ *
+ * @param WP_REST_Request $request
+ *
+ * @return WP_Error|WP_REST_Response
+ */
+ public function calculate_leave_days( $request ) {
+ $current = get_current_user_id();
+ $employee_id = absint( $request['employee_id'] );
+
+ if ( $employee_id <= 0 || $employee_id === $current ) {
+ $employee_id = $current;
+ } elseif ( ! current_user_can( 'erp_leave_manage' ) ) {
+ return new WP_Error( 'rest_forbidden_employee', __( 'You are not allowed to view leave duration for another employee.', 'erp' ), [ 'status' => 403 ] );
+ }
+
+ $policy_id = absint( $request['type'] );
+ $start_date = sanitize_text_field( $request['from'] );
+ $end_date = sanitize_text_field( $request['to'] );
+
+ if ( $start_date > $end_date ) {
+ return new WP_Error( 'rest_invalid_range', __( 'Invalid date range', 'erp' ), [ 'status' => 422 ] );
+ }
+
+ $entitlement = LeaveEntitlement::find( $policy_id );
+
+ if ( ! $entitlement ) {
+ return new WP_Error( 'rest_invalid_policy', __( 'Invalid leave policy.', 'erp' ), [ 'status' => 422 ] );
+ }
+
+ $f_year_start = erp_current_datetime()->setTimestamp( $entitlement->financial_year->start_date )->format( 'Y-m-d' );
+ $f_year_end = erp_current_datetime()->setTimestamp( $entitlement->financial_year->end_date )->format( 'Y-m-d' );
+
+ if ( ( $start_date < $f_year_start || $start_date > $f_year_end ) || ( $end_date < $f_year_start || $end_date > $f_year_end ) ) {
+ return new WP_Error(
+ 'rest_invalid_duration',
+ sprintf(
+ /* translators: 1: financial year start, 2: financial year end */
+ __( 'Invalid leave duration. Please apply between %1$s and %2$s.', 'erp' ),
+ erp_format_date( $f_year_start ),
+ erp_format_date( $f_year_end )
+ ),
+ [ 'status' => 422 ]
+ );
+ }
+
+ $leave_record_exist = erp_hrm_is_leave_recored_exist_between_date( $start_date, $end_date, $employee_id, $entitlement->f_year );
+
+ if ( $leave_record_exist ) {
+ return new WP_Error( 'rest_leave_exists', __( 'Existing Leave Record found within selected range!', 'erp' ), [ 'status' => 422 ] );
+ }
+
+ $is_extra_leave_enabled = get_option( 'enable_extra_leave', 'no' );
+
+ if ( 'yes' !== $is_extra_leave_enabled ) {
+ $is_policy_valid = erp_hrm_is_valid_leave_duration( $start_date, $end_date, $policy_id, $employee_id );
+
+ if ( ! $is_policy_valid ) {
+ return new WP_Error( 'rest_no_leave_left', __( 'Sorry! You do not have any leave left under this leave policy', 'erp' ), [ 'status' => 422 ] );
+ }
+ }
+
+ $days = erp_hr_get_work_days_between_dates( $start_date, $end_date, $employee_id );
+
+ if ( is_wp_error( $days ) ) {
+ return $days;
+ }
+
+ // Human-readable date labels, matching the admin-ajax response.
+ foreach ( $days['days'] as &$date ) {
+ $date['date'] = erp_format_date( $date['date'], 'D, M d' );
+ }
+ unset( $date );
+
+ $leave_count = $days['total'];
+ $days['total'] = sprintf( '%d %s', $days['total'], _n( 'day', 'days', $days['total'], 'erp' ) );
+
+ if ( 1 === intval( $days['sandwich'] ) ) {
+ $days['total'] .= ' ' . __( '(Sandwich rule applied)', 'erp' );
+ }
+
+ return rest_ensure_response(
+ [
+ 'print' => $days,
+ 'leave_count' => $leave_count,
+ ]
+ );
}
/**
@@ -663,7 +807,23 @@
*/
public function prepare_item_for_response( $item, $request, $additional_fields = [] ) {
$employee = new Employee( $item->user_id );
-error_log(print_r( [$item], true ));
+
+ $attachments = [];
+ $leave_attachment = get_user_meta( $item->user_id, 'leave_document_' . $item->id );
+
+ if ( ! empty( $leave_attachment ) ) {
+ foreach ( $leave_attachment as $attachment_id ) {
+ $file_url = wp_get_attachment_url( $attachment_id );
+
+ if ( $file_url ) {
+ $attachments[] = [
+ 'id' => (int) $attachment_id,
+ 'url' => esc_url_raw( $file_url ),
+ 'name' => basename( $file_url ),
+ ];
+ }
+ }
+ }
$data = [
'id' => (int) $item->id,
@@ -675,6 +835,7 @@
'start_date' => erp_format_date( $item->start_date, 'Y-m-d' ),
'end_date' => erp_format_date( $item->end_date, 'Y-m-d' ),
'reason' => $item->reason,
+ 'attachments' => $attachments,
'comments' => isset( $item->comments ) ? $item->comments : '',
'applied_on' => erp_format_date( $item->created_at, 'Y-m-d H:i:s' ),
'policy_name' => isset( $item->policy_name ) ? $item->policy_name : '',
@@ -763,6 +924,12 @@
'sanitize_callback' => 'sanitize_text_field',
],
],
+ 'attachments' => [
+ 'description' => __( 'Attachments uploaded with the leave request.', 'erp' ),
+ 'type' => 'array',
+ 'context' => [ 'view', 'edit' ],
+ 'readonly' => true,
+ ],
],
];
--- a/erp/vendor/autoload.php
+++ b/erp/vendor/autoload.php
@@ -2,6 +2,21 @@
// autoload.php @generated by Composer
+if (PHP_VERSION_ID < 50600) {
+ if (!headers_sent()) {
+ header('HTTP/1.1 500 Internal Server Error');
+ }
+ $err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
+ if (!ini_get('display_errors')) {
+ if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
+ fwrite(STDERR, $err);
+ } elseif (!headers_sent()) {
+ echo $err;
+ }
+ }
+ throw new RuntimeException($err);
+}
+
require_once __DIR__ . '/composer/autoload_real.php';
-return ComposerAutoloaderInit7b8b524b8d09b2b0b4cf2b7e5be5dbb1::getLoader();
+return ComposerAutoloaderInit8c832cdc6459f9d95a738614bf88dd0b::getLoader();
--- a/erp/vendor/composer/ClassLoader.php
+++ b/erp/vendor/composer/ClassLoader.php
@@ -42,35 +42,37 @@
*/
class ClassLoader
{
- /** @var ?string */
+ /** @var Closure(string):void */
+ private static $includeFile;
+
+ /** @var string|null */
private $vendorDir;
// PSR-4
/**
- * @var array[]
- * @psalm-var array<string, array<string, int>>
+ * @var array<string, array<string, int>>
*/
private $prefixLengthsPsr4 = array();
/**
- * @var array[]
- * @psalm-var array<string, array<int, string>>
+ * @var array<string, list<string>>
*/
private $prefixDirsPsr4 = array();
/**
- * @var array[]
- * @psalm-var array<string, string>
+ * @var list<string>
*/
private $fallbackDirsPsr4 = array();
// PSR-0
/**
- * @var array[]
- * @psalm-var array<string, array<string, string[]>>
+ * List of PSR-0 prefixes
+ *
+ * Structured as array('F (first letter)' => array('FooBar (full prefix)' => array('path', 'path2')))
+ *
+ * @var array<string, array<string, list<string>>>
*/
private $prefixesPsr0 = array();
/**
- * @var array[]
- * @psalm-var array<string, string>
+ * @var list<string>
*/
private $fallbackDirsPsr0 = array();
@@ -78,8 +80,7 @@
private $useIncludePath = false;
/**
- * @var string[]
- * @psalm-var array<string, string>
+ * @var array<string, string>
*/
private $classMap = array();
@@ -87,29 +88,29 @@
private $classMapAuthoritative = false;
/**
- * @var bool[]
- * @psalm-var array<string, bool>
+ * @var array<string, bool>
*/
private $missingClasses = array();
- /** @var ?string */
+ /** @var string|null */
private $apcuPrefix;
/**
- * @var self[]
+ * @var array<string, self>
*/
private static $registeredLoaders = array();
/**
- * @param ?string $vendorDir
+ * @param string|null $vendorDir
*/
public function __construct($vendorDir = null)
{
$this->vendorDir = $vendorDir;
+ self::initializeIncludeClosure();
}
/**
- * @return string[]
+ * @return array<string, list<string>>
*/
public function getPrefixes()
{
@@ -121,8 +122,7 @@
}
/**
- * @return array[]
- * @psalm-return array<string, array<int, string>>
+ * @return array<string, list<string>>
*/
public function getPrefixesPsr4()
{
@@ -130,8 +130,7 @@
}
/**
- * @return array[]
- * @psalm-return array<string, string>
+ * @return list<string>
*/
public function getFallbackDirs()
{
@@ -139,8 +138,7 @@
}
/**
- * @return array[]
- * @psalm-return array<string, string>
+ * @return list<string>
*/
public function getFallbackDirsPsr4()
{
@@ -148,8 +146,7 @@
}
/**
- * @return string[] Array of classname => path
- * @psalm-return array<string, string>
+ * @return array<string, string> Array of classname => path
*/
public function getClassMap()
{
@@ -157,8 +154,7 @@
}
/**
- * @param string[] $classMap Class to filename map
- * @psalm-param array<string, string> $classMap
+ * @param array<string, string> $classMap Class to filename map
*
* @return void
*/
@@ -175,24 +171,25 @@
* Registers a set of PSR-0 directories for a given prefix, either
* appending or prepending to the ones previously set for this prefix.
*
- * @param string $prefix The prefix
- * @param string[]|string $paths The PSR-0 root directories
- * @param bool $prepend Whether to prepend the directories
+ * @param string $prefix The prefix
+ * @param list<string>|string $paths The PSR-0 root directories
+ * @param bool $prepend Whether to prepend the directories
*
* @return void
*/
public function add($prefix, $paths, $prepend = false)
{
+ $paths = (array) $paths;
if (!$prefix) {
if ($prepend) {
$this->fallbackDirsPsr0 = array_merge(
- (array) $paths,
+ $paths,
$this->fallbackDirsPsr0
);
} else {
$this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0,
- (array) $paths
+ $paths
);
}
@@ -201,19 +198,19 @@
$first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) {
- $this->prefixesPsr0[$first][$prefix] = (array) $paths;
+ $this->prefixesPsr0[$first][$prefix] = $paths;
return;
}
if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge(
- (array) $paths,
+ $paths,
$this->prefixesPsr0[$first][$prefix]
);
} else {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix],
- (array) $paths
+ $paths
);
}
}
@@ -222,9 +219,9 @@
* Registers a set of PSR-4 directories for a given namespace, either
* appending or prepending to the ones previously set for this namespace.
*
- * @param string $prefix The prefix/namespace, with trailing '\'
- * @param string[]|string $paths The PSR-4 base directories
- * @param bool $prepend Whether to prepend the directories
+ * @param string $prefix The prefix/namespace, with trailing '\'
+ * @param list<string>|string $paths The PSR-4 base directories
+ * @param bool $prepend Whether to prepend the directories
*
* @throws InvalidArgumentException
*
@@ -232,17 +229,18 @@
*/
public function addPsr4($prefix, $paths, $prepend = false)
{
+ $paths = (array) $paths;
if (!$prefix) {
// Register directories for the root namespace.
if ($prepend) {
$this->fallbackDirsPsr4 = array_merge(
- (array) $paths,
+ $paths,
$this->fallbackDirsPsr4
);
} else {
$this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4,
- (array) $paths
+ $paths
);
}
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
@@ -252,18 +250,18 @@
throw new InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
- $this->prefixDirsPsr4[$prefix] = (array) $paths;
+ $this->prefixDirsPsr4[$prefix] = $paths;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
- (array) $paths,
+ $paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
- (array) $paths
+ $paths
);
}
}
@@ -272,8 +270,8 @@
* Registers a set of PSR-0 directories for a given prefix,
* replacing any others previously set for this prefix.
*
- * @param string $prefix The prefix
- * @param string[]|string $paths The PSR-0 base directories
+ * @param string $prefix The prefix
+ * @param list<string>|string $paths The PSR-0 base directories
*
* @return void
*/
@@ -290,8 +288,8 @@
* Registers a set of PSR-4 directories for a given namespace,
* replacing any others previously set for this namespace.
*
- * @param string $prefix The prefix/namespace, with trailing '\'
- * @param string[]|string $paths The PSR-4 base directories
+ * @param string $prefix The prefix/namespace, with trailing '\'
+ * @param list<string>|string $paths The PSR-4 base directories
*
* @throws InvalidArgumentException
*
@@ -425,7 +423,8 @@
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
- includeFile($file);
+ $includeFile = self::$includeFile;
+ $includeFile($file);
return true;
}
@@ -476,9 +475,9 @@
}
/**
- * Returns the currently registered loaders indexed by their corresponding vendor directories.
+ * Returns the currently registered loaders keyed by their corresponding vendor directories.
*
- * @return self[]
+ * @return array<string, self>
*/
public static function getRegisteredLoaders()
{
@@ -555,18 +554,26 @@
return false;
}
-}
-/**
- * Scope isolated include.
- *
- * Prevents access to $this/self from included files.
- *
- * @param string $file
- * @return void
- * @private
- */
-function includeFile($file)
-{
- include $file;
+ /**
+ * @return void
+ */
+ private static function initializeIncludeClosure()
+ {
+ if (self::$includeFile !== null) {
+ return;
+ }
+
+ /**
+ * Scope isolated include.
+ *
+ * Prevents access to $this/self from included files.
+ *
+ * @param string $file
+ * @return void
+ */
+ self::$includeFile = Closure::bind(static function($file) {
+ include $file;
+ }, null, null);
+ }
}
--- a/erp/vendor/composer/InstalledVersions.php
+++ b/erp/vendor/composer/InstalledVersions.php
@@ -21,23 +21,36 @@
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
*
* To require its presence, you can require `composer-runtime-api ^2.0`
+ *
+ * @final
*/
class InstalledVersions
{
/**
+ * @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to
+ * @internal
+ */
+ private static $selfDir = null;
+
+ /**
* @var mixed[]|null
- * @psalm-var array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}|array{}|null
+ * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
*/
private static $installed;
/**
+ * @var bool
+ */
+ private static $installedIsLocalDir;
+
+ /**
* @var bool|null
*/
private static $canGetVendors;
/**
* @var array[]
- * @psalm-var array<string, array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}>
+ * @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static $installedByVendor = array();
@@ -96,7 +109,7 @@
{
foreach (self::getInstalled() as $installed) {
if (isset($installed['versions'][$packageName])) {
- return $includeDevRequirements || empty($installed['versions'][$packageName]['dev_requirement']);
+ return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
}
}
@@ -117,7 +130,7 @@
*/
public static function satisfies(VersionParser $parser, $packageName, $constraint)
{
- $constraint = $parser->parseConstraints($constraint);
+ $constraint = $parser->parseConstraints((string) $constraint);
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
return $provided->matches($constraint);
@@ -241,7 +254,7 @@
/**
* @return array
- * @psalm-return array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev